Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,37 @@ Q: Why would you use key event remapping?

A: Instead of manually duplicating multiple context-dependent bindings from `Up` to `Ctrl+P` , you can use key event remapping to redirect `Ctrl+p` to `Up` globally.

#### Leader Keys

> [!CAUTION]
> This an experimental feature and might change. Feedback welcome

Flyline supports leader key sequences. A leader key sequence allows you to press a prefix key (like `Ctrl+x`), which activates a temporary leader key state (for up to 1000ms). While that state is active, you can press a subsequent key to trigger a specific binding.

To set up leader key bindings:
```bash
# Bind the prefix key to `setLeaderKey`**:
flyline key bind Ctrl+x always=setLeaderKey

# Ctrl+x then Ctrl+f clears the buffer, inserts "git status", and runs it
flyline key bind Ctrl+f 'leaderKeyActive=clearBuffer+insertString(git status)+submitOrNewline'

# Ctrl+x then g clears buffer, then inserts "git commit -m", and consumes the leader key
flyline key bind g 'leaderKeyActive=clearBuffer+insertString(git commit -m)+unsetLeaderKey'

# Or we could set the leader key again to chain leader key lead actions:
flyline key bind g 'leaderKeyActive=clearBuffer+insertString(git commit -m)+setLeaderKey'
```

To show a visual indicator in your prompt (e.g. `<leader>` or ` X `) when the leader key state is active, register a `leader-mode` prompt widget:
```bash
# This will show "LEADER" when active and nothing inactive
flyline create-prompt-widget leader-mode --name FLYLINE_LEADER_MODE 'LEADER' ''

# And include it in your `PS1`/`RPS1`/`PS1_FILL`:
export RPS1='FLYLINE_LEADER_MODE'
```

# Licensing

This project is multi-licensed:
Expand Down
22 changes: 22 additions & 0 deletions src/app/actions/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ pub enum KeyEventAction {
PromptDirMoveToEnd,
#[strum(message = "Return to the normal command editing mode")]
EscapeToNormalMode,
#[strum(message = "Activate the leader key state")]
SetLeaderKey,
#[strum(message = "Deactivate the leader key state")]
UnsetLeaderKey,
#[strum(message = "Insert a literal string of characters", disabled)]
InsertString(String),
}
Expand Down Expand Up @@ -908,6 +912,12 @@ impl KeyEventAction {
app.buffer.clear_selection();
app.content_mode = ContentMode::Normal;
}
KeyEventAction::SetLeaderKey => {
app.leader_key_active_at = Some(std::time::Instant::now());
}
KeyEventAction::UnsetLeaderKey => {
app.leader_key_active_at = None;
}
KeyEventAction::InsertString(s) => {
app.buffer.delete_selection();
app.buffer.insert_str(s);
Expand Down Expand Up @@ -3113,6 +3123,7 @@ pub fn print_bindings_table(
impl<'a> App<'a> {
pub fn handle_key_event(&mut self, key: KeyEvent) {
let _timer = crate::perf::PerfTimer::start("handle_key_event");
let initial_leader_time = self.leader_key_active_at;
log::trace!("Key event: {:?}", key);
self.right_click_popup_pos = None;
self.right_click_copy_target = None;
Expand Down Expand Up @@ -3180,6 +3191,12 @@ impl<'a> App<'a> {
self.mouse_state.enable();
}

// If the leader key was active before this key event, and was not refreshed or updated
// by a SetLeaderKey action during this key event, deactivate it now.
if initial_leader_time.is_some() && self.leader_key_active_at == initial_leader_time {
self.leader_key_active_at = None;
}

self.on_possible_buffer_change();
}
}
Expand Down Expand Up @@ -4097,6 +4114,8 @@ pub(crate) enum ContextVar {
FuzzyHistorySearchNoneSelected,
#[strum(message = "Agent output selection is active and no suggestion is currently selected")]
AgentOutputNoneSelected,
#[strum(message = "The leader key is currently active")]
LeaderKeyActive,
}

impl ContextVar {
Expand Down Expand Up @@ -4229,6 +4248,9 @@ impl ContextVar {
false
}
}
ContextVar::LeaderKeyActive => app.leader_key_active_at.map_or(false, |t| {
t.elapsed() < std::time::Duration::from_millis(1000)
}),
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ pub(crate) struct App<'a> {
pub(super) right_click_copy_target: Option<RightClickCopyTarget>,
/// Timestamp of the last keypress or mouse event; used for idle-based matrix animation.
pub(super) last_activity_time: std::time::Instant,
pub(super) leader_key_active_at: Option<std::time::Instant>,
}

impl<'a> App<'a> {
Expand Down Expand Up @@ -472,6 +473,7 @@ impl<'a> App<'a> {
right_click_popup_pos: None,
right_click_copy_target: None,
last_activity_time: std::time::Instant::now(),
leader_key_active_at: None,
};

app.on_possible_buffer_change();
Expand Down Expand Up @@ -583,6 +585,13 @@ impl<'a> App<'a> {
redraw = true;
}

if self.leader_key_active_at.map_or(false, |t| {
t.elapsed() >= std::time::Duration::from_millis(1000)
}) {
self.leader_key_active_at = None;
redraw = true;
}

if redraw {
let frame_area = terminal.get_frame().area();

Expand Down
5 changes: 5 additions & 0 deletions src/app/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,9 +554,14 @@ impl<'a> App<'a> {

content.prompt_start = Some(content.cursor_position());

let leader_active = self.leader_key_active_at.map_or(false, |t| {
t.elapsed() < std::time::Duration::from_millis(1000)
});

let (mut lprompt, rprompt, fill_span) = self.prompt_manager.get_ps1_lines(
self.settings.show_animations,
self.mouse_state.is_enabled(),
leader_active,
self.mode.is_running(),
);

Expand Down
38 changes: 38 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,24 @@ enum PromptWidgetSubcommands {
#[arg(long, default_value = "FLYLINE_LAST_COMMAND_DURATION")]
name: String,
},
/// Show different text depending on whether the leader key is active.
///
/// Instances of NAME in prompt strings are replaced with ACTIVE_TEXT when
/// the leader key is active, and INACTIVE_TEXT when not active.
///
/// Examples:
/// flyline create-prompt-widget leader-mode ' X ' ''
#[command(name = "leader-mode", verbatim_doc_comment)]
LeaderMode {
/// Name to embed in prompt strings as the widget placeholder.
/// Defaults to `FLYLINE_LEADER_MODE`.
#[arg(long, default_value = "FLYLINE_LEADER_MODE")]
name: String,
/// Text to display when the leader key is active.
active_text: String,
/// Text to display when the leader key is inactive.
inactive_text: String,
},
}
impl Flyline {
pub(crate) fn call(&mut self, words: *const bash_symbols::WordList) -> c_int {
Expand Down Expand Up @@ -1136,6 +1154,26 @@ impl Flyline {
settings::PromptWidget::LastCommandDuration { name },
);
}
PromptWidgetSubcommands::LeaderMode {
name,
active_text,
inactive_text,
} => {
log::info!(
"Registering leader-mode widget '{}' (active={:?}, inactive={:?})",
name,
active_text,
inactive_text
);
self.settings.custom_prompt_widgets.insert(
name.clone(),
settings::PromptWidget::LeaderMode {
name,
active_text,
inactive_text,
},
);
}
},
Some(Commands::SetColour {
default_theme,
Expand Down
Loading