Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ struct ProcessorState<T: Timeout> {

/// State for synchronized terminal updates.
sync_state: SyncState<T>,

/// Set on `command_start` or `command_executed` and cleared on
/// `command_finished`. Used to ignore `command_finished` when not set.
Comment on lines +251 to +252

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This documentation is not particularly helpful unless you already know what it's talking about.

Suggested change
/// Set on `command_start` or `command_executed` and cleared on
/// `command_finished`. Used to ignore `command_finished` when not set.
/// Prompt marker state.
///
/// Used to track whether we're inside a prompt region with marked boundaries. See [`Handler::command_finished`].

command_started: bool,
}

#[derive(Debug)]
Expand Down Expand Up @@ -700,6 +704,18 @@ pub trait Handler {
/// Set hyperlink.
fn set_hyperlink(&mut self, _: Option<Hyperlink>) {}

/// Mark the start of the prompt.
fn prompt_start(&mut self) {}

/// Mark the start of the command the user types, after the prompt.
fn command_start(&mut self) {}

/// Mark that the command was executed and its output begins.
fn command_executed(&mut self) {}

/// Mark that the command finished, with its exit status when known.
fn command_finished(&mut self, _: Option<u8>) {}

/// Set mouse cursor icon.
fn set_mouse_cursor_icon(&mut self, _: CursorIcon) {}

Expand Down Expand Up @@ -1520,6 +1536,33 @@ where
// Reset text cursor color.
b"112" => self.handler.reset_color(NamedColor::Cursor as usize),

// Prompt and command markers.
b"133" => {
if params.len() < 2 {
return unhandled(params);
}
match params[1] {
b"A" => self.handler.prompt_start(),
b"B" => {
self.state.command_started = true;
self.handler.command_start();
},
b"C" => {
self.state.command_started = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is only necessary if execution is possible without start, right? iterm2's document suggests this is not allowed, though it's not entirely clear about that.

Generally it doesn't really cover edgecases very well…

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It says this:

All text between FTCS_COMMAND_START and FTCS_COMMAND_EXECUTED at the time FTCS_COMMAND_EXECUTED is received excluding terminal whitespace is considered the command the user entered.

Which I don't interpret as a strict requirement, but more of an "if there is text, it's interpreted this way" kind of statement.

It then goes on to say this:

It is expected that user-entered commands will be edited interactively, so the screen contents are captured without regard to how they came to contain their state. If the cursor's location is before (above, or if on the same line, left of) its location when FTCS_COMMAND_START was received, then the command will be treated as the empty string.

Which explicitly allows for the case of a command being empty, even if it's not directly about sending C without B.

Given that, I feel like it's reasonable to allow for this, though I really don't have a strong feeling about it right now.

I do feel like I need to do a survey of existing OSC 133 supporting shells and applications.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To me the statement regarding FTCS_COMMAND_START suggests that you need to know about the command start to know whether it's empty or not. If you don't have this information, you won't be able to tell whether it should be empty or not.

It's fair to just assume it shouldn't be empty if you have no information, but it's not entirely clear (imo).

self.handler.command_executed();
},
b"D" => {
if self.state.command_started {
Comment thread
chrisduerr marked this conversation as resolved.
self.state.command_started = false;
let exit_code =
params.get(2).and_then(|p| str::from_utf8(p).ok()?.parse().ok());
self.handler.command_finished(exit_code);
}
},
_ => unhandled(params),
}
},

_ => unhandled(params),
}
}
Expand Down Expand Up @@ -2046,6 +2089,15 @@ mod tests {
identity_reported: bool,
color: Option<Rgb>,
reset_colors: Vec<usize>,
prompt_events: Vec<PromptEvent>,
}

#[derive(Debug, PartialEq, Eq)]
enum PromptEvent {
PromptStart,
CommandStart,
CommandExecuted,
CommandFinished(Option<u8>),
}

