Skip to content

Commit

Permalink
Add support for setting prompt marks via OSC 133
Browse files Browse the repository at this point in the history
Adds support for setting prompt marks with OSC 133 ; A ST. While
there're other markers for prompt `FTCS_PROMPT` (`A` parameter) is
likely the most widely used and useful one.

Fixes alacritty#5850.
  • Loading branch information
kchibisov committed Mar 4, 2022
1 parent d8113dc commit 61dc515
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -14,9 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

- Track and report surface damage information to Wayland compositors
<<<<<<< HEAD
- Escape sequence for undercurl, dotted and dashed underlines (`CSI 4 : [3-5] m`)
- `ToggleMaximized` key binding action to (un-)maximize the active window, not bound by default
- Support for OpenGL ES 2.0
- Prompt marker `OSC 133 ; A ST` and `NextPrompt`/`PreviousPrompt` bindings to jump between them

### Changed

Expand Down
4 changes: 4 additions & 0 deletions alacritty.yml
Expand Up @@ -595,6 +595,10 @@
# - ScrollLineDown
# - ScrollToTop
# - ScrollToBottom
# - NextPrompt
# Goes to next shell prompt marked with OSC 133.
# - PreviousPrompt
# Goes to previous shell prompt marked with OSC 133.
# - ClearHistory
# Remove the terminal's scrollback history.
# - Hide
Expand Down
6 changes: 6 additions & 0 deletions alacritty/src/config/bindings.rs
Expand Up @@ -208,6 +208,12 @@ pub enum Action {
/// Start a backward buffer search.
SearchBackward,

/// Jump to the next shell prompt marked with OSC 133.
NextPrompt,

/// Jump to the previous shell prompt marked with OSC 133.
PreviousPrompt,

/// No action.
None,
}
Expand Down
8 changes: 8 additions & 0 deletions alacritty/src/input.rs
Expand Up @@ -246,6 +246,14 @@ impl<T: EventListener> Execute<T> for Action {
Action::Mouse(MouseAction::ExpandSelection) => ctx.expand_selection(),
Action::SearchForward => ctx.start_search(Direction::Right),
Action::SearchBackward => ctx.start_search(Direction::Left),
Action::NextPrompt => {
ctx.terminal_mut().goto_next_prompt();
ctx.mark_dirty();
},
Action::PreviousPrompt => {
ctx.terminal_mut().goto_previous_prompt();
ctx.mark_dirty();
},
Action::Copy => ctx.copy_selection(ClipboardType::Clipboard),
#[cfg(not(any(target_os = "macos", windows)))]
Action::CopySelection => ctx.copy_selection(ClipboardType::Selection),
Expand Down
8 changes: 8 additions & 0 deletions alacritty_terminal/src/ansi.rs
Expand Up @@ -456,6 +456,9 @@ pub trait Handler {

/// Report text area size in characters.
fn text_area_size_chars(&mut self) {}

/// Set shell prompt mark.
fn set_prompt_mark(&mut self) {}
}

/// Terminal cursor configuration.
Expand Down Expand Up @@ -1095,6 +1098,11 @@ where
// Reset text cursor color.
b"112" => self.handler.reset_color(NamedColor::Cursor as usize),

// Set shell prompt mark.
b"133" => match params.get(1) {
Some(first) if first.get(0) == Some(&b'A') => self.handler.set_prompt_mark(),
_ => unhandled(params),
},
_ => unhandled(params),
}
}
Expand Down
1 change: 1 addition & 0 deletions alacritty_terminal/src/term/cell.rs
Expand Up @@ -30,6 +30,7 @@ bitflags! {
const ALL_UNDERLINES = Self::UNDERLINE.bits | Self::DOUBLE_UNDERLINE.bits
| Self::UNDERCURL.bits | Self::DOTTED_UNDERLINE.bits
| Self::DASHED_UNDERLINE.bits;
const PROMPT_MARK = 0b1000_0000_0000_0000;
}
}

Expand Down
75 changes: 73 additions & 2 deletions alacritty_terminal/src/term/mod.rs
Expand Up @@ -14,7 +14,7 @@ use crate::ansi::{
};
use crate::config::Config;
use crate::event::{Event, EventListener};
use crate::grid::{Dimensions, Grid, GridIterator, Scroll};
use crate::grid::{Dimensions, Grid, GridCell, GridIterator, Scroll};
use crate::index::{self, Boundary, Column, Direction, Line, Point, Side};
use crate::selection::{Selection, SelectionRange, SelectionType};
use crate::term::cell::{Cell, Flags, LineLength};
Expand Down Expand Up @@ -501,6 +501,57 @@ impl<T> Term<T> {
}
}

/// Go to next prompt farthest down in history.
pub fn goto_next_prompt(&mut self)
where
T: EventListener,
{
let point = if self.mode.contains(TermMode::VI) {
self.vi_mode_cursor.point
} else {
let display_offset = self.grid.display_offset() as i32;
// If we're outside of Vi mode use bottom most visible line.
Point::new(Line(self.screen_lines() as i32 - 1 - display_offset), Column(0))
};

let prompt_point = match self.next_prompt(point) {
Some(point) => point,
None => return,
};

if self.mode().contains(TermMode::VI) {
self.vi_goto_point(prompt_point);
} else {
let scroll = Scroll::Delta(point.line.0 - prompt_point.line.0);
self.scroll_display(scroll);
}
}

