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:
Christian Duerr 2021-04-03 23:52:44 +00:00 committed by GitHub
parent 531e494cf9
commit cbcc129440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 65 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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) {

View File

@ -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.

View File

@ -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]>>,
{

View File

@ -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.