Skip to main content

libghostty_vt/
osc.rs

1//! Handling OSC (Operating System Command) escape sequences.
2
3use std::{marker::PhantomData, mem::MaybeUninit};
4
5use crate::{
6    alloc::{Allocator, Object},
7    error::{Result, from_result},
8    ffi,
9};
10
11/// OSC (Operating System Command) sequence parser and command handling.
12///
13/// The parser operates in a streaming fashion, processing input byte-by-byte
14/// to handle OSC sequences that may arrive in fragments across multiple reads.
15/// This interface makes it easy to integrate into most environments and avoids
16/// over-allocating buffers.
17#[derive(Debug)]
18pub struct Parser<'alloc>(Object<'alloc, ffi::OscParserImpl>);
19
20impl<'alloc> Parser<'alloc> {
21    /// Create a new OSC parser.
22    pub fn new() -> Result<Self> {
23        // SAFETY: A NULL allocator is always valid
24        unsafe { Self::new_inner(std::ptr::null()) }
25    }
26
27    /// Create a new OSC parser with a custom allocator.
28    ///
29    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
30    /// regarding custom memory management and lifetimes.
31    pub fn new_with_alloc<'ctx: 'alloc>(alloc: &'alloc Allocator<'ctx>) -> Result<Self> {
32        // SAFETY: Borrow checking should forbid invalid allocators
33        unsafe { Self::new_inner(alloc.to_raw()) }
34    }
35
36    unsafe fn new_inner(alloc: *const ffi::Allocator) -> Result<Self> {
37        let mut raw: ffi::OscParser = std::ptr::null_mut();
38        let result = unsafe { ffi::ghostty_osc_new(alloc, &raw mut raw) };
39        from_result(result)?;
40        Ok(Self(Object::new(raw)?))
41    }
42
43    /// Reset an OSC parser instance to its initial state.
44    ///
45    /// Resets the parser state, clearing any partially parsed OSC sequences
46    /// and returning the parser to its initial state. This is useful for
47    /// reusing a parser instance or recovering from parse errors.
48    pub fn reset(&mut self) {
49        unsafe { ffi::ghostty_osc_reset(self.0.as_raw()) }
50    }
51
52    /// Parse the next byte in an OSC sequence.
53    ///
54    /// Processes a single byte as part of an OSC sequence. The parser maintains
55    /// internal state to track the progress through the sequence. Call this
56    /// function for each byte in the sequence data.
57    ///
58    /// When finished pumping the parser with bytes, call [`Parser::end`] to
59    /// get the final result.
60    pub fn next_byte(&mut self, byte: u8) {
61        unsafe { ffi::ghostty_osc_next(self.0.as_raw(), byte) }
62    }
63
64    /// Finalize OSC parsing and retrieve the parsed command.
65    ///
66    /// Call this function after feeding all bytes of an OSC sequence to the parser
67    /// using [`Parser::next_byte`] with the exception of the terminating character
68    /// (ESC or ST). This function finalizes the parsing process and returns the
69    /// parsed OSC command. Invalid commands will return a command with type
70    /// [`CommandType::Invalid`].
71    ///
72    /// The terminator parameter specifies the byte that terminated the OSC
73    /// sequence (typically 0x07 for BEL or 0x5C for ST after ESC).
74    /// This information is preserved in the parsed command so that responses
75    /// can use the same terminator format for better compatibility with the
76    /// calling program. For commands that do not require a response, this
77    /// parameter is ignored and the resulting command will not retain the
78    /// terminator information.
79    #[expect(clippy::missing_panics_doc, reason = "internal invariant")]
80    pub fn end<'p>(&'p mut self, terminator: u8) -> Command<'p, 'alloc> {
81        let raw = unsafe { ffi::ghostty_osc_end(self.0.as_raw(), terminator) };
82        Command {
83            inner: Object::new(raw).expect("command must not be null"),
84            _parser: PhantomData,
85        }
86    }
87}
88
89impl Drop for Parser<'_> {
90    fn drop(&mut self) {
91        unsafe { ffi::ghostty_osc_free(self.0.as_raw()) }
92    }
93}
94
95/// A parsed OSC (Operating System Command) command.
96///
97/// The command can be queried for its type and associated data.
98#[derive(Debug)]
99pub struct Command<'p, 'alloc> {
100    inner: Object<'alloc, ffi::OscCommandImpl>,
101    _parser: PhantomData<&'p Parser<'alloc>>,
102}
103
104impl<'p> Command<'p, '_> {
105    /// Get the type of an OSC command.
106    ///
107    /// This can be used to determine what kind of command was parsed and
108    /// what data might be available from it.
109    #[must_use]
110    pub fn command_type(self) -> CommandType<'p> {
111        self.command_type_inner().unwrap_or(CommandType::Invalid)
112    }
113
114    fn command_type_inner(&self) -> Option<CommandType<'p>> {
115        use ffi::OscCommandData as Data;
116        use ffi::OscCommandType as Type;
117
118        let raw_type = unsafe { ffi::ghostty_osc_command_type(self.inner.as_raw()) };
119        Some(match raw_type {
120            Type::CHANGE_WINDOW_TITLE => CommandType::ChangeWindowTitle {
121                title: self.get(Data::CHANGE_WINDOW_TITLE_STR)?,
122            },
123            Type::CHANGE_WINDOW_ICON => CommandType::ChangeWindowIcon,
124            Type::SEMANTIC_PROMPT => CommandType::SemanticPrompt,
125            Type::CLIPBOARD_CONTENTS => CommandType::ClipboardContents,
126            Type::REPORT_PWD => CommandType::ReportPwd,
127            Type::MOUSE_SHAPE => CommandType::MouseShape,
128            Type::COLOR_OPERATION => CommandType::ColorOperation,
129            Type::KITTY_COLOR_PROTOCOL => CommandType::KittyColorProtocol,
130            Type::SHOW_DESKTOP_NOTIFICATION => CommandType::ShowDesktopNotification,
131            Type::HYPERLINK_START => CommandType::HyperlinkStart,
132            Type::HYPERLINK_END => CommandType::HyperlinkEnd,
133            Type::CONEMU_SLEEP => CommandType::ConemuSleep,
134            Type::CONEMU_SHOW_MESSAGE_BOX => CommandType::ConemuShowMessageBox,
135            Type::CONEMU_CHANGE_TAB_TITLE => CommandType::ConemuChangeTabTitle,
136            Type::CONEMU_PROGRESS_REPORT => CommandType::ConemuProgressReport,
137            Type::CONEMU_WAIT_INPUT => CommandType::ConemuWaitInput,
138            Type::CONEMU_GUIMACRO => CommandType::ConemuGuiMacro,
139            Type::CONEMU_RUN_PROCESS => CommandType::ConemuRunProcess,
140            Type::CONEMU_OUTPUT_ENVIRONMENT_VARIABLE => {
141                CommandType::ConemuOutputEnvironmentVariable
142            }
143            Type::CONEMU_XTERM_EMULATION => CommandType::ConemuXtermEmulation,
144            Type::CONEMU_COMMENT => CommandType::ConemuComment,
145            Type::KITTY_TEXT_SIZING => CommandType::KittyTextSizing,
146
147            _ => return None,
148        })
149    }
150
151    fn get<T>(&self, tag: ffi::OscCommandData::Type) -> Option<T> {
152        let mut value = MaybeUninit::<T>::zeroed();
153        let result = unsafe {
154            ffi::ghostty_osc_command_data(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
155        };
156
157        if result {
158            // SAFETY: Value should be initialized after successful call.
159            Some(unsafe { value.assume_init() })
160        } else {
161            None
162        }
163    }
164}
165
166/// Type of an OSC command.
167#[repr(u32)]
168#[derive(Debug, Clone, Default)]
169#[expect(missing_docs, reason = "missing upstream docs")]
170pub enum CommandType<'p> {
171    #[default]
172    Invalid,
173    ChangeWindowTitle {
174        /// Window title string data.
175        title: &'p str,
176    },
177    ChangeWindowIcon,
178    SemanticPrompt,
179    ClipboardContents,
180    ReportPwd,
181    MouseShape,
182    ColorOperation,
183    KittyColorProtocol,
184    ShowDesktopNotification,
185    HyperlinkStart,
186    HyperlinkEnd,
187    ConemuSleep,
188    ConemuShowMessageBox,
189    ConemuChangeTabTitle,
190    ConemuProgressReport,
191    ConemuWaitInput,
192    ConemuGuiMacro,
193    ConemuRunProcess,
194    ConemuOutputEnvironmentVariable,
195    ConemuXtermEmulation,
196    ConemuComment,
197    KittyTextSizing,
198}