Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting prompt marks via OSC 133 #5860

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be above ALL_UNDERLINES. The "special" constants are usually best kept at the bottom because that way one can easily see at a glance that all bits are correctly set for the other constants.

}
}

Expand Down
75 changes: 73 additions & 2 deletions alacritty_terminal/src/term/mod.rs
Original file line number Diff line number Diff line change
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If we're outside of Vi mode use bottom most visible line.
// If we're outside of Vi mode, use bottommost visible line.

Topmost/bottommost are one word, as weird as it looks.

Point::new(Line(self.screen_lines() as i32 - 1 - display_offset), Column(0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using the bottommost line? If I have a prompt visible at the top and another prompt in the middle of my screen, I want to jump to the prompt in the middle of my screen. I don't want to jump to the next offscreen prompt.

This is the same as search, the top of the screen is always to be considered the origin of the search.

This also shouldn't be inverted for goto_previous. Even there we should still make it based on the topmost line and not switch to the bottommost. We always want our search match aligned at the top of the viewport.

};

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't go to the previous prompt farthest up in history, that would mean we skip over as many prompts as possible. We go to the previous prompt in history, so the opposite of farthest. Could say closest/nearest up in history but I feel like that's unnecessary.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If we're outside of Vi mode use topmost visible line.
// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Preserve PROMPT_MARK if we had it.
// Preserve PROMPT_MARK.

Also is this necessary? The prompt shouldn't usually be overwritten?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does get overwritten due to the way everyone is sending prompt marks. They send prompt mark and then start writing the prompt, leading to overwriting the cell. The documentation and recommendations state that you should send it before writing the prompt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really not a big fan of this aspect of the escape. Either way though this comment should be fixed.

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;

Comment on lines +1657 to +1659
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Who rewrites a prompt without writing the prompt there?

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, question if we should keep the prompt mark.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what zsh doing right after precmd. It calls ClearBelow. The bash and some other shells call ClearLine::Right. Though, we can remove the above one.

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
Original file line number Diff line number Diff line change
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);
}
chrisduerr marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +443 to +445
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
point.column = Column(0);
}
point.column = Column(0);

A bit shorter like this and probably compiles down to the same thing anyway?


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
Original file line number Diff line number Diff line change
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