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
|
# 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
|
# The fields `command`, `binding.key` and `binding.mods` accept the same
|
||||||
# values as they do in the `key_bindings` section.
|
# 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
|
# Example
|
||||||
#
|
#
|
||||||
# enabled:
|
# enabled:
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::de::Error as SerdeError;
|
use serde::de::Error as SerdeError;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{self, Deserialize, Deserializer};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
use alacritty_config_derive::ConfigDeserialize;
|
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.
|
/// Hint configuration.
|
||||||
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Hint {
|
pub struct Hint {
|
||||||
/// Command the text will be piped to.
|
/// Action executed when this hint is triggered.
|
||||||
pub command: Program,
|
#[serde(flatten)]
|
||||||
|
pub action: HintAction,
|
||||||
|
|
||||||
/// Regex for finding matches.
|
/// Regex for finding matches.
|
||||||
pub regex: LazyRegex,
|
pub regex: LazyRegex,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
use alacritty_terminal::term::search::Match;
|
||||||
use alacritty_terminal::term::Term;
|
use alacritty_terminal::term::Term;
|
||||||
|
|
||||||
use crate::config::ui_config::Hint;
|
use crate::config::ui_config::{Hint, HintAction};
|
||||||
use crate::daemon::start_daemon;
|
|
||||||
use crate::display::content::RegexMatches;
|
use crate::display::content::RegexMatches;
|
||||||
|
|
||||||
/// Percentage of characters in the hints alphabet used for the last character.
|
/// Percentage of characters in the hints alphabet used for the last character.
|
||||||
|
@ -88,7 +88,7 @@ impl HintState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle keyboard input during hint selection.
|
/// 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 {
|
match c {
|
||||||
// Use backspace to remove the last character pressed.
|
// Use backspace to remove the last character pressed.
|
||||||
'\x08' | '\x1f' => {
|
'\x08' | '\x1f' => {
|
||||||
|
@ -102,34 +102,25 @@ impl HintState {
|
||||||
// Update the visible matches.
|
// Update the visible matches.
|
||||||
self.update_matches(term);
|
self.update_matches(term);
|
||||||
|
|
||||||
let hint = match self.hint.as_ref() {
|
let hint = self.hint.as_ref()?;
|
||||||
Some(hint) => hint,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the last label starting with the input character.
|
// Find the last label starting with the input character.
|
||||||
let mut labels = self.labels.iter().enumerate().rev();
|
let mut labels = self.labels.iter().enumerate().rev();
|
||||||
let (index, label) = match labels.find(|(_, label)| !label.is_empty() && label[0] == c) {
|
let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?;
|
||||||
Some(last) => last,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the selected label is fully matched.
|
// Check if the selected label is fully matched.
|
||||||
if label.len() == 1 {
|
if label.len() == 1 {
|
||||||
// Get text for the hint's regex match.
|
let bounds = self.matches[index].clone();
|
||||||
let hint_match = &self.matches[index];
|
let action = hint.action.clone();
|
||||||
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);
|
|
||||||
|
|
||||||
self.stop();
|
self.stop();
|
||||||
|
|
||||||
|
Some(HintMatch { action, bounds })
|
||||||
} else {
|
} else {
|
||||||
// Store character to preserve the selection.
|
// Store character to preserve the selection.
|
||||||
self.keys.push(c);
|
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.
|
/// Generator for creating new hint labels.
|
||||||
struct HintLabels {
|
struct HintLabels {
|
||||||
/// Full character set available.
|
/// Full character set available.
|
||||||
|
|
|
@ -41,9 +41,10 @@ use alacritty_terminal::tty;
|
||||||
|
|
||||||
use crate::cli::Options as CLIOptions;
|
use crate::cli::Options as CLIOptions;
|
||||||
use crate::clipboard::Clipboard;
|
use crate::clipboard::Clipboard;
|
||||||
|
use crate::config::ui_config::{HintAction, HintInternalAction};
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config};
|
||||||
use crate::daemon::start_daemon;
|
use crate::daemon::start_daemon;
|
||||||
use crate::display::hint::HintState;
|
use crate::display::hint::{HintMatch, HintState};
|
||||||
use crate::display::window::Window;
|
use crate::display::window::Window;
|
||||||
use crate::display::{Display, DisplayUpdate};
|
use crate::display::{Display, DisplayUpdate};
|
||||||
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
|
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> {
|
impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionContext<'a, N, T> {
|
||||||
#[inline]
|
#[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);
|
self.notifier.notify(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,12 +222,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
|
||||||
*self.dirty = true;
|
*self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy text selection.
|
||||||
fn copy_selection(&mut self, ty: ClipboardType) {
|
fn copy_selection(&mut self, ty: ClipboardType) {
|
||||||
if let Some(selected) = self.terminal.selection_to_string() {
|
let text = match self.terminal.selection_to_string().filter(|s| !s.is_empty()) {
|
||||||
if !selected.is_empty() {
|
Some(text) => text,
|
||||||
self.clipboard.store(ty, selected);
|
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 {
|
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.terminal.selection = Some(selection);
|
||||||
*self.dirty = true;
|
*self.dirty = true;
|
||||||
|
|
||||||
|
self.copy_selection(ClipboardType::Selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
|
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
|
||||||
self.terminal.selection = Some(Selection::new(ty, point, side));
|
self.terminal.selection = Some(Selection::new(ty, point, side));
|
||||||
*self.dirty = true;
|
*self.dirty = true;
|
||||||
|
|
||||||
|
self.copy_selection(ClipboardType::Selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
|
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() => {
|
Some(selection) if !selection.is_empty() => {
|
||||||
selection.ty = ty;
|
selection.ty = ty;
|
||||||
*self.dirty = true;
|
*self.dirty = true;
|
||||||
|
|
||||||
|
self.copy_selection(ClipboardType::Selection);
|
||||||
},
|
},
|
||||||
_ => self.start_selection(ty, point, side),
|
_ => 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.
|
/// Process a new character for keyboard hints.
|
||||||
fn hint_input(&mut self, c: char) {
|
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;
|
*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.
|
/// Toggle the vi mode status.
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub struct Processor<T: EventListener, A: ActionContext<T>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ActionContext<T: EventListener> {
|
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 mark_dirty(&mut self) {}
|
||||||
fn size_info(&self) -> SizeInfo;
|
fn size_info(&self) -> SizeInfo;
|
||||||
fn copy_selection(&mut self, _ty: ClipboardType) {}
|
fn copy_selection(&mut self, _ty: ClipboardType) {}
|
||||||
|
@ -107,6 +107,7 @@ pub trait ActionContext<T: EventListener> {
|
||||||
fn toggle_vi_mode(&mut self) {}
|
fn toggle_vi_mode(&mut self) {}
|
||||||
fn hint_state(&mut self) -> &mut HintState;
|
fn hint_state(&mut self) -> &mut HintState;
|
||||||
fn hint_input(&mut self, _character: char) {}
|
fn hint_input(&mut self, _character: char) {}
|
||||||
|
fn paste(&mut self, _text: &str) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
|
@ -243,11 +244,11 @@ impl<T: EventListener> Execute<T> for Action {
|
||||||
Action::ClearSelection => ctx.clear_selection(),
|
Action::ClearSelection => ctx.clear_selection(),
|
||||||
Action::Paste => {
|
Action::Paste => {
|
||||||
let text = ctx.clipboard_mut().load(ClipboardType::Clipboard);
|
let text = ctx.clipboard_mut().load(ClipboardType::Clipboard);
|
||||||
paste(ctx, &text);
|
ctx.paste(&text);
|
||||||
},
|
},
|
||||||
Action::PasteSelection => {
|
Action::PasteSelection => {
|
||||||
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
|
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
|
||||||
paste(ctx, &text);
|
ctx.paste(&text);
|
||||||
},
|
},
|
||||||
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
|
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
|
||||||
#[cfg(target_os = "macos")]
|
#[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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum MouseState {
|
pub enum MouseState {
|
||||||
Url(Url),
|
Url(Url),
|
||||||
|
@ -680,7 +661,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling);
|
self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling);
|
||||||
self.copy_selection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
|
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.
|
/// Trigger redraw when URL highlight changed.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn update_url_state(&mut self, mouse_state: &MouseState) {
|
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.
|
/// Notify that an escape sequence should be written to the PTY.
|
||||||
///
|
///
|
||||||
/// TODO this needs to be able to error somehow.
|
/// 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.
|
/// Types that are interested in when the display is resized.
|
||||||
|
|
|
@ -62,7 +62,7 @@ struct Writing {
|
||||||
pub struct Notifier(pub Sender<Msg>);
|
pub struct Notifier(pub Sender<Msg>);
|
||||||
|
|
||||||
impl event::Notify for Notifier {
|
impl event::Notify for Notifier {
|
||||||
fn notify<B>(&mut self, bytes: B)
|
fn notify<B>(&self, bytes: B)
|
||||||
where
|
where
|
||||||
B: Into<Cow<'static, [u8]>>,
|
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
|
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
|
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
|
Hints can be configured in the `hints` and `colors.hints` sections in the
|
||||||
Alacritty configuration file.
|
Alacritty configuration file.
|
||||||
|
|
Loading…
Reference in a new issue