/// Go to previous prompt farthest up in history.
pub fn goto_previous_prompt(&mut self)
where
T: EventListener,
{
let point = if self.mode.contains(TermMode::VI) {
self.vi_mode_cursor.point
} else {
// If we're outside of Vi mode use topmost visible line.
Point::new(Line(-(self.grid.display_offset() as i32)), Column(0))
};

let prompt_point = match self.previous_prompt(point) {
Some(point) => point,
None => return,
};

if self.mode.contains(TermMode::VI) {
self.vi_goto_point(prompt_point);
} else {
let scroll = Scroll::Delta(point.line.0 - prompt_point.line.0);
self.scroll_display(scroll);
}
}

#[must_use]
pub fn damage(&mut self, selection: Option<SelectionRange>) -> TermDamage<'_> {
// Ensure the entire terminal is damaged after entering insert mode.
Expand Down Expand Up @@ -1046,10 +1097,13 @@ impl<T> Term<T> {
let c = self.grid.cursor.charsets[self.active_charset].map(c);
let fg = self.grid.cursor.template.fg;
let bg = self.grid.cursor.template.bg;
let flags = self.grid.cursor.template.flags;
let mut flags = self.grid.cursor.template.flags;

let mut cursor_cell = self.grid.cursor_cell();

// Preserve PROMPT_MARK if we had it.
flags |= cursor_cell.flags & Flags::PROMPT_MARK;

// Clear all related cells when overwriting a fullwidth cell.
if cursor_cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) {
// Remove wide char and spacer.
Expand Down Expand Up @@ -1600,6 +1654,9 @@ impl<T: EventListener> Handler for Term<T> {
let bg = cursor.template.bg;
let point = cursor.point;

// We don't want to clear prompt markers.
let prompt_marked = *self.grid[point.line][Column(0)].flags() & Flags::PROMPT_MARK;

let (left, right) = match mode {
ansi::LineClearMode::Right => (point.column, Column(self.columns())),
ansi::LineClearMode::Left => (Column(0), point.column + 1),
Expand All @@ -1613,6 +1670,9 @@ impl<T: EventListener> Handler for Term<T> {
*cell = bg.into();
}

// Restore prompt mark if it got reset.
self.grid[point.line][Column(0)].flags_mut().insert(prompt_marked);

let range = self.grid.cursor.point.line..=self.grid.cursor.point.line;
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
}
Expand Down Expand Up @@ -1724,13 +1784,15 @@ impl<T: EventListener> Handler for Term<T> {
},
ansi::ClearMode::Below => {
let cursor = self.grid.cursor.point;
let prompt_mark = self.grid[cursor.line][Column(0)].flags & Flags::PROMPT_MARK;
for cell in &mut self.grid[cursor.line][cursor.column..] {
*cell = bg.into();
}

if (cursor.line.0 as usize) < screen_lines - 1 {
self.grid.reset_region((cursor.line + 1)..);
}
self.grid[cursor.line][Column(0)].flags.insert(prompt_mark);

let range = cursor.line..Line(screen_lines as i32);
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
Expand Down Expand Up @@ -2072,6 +2134,15 @@ impl<T: EventListener> Handler for Term<T> {
}
}

#[inline]
fn set_prompt_mark(&mut self) {
let mark_point = self.line_search_left(self.grid.cursor.point);
trace!("Setting shell prompt mark at: line={}", mark_point.line);

// Set prompt mark flag.
self.grid[mark_point].flags_mut().insert(Flags::PROMPT_MARK);
}

#[inline]
fn text_area_size_pixels(&mut self) {
let width = self.cell_width * self.columns();
Expand Down
40 changes: 40 additions & 0 deletions alacritty_terminal/src/term/search.rs
Expand Up @@ -434,6 +434,46 @@ impl<T> Term<T> {

point
}

/// Find the next shell prompt.
pub fn next_prompt(&self, mut point: Point) -> Option<Point> {
// If we're at the start of the prompt go to the next.
if point.column == Column(0) {
point.line += 1
} else {
point.column = Column(0);
}

while point.line <= self.bottommost_line() {
if self.grid[point].flags.contains(Flags::PROMPT_MARK) {
return Some(point);
}

point.line += 1;
}

None
}

/// Find the previous shell prompt.
pub fn previous_prompt(&self, mut point: Point) -> Option<Point> {
// If we're at the start of the prompt go to the previous.
if point.column == Column(0) {
point.line.0 -= 1
} else {
point.column = Column(0);
}

while point.line >= self.grid.topmost_line() {
if self.grid[point].flags.contains(Flags::PROMPT_MARK) {
return Some(point);
}

point.line -= 1;
}

None
}
}

/// Iterator over regex matches.
Expand Down
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref.rs
Expand Up @@ -67,6 +67,7 @@ ref_tests! {
saved_cursor_alt
sgr
underline
prompt_marks
}

fn read_u8<P>(path: P) -> Vec<u8>
Expand Down
8 changes: 8 additions & 0 deletions alacritty_terminal/tests/ref/prompt_marks/alacritty.recording
@@ -0,0 +1,8 @@
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  osc-133 via 🦀 v1.58.1 ➜ [?2004h[?2004l
Expand Down
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/prompt_marks/config.json
@@ -0,0 +1 @@
{"history_size":0}
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/prompt_marks/grid.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/prompt_marks/size.json
@@ -0,0 +1 @@
{"width":1269.0,"height":696.0,"cell_width":7.0,"cell_height":16.0,"padding_x":4.0,"padding_y":4.0,"screen_lines":43,"columns":180}
1 change: 1 addition & 0 deletions docs/escape_support.md
Expand Up @@ -96,6 +96,7 @@ brevity.
| `OSC 110` | IMPLEMENTED | |
| `OSC 111` | IMPLEMENTED | |
| `OSC 112` | IMPLEMENTED | |
| `OSC 133` | PARTIAL | Only `A` is supported |

### DCS (Device Control String) - `ESC P`

Expand Down

0 comments on commit 61dc515

Please sign in to comment.