feat: Add support for action modifier keys like ctrl,shift,alt.
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled

This commit is contained in:
2026-03-15 11:51:17 -04:00
parent 0d241841bc
commit 47437f536b
18 changed files with 1055 additions and 244 deletions

View File

@@ -15,7 +15,9 @@ use iced_layershell::settings::{LayerShellSettings, Settings};
use iced_layershell::to_layer_message;
use tokio::sync::{broadcast, mpsc};
use pikl_core::event::{Action, MenuEvent, Mode, ViewState, VisibleItem};
use pikl_core::event::{
Action, MenuEvent, ModifiedAction, Modifiers as PiklModifiers, Mode, ViewState, VisibleItem,
};
/// Number of visible items in the list viewport.
const VIEWPORT_HEIGHT: u16 = 20;
@@ -52,12 +54,12 @@ static BRIDGE_RX: OnceLock<Mutex<Option<mpsc::Receiver<Message>>>> = OnceLock::n
/// Global slot for the action sender. Moved into the Pikl
/// struct during boot (called exactly once by iced).
static ACTION_TX: OnceLock<Mutex<Option<mpsc::Sender<Action>>>> = OnceLock::new();
static ACTION_TX: OnceLock<Mutex<Option<mpsc::Sender<ModifiedAction>>>> = OnceLock::new();
/// Top-level GUI state.
struct Pikl {
/// Channel to send actions into the core engine.
action_tx: mpsc::Sender<Action>,
action_tx: mpsc::Sender<ModifiedAction>,
/// Current view snapshot from core.
view_state: Option<ViewState>,
/// Local copy of filter text for the text input widget.
@@ -82,13 +84,13 @@ struct Pikl {
/// runs the iced event loop, and exits when the user
/// confirms or cancels.
pub fn run(
action_tx: mpsc::Sender<Action>,
action_tx: mpsc::Sender<ModifiedAction>,
event_rx: broadcast::Receiver<MenuEvent>,
) -> Result<(), iced_layershell::Error> {
// Send initial resize to core so it knows our viewport.
let _ = action_tx.blocking_send(Action::Resize {
let _ = action_tx.blocking_send(ModifiedAction::new(Action::Resize {
height: VIEWPORT_HEIGHT,
});
}));
// Bridge: spawn a thread that reads core events and
// forwards them as Messages through an mpsc channel.
@@ -109,7 +111,7 @@ pub fn run(
.get()
.and_then(|m| m.lock().unwrap_or_else(|e| e.into_inner()).take())
.unwrap_or_else(|| {
let (tx, _) = mpsc::channel(1);
let (tx, _) = mpsc::channel::<ModifiedAction>(1);
tx
});
let pikl = Pikl {
@@ -228,7 +230,7 @@ fn update(state: &mut Pikl, message: Message) -> Task<Message> {
let tx = state.action_tx.clone();
Task::perform(
async move {
let _ = tx.send(Action::UpdateFilter(new_text)).await;
let _ = tx.send(ModifiedAction::new(Action::UpdateFilter(new_text))).await;
},
|_| Message::Sent,
)
@@ -415,6 +417,15 @@ fn core_event_stream() -> impl iced::futures::Stream<Item = Message> {
})
}
/// Convert iced keyboard modifiers to pikl Modifiers.
fn iced_modifiers(m: &iced::keyboard::Modifiers) -> PiklModifiers {
PiklModifiers {
shift: m.shift(),
ctrl: m.control(),
alt: m.alt(),
}
}
/// Handle a keyboard event and return the appropriate task.
fn handle_key(state: &mut Pikl, key: Key, modifiers: Modifiers) -> Task<Message> {
let action = match state.mode {
@@ -443,9 +454,10 @@ fn handle_key(state: &mut Pikl, key: Key, modifiers: Modifiers) -> Task<Message>
};
let tx = state.action_tx.clone();
let mods = iced_modifiers(&modifiers);
let send_task = Task::perform(
async move {
let _ = tx.send(action).await;
let _ = tx.send(ModifiedAction::with_modifiers(action, mods)).await;
},
|_| Message::Sent,
);
@@ -470,9 +482,10 @@ fn map_insert_key(state: &mut Pikl, key: &Key, modifiers: &Modifiers) -> Option<
Key::Named(Named::PageDown) => Some(Action::PageDown(1)),
Key::Named(Named::Tab) if state.multi_enabled => {
let tx = state.action_tx.clone();
let mods = iced_modifiers(modifiers);
tokio::spawn(async move {
let _ = tx.send(Action::ToggleSelect).await;
let _ = tx.send(Action::MoveDown(1)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
});
None
}
@@ -515,9 +528,10 @@ fn map_normal_key(state: &mut Pikl, key: &Key, modifiers: &Modifiers) -> Option<
"u" => Some(Action::UndoSelection),
" " if state.multi_enabled => {
let tx = state.action_tx.clone();
let mods = iced_modifiers(modifiers);
tokio::spawn(async move {
let _ = tx.send(Action::ToggleSelect).await;
let _ = tx.send(Action::MoveDown(1)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
});
None
}
@@ -539,9 +553,10 @@ fn map_normal_key(state: &mut Pikl, key: &Key, modifiers: &Modifiers) -> Option<
Key::Named(Named::ArrowDown) => Some(Action::MoveDown(1)),
Key::Named(Named::Tab) if state.multi_enabled => {
let tx = state.action_tx.clone();
let mods = iced_modifiers(modifiers);
tokio::spawn(async move {
let _ = tx.send(Action::ToggleSelect).await;
let _ = tx.send(Action::MoveDown(1)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
});
None
}
@@ -580,11 +595,12 @@ fn map_visual_key(state: &mut Pikl, key: &Key, modifiers: &Modifiers) -> Option<
let tx = state.action_tx.clone();
let start = anchor;
let end = vs.cursor;
let mods = iced_modifiers(modifiers);
tokio::spawn(async move {
let _ = tx
.send(Action::SelectRange { start, end })
.send(ModifiedAction::with_modifiers(Action::SelectRange { start, end }, mods))
.await;
let _ = tx.send(Action::SetMode(Mode::Normal)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::SetMode(Mode::Normal), mods)).await;
});
}
state.visual_anchor = None;
@@ -599,11 +615,12 @@ fn map_visual_key(state: &mut Pikl, key: &Key, modifiers: &Modifiers) -> Option<
let tx = state.action_tx.clone();
let start = anchor;
let end = vs.cursor;
let mods = iced_modifiers(modifiers);
tokio::spawn(async move {
let _ = tx
.send(Action::SelectRange { start, end })
.send(ModifiedAction::with_modifiers(Action::SelectRange { start, end }, mods))
.await;
let _ = tx.send(Action::SetMode(Mode::Normal)).await;
let _ = tx.send(ModifiedAction::with_modifiers(Action::SetMode(Mode::Normal), mods)).await;
});
}
state.visual_anchor = None;