refactor: resolve cross-crate integration TODOs and resources discovery
Cleanup pass over items collected in docs/rewrite/INTEGRATION_TODO.md: - OSC 133 printf-q / percent decoders live once in ghostty_vt::string_encoding; ghostty-os re-exports them. - CursorStyle and clipboard codepoint map entries have one definition (ghostty-terminal); ghostty-config now depends on ghostty-terminal. - ActionKey::docs() trailing blank lines fixed at the source; cli's workaround removed (+list-actions --docs still byte-identical). - x11_color exposes names in original case; cli no longer re-parses rgb.txt. - One cgroup scope wait loop (ghostty_os::cgroup::wait_for_scope). - Flatpak passwd lookup implemented in ghostty-os via a local pty. - +version shows the rustc version captured at build time. Resources: dev builds find <workspace>/resources automatically (keeping Ghostty's sentinel-file detection and GHOSTTY_RESOURCES_DIR); scripts/gen-terminfo.sh (just terminfo) regenerates the compiled terminfo; `just install [PREFIX]` installs binary, share/ghostty, terminfo, desktop/D-Bus/systemd/metainfo files and icons. Test Plan: - cargo test for vt, os, config, input, cli - diff `+list-keybinds`, `+list-actions --docs`, `+show-config --default` against /usr/bin/ghostty
This commit is contained in:
1 parent
0c4f0ee412
commit
967e3a176d
24 files changed
+656
-513
No files matched your search
@@ -17,6 +17,7 @@ pub mod parser;
|
||||
pub mod sgr;
|
||||
pub mod simd;
|
||||
pub mod stream;
|
||||
pub mod string_encoding;
|
||||
pub mod tmux;
|
||||
pub mod utf8_decoder;
|
||||
pub mod x11_color;
|
||||
@@ -4,19 +4,11 @@
|
||||
//! <https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md>
|
||||
|
||||
use crate::osc::{Command as OscCommand, Parser, State};
|
||||
use crate::string_encoding::{printf_q_decode, url_percent_decode};
|
||||
|
||||
/// Error returned when decoding a command line fails (Zig
|
||||
/// `error.DecodeError`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeError;
|
||||
|
||||
impl std::fmt::Display for DecodeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("decode error")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DecodeError {}
|
||||
pub type DecodeError = crate::string_encoding::Error;
|
||||
|
||||
/// A single semantic prompt command.
|
||||
///
|
||||
@@ -121,11 +113,6 @@ impl Command {
|
||||
|
||||
/// Write the decoded command line (if any) to the writer. If an error
|
||||
/// occurs garbage may have been written to the writer.
|
||||
///
|
||||
/// TODO(integration): uses local copies of `printfQDecode` /
|
||||
/// `urlPercentDecode` (ZIG/src/os/string_encoding.zig); switch to
|
||||
/// `ghostty-os` once available (that crate would need to be a dependency
|
||||
/// of ghostty-vt, so these remain local for now).
|
||||
pub fn write_command_line(&self, writer: &mut Vec<u8>) -> Result<(), DecodeError> {
|
||||
if let Some(OptionValue::Bytes(command_line)) = self.read_option(OptionKey::Cmdline) {
|
||||
return printf_q_decode(writer, command_line);
|
||||
@@ -416,91 +403,6 @@ pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscComma
|
||||
Some(OscCommand::SemanticPrompt(cmd))
|
||||
}
|
||||
|
||||
// TODO(integration): local copies of ZIG/src/os/string_encoding.zig.
|
||||
|
||||
/// Decode data that has been encoded in the same way that `bash`'s
|
||||
/// `printf %q` encodes a string and write it to the writer. If an error is
|
||||
/// returned garbage may have been written to the buffer.
|
||||
pub fn printf_q_decode(writer: &mut Vec<u8>, buf: &[u8]) -> Result<(), DecodeError> {
|
||||
let data: &[u8] = if buf.starts_with(b"$'") {
|
||||
// Strip off `$''` quoting.
|
||||
if buf.len() < 3 || !buf.ends_with(b"'") {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
&buf[2..buf.len() - 1]
|
||||
} else if buf.starts_with(b"'") {
|
||||
// Strip off `''` quoting.
|
||||
if buf.len() < 2 || !buf.ends_with(b"'") {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
&buf[1..buf.len() - 1]
|
||||
} else {
|
||||
buf
|
||||
};
|
||||
|
||||
let mut src = 0;
|
||||
while src < data.len() {
|
||||
match data[src] {
|
||||
b'\\' => {
|
||||
if src + 1 >= data.len() {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
match data[src + 1] {
|
||||
c @ (b' ' | b'\\' | b'"' | b'\'' | b'$') => writer.push(c),
|
||||
b'e' => writer.push(0x1b),
|
||||
b'n' => writer.push(b'\n'),
|
||||
b'r' => writer.push(b'\r'),
|
||||
b't' => writer.push(b'\t'),
|
||||
b'v' => writer.push(0x0b),
|
||||
_ => return Err(DecodeError),
|
||||
}
|
||||
src += 2;
|
||||
}
|
||||
c => {
|
||||
writer.push(c);
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode data that has been URL percent encoded and write it to the given
|
||||
/// buffer. If an error is returned garbage may have been written.
|
||||
pub fn url_percent_decode(writer: &mut Vec<u8>, buf: &[u8]) -> Result<(), DecodeError> {
|
||||
fn hex(c: u8) -> Option<u8> {
|
||||
match c {
|
||||
b'0'..=b'9' => Some(c - b'0'),
|
||||
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let mut src = 0;
|
||||
while src < buf.len() {
|
||||
match buf[src] {
|
||||
b'%' => {
|
||||
if src + 2 >= buf.len() {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
match (hex(buf[src + 1]), hex(buf[src + 2])) {
|
||||
(Some(a), Some(b)) => {
|
||||
writer.push((a << 4) | b);
|
||||
src += 3;
|
||||
}
|
||||
_ => return Err(DecodeError),
|
||||
}
|
||||
}
|
||||
c => {
|
||||
writer.push(c);
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod tests {
|
||||
@@ -531,7 +433,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn cmdline_err(input: &str) {
|
||||
assert_eq!(cmdline(input).1, Err(DecodeError));
|
||||
assert_eq!(cmdline(input).1, Err(DecodeError::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -980,19 +882,4 @@ mod tests {
|
||||
assert!(r("abc").is_none());
|
||||
assert_eq!(r("127;aid=foo"), Some(OptionValue::Int(127)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_encoding_extra_printf_q_and_percent() {
|
||||
let mut w = Vec::new();
|
||||
printf_q_decode(&mut w, b"bobr\\ kurwa").unwrap();
|
||||
assert_eq!(w, b"bobr kurwa");
|
||||
assert_eq!(
|
||||
printf_q_decode(&mut Vec::new(), b"bobr\\dkurwa"),
|
||||
Err(DecodeError)
|
||||
);
|
||||
assert_eq!(
|
||||
printf_q_decode(&mut Vec::new(), b"bobr kurwa\\"),
|
||||
Err(DecodeError)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//! String decoding/encoding helpers (ZIG/src/os/string_encoding.zig).
|
||||
//!
|
||||
//! Canonical implementation; `ghostty-os::string_encoding` re-exports it.
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("decode error")]
|
||||
DecodeError,
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
matches!((self, other), (Error::DecodeError, Error::DecodeError))
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode date from the buffer that has been encoded in the same way that
|
||||
/// `bash`'s `printf %q` encodes a string and write it to the writer. If an
|
||||
/// error is returned garbage may have been written to the buffer.
|
||||
pub fn printf_q_decode<W: Write>(writer: &mut W, buf: &[u8]) -> Result<(), Error> {
|
||||
let data: &[u8] = if buf.starts_with(b"$'") {
|
||||
// Strip off `$''` quoting.
|
||||
if buf.len() < 3 || !buf.ends_with(b"'") {
|
||||
return Err(Error::DecodeError);
|
||||
}
|
||||
&buf[2..buf.len() - 1]
|
||||
} else if buf.starts_with(b"'") {
|
||||
// Strip off `''` quoting.
|
||||
if buf.len() < 2 || !buf.ends_with(b"'") {
|
||||
return Err(Error::DecodeError);
|
||||
}
|
||||
&buf[1..buf.len() - 1]
|
||||
} else {
|
||||
buf
|
||||
};
|
||||
|
||||
let mut src = 0;
|
||||
while src < data.len() {
|
||||
match data[src] {
|
||||
b'\\' => {
|
||||
if src + 1 >= data.len() {
|
||||
return Err(Error::DecodeError);
|
||||
}
|
||||
let out = match data[src + 1] {
|
||||
c @ (b' ' | b'\\' | b'"' | b'\'' | b'$') => c,
|
||||
b'e' => 0x1b,
|
||||
b'n' => b'\n',
|
||||
b'r' => b'\r',
|
||||
b't' => b'\t',
|
||||
b'v' => 0x0b,
|
||||
_ => return Err(Error::DecodeError),
|
||||
};
|
||||
writer.write_all(&[out])?;
|
||||
src += 2;
|
||||
}
|
||||
c => {
|
||||
writer.write_all(&[c])?;
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode data from the buffer that has been URL percent encoded and write
|
||||
/// it to the given buffer. If an error is returned the garbage may have been
|
||||
/// written to the writer.
|
||||
pub fn url_percent_decode<W: Write>(writer: &mut W, buf: &[u8]) -> Result<(), Error> {
|
||||
let mut src = 0;
|
||||
while src < buf.len() {
|
||||
match buf[src] {
|
||||
b'%' => {
|
||||
if src + 2 >= buf.len() {
|
||||
return Err(Error::DecodeError);
|
||||
}
|
||||
let (Some(hi), Some(lo)) = (hex(buf[src + 1]), hex(buf[src + 2])) else {
|
||||
return Err(Error::DecodeError);
|
||||
};
|
||||
writer.write_all(&[(hi << 4) | lo])?;
|
||||
src += 3;
|
||||
}
|
||||
c => {
|
||||
writer.write_all(&[c])?;
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hex(c: u8) -> Option<u8> {
|
||||
match c {
|
||||
b'0'..=b'9' => Some(c - b'0'),
|
||||
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the given character valid in URI percent encoding?
|
||||
fn is_valid_char(c: u8) -> bool {
|
||||
match c {
|
||||
b' ' | b';' | b'=' => false,
|
||||
_ => (0x20..=0x7e).contains(&c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write data to the writer after URI percent encoding.
|
||||
pub fn url_percent_encode<W: Write>(writer: &mut W, data: &[u8]) -> io::Result<()> {
|
||||
for &c in data {
|
||||
if is_valid_char(c) {
|
||||
writer.write_all(&[c])?;
|
||||
} else {
|
||||
write!(writer, "%{c:02X}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
fn pq(s: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut w = Vec::new();
|
||||
printf_q_decode(&mut w, s).map(|()| w)
|
||||
}
|
||||
|
||||
fn pd(s: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut w = Vec::new();
|
||||
url_percent_decode(&mut w, s).map(|()| w)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_1() {
|
||||
assert_eq!(pq(b"bobr\\ kurwa").unwrap(), b"bobr kurwa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_2() {
|
||||
assert_eq!(pq(b"bobr\\nkurwa").unwrap(), b"bobr\nkurwa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_3() {
|
||||
assert_eq!(pq(b"bobr\\dkurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_4() {
|
||||
assert_eq!(pq(b"bobr kurwa\\"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_5() {
|
||||
assert_eq!(pq(b"$'bobr kurwa'").unwrap(), b"bobr kurwa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_6() {
|
||||
assert_eq!(pq(b"'bobr kurwa'").unwrap(), b"bobr kurwa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_7() {
|
||||
assert_eq!(pq(b"$'bobr kurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_8() {
|
||||
assert_eq!(pq(b"$'"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_9() {
|
||||
assert_eq!(pq(b"'bobr kurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_10() {
|
||||
assert_eq!(pq(b"'"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printf_q_escapes() {
|
||||
assert_eq!(
|
||||
pq(b"\\e\\r\\t\\v\\\\\\\"\\'\\$").unwrap(),
|
||||
b"\x1b\r\t\x0b\\\"'$"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singles_percent() {
|
||||
for c in 0..255u32 {
|
||||
let lower = format!("%{c:02x}");
|
||||
let decoded = pd(lower.as_bytes()).unwrap();
|
||||
assert_eq!(decoded, [c as u8]);
|
||||
let upper = format!("%{c:02X}");
|
||||
let decoded = pd(upper.as_bytes()).unwrap();
|
||||
assert_eq!(decoded, [c as u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_1() {
|
||||
assert_eq!(pd(b"bobr%20kurwa").unwrap(), b"bobr kurwa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_2() {
|
||||
assert_eq!(pd(b"bobr%2kurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_3() {
|
||||
assert_eq!(pd(b"bobr%kurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_4() {
|
||||
assert_eq!(pd(b"bobr%%kurwa"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_5() {
|
||||
assert_eq!(pd(b"bobr%20kurwa%20").unwrap(), b"bobr kurwa ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_6() {
|
||||
assert_eq!(pd(b"bobr%20kurwa%2"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_7() {
|
||||
assert_eq!(pd(b"bobr%20kurwa%"), Err(Error::DecodeError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_encode() {
|
||||
let mut w = Vec::new();
|
||||
url_percent_encode(&mut w, b"a b;c=d\n\xc3\xa9~").unwrap();
|
||||
assert_eq!(String::from_utf8(w).unwrap(), "a%20b%3Bc%3Dd%0A%C3%A9~");
|
||||
}
|
||||
}
|
||||
@@ -10,29 +10,45 @@ use std::sync::OnceLock;
|
||||
/// licensed under the same license as Ghostty.
|
||||
const DATA: &str = include_str!("res/rgb.txt");
|
||||
|
||||
/// Parse rgb.txt into `(name with original case, value)` entries in file
|
||||
/// order. This is not very robust parsing, because we expect a very exact
|
||||
/// format for rgb.txt.
|
||||
fn parse_entries() -> impl Iterator<Item = (&'static str, Rgb)> {
|
||||
DATA.split('\n').filter(|l| !l.is_empty()).map(|line| {
|
||||
let r: u8 = line[0..3].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let g: u8 = line[4..7].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let b: u8 = line[8..11].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let name = line[12..].trim_matches(|c| c == ' ' || c == '\t' || c == '\n');
|
||||
(name, Rgb { r, g, b })
|
||||
})
|
||||
}
|
||||
|
||||
/// The map of all available X11 colors. Keys are lowercase ASCII; lookups
|
||||
/// are ASCII case-insensitive (see [`get`]).
|
||||
fn map() -> &'static HashMap<String, Rgb> {
|
||||
static MAP: OnceLock<HashMap<String, Rgb>> = OnceLock::new();
|
||||
MAP.get_or_init(|| {
|
||||
let mut m = HashMap::new();
|
||||
// Parse the line. This is not very robust parsing, because we expect
|
||||
// a very exact format for rgb.txt.
|
||||
for line in DATA.split('\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let r: u8 = line[0..3].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let g: u8 = line[4..7].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let b: u8 = line[8..11].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||
let name = line[12..].trim_matches(|c| c == ' ' || c == '\t' || c == '\n');
|
||||
m.entry(name.to_ascii_lowercase())
|
||||
.or_insert(Rgb { r, g, b });
|
||||
for (name, rgb) in parse_entries() {
|
||||
m.entry(name.to_ascii_lowercase()).or_insert(rgb);
|
||||
}
|
||||
m
|
||||
})
|
||||
}
|
||||
|
||||
/// All X11 color names with their original spelling (e.g. `AliceBlue`,
|
||||
/// `alice blue`) and values, in rgb.txt order. Like the case-insensitive
|
||||
/// map, the first spelling of a name wins.
|
||||
pub fn names() -> &'static [(&'static str, Rgb)] {
|
||||
static NAMES: OnceLock<Vec<(&'static str, Rgb)>> = OnceLock::new();
|
||||
NAMES.get_or_init(|| {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
parse_entries()
|
||||
.filter(|(n, _)| seen.insert(n.to_ascii_lowercase()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up an X11 color by name (ASCII case-insensitive).
|
||||
pub fn get(name: &str) -> Option<Rgb> {
|
||||
map().get(&name.to_ascii_lowercase()).copied()
|
||||
@@ -48,6 +64,14 @@ mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn names_keep_original_case() {
|
||||
let n = names();
|
||||
assert!(n.iter().any(|(name, _)| *name == "AliceBlue"));
|
||||
assert!(n.iter().any(|(name, _)| *name == "alice blue"));
|
||||
assert!(n.iter().all(|(name, rgb)| get(name) == Some(*rgb)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x11_color_map() {
|
||||
assert_eq!(None, get("nosuchcolor"));
|
||||
|
||||
Reference in new issue
Block a user