mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-18 13:55:23 -05:00
Add copy/paste/select hint actions
This adds some built-in actions for handling hint selections without having to spawn external applications. The new actions are `Copy`, `Select` and `Paste`.
This commit is contained in:
parent
531e494cf9
commit
cbcc129440
8 changed files with 135 additions and 65 deletions
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<T>(&mut self, term: &Term<T>, c: char) {
|
||||
pub fn keyboard_input<T>(&mut self, term: &Term<T>, c: char) -> Option<HintMatch> {
|
||||
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.
|
||||
|
|
|
@ -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<T> for ActionContext<'a, N, T> {
|
||||
#[inline]
|
||||
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, val: B) {
|
||||
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&self, val: B) {
|
||||
self.notifier.notify(val);
|
||||
}
|
||||
|
||||
|
@ -221,12 +222,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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<T> 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<T> 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<T> 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.
|
||||
|
|
|
@ -60,7 +60,7 @@ pub struct Processor<T: EventListener, A: ActionContext<T>> {
|
|||
}
|
||||
|
||||
pub trait ActionContext<T: EventListener> {
|
||||
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _data: B) {}
|
||||
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&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<T: EventListener> {
|
|||
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<T: EventListener> Execute<T> 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<T: EventListener> Execute<T> for Action {
|
|||
}
|
||||
}
|
||||
|
||||
fn paste<T: EventListener, A: ActionContext<T>>(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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
|||
}
|
||||
|
||||
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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
|
|
|
@ -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<B: Into<Cow<'static, [u8]>>>(&mut self, _: B);
|
||||
fn notify<B: Into<Cow<'static, [u8]>>>(&self, _: B);
|
||||
}
|
||||
|
||||
/// Types that are interested in when the display is resized.
|
||||
|
|
|
@ -62,7 +62,7 @@ struct Writing {
|
|||
pub struct Notifier(pub Sender<Msg>);
|
||||
|
||||
impl event::Notify for Notifier {
|
||||
fn notify<B>(&mut self, bytes: B)
|
||||
fn notify<B>(&self, bytes: B)
|
||||
where
|
||||
B: Into<Cow<'static, [u8]>>,
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue