Add modal keyboard motion mode

This implements a basic mode for navigating inside of Alacritty's
history with keyboard bindings. They're bound by default to vi's motion
shortcuts but are fully customizable. Since this relies on key bindings
only single key bindings are currently supported (so no `ge`, or
repetition).

Other than navigating the history and moving the viewport, this mode
should enable making use of all available selection modes to copy
content to the clipboard and launch URLs below the cursor.

This also changes the rendering of the block cursor at the side of
selections, since previously it could be inverted to be completely
invisible. Since that would have caused some troubles with this keyboard
selection mode, the block cursor now is no longer inverted when it is at
the edges of a selection.

Fixes #262.
This commit is contained in:
Christian Duerr 2020-03-18 02:35:08 +00:00 committed by GitHub
parent 64db7d3daa
commit 1a8cd172e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1915 additions and 538 deletions

View File

@ -16,7 +16,7 @@ os:
- osx
rust:
- 1.37.0
- 1.39.0
- stable
- nightly
@ -30,22 +30,22 @@ matrix:
- name: "Clippy Linux"
os: linux
env: CLIPPY=true
rust: 1.37.0
rust: 1.39.0
- name: "Clippy OSX"
os: osx
env: CLIPPY=true
rust: 1.37.0
rust: 1.39.0
- name: "Clippy Windows"
os: windows
env: CLIPPY=true
rust: 1.37.0-x86_64-pc-windows-msvc
rust: 1.39.0-x86_64-pc-windows-msvc
- name: "Rustfmt"
os: linux
env: RUSTFMT=true
rust: nightly
- name: "Windows 1.37.0"
- name: "Windows 1.39.0"
os: windows
rust: 1.37.0-x86_64-pc-windows-msvc
rust: 1.39.0-x86_64-pc-windows-msvc
- name: "Windows Stable"
os: windows
rust: stable-x86_64-pc-windows-msvc

View File

@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Default Command+N keybinding for SpawnNewInstance on macOS
- Vi mode for copying text and opening links
### Changed
- Block cursor is no longer inverted at the start/end of a selection
## 0.4.2-dev

View File

