fix: close the tail items from the targeted re-audit
Resolves the 13 rows still partial in docs/rewrite/notes/review-rv4- codex.md (details in final-Z5.md): - Escaped config strings are decoded to raw bytes at every OS boundary: Flatpak cwd/argv to D-Bus, screen-file paths, the default config template, GHOSTTY_RESOURCES_DIR/TERMINFO/shell integration/ bindtextdomain consumers, custom shader loading, list-themes hyperlinks. - osbytes escapes literal U+FDD0 on non-Unix too (bijective everywhere); the GTK paste dialog only decodes text that was actually encoded. - shadertoy: whole-array uses of iChannelTime rebuild a plain float[4] from the std140-padded members (naga-validated, MSL offsets checked). - libghostty-vt: the OSC parser holds caller-allocated storage inline and command payloads borrow from it, so ghostty_osc_end performs no global allocations (verified from Rust and C). - crash: aarch64 Linux minidump context fixed; macOS fatal signals record siginfo, registers and a fault-safe stack walk. - tripwire: selectionString map allocation-failure cleanup test ported. - libghostty-vt emits the versioned soname libghostty-vt.so.0 at build time (no patchelf needed at install). Test Plan: - cargo test --workspace (3342 passed) - just check-macos; just check-macos x86_64-apple-darwin; just check-windows; just check-wasm - cargo check --target aarch64-unknown-linux-gnu -p ghostty-crash
This commit is contained in:
1 parent
bc297c823b
commit
184cdd5c62
49 files changed
+1785
-557
No files matched your search
+139
-122
@@ -23,7 +23,7 @@ pub use parsers::{color, semantic_prompt};
|
||||
use crate::kitty::color as kitty_color;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Command {
|
||||
pub enum Command<'a> {
|
||||
/// This generally shouldn't ever be set except as an initial zero value.
|
||||
/// Ignore it.
|
||||
Invalid,
|
||||
@@ -36,23 +36,23 @@ pub enum Command {
|
||||
/// If title mode 2 is set or the terminal is setup for unconditional
|
||||
/// utf-8 titles text is interpreted as utf-8. Else text is interpreted
|
||||
/// as latin1.
|
||||
ChangeWindowTitle(Vec<u8>),
|
||||
ChangeWindowTitle(Bytes<'a>),
|
||||
|
||||
/// Set the icon of the terminal window. The name of the icon is not
|
||||
/// well defined, so this is currently ignored by Ghostty at the time
|
||||
/// of writing this. We just parse it so that we don't get parse errors
|
||||
/// in the log.
|
||||
ChangeWindowIcon(Vec<u8>),
|
||||
ChangeWindowIcon(Bytes<'a>),
|
||||
|
||||
/// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
|
||||
SemanticPrompt(SemanticPrompt),
|
||||
SemanticPrompt(SemanticPrompt<'a>),
|
||||
|
||||
/// Set or get clipboard contents. If data is null, then the current
|
||||
/// clipboard contents are sent to the pty. If data is set, this
|
||||
/// contents is set on the clipboard.
|
||||
ClipboardContents {
|
||||
kind: u8,
|
||||
data: Vec<u8>,
|
||||
data: Bytes<'a>,
|
||||
},
|
||||
|
||||
/// OSC 7. Reports the current working directory of the shell. This is
|
||||
@@ -62,7 +62,7 @@ pub enum Command {
|
||||
ReportPwd {
|
||||
/// The reported pwd value. This is not checked for validity. It should
|
||||
/// be a file URL but it is up to the caller to utilize this value.
|
||||
value: Vec<u8>,
|
||||
value: Bytes<'a>,
|
||||
},
|
||||
|
||||
/// OSC 22. Set the mouse shape. There doesn't seem to be a standard
|
||||
@@ -70,7 +70,7 @@ pub enum Command {
|
||||
/// are moving towards using the W3C CSS cursor names. For OSC parsing,
|
||||
/// we just parse whatever string is given.
|
||||
MouseShape {
|
||||
value: Vec<u8>,
|
||||
value: Bytes<'a>,
|
||||
},
|
||||
|
||||
/// OSC color operations to set, reset, or report color settings. Some OSCs
|
||||
@@ -92,14 +92,14 @@ pub enum Command {
|
||||
|
||||
/// Show a desktop notification (OSC 9 or OSC 777)
|
||||
ShowDesktopNotification {
|
||||
title: Vec<u8>,
|
||||
body: Vec<u8>,
|
||||
title: Bytes<'a>,
|
||||
body: Bytes<'a>,
|
||||
},
|
||||
|
||||
/// Start a hyperlink (OSC 8)
|
||||
HyperlinkStart {
|
||||
id: Option<Vec<u8>>,
|
||||
uri: Vec<u8>,
|
||||
id: Option<Bytes<'a>>,
|
||||
uri: Bytes<'a>,
|
||||
},
|
||||
|
||||
/// End a hyperlink (OSC 8)
|
||||
@@ -111,10 +111,10 @@ pub enum Command {
|
||||
},
|
||||
|
||||
/// ConEmu show GUI message box (OSC 9;2)
|
||||
ConemuShowMessageBox(Vec<u8>),
|
||||
ConemuShowMessageBox(Bytes<'a>),
|
||||
|
||||
/// ConEmu change tab title (OSC 9;3)
|
||||
ConemuChangeTabTitle(ConemuChangeTabTitle),
|
||||
ConemuChangeTabTitle(ConemuChangeTabTitle<'a>),
|
||||
|
||||
/// ConEmu progress report (OSC 9;4)
|
||||
ConemuProgressReport(ProgressReport),
|
||||
@@ -123,13 +123,13 @@ pub enum Command {
|
||||
ConemuWaitInput,
|
||||
|
||||
/// ConEmu GUI macro (OSC 9;6)
|
||||
ConemuGuimacro(Vec<u8>),
|
||||
ConemuGuimacro(Bytes<'a>),
|
||||
|
||||
/// ConEmu run process (OSC 9;7)
|
||||
ConemuRunProcess(Vec<u8>),
|
||||
ConemuRunProcess(Bytes<'a>),
|
||||
|
||||
/// ConEmu output environment variable (OSC 9;8)
|
||||
ConemuOutputEnvironmentVariable(Vec<u8>),
|
||||
ConemuOutputEnvironmentVariable(Bytes<'a>),
|
||||
|
||||
/// ConEmu XTerm keyboard and output emulation (OSC 9;10)
|
||||
/// https://conemu.github.io/en/TerminalModes.html
|
||||
@@ -145,26 +145,32 @@ pub enum Command {
|
||||
},
|
||||
|
||||
/// ConEmu comment (OSC 9;11)
|
||||
ConemuComment(Vec<u8>),
|
||||
ConemuComment(Bytes<'a>),
|
||||
|
||||
/// Kitty text sizing protocol (OSC 66)
|
||||
KittyTextSizing(parsers::kitty_text_sizing::OSC),
|
||||
KittyTextSizing(parsers::kitty_text_sizing::OSC<'a>),
|
||||
|
||||
KittyClipboardProtocol(KittyClipboardProtocol),
|
||||
KittyClipboardProtocol(KittyClipboardProtocol<'a>),
|
||||
|
||||
/// OSC 3008. Hierarchical context signalling (UAPI spec).
|
||||
/// https://uapi-group.org/specifications/specs/osc_context/
|
||||
ContextSignal(parsers::context_signal::Command),
|
||||
ContextSignal(parsers::context_signal::Command<'a>),
|
||||
}
|
||||
|
||||
pub type SemanticPrompt = parsers::semantic_prompt::Command;
|
||||
pub type KittyClipboardProtocol = parsers::kitty_clipboard_protocol::OSC;
|
||||
/// Payload bytes of a [`Command`]: borrowed from the parser's payload storage
|
||||
/// (Zig: slices into the parser's buffer), so producing a command does not
|
||||
/// allocate. Use [`Command::into_owned`] to keep one past the next parser
|
||||
/// operation.
|
||||
pub type Bytes<'a> = std::borrow::Cow<'a, [u8]>;
|
||||
|
||||
pub type SemanticPrompt<'a> = parsers::semantic_prompt::Command<'a>;
|
||||
pub type KittyClipboardProtocol<'a> = parsers::kitty_clipboard_protocol::OSC<'a>;
|
||||
|
||||
/// Payload of `Command::ConemuChangeTabTitle`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConemuChangeTabTitle {
|
||||
pub enum ConemuChangeTabTitle<'a> {
|
||||
Reset,
|
||||
Value(Vec<u8>),
|
||||
Value(Bytes<'a>),
|
||||
}
|
||||
|
||||
/// The discriminant of a [`Command`] (Zig `Command.Key`). Order matches Zig.
|
||||
@@ -197,7 +203,67 @@ pub enum Key {
|
||||
ContextSignal,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn own(b: Bytes<'_>) -> Bytes<'static> {
|
||||
Bytes::Owned(b.into_owned())
|
||||
}
|
||||
|
||||
impl Command<'_> {
|
||||
/// Detach the command from the parser's storage (copies the payloads),
|
||||
/// so it can outlive the next parser operation.
|
||||
pub fn into_owned(self) -> Command<'static> {
|
||||
match self {
|
||||
Command::Invalid => Command::Invalid,
|
||||
Command::ChangeWindowTitle(v) => Command::ChangeWindowTitle(own(v)),
|
||||
Command::ChangeWindowIcon(v) => Command::ChangeWindowIcon(own(v)),
|
||||
Command::SemanticPrompt(v) => Command::SemanticPrompt(v.into_owned()),
|
||||
Command::ClipboardContents { kind, data } => Command::ClipboardContents {
|
||||
kind,
|
||||
data: own(data),
|
||||
},
|
||||
Command::ReportPwd { value } => Command::ReportPwd { value: own(value) },
|
||||
Command::MouseShape { value } => Command::MouseShape { value: own(value) },
|
||||
Command::ColorOperation {
|
||||
op,
|
||||
requests,
|
||||
terminator,
|
||||
} => Command::ColorOperation {
|
||||
op,
|
||||
requests,
|
||||
terminator,
|
||||
},
|
||||
Command::KittyColorProtocol(v) => Command::KittyColorProtocol(v),
|
||||
Command::ShowDesktopNotification { title, body } => Command::ShowDesktopNotification {
|
||||
title: own(title),
|
||||
body: own(body),
|
||||
},
|
||||
Command::HyperlinkStart { id, uri } => Command::HyperlinkStart {
|
||||
id: id.map(own),
|
||||
uri: own(uri),
|
||||
},
|
||||
Command::HyperlinkEnd => Command::HyperlinkEnd,
|
||||
Command::ConemuSleep { duration_ms } => Command::ConemuSleep { duration_ms },
|
||||
Command::ConemuShowMessageBox(v) => Command::ConemuShowMessageBox(own(v)),
|
||||
Command::ConemuChangeTabTitle(v) => Command::ConemuChangeTabTitle(match v {
|
||||
ConemuChangeTabTitle::Reset => ConemuChangeTabTitle::Reset,
|
||||
ConemuChangeTabTitle::Value(b) => ConemuChangeTabTitle::Value(own(b)),
|
||||
}),
|
||||
Command::ConemuProgressReport(v) => Command::ConemuProgressReport(v),
|
||||
Command::ConemuWaitInput => Command::ConemuWaitInput,
|
||||
Command::ConemuGuimacro(v) => Command::ConemuGuimacro(own(v)),
|
||||
Command::ConemuRunProcess(v) => Command::ConemuRunProcess(own(v)),
|
||||
Command::ConemuOutputEnvironmentVariable(v) => {
|
||||
Command::ConemuOutputEnvironmentVariable(own(v))
|
||||
}
|
||||
Command::ConemuXtermEmulation { keyboard, output } => {
|
||||
Command::ConemuXtermEmulation { keyboard, output }
|
||||
}
|
||||
Command::ConemuComment(v) => Command::ConemuComment(own(v)),
|
||||
Command::KittyTextSizing(v) => Command::KittyTextSizing(v.into_owned()),
|
||||
Command::KittyClipboardProtocol(v) => Command::KittyClipboardProtocol(v.into_owned()),
|
||||
Command::ContextSignal(v) => Command::ContextSignal(v.into_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> Key {
|
||||
match self {
|
||||
Command::Invalid => Key::Invalid,
|
||||
@@ -374,8 +440,14 @@ pub trait ByteStorage: std::fmt::Debug + Send {
|
||||
/// Drop the contents and release any memory acquired by growth (Zig
|
||||
/// `reset` deinits the allocating writer).
|
||||
fn reset(&mut self);
|
||||
/// For `Parser: Clone`.
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage>;
|
||||
/// Number of accumulated bytes.
|
||||
fn len(&self) -> usize {
|
||||
self.bytes().len()
|
||||
}
|
||||
/// Whether nothing has been accumulated.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.bytes().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteStorage for Vec<u8> {
|
||||
@@ -393,74 +465,16 @@ impl ByteStorage for Vec<u8> {
|
||||
fn reset(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn ByteStorage> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
/// Maximum size of a "normal" OSC.
|
||||
pub const MAX_BUF: usize = 2048;
|
||||
|
||||
/// Payload buffer: a plain `Vec` (fast path) or custom allocator-backed
|
||||
/// storage.
|
||||
/// The OSC parser. `S` is the payload storage: a `Vec<u8>` on the global
|
||||
/// allocator by default, or caller-allocated storage (C API), in which case
|
||||
/// the parser holds it inline (no extra allocation).
|
||||
#[derive(Debug, Clone)]
|
||||
enum Buffer {
|
||||
Vec(Vec<u8>),
|
||||
Custom(Box<dyn ByteStorage>),
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
#[inline]
|
||||
fn bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
Buffer::Vec(v) => v,
|
||||
Buffer::Custom(c) => c.bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.bytes().len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_extend(&mut self, b: &[u8]) -> Result<(), StorageFull> {
|
||||
match self {
|
||||
Buffer::Vec(v) => {
|
||||
v.extend_from_slice(b);
|
||||
Ok(())
|
||||
}
|
||||
Buffer::Custom(c) => c.try_extend(b),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_reserve(&mut self, n: usize) -> Result<(), StorageFull> {
|
||||
match self {
|
||||
Buffer::Vec(v) => {
|
||||
v.reserve(n);
|
||||
Ok(())
|
||||
}
|
||||
Buffer::Custom(c) => c.try_reserve(n),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
match self {
|
||||
Buffer::Vec(v) => v.clear(),
|
||||
Buffer::Custom(c) => c.reset(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_vec(&self) -> Vec<u8> {
|
||||
self.bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parser {
|
||||
pub struct Parser<S: ByteStorage = Vec<u8>> {
|
||||
/// Whether an allocator is available, i.e. whether data longer than
|
||||
/// MAX_BUF may be accepted for some commands (e.g. OSC 52), and whether
|
||||
/// commands that require an allocator are processed. (Zig:
|
||||
@@ -471,7 +485,7 @@ pub struct Parser {
|
||||
pub state: State,
|
||||
|
||||
/// Buffer for temporary storage of OSC data
|
||||
buffer: Buffer,
|
||||
buffer: S,
|
||||
writer: Writer,
|
||||
}
|
||||
|
||||
@@ -482,25 +496,27 @@ impl Default for Parser {
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
/// Maximum size of a "normal" OSC.
|
||||
pub const MAX_BUF: usize = 2048;
|
||||
|
||||
pub fn init(alloc: bool) -> Parser {
|
||||
Parser {
|
||||
alloc,
|
||||
state: State::Start,
|
||||
buffer: Buffer::Vec(Vec::new()),
|
||||
buffer: Vec::new(),
|
||||
writer: Writer::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ByteStorage> Parser<S> {
|
||||
/// Maximum size of a "normal" OSC.
|
||||
pub const MAX_BUF: usize = MAX_BUF;
|
||||
|
||||
/// Like [`Parser::init`] with an allocator-backed payload buffer
|
||||
/// (`alloc` is true: an allocator is available).
|
||||
pub fn with_storage(storage: Box<dyn ByteStorage>) -> Parser {
|
||||
pub fn with_storage(storage: S) -> Parser<S> {
|
||||
Parser {
|
||||
alloc: true,
|
||||
state: State::Start,
|
||||
buffer: Buffer::Custom(storage),
|
||||
buffer: storage,
|
||||
writer: Writer::None,
|
||||
}
|
||||
}
|
||||
@@ -549,11 +565,11 @@ impl Parser {
|
||||
}
|
||||
|
||||
/// Helper for the per-command parsers. Mirrors the Zig prologue
|
||||
/// `writer orelse invalid; writer.writeByte(0) catch invalid;
|
||||
/// writer.buffered()`: returns a copy of the accumulated data (without
|
||||
/// the NUL terminator) or `None` (and marks the parser invalid) if no
|
||||
/// data was being accumulated or there was no room for the terminator.
|
||||
pub fn data(&mut self) -> Option<Vec<u8>> {
|
||||
/// `writer orelse invalid; writer.writeByte(0) catch invalid`: returns
|
||||
/// `None` (and marks the parser invalid) if no data was being accumulated
|
||||
/// or there was no room for the NUL terminator. On success the payload
|
||||
/// is available, borrowed and without copying, from [`Parser::payload`].
|
||||
pub fn data(&mut self) -> Option<()> {
|
||||
if self.writer == Writer::None {
|
||||
self.state = State::Invalid;
|
||||
return None;
|
||||
@@ -563,29 +579,33 @@ impl Parser {
|
||||
self.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
Some(self.buffer.to_vec())
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Zig `writer.writeByte(0)` capacity check: when the fixed buffer is
|
||||
/// full there is no room for the NUL terminator, so the parser becomes
|
||||
/// invalid and this returns false.
|
||||
pub fn reserve_nul(&mut self) -> bool {
|
||||
if self.writer == Writer::Fixed && self.buffer.len() >= Self::MAX_BUF {
|
||||
self.state = State::Invalid;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
/// Whether `writer.writeByte(0)` would succeed: when the fixed buffer is
|
||||
/// full there is no room for the NUL terminator. Pure so that it can be
|
||||
/// asked while the payload is borrowed; the caller returns `None`, which
|
||||
/// invalidates the OSC.
|
||||
pub fn reserve_nul(&self) -> bool {
|
||||
!(self.writer == Writer::Fixed && self.buffer.len() >= Self::MAX_BUF)
|
||||
}
|
||||
|
||||
/// Like [`Parser::data`] but without reserving room for the NUL
|
||||
/// terminator (for parsers that, like Zig's color parsers, read
|
||||
/// `writer.buffered()` directly).
|
||||
pub fn buffered(&mut self) -> Option<Vec<u8>> {
|
||||
pub fn buffered(&mut self) -> Option<()> {
|
||||
if self.writer == Writer::None {
|
||||
self.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
Some(self.buffer.to_vec())
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The accumulated OSC payload (Zig `writer.buffered()`), borrowed from
|
||||
/// the parser's storage. Commands built from it borrow it too, which is
|
||||
/// why producing a command never allocates.
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
self.buffer.bytes()
|
||||
}
|
||||
|
||||
/// Whether any payload writer has been set up (Zig `parser.writer != null`).
|
||||
@@ -871,7 +891,7 @@ impl Parser {
|
||||
/// is None, then no valid command was found. The optional terminator_ch
|
||||
/// is the final character in the OSC sequence. This is used to determine
|
||||
/// the response terminator.
|
||||
pub fn end(&mut self, terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn end(&mut self, terminator_ch: Option<u8>) -> Option<Command<'_>> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use State::*;
|
||||
match self.state {
|
||||
@@ -953,17 +973,14 @@ mod next_slice_tests {
|
||||
fn reset(&mut self) {
|
||||
self.data.clear();
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn ByteStorage> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_failure_invalidates_osc() {
|
||||
let mut p = Parser::with_storage(Box::new(Limited {
|
||||
let mut p = Parser::with_storage(Limited {
|
||||
data: Vec::new(),
|
||||
cap: 8,
|
||||
}));
|
||||
});
|
||||
for b in b"0;abcdefghijkl" {
|
||||
p.next(*b);
|
||||
}
|
||||
@@ -976,7 +993,7 @@ mod next_slice_tests {
|
||||
p.next(*b);
|
||||
}
|
||||
let cmd = p.end(Some(0x07)).expect("command");
|
||||
assert!(matches!(cmd, Command::ChangeWindowTitle(t) if t == b"abc"));
|
||||
assert!(matches!(cmd, Command::ChangeWindowTitle(t) if &t[..] == b"abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
//! OSC 1 (ZIG/src/terminal/osc/parsers/change_window_icon.zig).
|
||||
use crate::osc::{Command, Parser};
|
||||
use crate::osc::{ByteStorage, Command, Parser};
|
||||
|
||||
/// Parse OSC 1
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
let data = parser.data()?;
|
||||
Some(Command::ChangeWindowIcon(data))
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
parser.data()?;
|
||||
Some(Command::ChangeWindowIcon(parser.payload().into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -19,6 +22,6 @@ mod tests {
|
||||
p.next(*c);
|
||||
}
|
||||
let cmd = p.end(None).unwrap();
|
||||
assert_eq!(cmd, Command::ChangeWindowIcon(b"ab".to_vec()));
|
||||
assert_eq!(cmd, Command::ChangeWindowIcon(b"ab".to_vec().into()));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! OSC 0 and OSC 2 (ZIG/src/terminal/osc/parsers/change_window_title.zig).
|
||||
use crate::osc::{Command, Parser};
|
||||
use crate::osc::{ByteStorage, Command, Parser};
|
||||
|
||||
/// Parse OSC 0 and OSC 2
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
let data = parser.data()?;
|
||||
Some(Command::ChangeWindowTitle(data))
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
parser.data()?;
|
||||
Some(Command::ChangeWindowTitle(parser.payload().into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -24,7 +27,7 @@ mod tests {
|
||||
feed(&mut p, b"0;ab");
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ChangeWindowTitle(b"ab".to_vec())
|
||||
Command::ChangeWindowTitle(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +35,7 @@ mod tests {
|
||||
fn osc_0_longer_than_buffer() {
|
||||
let mut p = Parser::init(false);
|
||||
let mut input = b"0;".to_vec();
|
||||
input.extend(std::iter::repeat_n(b'a', Parser::MAX_BUF + 2));
|
||||
input.extend(std::iter::repeat_n(b'a', crate::osc::MAX_BUF + 2));
|
||||
feed(&mut p, &input);
|
||||
assert!(p.end(None).is_none());
|
||||
}
|
||||
@@ -40,18 +43,21 @@ mod tests {
|
||||
#[test]
|
||||
fn osc_0_one_shorter_than_buffer_length() {
|
||||
let mut p = Parser::init(false);
|
||||
let title = vec![b'a'; Parser::MAX_BUF - 1];
|
||||
let title = vec![b'a'; crate::osc::MAX_BUF - 1];
|
||||
let mut input = b"0;".to_vec();
|
||||
input.extend(&title);
|
||||
feed(&mut p, &input);
|
||||
assert_eq!(p.end(None).unwrap(), Command::ChangeWindowTitle(title));
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ChangeWindowTitle(title.into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_0_exactly_at_buffer_length() {
|
||||
let mut p = Parser::init(false);
|
||||
let mut input = b"0;".to_vec();
|
||||
input.extend(vec![b'a'; Parser::MAX_BUF]);
|
||||
input.extend(vec![b'a'; crate::osc::MAX_BUF]);
|
||||
feed(&mut p, &input);
|
||||
// This should be None because we always reserve space for a null terminator.
|
||||
assert!(p.end(None).is_none());
|
||||
@@ -63,7 +69,7 @@ mod tests {
|
||||
feed(&mut p, b"2;ab");
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ChangeWindowTitle(b"ab".to_vec())
|
||||
Command::ChangeWindowTitle(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +85,7 @@ mod tests {
|
||||
feed(&mut p, &[0xE2, 0x80, 0x90]);
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ChangeWindowTitle("— ‐".as_bytes().to_vec())
|
||||
Command::ChangeWindowTitle("— ‐".as_bytes().to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,6 +93,9 @@ mod tests {
|
||||
fn osc_2_change_window_title_empty() {
|
||||
let mut p = Parser::init(false);
|
||||
feed(&mut p, b"2;");
|
||||
assert_eq!(p.end(None).unwrap(), Command::ChangeWindowTitle(Vec::new()));
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ChangeWindowTitle(Vec::new().into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
//! OSC 52 (ZIG/src/terminal/osc/parsers/clipboard_operation.zig).
|
||||
use crate::osc::{Command, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State};
|
||||
|
||||
/// Parse OSC 52
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S52);
|
||||
let data = parser.data()?;
|
||||
// Zig's `data` includes the NUL terminator, so `data.len == 1` there
|
||||
// means the payload is empty.
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
if data[0] == b';' {
|
||||
Some(Command::ClipboardContents {
|
||||
kind: b'c',
|
||||
data: data[1..].to_vec(),
|
||||
})
|
||||
} else {
|
||||
if data.len() < 2 {
|
||||
parser.data()?;
|
||||
let (kind, skip) = {
|
||||
let data = parser.payload();
|
||||
// Zig's `data` includes the NUL terminator, so `data.len == 1` there
|
||||
// means the payload is empty.
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
if data[1] != b';' {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
if data[0] == b';' {
|
||||
(b'c', 1)
|
||||
} else {
|
||||
if data.len() < 2 || data[1] != b';' {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
(data[0], 2)
|
||||
}
|
||||
Some(Command::ClipboardContents {
|
||||
kind: data[0],
|
||||
data: data[2..].to_vec(),
|
||||
})
|
||||
}
|
||||
};
|
||||
Some(Command::ClipboardContents {
|
||||
kind,
|
||||
data: parser.payload()[skip..].into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -37,12 +37,12 @@ mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
fn run(alloc: bool, input: &[u8]) -> Option<Command> {
|
||||
fn run(alloc: bool, input: &[u8]) -> Option<Command<'static>> {
|
||||
let mut p = Parser::init(alloc);
|
||||
for c in input {
|
||||
p.next(*c);
|
||||
}
|
||||
p.end(None)
|
||||
p.end(None).map(Command::into_owned)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
run(false, b"52;s;?").unwrap(),
|
||||
Command::ClipboardContents {
|
||||
kind: b's',
|
||||
data: b"?".to_vec()
|
||||
data: b"?".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ mod tests {
|
||||
run(false, b"52;;?").unwrap(),
|
||||
Command::ClipboardContents {
|
||||
kind: b'c',
|
||||
data: b"?".to_vec()
|
||||
data: b"?".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ mod tests {
|
||||
run(true, b"52;s;?").unwrap(),
|
||||
Command::ClipboardContents {
|
||||
kind: b's',
|
||||
data: b"?".to_vec()
|
||||
data: b"?".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ mod tests {
|
||||
run(false, b"52;;").unwrap(),
|
||||
Command::ClipboardContents {
|
||||
kind: b'c',
|
||||
data: Vec::new()
|
||||
data: Vec::new().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::{
|
||||
color::{Dynamic as DynamicColor, Rgb, Special as SpecialColor},
|
||||
osc::{Command, Parser, State, Terminator},
|
||||
osc::{ByteStorage, Command, Parser, State, Terminator},
|
||||
};
|
||||
|
||||
/// The possible operations we support for colors.
|
||||
@@ -36,17 +36,23 @@ pub enum Operation {
|
||||
}
|
||||
|
||||
/// Parse OSCs 4, 5, 10-19, 104, 110-119
|
||||
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
if !parser.alloc {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
// If we've collected any extra data parse that, otherwise use an empty
|
||||
// string.
|
||||
let data = if parser.has_writer() {
|
||||
parser.buffered()?
|
||||
if parser.has_writer() {
|
||||
parser.buffered()?;
|
||||
}
|
||||
let data: &[u8] = if parser.has_writer() {
|
||||
parser.payload()
|
||||
} else {
|
||||
Vec::new()
|
||||
&[]
|
||||
};
|
||||
// Check and make sure that we're parsing the correct OSCs
|
||||
let op = match parser.state {
|
||||
@@ -80,7 +86,7 @@ pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command>
|
||||
};
|
||||
Some(Command::ColorOperation {
|
||||
op,
|
||||
requests: parse_color(op, &data),
|
||||
requests: parse_color(op, data),
|
||||
terminator: Terminator::init(terminator_ch),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! Each context has an identifier and metadata fields. Contexts are hierarchical
|
||||
//! and form a stack.
|
||||
|
||||
use crate::osc::{Command as OscCommand, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command as OscCommand, Parser, State};
|
||||
|
||||
/// Maximum length of a context identifier (per spec).
|
||||
const MAX_CONTEXT_ID_LEN: usize = 64;
|
||||
@@ -21,14 +21,14 @@ pub enum Action {
|
||||
|
||||
/// A single OSC 3008 context signal command.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Command {
|
||||
pub struct Command<'a> {
|
||||
pub action: Action,
|
||||
/// The context identifier. Must be 1-64 characters in the 32..126 byte range.
|
||||
pub id: Vec<u8>,
|
||||
pub id: crate::osc::Bytes<'a>,
|
||||
/// Raw unparsed metadata fields after the context ID.
|
||||
/// Fields are semicolon-separated key=value pairs.
|
||||
/// Parsed lazily via `read_option`.
|
||||
pub metadata: Vec<u8>,
|
||||
pub metadata: crate::osc::Bytes<'a>,
|
||||
}
|
||||
|
||||
/// Value of a decoded metadata field (Zig `Field.Type()`).
|
||||
@@ -40,7 +40,15 @@ pub enum FieldValue<'a> {
|
||||
Str(&'a [u8]),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
impl Command<'_> {
|
||||
pub fn into_owned(self) -> Command<'static> {
|
||||
Command {
|
||||
action: self.action,
|
||||
id: super::super::own(self.id),
|
||||
metadata: super::super::own(self.metadata),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a metadata field value from the raw fields string.
|
||||
/// Returns None if the field is not present or malformed.
|
||||
pub fn read_option(&self, option: Field) -> Option<FieldValue<'_>> {
|
||||
@@ -234,71 +242,83 @@ impl Field {
|
||||
/// Expected data format (after "3008;" prefix has been consumed by the state machine):
|
||||
/// start=<id>[;<field>=<value>]*
|
||||
/// end=<id>[;<field>=<value>]*
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscCommand> {
|
||||
let data = parser.buffered()?;
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<OscCommand<'_>> {
|
||||
parser.buffered()?;
|
||||
|
||||
// Determine the action (start= or end=)
|
||||
let (action, prefix_len) = if data.starts_with(b"start=") {
|
||||
(Action::Start, b"start=".len())
|
||||
} else if data.starts_with(b"end=") {
|
||||
(Action::End, b"end=".len())
|
||||
} else {
|
||||
log::warn!(
|
||||
"OSC 3008: expected 'start=' or 'end=' prefix, got: {}",
|
||||
String::from_utf8_lossy(&data[..data.len().min(10)])
|
||||
);
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Skip past the "start=" or "end=" prefix
|
||||
let rest = &data[prefix_len..];
|
||||
|
||||
if rest.is_empty() {
|
||||
log::warn!("OSC 3008: missing context ID");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the context ID (up to the first semicolon or end of data)
|
||||
let id_end = rest.iter().position(|&c| c == b';').unwrap_or(rest.len());
|
||||
let id = &rest[..id_end];
|
||||
|
||||
// Validate context ID length (1-64 chars per spec)
|
||||
if id.is_empty() || id.len() > MAX_CONTEXT_ID_LEN {
|
||||
log::warn!(
|
||||
"OSC 3008: context ID length {} out of range (1-{})",
|
||||
id.len(),
|
||||
MAX_CONTEXT_ID_LEN
|
||||
);
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Validate context ID characters (32..126 byte range per spec)
|
||||
for &c in id {
|
||||
if !(0x20..=0x7e).contains(&c) {
|
||||
log::warn!("OSC 3008: invalid character 0x{c:02x} in context ID");
|
||||
// Validate with a short-lived borrow; the command borrows the payload
|
||||
// again below (so marking the parser invalid does not overlap with the
|
||||
// returned borrow).
|
||||
let (action, id_range, meta_start) = {
|
||||
let data = parser.payload();
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract raw metadata fields (everything after the ID)
|
||||
let metadata = if id_end < rest.len() {
|
||||
rest[id_end + 1..].to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
// Determine the action (start= or end=)
|
||||
let (action, prefix_len) = if data.starts_with(b"start=") {
|
||||
(Action::Start, b"start=".len())
|
||||
} else if data.starts_with(b"end=") {
|
||||
(Action::End, b"end=".len())
|
||||
} else {
|
||||
log::warn!(
|
||||
"OSC 3008: expected 'start=' or 'end=' prefix, got: {}",
|
||||
String::from_utf8_lossy(&data[..data.len().min(10)])
|
||||
);
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Skip past the "start=" or "end=" prefix
|
||||
let rest = &data[prefix_len..];
|
||||
|
||||
if rest.is_empty() {
|
||||
log::warn!("OSC 3008: missing context ID");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the context ID (up to the first semicolon or end of data)
|
||||
let id_end = rest.iter().position(|&c| c == b';').unwrap_or(rest.len());
|
||||
let id = &rest[..id_end];
|
||||
|
||||
// Validate context ID length (1-64 chars per spec)
|
||||
if id.is_empty() || id.len() > MAX_CONTEXT_ID_LEN {
|
||||
log::warn!(
|
||||
"OSC 3008: context ID length {} out of range (1-{})",
|
||||
id.len(),
|
||||
MAX_CONTEXT_ID_LEN
|
||||
);
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Validate context ID characters (32..126 byte range per spec)
|
||||
for &c in id {
|
||||
if !(0x20..=0x7e).contains(&c) {
|
||||
log::warn!("OSC 3008: invalid character 0x{c:02x} in context ID");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata fields are everything after the ID.
|
||||
let meta_start = if id_end < rest.len() {
|
||||
prefix_len + id_end + 1
|
||||
} else {
|
||||
data.len()
|
||||
};
|
||||
(action, prefix_len..prefix_len + id_end, meta_start)
|
||||
};
|
||||
|
||||
let data = parser.payload();
|
||||
Some(OscCommand::ContextSignal(Command {
|
||||
action,
|
||||
id: id.to_vec(),
|
||||
metadata,
|
||||
id: data[id_range].into(),
|
||||
metadata: data[meta_start..].into(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -307,15 +327,15 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscComma
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &str) -> Option<OscCommand> {
|
||||
fn run(input: &str) -> Option<OscCommand<'static>> {
|
||||
let mut p = Parser::init(false);
|
||||
for &ch in input.as_bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
p.end(None)
|
||||
p.end(None).map(OscCommand::into_owned)
|
||||
}
|
||||
|
||||
fn cs(input: &str) -> Command {
|
||||
fn cs(input: &str) -> Command<'static> {
|
||||
match run(input) {
|
||||
Some(OscCommand::ContextSignal(c)) => c,
|
||||
other => panic!("unexpected {other:?}"),
|
||||
|
||||
@@ -1,57 +1,59 @@
|
||||
//! OSC 8 hyperlinks (ZIG/src/terminal/osc/parsers/hyperlink.zig).
|
||||
use crate::osc::{Command, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State};
|
||||
|
||||
/// Parse OSC 8 hyperlinks
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
let data = parser.data()?;
|
||||
let Some(s) = data.iter().position(|&b| b == b';') else {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
parser.data()?;
|
||||
|
||||
// Work out the layout with a short-lived borrow of the payload; the
|
||||
// command is built from a fresh borrow at the end (so that marking the
|
||||
// parser invalid above does not conflict with the returned borrow).
|
||||
let (s, id) = {
|
||||
let data = parser.payload();
|
||||
let Some(s) = data.iter().position(|&b| b == b';') else {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut id: Option<(usize, usize)> = None;
|
||||
|
||||
// The params section is `key=value` pairs separated by ':'. This
|
||||
// follows the Zig implementation byte for byte: ':' and the ';'
|
||||
// terminator are turned into NULs and the segments are walked by
|
||||
// searching for the next NUL starting one past the segment start. The
|
||||
// NUL-converted copy Zig makes in place is represented by `is_nul`
|
||||
// instead (no allocation).
|
||||
let is_nul = |i: usize| i == s || data[i] == b':' || data[i] == 0;
|
||||
let mut kv_start = 0;
|
||||
while kv_start <= s {
|
||||
let Some(kv_end) = (kv_start + 1..=s).find(|&i| is_nul(i)) else {
|
||||
break;
|
||||
};
|
||||
let kv = &data[kv_start..kv_end];
|
||||
let Some(v) = kv.iter().position(|&b| b == b'=') else {
|
||||
break;
|
||||
};
|
||||
let key = &kv[..v];
|
||||
if key == b"id" {
|
||||
if v + 1 < kv.len() {
|
||||
id = Some((kv_start + v + 1, kv_end));
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"unknown hyperlink option: '{}'",
|
||||
String::from_utf8_lossy(key)
|
||||
);
|
||||
}
|
||||
kv_start = kv_end + 1;
|
||||
}
|
||||
(s, id)
|
||||
};
|
||||
|
||||
let uri = data[s + 1..].to_vec();
|
||||
let mut id: Option<Vec<u8>> = None;
|
||||
|
||||
// The params section is `key=value` pairs separated by ':'. This follows
|
||||
// the Zig implementation byte for byte: ':' and the ';' terminator are
|
||||
// turned into NULs and the segments are walked by searching for the next
|
||||
// NUL starting one past the segment start.
|
||||
let mut kvs = data[..s].to_vec();
|
||||
kvs.push(0);
|
||||
for b in &mut kvs {
|
||||
if *b == b':' {
|
||||
*b = 0;
|
||||
}
|
||||
}
|
||||
let mut kv_start = 0;
|
||||
while kv_start < kvs.len() {
|
||||
let Some(kv_end) = kvs
|
||||
.iter()
|
||||
.skip(kv_start + 1)
|
||||
.position(|&b| b == 0)
|
||||
.map(|i| i + kv_start + 1)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let kv = &kvs[kv_start..kv_end];
|
||||
let Some(v) = kv.iter().position(|&b| b == b'=') else {
|
||||
break;
|
||||
};
|
||||
let key = &kv[..v];
|
||||
let value = &kv[v + 1..];
|
||||
if key == b"id" {
|
||||
if !value.is_empty() {
|
||||
id = Some(value.to_vec());
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"unknown hyperlink option: '{}'",
|
||||
String::from_utf8_lossy(key)
|
||||
);
|
||||
}
|
||||
kv_start = kv_end + 1;
|
||||
}
|
||||
|
||||
let data = parser.payload();
|
||||
let uri = &data[s + 1..];
|
||||
if uri.is_empty() {
|
||||
if id.is_some() {
|
||||
parser.state = State::Invalid;
|
||||
@@ -60,7 +62,11 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
return Some(Command::HyperlinkEnd);
|
||||
}
|
||||
|
||||
Some(Command::HyperlinkStart { id, uri })
|
||||
let data = parser.payload();
|
||||
Some(Command::HyperlinkStart {
|
||||
id: id.map(|(a, b)| data[a..b].into()),
|
||||
uri: data[s + 1..].into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -68,18 +74,18 @@ mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
fn run(input: &[u8]) -> Option<Command> {
|
||||
fn run(input: &[u8]) -> Option<Command<'_>> {
|
||||
let mut p = Parser::init(false);
|
||||
for c in input {
|
||||
p.next(*c);
|
||||
}
|
||||
p.end(Some(0x1b))
|
||||
p.end(Some(0x1b)).map(Command::into_owned)
|
||||
}
|
||||
|
||||
fn start(id: Option<&str>, uri: &str) -> Command {
|
||||
fn start(id: Option<&str>, uri: &str) -> Command<'static> {
|
||||
Command::HyperlinkStart {
|
||||
id: id.map(|s| s.as_bytes().to_vec()),
|
||||
uri: uri.as_bytes().to_vec(),
|
||||
id: id.map(|s| s.as_bytes().to_vec().into()),
|
||||
uri: uri.as_bytes().to_vec().into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! OSC 1337 (iTerm2) (ZIG/src/terminal/osc/parsers/iterm2.zig).
|
||||
|
||||
use crate::osc::{Command, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State};
|
||||
|
||||
/// iTerm2 OSC 1337 keys. Lookup is ASCII case-insensitive.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -90,13 +90,17 @@ fn lookup(s: &[u8]) -> Option<Key> {
|
||||
|
||||
/// Parse OSC 1337
|
||||
/// https://iterm2.com/documentation-escape-codes.html
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S1337);
|
||||
|
||||
let data = parser.data()?;
|
||||
parser.data()?;
|
||||
let data = parser.payload();
|
||||
|
||||
let (key_str, value): (&[u8], Option<&[u8]>) = match data.iter().position(|&b| b == b'=') {
|
||||
None => (&data[..], None),
|
||||
None => (data, None),
|
||||
Some(index) => (&data[..index], Some(&data[index + 1..])),
|
||||
};
|
||||
|
||||
@@ -138,7 +142,7 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
|
||||
Some(Command::ClipboardContents {
|
||||
kind: b'c',
|
||||
data: value.to_vec(),
|
||||
data: value.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,7 +152,7 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
return None;
|
||||
}
|
||||
Some(Command::ReportPwd {
|
||||
value: value.to_vec(),
|
||||
value: value.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,12 +168,12 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &str) -> Option<Command> {
|
||||
fn run(input: &str) -> Option<Command<'static>> {
|
||||
let mut p = Parser::init(true);
|
||||
for ch in input.bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
p.end(Some(0x1b))
|
||||
p.end(Some(0x1b)).map(Command::into_owned)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -257,7 +261,7 @@ mod tests {
|
||||
run("1337;Copy=:YWJjMTIz"),
|
||||
Some(Command::ClipboardContents {
|
||||
kind: b'c',
|
||||
data: b"YWJjMTIz".to_vec()
|
||||
data: b"YWJjMTIz".to_vec().into()
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -277,7 +281,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
run("1337;CurrentDir=abc123"),
|
||||
Some(Command::ReportPwd {
|
||||
value: b"abc123".to_vec()
|
||||
value: b"abc123".to_vec().into()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
//! https://rockorager.dev/misc/bracketed-paste-mime/
|
||||
//! (ZIG/src/terminal/osc/parsers/kitty_clipboard_protocol.zig)
|
||||
|
||||
use crate::osc::{Command, Parser, State, Terminator};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State, Terminator};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OSC {
|
||||
pub struct OSC<'a> {
|
||||
/// The raw metadata that was received. It can be parsed by using the
|
||||
/// `read_option` method (or the typed helpers).
|
||||
pub metadata: Vec<u8>,
|
||||
pub metadata: crate::osc::Bytes<'a>,
|
||||
/// The raw payload. It may be Base64 encoded, check the `e` option.
|
||||
pub payload: Option<Vec<u8>>,
|
||||
pub payload: Option<crate::osc::Bytes<'a>>,
|
||||
/// The terminator that was used in case we need to send a response.
|
||||
pub terminator: Terminator,
|
||||
}
|
||||
@@ -25,7 +25,15 @@ pub enum OptionValue<'a> {
|
||||
Type(Operation),
|
||||
}
|
||||
|
||||
impl OSC {
|
||||
impl OSC<'_> {
|
||||
pub fn into_owned(self) -> OSC<'static> {
|
||||
OSC {
|
||||
metadata: super::super::own(self.metadata),
|
||||
payload: self.payload.map(super::super::own),
|
||||
terminator: self.terminator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode an option from the metadata.
|
||||
pub fn read_option(&self, key: Opt) -> Option<OptionValue<'_>> {
|
||||
key.read(&self.metadata)
|
||||
@@ -283,14 +291,18 @@ fn parse_identifier(s: &[u8]) -> Option<&[u8]> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S5522);
|
||||
|
||||
let data = parser.buffered()?;
|
||||
parser.buffered()?;
|
||||
let data = parser.payload();
|
||||
|
||||
let (metadata, payload) = match data.iter().position(|&c| c == b';') {
|
||||
None => (data, None),
|
||||
Some(start) => (data[..start].to_vec(), Some(data[start + 1..].to_vec())),
|
||||
None => (data.into(), None),
|
||||
Some(start) => (data[..start].into(), Some(data[start + 1..].into())),
|
||||
};
|
||||
|
||||
Some(Command::KittyClipboardProtocol(OSC {
|
||||
@@ -305,13 +317,13 @@ pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command>
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &str) -> OSC {
|
||||
fn run(input: &str) -> OSC<'static> {
|
||||
let mut p = Parser::init(true);
|
||||
for &ch in input.as_bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
match p.end(Some(0x1b)) {
|
||||
Some(Command::KittyClipboardProtocol(o)) => o,
|
||||
Some(Command::KittyClipboardProtocol(o)) => o.into_owned(),
|
||||
other => panic!("unexpected {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -337,7 +349,7 @@ mod tests {
|
||||
assert_eq!(m.as_bytes(), &c.metadata[..]);
|
||||
}
|
||||
if let Some(p) = e.payload {
|
||||
assert_eq!(p.map(|s| s.as_bytes().to_vec()), c.payload);
|
||||
assert_eq!(p.map(str::as_bytes), c.payload.as_deref());
|
||||
}
|
||||
assert_eq!(e.id.map(str::as_bytes), c.id());
|
||||
assert_eq!(e.loc, c.loc());
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
use crate::{
|
||||
color::Rgb,
|
||||
kitty::color::{Kind, OSC, Request},
|
||||
osc::{Command, Parser, State, Terminator},
|
||||
osc::{ByteStorage, Command, Parser, State, Terminator},
|
||||
};
|
||||
|
||||
/// Parse OSC 21, the Kitty Color Protocol.
|
||||
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S21);
|
||||
|
||||
if !parser.alloc {
|
||||
@@ -18,8 +21,12 @@ pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command>
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
let data = parser.buffered()?;
|
||||
parser.buffered()?;
|
||||
|
||||
// The parse only reads the payload; failure marks the parser invalid
|
||||
// after the borrow ends.
|
||||
let data = parser.payload();
|
||||
let mut exceeded = false;
|
||||
let mut osc = OSC {
|
||||
list: Vec::new(),
|
||||
terminator: Terminator::init(terminator_ch),
|
||||
@@ -27,8 +34,8 @@ pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command>
|
||||
for kv in data.split(|&c| c == b';') {
|
||||
if osc.list.len() >= Kind::MAX * 2 {
|
||||
log::warn!("exceeded limit for number of keys in kitty color protocol, ignoring");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
exceeded = true;
|
||||
break;
|
||||
}
|
||||
let (k, rest) = match kv.iter().position(|&c| c == b'=') {
|
||||
Some(p) => (&kv[..p], &kv[p + 1..]),
|
||||
@@ -59,6 +66,10 @@ pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command>
|
||||
);
|
||||
}
|
||||
}
|
||||
if exceeded {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
Some(Command::KittyColorProtocol(osc))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
|
||||
//! (ZIG/src/terminal/osc/parsers/kitty_text_sizing.zig)
|
||||
|
||||
use crate::osc::{Command, Parser, State, encoding};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State, encoding};
|
||||
|
||||
pub const MAX_PAYLOAD_LENGTH: usize = 4096;
|
||||
|
||||
@@ -45,7 +45,7 @@ impl HAlign {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OSC {
|
||||
pub struct OSC<'a> {
|
||||
/// 1 - 7 (u3)
|
||||
pub scale: u8,
|
||||
/// 0 - 7 (u3), 0 means default
|
||||
@@ -56,7 +56,7 @@ pub struct OSC {
|
||||
pub denominator: u8,
|
||||
pub valign: VAlign,
|
||||
pub halign: HAlign,
|
||||
pub text: Vec<u8>,
|
||||
pub text: std::borrow::Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -91,7 +91,19 @@ fn parse_u4(value: &[u8]) -> Option<u8> {
|
||||
Some(acc as u8)
|
||||
}
|
||||
|
||||
impl OSC {
|
||||
impl OSC<'_> {
|
||||
pub fn into_owned(self) -> OSC<'static> {
|
||||
OSC {
|
||||
scale: self.scale,
|
||||
width: self.width,
|
||||
numerator: self.numerator,
|
||||
denominator: self.denominator,
|
||||
valign: self.valign,
|
||||
halign: self.halign,
|
||||
text: super::super::own(self.text),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, key: u8, value: &[u8]) -> Result<(), UpdateError> {
|
||||
// All values are numeric, so we can do a small hack here
|
||||
let v = parse_u4(value).ok_or(UpdateError::InvalidValue)?;
|
||||
@@ -119,31 +131,41 @@ impl OSC {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S66);
|
||||
|
||||
let data = parser.data()?;
|
||||
parser.data()?;
|
||||
|
||||
let Some(payload_start) = data.iter().position(|&c| c == b';') else {
|
||||
log::warn!("missing semicolon before payload");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
// Validate with a short-lived borrow (marking the parser invalid cannot
|
||||
// overlap with the borrow returned in the command below).
|
||||
let payload_start = {
|
||||
let data = parser.payload();
|
||||
let Some(payload_start) = data.iter().position(|&c| c == b';') else {
|
||||
log::warn!("missing semicolon before payload");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
let payload = &data[payload_start + 1..];
|
||||
|
||||
// Payload has to be a URL-safe UTF-8 string,
|
||||
// and be under the size limit.
|
||||
if payload.len() > MAX_PAYLOAD_LENGTH {
|
||||
log::warn!("payload is too long");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
if !encoding::is_safe_utf8(payload) {
|
||||
log::warn!("payload is not escape code safe UTF-8");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
payload_start
|
||||
};
|
||||
let payload = &data[payload_start + 1..];
|
||||
|
||||
// Payload has to be a URL-safe UTF-8 string,
|
||||
// and be under the size limit.
|
||||
if payload.len() > MAX_PAYLOAD_LENGTH {
|
||||
log::warn!("payload is too long");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
if !encoding::is_safe_utf8(payload) {
|
||||
log::warn!("payload is not escape code safe UTF-8");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = parser.payload();
|
||||
let mut cmd = OSC {
|
||||
scale: 1,
|
||||
width: 0,
|
||||
@@ -151,7 +173,7 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
denominator: 0,
|
||||
valign: VAlign::Top,
|
||||
halign: HAlign::Left,
|
||||
text: payload.to_vec(),
|
||||
text: data[payload_start + 1..].into(),
|
||||
};
|
||||
|
||||
// Parse any arguments if given
|
||||
@@ -191,15 +213,15 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &[u8]) -> Option<Command> {
|
||||
fn run(input: &[u8]) -> Option<Command<'static>> {
|
||||
let mut p = Parser::init(false);
|
||||
for &ch in input {
|
||||
p.next(ch);
|
||||
}
|
||||
p.end(Some(0x1b))
|
||||
p.end(Some(0x1b)).map(Command::into_owned)
|
||||
}
|
||||
|
||||
fn sizing(input: &[u8]) -> OSC {
|
||||
fn sizing(input: &[u8]) -> OSC<'static> {
|
||||
match run(input) {
|
||||
Some(Command::KittyTextSizing(o)) => o,
|
||||
other => panic!("unexpected {other:?}"),
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
//! OSC 22 (ZIG/src/terminal/osc/parsers/mouse_shape.zig).
|
||||
use crate::osc::{Command, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State};
|
||||
|
||||
/// Parse OSC 22
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
debug_assert_eq!(parser.state, State::S22);
|
||||
let data = parser.data()?;
|
||||
Some(Command::MouseShape { value: data })
|
||||
parser.data()?;
|
||||
Some(Command::MouseShape {
|
||||
value: parser.payload().into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -22,7 +27,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::MouseShape {
|
||||
value: b"pointer".to_vec()
|
||||
value: b"pointer".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
//! (ZIG/src/terminal/osc/parsers/osc9.zig).
|
||||
|
||||
use super::semantic_prompt;
|
||||
use crate::osc::{Command, ConemuChangeTabTitle, Parser, ProgressReport, ProgressState};
|
||||
use crate::osc::{
|
||||
ByteStorage,
|
||||
Command,
|
||||
ConemuChangeTabTitle,
|
||||
Parser,
|
||||
ProgressReport,
|
||||
ProgressState,
|
||||
};
|
||||
|
||||
/// Parse a base-10 unsigned integer from raw bytes (Zig `parseUnsigned`).
|
||||
fn parse_unsigned<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
|
||||
@@ -10,16 +17,20 @@ fn parse_unsigned<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
|
||||
}
|
||||
|
||||
/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension.
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
// Zig prologue: writer must exist. The NUL-terminator reservation is NOT
|
||||
// done here: Zig only appends the NUL (`writer.writeByte(0)`) in the
|
||||
// branches that return text, so full-buffer control commands (sleep,
|
||||
// xterm emulation, progress, ...) still parse.
|
||||
let data = parser.buffered()?;
|
||||
parser.buffered()?;
|
||||
let data = parser.payload();
|
||||
|
||||
// Check first to see if this is a ConEmu OSC
|
||||
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
|
||||
if let Some((cmd, needs_nul)) = conemu(&data) {
|
||||
if let Some((cmd, needs_nul)) = conemu(data) {
|
||||
if needs_nul && !parser.reserve_nul() {
|
||||
return None;
|
||||
}
|
||||
@@ -31,14 +42,14 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command>
|
||||
return None;
|
||||
}
|
||||
Some(Command::ShowDesktopNotification {
|
||||
title: Vec::new(),
|
||||
body: data,
|
||||
title: std::borrow::Cow::Borrowed(&[]),
|
||||
body: data.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns None where the Zig code does `break :conemu`. The flag is true
|
||||
/// when the Zig branch appends a NUL terminator (needs buffer room).
|
||||
fn conemu(data: &[u8]) -> Option<(Command, bool)> {
|
||||
fn conemu(data: &[u8]) -> Option<(Command<'_>, bool)> {
|
||||
let r = conemu_inner(data)?;
|
||||
let needs_nul = matches!(
|
||||
r,
|
||||
@@ -53,11 +64,11 @@ fn conemu(data: &[u8]) -> Option<(Command, bool)> {
|
||||
Some((r, needs_nul))
|
||||
}
|
||||
|
||||
fn conemu_inner(data: &[u8]) -> Option<Command> {
|
||||
fn conemu_inner(data: &[u8]) -> Option<Command<'_>> {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let msg = |skip: usize| data[skip..].to_vec();
|
||||
let msg = |skip: usize| -> crate::osc::Bytes<'_> { data[skip..].into() };
|
||||
match data[0] {
|
||||
// Check for OSC 9;1 9;10 9;11 9;12
|
||||
b'1' => {
|
||||
@@ -210,18 +221,18 @@ fn conemu_inner(data: &[u8]) -> Option<Command> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &str) -> Command {
|
||||
fn run(input: &str) -> Command<'static> {
|
||||
let mut p = Parser::init(false);
|
||||
for ch in input.bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
p.end(Some(0x1b)).unwrap()
|
||||
p.end(Some(0x1b)).unwrap().into_owned()
|
||||
}
|
||||
|
||||
fn notif(input: &str, body: &str) {
|
||||
match run(input) {
|
||||
Command::ShowDesktopNotification { title, body: b } => {
|
||||
assert_eq!(title, b"");
|
||||
assert_eq!(&title[..], b"");
|
||||
assert_eq!(b, body.as_bytes());
|
||||
}
|
||||
c => panic!("unexpected {c:?}"),
|
||||
@@ -242,17 +253,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn run_opt(input: &str) -> Option<Command> {
|
||||
fn run_opt(input: &str) -> Option<Command<'static>> {
|
||||
let mut p = Parser::init(false);
|
||||
for ch in input.bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
p.end(Some(0x1b))
|
||||
p.end(Some(0x1b)).map(Command::into_owned)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_buffer_control_commands_do_not_need_nul() {
|
||||
let fill = Parser::MAX_BUF;
|
||||
let fill = crate::osc::MAX_BUF;
|
||||
// OSC 9;1 sleep with a buffer that has no room for a NUL: Zig never
|
||||
// appends one for this command, so it still parses (default 100ms).
|
||||
let input = format!("9;1;{}", "x".repeat(fill - 2));
|
||||
@@ -267,7 +278,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn full_buffer_text_commands_are_invalid() {
|
||||
let fill = Parser::MAX_BUF;
|
||||
let fill = crate::osc::MAX_BUF;
|
||||
// These append a NUL in Zig, which fails on a full buffer.
|
||||
let input = format!("9;6;{}", "a".repeat(fill - 2));
|
||||
assert_eq!(run_opt(&input), None);
|
||||
@@ -322,7 +333,7 @@ mod tests {
|
||||
fn osc_9_2_conemu_message_box() {
|
||||
assert_eq!(
|
||||
run("9;2;hello world"),
|
||||
Command::ConemuShowMessageBox(b"hello world".to_vec())
|
||||
Command::ConemuShowMessageBox(b"hello world".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -333,14 +344,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn osc_9_2_conemu_message_box_empty_message() {
|
||||
assert_eq!(run("9;2;"), Command::ConemuShowMessageBox(Vec::new()));
|
||||
assert_eq!(
|
||||
run("9;2;"),
|
||||
Command::ConemuShowMessageBox(Vec::new().into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_9_2_conemu_message_box_spaces_only_message() {
|
||||
assert_eq!(
|
||||
run("9;2; "),
|
||||
Command::ConemuShowMessageBox(b" ".to_vec())
|
||||
Command::ConemuShowMessageBox(b" ".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,7 +372,7 @@ mod tests {
|
||||
fn osc_9_3_conemu_change_tab_title() {
|
||||
assert_eq!(
|
||||
run("9;3;foo bar"),
|
||||
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b"foo bar".to_vec()))
|
||||
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b"foo bar".to_vec().into()))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,7 +388,7 @@ mod tests {
|
||||
fn osc_9_3_conemu_change_tab_title_spaces_only() {
|
||||
assert_eq!(
|
||||
run("9;3; "),
|
||||
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b" ".to_vec()))
|
||||
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b" ".to_vec().into()))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -488,12 +502,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn osc_9_6_conemu_guimacro_1() {
|
||||
assert_eq!(run("9;6;a"), Command::ConemuGuimacro(b"a".to_vec()));
|
||||
assert_eq!(run("9;6;a"), Command::ConemuGuimacro(b"a".to_vec().into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_9_6_conemu_guimacro_2() {
|
||||
assert_eq!(run("9;6;ab"), Command::ConemuGuimacro(b"ab".to_vec()));
|
||||
assert_eq!(
|
||||
run("9;6;ab"),
|
||||
Command::ConemuGuimacro(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -503,12 +520,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn osc_9_7_conemu_run_process_1() {
|
||||
assert_eq!(run("9;7;ab"), Command::ConemuRunProcess(b"ab".to_vec()));
|
||||
assert_eq!(
|
||||
run("9;7;ab"),
|
||||
Command::ConemuRunProcess(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_9_7_conemu_run_process_2() {
|
||||
assert_eq!(run("9;7;"), Command::ConemuRunProcess(Vec::new()));
|
||||
assert_eq!(run("9;7;"), Command::ConemuRunProcess(Vec::new().into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -520,7 +540,7 @@ mod tests {
|
||||
fn osc_9_8_conemu_output_environment_variable_1() {
|
||||
assert_eq!(
|
||||
run("9;8;ab"),
|
||||
Command::ConemuOutputEnvironmentVariable(b"ab".to_vec())
|
||||
Command::ConemuOutputEnvironmentVariable(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,7 +548,7 @@ mod tests {
|
||||
fn osc_9_8_conemu_output_environment_variable_2() {
|
||||
assert_eq!(
|
||||
run("9;8;"),
|
||||
Command::ConemuOutputEnvironmentVariable(Vec::new())
|
||||
Command::ConemuOutputEnvironmentVariable(Vec::new().into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,7 +562,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
run("9;9;ab"),
|
||||
Command::ReportPwd {
|
||||
value: b"ab".to_vec()
|
||||
value: b"ab".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -594,7 +614,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn osc_9_11_conemu_comment() {
|
||||
assert_eq!(run("9;11;ab"), Command::ConemuComment(b"ab".to_vec()));
|
||||
assert_eq!(
|
||||
run("9;11;ab"),
|
||||
Command::ConemuComment(b"ab".to_vec().into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! OSC 7 (ZIG/src/terminal/osc/parsers/report_pwd.zig).
|
||||
use crate::osc::{Command, Parser};
|
||||
use crate::osc::{ByteStorage, Command, Parser};
|
||||
|
||||
/// Parse OSC 7
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
let data = parser.data()?;
|
||||
Some(Command::ReportPwd { value: data })
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
parser.data()?;
|
||||
Some(Command::ReportPwd {
|
||||
value: parser.payload().into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -21,7 +26,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ReportPwd {
|
||||
value: b"file:///tmp/example".to_vec()
|
||||
value: b"file:///tmp/example".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -34,7 +39,9 @@ mod tests {
|
||||
}
|
||||
assert_eq!(
|
||||
p.end(None).unwrap(),
|
||||
Command::ReportPwd { value: Vec::new() }
|
||||
Command::ReportPwd {
|
||||
value: Vec::new().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
//! OSC 777 (ZIG/src/terminal/osc/parsers/rxvt_extension.zig).
|
||||
use crate::osc::{Command, Parser, State};
|
||||
use crate::osc::{ByteStorage, Command, Parser, State};
|
||||
|
||||
/// Parse OSC 777
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||
let data = parser.data()?;
|
||||
let Some(k) = data.iter().position(|&b| b == b';') else {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
let ext = &data[..k];
|
||||
if ext != b"notify" {
|
||||
log::warn!("unknown rxvt extension: {}", String::from_utf8_lossy(ext));
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
let Some(t) = data[k + 1..]
|
||||
.iter()
|
||||
.position(|&b| b == b';')
|
||||
.map(|i| i + k + 1)
|
||||
else {
|
||||
log::warn!("rxvt notify extension is missing the title");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<Command<'_>> {
|
||||
parser.data()?;
|
||||
let (k, t) = {
|
||||
let data = parser.payload();
|
||||
let Some(k) = data.iter().position(|&b| b == b';') else {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
let ext = &data[..k];
|
||||
if ext != b"notify" {
|
||||
log::warn!("unknown rxvt extension: {}", String::from_utf8_lossy(ext));
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
let Some(t) = data[k + 1..]
|
||||
.iter()
|
||||
.position(|&b| b == b';')
|
||||
.map(|i| i + k + 1)
|
||||
else {
|
||||
log::warn!("rxvt notify extension is missing the title");
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
};
|
||||
(k, t)
|
||||
};
|
||||
let data = parser.payload();
|
||||
Some(Command::ShowDesktopNotification {
|
||||
title: data[k + 1..t].to_vec(),
|
||||
body: data[t + 1..].to_vec(),
|
||||
title: data[k + 1..t].into(),
|
||||
body: data[t + 1..].into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,8 +51,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
p.end(Some(0x1b)).unwrap(),
|
||||
Command::ShowDesktopNotification {
|
||||
title: b"Title".to_vec(),
|
||||
body: b"Body".to_vec()
|
||||
title: b"Title".to_vec().into(),
|
||||
body: b"Body".to_vec().into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! <https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md>
|
||||
|
||||
use crate::{
|
||||
osc::{Command as OscCommand, Parser, State},
|
||||
osc::{ByteStorage, Command as OscCommand, Parser, State},
|
||||
string_encoding::{printf_q_decode, url_percent_decode},
|
||||
};
|
||||
|
||||
@@ -19,9 +19,9 @@ pub type DecodeError = crate::string_encoding::Error;
|
||||
/// all except one do and the spec does also say to ignore unknown
|
||||
/// options. So, I think this is a fair interpretation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Command {
|
||||
pub struct Command<'a> {
|
||||
pub action: Action,
|
||||
pub options_unvalidated: Vec<u8>,
|
||||
pub options_unvalidated: crate::osc::Bytes<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -36,11 +36,18 @@ pub enum Action {
|
||||
EndCommand, // 'D'
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn init(action: Action) -> Command {
|
||||
impl<'a> Command<'a> {
|
||||
pub fn into_owned(self) -> Command<'static> {
|
||||
Command {
|
||||
action: self.action,
|
||||
options_unvalidated: super::super::own(self.options_unvalidated),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(action: Action) -> Command<'a> {
|
||||
Command {
|
||||
action,
|
||||
options_unvalidated: Vec::new(),
|
||||
options_unvalidated: std::borrow::Cow::Borrowed(&[]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,43 +371,54 @@ pub enum Redraw {
|
||||
}
|
||||
|
||||
/// Parse OSC 133, semantic prompts
|
||||
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscCommand> {
|
||||
let data = parser.buffered()?;
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
pub fn parse<S: ByteStorage>(
|
||||
parser: &mut Parser<S>,
|
||||
_terminator_ch: Option<u8>,
|
||||
) -> Option<OscCommand<'_>> {
|
||||
parser.buffered()?;
|
||||
|
||||
// All valid cases terminate within this block. Any fallthroughs
|
||||
// are invalid.
|
||||
let action = match data[0] {
|
||||
b'A' => Action::FreshLineNewPrompt,
|
||||
b'B' => Action::EndPromptStartInput,
|
||||
b'I' => Action::EndPromptStartInputTerminateEol,
|
||||
b'C' => Action::EndInputStartOutput,
|
||||
b'D' => Action::EndCommand,
|
||||
b'N' => Action::NewCommand,
|
||||
b'P' => Action::PromptStart,
|
||||
b'L' => {
|
||||
if data.len() > 1 {
|
||||
// Validate with a short-lived borrow (marking the parser invalid cannot
|
||||
// overlap with the borrow handed to the command below).
|
||||
let (action, has_options) = {
|
||||
let data = parser.payload();
|
||||
if data.is_empty() {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
|
||||
// All valid cases terminate within this block. Any fallthroughs
|
||||
// are invalid.
|
||||
let action = match data[0] {
|
||||
b'A' => Action::FreshLineNewPrompt,
|
||||
b'B' => Action::EndPromptStartInput,
|
||||
b'I' => Action::EndPromptStartInputTerminateEol,
|
||||
b'C' => Action::EndInputStartOutput,
|
||||
b'D' => Action::EndCommand,
|
||||
b'N' => Action::NewCommand,
|
||||
b'P' => Action::PromptStart,
|
||||
b'L' => {
|
||||
if data.len() > 1 {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
return Some(OscCommand::SemanticPrompt(Command::init(Action::FreshLine)));
|
||||
}
|
||||
_ => {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
return Some(OscCommand::SemanticPrompt(Command::init(Action::FreshLine)));
|
||||
}
|
||||
_ => {
|
||||
};
|
||||
|
||||
if data.len() > 1 && data[1] != b';' {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
(action, data.len() > 1)
|
||||
};
|
||||
|
||||
let mut cmd = Command::init(action);
|
||||
if data.len() > 1 {
|
||||
if data[1] != b';' {
|
||||
parser.state = State::Invalid;
|
||||
return None;
|
||||
}
|
||||
cmd.options_unvalidated = data[2..].to_vec();
|
||||
if has_options {
|
||||
cmd.options_unvalidated = parser.payload()[2..].into();
|
||||
}
|
||||
Some(OscCommand::SemanticPrompt(cmd))
|
||||
}
|
||||
@@ -410,19 +428,19 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscComma
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(input: &str) -> Option<Command> {
|
||||
fn run(input: &str) -> Option<Command<'static>> {
|
||||
let mut p = Parser::init(false);
|
||||
for ch in input.bytes() {
|
||||
p.next(ch);
|
||||
}
|
||||
match p.end(None) {
|
||||
Some(OscCommand::SemanticPrompt(c)) => Some(c),
|
||||
Some(OscCommand::SemanticPrompt(c)) => Some(c.into_owned()),
|
||||
Some(c) => panic!("unexpected {c:?}"),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cmdline(input: &str) -> (Command, Result<Vec<u8>, DecodeError>) {
|
||||
fn cmdline(input: &str) -> (Command<'static>, Result<Vec<u8>, DecodeError>) {
|
||||
let cmd = run(input).unwrap();
|
||||
assert_eq!(cmd.action, Action::EndInputStartOutput);
|
||||
let mut w = Vec::new();
|
||||
|
||||
@@ -242,7 +242,7 @@ pub enum Action {
|
||||
EscDispatch(Esc),
|
||||
|
||||
/// Execute the OSC command.
|
||||
OscDispatch(osc::Command),
|
||||
OscDispatch(osc::Command<'static>),
|
||||
|
||||
/// DCS-related events.
|
||||
DcsHook(Dcs),
|
||||
@@ -358,7 +358,7 @@ impl Parser {
|
||||
match self.state {
|
||||
State::OscString => {
|
||||
if let Some(cmd) = self.osc_parser.end(Some(c)) {
|
||||
f(0, Action::OscDispatch(cmd));
|
||||
f(0, Action::OscDispatch(cmd.into_owned()));
|
||||
}
|
||||
}
|
||||
State::DcsPassthrough => f(0, Action::DcsUnhook),
|
||||
@@ -840,7 +840,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
a[0],
|
||||
Some(Action::OscDispatch(Command::ChangeWindowTitle(
|
||||
b"abc".to_vec()
|
||||
b"abc".to_vec().into()
|
||||
)))
|
||||
);
|
||||
}
|
||||
@@ -860,7 +860,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
a[0],
|
||||
Some(Action::OscDispatch(Command::ChangeWindowTitle(
|
||||
b"abc".to_vec()
|
||||
b"abc".to_vec().into()
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ define_actions! {
|
||||
SetAttribute(sgr::Attribute<'a>),
|
||||
KittyColorReport(&'a kitty::color::OSC),
|
||||
ColorOperation(ColorOperation<'a>),
|
||||
SemanticPrompt(&'a osc::SemanticPrompt),
|
||||
SemanticPrompt(&'a osc::SemanticPrompt<'a>),
|
||||
}
|
||||
|
||||
/// Field types of [`Action`] payloads.
|
||||
@@ -253,7 +253,7 @@ pub struct ColorOperation<'a> {
|
||||
}
|
||||
|
||||
/// Alias matching Zig `Action.SemanticPrompt`.
|
||||
pub type SemanticPrompt = osc::SemanticPrompt;
|
||||
pub type SemanticPrompt<'a> = osc::SemanticPrompt<'a>;
|
||||
|
||||
/// The receiver of [`Action`]s (Zig: a type with a
|
||||
/// `vt(comptime action, value) !void` method).
|
||||
|
||||
Reference in new issue
Block a user