feat: Add support for action modifier keys like ctrl,shift,alt.
This commit is contained in:
@@ -14,7 +14,7 @@ use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{List, ListItem, Paragraph};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
use pikl_core::event::{Action, MenuEvent, Mode, ViewState};
|
||||
use pikl_core::event::{Action, MenuEvent, ModifiedAction, Modifiers, Mode, ViewState};
|
||||
|
||||
/// Pending key state for multi-key sequences (e.g. `gg`).
|
||||
/// TUI-local, not part of core state.
|
||||
@@ -32,12 +32,21 @@ pub fn restore_terminal() {
|
||||
let _ = crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen);
|
||||
}
|
||||
|
||||
/// Convert crossterm key modifiers to pikl Modifiers.
|
||||
fn crossterm_modifiers(km: KeyModifiers) -> Modifiers {
|
||||
Modifiers {
|
||||
shift: km.contains(KeyModifiers::SHIFT),
|
||||
ctrl: km.contains(KeyModifiers::CONTROL),
|
||||
alt: km.contains(KeyModifiers::ALT),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the TUI. Enters the alternate screen, runs the
|
||||
/// event loop, and restores the terminal on exit. Translates
|
||||
/// crossterm key events into [`Action`]s and renders
|
||||
/// [`ViewState`] snapshots.
|
||||
pub async fn run(
|
||||
action_tx: mpsc::Sender<Action>,
|
||||
action_tx: mpsc::Sender<ModifiedAction>,
|
||||
mut event_rx: broadcast::Receiver<MenuEvent>,
|
||||
filter_history: Option<Vec<String>>,
|
||||
) -> std::io::Result<()> {
|
||||
@@ -65,7 +74,7 @@ pub async fn run(
|
||||
/// Inner event loop. Separated from [`run`] so terminal
|
||||
/// cleanup always happens even if this returns an error.
|
||||
async fn run_inner(
|
||||
action_tx: &mpsc::Sender<Action>,
|
||||
action_tx: &mpsc::Sender<ModifiedAction>,
|
||||
event_rx: &mut broadcast::Receiver<MenuEvent>,
|
||||
terminal: &mut Terminal<CrosstermBackend<std::io::Stderr>>,
|
||||
filter_history: Option<Vec<String>>,
|
||||
@@ -74,9 +83,9 @@ async fn run_inner(
|
||||
let size = terminal.size()?;
|
||||
let list_height = size.height.saturating_sub(1);
|
||||
if action_tx
|
||||
.send(Action::Resize {
|
||||
.send(ModifiedAction::new(Action::Resize {
|
||||
height: list_height,
|
||||
})
|
||||
}))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@@ -135,7 +144,7 @@ async fn run_inner(
|
||||
history_cursor = Some(new_cursor);
|
||||
filter_text.clone_from(&history[new_cursor]);
|
||||
let _ = action_tx
|
||||
.send(Action::UpdateFilter(filter_text.clone()))
|
||||
.send(ModifiedAction::new(Action::UpdateFilter(filter_text.clone())))
|
||||
.await;
|
||||
true
|
||||
}
|
||||
@@ -153,7 +162,7 @@ async fn run_inner(
|
||||
pre_history_text = None;
|
||||
}
|
||||
let _ = action_tx
|
||||
.send(Action::UpdateFilter(filter_text.clone()))
|
||||
.send(ModifiedAction::new(Action::UpdateFilter(filter_text.clone())))
|
||||
.await;
|
||||
true
|
||||
} else {
|
||||
@@ -196,14 +205,14 @@ async fn run_inner(
|
||||
mode = *m;
|
||||
pending = PendingKey::None;
|
||||
}
|
||||
if action_tx.send(action).await.is_err() {
|
||||
if action_tx.send(ModifiedAction::with_modifiers(action, crossterm_modifiers(key.modifiers))).await.is_err() {
|
||||
break; // core is gone
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Resize(_, h) => {
|
||||
let list_height = h.saturating_sub(1);
|
||||
if action_tx.send(Action::Resize { height: list_height }).await.is_err() {
|
||||
if action_tx.send(ModifiedAction::new(Action::Resize { height: list_height })).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -259,7 +268,7 @@ async fn handle_multi_action_key(
|
||||
multi_enabled: bool,
|
||||
visual_anchor: &mut Option<usize>,
|
||||
view_state: &Option<ViewState>,
|
||||
action_tx: &mpsc::Sender<Action>,
|
||||
action_tx: &mpsc::Sender<ModifiedAction>,
|
||||
) -> bool {
|
||||
match mode {
|
||||
Mode::Insert => {
|
||||
@@ -268,13 +277,15 @@ async fn handle_multi_action_key(
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Tab => {
|
||||
let _ = action_tx.send(Action::ToggleSelect).await;
|
||||
let _ = action_tx.send(Action::MoveDown(1)).await;
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
|
||||
true
|
||||
}
|
||||
KeyCode::BackTab => {
|
||||
let _ = action_tx.send(Action::ToggleSelect).await;
|
||||
let _ = action_tx.send(Action::MoveUp(1)).await;
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::MoveUp(1), mods)).await;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -288,18 +299,21 @@ async fn handle_multi_action_key(
|
||||
(KeyCode::Char(' '), m)
|
||||
if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
|
||||
{
|
||||
let _ = action_tx.send(Action::ToggleSelect).await;
|
||||
let _ = action_tx.send(Action::MoveDown(1)).await;
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
|
||||
true
|
||||
}
|
||||
(KeyCode::Tab, _) => {
|
||||
let _ = action_tx.send(Action::ToggleSelect).await;
|
||||
let _ = action_tx.send(Action::MoveDown(1)).await;
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::MoveDown(1), mods)).await;
|
||||
true
|
||||
}
|
||||
(KeyCode::BackTab, _) => {
|
||||
let _ = action_tx.send(Action::ToggleSelect).await;
|
||||
let _ = action_tx.send(Action::MoveUp(1)).await;
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::ToggleSelect, mods)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::MoveUp(1), mods)).await;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -310,18 +324,19 @@ async fn handle_multi_action_key(
|
||||
(KeyCode::Char(' '), m) | (KeyCode::Enter, m)
|
||||
if !m.intersects(KeyModifiers::ALT) =>
|
||||
{
|
||||
let mods = crossterm_modifiers(key.modifiers);
|
||||
// Apply visual selection
|
||||
if let (Some(anchor), Some(vs)) = (*visual_anchor, view_state) {
|
||||
let cursor = vs.cursor;
|
||||
let _ = action_tx
|
||||
.send(Action::SelectRange {
|
||||
.send(ModifiedAction::with_modifiers(Action::SelectRange {
|
||||
start: anchor,
|
||||
end: cursor,
|
||||
})
|
||||
}, mods))
|
||||
.await;
|
||||
}
|
||||
*visual_anchor = None;
|
||||
let _ = action_tx.send(Action::SetMode(Mode::Normal)).await;
|
||||
let _ = action_tx.send(ModifiedAction::with_modifiers(Action::SetMode(Mode::Normal), mods)).await;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
|
||||
Reference in New Issue
Block a user