@ -42,7 +42,7 @@ and
[easy](https://github.com/alacritty/alacritty/issues?q=is%3Aopen+is%3Aissue+label%3A%22D+-+easy%22)
issues.
Please note that the minimum supported version of Alacritty is Rust 1.37.0. All patches are expected
Please note that the minimum supported version of Alacritty is Rust 1.39.0. All patches are expected
to work with the minimum supported version.
### Testing

View File

@ -187,12 +187,20 @@
# Cursor colors
#
# Colors which should be used to draw the terminal cursor. If these are unset,
# the cursor color will be the inverse of the cell color.
# Colors which should be used to draw the terminal cursor. If these are
# unset, the cursor color will be the inverse of the cell color.
#cursor:
# text: '#000000'
# cursor: '#ffffff'
# Vi mode cursor colors
#
# Colors for the cursor when the vi mode is active. If these are unset, the
# cursor color will be the inverse of the cell color.
#vi_mode_cursor:
# text: '#000000'
# cursor: '#ffffff'
# Selection colors
#
# Colors which should be used to draw the selection area. If selection
@ -298,6 +306,14 @@
# - | Beam
#style: Block
# Vi mode cursor style
#
# If the vi mode cursor style is `None` or not specified, it will fall back to
# the style of the active value of the normal cursor.
#
# See `cursor.style` for available options.
#vi_mode_style: None
# If this is `true`, the cursor will be rendered as a hollow box when the
# window is not focused.
#unfocused_hollow: true
@ -435,6 +451,7 @@
#
# - `action`: Execute a predefined action
#
# - ToggleViMode
# - Copy
# - Paste
# - PasteSelection
@ -454,9 +471,36 @@
# - ToggleFullscreen
# - SpawnNewInstance
# - ClearLogNotice
# - ClearSelection
# - ReceiveChar
# - None
#
# (`mode: Vi` only):
# - Open
# - Up
# - Down
# - Left
# - Right
# - First
# - Last
# - FirstOccupied
# - High
# - Middle
# - Low
# - SemanticLeft
# - SemanticRight
# - SemanticLeftEnd
# - SemanticRightEnd
# - WordRight
# - WordLeft
# - WordRightEnd
# - WordLeftEnd
# - Bracket
# - ToggleNormalSelection
# - ToggleLineSelection
# - ToggleBlockSelection
# - ToggleSemanticSelection
#
# (macOS only):
# - ToggleSimpleFullscreen: Enters fullscreen without occupying another space
#
@ -501,6 +545,57 @@
# If the same trigger is assigned to multiple actions, all of them are executed
# at once.
#key_bindings:
#- { key: Paste, action: Paste }
#- { key: Copy, action: Copy }
#- { key: L, mods: Control, action: ClearLogNotice }
#- { key: L, mods: Control, chars: "\x0c" }
#- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt }
#- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt }
#- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt }
#- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt }
# Vi Mode
#- { key: Space, mods: Shift|Control, mode: Vi, action: ScrollToBottom }
#- { key: Space, mods: Shift|Control, action: ToggleViMode }
#- { key: Escape, mode: Vi, action: ClearSelection }
#- { key: I, mode: Vi, action: ScrollToBottom }
#- { key: I, mode: Vi, action: ToggleViMode }
#- { key: Y, mods: Control, mode: Vi, action: ScrollLineUp }
#- { key: E, mods: Control, mode: Vi, action: ScrollLineDown }
#- { key: G, mode: Vi, action: ScrollToTop }
#- { key: G, mods: Shift, mode: Vi, action: ScrollToBottom }
#- { key: B, mods: Control, mode: Vi, action: ScrollPageUp }
#- { key: F, mods: Control, mode: Vi, action: ScrollPageDown }
#- { key: U, mods: Control, mode: Vi, action: ScrollHalfPageUp }
#- { key: D, mods: Control, mode: Vi, action: ScrollHalfPageDown }
#- { key: Y, mode: Vi, action: Copy }
#- { key: V, mode: Vi, action: ToggleNormalSelection }
#- { key: V, mods: Shift, mode: Vi, action: ToggleLineSelection }
#- { key: V, mods: Control, mode: Vi, action: ToggleBlockSelection }
#- { key: V, mods: Alt, mode: Vi, action: ToggleSemanticSelection }
#- { key: Return, mode: Vi, action: Open }
#- { key: K, mode: Vi, action: Up }
#- { key: J, mode: Vi, action: Down }
#- { key: H, mode: Vi, action: Left }
#- { key: L, mode: Vi, action: Right }
#- { key: Up, mode: Vi, action: Up }
#- { key: Down, mode: Vi, action: Down }
#- { key: Left, mode: Vi, action: Left }
#- { key: Right, mode: Vi, action: Right }
#- { key: Key0, mode: Vi, action: First }
#- { key: Key4, mods: Shift, mode: Vi, action: Last }
#- { key: Key6, mods: Shift, mode: Vi, action: FirstOccupied }
#- { key: H, mods: Shift, mode: Vi, action: High }
#- { key: M, mods: Shift, mode: Vi, action: Middle }
#- { key: L, mods: Shift, mode: Vi, action: Low }
#- { key: B, mode: Vi, action: SemanticLeft }
#- { key: W, mode: Vi, action: SemanticRight }
#- { key: E, mode: Vi, action: SemanticRightEnd }
#- { key: B, mods: Shift, mode: Vi, action: WordLeft }
#- { key: W, mods: Shift, mode: Vi, action: WordRight }
#- { key: E, mods: Shift, mode: Vi, action: WordRightEnd }
#- { key: Key5, mods: Shift, mode: Vi, action: Bracket }
# (Windows, Linux, and BSD only)
#- { key: V, mods: Control|Shift, action: Paste }
#- { key: C, mods: Control|Shift, action: Copy }
@ -530,14 +625,14 @@
#- { key: N, mods: Command, action: SpawnNewInstance }
#- { key: F, mods: Command|Control, action: ToggleFullscreen }
#- { key: Paste, action: Paste }
#- { key: Copy, action: Copy }
#- { key: L, mods: Control, action: ClearLogNotice }
#- { key: L, mods: Control, chars: "\x0c" }
#- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt }
#- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt }
#- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt }
#- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt }
#- { key: Paste, action: Paste }
#- { key: Copy, action: Copy }
#- { key: L, mods: Control, action: ClearLogNotice }
#- { key: L, mods: Control, chars: "\x0c" }
#- { key: PageUp, mods: Shift, action: ScrollPageUp, mode: ~Alt }
#- { key: PageDown, mods: Shift, action: ScrollPageDown, mode: ~Alt }
#- { key: Home, mods: Shift, action: ScrollToTop, mode: ~Alt }
#- { key: End, mods: Shift, action: ScrollToBottom, mode: ~Alt }
#debug:
# Display the time it takes to redraw each frame.

View File

@ -13,18 +13,18 @@
// limitations under the License.
#![allow(clippy::enum_glob_use)]
use std::fmt;
use std::fmt::{self, Debug, Display};
use std::str::FromStr;
use glutin::event::VirtualKeyCode::*;
use glutin::event::{ModifiersState, MouseButton, VirtualKeyCode};
use log::error;
use serde::de::Error as SerdeError;
use serde::de::{self, MapAccess, Unexpected, Visitor};
use serde::{Deserialize, Deserializer};
use serde_yaml::Value as SerdeValue;
use alacritty_terminal::config::LOG_TARGET_CONFIG;
use alacritty_terminal::term::TermMode;
use alacritty_terminal::vi_mode::ViMotion;
/// Describes a state and action to take in that state
///
@ -55,30 +55,6 @@ pub type KeyBinding = Binding<Key>;
/// Bindings that are triggered by a mouse button
pub type MouseBinding = Binding<MouseButton>;
impl Default for KeyBinding {
fn default() -> KeyBinding {
KeyBinding {
mods: Default::default(),
action: Action::Esc(String::new()),
mode: TermMode::NONE,
notmode: TermMode::NONE,
trigger: Key::Keycode(A),
}
}
}
impl Default for MouseBinding {
fn default() -> MouseBinding {
MouseBinding {
mods: Default::default(),
action: Action::Esc(String::new()),
mode: TermMode::NONE,
notmode: TermMode::NONE,
trigger: MouseButton::Left,
}
}
}
impl<T: Eq> Binding<T> {
#[inline]
pub fn is_triggered_by(&self, mode: TermMode, mods: ModifiersState, input: &T) -> bool {
@ -117,6 +93,18 @@ pub enum Action {
#[serde(skip)]
Esc(String),
/// Run given command.
#[serde(skip)]
Command(String, Vec<String>),
/// Move vi mode cursor.
#[serde(skip)]
ViMotion(ViMotion),
/// Perform vi mode action.
#[serde(skip)]
ViAction(ViAction),
/// Paste contents of system clipboard.
Paste,
@ -141,6 +129,12 @@ pub enum Action {
/// Scroll exactly one page down.
ScrollPageDown,
/// Scroll half a page up.
ScrollHalfPageUp,
/// Scroll half a page down.
ScrollHalfPageDown,
/// Scroll one line up.
ScrollLineUp,
@ -156,10 +150,6 @@ pub enum Action {
/// Clear the display buffer(s) to remove history.
ClearHistory,
/// Run given command.
#[serde(skip)]
Command(String, Vec<String>),
/// Hide the Alacritty window.
Hide,
@ -182,6 +172,12 @@ pub enum Action {
#[cfg(target_os = "macos")]
ToggleSimpleFullscreen,
/// Clear active selection.
ClearSelection,
/// Toggle vi mode.
ToggleViMode,
/// Allow receiving char input.
ReceiveChar,
@ -189,18 +185,50 @@ pub enum Action {
None,
}
impl Default for Action {
fn default() -> Action {
Action::None
}
}
impl From<&'static str> for Action {
fn from(s: &'static str) -> Action {
Action::Esc(s.into())
}
}
/// Display trait used for error logging.
impl Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Action::ViMotion(motion) => motion.fmt(f),
Action::ViAction(action) => action.fmt(f),
_ => write!(f, "{:?}", self),
}
}
}
/// Vi mode specific actions.
#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
pub enum ViAction {
/// Toggle normal vi selection.
ToggleNormalSelection,
/// Toggle line vi selection.
ToggleLineSelection,
/// Toggle block vi selection.
ToggleBlockSelection,
/// Toggle semantic vi selection.
ToggleSemanticSelection,
/// Launch the URL below the vi mode cursor.
Open,
}
impl From<ViAction> for Action {
fn from(action: ViAction) -> Self {
Self::ViAction(action)
}
}
impl From<ViMotion> for Action {
fn from(motion: ViMotion) -> Self {
Self::ViMotion(motion)
}
}
macro_rules! bindings {
(
KeyBinding;
@ -241,16 +269,16 @@ macro_rules! bindings {
let mut _mods = ModifiersState::empty();
$(_mods = $mods;)*
let mut _mode = TermMode::empty();
$(_mode = $mode;)*
$(_mode.insert($mode);)*
let mut _notmode = TermMode::empty();
$(_notmode = $notmode;)*
$(_notmode.insert($notmode);)*
v.push($ty {
trigger: $key,
mods: _mods,
mode: _mode,
notmode: _notmode,
action: $action,
action: $action.into(),
});
)*
@ -261,65 +289,109 @@ macro_rules! bindings {
pub fn default_mouse_bindings() -> Vec<MouseBinding> {
bindings!(
MouseBinding;
MouseButton::Middle; Action::PasteSelection;
MouseButton::Middle, ~TermMode::VI; Action::PasteSelection;
)
}
pub fn default_key_bindings() -> Vec<KeyBinding> {
let mut bindings = bindings!(
KeyBinding;
Paste; Action::Paste;
Copy; Action::Copy;
Paste, ~TermMode::VI; Action::Paste;
L, ModifiersState::CTRL; Action::ClearLogNotice;
L, ModifiersState::CTRL; Action::Esc("\x0c".into());
PageUp, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageUp;
PageDown, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageDown;
L, ModifiersState::CTRL, ~TermMode::VI; Action::Esc("\x0c".into());
Tab, ModifiersState::SHIFT, ~TermMode::VI; Action::Esc("\x1b[Z".into());
Back, ModifiersState::ALT, ~TermMode::VI; Action::Esc("\x1b\x7f".into());
Home, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollToTop;
End, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollToBottom;
Home, +TermMode::APP_CURSOR; Action::Esc("\x1bOH".into());
Home, ~TermMode::APP_CURSOR; Action::Esc("\x1b[H".into());
Home, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2H".into());
End, +TermMode::APP_CURSOR; Action::Esc("\x1bOF".into());
End, ~TermMode::APP_CURSOR; Action::Esc("\x1b[F".into());
End, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2F".into());
PageUp; Action::Esc("\x1b[5~".into());
PageUp, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[5;2~".into());
PageDown; Action::Esc("\x1b[6~".into());
PageDown, ModifiersState::SHIFT, +TermMode::ALT_SCREEN; Action::Esc("\x1b[6;2~".into());
Tab, ModifiersState::SHIFT; Action::Esc("\x1b[Z".into());
Back; Action::Esc("\x7f".into());
Back, ModifiersState::ALT; Action::Esc("\x1b\x7f".into());
Insert; Action::Esc("\x1b[2~".into());
Delete; Action::Esc("\x1b[3~".into());
Up, +TermMode::APP_CURSOR; Action::Esc("\x1bOA".into());
Up, ~TermMode::APP_CURSOR; Action::Esc("\x1b[A".into());
Down, +TermMode::APP_CURSOR; Action::Esc("\x1bOB".into());
Down, ~TermMode::APP_CURSOR; Action::Esc("\x1b[B".into());
Right, +TermMode::APP_CURSOR; Action::Esc("\x1bOC".into());
Right, ~TermMode::APP_CURSOR; Action::Esc("\x1b[C".into());
Left, +TermMode::APP_CURSOR; Action::Esc("\x1bOD".into());
Left, ~TermMode::APP_CURSOR; Action::Esc("\x1b[D".into());
F1; Action::Esc("\x1bOP".into());
F2; Action::Esc("\x1bOQ".into());
F3; Action::Esc("\x1bOR".into());
F4; Action::Esc("\x1bOS".into());
F5; Action::Esc("\x1b[15~".into());
F6; Action::Esc("\x1b[17~".into());
F7; Action::Esc("\x1b[18~".into());
F8; Action::Esc("\x1b[19~".into());
F9; Action::Esc("\x1b[20~".into());
F10; Action::Esc("\x1b[21~".into());
F11; Action::Esc("\x1b[23~".into());
F12; Action::Esc("\x1b[24~".into());
F13; Action::Esc("\x1b[25~".into());
F14; Action::Esc("\x1b[26~".into());
F15; Action::Esc("\x1b[28~".into());
F16; Action::Esc("\x1b[29~".into());
F17; Action::Esc("\x1b[31~".into());
F18; Action::Esc("\x1b[32~".into());
F19; Action::Esc("\x1b[33~".into());
F20; Action::Esc("\x1b[34~".into());
NumpadEnter; Action::Esc("\n".into());
PageUp, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageUp;
PageDown, ModifiersState::SHIFT, ~TermMode::ALT_SCREEN; Action::ScrollPageDown;
Home, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI;
Action::Esc("\x1b[1;2H".into());
End, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI;
Action::Esc("\x1b[1;2F".into());
PageUp, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI;
Action::Esc("\x1b[5;2~".into());
PageDown, ModifiersState::SHIFT, +TermMode::ALT_SCREEN, ~TermMode::VI;
Action::Esc("\x1b[6;2~".into());
Home, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOH".into());
Home, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[H".into());
End, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOF".into());
End, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[F".into());
Up, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOA".into());
Up, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[A".into());
Down, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOB".into());
Down, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[B".into());
Right, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOC".into());
Right, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[C".into());
Left, +TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1bOD".into());
Left, ~TermMode::APP_CURSOR, ~TermMode::VI; Action::Esc("\x1b[D".into());
Back, ~TermMode::VI; Action::Esc("\x7f".into());
Insert, ~TermMode::VI; Action::Esc("\x1b[2~".into());
Delete, ~TermMode::VI; Action::Esc("\x1b[3~".into());
PageUp, ~TermMode::VI; Action::Esc("\x1b[5~".into());
PageDown, ~TermMode::VI; Action::Esc("\x1b[6~".into());
F1, ~TermMode::VI; Action::Esc("\x1bOP".into());
F2, ~TermMode::VI; Action::Esc("\x1bOQ".into());
F3, ~TermMode::VI; Action::Esc("\x1bOR".into());
F4, ~TermMode::VI; Action::Esc("\x1bOS".into());
F5, ~TermMode::VI; Action::Esc("\x1b[15~".into());
F6, ~TermMode::VI; Action::Esc("\x1b[17~".into());
F7, ~TermMode::VI; Action::Esc("\x1b[18~".into());
F8, ~TermMode::VI; Action::Esc("\x1b[19~".into());
F9, ~TermMode::VI; Action::Esc("\x1b[20~".into());
F10, ~TermMode::VI; Action::Esc("\x1b[21~".into());
F11, ~TermMode::VI; Action::Esc("\x1b[23~".into());
F12, ~TermMode::VI; Action::Esc("\x1b[24~".into());
F13, ~TermMode::VI; Action::Esc("\x1b[25~".into());
F14, ~TermMode::VI; Action::Esc("\x1b[26~".into());
F15, ~TermMode::VI; Action::Esc("\x1b[28~".into());
F16, ~TermMode::VI; Action::Esc("\x1b[29~".into());
F17, ~TermMode::VI; Action::Esc("\x1b[31~".into());
F18, ~TermMode::VI; Action::Esc("\x1b[32~".into());
F19, ~TermMode::VI; Action::Esc("\x1b[33~".into());
F20, ~TermMode::VI; Action::Esc("\x1b[34~".into());
NumpadEnter, ~TermMode::VI; Action::Esc("\n".into());
Space, ModifiersState::SHIFT | ModifiersState::CTRL, +TermMode::VI; Action::ScrollToBottom;
Space, ModifiersState::SHIFT | ModifiersState::CTRL; Action::ToggleViMode;
Escape, +TermMode::VI; Action::ClearSelection;
I, +TermMode::VI; Action::ScrollToBottom;
I, +TermMode::VI; Action::ToggleViMode;
Y, ModifiersState::CTRL, +TermMode::VI; Action::ScrollLineUp;
E, ModifiersState::CTRL, +TermMode::VI; Action::ScrollLineDown;
G, +TermMode::VI; Action::ScrollToTop;
G, ModifiersState::SHIFT, +TermMode::VI; Action::ScrollToBottom;
B, ModifiersState::CTRL, +TermMode::VI; Action::ScrollPageUp;
F, ModifiersState::CTRL, +TermMode::VI; Action::ScrollPageDown;
U, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageUp;
D, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageDown;
Y, +TermMode::VI; Action::Copy;
V, +TermMode::VI; ViAction::ToggleNormalSelection;
V, ModifiersState::SHIFT, +TermMode::VI; ViAction::ToggleLineSelection;
V, ModifiersState::CTRL, +TermMode::VI; ViAction::ToggleBlockSelection;
V, ModifiersState::ALT, +TermMode::VI; ViAction::ToggleSemanticSelection;
Return, +TermMode::VI; ViAction::Open;
K, +TermMode::VI; ViMotion::Up;
J, +TermMode::VI; ViMotion::Down;
H, +TermMode::VI; ViMotion::Left;
L, +TermMode::VI; ViMotion::Right;
Up, +TermMode::VI; ViMotion::Up;
Down, +TermMode::VI; ViMotion::Down;
Left, +TermMode::VI; ViMotion::Left;
Right, +TermMode::VI; ViMotion::Right;
Key0, +TermMode::VI; ViMotion::First;
Key4, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Last;
Key6, ModifiersState::SHIFT, +TermMode::VI; ViMotion::FirstOccupied;
H, ModifiersState::SHIFT, +TermMode::VI; ViMotion::High;
M, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Middle;
L, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Low;
B, +TermMode::VI; ViMotion::SemanticLeft;
W, +TermMode::VI; ViMotion::SemanticRight;
E, +TermMode::VI; ViMotion::SemanticRightEnd;
B, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordLeft;
W, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordRight;
E, ModifiersState::SHIFT, +TermMode::VI; ViMotion::WordRightEnd;
Key5, ModifiersState::SHIFT, +TermMode::VI; ViMotion::Bracket;
);
// Code Modifiers
@ -348,31 +420,31 @@ pub fn default_key_bindings() -> Vec<KeyBinding> {
let modifiers_code = index + 2;
bindings.extend(bindings!(
KeyBinding;
Delete, mods; Action::Esc(format!("\x1b[3;{}~", modifiers_code));
Up, mods; Action::Esc(format!("\x1b[1;{}A", modifiers_code));
Down, mods; Action::Esc(format!("\x1b[1;{}B", modifiers_code));
Right, mods; Action::Esc(format!("\x1b[1;{}C", modifiers_code));
Left, mods; Action::Esc(format!("\x1b[1;{}D", modifiers_code));
F1, mods; Action::Esc(format!("\x1b[1;{}P", modifiers_code));
F2, mods; Action::Esc(format!("\x1b[1;{}Q", modifiers_code));
F3, mods; Action::Esc(format!("\x1b[1;{}R", modifiers_code));
F4, mods; Action::Esc(format!("\x1b[1;{}S", modifiers_code));
F5, mods; Action::Esc(format!("\x1b[15;{}~", modifiers_code));
F6, mods; Action::Esc(format!("\x1b[17;{}~", modifiers_code));
F7, mods; Action::Esc(format!("\x1b[18;{}~", modifiers_code));
F8, mods; Action::Esc(format!("\x1b[19;{}~", modifiers_code));
F9, mods; Action::Esc(format!("\x1b[20;{}~", modifiers_code));
F10, mods; Action::Esc(format!("\x1b[21;{}~", modifiers_code));
F11, mods; Action::Esc(format!("\x1b[23;{}~", modifiers_code));
F12, mods; Action::Esc(format!("\x1b[24;{}~", modifiers_code));
F13, mods; Action::Esc(format!("\x1b[25;{}~", modifiers_code));
F14, mods; Action::Esc(format!("\x1b[26;{}~", modifiers_code));
F15, mods; Action::Esc(format!("\x1b[28;{}~", modifiers_code));
F16, mods; Action::Esc(format!("\x1b[29;{}~", modifiers_code));
F17, mods; Action::Esc(format!("\x1b[31;{}~", modifiers_code));
F18, mods; Action::Esc(format!("\x1b[32;{}~", modifiers_code));
F19, mods; Action::Esc(format!("\x1b[33;{}~", modifiers_code));
F20, mods; Action::Esc(format!("\x1b[34;{}~", modifiers_code));
Delete, mods, ~TermMode::VI; Action::Esc(format!("\x1b[3;{}~", modifiers_code));
Up, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}A", modifiers_code));
Down, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}B", modifiers_code));
Right, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}C", modifiers_code));
Left, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}D", modifiers_code));
F1, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}P", modifiers_code));
F2, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}Q", modifiers_code));
F3, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}R", modifiers_code));
F4, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}S", modifiers_code));
F5, mods, ~TermMode::VI; Action::Esc(format!("\x1b[15;{}~", modifiers_code));
F6, mods, ~TermMode::VI; Action::Esc(format!("\x1b[17;{}~", modifiers_code));
F7, mods, ~TermMode::VI; Action::Esc(format!("\x1b[18;{}~", modifiers_code));
F8, mods, ~TermMode::VI; Action::Esc(format!("\x1b[19;{}~", modifiers_code));
F9, mods, ~TermMode::VI; Action::Esc(format!("\x1b[20;{}~", modifiers_code));
F10, mods, ~TermMode::VI; Action::Esc(format!("\x1b[21;{}~", modifiers_code));
F11, mods, ~TermMode::VI; Action::Esc(format!("\x1b[23;{}~", modifiers_code));
F12, mods, ~TermMode::VI; Action::Esc(format!("\x1b[24;{}~", modifiers_code));
F13, mods, ~TermMode::VI; Action::Esc(format!("\x1b[25;{}~", modifiers_code));
F14, mods, ~TermMode::VI; Action::Esc(format!("\x1b[26;{}~", modifiers_code));
F15, mods, ~TermMode::VI; Action::Esc(format!("\x1b[28;{}~", modifiers_code));
F16, mods, ~TermMode::VI; Action::Esc(format!("\x1b[29;{}~", modifiers_code));
F17, mods, ~TermMode::VI; Action::Esc(format!("\x1b[31;{}~", modifiers_code));
F18, mods, ~TermMode::VI; Action::Esc(format!("\x1b[32;{}~", modifiers_code));
F19, mods, ~TermMode::VI; Action::Esc(format!("\x1b[33;{}~", modifiers_code));
F20, mods, ~TermMode::VI; Action::Esc(format!("\x1b[34;{}~", modifiers_code));
));
// We're adding the following bindings with `Shift` manually above, so skipping them here
@ -380,11 +452,11 @@ pub fn default_key_bindings() -> Vec<KeyBinding> {
if modifiers_code != 2 {
bindings.extend(bindings!(
KeyBinding;
Insert, mods; Action::Esc(format!("\x1b[2;{}~", modifiers_code));
PageUp, mods; Action::Esc(format!("\x1b[5;{}~", modifiers_code));
PageDown, mods; Action::Esc(format!("\x1b[6;{}~", modifiers_code));
End, mods; Action::Esc(format!("\x1b[1;{}F", modifiers_code));
Home, mods; Action::Esc(format!("\x1b[1;{}H", modifiers_code));
Insert, mods, ~TermMode::VI; Action::Esc(format!("\x1b[2;{}~", modifiers_code));
PageUp, mods, ~TermMode::VI; Action::Esc(format!("\x1b[5;{}~", modifiers_code));
PageDown, mods, ~TermMode::VI; Action::Esc(format!("\x1b[6;{}~", modifiers_code));
End, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}F", modifiers_code));
Home, mods, ~TermMode::VI; Action::Esc(format!("\x1b[1;{}H", modifiers_code));
));
}
}
@ -398,9 +470,9 @@ pub fn default_key_bindings() -> Vec<KeyBinding> {
fn common_keybindings() -> Vec<KeyBinding> {
bindings!(
KeyBinding;
V, ModifiersState::CTRL | ModifiersState::SHIFT; Action::Paste;
V, ModifiersState::CTRL | ModifiersState::SHIFT, ~TermMode::VI; Action::Paste;
C, ModifiersState::CTRL | ModifiersState::SHIFT; Action::Copy;
Insert, ModifiersState::SHIFT; Action::PasteSelection;
Insert, ModifiersState::SHIFT, ~TermMode::VI; Action::PasteSelection;
Key0, ModifiersState::CTRL; Action::ResetFontSize;
Equals, ModifiersState::CTRL; Action::IncreaseFontSize;
Add, ModifiersState::CTRL; Action::IncreaseFontSize;
@ -428,16 +500,16 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> {
pub fn platform_key_bindings() -> Vec<KeyBinding> {
bindings!(
KeyBinding;
Key0, ModifiersState::LOGO; Action::ResetFontSize;
Equals, ModifiersState::LOGO; Action::IncreaseFontSize;
Add, ModifiersState::LOGO; Action::IncreaseFontSize;
Minus, ModifiersState::LOGO; Action::DecreaseFontSize;
Insert, ModifiersState::SHIFT; Action::Esc("\x1b[2;2~".into());
Key0, ModifiersState::LOGO; Action::ResetFontSize;
Equals, ModifiersState::LOGO; Action::IncreaseFontSize;
Add, ModifiersState::LOGO; Action::IncreaseFontSize;
Minus, ModifiersState::LOGO; Action::DecreaseFontSize;
Insert, ModifiersState::SHIFT, ~TermMode::VI; Action::Esc("\x1b[2;2~".into());
K, ModifiersState::LOGO, ~TermMode::VI; Action::Esc("\x0c".into());
V, ModifiersState::LOGO, ~TermMode::VI; Action::Paste;
N, ModifiersState::LOGO; Action::SpawnNewInstance;
F, ModifiersState::CTRL | ModifiersState::LOGO; Action::ToggleFullscreen;
K, ModifiersState::LOGO; Action::ClearHistory;
K, ModifiersState::LOGO; Action::Esc("\x0c".into());
V, ModifiersState::LOGO; Action::Paste;
C, ModifiersState::LOGO; Action::Copy;
H, ModifiersState::LOGO; Action::Hide;
M, ModifiersState::LOGO; Action::Minimize;
@ -463,7 +535,7 @@ impl<'a> Deserialize<'a> for Key {
where
D: Deserializer<'a>,
{
let value = serde_yaml::Value::deserialize(deserializer)?;
let value = SerdeValue::deserialize(deserializer)?;
match u32::deserialize(value.clone()) {
Ok(scancode) => Ok(Key::Scancode(scancode)),
Err(_) => {
@ -491,7 +563,7 @@ impl<'a> Deserialize<'a> for ModeWrapper {
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"Combination of AppCursor | AppKeypad | Alt, possibly with negation (~)",
"a combination of AppCursor | AppKeypad | Alt | Vi, possibly with negation (~)",
)
}
@ -509,7 +581,9 @@ impl<'a> Deserialize<'a> for ModeWrapper {
"~appkeypad" => res.not_mode |= TermMode::APP_KEYPAD,
"alt" => res.mode |= TermMode::ALT_SCREEN,
"~alt" => res.not_mode |= TermMode::ALT_SCREEN,
_ => error!(target: LOG_TARGET_CONFIG, "Unknown mode {:?}", modifier),
"vi" => res.mode |= TermMode::VI,
"~vi" => res.not_mode |= TermMode::VI,
_ => return Err(E::invalid_value(Unexpected::Str(modifier), &self)),
}
}
@ -612,6 +686,8 @@ impl<'a> Deserialize<'a> for RawBinding {
where
D: Deserializer<'a>,
{
const FIELDS: &[&str] = &["key", "mods", "mode", "action", "chars", "mouse", "command"];
enum Field {
Key,
Mods,
@ -629,9 +705,6 @@ impl<'a> Deserialize<'a> for RawBinding {
{
struct FieldVisitor;
static FIELDS: &[&str] =
&["key", "mods", "mode", "action", "chars", "mouse", "command"];
impl<'a> Visitor<'a> for FieldVisitor {
type Value = Field;
@ -681,7 +754,7 @@ impl<'a> Deserialize<'a> for RawBinding {
let mut mouse: Option<MouseButton> = None;
let mut command: Option<CommandWrapper> = None;
use ::serde::de::Error;
use de::Error;
while let Some(struct_key) = map.next_key::<Field>()? {
match struct_key {
@ -690,10 +763,10 @@ impl<'a> Deserialize<'a> for RawBinding {
return Err(<V::Error as Error>::duplicate_field("key"));
}
let val = map.next_value::<serde_yaml::Value>()?;
let val = map.next_value::<SerdeValue>()?;
if val.is_u64() {
let scancode = val.as_u64().unwrap();
if scancode > u64::from(::std::u32::MAX) {
if scancode > u64::from(std::u32::MAX) {
return Err(<V::Error as Error>::custom(format!(
"Invalid key binding, scancode too big: {}",
scancode
@ -726,7 +799,36 @@ impl<'a> Deserialize<'a> for RawBinding {
return Err(<V::Error as Error>::duplicate_field("action"));
}
action = Some(map.next_value::<Action>()?);
let value = map.next_value::<SerdeValue>()?;
action = if let Ok(vi_action) = ViAction::deserialize(value.clone()) {
Some(vi_action.into())
} else if let Ok(vi_motion) = ViMotion::deserialize(value.clone()) {
Some(vi_motion.into())
} else {
match Action::deserialize(value.clone()).map_err(V::Error::custom) {
Ok(action) => Some(action),
Err(err) => {
let value = match value {
SerdeValue::String(string) => string,
SerdeValue::Mapping(map) if map.len() == 1 => {
match map.into_iter().next() {
Some((
SerdeValue::String(string),
SerdeValue::Null,
)) => string,
_ => return Err(err),
}
},
_ => return Err(err),
};
return Err(V::Error::custom(format!(
"unknown keyboard action `{}`",
value
)));
},
}
};
},
Field::Chars => {
if chars.is_some() {
@ -752,7 +854,21 @@ impl<'a> Deserialize<'a> for RawBinding {
}
}
let mode = mode.unwrap_or_else(TermMode::empty);
let not_mode = not_mode.unwrap_or_else(TermMode::empty);
let mods = mods.unwrap_or_else(ModifiersState::default);
let action = match (action, chars, command) {
(Some(action @ Action::ViMotion(_)), None, None)
| (Some(action @ Action::ViAction(_)), None, None) => {
if !mode.intersects(TermMode::VI) || not_mode.intersects(TermMode::VI) {
return Err(V::Error::custom(format!(
"action `{}` is only available in vi mode, try adding `mode: Vi`",
action,
)));
}
action
},
(Some(action), None, None) => action,
(None, Some(chars), None) => Action::Esc(chars),
(None, None, Some(cmd)) => match cmd {
@ -761,18 +877,13 @@ impl<'a> Deserialize<'a> for RawBinding {
Action::Command(program, args)
},
},
(None, None, None) => {
return Err(V::Error::custom("must specify chars, action or command"));
},
_ => {
return Err(V::Error::custom("must specify only chars, action or command"))
return Err(V::Error::custom(
"must specify exactly one of chars, action or command",
))
},
};
let mode = mode.unwrap_or_else(TermMode::empty);
let not_mode = not_mode.unwrap_or_else(TermMode::empty);
let mods = mods.unwrap_or_else(ModifiersState::default);
if mouse.is_none() && key.is_none() {
return Err(V::Error::custom("bindings require mouse button or key"));
}
@ -781,8 +892,6 @@ impl<'a> Deserialize<'a> for RawBinding {
}
}
const FIELDS: &[&str] = &["key", "mods", "mode", "action", "chars", "mouse", "command"];
deserializer.deserialize_struct("RawBinding", FIELDS, RawBindingVisitor)
}
}
@ -793,7 +902,8 @@ impl<'a> Deserialize<'a> for MouseBinding {
D: Deserializer<'a>,
{
let raw = RawBinding::deserialize(deserializer)?;
raw.into_mouse_binding().map_err(|_| D::Error::custom("expected mouse binding"))
raw.into_mouse_binding()
.map_err(|_| D::Error::custom("expected mouse binding, got key binding"))
}
}
@ -803,7 +913,8 @@ impl<'a> Deserialize<'a> for KeyBinding {
D: Deserializer<'a>,
{
let raw = RawBinding::deserialize(deserializer)?;
raw.into_key_binding().map_err(|_| D::Error::custom("expected key binding"))
raw.into_key_binding()
.map_err(|_| D::Error::custom("expected key binding, got mouse binding"))
}
}
@ -858,7 +969,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper {
type Value = ModsWrapper;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Some subset of Command|Shift|Super|Alt|Option|Control")
f.write_str("a subset of Shift|Control|Super|Command|Alt|Option")
}
fn visit_str<E>(self, value: &str) -> Result<ModsWrapper, E>
@ -873,7 +984,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper {
"alt" | "option" => res.insert(ModifiersState::ALT),
"control" => res.insert(ModifiersState::CTRL),
"none" => (),
_ => error!(target: LOG_TARGET_CONFIG, "Unknown modifier {:?}", modifier),
_ => return Err(E::invalid_value(Unexpected::Str(modifier), &self)),
}
}
@ -899,7 +1010,7 @@ mod tests {
fn default() -> Self {
Self {
mods: Default::default(),
action: Default::default(),
action: Action::None,
mode: TermMode::empty(),
notmode: TermMode::empty(),
trigger: Default::default(),

View File

@ -15,7 +15,7 @@ pub mod monitor;
mod mouse;
mod ui_config;
pub use crate::config::bindings::{Action, Binding, Key};
pub use crate::config::bindings::{Action, Binding, Key, ViAction};
#[cfg(test)]
pub use crate::config::mouse::{ClickHandler, Mouse};
use crate::config::ui_config::UIConfig;

View File

@ -1,6 +1,7 @@
use log::error;
use serde::{Deserialize, Deserializer};
use alacritty_terminal::config::failure_default;
use alacritty_terminal::config::{failure_default, LOG_TARGET_CONFIG};
use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding};
use crate::config::mouse::Mouse;
@ -60,7 +61,18 @@ where
T: Copy + Eq,
Binding<T>: Deserialize<'a>,
{
let mut bindings: Vec<Binding<T>> = failure_default(deserializer)?;
let values = Vec::<serde_yaml::Value>::deserialize(deserializer)?;
// Skip all invalid values
let mut bindings = Vec::with_capacity(values.len());
for value in values {
match Binding::<T>::deserialize(value) {
Ok(binding) => bindings.push(binding),
Err(err) => {
error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; ignoring binding", err);
},
}
}
// Remove matching default bindings
for binding in bindings.iter() {

View File

@ -366,6 +366,12 @@ impl Display {
let selection = !terminal.selection().as_ref().map(Selection::is_empty).unwrap_or(true);
let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE);
let vi_mode_cursor = if terminal.mode().contains(TermMode::VI) {
Some(terminal.vi_mode_cursor)
} else {
None
};
// Update IME position
#[cfg(not(windows))]
self.window.update_ime_position(&terminal, &self.size_info);
@ -419,6 +425,13 @@ impl Display {
}
}
// Highlight URLs at the vi mode cursor position
if let Some(vi_mode_cursor) = vi_mode_cursor {
if let Some(url) = self.urls.find_at(vi_mode_cursor.point) {
rects.append(&mut url.rects(&metrics, &size_info));
}
}
// Push visual bell after url/underline/strikeout rects
if visual_bell_intensity != 0. {
let visual_bell_rect = RenderRect::new(

View File

@ -27,10 +27,10 @@ use alacritty_terminal::event::{Event, EventListener, Notify};
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Column, Line, Point, Side};
use alacritty_terminal::message_bar::{Message, MessageBuffer};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::cell::Cell;
use alacritty_terminal::term::{SizeInfo, Term};
use alacritty_terminal::term::{SizeInfo, Term, TermMode};
#[cfg(not(windows))]
use alacritty_terminal::tty;
use alacritty_terminal::util::{limit, start_daemon};
@ -40,6 +40,7 @@ use crate::config;
use crate::config::Config;
use crate::display::Display;
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
use crate::url::{Url, Urls};
use crate::window::Window;
#[derive(Default, Clone, Debug, PartialEq)]
@ -68,6 +69,7 @@ pub struct ActionContext<'a, N, T> {
pub display_update_pending: &'a mut DisplayUpdate,
pub config: &'a mut Config,
pub event_loop: &'a EventLoopWindowTarget<Event>,
pub urls: &'a Urls,
font_size: &'a mut Size,
}
@ -83,7 +85,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
fn scroll(&mut self, scroll: Scroll) {
self.terminal.scroll_display(scroll);
if let ElementState::Pressed = self.mouse().left_button_state {
// Update selection
if self.terminal.mode().contains(TermMode::VI)
&& self.terminal.selection().as_ref().map(|s| s.is_empty()) != Some(true)
{
self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right);
} else if ElementState::Pressed == self.mouse().left_button_state {
let (x, y) = (self.mouse().x, self.mouse().y);
let size_info = self.size_info();
let point = size_info.pixels_to_coords(x, y);
@ -113,35 +120,35 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
let point = self.terminal.visible_to_buffer(point);
// Update selection if one exists
if let Some(ref mut selection) = self.terminal.selection_mut() {
let vi_mode = self.terminal.mode().contains(TermMode::VI);
if let Some(selection) = self.terminal.selection_mut() {
selection.update(point, side);
if vi_mode {
selection.include_all();
}
self.terminal.dirty = true;
}
}
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::new(ty, point, side));
self.terminal.dirty = true;
}
fn simple_selection(&mut self, point: Point, side: Side) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::simple(point, side));
self.terminal.dirty = true;
}
fn block_selection(&mut self, point: Point, side: Side) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::block(point, side));
self.terminal.dirty = true;
}
fn semantic_selection(&mut self, point: Point) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::semantic(point));
self.terminal.dirty = true;
}
fn line_selection(&mut self, point: Point) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::lines(point));
self.terminal.dirty = true;
fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
match self.terminal.selection_mut() {
Some(selection) if selection.ty == ty && !selection.is_empty() => {
self.clear_selection();
},
Some(selection) if !selection.is_empty() => {
selection.ty = ty;
self.terminal.dirty = true;
},
_ => self.start_selection(ty, point, side),
}
}
fn mouse_coords(&self) -> Option<Point> {
@ -155,6 +162,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
}
#[inline]
fn mouse_mode(&self) -> bool {
self.terminal.mode().intersects(TermMode::MOUSE_MODE)
&& !self.terminal.mode().contains(TermMode::VI)
}
#[inline]
fn mouse_mut(&mut self) -> &mut Mouse {
self.mouse
@ -254,8 +267,32 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
fn event_loop(&self) -> &EventLoopWindowTarget<Event> {
self.event_loop
}
fn urls(&self) -> &Urls {
self.urls
}
/// Spawn URL launcher when clicking on URLs.
fn launch_url(&self, url: Url) {
if self.mouse.block_url_launcher {
return;
}
if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher {
let mut args = launcher.args().to_vec();
let start = self.terminal.visible_to_buffer(url.start());
let end = self.terminal.visible_to_buffer(url.end());
args.push(self.terminal.bounds_to_string(start, end));
match start_daemon(launcher.program(), &args) {
Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args),
Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args),
}
}
}
}
#[derive(Debug)]
pub enum ClickState {
None,
Click,
@ -264,6 +301,7 @@ pub enum ClickState {
}
/// State of the mouse
#[derive(Debug)]
pub struct Mouse {
pub x: usize,
pub y: usize,
@ -412,10 +450,10 @@ impl<N: Notify + OnResize> Processor<N> {
window: &mut self.display.window,
font_size: &mut self.font_size,
config: &mut self.config,
urls: &self.display.urls,
event_loop,
};
let mut processor =
input::Processor::new(context, &self.display.urls, &self.display.highlighted_url);
let mut processor = input::Processor::new(context, &self.display.highlighted_url);
for event in event_queue.drain(..) {
Processor::handle_event(event, &mut processor);

View File

@ -19,8 +19,7 @@
//! needs to be tracked. Additionally, we need a bit of a state machine to
//! determine what to do when a non-modifier key is pressed.
use std::borrow::Cow;
use std::cmp::min;
use std::cmp::Ordering;
use std::cmp::{min, Ordering};
use std::marker::PhantomData;
use std::time::Instant;
@ -40,12 +39,13 @@ use alacritty_terminal::event::{Event, EventListener};
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Column, Line, Point, Side};
use alacritty_terminal::message_bar::{self, Message};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::selection::SelectionType;
use alacritty_terminal::term::mode::TermMode;
use alacritty_terminal::term::{SizeInfo, Term};
use alacritty_terminal::util::start_daemon;
use alacritty_terminal::vi_mode::ViMotion;
use crate::config::{Action, Binding, Config, Key};
use crate::config::{Action, Binding, Config, Key, ViAction};
use crate::event::{ClickState, Mouse};
use crate::url::{Url, Urls};
use crate::window::Window;
@ -59,21 +59,18 @@ pub const FONT_SIZE_STEP: f32 = 0.5;
/// are activated.
pub struct Processor<'a, T: EventListener, A: ActionContext<T>> {
pub ctx: A,
pub urls: &'a Urls,
pub highlighted_url: &'a Option<Url>,
_phantom: PhantomData<T>,
}
pub trait ActionContext<T: EventListener> {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B);
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, data: B);
fn size_info(&self) -> SizeInfo;
fn copy_selection(&mut self, _: ClipboardType);
fn clear_selection(&mut self);
fn copy_selection(&mut self, ty: ClipboardType);
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side);
fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side);
fn update_selection(&mut self, point: Point, side: Side);
fn simple_selection(&mut self, point: Point, side: Side);
fn block_selection(&mut self, point: Point, side: Side);
fn semantic_selection(&mut self, point: Point);
fn line_selection(&mut self, point: Point);
fn clear_selection(&mut self);
fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
@ -93,6 +90,9 @@ pub trait ActionContext<T: EventListener> {
fn message(&self) -> Option<&Message>;
fn config(&self) -> &Config;
fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
fn urls(&self) -> &Urls;
fn launch_url(&self, url: Url);
fn mouse_mode(&self) -> bool;
}
trait Execute<T: EventListener> {
@ -107,6 +107,22 @@ impl<T, U: EventListener> Execute<U> for Binding<T> {
}
}
impl Action {
fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
where
T: EventListener,
A: ActionContext<T>,
{
let cursor_point = ctx.terminal().vi_mode_cursor.point;
ctx.toggle_selection(ty, cursor_point, Side::Left);
// Make sure initial selection is not empty
if let Some(selection) = ctx.terminal_mut().selection_mut() {
selection.include_all();
}
}
}
impl<T: EventListener> Execute<T> for Action {
#[inline]
fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
@ -118,6 +134,11 @@ impl<T: EventListener> Execute<T> for Action {
},
Action::Copy => {
ctx.copy_selection(ClipboardType::Clipboard);
// Clear selection in vi mode for better user feedback
if ctx.terminal().mode().contains(TermMode::VI) {
ctx.clear_selection();
}
},
Action::Paste => {
let text = ctx.terminal_mut().clipboard().load(ClipboardType::Clipboard);
@ -135,6 +156,27 @@ impl<T: EventListener> Execute<T> for Action {
Err(err) => warn!("Couldn't run command {}", err),
}
},
Action::ClearSelection => ctx.clear_selection(),
Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(),
Action::ViAction(ViAction::ToggleNormalSelection) => {
Self::toggle_selection(ctx, SelectionType::Simple)
},
Action::ViAction(ViAction::ToggleLineSelection) => {
Self::toggle_selection(ctx, SelectionType::Lines)
},
Action::ViAction(ViAction::ToggleBlockSelection) => {
Self::toggle_selection(ctx, SelectionType::Block)
},
Action::ViAction(ViAction::ToggleSemanticSelection) => {
Self::toggle_selection(ctx, SelectionType::Semantic)
},
Action::ViAction(ViAction::Open) => {
ctx.mouse_mut().block_url_launcher = false;
if let Some(url) = ctx.urls().find_at(ctx.terminal().vi_mode_cursor.point) {
ctx.launch_url(url);
}
},
Action::ViMotion(motion) => ctx.terminal_mut().vi_motion(motion),
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
#[cfg(target_os = "macos")]
Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(),
@ -147,12 +189,74 @@ impl<T: EventListener> Execute<T> for Action {
Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP),
Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.),
Action::ResetFontSize => ctx.reset_font_size(),
Action::ScrollPageUp => ctx.scroll(Scroll::PageUp),
Action::ScrollPageDown => ctx.scroll(Scroll::PageDown),
Action::ScrollLineUp => ctx.scroll(Scroll::Lines(1)),
Action::ScrollLineDown => ctx.scroll(Scroll::Lines(-1)),
Action::ScrollToTop => ctx.scroll(Scroll::Top),
Action::ScrollToBottom => ctx.scroll(Scroll::Bottom),
Action::ScrollPageUp => {
// Move vi mode cursor
let term = ctx.terminal_mut();
let scroll_lines = term.grid().num_lines().0 as isize;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageUp);
},
Action::ScrollPageDown => {
// Move vi mode cursor
let term = ctx.terminal_mut();
let scroll_lines = -(term.grid().num_lines().0 as isize);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageDown);
},
Action::ScrollHalfPageUp => {
// Move vi mode cursor
let term = ctx.terminal_mut();
let scroll_lines = term.grid().num_lines().0 as isize / 2;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Lines(scroll_lines));
},
Action::ScrollHalfPageDown => {
// Move vi mode cursor
let term = ctx.terminal_mut();
let scroll_lines = -(term.grid().num_lines().0 as isize / 2);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Lines(scroll_lines));
},
Action::ScrollLineUp => {
// Move vi mode cursor
let term = ctx.terminal();
if term.grid().display_offset() != term.grid().history_size()
&& term.vi_mode_cursor.point.line + 1 != term.grid().num_lines()
{
ctx.terminal_mut().vi_mode_cursor.point.line += 1;
}
ctx.scroll(Scroll::Lines(1));
},
Action::ScrollLineDown => {
// Move vi mode cursor
if ctx.terminal().grid().display_offset() != 0
&& ctx.terminal().vi_mode_cursor.point.line.0 != 0
{
ctx.terminal_mut().vi_mode_cursor.point.line -= 1;
}
ctx.scroll(Scroll::Lines(-1));
},
Action::ScrollToTop => {
ctx.scroll(Scroll::Top);
// Move vi mode cursor
ctx.terminal_mut().vi_mode_cursor.point.line = Line(0);
ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied);
},
Action::ScrollToBottom => {
ctx.scroll(Scroll::Bottom);
// Move vi mode cursor
let term = ctx.terminal_mut();
term.vi_mode_cursor.point.line = term.grid().num_lines() - 1;
term.vi_motion(ViMotion::FirstOccupied);
},
Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved),
Action::ClearLogNotice => ctx.pop_message(),
Action::SpawnNewInstance => ctx.spawn_new_instance(),
@ -197,8 +301,8 @@ impl From<MouseState> for CursorIcon {
}
impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
pub fn new(ctx: A, urls: &'a Urls, highlighted_url: &'a Option<Url>) -> Self {
Self { ctx, urls, highlighted_url, _phantom: Default::default() }
pub fn new(ctx: A, highlighted_url: &'a Option<Url>) -> Self {
Self { ctx, highlighted_url, _phantom: Default::default() }
}
#[inline]
@ -238,12 +342,16 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
let last_term_line = self.ctx.terminal().grid().num_lines() - 1;
if self.ctx.mouse().left_button_state == ElementState::Pressed
&& (self.ctx.modifiers().shift()
|| !self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE))
&& (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
{
// Treat motion over message bar like motion over the last line
let line = min(point.line, last_term_line);
// Move vi mode cursor to mouse cursor position
if self.ctx.terminal().mode().contains(TermMode::VI) {
self.ctx.terminal_mut().vi_mode_cursor.point = point;
}
self.ctx.update_selection(Point { line, col: point.col }, cell_side);
} else if inside_grid
&& cell_changed
@ -354,13 +462,15 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn on_mouse_double_click(&mut self, button: MouseButton, point: Point) {
if button == MouseButton::Left {
self.ctx.semantic_selection(point);
let side = self.ctx.mouse().cell_side;
self.ctx.start_selection(SelectionType::Semantic, point, side);
}
}
fn on_mouse_triple_click(&mut self, button: MouseButton, point: Point) {
if button == MouseButton::Left {
self.ctx.line_selection(point);
let side = self.ctx.mouse().cell_side;
self.ctx.start_selection(SelectionType::Lines, point, side);
}
}
@ -402,14 +512,18 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Start new empty selection
let side = self.ctx.mouse().cell_side;
if self.ctx.modifiers().ctrl() {
self.ctx.block_selection(point, side);
self.ctx.start_selection(SelectionType::Block, point, side);
} else {
self.ctx.simple_selection(point, side);
self.ctx.start_selection(SelectionType::Simple, point, side);
}
if !self.ctx.modifiers().shift()
&& self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)
{
// Move vi mode cursor to mouse position
if self.ctx.terminal().mode().contains(TermMode::VI) {
// Update vi mode cursor position on click
self.ctx.terminal_mut().vi_mode_cursor.point = point;
}
if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
let code = match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
@ -427,9 +541,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
}
fn on_mouse_release(&mut self, button: MouseButton) {
if !self.ctx.modifiers().shift()
&& self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)
{
if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
let code = match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
@ -440,31 +552,12 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
self.mouse_report(code, ElementState::Released);
return;
} else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) {
self.launch_url(url);
self.ctx.launch_url(url);
}
self.copy_selection();
}
/// Spawn URL launcher when clicking on URLs.
fn launch_url(&self, url: Url) {
if self.ctx.mouse().block_url_launcher {
return;
}
if let Some(ref launcher) = self.ctx.config().ui_config.mouse.url.launcher {
let mut args = launcher.args().to_vec();
let start = self.ctx.terminal().visible_to_buffer(url.start());
let end = self.ctx.terminal().visible_to_buffer(url.end());
args.push(self.ctx.terminal().bounds_to_string(start, end));
match start_daemon(launcher.program(), &args) {
Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args),
Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args),
}
}
}
pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
@ -489,7 +582,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn scroll_terminal(&mut self, new_scroll_px: f64) {
let height = f64::from(self.ctx.size_info().cell_height);
if self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) {
if self.ctx.mouse_mode() {
self.ctx.mouse_mut().scroll_px += new_scroll_px;
let code = if new_scroll_px > 0. { 64 } else { 65 };
@ -530,7 +623,22 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
let lines = self.ctx.mouse().scroll_px / height;
// Store absolute position of vi mode cursor
let term = self.ctx.terminal();
let absolute = term.visible_to_buffer(term.vi_mode_cursor.point);
self.ctx.scroll(Scroll::Lines(lines as isize));
// Try to restore vi mode cursor position, to keep it above its previous content
let term = self.ctx.terminal_mut();
term.vi_mode_cursor.point = term.grid().clamp_buffer_to_visible(absolute);
term.vi_mode_cursor.point.col = absolute.col;
// Update selection
let point = term.vi_mode_cursor.point;
if !self.ctx.selection_is_empty() {
self.ctx.update_selection(point, Side::Right);
}
}
self.ctx.mouse_mut().scroll_px %= height;
@ -560,7 +668,6 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Reset cursor when message bar height changed or all messages are gone
let size = self.ctx.size_info();
let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE);
let current_lines = (size.lines() - self.ctx.terminal().grid().num_lines()).0;
let new_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0);
@ -568,7 +675,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
Ordering::Less => CursorIcon::Default,
Ordering::Equal => CursorIcon::Hand,
Ordering::Greater => {
if mouse_mode {
if self.ctx.mouse_mode() {
CursorIcon::Default
} else {
CursorIcon::Text
@ -613,7 +720,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
/// Process a received character.
pub fn received_char(&mut self, c: char) {
if *self.ctx.suppress_chars() {
if *self.ctx.suppress_chars() || self.ctx.terminal().mode().contains(TermMode::VI) {
return;
}
@ -685,7 +792,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn process_mouse_bindings(&mut self, button: MouseButton) {
let mods = *self.ctx.modifiers();
let mode = *self.ctx.terminal().mode();
let mouse_mode = mode.intersects(TermMode::MOUSE_MODE);
let mouse_mode = self.ctx.mouse_mode();
for i in 0..self.ctx.config().ui_config.mouse_bindings.len() {
let mut binding = self.ctx.config().ui_config.mouse_bindings[i].clone();
@ -743,22 +850,24 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
return MouseState::MessageBar;
}
let mouse_mode = self.ctx.mouse_mode();
// Check for URL at mouse cursor
let mods = *self.ctx.modifiers();
let selection =
!self.ctx.terminal().selection().as_ref().map(Selection::is_empty).unwrap_or(true);
let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE);
let highlighted_url =
self.urls.highlighted(self.ctx.config(), self.ctx.mouse(), mods, mouse_mode, selection);
let highlighted_url = self.ctx.urls().highlighted(
self.ctx.config(),
self.ctx.mouse(),
mods,
mouse_mode,
!self.ctx.selection_is_empty(),
);
if let Some(url) = highlighted_url {
return MouseState::Url(url);
}
// Check mouse mode if location is not special
if self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)
&& !self.ctx.modifiers().shift()
{
if !self.ctx.modifiers().shift() && mouse_mode {
MouseState::Mouse
} else {
MouseState::Text
@ -781,12 +890,12 @@ mod tests {
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Point, Side};
use alacritty_terminal::message_bar::{Message, MessageBuffer};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::term::{SizeInfo, Term, TermMode};
use crate::config::{ClickHandler, Config};
use crate::event::{ClickState, Mouse};
use crate::url::Urls;
use crate::url::{Url, Urls};
use crate::window::Window;
use super::{Action, Binding, Processor};
@ -799,7 +908,7 @@ mod tests {
fn send_event(&self, _event: TerminalEvent) {}
}
#[derive(PartialEq)]
#[derive(Debug, PartialEq)]
enum MultiClick {
DoubleClick,
TripleClick,
@ -824,9 +933,15 @@ mod tests {
fn update_selection(&mut self, _point: Point, _side: Side) {}
fn simple_selection(&mut self, _point: Point, _side: Side) {}
fn start_selection(&mut self, ty: SelectionType, _point: Point, _side: Side) {
match ty {
SelectionType::Semantic => self.last_action = MultiClick::DoubleClick,
SelectionType::Lines => self.last_action = MultiClick::TripleClick,
_ => (),
}
}
fn block_selection(&mut self, _point: Point, _side: Side) {}
fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn copy_selection(&mut self, _: ClipboardType) {}
@ -850,15 +965,6 @@ mod tests {
*self.size_info
}
fn semantic_selection(&mut self, _point: Point) {
// set something that we can check for here
self.last_action = MultiClick::DoubleClick;
}
fn line_selection(&mut self, _point: Point) {
self.last_action = MultiClick::TripleClick;
}
fn selection_is_empty(&self) -> bool {
true
}
@ -878,6 +984,10 @@ mod tests {
}
}
fn mouse_mode(&self) -> bool {
false
}
#[inline]
fn mouse_mut(&mut self) -> &mut Mouse {
self.mouse
@ -923,6 +1033,14 @@ mod tests {
fn event_loop(&self) -> &EventLoopWindowTarget<TerminalEvent> {
unimplemented!();
}
fn urls(&self) -> &Urls {
unimplemented!();
}
fn launch_url(&self, _: Url) {
unimplemented!();
}
}
macro_rules! test_clickstate {
@ -981,8 +1099,7 @@ mod tests {
config: &cfg,
};
let urls = Urls::new();
let mut processor = Processor::new(context, &urls, &None);
let mut processor = Processor::new(context, &None);
let event: Event::<'_, TerminalEvent> = $input;
if let Event::WindowEvent {

View File

@ -147,6 +147,7 @@ impl Urls {
url.end_offset = end_offset;
}
/// Find URL below the mouse cursor.
pub fn highlighted(
&self,
config: &Config,
@ -171,12 +172,16 @@ impl Urls {
return None;
}
self.find_at(Point::new(mouse.line, mouse.column))
}
/// Find URL at location.
pub fn find_at(&self, point: Point) -> Option<Url> {
for url in &self.urls {
if (url.start()..=url.end()).contains(&Point::new(mouse.line, mouse.column)) {
if (url.start()..=url.end()).contains(&point) {
return Some(url.clone());
}
}
None
}

View File

@ -12,6 +12,8 @@ pub struct Colors {
#[serde(deserialize_with = "failure_default")]
pub cursor: CursorColors,
#[serde(deserialize_with = "failure_default")]
pub vi_mode_cursor: CursorColors,
#[serde(deserialize_with = "failure_default")]
pub selection: SelectionColors,
#[serde(deserialize_with = "failure_default")]
normal: NormalColors,

View File

@ -28,7 +28,7 @@ mod scrolling;
mod visual_bell;
mod window;
use crate::ansi::{Color, CursorStyle, NamedColor};
use crate::ansi::{CursorStyle, NamedColor};
pub use crate::config::colors::Colors;
pub use crate::config::debug::Debug;
@ -170,16 +170,28 @@ impl<T> Config<T> {
self.dynamic_title.0
}
/// Cursor foreground color
/// Cursor foreground color.
#[inline]
pub fn cursor_text_color(&self) -> Option<Rgb> {
self.colors.cursor.text
}
/// Cursor background color
/// Cursor background color.
#[inline]
pub fn cursor_cursor_color(&self) -> Option<Color> {
self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor))
pub fn cursor_cursor_color(&self) -> Option<NamedColor> {
self.colors.cursor.cursor.map(|_| NamedColor::Cursor)
}
/// Vi mode cursor foreground color.
#[inline]
pub fn vi_mode_cursor_text_color(&self) -> Option<Rgb> {
self.colors.vi_mode_cursor.text
}
/// Vi mode cursor background color.
#[inline]
pub fn vi_mode_cursor_cursor_color(&self) -> Option<Rgb> {
self.colors.vi_mode_cursor.cursor
}
#[inline]
@ -230,20 +242,16 @@ impl Default for EscapeChars {
}
#[serde(default)]
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
#[derive(Deserialize, Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Cursor {
#[serde(deserialize_with = "failure_default")]
pub style: CursorStyle,
#[serde(deserialize_with = "option_explicit_none")]
pub vi_mode_style: Option<CursorStyle>,
#[serde(deserialize_with = "failure_default")]
unfocused_hollow: DefaultTrueBool,
}
impl Default for Cursor {
fn default() -> Self {
Self { style: Default::default(), unfocused_hollow: Default::default() }
}
}
impl Cursor {
pub fn unfocused_hollow(self) -> bool {
self.unfocused_hollow.0

View File

@ -264,11 +264,9 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
let mut new_empty_lines = 0;
let mut reversed: Vec<Row<T>> = Vec::with_capacity(self.raw.len());
for (i, mut row) in self.raw.drain().enumerate().rev() {
// FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Check if reflowing shoud be performed
let mut last_row = reversed.last_mut();
let last_row = match last_row {
Some(ref mut last_row) if should_reflow(last_row) => last_row,
let last_row = match reversed.last_mut() {
Some(last_row) if should_reflow(last_row) => last_row,
_ => {
reversed.push(row);
continue;
@ -356,11 +354,9 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
}
loop {
// FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Check if reflowing shoud be performed
let wrapped = row.shrink(cols);
let mut wrapped = match wrapped {
Some(_) if reflow => wrapped.unwrap(),
let mut wrapped = match row.shrink(cols) {
Some(wrapped) if reflow => wrapped,
_ => {
new_raw.push(row);
break;

View File

@ -30,6 +30,15 @@ pub enum Side {
Right,
}
impl Side {
pub fn opposite(self) -> Self {
match self {
Side::Right => Side::Left,
Side::Left => Side::Right,
}
}
}
/// Index in the grid using row, column notation
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)]
pub struct Point<L = Line> {
@ -49,7 +58,7 @@ impl<L> Point<L> {
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
let line_changes =
f32::ceil(rhs.saturating_sub(self.col.0) as f32 / num_cols as f32) as usize;
(rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize;
if self.line.into() > Line(line_changes) {
self.line = self.line - line_changes;
} else {
@ -63,12 +72,40 @@ impl<L> Point<L> {
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn add(mut self, num_cols: usize, rhs: usize) -> Point<L>
where
L: Add<usize, Output = L> + Sub<usize, Output = L>,
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
self.line = self.line + (rhs + self.col.0) / num_cols;
self.col = Column((self.col.0 + rhs) % num_cols);
self
}
#[inline]
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn sub_absolute(mut self, num_cols: usize, rhs: usize) -> Point<L>
where
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
self.line =
self.line + (rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize;
self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols);
self
}
#[inline]
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn add_absolute(mut self, num_cols: usize, rhs: usize) -> Point<L>
where
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
let line_changes = (rhs + self.col.0) / num_cols;
if self.line.into() > Line(line_changes) {
self.line = self.line - line_changes;
} else {
self.line = Default::default();
}
self.col = Column((self.col.0 + rhs) % num_cols);
self
}
}
impl Ord for Point {

View File

@ -37,6 +37,7 @@ pub mod sync;
pub mod term;
pub mod tty;
pub mod util;
pub mod vi_mode;
pub use crate::grid::Grid;
pub use crate::term::Term;

View File

@ -27,7 +27,7 @@ use crate::term::{Search, Term};
/// A Point and side within that point.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Anchor {
struct Anchor {
point: Point<usize>,
side: Side,
}
@ -67,7 +67,7 @@ impl<L> SelectionRange<L> {
/// Different kinds of selection.
#[derive(Debug, Copy, Clone, PartialEq)]
enum SelectionType {
pub enum SelectionType {
Simple,
Block,
Semantic,
@ -94,48 +94,20 @@ enum SelectionType {
/// [`update`]: enum.Selection.html#method.update
#[derive(Debug, Clone, PartialEq)]
pub struct Selection {
pub ty: SelectionType,
region: Range<Anchor>,
ty: SelectionType,
}
impl Selection {
pub fn simple(location: Point<usize>, side: Side) -> Selection {
pub fn new(ty: SelectionType, location: Point<usize>, side: Side) -> Selection {
Self {
region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) },
ty: SelectionType::Simple,
ty,
}
}
pub fn block(location: Point<usize>, side: Side) -> Selection {
Self {
region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) },
ty: SelectionType::Block,
}
}
pub fn semantic(location: Point<usize>) -> Selection {
Self {
region: Range {
start: Anchor::new(location, Side::Left),
end: Anchor::new(location, Side::Right),
},
ty: SelectionType::Semantic,
}
}
pub fn lines(location: Point<usize>) -> Selection {
Self {
region: Range {
start: Anchor::new(location, Side::Left),
end: Anchor::new(location, Side::Right),
},
ty: SelectionType::Lines,
}
}
pub fn update(&mut self, location: Point<usize>, side: Side) {
self.region.end.point = location;
self.region.end.side = side;
pub fn update(&mut self, point: Point<usize>, side: Side) {
self.region.end = Anchor::new(point, side);
}
pub fn rotate(
@ -233,6 +205,24 @@ impl Selection {
}
}
/// Expand selection sides to include all cells.
pub fn include_all(&mut self) {
let (start, end) = (self.region.start.point, self.region.end.point);
let (start_side, end_side) = match self.ty {
SelectionType::Block
if start.col > end.col || (start.col == end.col && start.line < end.line) =>
{
(Side::Right, Side::Left)
},
SelectionType::Block => (Side::Left, Side::Right),
_ if Self::points_need_swap(start, end) => (Side::Right, Side::Left),
_ => (Side::Left, Side::Right),
};
self.region.start.side = start_side;
self.region.end.side = end_side;
}
/// Convert selection to grid coordinates.
pub fn to_range<T>(&self, term: &Term<T>) -> Option<SelectionRange> {
let grid = term.grid();
@ -392,7 +382,8 @@ impl Selection {
/// look like [ B] and [E ].
#[cfg(test)]
mod tests {
use super::{Selection, SelectionRange};
use super::*;
use crate::clipboard::Clipboard;
use crate::config::MockConfig;
use crate::event::{Event, EventListener};
@ -425,7 +416,7 @@ mod tests {
#[test]
fn single_cell_left_to_right() {
let location = Point { line: 0, col: Column(0) };
let mut selection = Selection::simple(location, Side::Left);
let mut selection = Selection::new(SelectionType::Simple, location, Side::Left);
selection.update(location, Side::Right);
assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange {
@ -443,7 +434,7 @@ mod tests {
#[test]
fn single_cell_right_to_left() {
let location = Point { line: 0, col: Column(0) };
let mut selection = Selection::simple(location, Side::Right);
let mut selection = Selection::new(SelectionType::Simple, location, Side::Right);
selection.update(location, Side::Left);
assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange {
@ -460,7 +451,8 @@ mod tests {
/// 3. [ B][E ]
#[test]
fn between_adjacent_cells_left_to_right() {
let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right);
selection.update(Point::new(0, Column(1)), Side::Left);
assert_eq!(selection.to_range(&term(2, 1)), None);
@ -473,7 +465,8 @@ mod tests {
/// 3. [ E][B ]
#[test]
fn between_adjacent_cells_right_to_left() {
let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Left);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Left);
selection.update(Point::new(0, Column(0)), Side::Right);
assert_eq!(selection.to_range(&term(2, 1)), None);
@ -489,7 +482,8 @@ mod tests {
/// [XX][XE][ ][ ][ ]
#[test]
fn across_adjacent_lines_upward_final_cell_exclusive() {
let mut selection = Selection::simple(Point::new(1, Column(1)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(1, Column(1)), Side::Right);
selection.update(Point::new(0, Column(1)), Side::Right);
assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange {
@ -511,7 +505,8 @@ mod tests {
/// [XX][XB][ ][ ][ ]
#[test]
fn selection_bigger_then_smaller() {
let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Right);
selection.update(Point::new(1, Column(1)), Side::Right);
selection.update(Point::new(1, Column(0)), Side::Right);
@ -526,7 +521,8 @@ mod tests {
fn line_selection() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::lines(Point::new(0, Column(1)));
let mut selection =
Selection::new(SelectionType::Lines, Point::new(0, Column(1)), Side::Left);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@ -541,7 +537,8 @@ mod tests {
fn semantic_selection() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::semantic(Point::new(0, Column(3)));
let mut selection =
Selection::new(SelectionType::Semantic, Point::new(0, Column(3)), Side::Left);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@ -556,7 +553,8 @@ mod tests {
fn simple_selection() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::simple(Point::new(0, Column(3)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(0, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@ -571,7 +569,8 @@ mod tests {
fn block_selection() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::block(Point::new(0, Column(3)), Side::Right);
let mut selection =
Selection::new(SelectionType::Block, Point::new(0, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@ -584,7 +583,8 @@ mod tests {
#[test]
fn simple_is_empty() {
let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right);
assert!(selection.is_empty());
selection.update(Point::new(0, Column(1)), Side::Left);
assert!(selection.is_empty());
@ -594,7 +594,8 @@ mod tests {
#[test]
fn block_is_empty() {
let mut selection = Selection::block(Point::new(0, Column(0)), Side::Right);
let mut selection =
Selection::new(SelectionType::Block, Point::new(0, Column(0)), Side::Right);
assert!(selection.is_empty());
selection.update(Point::new(0, Column(1)), Side::Left);
assert!(selection.is_empty());
@ -612,7 +613,8 @@ mod tests {
fn rotate_in_region_up() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::simple(Point::new(2, Column(3)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(2, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap();
@ -628,7 +630,8 @@ mod tests {
fn rotate_in_region_down() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::simple(Point::new(5, Column(3)), Side::Right);
let mut selection =
Selection::new(SelectionType::Simple, Point::new(5, Column(3)), Side::Right);
selection.update(Point::new(8, Column(1)), Side::Left);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), -5).unwrap();
@ -644,7 +647,8 @@ mod tests {
fn rotate_in_region_up_block() {
let num_lines = 10;
let num_cols = 5;
let mut selection = Selection::block(Point::new(2, Column(3)), Side::Right);
let mut selection =
Selection::new(SelectionType::Block, Point::new(2, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap();

View File

@ -31,10 +31,11 @@ use crate::event::{Event, EventListener};
use crate::grid::{
BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll,
};
use crate::index::{self, Column, IndexRange, Line, Point};
use crate::index::{self, Column, IndexRange, Line, Point, Side};
use crate::selection::{Selection, SelectionRange};
use crate::term::cell::{Cell, Flags, LineLength};
use crate::term::color::Rgb;
use crate::vi_mode::{ViModeCursor, ViMotion};
pub mod cell;
pub mod color;
@ -180,7 +181,17 @@ impl<T> Search for Term<T> {
}
}
/// A key for caching cursor glyphs
/// Cursor storing all information relevant for rendering.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
struct RenderableCursor {
text_color: Option<Rgb>,
cursor_color: Option<Rgb>,
key: CursorKey,
point: Point,
rendered: bool,
}
/// A key for caching cursor glyphs.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
pub struct CursorKey {
pub style: CursorStyle,
@ -198,10 +209,7 @@ pub struct CursorKey {
pub struct RenderableCellsIter<'a, C> {
inner: DisplayIter<'a, Cell>,
grid: &'a Grid<Cell>,
cursor: &'a Point,
cursor_offset: usize,
cursor_key: Option<CursorKey>,
cursor_style: CursorStyle,
cursor: RenderableCursor,
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> {
term: &'b Term<T>,
config: &'b Config<C>,
selection: Option<SelectionRange>,
mut cursor_style: CursorStyle,
) -> RenderableCellsIter<'b, C> {
let grid = &term.grid;
let num_cols = grid.num_cols();
let cursor_offset = grid.num_lines().0 - term.cursor.point.line.0 - 1;
let inner = grid.display_iter();
let selection_range = selection.and_then(|span| {
@ -242,29 +248,13 @@ impl<'a, C> RenderableCellsIter<'a, C> {
Some(SelectionRange::new(start, end, span.is_block))
});
// Load cursor glyph
let cursor = &term.cursor.point;
let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor);
let cursor_key = if cursor_visible {
let is_wide =
grid[cursor].flags.contains(Flags::WIDE_CHAR) && (cursor.col + 1) < num_cols;
Some(CursorKey { style: cursor_style, is_wide })
} else {
// Use hidden cursor so text will not get inverted
cursor_style = CursorStyle::Hidden;
None
};
RenderableCellsIter {
cursor,
cursor_offset,
cursor: term.renderable_cursor(config),
grid,
inner,
selection: selection_range,
config,
colors: &term.colors,
cursor_key,
cursor_style,
}
}
@ -275,6 +265,18 @@ impl<'a, C> RenderableCellsIter<'a, C> {
None => return false,
};
// Do not invert block cursor at selection boundaries
if self.cursor.key.style == CursorStyle::Block
&& self.cursor.point == point
&& (selection.start == point
|| selection.end == point
|| (selection.is_block
&& ((selection.start.line == point.line && selection.end.col == point.col)
|| (selection.end.line == point.line && selection.start.col == point.col))))
{
return false;
}
// Point itself is selected
if selection.contains(point.col, point.line) {
return true;
@ -442,43 +444,46 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col {
let selected = self.is_selected(Point::new(self.cursor.line, self.cursor.col));
if self.cursor.point.line == self.inner.line()
&& self.cursor.point.col == self.inner.column()
{
let selected = self.is_selected(self.cursor.point);
// Handle cursor
if let Some(cursor_key) = self.cursor_key.take() {
let cell = Indexed {
inner: self.grid[self.cursor],
column: self.cursor.col,
// Using `self.cursor.line` leads to inconsitent cursor position when
// scrolling. See https://github.com/alacritty/alacritty/issues/2570 for more
// info.
line: self.inner.line(),
};
let mut renderable_cell =
RenderableCell::new(self.config, self.colors, cell, selected);
renderable_cell.inner = RenderableCellContent::Cursor(cursor_key);
if let Some(color) = self.config.cursor_cursor_color() {
renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color);
}
return Some(renderable_cell);
} else {
// Handle cell below cursor
if self.cursor.rendered {
let mut cell =
RenderableCell::new(self.config, self.colors, self.inner.next()?, selected);
if self.cursor_style == CursorStyle::Block {
std::mem::swap(&mut cell.bg, &mut cell.fg);
if self.cursor.key.style == CursorStyle::Block {
mem::swap(&mut cell.bg, &mut cell.fg);
if let Some(color) = self.config.cursor_text_color() {
if let Some(color) = self.cursor.text_color {
cell.fg = color;
}
}
return Some(cell);
} else {
// Handle cursor
self.cursor.rendered = true;
let buffer_point = self.grid.visible_to_buffer(self.cursor.point);
let cell = Indexed {
inner: self.grid[buffer_point.line][buffer_point.col],
column: self.cursor.point.col,
line: self.cursor.point.line,
};
let mut renderable_cell =
RenderableCell::new(self.config, self.colors, cell, selected);
renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key);
if let Some(color) = self.cursor.cursor_color {
renderable_cell.fg = color;
}
return Some(renderable_cell);
}
} else {
let cell = self.inner.next()?;
@ -497,26 +502,27 @@ pub mod mode {
use bitflags::bitflags;
bitflags! {
pub struct TermMode: u16 {
const SHOW_CURSOR = 0b0000_0000_0000_0001;
const APP_CURSOR = 0b0000_0000_0000_0010;
const APP_KEYPAD = 0b0000_0000_0000_0100;
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_1000;
const BRACKETED_PASTE = 0b0000_0000_0001_0000;
const SGR_MOUSE = 0b0000_0000_0010_0000;
const MOUSE_MOTION = 0b0000_0000_0100_0000;
const LINE_WRAP = 0b0000_0000_1000_0000;
const LINE_FEED_NEW_LINE = 0b0000_0001_0000_0000;
const ORIGIN = 0b0000_0010_0000_0000;
const INSERT = 0b0000_0100_0000_0000;
const FOCUS_IN_OUT = 0b0000_1000_0000_0000;
const ALT_SCREEN = 0b0001_0000_0000_0000;
const MOUSE_DRAG = 0b0010_0000_0000_0000;
const MOUSE_MODE = 0b0010_0000_0100_1000;
const UTF8_MOUSE = 0b0100_0000_0000_0000;
const ALTERNATE_SCROLL = 0b1000_0000_0000_0000;
const ANY = 0b1111_1111_1111_1111;
pub struct TermMode: u32 {
const NONE = 0;
const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
const APP_CURSOR = 0b0000_0000_0000_0000_0010;
const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
const LINE_WRAP = 0b0000_0000_0000_1000_0000;
const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
const ORIGIN = 0b0000_0000_0010_0000_0000;
const INSERT = 0b0000_0000_0100_0000_0000;
const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
const VI = 0b0001_0000_0000_0000_0000;
const ANY = std::u32::MAX;
}
}
@ -730,113 +736,28 @@ impl VisualBell {
}
}
pub struct Term<T> {
/// Terminal focus
pub is_focused: bool,
/// The grid
grid: Grid<Cell>,
/// Tracks if the next call to input will need to first handle wrapping.
/// This is true after the last column is set with the input function. Any function that
/// implicitly sets the line or column needs to set this to false to avoid wrapping twice.
/// input_needs_wrap ensures that cursor.col is always valid for use into indexing into
/// arrays. Without it we would have to sanitize cursor.col every time we used it.
input_needs_wrap: bool,
/// Alternate grid
alt_grid: Grid<Cell>,
/// Alt is active
alt: bool,
/// The cursor
cursor: Cursor,
/// The graphic character set, out of `charsets`, which ASCII is currently
/// being mapped to
active_charset: CharsetIndex,
/// Tabstops
tabs: TabStops,
/// Mode flags
mode: TermMode,
/// Scroll region.
///
/// Range going from top to bottom of the terminal, indexed from the top of the viewport.
scroll_region: Range<Line>,
pub dirty: bool,
pub visual_bell: VisualBell,
/// Saved cursor from main grid
cursor_save: Cursor,
/// Saved cursor from alt grid
cursor_save_alt: Cursor,
semantic_escape_chars: String,
/// Colors used for rendering
colors: color::List,
/// Is color in `colors` modified or not
color_modified: [bool; color::COUNT],
/// Original colors from config
original_colors: color::List,
/// Current style of the cursor
cursor_style: Option<CursorStyle>,
/// Default style for resetting the cursor
default_cursor_style: CursorStyle,
/// Clipboard access coupled to the active window
clipboard: Clipboard,
/// Proxy for sending events to the event loop
event_proxy: T,
/// Current title of the window.
title: Option<String>,
/// Default title for resetting it.
default_title: String,
/// Whether to permit updating the terminal title.
dynamic_title: bool,
/// Stack of saved window titles. When a title is popped from this stack, the `title` for the
/// term is set, and the Glutin window's title attribute is changed through the event listener.
title_stack: Vec<Option<String>>,
}
/// Terminal size info
/// Terminal size info.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
pub struct SizeInfo {
/// Terminal window width
/// Terminal window width.
pub width: f32,
/// Terminal window height
/// Terminal window height.
pub height: f32,
/// Width of individual cell
/// Width of individual cell.
pub cell_width: f32,
/// Height of individual cell
/// Height of individual cell.
pub cell_height: f32,
/// Horizontal window padding
/// Horizontal window padding.
pub padding_x: f32,
/// Horizontal window padding
/// Horizontal window padding.
pub padding_y: f32,
/// DPI factor of the current window
/// DPI factor of the current window.
#[serde(default)]
pub dpr: f64,
}
@ -873,6 +794,96 @@ impl SizeInfo {
}
}
pub struct Term<T> {
/// Terminal focus.
pub is_focused: bool,
/// The grid.
grid: Grid<Cell>,
/// Tracks if the next call to input will need to first handle wrapping.
/// This is true after the last column is set with the input function. Any function that
/// implicitly sets the line or column needs to set this to false to avoid wrapping twice.
/// input_needs_wrap ensures that cursor.col is always valid for use into indexing into
/// arrays. Without it we would have to sanitize cursor.col every time we used it.
input_needs_wrap: bool,
/// Alternate grid.
alt_grid: Grid<Cell>,
/// Alt is active.
alt: bool,
/// The cursor.
cursor: Cursor,
/// Cursor location for vi mode.
pub vi_mode_cursor: ViModeCursor,
/// Index into `charsets`, pointing to what ASCII is currently being mapped to.
active_charset: CharsetIndex,
/// Tabstops.
tabs: TabStops,
/// Mode flags.
mode: TermMode,
/// Scroll region.
///
/// Range going from top to bottom of the terminal, indexed from the top of the viewport.
scroll_region: Range<Line>,
pub dirty: bool,
pub visual_bell: VisualBell,
/// Saved cursor from main grid.
cursor_save: Cursor,
/// Saved cursor from alt grid.
cursor_save_alt: Cursor,
semantic_escape_chars: String,
/// Colors used for rendering.
colors: color::List,
/// Is color in `colors` modified or not.
color_modified: [bool; color::COUNT],
/// Original colors from config.
original_colors: color::List,
/// Current style of the cursor.
cursor_style: Option<CursorStyle>,
/// Default style for resetting the cursor.
default_cursor_style: CursorStyle,
/// Style of the vi mode cursor.
vi_mode_cursor_style: Option<CursorStyle>,
/// Clipboard access coupled to the active window
clipboard: Clipboard,
/// Proxy for sending events to the event loop.
event_proxy: T,
/// Current title of the window.
title: Option<String>,
/// Default title for resetting it.
default_title: String,
/// Whether to permit updating the terminal title.
dynamic_title: bool,
/// Stack of saved window titles. When a title is popped from this stack, the `title` for the
/// term is set, and the Glutin window's title attribute is changed through the event listener.
title_stack: Vec<Option<String>>,
}
impl<T> Term<T> {
pub fn selection(&self) -> &Option<Selection> {
&self.grid.selection
@ -920,6 +931,7 @@ impl<T> Term<T> {
alt: false,
active_charset: Default::default(),
cursor: Default::default(),
vi_mode_cursor: Default::default(),
cursor_save: Default::default(),
cursor_save_alt: Default::default(),
tabs,
@ -931,6 +943,7 @@ impl<T> Term<T> {
semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(),
cursor_style: None,
default_cursor_style: config.cursor.style,
vi_mode_cursor_style: config.cursor.vi_mode_style,
dynamic_title: config.dynamic_title(),
clipboard,
event_proxy,
@ -959,6 +972,7 @@ impl<T> Term<T> {
self.mode.remove(TermMode::ALTERNATE_SCROLL);
}
self.default_cursor_style = config.cursor.style;
self.vi_mode_cursor_style = config.cursor.vi_mode_style;
self.default_title = config.window.title.clone();
self.dynamic_title = config.dynamic_title();
@ -1105,13 +1119,7 @@ impl<T> Term<T> {
pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> RenderableCellsIter<'_, C> {
let selection = self.grid.selection.as_ref().and_then(|s| s.to_range(self));
let cursor = if self.is_focused || !config.cursor.unfocused_hollow() {
self.cursor_style.unwrap_or(self.default_cursor_style)
} else {
CursorStyle::HollowBlock
};
RenderableCellsIter::new(&self, config, selection, cursor)
RenderableCellsIter::new(&self, config, selection)
}
/// Resize terminal to new dimensions
@ -1129,12 +1137,12 @@ impl<T> Term<T> {
self.grid.selection = None;
self.alt_grid.selection = None;
// Should not allow less than 1 col, causes all sorts of checks to be required.
// Should not allow less than 2 cols, causes all sorts of checks to be required.
if num_cols <= Column(1) {
num_cols = Column(2);
}
// Should not allow less than 1 line, causes all sorts of checks to be required.
// Should not allow less than 2 lines, causes all sorts of checks to be required.
if num_lines <= Line(1) {
num_lines = Line(2);
}
@ -1178,6 +1186,8 @@ impl<T> Term<T> {
self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1);
self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1);
self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1);
self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1);
self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1);
// Recreate tabs list
self.tabs.resize(self.grid.num_cols());
@ -1200,7 +1210,7 @@ impl<T> Term<T> {
}
self.alt = !self.alt;
std::mem::swap(&mut self.grid, &mut self.alt_grid);
mem::swap(&mut self.grid, &mut self.alt_grid);
}
/// Scroll screen down
@ -1258,10 +1268,58 @@ impl<T> Term<T> {
self.event_proxy.send_event(Event::Exit);
}
#[inline]
pub fn clipboard(&mut self) -> &mut Clipboard {
&mut self.clipboard
}
/// Toggle the vi mode.
#[inline]
pub fn toggle_vi_mode(&mut self) {
self.mode ^= TermMode::VI;
self.grid.selection = None;
// Reset vi mode cursor position to match primary cursor
if self.mode.contains(TermMode::VI) {
let line = min(self.cursor.point.line + self.grid.display_offset(), self.lines() - 1);
self.vi_mode_cursor = ViModeCursor::new(Point::new(line, self.cursor.point.col));
}
self.dirty = true;
}
/// Move vi mode cursor.
#[inline]
pub fn vi_motion(&mut self, motion: ViMotion)
where
T: EventListener,
{
// Require vi mode to be active
if !self.mode.contains(TermMode::VI) {
return;
}
// Move cursor
self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion);
// Update selection if one is active
let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point);
if let Some(selection) = &mut self.grid.selection {
// Do not extend empty selections started by single mouse click
if !selection.is_empty() {
selection.update(viewport_point, Side::Left);
selection.include_all();
}
}
self.dirty = true;
}
#[inline]
pub fn semantic_escape_chars(&self) -> &str {
&self.semantic_escape_chars
}
/// Insert a linebreak at the current cursor position.
#[inline]
fn wrapline(&mut self)
@ -1297,6 +1355,65 @@ impl<T> Term<T> {
cell.c = self.cursor.charsets[self.active_charset].map(c);
cell
}
/// Get rendering information about the active cursor.
fn renderable_cursor<C>(&self, config: &Config<C>) -> RenderableCursor {
let vi_mode = self.mode.contains(TermMode::VI);
// Cursor position
let mut point = if vi_mode {
self.vi_mode_cursor.point
} else {
let mut point = self.cursor.point;
point.line += self.grid.display_offset();
point
};
// Cursor shape
let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines();
let cursor_style = if hidden && !vi_mode {
point.line = Line(0);
CursorStyle::Hidden
} else if !self.is_focused && config.cursor.unfocused_hollow() {
CursorStyle::HollowBlock
} else {
let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
if vi_mode {
self.vi_mode_cursor_style.unwrap_or(cursor_style)
} else {
cursor_style
}
};
// Cursor colors
let (text_color, cursor_color) = if vi_mode {
(config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color())
} else {
let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]);
(config.cursor_text_color(), cursor_cursor_color)
};
// Expand across wide cell when inside wide char or spacer
let buffer_point = self.visible_to_buffer(point);
let cell = self.grid[buffer_point.line][buffer_point.col];
let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER)
&& self.grid[buffer_point.line][buffer_point.col - 1].flags.contains(Flags::WIDE_CHAR)
{
point.col -= 1;
true
} else {
cell.flags.contains(Flags::WIDE_CHAR)
};
RenderableCursor {
text_color,
cursor_color,
key: CursorKey { style: cursor_style, is_wide },
point,
rendered: false,
}
}
}
impl<T> TermInfo for Term<T> {
@ -2184,7 +2301,7 @@ mod tests {
use crate::event::{Event, EventListener};
use crate::grid::{Grid, Scroll};
use crate::index::{Column, Line, Point, Side};
use crate::selection::Selection;
use crate::selection::{Selection, SelectionType};
use crate::term::cell::{Cell, Flags};
use crate::term::{SizeInfo, Term};
@ -2222,17 +2339,29 @@ mod tests {
mem::swap(&mut term.semantic_escape_chars, &mut escape_chars);
{
*term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) }));
*term.selection_mut() = Some(Selection::new(
SelectionType::Semantic,
Point { line: 2, col: Column(1) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aa")));
}
{
*term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) }));
*term.selection_mut() = Some(Selection::new(
SelectionType::Semantic,
Point { line: 2, col: Column(4) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
{
*term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) }));
*term.selection_mut() = Some(Selection::new(
SelectionType::Semantic,
Point { line: 1, col: Column(1) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
}
@ -2258,7 +2387,11 @@ mod tests {
mem::swap(&mut term.grid, &mut grid);
*term.selection_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) }));
*term.selection_mut() = Some(Selection::new(
SelectionType::Lines,
Point { line: 0, col: Column(3) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n")));
}
@ -2285,7 +2418,8 @@ mod tests {
mem::swap(&mut term.grid, &mut grid);
let mut selection = Selection::simple(Point { line: 2, col: Column(0) }, Side::Left);
let mut selection =
Selection::new(SelectionType::Simple, Point { line: 2, col: Column(0) }, Side::Left);
selection.update(Point { line: 0, col: Column(2) }, Side::Right);
*term.selection_mut() = Some(selection);
assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into()));

View File

@ -0,0 +1,799 @@
use std::cmp::{max, min};
use serde::Deserialize;
use crate::event::EventListener;
use crate::grid::{GridCell, Scroll};
use crate::index::{Column, Line, Point};
use crate::term::cell::Flags;
use crate::term::{Search, Term};
/// Possible vi mode motion movements.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub enum ViMotion {
/// Move up.
Up,
/// Move down.
Down,
/// Move left.
Left,
/// Move right.
Right,
/// Move to start of line.
First,
/// Move to end of line.
Last,
/// Move to the first non-empty cell.
FirstOccupied,
/// Move to top of screen.
High,
/// Move to center of screen.
Middle,
/// Move to bottom of screen.
Low,
/// Move to start of semantically separated word.
SemanticLeft,
/// Move to start of next semantically separated word.
SemanticRight,
/// Move to end of previous semantically separated word.
SemanticLeftEnd,
/// Move to end of semantically separated word.
SemanticRightEnd,
/// Move to start of whitespace separated word.
WordLeft,
/// Move to start of next whitespace separated word.
WordRight,
/// Move to end of previous whitespace separated word.
WordLeftEnd,
/// Move to end of whitespace separated word.
WordRightEnd,
/// Move to opposing bracket.
Bracket,
}
/// Cursor tracking vi mode position.
#[derive(Default, Copy, Clone)]
pub struct ViModeCursor {
pub point: Point,
}
impl ViModeCursor {
pub fn new(point: Point) -> Self {
Self { point }
}
/// Move vi mode cursor.
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn motion<T: EventListener>(mut self, term: &mut Term<T>, motion: ViMotion) -> Self {
let display_offset = term.grid().display_offset();
let lines = term.grid().num_lines();
let cols = term.grid().num_cols();
let mut buffer_point = term.visible_to_buffer(self.point);
match motion {
ViMotion::Up => {
if buffer_point.line + 1 < term.grid().len() {
buffer_point.line += 1;
}
},
ViMotion::Down => buffer_point.line = buffer_point.line.saturating_sub(1),
ViMotion::Left => {
buffer_point = expand_wide(term, buffer_point, true);
let wrap_point = Point::new(buffer_point.line + 1, cols - 1);
if buffer_point.col.0 == 0
&& buffer_point.line + 1 < term.grid().len()
&& is_wrap(term, wrap_point)
{
buffer_point = wrap_point;
} else {
buffer_point.col = Column(buffer_point.col.saturating_sub(1));
}
},
ViMotion::Right => {
buffer_point = expand_wide(term, buffer_point, false);
if is_wrap(term, buffer_point) {
buffer_point = Point::new(buffer_point.line - 1, Column(0));
} else {
buffer_point.col = min(buffer_point.col + 1, cols - 1);
}
},
ViMotion::First => {
buffer_point = expand_wide(term, buffer_point, true);
while buffer_point.col.0 == 0
&& buffer_point.line + 1 < term.grid().len()
&& is_wrap(term, Point::new(buffer_point.line + 1, cols - 1))
{
buffer_point.line += 1;
}
buffer_point.col = Column(0);
},
ViMotion::Last => buffer_point = last(term, buffer_point),
ViMotion::FirstOccupied => buffer_point = first_occupied(term, buffer_point),
ViMotion::High => {
let line = display_offset + lines.0 - 1;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
buffer_point = Point::new(line, col);
},
ViMotion::Middle => {
let line = display_offset + lines.0 / 2;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
buffer_point = Point::new(line, col);
},
ViMotion::Low => {
let line = display_offset;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
buffer_point = Point::new(line, col);
},
ViMotion::SemanticLeft => buffer_point = semantic(term, buffer_point, true, true),
ViMotion::SemanticRight => buffer_point = semantic(term, buffer_point, false, true),
ViMotion::SemanticLeftEnd => buffer_point = semantic(term, buffer_point, true, false),
ViMotion::SemanticRightEnd => buffer_point = semantic(term, buffer_point, false, false),
ViMotion::WordLeft => buffer_point = word(term, buffer_point, true, true),
ViMotion::WordRight => buffer_point = word(term, buffer_point, false, true),
ViMotion::WordLeftEnd => buffer_point = word(term, buffer_point, true, false),
ViMotion::WordRightEnd => buffer_point = word(term, buffer_point, false, false),
ViMotion::Bracket => {
buffer_point = term.bracket_search(buffer_point).unwrap_or(buffer_point);
},
}
scroll_to_point(term, buffer_point);
self.point = term.grid().clamp_buffer_to_visible(buffer_point);
self
}
/// Get target cursor point for vim-like page movement.
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn scroll<T: EventListener>(mut self, term: &Term<T>, lines: isize) -> Self {
// Check number of lines the cursor needs to be moved
let overscroll = if lines > 0 {
let max_scroll = term.grid().history_size() - term.grid().display_offset();
max(0, lines - max_scroll as isize)
} else {
let max_scroll = term.grid().display_offset();
min(0, lines + max_scroll as isize)
};
// Clamp movement to within visible region
let mut line = self.point.line.0 as isize;
line -= overscroll;
line = max(0, min(term.grid().num_lines().0 as isize - 1, line));
// Find the first occupied cell after scrolling has been performed
let buffer_point = term.visible_to_buffer(self.point);
let mut target_line = buffer_point.line as isize + lines;
target_line = max(0, min(term.grid().len() as isize - 1, target_line));
let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col;
// Move cursor
self.point = Point::new(Line(line as usize), col);
self
}
}
/// Scroll display if point is outside of viewport.
fn scroll_to_point<T: EventListener>(term: &mut Term<T>, point: Point<usize>) {
let display_offset = term.grid().display_offset();
let lines = term.grid().num_lines();
// Scroll once the top/bottom has been reached
if point.line >= display_offset + lines.0 {
let lines = point.line.saturating_sub(display_offset + lines.0 - 1);
term.scroll_display(Scroll::Lines(lines as isize));
} else if point.line < display_offset {
let lines = display_offset.saturating_sub(point.line);
term.scroll_display(Scroll::Lines(-(lines as isize)));
};
}
/// Find next end of line to move to.
fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
let cols = term.grid().num_cols();
// Expand across wide cells
point = expand_wide(term, point, false);
// Find last non-empty cell in the current line
let occupied = last_occupied_in_line(term, point.line).unwrap_or_default();
if point.col < occupied.col {
// Jump to last occupied cell when not already at or beyond it
occupied
} else if is_wrap(term, point) {
// Jump to last occupied cell across linewraps
while point.line > 0 && is_wrap(term, point) {
point.line -= 1;
}
last_occupied_in_line(term, point.line).unwrap_or(point)
} else {
// Jump to last column when beyond the last occupied cell
Point::new(point.line, cols - 1)
}
}
/// Find next non-empty cell to move to.
fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
let cols = term.grid().num_cols();
// Expand left across wide chars, since we're searching lines left to right
point = expand_wide(term, point, true);
// Find first non-empty cell in current line
let occupied = first_occupied_in_line(term, point.line)
.unwrap_or_else(|| Point::new(point.line, cols - 1));
// Jump across wrapped lines if we're already at this line's first occupied cell
if point == occupied {
let mut occupied = None;
// Search for non-empty cell in previous lines
for line in (point.line + 1)..term.grid().len() {
if !is_wrap(term, Point::new(line, cols - 1)) {
break;
}
occupied = first_occupied_in_line(term, line).or(occupied);
}
// Fallback to the next non-empty cell
let mut line = point.line;
occupied.unwrap_or_else(|| loop {
if let Some(occupied) = first_occupied_in_line(term, line) {
break occupied;
}
let last_cell = Point::new(line, cols - 1);
if line == 0 || !is_wrap(term, last_cell) {
break last_cell;
}
line -= 1;
})
} else {
occupied
}
}
/// Move by semantically separated word, like w/b/e/ge in vi.
fn semantic<T: EventListener>(
term: &mut Term<T>,
mut point: Point<usize>,
left: bool,
start: bool,
) -> Point<usize> {
// Expand semantically based on movement direction
let expand_semantic = |point: Point<usize>| {
// Do not expand when currently on a semantic escape char
let cell = term.grid()[point.line][point.col];
if term.semantic_escape_chars().contains(cell.c)
&& !cell.flags.contains(Flags::WIDE_CHAR_SPACER)
{
point
} else if left {
term.semantic_search_left(point)
} else {
term.semantic_search_right(point)
}
};
// Make sure we jump above wide chars
point = expand_wide(term, point, left);
// Move to word boundary
if left != start && !is_boundary(term, point, left) {
point = expand_semantic(point);
}
// Skip whitespace
let mut next_point = advance(term, point, left);
while !is_boundary(term, point, left) && is_space(term, next_point) {
point = next_point;
next_point = advance(term, point, left);
}
// Assure minimum movement of one cell
if !is_boundary(term, point, left) {
point = advance(term, point, left);
}
// Move to word boundary
if left == start && !is_boundary(term, point, left) {
point = expand_semantic(point);
}
point
}
/// Move by whitespace separated word, like W/B/E/gE in vi.
fn word<T: EventListener>(
term: &mut Term<T>,
mut point: Point<usize>,
left: bool,
start: bool,
) -> Point<usize> {
// Make sure we jump above wide chars
point = expand_wide(term, point, left);
if left == start {
// Skip whitespace until right before a word
let mut next_point = advance(term, point, left);
while !is_boundary(term, point, left) && is_space(term, next_point) {
point = next_point;
next_point = advance(term, point, left);
}
// Skip non-whitespace until right inside word boundary
let mut next_point = advance(term, point, left);
while !is_boundary(term, point, left) && !is_space(term, next_point) {
point = next_point;
next_point = advance(term, point, left);
}
}
if left != start {
// Skip non-whitespace until just beyond word
while !is_boundary(term, point, left) && !is_space(term, point) {
point = advance(term, point, left);
}
// Skip whitespace until right inside word boundary
while !is_boundary(term, point, left) && is_space(term, point) {
point = advance(term, point, left);
}
}
point
}
/// Jump to the end of a wide cell.
fn expand_wide<T, P>(term: &Term<T>, point: P, left: bool) -> Point<usize>
where
P: Into<Point<usize>>,
{
let mut point = point.into();
let cell = term.grid()[point.line][point.col];
if cell.flags.contains(Flags::WIDE_CHAR) && !left {
point.col += 1;
} else if cell.flags.contains(Flags::WIDE_CHAR_SPACER)
&& term.grid()[point.line][point.col - 1].flags.contains(Flags::WIDE_CHAR)
&& left
{
point.col -= 1;
}
point
}
/// Find first non-empty cell in line.
fn first_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
(0..term.grid().num_cols().0)
.map(|col| Point::new(line, Column(col)))
.find(|&point| !is_space(term, point))
}
/// Find last non-empty cell in line.
fn last_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
(0..term.grid().num_cols().0)
.map(|col| Point::new(line, Column(col)))
.rfind(|&point| !is_space(term, point))
}
/// Advance point based on direction.
fn advance<T>(term: &Term<T>, point: Point<usize>, left: bool) -> Point<usize> {
let cols = term.grid().num_cols();
if left {
point.sub_absolute(cols.0, 1)
} else {
point.add_absolute(cols.0, 1)
}
}
/// Check if cell at point contains whitespace.
fn is_space<T>(term: &Term<T>, point: Point<usize>) -> bool {
let cell = term.grid()[point.line][point.col];
cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER)
}
fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE)
}
/// Check if point is at screen boundary.
fn is_boundary<T>(term: &Term<T>, point: Point<usize>, left: bool) -> bool {
(point.line == 0 && point.col + 1 >= term.grid().num_cols() && !left)
|| (point.line + 1 >= term.grid().len() && point.col.0 == 0 && left)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clipboard::Clipboard;
use crate::config::MockConfig;
use crate::event::Event;
use crate::index::{Column, Line};
use crate::term::{SizeInfo, Term};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
fn term() -> Term<Mock> {
let size = SizeInfo {
width: 20.,
height: 20.,
cell_width: 1.0,
cell_height: 1.0,
padding_x: 0.0,
padding_y: 0.0,
dpr: 1.0,
};
Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock)
}
#[test]
fn motion_simple() {
let mut term = term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Right);
assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
cursor = cursor.motion(&mut term, ViMotion::Left);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Down);
assert_eq!(cursor.point, Point::new(Line(1), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Up);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn simple_wide() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'a';
term.grid_mut()[Line(0)][Column(1)].c = '汉';
term.grid_mut()[Line(0)][Column(1)].flags.insert(Flags::WIDE_CHAR);
term.grid_mut()[Line(0)][Column(2)].c = ' ';
term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR_SPACER);
term.grid_mut()[Line(0)][Column(3)].c = 'a';
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(1)));
cursor = cursor.motion(&mut term, ViMotion::Right);
assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::Left);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn motion_start_end() {
let mut term = term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Last);
assert_eq!(cursor.point, Point::new(Line(0), Column(19)));
cursor = cursor.motion(&mut term, ViMotion::First);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn motion_first_occupied() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = ' ';
term.grid_mut()[Line(0)][Column(1)].c = 'x';
term.grid_mut()[Line(0)][Column(2)].c = ' ';
term.grid_mut()[Line(0)][Column(3)].c = 'y';
term.grid_mut()[Line(0)][Column(19)].flags.insert(Flags::WRAPLINE);
term.grid_mut()[Line(1)][Column(19)].flags.insert(Flags::WRAPLINE);
term.grid_mut()[Line(2)][Column(0)].c = 'z';
term.grid_mut()[Line(2)][Column(1)].c = ' ';
let mut cursor = ViModeCursor::new(Point::new(Line(2), Column(1)));
cursor = cursor.motion(&mut term, ViMotion::FirstOccupied);
assert_eq!(cursor.point, Point::new(Line(2), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::FirstOccupied);
assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
}
#[test]
fn motion_high_middle_low() {
let mut term = term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::High);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Middle);
assert_eq!(cursor.point, Point::new(Line(9), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Low);
assert_eq!(cursor.point, Point::new(Line(19), Column(0)));
}
#[test]
fn motion_bracket() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = '(';
term.grid_mut()[Line(0)][Column(1)].c = 'x';
term.grid_mut()[Line(0)][Column(2)].c = ')';
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::Bracket);
assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::Bracket);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
fn motion_semantic_term() -> Term<Mock> {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'x';
term.grid_mut()[Line(0)][Column(1)].c = ' ';
term.grid_mut()[Line(0)][Column(2)].c = 'x';
term.grid_mut()[Line(0)][Column(3)].c = 'x';
term.grid_mut()[Line(0)][Column(4)].c = ' ';
term.grid_mut()[Line(0)][Column(5)].c = ' ';
term.grid_mut()[Line(0)][Column(6)].c = ':';
term.grid_mut()[Line(0)][Column(7)].c = ' ';
term.grid_mut()[Line(0)][Column(8)].c = 'x';
term.grid_mut()[Line(0)][Column(9)].c = ':';
term.grid_mut()[Line(0)][Column(10)].c = 'x';
term.grid_mut()[Line(0)][Column(11)].c = ' ';
term.grid_mut()[Line(0)][Column(12)].c = ' ';
term.grid_mut()[Line(0)][Column(13)].c = ':';
term.grid_mut()[Line(0)][Column(14)].c = ' ';
term.grid_mut()[Line(0)][Column(15)].c = 'x';
term
}
#[test]
fn motion_semantic_right_end() {
let mut term = motion_semantic_term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(15)));
}
#[test]
fn motion_semantic_left_start() {
let mut term = motion_semantic_term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn motion_semantic_right_start() {
let mut term = motion_semantic_term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(15)));
}
#[test]
fn motion_semantic_left_end() {
let mut term = motion_semantic_term();
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn scroll_semantic() {
let mut term = term();
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default());
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
assert_eq!(term.grid().display_offset(), 5);
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
assert_eq!(term.grid().display_offset(), 0);
cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
assert_eq!(term.grid().display_offset(), 5);
cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
assert_eq!(term.grid().display_offset(), 0);
}
#[test]
fn semantic_wide() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'a';
term.grid_mut()[Line(0)][Column(1)].c = ' ';
term.grid_mut()[Line(0)][Column(2)].c = '汉';
term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
term.grid_mut()[Line(0)][Column(3)].c = ' ';
term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
term.grid_mut()[Line(0)][Column(4)].c = ' ';
term.grid_mut()[Line(0)][Column(5)].c = 'a';
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3)));
cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
#[test]
fn motion_word() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'a';
term.grid_mut()[Line(0)][Column(1)].c = ';';
term.grid_mut()[Line(0)][Column(2)].c = ' ';
term.grid_mut()[Line(0)][Column(3)].c = ' ';
term.grid_mut()[Line(0)][Column(4)].c = 'a';
term.grid_mut()[Line(0)][Column(5)].c = ';';
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
cursor = cursor.motion(&mut term, ViMotion::WordLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
cursor = cursor.motion(&mut term, ViMotion::WordLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::WordRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
}
#[test]
fn scroll_word() {
let mut term = term();
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default());
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
cursor = cursor.motion(&mut term, ViMotion::WordLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
assert_eq!(term.grid().display_offset(), 5);
cursor = cursor.motion(&mut term, ViMotion::WordRight);
assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
assert_eq!(term.grid().display_offset(), 0);
cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
assert_eq!(term.grid().display_offset(), 5);
cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
assert_eq!(term.grid().display_offset(), 0);
}
#[test]
fn word_wide() {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'a';
term.grid_mut()[Line(0)][Column(1)].c = ' ';
term.grid_mut()[Line(0)][Column(2)].c = '汉';
term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
term.grid_mut()[Line(0)][Column(3)].c = ' ';
term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
term.grid_mut()[Line(0)][Column(4)].c = ' ';
term.grid_mut()[Line(0)][Column(5)].c = 'a';
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
cursor = cursor.motion(&mut term, ViMotion::WordRight);
assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3)));
cursor = cursor.motion(&mut term, ViMotion::WordLeft);
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
}

View File

@ -7,7 +7,7 @@ URL: https://github.com/alacritty/alacritty
VCS: https://github.com/alacritty/alacritty.git
Source: alacritty-%{version}.tar
BuildRequires: rust >= 1.37.0
BuildRequires: rust >= 1.39.0
BuildRequires: cargo
BuildRequires: cmake
BuildRequires: freetype-devel