impl Handler for MockHandler {
Expand Down Expand Up @@ -2077,6 +2129,22 @@ mod tests {
fn reset_color(&mut self, index: usize) {
self.reset_colors.push(index)
}

fn prompt_start(&mut self) {
self.prompt_events.push(PromptEvent::PromptStart);
}

fn command_start(&mut self) {
self.prompt_events.push(PromptEvent::CommandStart);
}

fn command_executed(&mut self) {
self.prompt_events.push(PromptEvent::CommandExecuted);
}

fn command_finished(&mut self, exit_code: Option<u8>) {
self.prompt_events.push(PromptEvent::CommandFinished(exit_code));
}
}

impl Default for MockHandler {
Expand All @@ -2088,6 +2156,7 @@ mod tests {
identity_reported: false,
color: None,
reset_colors: Vec::new(),
prompt_events: Vec::new(),
}
}
}
Expand Down Expand Up @@ -2170,6 +2239,95 @@ mod tests {
assert_eq!(handler.attr, Some(Attr::Foreground(Color::Spec(spec))));
}

#[test]
fn osc_133_prompt_markers() {
let mut parser = Processor::<TestSyncHandler>::new();
let mut handler = MockHandler::default();

parser.advance(&mut handler, b"\x1b]133;A\x07");
parser.advance(&mut handler, b"\x1b]133;B\x07");
parser.advance(&mut handler, b"\x1b]133;C\x07");
parser.advance(&mut handler, b"\x1b]133;D\x07");

assert_eq!(
handler.prompt_events,
vec![
PromptEvent::PromptStart,
PromptEvent::CommandStart,
PromptEvent::CommandExecuted,
PromptEvent::CommandFinished(None),
]
);
}

#[test]
fn osc_133_command_end_exit_code() {
let mut parser = Processor::<TestSyncHandler>::new();
let mut handler = MockHandler::default();

parser.advance(&mut handler, b"\x1b]133;C\x07");
parser.advance(&mut handler, b"\x1b]133;D;0\x07");
parser.advance(&mut handler, b"\x1b]133;C\x07");
parser.advance(&mut handler, b"\x1b]133;D;130\x07");

assert_eq!(
handler.prompt_events,
vec![
PromptEvent::CommandExecuted,
PromptEvent::CommandFinished(Some(0)),
PromptEvent::CommandExecuted,
PromptEvent::CommandFinished(Some(130)),
]
);
}

#[test]
fn osc_133_command_finished_without_command_ignored() {
let mut parser = Processor::<TestSyncHandler>::new();
let mut handler = MockHandler::default();

// `prompt_start` does not begin a command, so `command_finished` has
// no preceding `command_start` or `command_executed`.
parser.advance(&mut handler, b"\x1b]133;A\x07");
parser.advance(&mut handler, b"\x1b]133;D;0\x07");

// This next `command_finished` is paired with a `command_start`, and
// the final `command_finished` with no new command start is ignored.
parser.advance(&mut handler, b"\x1b]133;B\x07");
parser.advance(&mut handler, b"\x1b]133;D\x07");
parser.advance(&mut handler, b"\x1b]133;D\x07");

assert_eq!(
handler.prompt_events,
vec![
PromptEvent::PromptStart,
PromptEvent::CommandStart,
PromptEvent::CommandFinished(None),
]
);
}

#[test]
fn osc_133_st_terminated() {
let mut parser = Processor::<TestSyncHandler>::new();
let mut handler = MockHandler::default();

parser.advance(&mut handler, b"\x1b]133;A\x1b\\");

assert_eq!(handler.prompt_events, vec![PromptEvent::PromptStart]);
}

#[test]
fn osc_133_unknown_subcommand_ignored() {
let mut parser = Processor::<TestSyncHandler>::new();
let mut handler = MockHandler::default();

parser.advance(&mut handler, b"\x1b]133\x07");
parser.advance(&mut handler, b"\x1b]133;Z\x07");

assert!(handler.prompt_events.is_empty());
}

/// No exactly a test; useful for debugging.
#[test]
fn parse_zsh_startup() {
Expand Down