diff --git a/alacritty.yml b/alacritty.yml index d049e665..3df946a9 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -478,9 +478,19 @@ # List with all available hints # + # Each hint takes a `regex`, `binding` and either a `command` or an `action`. + # # The fields `command`, `binding.key` and `binding.mods` accept the same # values as they do in the `key_bindings` section. # + # Values for `action`: + # - Copy + # Copy the hint's text to the clipboard. + # - Paste + # Paste the hint's text to the terminal or search. + # - Select + # Select the hint's text. + # # Example # # enabled: diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 6d06aa7d..3cd2ad88 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use log::error; use serde::de::Error as SerdeError; -use serde::{Deserialize, Deserializer}; +use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; use alacritty_config_derive::ConfigDeserialize; @@ -245,11 +245,35 @@ impl<'de> Deserialize<'de> for HintsAlphabet { } } +/// Built-in actions for hint mode. +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum HintInternalAction { + /// Copy the text to the clipboard. + Copy, + /// Write the text to the PTY/search. + Paste, + /// Select the text matching the hint. + Select, +} + +/// Actions for hint bindings. +#[derive(Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum HintAction { + /// Built-in hint action. + #[serde(rename = "action")] + Action(HintInternalAction), + + /// Command the text will be piped to. + #[serde(rename = "command")] + Command(Program), +} + /// Hint configuration. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Hint { - /// Command the text will be piped to. - pub command: Program, + /// Action executed when this hint is triggered. + #[serde(flatten)] + pub action: HintAction, /// Regex for finding matches. pub regex: LazyRegex, diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs index fe107139..2a5e9c65 100644 --- a/alacritty/src/display/hint.rs +++ b/alacritty/src/display/hint.rs @@ -1,7 +1,7 @@ +use alacritty_terminal::term::search::Match; use alacritty_terminal::term::Term; -use crate::config::ui_config::Hint; -use crate::daemon::start_daemon; +use crate::config::ui_config::{Hint, HintAction}; use crate::display::content::RegexMatches; /// Percentage of characters in the hints alphabet used for the last character. @@ -88,7 +88,7 @@ impl HintState { } /// Handle keyboard input during hint selection. - pub fn keyboard_input(&mut self, term: &Term, c: char) { + pub fn keyboard_input(&mut self, term: &Term, c: char) -> Option { match c { // Use backspace to remove the last character pressed. '\x08' | '\x1f' => { @@ -102,34 +102,25 @@ impl HintState { // Update the visible matches. self.update_matches(term); - let hint = match self.hint.as_ref() { - Some(hint) => hint, - None => return, - }; + let hint = self.hint.as_ref()?; // Find the last label starting with the input character. let mut labels = self.labels.iter().enumerate().rev(); - let (index, label) = match labels.find(|(_, label)| !label.is_empty() && label[0] == c) { - Some(last) => last, - None => return, - }; + let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?; // Check if the selected label is fully matched. if label.len() == 1 { - // Get text for the hint's regex match. - let hint_match = &self.matches[index]; - let text = term.bounds_to_string(*hint_match.start(), *hint_match.end()); - - // Append text as last argument and launch command. - let program = hint.command.program(); - let mut args = hint.command.args().to_vec(); - args.push(text); - start_daemon(program, &args); + let bounds = self.matches[index].clone(); + let action = hint.action.clone(); self.stop(); + + Some(HintMatch { action, bounds }) } else { // Store character to preserve the selection. self.keys.push(c); + + None } } @@ -152,6 +143,15 @@ impl HintState { } } +/// Hint match which was selected by the user. +pub struct HintMatch { + /// Action for handling the text. + pub action: HintAction, + + /// Terminal range matching the hint. + pub bounds: Match, +} + /// Generator for creating new hint labels. struct HintLabels { /// Full character set available. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 94c40a39..341f398a 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -41,9 +41,10 @@ use alacritty_terminal::tty; use crate::cli::Options as CLIOptions; use crate::clipboard::Clipboard; +use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::{self, Config}; use crate::daemon::start_daemon; -use crate::display::hint::HintState; +use crate::display::hint::{HintMatch, HintState}; use crate::display::window::Window; use crate::display::{Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; @@ -178,7 +179,7 @@ pub struct ActionContext<'a, N, T> { impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionContext<'a, N, T> { #[inline] - fn write_to_pty>>(&mut self, val: B) { + fn write_to_pty>>(&self, val: B) { self.notifier.notify(val); } @@ -221,12 +222,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon *self.dirty = true; } + // Copy text selection. fn copy_selection(&mut self, ty: ClipboardType) { - if let Some(selected) = self.terminal.selection_to_string() { - if !selected.is_empty() { - self.clipboard.store(ty, selected); - } + let text = match self.terminal.selection_to_string().filter(|s| !s.is_empty()) { + Some(text) => text, + None => return, + }; + + if ty == ClipboardType::Selection && self.config.selection.save_to_clipboard { + self.clipboard.store(ClipboardType::Clipboard, text.clone()); } + self.clipboard.store(ty, text); } fn selection_is_empty(&self) -> bool { @@ -258,11 +264,15 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.terminal.selection = Some(selection); *self.dirty = true; + + self.copy_selection(ClipboardType::Selection); } fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { self.terminal.selection = Some(Selection::new(ty, point, side)); *self.dirty = true; + + self.copy_selection(ClipboardType::Selection); } fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) { @@ -273,6 +283,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon Some(selection) if !selection.is_empty() => { selection.ty = ty; *self.dirty = true; + + self.copy_selection(ClipboardType::Selection); }, _ => self.start_selection(ty, point, side), } @@ -639,8 +651,59 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon /// Process a new character for keyboard hints. fn hint_input(&mut self, c: char) { - self.display.hint_state.keyboard_input(self.terminal, c); + let action = self.display.hint_state.keyboard_input(self.terminal, c); *self.dirty = true; + + let HintMatch { action, bounds } = match action { + Some(action) => action, + None => return, + }; + + match action { + // Launch an external program. + HintAction::Command(command) => { + let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + let mut args = command.args().to_vec(); + args.push(text); + start_daemon(command.program(), &args); + }, + // Copy the text to the clipboard. + HintAction::Action(HintInternalAction::Copy) => { + let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + self.clipboard.store(ClipboardType::Clipboard, text); + }, + // Write the text to the PTY/search. + HintAction::Action(HintInternalAction::Paste) => { + let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); + self.paste(&text); + }, + // Select the text. + HintAction::Action(HintInternalAction::Select) => { + self.start_selection(SelectionType::Simple, *bounds.start(), Side::Left); + self.update_selection(*bounds.end(), Side::Right); + }, + } + } + + /// Paste a text into the terminal. + fn paste(&mut self, text: &str) { + if self.search_active() { + for c in text.chars() { + self.search_input(c); + } + } else if self.terminal().mode().contains(TermMode::BRACKETED_PASTE) { + self.write_to_pty(&b"\x1b[200~"[..]); + self.write_to_pty(text.replace("\x1b", "").into_bytes()); + self.write_to_pty(&b"\x1b[201~"[..]); + } else { + // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish + // pasted data from keystrokes. + // In theory, we should construct the keystrokes needed to produce the data we are + // pasting... since that's neither practical nor sensible (and probably an impossible + // task to solve in a general way), we'll just replace line breaks (windows and unix + // style) with a single carriage return (\r, which is what the Enter key produces). + self.write_to_pty(text.replace("\r\n", "\r").replace("\n", "\r").into_bytes()); + } } /// Toggle the vi mode status. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index c5f41b6e..a66511cf 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -60,7 +60,7 @@ pub struct Processor> { } pub trait ActionContext { - fn write_to_pty>>(&mut self, _data: B) {} + fn write_to_pty>>(&self, _data: B) {} fn mark_dirty(&mut self) {} fn size_info(&self) -> SizeInfo; fn copy_selection(&mut self, _ty: ClipboardType) {} @@ -107,6 +107,7 @@ pub trait ActionContext { fn toggle_vi_mode(&mut self) {} fn hint_state(&mut self) -> &mut HintState; fn hint_input(&mut self, _character: char) {} + fn paste(&mut self, _text: &str) {} } impl Action { @@ -243,11 +244,11 @@ impl Execute for Action { Action::ClearSelection => ctx.clear_selection(), Action::Paste => { let text = ctx.clipboard_mut().load(ClipboardType::Clipboard); - paste(ctx, &text); + ctx.paste(&text); }, Action::PasteSelection => { let text = ctx.clipboard_mut().load(ClipboardType::Selection); - paste(ctx, &text); + ctx.paste(&text); }, Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), #[cfg(target_os = "macos")] @@ -324,26 +325,6 @@ impl Execute for Action { } } -fn paste>(ctx: &mut A, contents: &str) { - if ctx.search_active() { - for c in contents.chars() { - ctx.search_input(c); - } - } else if ctx.terminal().mode().contains(TermMode::BRACKETED_PASTE) { - ctx.write_to_pty(&b"\x1b[200~"[..]); - ctx.write_to_pty(contents.replace("\x1b", "").into_bytes()); - ctx.write_to_pty(&b"\x1b[201~"[..]); - } else { - // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish - // pasted data from keystrokes. - // In theory, we should construct the keystrokes needed to produce the data we are - // pasting... since that's neither practical nor sensible (and probably an impossible - // task to solve in a general way), we'll just replace line breaks (windows and unix - // style) with a single carriage return (\r, which is what the Enter key produces). - ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes()); - } -} - #[derive(Debug, Clone, PartialEq)] pub enum MouseState { Url(Url), @@ -680,7 +661,6 @@ impl> Processor { } self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); - self.copy_selection(); } pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { @@ -965,14 +945,6 @@ impl> Processor { } } - /// Copy text selection. - fn copy_selection(&mut self) { - if self.ctx.config().selection.save_to_clipboard { - self.ctx.copy_selection(ClipboardType::Clipboard); - } - self.ctx.copy_selection(ClipboardType::Selection); - } - /// Trigger redraw when URL highlight changed. #[inline] fn update_url_state(&mut self, mouse_state: &MouseState) { diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index a1252570..70d16127 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -70,7 +70,7 @@ pub trait Notify { /// Notify that an escape sequence should be written to the PTY. /// /// TODO this needs to be able to error somehow. - fn notify>>(&mut self, _: B); + fn notify>>(&self, _: B); } /// Types that are interested in when the display is resized. diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs index 09c71668..c3224dfe 100644 --- a/alacritty_terminal/src/event_loop.rs +++ b/alacritty_terminal/src/event_loop.rs @@ -62,7 +62,7 @@ struct Writing { pub struct Notifier(pub Sender); impl event::Notify for Notifier { - fn notify(&mut self, bytes: B) + fn notify(&self, bytes: B) where B: Into>, { diff --git a/docs/features.md b/docs/features.md index 55f1d91a..3aa87aab 100644 --- a/docs/features.md +++ b/docs/features.md @@ -58,7 +58,8 @@ stays selected, allowing you to easily copy it. Terminal hints allow easily interacting with visible text without having to start vi mode. They consist of a regex that detects these text elements and then -feeds them to an external application. +either feeds them to an external application or triggers one of Alacritty's +built-in actions. Hints can be configured in the `hints` and `colors.hints` sections in the Alacritty configuration file.