fix: close remaining gaps found by the completeness audit

Addresses the partial ports listed in
docs/rewrite/notes/review-completeness-codex.md (details in
docs/rewrite/notes/gaps-X2.md):
- Flatpak: the exit watcher no longer consumes the host command; a
  shared FlatpakSignalHandle lets closing a surface still SIGHUP the
  host process group.
- Child environment is byte-preserving end to end (ghostty_os::EnvMap
  of OsStrings) and Snap paths are filtered like filterSnapPaths.
- argv handling no longer panics on non-UTF-8 arguments.
- URI percent-decoding keeps raw bytes; OSC 7 pwd is passed through
  without lossy conversion.
- Keybinding comparison/hashing uses full Unicode case folding from a
  table generated from CaseFolding.txt (Unicode 17.0.0).
- Link regexes set Oniguruma's whole-search retry limit through the new
  minimal ghostty-onig crate (one SAFETY-commented FFI call).
- OSC 9 NUL-capacity check ordering matches Zig.
- Wayland registry binds/clears globals advertised after startup.
- Custom shaders may pass sampler2D to helper functions (naga adapter).
- +list-themes preview renders OSC 8 links, styled underlines and
  follows color-scheme reports.

Also includes the inspector's integration edits in shared files
(renderer state hook, termio VT recording, core/apprt-gtk wiring), X1's
config/input/cli additions for completion generation, and a fix for a
race in the termio backpressure test (it wrote before `stty raw`
applied, so the cooked line discipline dropped bytes past MAX_CANON).

Test Plan:
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-12 04:39:24 +02:00
1 parent d1a5e55198
commit 2aa2a12de0
61 files changed
+4210 -298

No files matched your search

Generated
+110
View File
@@ -664,6 +664,51 @@ dependencies = [
"syn 3.0.8", "syn 3.0.8",
] ]
[[package]]
name = "dear-imgui-build-support"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728af47c10cb2b12aa36b942e610f894ecafc5d61d0056ab61f9629f02b3d5dc"
dependencies = [
"cc",
"serde",
"serde_json",
]
[[package]]
name = "dear-imgui-glow"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39146f8b48d335ed4bc3e40e6c216fe3dfc8d3e1da4d1ea1550d4545b339d117"
dependencies = [
"dear-imgui-rs",
"glow",
"thiserror 2.0.21",
]
[[package]]
name = "dear-imgui-rs"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40f283479a70f65bb3f1239b48116a65272a51c7d1cda718b6149823fb0f2444"
dependencies = [
"bitflags 2.13.2",
"dear-imgui-sys",
"parking_lot",
"thiserror 2.0.21",
]
[[package]]
name = "dear-imgui-sys"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c70a5f41bba03683065fd068881da406d1dfe76ec47fe276837017ac210a46f8"
dependencies = [
"cc",
"dear-imgui-build-support",
"mint",
]
[[package]] [[package]]
name = "defmt" name = "defmt"
version = "1.1.1" version = "1.1.1"
@@ -1208,6 +1253,7 @@ dependencies = [
"ghostty-config", "ghostty-config",
"ghostty-datastruct", "ghostty-datastruct",
"ghostty-input", "ghostty-input",
"ghostty-inspector",
"ghostty-renderer", "ghostty-renderer",
"ghostty-terminal", "ghostty-terminal",
"ghostty-vt", "ghostty-vt",
@@ -1219,6 +1265,7 @@ name = "ghostty-apprt-gtk"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"dear-imgui-glow",
"env_logger", "env_logger",
"gdk4-wayland", "gdk4-wayland",
"gdk4-x11", "gdk4-x11",
@@ -1226,6 +1273,7 @@ dependencies = [
"ghostty-config", "ghostty-config",
"ghostty-datastruct", "ghostty-datastruct",
"ghostty-input", "ghostty-input",
"ghostty-inspector",
"ghostty-os", "ghostty-os",
"ghostty-vt", "ghostty-vt",
"glib-build-tools", "glib-build-tools",
@@ -1255,6 +1303,17 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "ghostty-build-data"
version = "0.1.0"
dependencies = [
"ghostty-cli",
"ghostty-config",
"ghostty-input",
"ghostty-os",
"ghostty-termio",
]
[[package]] [[package]]
name = "ghostty-cli" name = "ghostty-cli"
version = "0.1.0" version = "0.1.0"
@@ -1269,6 +1328,7 @@ dependencies = [
"log", "log",
"ratatui", "ratatui",
"tempfile", "tempfile",
"termina",
"thiserror 2.0.21", "thiserror 2.0.21",
"unicode-width", "unicode-width",
] ]
@@ -1298,6 +1358,7 @@ dependencies = [
"ghostty-datastruct", "ghostty-datastruct",
"ghostty-font", "ghostty-font",
"ghostty-input", "ghostty-input",
"ghostty-inspector",
"ghostty-os", "ghostty-os",
"ghostty-renderer", "ghostty-renderer",
"ghostty-terminal", "ghostty-terminal",
@@ -1341,11 +1402,33 @@ dependencies = [
name = "ghostty-input" name = "ghostty-input"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ghostty-unicode",
"ghostty-vt", "ghostty-vt",
"indexmap", "indexmap",
"thiserror 2.0.21", "thiserror 2.0.21",
] ]
[[package]]
name = "ghostty-inspector"
version = "0.1.0"
dependencies = [
"dear-imgui-rs",
"ghostty-font",
"ghostty-input",
"ghostty-renderer",
"ghostty-terminal",
"ghostty-vt",
"log",
]
[[package]]
name = "ghostty-onig"
version = "0.1.0"
dependencies = [
"onig",
"onig_sys",
]
[[package]] [[package]]
name = "ghostty-os" name = "ghostty-os"
version = "0.1.0" version = "0.1.0"
@@ -1392,6 +1475,7 @@ dependencies = [
"ghostty-cli", "ghostty-cli",
"ghostty-config", "ghostty-config",
"ghostty-core", "ghostty-core",
"ghostty-os",
"log", "log",
] ]
@@ -1404,6 +1488,7 @@ dependencies = [
"flate2", "flate2",
"ghostty-config", "ghostty-config",
"ghostty-datastruct", "ghostty-datastruct",
"ghostty-onig",
"ghostty-unicode", "ghostty-unicode",
"ghostty-vt", "ghostty-vt",
"log", "log",
@@ -2147,6 +2232,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mint"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.4" version = "1.2.4"
@@ -2955,6 +3046,19 @@ dependencies = [
"syn 3.0.8", "syn 3.0.8",
] ]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]] [[package]]
name = "serde_repr" name = "serde_repr"
version = "0.1.21" version = "0.1.21"
@@ -4074,6 +4178,12 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.15.0" version = "5.15.0"
+8
View File
@@ -14,6 +14,7 @@ ghostty-config.workspace = true
ghostty-apprt-gtk.workspace = true ghostty-apprt-gtk.workspace = true
ghostty-apprt.workspace = true ghostty-apprt.workspace = true
ghostty-cli.workspace = true ghostty-cli.workspace = true
ghostty-os.workspace = true
log = "0.4" log = "0.4"
[lints] [lints]
@@ -23,6 +24,11 @@ workspace = true
resolver = "3" resolver = "3"
members = [".", "crates/*"] members = [".", "crates/*"]
# The upstream Ghostty version this rewrite tracks (Zig: `build.zig.zon`
# `.version`); consumed by `crates/ghostty-cli/build.rs`.
[workspace.metadata.ghostty]
app-version = "1.3.1"
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
@@ -38,12 +44,14 @@ ghostty-font = { path = "crates/ghostty-font" }
ghostty-renderer = { path = "crates/ghostty-renderer" } ghostty-renderer = { path = "crates/ghostty-renderer" }
ghostty-termio = { path = "crates/ghostty-termio" } ghostty-termio = { path = "crates/ghostty-termio" }
ghostty-os = { path = "crates/ghostty-os" } ghostty-os = { path = "crates/ghostty-os" }
ghostty-onig = { path = "crates/ghostty-onig" }
ghostty-datastruct = { path = "crates/ghostty-datastruct" } ghostty-datastruct = { path = "crates/ghostty-datastruct" }
ghostty-core = { path = "crates/ghostty-core" } ghostty-core = { path = "crates/ghostty-core" }
ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" } ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" }
ghostty-apprt = { path = "crates/ghostty-apprt" } ghostty-apprt = { path = "crates/ghostty-apprt" }
ghostty-cli = { path = "crates/ghostty-cli" } ghostty-cli = { path = "crates/ghostty-cli" }
ghostty-bench = { path = "crates/ghostty-bench" } ghostty-bench = { path = "crates/ghostty-bench" }
ghostty-inspector = { path = "crates/ghostty-inspector" }
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
+3 -1
View File
@@ -10,6 +10,7 @@ ghostty-apprt.workspace = true
ghostty-config.workspace = true ghostty-config.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-datastruct.workspace = true ghostty-datastruct.workspace = true
ghostty-inspector.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
ghostty-vt.workspace = true ghostty-vt.workspace = true
log = "0.4" log = "0.4"
@@ -28,13 +29,14 @@ half = "2"
libc = "0.2.190" libc = "0.2.190"
signal-hook = "0.4.5" signal-hook = "0.4.5"
libloading = "0.9.0" libloading = "0.9.0"
dear-imgui-glow = "0.19"
glow = "0.18"
[build-dependencies] [build-dependencies]
glib-build-tools = "0.22" glib-build-tools = "0.22"
[dev-dependencies] [dev-dependencies]
env_logger = "0.11" env_logger = "0.11"
glow = "0.18"
[lints.rust] [lints.rust]
unsafe_code = "deny" unsafe_code = "deny"
@@ -9,6 +9,9 @@ pub mod config_errors_dialog;
pub mod debug_warning; pub mod debug_warning;
pub mod dialog; pub mod dialog;
pub mod global_shortcuts; pub mod global_shortcuts;
pub mod imgui_widget;
pub mod inspector_widget;
pub mod inspector_window;
pub mod key_state_overlay; pub mod key_state_overlay;
pub mod resize_overlay; pub mod resize_overlay;
pub mod search_overlay; pub mod search_overlay;
+100 -29
View File
@@ -38,6 +38,7 @@ use gtk::{gdk, gio, glib};
use crate::class::application::Application; use crate::class::application::Application;
use crate::class::clipboard_confirmation_dialog::ClipboardConfirmationDialog; use crate::class::clipboard_confirmation_dialog::ClipboardConfirmationDialog;
use crate::class::config::Config; use crate::class::config::Config;
use crate::class::inspector_window::InspectorWindow;
use crate::class::key_state_overlay::KeyStateOverlay; use crate::class::key_state_overlay::KeyStateOverlay;
use crate::class::resize_overlay::ResizeOverlay; use crate::class::resize_overlay::ResizeOverlay;
use crate::class::search_overlay::SearchOverlay; use crate::class::search_overlay::SearchOverlay;
@@ -246,18 +247,66 @@ impl Surface {
.is_ok() .is_ok()
} }
/// Show/hide the inspector. /// Show/hide the inspector (Zig: `controlInspector`).
///
/// TODO(inspector): the inspector window is deferred; this is a no-op
/// that reports success like Zig does after (not) creating a window.
pub fn control_inspector(&self, value: Inspector) -> bool { pub fn control_inspector(&self, value: Inspector) -> bool {
log::debug!("inspector action {value:?} ignored (inspector deferred)"); // Let's see if we have an inspector already.
let priv_ = self.imp();
if let Some(inspector) = priv_.inspector.upgrade() {
match value {
Inspector::Show => {}
// Our weak ref will set our private value to null
Inspector::Toggle | Inspector::Hide => inspector.destroy(),
}
} else {
match value {
Inspector::Toggle | Inspector::Show => {
let inspector = InspectorWindow::new(self);
inspector.present_window();
priv_.inspector.set(Some(&inspector));
}
Inspector::Hide => {}
}
}
true true
} }
/// Redraw our inspector, if there is one associated with this surface. /// Redraw our inspector, if there is one associated with this surface.
/// TODO(inspector): deferred. pub fn redraw_inspector(&self) {
pub fn redraw_inspector(&self) {} if let Some(v) = self.imp().inspector.upgrade() {
v.queue_render();
}
}
/// Debug input replay: click in the inspector window. Returns false if
/// there is no inspector.
pub fn debug_inspector_click(&self, x: f64, y: f64) -> bool {
let Some(v) = self.imp().inspector.upgrade() else {
return false;
};
v.debug_click(x, y);
true
}
/// Debug input replay: scroll in the inspector window. Returns false if
/// there is no inspector.
pub fn debug_inspector_scroll(&self, x: f64, y: f64, dy: f64) -> bool {
let Some(v) = self.imp().inspector.upgrade() else {
return false;
};
v.debug_scroll(x, y, dy);
true
}
/// Debug input replay: write the next rendered frame of the inspector
/// window to `path` (PNG). Returns false if there is no inspector.
pub fn debug_inspector_screenshot(&self, path: &str) -> bool {
let Some(v) = self.imp().inspector.upgrade() else {
return false;
};
v.debug_screenshot(path);
true
}
/// Handle a key sequence action from the apprt. /// Handle a key sequence action from the apprt.
pub fn key_sequence_action(&self, value: &KeySequence) { pub fn key_sequence_action(&self, value: &KeySequence) {
@@ -963,33 +1012,38 @@ impl Surface {
self.imp().cursor_pos.get() self.imp().cursor_pos.get()
} }
pub fn default_termio_env(&self) -> Vec<(String, String)> { pub fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let application = app(); let application = app();
let mut env: Vec<(String, String)> = std::env::vars().collect(); // Byte-preserving environment (Zig `internal_os.getEnvMap`).
let remove = |env: &mut Vec<(String, String)>, k: &str| env.retain(|(n, _)| n != k); let mut env = ghostty_os::env::get_env_map();
remove(&mut env, "LANG");
if let Some(language) = application.saved_language() { if let Some(language) = application.saved_language() {
env.push(("LANG".to_owned(), language)); env.insert("LANG", language);
} else {
env.remove("LANG");
} }
// Don't leak these GTK environment variables to child processes. // Don't leak these GTK environment variables to child processes.
remove(&mut env, "GDK_DEBUG"); env.remove("GDK_DEBUG");
remove(&mut env, "GDK_DISABLE"); env.remove("GDK_DISABLE");
remove(&mut env, "GSK_RENDERER"); env.remove("GSK_RENDERER");
// Remove some environment variables that are set when Ghostty is // Remove some environment variables that are set when Ghostty is
// launched from a `.desktop` file, by D-Bus activation, or systemd. // launched from a `.desktop` file, by D-Bus activation, or systemd.
remove(&mut env, "GIO_LAUNCHED_DESKTOP_FILE"); env.remove("GIO_LAUNCHED_DESKTOP_FILE");
remove(&mut env, "GIO_LAUNCHED_DESKTOP_FILE_PID"); env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
remove(&mut env, "DBUS_STARTER_ADDRESS"); env.remove("DBUS_STARTER_ADDRESS");
remove(&mut env, "DBUS_STARTER_BUS_TYPE"); env.remove("DBUS_STARTER_BUS_TYPE");
remove(&mut env, "INVOCATION_ID"); env.remove("INVOCATION_ID");
remove(&mut env, "JOURNAL_STREAM"); env.remove("JOURNAL_STREAM");
remove(&mut env, "NOTIFY_SOCKET"); env.remove("NOTIFY_SOCKET");
// Snap path filtering (`build_config.snap`) is a build-time option // Unset environment varies set by snaps if we're running in a snap.
// of the Zig build and is not supported here. // This allows Ghostty to further launch additional snaps. (Zig gates
// this on the `snap` build option; running inside a snap implies it.)
if env.get("SNAP").is_some() {
ghostty_os::filter_snap_paths(&mut env);
}
// This is a hack because it ties ourselves (optionally) to the // This is a hack because it ties ourselves (optionally) to the
// Window class. The right solution we should do is emit a signal // Window class. The right solution we should do is emit a signal
@@ -999,16 +1053,20 @@ impl Surface {
.ancestor(Window::static_type()) .ancestor(Window::static_type())
.and_then(|w| w.downcast::<Window>().ok()) .and_then(|w| w.downcast::<Window>().ok())
{ {
if let Err(e) = window.winproto().add_subprocess_env(&mut env) { // winproto only ever adds (UTF-8) variables.
let mut extra: Vec<(String, String)> = Vec::new();
if let Err(e) = window.winproto().add_subprocess_env(&mut extra) {
log::warn!("winproto add_subprocess_env failed: {e}"); log::warn!("winproto add_subprocess_env failed: {e}");
} }
for (k, v) in extra {
env.insert(k, v);
}
if window.is_quick_terminal() { if window.is_quick_terminal() {
env.retain(|(n, _)| n != "GHOSTTY_QUICK_TERMINAL"); env.insert("GHOSTTY_QUICK_TERMINAL", "1");
env.push(("GHOSTTY_QUICK_TERMINAL".to_owned(), "1".to_owned()));
} }
} }
env env.into_iter().collect()
} }
pub fn clipboard_request(&self, clipboard_type: Clipboard, state: ClipboardRequest) -> bool { pub fn clipboard_request(&self, clipboard_type: Clipboard, state: ClipboardRequest) -> bool {
@@ -1939,7 +1997,7 @@ impl RtSurface for RtSurfaceHandle {
} }
} }
fn default_termio_env(&self) -> Vec<(String, String)> { fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
self.gobj() self.gobj()
.map(|s| s.default_termio_env()) .map(|s| s.default_termio_env())
.unwrap_or_default() .unwrap_or_default()
@@ -2365,6 +2423,9 @@ mod imp {
/// The source that handles setting our child property. /// The source that handles setting our child property.
pub idle_rechild: Cell<Option<glib::SourceId>>, pub idle_rechild: Cell<Option<glib::SourceId>>,
/// A weak reference to an inspector window.
pub inspector: glib::WeakRef<InspectorWindow>,
pub action_group: RefCell<Option<gio::SimpleActionGroup>>, pub action_group: RefCell<Option<gio::SimpleActionGroup>>,
pub vadj_handler: RefCell<Option<(gtk::Adjustment, glib::SignalHandlerId)>>, pub vadj_handler: RefCell<Option<(gtk::Adjustment, glib::SignalHandlerId)>>,
@@ -2457,6 +2518,7 @@ mod imp {
precision_scroll: Cell::new(false), precision_scroll: Cell::new(false),
progress_bar_timer: Cell::new(None), progress_bar_timer: Cell::new(None),
idle_rechild: Cell::new(None), idle_rechild: Cell::new(None),
inspector: glib::WeakRef::new(),
action_group: RefCell::new(None), action_group: RefCell::new(None),
vadj_handler: RefCell::new(None), vadj_handler: RefCell::new(None),
context: Cell::new(NewSurfaceContext::Window), context: Cell::new(NewSurfaceContext::Window),
@@ -2632,6 +2694,15 @@ mod imp {
} }
fn dispose(&self) { fn dispose(&self) {
// The inspector window holds on to our core surface to
// deactivate the inspector; Zig does that from a weak notify
// during our dispose. Closing the window here makes the inspector
// widget deactivate the inspector while the core surface is
// still alive.
if let Some(inspector) = self.inspector.upgrade() {
inspector.destroy();
}
if let Some((adj, id)) = self.vadj_handler.take() { if let Some((adj, id)) = self.vadj_handler.take() {
adj.disconnect(id); adj.disconnect(id);
} }
+4 -4
View File
@@ -1733,10 +1733,10 @@ impl Window {
/// Toggle the Ghostty inspector for the active surface. /// Toggle the Ghostty inspector for the active surface.
fn toggle_inspector(&self) { fn toggle_inspector(&self) {
// TODO(inspector): deferred; the inspector is not ported yet. let Some(surface) = self.get_active_surface() else {
if let Some(surface) = self.get_active_surface() { return;
log::debug!("toggle-inspector requested for {surface:?} (inspector deferred)"); };
} surface.control_inspector(ghostty_apprt::action::Inspector::Toggle);
} }
} }
+61 -3
View File
@@ -14,6 +14,10 @@
//! text hello world # IM commit of the rest of the line //! text hello world # IM commit of the rest of the line
//! wait 200 # milliseconds //! wait 200 # milliseconds
//! screenshot /abs/out.png # dump the renderer's last frame (PNG) //! screenshot /abs/out.png # dump the renderer's last frame (PNG)
//! inspector-screenshot /abs/out.png # dump the next frame of the terminal
//! # inspector window (PNG)
//! inspector-click 100 20 # left click in the inspector (logical px)
//! inspector-scroll 100 200 -3 # mouse wheel in the inspector
//! quit # quit the application //! quit # quit the application
//! ``` //! ```
//! //!
@@ -34,12 +38,15 @@ use crate::class::surface::Surface;
use crate::class::window::Window; use crate::class::window::Window;
/// One parsed command. /// One parsed command.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq)]
pub enum Step { pub enum Step {
Key(String), Key(String),
Text(String), Text(String),
Wait(u64), Wait(u64),
Screenshot(String), Screenshot(String),
InspectorScreenshot(String),
InspectorClick(f64, f64),
InspectorScroll(f64, f64, f64),
Quit, Quit,
} }
@@ -59,6 +66,31 @@ pub fn parse(script: &str) -> Result<Vec<Step>, String> {
"text" if !rest.is_empty() => Step::Text(rest.to_owned()), "text" if !rest.is_empty() => Step::Text(rest.to_owned()),
"wait" => Step::Wait(rest.parse().map_err(|_| err("wait needs milliseconds"))?), "wait" => Step::Wait(rest.parse().map_err(|_| err("wait needs milliseconds"))?),
"screenshot" if !rest.is_empty() => Step::Screenshot(rest.to_owned()), "screenshot" if !rest.is_empty() => Step::Screenshot(rest.to_owned()),
"inspector-screenshot" if !rest.is_empty() => {
Step::InspectorScreenshot(rest.to_owned())
}
"inspector-click" => {
let v: Vec<f64> = rest
.split_whitespace()
.map(str::parse)
.collect::<Result<_, _>>()
.map_err(|_| err("inspector-click needs x y"))?;
let [x, y] = v[..] else {
return Err(err("inspector-click needs x y"));
};
Step::InspectorClick(x, y)
}
"inspector-scroll" => {
let v: Vec<f64> = rest
.split_whitespace()
.map(str::parse)
.collect::<Result<_, _>>()
.map_err(|_| err("inspector-scroll needs x y dy"))?;
let [x, y, dy] = v[..] else {
return Err(err("inspector-scroll needs x y dy"));
};
Step::InspectorScroll(x, y, dy)
}
"quit" => Step::Quit, "quit" => Step::Quit,
_ => return Err(err(&format!("bad command {line:?}"))), _ => return Err(err(&format!("bad command {line:?}"))),
}); });
@@ -190,6 +222,30 @@ impl Runner {
Some(s) => s.debug_screenshot(&path), Some(s) => s.debug_screenshot(&path),
None => log::warn!("debug input: no active surface"), None => log::warn!("debug input: no active surface"),
}, },
Step::InspectorClick(x, y) => match self.active_surface() {
Some(s) => {
if !s.debug_inspector_click(x, y) {
log::warn!("debug input: the surface has no inspector");
}
}
None => log::warn!("debug input: no active surface"),
},
Step::InspectorScroll(x, y, dy) => match self.active_surface() {
Some(s) => {
if !s.debug_inspector_scroll(x, y, dy) {
log::warn!("debug input: the surface has no inspector");
}
}
None => log::warn!("debug input: no active surface"),
},
Step::InspectorScreenshot(path) => match self.active_surface() {
Some(s) => {
if !s.debug_inspector_screenshot(&path) {
log::warn!("debug input: the surface has no inspector");
}
}
None => log::warn!("debug input: no active surface"),
},
} }
} }
} }
@@ -228,8 +284,7 @@ mod tests {
#[test] #[test]
fn parse_script() { fn parse_script() {
let s = let s = "# c\n\nkey ctrl+shift+t\ntext hello world\nwait 200\nscreenshot /tmp/a.png\ninspector-screenshot /tmp/i.png\ninspector-click 10 20\ninspector-scroll 1 2 -3\nquit\n";
"# c\n\nkey ctrl+shift+t\ntext hello world\nwait 200\nscreenshot /tmp/a.png\nquit\n";
assert_eq!( assert_eq!(
parse(s).expect("valid script"), parse(s).expect("valid script"),
vec![ vec![
@@ -237,6 +292,9 @@ mod tests {
Step::Text("hello world".into()), Step::Text("hello world".into()),
Step::Wait(200), Step::Wait(200),
Step::Screenshot("/tmp/a.png".into()), Step::Screenshot("/tmp/a.png".into()),
Step::InspectorScreenshot("/tmp/i.png".into()),
Step::InspectorClick(10.0, 20.0),
Step::InspectorScroll(1.0, 2.0, -3.0),
Step::Quit Step::Quit
] ]
); );
@@ -81,6 +81,69 @@ struct State {
xdg_wm_dialog_present: bool, xdg_wm_dialog_present: bool,
/// Activation tokens requested but not yet `done`. /// Activation tokens requested but not yet `done`.
pending_tokens: usize, pending_tokens: usize,
/// The registry name of each global we bound (interface -> name), used
/// to unset the global when the compositor removes it.
bound_names: std::collections::HashMap<&'static str, u32>,
}
impl State {
/// Bind a newly advertised global if it is one we support
/// (Zig `registryListener` `.global`).
fn registry_global(
&mut self,
registry: &WlRegistry,
qh: &QueueHandle<State>,
name: u32,
interface: &str,
) {
log::debug!("found global {interface}");
// We don't actually do anything with this other than checking
// for its existence, so we process this separately.
if interface == "xdg_wm_dialog_v1" {
self.xdg_wm_dialog_present = true;
return;
}
macro_rules! try_bind {
($field:ident, $t:ty) => {
if interface == <$t as Proxy>::interface().name {
log::debug!("matched {}", interface);
self.$field = Some(registry.bind::<$t, _, _>(name, 1, qh, ()));
self.bound_names
.insert(<$t as Proxy>::interface().name, name);
}
};
}
try_bind!(kde_blur_manager, OrgKdeKwinBlurManager);
try_bind!(kde_decoration_manager, OrgKdeKwinServerDecorationManager);
try_bind!(kde_slide_manager, OrgKdeKwinSlideManager);
try_bind!(xdg_activation, XdgActivationV1);
}
/// A global is suddenly no longer available: destroy and unset it as the
/// protocol mandates (Zig `registryListener` `.global_remove`).
fn registry_global_remove(&mut self, name: u32) {
macro_rules! try_remove {
($field:ident, $t:ty) => {
let iface = <$t as Proxy>::interface().name;
if self.bound_names.get(iface) == Some(&name) {
self.bound_names.remove(iface);
// Dropping the proxy releases it client-side.
self.$field = None;
}
};
}
try_remove!(kde_blur_manager, OrgKdeKwinBlurManager);
try_remove!(kde_decoration_manager, OrgKdeKwinServerDecorationManager);
try_remove!(kde_slide_manager, OrgKdeKwinSlideManager);
if self.bound_names.get("xdg_activation_v1") == Some(&name) {
self.bound_names.remove("xdg_activation_v1");
if let Some(a) = self.xdg_activation.take() {
a.destroy();
}
}
}
} }
struct Shared { struct Shared {
@@ -106,6 +169,14 @@ impl Shared {
} }
} }
/// Interface names of the globals we bind.
const BOUND_INTERFACES: [&str; 4] = [
"org_kde_kwin_blur_manager",
"org_kde_kwin_server_decoration_manager",
"org_kde_kwin_slide_manager",
"xdg_activation_v1",
];
fn bind<I: Proxy + 'static>( fn bind<I: Proxy + 'static>(
globals: &wayland_client::globals::GlobalList, globals: &wayland_client::globals::GlobalList,
qh: &QueueHandle<State>, qh: &QueueHandle<State>,
@@ -162,6 +233,22 @@ impl App {
state.kde_decoration_manager = bind(&globals, &qh); state.kde_decoration_manager = bind(&globals, &qh);
state.kde_slide_manager = bind(&globals, &qh); state.kde_slide_manager = bind(&globals, &qh);
state.xdg_activation = bind(&globals, &qh); state.xdg_activation = bind(&globals, &qh);
globals.contents().with_list(|list| {
for g in list {
let bound = match g.interface.as_str() {
"org_kde_kwin_blur_manager" => state.kde_blur_manager.is_some(),
"org_kde_kwin_server_decoration_manager" => {
state.kde_decoration_manager.is_some()
}
"org_kde_kwin_slide_manager" => state.kde_slide_manager.is_some(),
"xdg_activation_v1" => state.xdg_activation.is_some(),
_ => false,
};
if bound && let Some(name) = BOUND_INTERFACES.iter().find(|n| **n == g.interface) {
state.bound_names.insert(name, g.name);
}
}
});
// Do another round-trip to get the default decoration mode // Do another round-trip to get the default decoration mode
queue queue
@@ -530,14 +617,21 @@ fn entered_monitor(host: &dyn WindowHost, monitor: &gdk::Monitor) {
impl Dispatch<WlRegistry, GlobalListContents> for State { impl Dispatch<WlRegistry, GlobalListContents> for State {
fn event( fn event(
_: &mut Self, state: &mut Self,
_: &WlRegistry, registry: &WlRegistry,
_: wayland_client::protocol::wl_registry::Event, event: wayland_client::protocol::wl_registry::Event,
_: &GlobalListContents, _: &GlobalListContents,
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, qh: &QueueHandle<Self>,
) { ) {
// Globals appearing/disappearing after init are not tracked. use wayland_client::protocol::wl_registry::Event;
match event {
Event::Global {
name, interface, ..
} => state.registry_global(registry, qh, name, &interface),
Event::GlobalRemove { name } => state.registry_global_remove(name),
_ => {}
}
} }
} }
+1
View File
@@ -9,6 +9,7 @@ ghostty-vt.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-config.workspace = true ghostty-config.workspace = true
ghostty-datastruct.workspace = true ghostty-datastruct.workspace = true
ghostty-inspector.workspace = true
ghostty-terminal.workspace = true ghostty-terminal.workspace = true
ghostty-renderer.workspace = true ghostty-renderer.workspace = true
thiserror = "2.0.21" thiserror = "2.0.21"
+10 -2
View File
@@ -242,11 +242,19 @@ pub trait CoreSurface {
Err("screenshots are not supported".into()) Err("screenshots are not supported".into())
} }
// ---- inspector (deferred) ------------------------------------------- // ---- inspector ----------------------------------------------------
/// `activateInspector`: begin collecting inspection data. This does not
/// affect the GUI, the runtime shows/hides the inspector UI itself.
fn activate_inspector(&self) -> Result<(), CoreError> { fn activate_inspector(&self) -> Result<(), CoreError> {
Ok(()) Ok(())
} }
/// `deactivateInspector`: stop collecting inspection data.
fn deactivate_inspector(&self) {} fn deactivate_inspector(&self) {}
/// `Inspector.render`: draw the inspector into the current Dear ImGui
/// frame (the main thread; called from the inspector widget's render).
fn render_inspector(&self, ui: &ghostty_inspector::imgui::Ui) {
let _ = ui;
}
} }
/// Implemented by this crate (GTK `Application`); handed to core. /// Implemented by this crate (GTK `Application`); handed to core.
@@ -278,7 +286,7 @@ pub trait RtSurface {
fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool); fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool);
/// `defaultTermioEnv`: env vars to set for the child (GDK/GTK related /// `defaultTermioEnv`: env vars to set for the child (GDK/GTK related
/// cleanups). /// cleanups).
fn default_termio_env(&self) -> Vec<(String, String)>; fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)>;
fn redraw_inspector(&self) {} fn redraw_inspector(&self) {}
/// Resolve an OpenGL function pointer for the surface's GL context /// Resolve an OpenGL function pointer for the surface's GL context
/// (GTK: dlsym on libepoxy). Called with the context current. Null if /// (GTK: dlsym on libepoxy). Called with the context current. Null if
+1
View File
@@ -14,6 +14,7 @@ ghostty-os.workspace = true
ghostty-vt.workspace = true ghostty-vt.workspace = true
log = "0.4.34" log = "0.4.34"
ratatui = "0.30.2" ratatui = "0.30.2"
termina = "0.3.3"
thiserror = "2.0.21" thiserror = "2.0.21"
unicode-width = "0.2.2" unicode-width = "0.2.2"
+1 -1
View File
@@ -53,7 +53,7 @@ pub trait ActionEnum: Sized + Copy {
/// Detect the action from the process CLI args (Zig: `detectArgs`). /// Detect the action from the process CLI args (Zig: `detectArgs`).
pub fn detect_args<E: ActionEnum>() -> Result<Option<E>, DetectError> { pub fn detect_args<E: ActionEnum>() -> Result<Option<E>, DetectError> {
detect_iter(std::env::args()) detect_iter(ghostty_os::args::collect_lossy())
} }
/// Detect the action from any iterator. Each iterator value should yield /// Detect the action from any iterator. Each iterator value should yield
+5 -9
View File
@@ -49,15 +49,11 @@ pub struct BuildInfo {
impl Default for BuildInfo { impl Default for BuildInfo {
fn default() -> Self { fn default() -> Self {
BuildInfo { BuildInfo {
version_string: "1.3.1".to_string(), // Derived by `build.rs` (git describe / workspace app-version).
commit: None, version_string: env!("GHOSTTY_VERSION_STRING").to_string(),
release_channel: "stable".to_string(), commit: Some(env!("GHOSTTY_COMMIT").to_string()).filter(|c| !c.is_empty()),
build_mode: if cfg!(debug_assertions) { release_channel: env!("GHOSTTY_RELEASE_CHANNEL").to_string(),
".Debug" build_mode: env!("GHOSTTY_BUILD_MODE").to_string(),
} else {
".ReleaseFast"
}
.to_string(),
app_runtime: ".gtk".to_string(), app_runtime: ".gtk".to_string(),
font_backend: ".fontconfig_freetype".to_string(), font_backend: ".fontconfig_freetype".to_string(),
renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(), renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(),
+31
View File
@@ -112,6 +112,37 @@ impl Action {
format!("cli/{}.zig", self.name().replace('-', "_")) format!("cli/{}.zig", self.name().replace('-', "_"))
} }
/// The non-internal option fields of the action's `Options` struct with
/// their type classification (Zig: `Action.options()` + `@typeInfo`).
/// Used by the build-data generator for shell completions.
#[must_use]
pub fn option_fields(self) -> Vec<(&'static str, ghostty_config::cli_args::FieldKind)> {
use ghostty_config::cli_args::FieldKind;
use ghostty_config::macros::ConfigStruct;
fn of<T: ConfigStruct>() -> Vec<(&'static str, FieldKind)> {
T::FIELDS
.iter()
.filter(|f| !f.name.starts_with('_'))
.map(|f| (f.name, f.kind))
.collect()
}
match self {
Action::Version | Action::EditConfig | Action::CrashReport | Action::Boo => Vec::new(),
Action::Help => of::<crate::help::Options>(),
Action::ListFonts => of::<crate::list_fonts::Options>(),
Action::ListKeybinds => of::<crate::list_keybinds::Options>(),
Action::ListThemes => of::<crate::list_themes::Options>(),
Action::ListColors => of::<crate::list_colors::Options>(),
Action::ListActions => of::<crate::list_actions::Options>(),
Action::SshCache => of::<crate::ssh_cache::Options>(),
Action::ShowConfig => of::<crate::show_config::Options>(),
Action::ValidateConfig => of::<crate::validate_config::Options>(),
Action::ShowFace => of::<crate::show_face::Options>(),
// `class: ?[:0]const u8` (the other fields are internal).
Action::NewWindow => vec![("class", FieldKind::OptionalOther)],
}
}
/// The help text of the action (the doc comment of the Zig `run` /// The help text of the action (the doc comment of the Zig `run`
/// function, i.e. `help_strings.Action.<name>`), without trailing newline. /// function, i.e. `help_strings.Action.<name>`), without trailing newline.
#[must_use] #[must_use]
+471 -42
View File
@@ -3,24 +3,29 @@
//! implementation with the same layout, key bindings and behaviour. //! implementation with the same layout, key bindings and behaviour.
//! //!
//! Known differences from the vaxis version: //! Known differences from the vaxis version:
//! * hyperlinks (OSC 8) on theme names/paths are not emitted, //! * input is read with `termina` (not crossterm) so terminal responses
//! * double/dashed/dotted/curly underlines are drawn as single underlines, //! such as the color-scheme report are visible; drawing still goes
//! * the terminal color scheme is not queried live: it defaults to light //! through ratatui's crossterm backend,
//! * OSC 8 hyperlinks and the double/dashed/dotted/curly underline styles
//! are re-emitted as raw escapes after each frame (see [`Overlay`]),
//! * the color scheme is queried (`CSI ? 996 n`) and tracked
//! (`CSI ? 2031 h`) (sent unconditionally; vaxis gates on DECRQM 2031 which
//! termina cannot parse); it starts as light
//! like the Zig code does, but `COLORFGBG` is used as a hint if set. //! like the Zig code does, but `COLORFGBG` is used as a hint if set.
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Write as _;
use std::io::Write; use std::io::Write;
use ghostty_config::config::Config; use ghostty_config::config::Config;
use ghostty_config::types_a::TerminalColor; use ghostty_config::types_a::TerminalColor;
use ratatui::Frame;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{ use ratatui::crossterm::event::{
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
MouseEventKind,
}; };
use ratatui::layout::{Position, Rect}; use ratatui::layout::{Position, Rect};
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::{Frame, crossterm};
use super::{ use super::{
ColorScheme, SMALL_LIST_THRESHOLD, ThemeListElement, absolute_path, should_include_theme, ColorScheme, SMALL_LIST_THRESHOLD, ThemeListElement, absolute_path, should_include_theme,
@@ -189,6 +194,146 @@ fn base64(data: &[u8]) -> String {
out out
} }
/// Underline styles ratatui cannot express (`SGR 4:N`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ul {
Double,
Curly,
Dotted,
Dashed,
}
impl Ul {
fn sgr(self) -> &'static str {
match self {
Ul::Double => "4:2",
Ul::Curly => "4:3",
Ul::Dotted => "4:4",
Ul::Dashed => "4:5",
}
}
}
/// A recorded decoration of drawn text (absolute cell coordinates).
struct Deco {
x: u16,
y: u16,
width: u16,
link: Option<String>,
ul: Option<Ul>,
}
/// A decorated run of cells, re-emitted as raw escapes after ratatui has
/// flushed the frame: text with an OSC 8 hyperlink and/or an extended
/// underline style. Each run is a single-style, single-row segment.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Overlay {
pub x: u16,
pub y: u16,
/// The cell text of the run.
pub text: String,
pub style: Style,
pub link: Option<String>,
pub ul: Option<Ul>,
}
impl Overlay {
fn capture(buf: &Buffer, d: &Deco) -> Overlay {
let mut text = String::new();
let mut skip = 0u16;
let mut style = Style::default();
for dx in 0..d.width {
let Some(cell) = buf.cell((d.x + dx, d.y)) else {
break;
};
if dx == 0 {
style = cell.style();
}
if skip > 0 {
skip -= 1;
continue;
}
text.push_str(cell.symbol());
skip = gw(cell.symbol()).saturating_sub(1);
}
Overlay {
x: d.x,
y: d.y,
text,
style,
link: d.link.clone(),
ul: d.ul,
}
}
}
fn sgr_color(c: Color, fg: bool) -> String {
let (base, bright, ext, reset) = if fg {
(30, 90, 38, 39)
} else {
(40, 100, 48, 49)
};
match c {
Color::Reset => reset.to_string(),
Color::Black => base.to_string(),
Color::Red => (base + 1).to_string(),
Color::Green => (base + 2).to_string(),
Color::Yellow => (base + 3).to_string(),
Color::Blue => (base + 4).to_string(),
Color::Magenta => (base + 5).to_string(),
Color::Cyan => (base + 6).to_string(),
Color::Gray => (base + 7).to_string(),
Color::DarkGray => bright.to_string(),
Color::LightRed => (bright + 1).to_string(),
Color::LightGreen => (bright + 2).to_string(),
Color::LightYellow => (bright + 3).to_string(),
Color::LightBlue => (bright + 4).to_string(),
Color::LightMagenta => (bright + 5).to_string(),
Color::LightCyan => (bright + 6).to_string(),
Color::White => (bright + 7).to_string(),
Color::Rgb(r, g, b) => format!("{ext};2;{r};{g};{b}"),
Color::Indexed(i) => format!("{ext};5;{i}"),
}
}
/// The raw escape output that draws `overlays` on top of the frame.
#[must_use]
pub fn render_overlays(overlays: &[Overlay]) -> String {
let mut out = String::new();
for o in overlays {
let mut sgr: Vec<String> = vec!["0".to_owned()];
if let Some(c) = o.style.fg.map(|c| sgr_color(c, true)) {
sgr.push(c);
}
if let Some(c) = o.style.bg.map(|c| sgr_color(c, false)) {
sgr.push(c);
}
if o.style.add_modifier.contains(Modifier::BOLD) {
sgr.push("1".to_owned());
}
if o.style.add_modifier.contains(Modifier::ITALIC) {
sgr.push("3".to_owned());
}
if o.style.add_modifier.contains(Modifier::UNDERLINED) {
sgr.push("4".to_owned());
}
if let Some(ul) = o.ul {
sgr.push(ul.sgr().to_owned());
}
let _ = write!(out, "\x1b[{};{}H", o.y + 1, o.x + 1);
let _ = write!(out, "\x1b[{}m", sgr.join(";"));
if let Some(link) = &o.link {
let _ = write!(out, "\x1b]8;;{link}\x1b\\");
}
out.push_str(&o.text);
if o.link.is_some() {
out.push_str("\x1b]8;;\x1b\\");
}
out.push_str("\x1b[0m");
}
out
}
/// Cached result of loading the currently previewed theme. /// Cached result of loading the currently previewed theme.
enum Loaded { enum Loaded {
Ok(Box<Config>), Ok(Box<Config>),
@@ -222,6 +367,7 @@ pub struct Preview {
/// Where `w` writes the auto theme file (overridable for tests). /// Where `w` writes the auto theme file (overridable for tests).
pub auto_theme_path: Option<String>, pub auto_theme_path: Option<String>,
effects: Vec<Effect>, effects: Vec<Effect>,
overlays: Vec<Overlay>,
} }
fn initial_scheme() -> Scheme { fn initial_scheme() -> Scheme {
@@ -257,6 +403,7 @@ impl Preview {
loaded: None, loaded: None,
auto_theme_path: None, auto_theme_path: None,
effects: vec![], effects: vec![],
overlays: vec![],
}; };
p.update_filtered(); p.update_filtered();
p p
@@ -535,7 +682,12 @@ impl Preview {
pub fn draw(&mut self, buf: &mut Buffer) -> Option<Position> { pub fn draw(&mut self, buf: &mut Buffer) -> Option<Position> {
let area = buf.area; let area = buf.area;
let cell = RefCell::new(buf); let cell = RefCell::new(buf);
let win = Win { buf: &cell, area }; let decos = RefCell::new(Vec::new());
let win = Win {
buf: &cell,
decos: &decos,
area,
};
win.clear(); win.clear();
let theme_list = win.child(0, 0, LIST_WIDTH.min(area.width), area.height); let theme_list = win.child(0, 0, LIST_WIDTH.min(area.width), area.height);
@@ -607,7 +759,7 @@ impl Preview {
if style == St::Selected { if style == St::Selected {
theme_list.print("\u{276f} ", self.ui_selected(), row, 0); theme_list.print("\u{276f} ", self.ui_selected(), row, 0);
} }
theme_list.print( theme_list.print_ex(
&theme.theme, &theme.theme,
match style { match style {
St::Normal => self.ui_standard(), St::Normal => self.ui_standard(),
@@ -616,6 +768,8 @@ impl Preview {
}, },
row, row,
2, 2,
Some(&theme.to_uri()),
None,
); );
if style == St::Selected { if style == St::Selected {
let name_len = gw(&theme.theme); let name_len = gw(&theme.theme);
@@ -732,9 +886,24 @@ impl Preview {
} }
} }
} }
// Collect the decorations (hyperlinks, extended underlines) with the
// final cell contents/styles for the raw-escape overlay pass.
let decos = decos.into_inner();
let buf = cell.into_inner();
self.overlays = decos
.iter()
.map(|d| Overlay::capture(buf, d))
.collect::<Vec<_>>();
cursor cursor
} }
/// Hyperlink / extended-underline overlays of the last [`Self::draw`].
#[must_use]
pub fn overlays(&self) -> &[Overlay] {
&self.overlays
}
fn load_current(&mut self) -> Option<&Loaded> { fn load_current(&mut self) -> Option<&Loaded> {
let theme = self.selected()?.clone(); let theme = self.selected()?.clone();
let stale = self.loaded.as_ref().is_none_or(|(p, _)| p != &theme.path); let stale = self.loaded.as_ref().is_none_or(|(p, _)| p != &theme.path);
@@ -785,11 +954,13 @@ impl Preview {
middle.saturating_sub(1), middle.saturating_sub(1),
(child.area.width / 2).saturating_sub(gw(&text) / 2), (child.area.width / 2).saturating_sub(gw(&text) / 2),
); );
child.print( child.print_ex(
&theme.path, &theme.path,
ui_err, ui_err,
middle, middle,
(child.area.width / 2).saturating_sub(gw(&theme.path) / 2), (child.area.width / 2).saturating_sub(gw(&theme.path) / 2),
Some(&theme.to_uri()),
None,
); );
child.print( child.print(
err, err,
@@ -821,8 +992,8 @@ impl Preview {
let standard_bold = standard.add_modifier(Modifier::BOLD); let standard_bold = standard.add_modifier(Modifier::BOLD);
let standard_italic = standard.add_modifier(Modifier::ITALIC); let standard_italic = standard.add_modifier(Modifier::ITALIC);
let standard_bold_italic = standard.add_modifier(Modifier::BOLD | Modifier::ITALIC); let standard_bold_italic = standard.add_modifier(Modifier::BOLD | Modifier::ITALIC);
// TODO: vaxis supports double/dashed/dotted/curly underlines. // Single underline is a plain SGR 4; the other styles (double,
let standard_underline = standard.add_modifier(Modifier::UNDERLINED); // dashed, dotted, curly) are re-emitted as raw `SGR 4:N` overlays.
let color = |i: usize| { let color = |i: usize| {
let c = config.palette.value[i]; let c = config.palette.value[i];
@@ -833,17 +1004,22 @@ impl Preview {
{ {
let child = win.child(x_off, next_start, width, 4); let child = win.child(x_off, next_start, width, 4);
child.fill(standard); child.fill(standard);
child.print( let uri = theme.to_uri();
child.print_ex(
&theme.theme, &theme.theme,
standard_bold_italic, standard_bold_italic,
1, 1,
(child.area.width / 2).saturating_sub(gw(&theme.theme) / 2), (child.area.width / 2).saturating_sub(gw(&theme.theme) / 2),
Some(&uri),
None,
); );
child.print( child.print_ex(
&theme.path, &theme.path,
standard, standard,
2, 2,
(child.area.width / 2).saturating_sub(gw(&theme.path) / 2), (child.area.width / 2).saturating_sub(gw(&theme.path) / 2),
Some(&uri),
None,
); );
next_start += child.area.height; next_start += child.area.height;
} }
@@ -904,7 +1080,6 @@ impl Preview {
(c(1), c(2), c(3), c(4), c(5), c(6)); (c(1), c(2), c(3), c(4), c(5), c(6));
let color6ul = color6.add_modifier(Modifier::UNDERLINED); let color6ul = color6.add_modifier(Modifier::UNDERLINED);
let (color10, color12, color238) = (c(10), c(12), c(238)); let (color10, color12, color238) = (c(10), c(12), c(238));
let _ = standard_underline;
child.fill(standard); child.fill(standard);
let rule = |row: u16, head: &str| { let rule = |row: u16, head: &str| {
@@ -1174,19 +1349,32 @@ impl Preview {
row += 1; row += 1;
col = 2; col = 2;
} }
let mut ul = None;
let style = match word { let style = match word {
"ipsum" => Style::default().fg(color(2)).bg(bg), "ipsum" => Style::default().fg(color(2)).bg(bg),
"consectetur" => standard_bold, "consectetur" => standard_bold,
"reprehenderit" => standard_italic, "reprehenderit" => standard_italic,
"Praesent" => standard_bold_italic, "Praesent" => standard_bold_italic,
"auctor" | "dui" | "erat" | "enim" | "odio" => { "auctor" => standard.add_modifier(Modifier::UNDERLINED),
// TODO: distinct underline styles (double, dashed, "dui" => {
// dotted, curly) are drawn as single underlines. ul = Some(Ul::Double);
standard.add_modifier(Modifier::UNDERLINED) standard
}
"erat" => {
ul = Some(Ul::Dashed);
standard
}
"enim" => {
ul = Some(Ul::Dotted);
standard
}
"odio" => {
ul = Some(Ul::Curly);
standard
} }
_ => standard, _ => standard,
}; };
child.print(word, style, row, col); child.print_ex(word, style, row, col, None, ul);
col = col.saturating_add(word_len).saturating_add(1); col = col.saturating_add(word_len).saturating_add(1);
} }
} }
@@ -1245,6 +1433,7 @@ fn gw(s: &str) -> u16 {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct Win<'a, 'b> { struct Win<'a, 'b> {
buf: &'a RefCell<&'b mut Buffer>, buf: &'a RefCell<&'b mut Buffer>,
decos: &'a RefCell<Vec<Deco>>,
area: Rect, area: Rect,
} }
@@ -1267,6 +1456,12 @@ impl Win<'_, '_> {
/// `Window.fill` with a blank cell of the given style. /// `Window.fill` with a blank cell of the given style.
fn fill(&self, style: Style) { fn fill(&self, style: Style) {
// Anything previously drawn here is overwritten, including its
// hyperlink/underline decorations.
let a = self.area;
self.decos.borrow_mut().retain(|d| {
!(d.y >= a.top() && d.y < a.bottom() && d.x < a.right() && d.x + d.width > a.left())
});
let mut buf = self.buf.borrow_mut(); let mut buf = self.buf.borrow_mut();
for y in self.area.top()..self.area.bottom() { for y in self.area.top()..self.area.bottom() {
for x in self.area.left()..self.area.right() { for x in self.area.left()..self.area.right() {
@@ -1287,6 +1482,7 @@ impl Win<'_, '_> {
let h = height.min(self.area.bottom() - y); let h = height.min(self.area.bottom() - y);
Win { Win {
buf: self.buf, buf: self.buf,
decos: self.decos,
area: Rect::new(x, y, w, h), area: Rect::new(x, y, w, h),
} }
} }
@@ -1323,6 +1519,21 @@ impl Win<'_, '_> {
/// `Window.printSegment` without wrapping (text is clipped). Returns the /// `Window.printSegment` without wrapping (text is clipped). Returns the
/// column after the text. /// column after the text.
fn print(&self, text: &str, style: Style, row: u16, col: u16) -> u16 { fn print(&self, text: &str, style: Style, row: u16, col: u16) -> u16 {
self.print_ex(text, style, row, col, None, None)
}
/// `Window.printSegment` with an optional OSC 8 hyperlink and an optional
/// extended underline style (ratatui cannot express either, so they are
/// recorded and re-emitted as raw escapes after the frame is flushed).
fn print_ex(
&self,
text: &str,
style: Style,
row: u16,
col: u16,
link: Option<&str>,
ul: Option<Ul>,
) -> u16 {
if row >= self.area.height || col >= self.area.width { if row >= self.area.height || col >= self.area.width {
return col; return col;
} }
@@ -1330,6 +1541,15 @@ impl Win<'_, '_> {
let y = self.area.y + row; let y = self.area.y + row;
let max = usize::from(self.area.width - col); let max = usize::from(self.area.width - col);
let (nx, _) = self.buf.borrow_mut().set_stringn(x, y, text, max, style); let (nx, _) = self.buf.borrow_mut().set_stringn(x, y, text, max, style);
if (link.is_some() || ul.is_some()) && nx > x {
self.decos.borrow_mut().push(Deco {
x,
y,
width: nx - x,
link: link.map(str::to_owned),
ul,
});
}
nx - self.area.x nx - self.area.x
} }
@@ -1345,37 +1565,158 @@ impl Win<'_, '_> {
// ---- The terminal driver ---- // ---- The terminal driver ----
/// Convert a `termina` key event to the crossterm event type `Preview`
/// handles. Keys the preview has no use for map to `None`.
fn convert_key(k: termina::event::KeyEvent) -> Option<KeyEvent> {
use termina::event::{KeyCode as T, KeyEventKind as TK, Modifiers as TM};
let code = match k.code {
T::Char(c) => KeyCode::Char(c),
T::Enter => KeyCode::Enter,
T::Backspace => KeyCode::Backspace,
T::Tab => KeyCode::Tab,
T::Escape => KeyCode::Esc,
T::Left => KeyCode::Left,
T::Right => KeyCode::Right,
T::Up => KeyCode::Up,
T::Down => KeyCode::Down,
T::Home => KeyCode::Home,
T::End => KeyCode::End,
T::BackTab => KeyCode::BackTab,
T::PageUp => KeyCode::PageUp,
T::PageDown => KeyCode::PageDown,
T::Insert => KeyCode::Insert,
T::Delete => KeyCode::Delete,
T::Function(n) => KeyCode::F(n),
_ => return None,
};
let mut modifiers = KeyModifiers::NONE;
for (t, c) in [
(TM::SHIFT, KeyModifiers::SHIFT),
(TM::ALT, KeyModifiers::ALT),
(TM::CONTROL, KeyModifiers::CONTROL),
(TM::SUPER, KeyModifiers::SUPER),
(TM::HYPER, KeyModifiers::HYPER),
(TM::META, KeyModifiers::META),
] {
if k.modifiers.contains(t) {
modifiers |= c;
}
}
let kind = match k.kind {
TK::Press => KeyEventKind::Press,
TK::Release => KeyEventKind::Release,
TK::Repeat => KeyEventKind::Repeat,
};
let mut ev = KeyEvent::new(code, modifiers);
ev.kind = kind;
Some(ev)
}
fn convert_mouse(m: termina::event::MouseEvent) -> MouseEvent {
use termina::event::{MouseButton as TB, MouseEventKind as TK};
let button = |b: TB| match b {
TB::Left => MouseButton::Left,
TB::Right => MouseButton::Right,
TB::Middle => MouseButton::Middle,
};
let kind = match m.kind {
TK::Down(b) => MouseEventKind::Down(button(b)),
TK::Up(b) => MouseEventKind::Up(button(b)),
TK::Drag(b) => MouseEventKind::Drag(button(b)),
TK::Moved => MouseEventKind::Moved,
TK::ScrollDown => MouseEventKind::ScrollDown,
TK::ScrollUp => MouseEventKind::ScrollUp,
TK::ScrollLeft => MouseEventKind::ScrollLeft,
TK::ScrollRight => MouseEventKind::ScrollRight,
};
MouseEvent {
kind,
column: m.column,
row: m.row,
modifiers: KeyModifiers::NONE,
}
}
/// Feed one terminal input event to the preview. Handles the terminal's
/// color-scheme report (`CSI ? 997 ; 1|2 n`, sent in answer to
/// `CSI ? 996 n` and on every change while `CSI ? 2031 h` is active), which
/// vaxis delivers as its `color_scheme` event.
fn handle_terminal_event(app: &mut Preview, ev: termina::Event) {
use termina::escape::csi::{Csi, Mode, ThemeMode};
match ev {
termina::Event::Key(k) => {
if let Some(k) = convert_key(k) {
app.update(&Event::Key(k));
}
}
termina::Event::Mouse(m) => app.update(&Event::Mouse(convert_mouse(m))),
termina::Event::Csi(Csi::Mode(Mode::ReportTheme(t))) => {
app.color_scheme = match t {
ThemeMode::Dark => Scheme::Dark,
ThemeMode::Light => Scheme::Light,
};
}
_ => {}
}
}
/// Run the interactive preview (Zig: `preview`). Blocks until the user quits. /// Run the interactive preview (Zig: `preview`). Blocks until the user quits.
pub fn preview(themes: Vec<ThemeListElement>, theme_filter: ColorScheme) -> Result<(), Error> { pub fn preview(themes: Vec<ThemeListElement>, theme_filter: ColorScheme) -> Result<(), Error> {
let mut app = Preview::new(themes, theme_filter); let mut app = Preview::new(themes, theme_filter);
let mut terminal = ratatui::try_init()?; run_loop(&mut app)
let result = run_loop(&mut terminal, &mut app);
ratatui::restore();
result
} }
fn run_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut Preview) -> Result<(), Error> { fn run_loop(app: &mut Preview) -> Result<(), Error> {
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use ratatui::Terminal as RatTerminal;
use crossterm::terminal::SetTitle; use ratatui::backend::CrosstermBackend;
use termina::escape::csi::{Csi, DecPrivateMode, DecPrivateModeCode, Mode};
use termina::{PlatformTerminal, Terminal as _};
{ let mut tty = PlatformTerminal::new()?;
let out = terminal.backend_mut(); tty.enter_raw_mode()?;
crossterm::execute!(
out,
SetTitle("\u{1f47b} Ghostty Theme Preview \u{1f47b}"),
EnableMouseCapture
)?;
}
// Alternate screen, title, mouse reporting.
write!(
tty,
"\x1b[?1049h\x1b]2;\u{1f47b} Ghostty Theme Preview \u{1f47b}\x1b\\\
\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h"
)?;
// Request the current color scheme (`CSI ? 996 n`) and subscribe to
// changes (`CSI ? 2031 h`). vaxis gates this on a DECRQM 2031 answer,
// but termina cannot parse that report, so (like most TUIs) both are
// sent unconditionally: terminals without support ignore them.
let theme_mode = DecPrivateMode::Code(DecPrivateModeCode::Theme);
write!(
tty,
"{}{}",
Csi::Mode(Mode::QueryTheme),
Csi::Mode(Mode::SetDecPrivateMode(theme_mode)),
)?;
tty.flush()?;
let mut terminal = RatTerminal::new(CrosstermBackend::new(std::io::stdout()))?;
let result = (|| -> Result<(), Error> { let result = (|| -> Result<(), Error> {
while !app.should_quit { while !app.should_quit {
terminal.draw(|f| draw_frame(app, f))?; let mut cursor = None;
terminal.draw(|f| cursor = draw_frame(app, f))?;
// Hyperlinks and extended underlines ratatui can't express.
if !app.overlays().is_empty() {
let backend = terminal.backend_mut();
write!(backend, "{}", render_overlays(app.overlays()))?;
if let Some(pos) = cursor {
write!(backend, "\x1b[{};{}H", pos.y + 1, pos.x + 1)?;
}
backend.flush()?;
}
// Block for an event, then drain everything that is pending. // Block for an event, then drain everything that is pending.
let first = event::read()?; let first = tty.read(|_| true)?;
app.update(&first); handle_terminal_event(app, first);
while event::poll(std::time::Duration::ZERO)? { while tty.poll(|_| true, Some(std::time::Duration::ZERO))? {
let ev = event::read()?; let ev = tty.read(|_| true)?;
app.update(&ev); handle_terminal_event(app, ev);
} }
for effect in app.take_effects() { for effect in app.take_effects() {
match effect { match effect {
@@ -1390,15 +1731,23 @@ fn run_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut Preview) -> Resul
Ok(()) Ok(())
})(); })();
let _ = crossterm::execute!(terminal.backend_mut(), DisableMouseCapture); let _ = terminal.show_cursor();
let _ = write!(
tty,
"{}\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1049l",
Csi::Mode(Mode::ResetDecPrivateMode(theme_mode)),
);
let _ = tty.flush();
let _ = tty.enter_cooked_mode();
result result
} }
fn draw_frame(app: &mut Preview, f: &mut Frame<'_>) { fn draw_frame(app: &mut Preview, f: &mut Frame<'_>) -> Option<Position> {
let cursor = app.draw(f.buffer_mut()); let cursor = app.draw(f.buffer_mut());
if let Some(pos) = cursor { if let Some(pos) = cursor {
f.set_cursor_position(pos); f.set_cursor_position(pos);
} }
cursor
} }
#[cfg(test)] #[cfg(test)]
@@ -1617,6 +1966,86 @@ mod tests {
assert_eq!(Color::Rgb(255, 255, 255), buf[(100, 0)].bg); assert_eq!(Color::Rgb(255, 255, 255), buf[(100, 0)].bg);
} }
#[test]
fn hyperlinks_and_underline_overlays() {
let tmp = tempfile::tempdir().unwrap();
let mut p = app(&tmp);
let _ = render(&mut p, 120, 60);
let ov = p.overlays().to_vec();
// List entries (4) + header name + header path are hyperlinks.
let links: Vec<&Overlay> = ov.iter().filter(|o| o.link.is_some()).collect();
assert!(links.len() >= 6, "{links:?}");
let alpha = links
.iter()
.find(|o| o.text == "alpha" && o.x == 2)
.unwrap();
assert!(alpha.link.as_deref().unwrap().starts_with("file:///"));
assert!(alpha.link.as_deref().unwrap().ends_with("/alpha"));
// The five lorem ipsum words get their own underline overlays.
let uls: Vec<(String, Ul)> = ov
.iter()
.filter_map(|o| o.ul.map(|u| (o.text.clone(), u)))
.collect();
for (word, ul) in [
("dui", Ul::Double),
("erat", Ul::Dashed),
("enim", Ul::Dotted),
("odio", Ul::Curly),
] {
assert!(uls.contains(&(word.to_owned(), ul)), "{word}: {uls:?}");
}
// And the raw escapes carry OSC 8 and the 4:N styles.
let raw = render_overlays(&ov);
assert!(raw.contains("\x1b]8;;file:///"));
assert!(raw.contains("\x1b]8;;\x1b\\"));
for sgr in ["4:2", "4:3", "4:4", "4:5"] {
assert!(raw.contains(sgr), "{sgr}");
}
}
#[test]
fn popup_drops_covered_overlays() {
let tmp = tempfile::tempdir().unwrap();
let mut p = app(&tmp);
p.update(&ch('?'));
let _ = render(&mut p, 120, 40);
// The help window covers the middle of the screen: no overlay may
// intersect it.
let (w, h) = (60u16, 22u16);
let (x0, y0) = (60 - w / 2, 20 - h / 2);
for o in p.overlays() {
let inside = o.y >= y0 && o.y < y0 + h && o.x < x0 + w && o.x + 1 > x0;
assert!(!inside, "{o:?}");
}
}
#[test]
fn terminal_theme_report_sets_scheme() {
let tmp = tempfile::tempdir().unwrap();
let mut p = app(&tmp);
let mut parser = termina::Parser::default();
parser.parse(b"\x1b[?997;1n", false);
handle_terminal_event(&mut p, parser.pop().unwrap());
assert_eq!(Scheme::Dark, p.color_scheme);
parser.parse(b"\x1b[?997;2n", false);
handle_terminal_event(&mut p, parser.pop().unwrap());
assert_eq!(Scheme::Light, p.color_scheme);
}
#[test]
fn termina_key_conversion() {
use termina::event::{KeyCode as T, Modifiers};
let k = convert_key(termina::event::KeyEvent::new(
T::Char('x'),
Modifiers::CONTROL,
))
.unwrap();
assert_eq!(KeyCode::Char('x'), k.code);
assert!(k.modifiers.contains(KeyModifiers::CONTROL));
assert_eq!(KeyCode::Esc, convert_key(T::Escape.into()).unwrap().code);
assert!(convert_key(T::CapsLock.into()).is_none());
}
#[test] #[test]
fn draw_help_and_save_and_search_overlays() { fn draw_help_and_save_and_search_overlays() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+7 -1
View File
@@ -92,7 +92,13 @@ mod tests {
#[test] #[test]
fn version_output_layout() { fn version_output_layout() {
let mut buf = Vec::new(); let mut buf = Vec::new();
write_version(&mut buf, &BuildInfo::default(), false).unwrap(); let info = BuildInfo {
version_string: "1.3.1".into(),
commit: None,
release_channel: "stable".into(),
..BuildInfo::default()
};
write_version(&mut buf, &info, false).unwrap();
let s = String::from_utf8(buf).unwrap(); let s = String::from_utf8(buf).unwrap();
assert!(s.starts_with( assert!(s.starts_with(
"Ghostty 1.3.1\n\nVersion\n - version: 1.3.1\n - channel: stable\nBuild Config\n" "Ghostty 1.3.1\n\nVersion\n - version: 1.3.1\n - channel: stable\nBuild Config\n"
+39 -2
View File
@@ -65,11 +65,39 @@ impl From<crate::comma_splitter::Error> for ArgError {
} }
} }
/// Coarse classification of a field's Zig type, used by the build-data
/// generator (shell completions) which switches on `@typeInfo(field.type)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
/// `bool`
Bool,
/// `?bool`
OptionalBool,
/// An enum with the given variant names.
Enum(&'static [&'static str]),
/// `?Enum`
OptionalEnum(&'static [&'static str]),
/// An optional of any other type (`?T`).
OptionalOther,
/// A packed struct of bools without `parseCLI`, with its field names.
Packed(&'static [&'static str]),
/// `RepeatablePath`
RepeatablePath,
/// Anything else.
Other,
}
/// A type that can be the value of a configuration / CLI field. /// A type that can be the value of a configuration / CLI field.
pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug { pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug {
/// For enum types, the list of valid values (used in error messages). /// For enum types, the list of valid values (used in error messages).
const VALID_VALUES: Option<&'static [&'static str]> = None; const VALID_VALUES: Option<&'static [&'static str]> = None;
/// Classification of this type for completion generation.
const KIND: FieldKind = match Self::VALID_VALUES {
Some(v) => FieldKind::Enum(v),
None => FieldKind::Other,
};
/// Parse `value` (the text after `=`, `None` if there was no `=`) into /// Parse `value` (the text after `=`, `None` if there was no `=`) into
/// `self`. Repeatable types accumulate; scalar types overwrite. /// `self`. Repeatable types accumulate; scalar types overwrite.
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError>; fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError>;
@@ -424,6 +452,8 @@ pub fn parse_int_base0(s: &str) -> Option<i128> {
// ConfigField impls for primitives // ConfigField impls for primitives
impl ConfigField for bool { impl ConfigField for bool {
const KIND: FieldKind = FieldKind::Bool;
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> { fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
*self = parse_bool(value.unwrap_or("t"))?; *self = parse_bool(value.unwrap_or("t"))?;
Ok(()) Ok(())
@@ -481,6 +511,12 @@ impl ConfigField for String {
} }
impl<T: ConfigField> ConfigField for Option<T> { impl<T: ConfigField> ConfigField for Option<T> {
const KIND: FieldKind = match T::KIND {
FieldKind::Bool => FieldKind::OptionalBool,
FieldKind::Enum(v) => FieldKind::OptionalEnum(v),
_ => FieldKind::OptionalOther,
};
fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> { fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> {
// For optional fields, we just treat it as the child type. // For optional fields, we just treat it as the child type.
if let Some(v) = self { if let Some(v) = self {
@@ -550,8 +586,9 @@ impl<I: Iterator<Item = String>> ArgIter for ArgsIterator<I> {
/// Create an args iterator for the process args. This will skip argv0. /// Create an args iterator for the process args. This will skip argv0.
#[must_use] #[must_use]
pub fn args_iterator() -> ArgsIterator<std::iter::Skip<std::env::Args>> { pub fn args_iterator() -> ArgsIterator<std::iter::Skip<std::vec::IntoIter<String>>> {
ArgsIterator::new(std::env::args().skip(1)) // Byte-safe: never panics on non-UTF-8 arguments.
ArgsIterator::new(ghostty_os::args::collect_lossy().into_iter().skip(1))
} }
/// An iterator valid for arg parsing from a slice. /// An iterator valid for arg parsing from a slice.
+6
View File
@@ -31,6 +31,8 @@ pub struct FieldInfo<T> {
pub name: &'static str, pub name: &'static str,
/// Raw doc comment lines. /// Raw doc comment lines.
pub doc: &'static [&'static str], pub doc: &'static [&'static str],
/// Classification of the field's type (for completion generation).
pub kind: crate::cli_args::FieldKind,
/// Deep equality of this field between two structs. /// Deep equality of this field between two structs.
pub eq: fn(&T, &T) -> bool, pub eq: fn(&T, &T) -> bool,
/// Format this field as in `+show-config`. /// Format this field as in `+show-config`.
@@ -139,6 +141,7 @@ macro_rules! config_struct {
$crate::macros::FieldInfo { $crate::macros::FieldInfo {
name: $key, name: $key,
doc: &[$($doc),*], doc: &[$($doc),*],
kind: <$ty as $crate::cli_args::ConfigField>::KIND,
eq: |a: &$name, b: &$name| a.$field == b.$field, eq: |a: &$name, b: &$name| a.$field == b.$field,
format: |a: &$name, name: &str, w: &mut dyn ::core::fmt::Write| { format: |a: &$name, name: &str, w: &mut dyn ::core::fmt::Write| {
let mut f = $crate::formatter::EntryFormatter::new(name, w); let mut f = $crate::formatter::EntryFormatter::new(name, w);
@@ -268,6 +271,9 @@ macro_rules! config_packed {
} }
impl $crate::cli_args::ConfigField for $name { impl $crate::cli_args::ConfigField for $name {
const KIND: $crate::cli_args::FieldKind =
$crate::cli_args::FieldKind::Packed(Self::NAMES);
fn parse_cli( fn parse_cli(
&mut self, &mut self,
value: ::core::option::Option<&str>, value: ::core::option::Option<&str>,
+2
View File
@@ -206,6 +206,8 @@ impl RepeatablePath {
} }
impl ConfigField for RepeatablePath { impl ConfigField for RepeatablePath {
const KIND: crate::cli_args::FieldKind = crate::cli_args::FieldKind::RepeatablePath;
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> { fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
let Some(item) = Path::parse(input)? else { let Some(item) = Path::parse(input)? else {
self.value.clear(); self.value.clear();
+1
View File
@@ -11,6 +11,7 @@ ghostty-apprt.workspace = true
ghostty-config.workspace = true ghostty-config.workspace = true
ghostty-datastruct.workspace = true ghostty-datastruct.workspace = true
ghostty-font.workspace = true ghostty-font.workspace = true
ghostty-inspector.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
ghostty-renderer.workspace = true ghostty-renderer.workspace = true
+50 -6
View File
@@ -17,6 +17,7 @@ use ghostty_input::binding::set::Value;
use ghostty_input::binding::{Action as BindingAction, Flags, Scope, Set, Trigger, TriggerKey}; use ghostty_input::binding::{Action as BindingAction, Flags, Scope, Set, Trigger, TriggerKey};
use ghostty_input::key_encode; use ghostty_input::key_encode;
use ghostty_input::{Action as KeyAction, Key, KeyEvent, Mods}; use ghostty_input::{Action as KeyAction, Key, KeyEvent, Mods};
use ghostty_inspector as inspector;
use ghostty_terminal::terminal::{MouseEvents, ScrollViewport, Terminal}; use ghostty_terminal::terminal::{MouseEvents, ScrollViewport, Terminal};
use ghostty_termio::message::Message as IoMessage; use ghostty_termio::message::Message as IoMessage;
use ghostty_vt::modes::Mode; use ghostty_vt::modes::Mode;
@@ -331,10 +332,40 @@ impl Surface {
// level. // level.
let event = Self::remap_event(&cfg, &event_orig); let event = Self::remap_event(&cfg, &event_orig);
// Setup our inspector event if we have an inspector.
let mut insp_ev = self
.inspector
.borrow()
.as_ref()
.map(|_| inspector::KeyEvent::new(&event));
let result = self.key_callback_remapped(&cfg, &event, insp_ev.as_mut());
// When we're done processing, we always want to add the event to
// the inspector. We have to check for the inspector again because
// our keybinding might have closed it.
if let Some(ev) = insp_ev
&& let Some(insp) = self.inspector()
{
insp.lock().record_key_event(ev);
self.queue_render();
}
result
}
/// The remainder of [`Surface::key_callback`] after the key remapping and
/// the inspector event setup.
fn key_callback_remapped(
&self,
cfg: &DerivedConfig,
event: &KeyEvent<'_>,
mut insp_ev: Option<&mut inspector::KeyEvent>,
) -> Result<InputEffect, CoreError> {
// Handle keybindings first. We need to handle this on all events // Handle keybindings first. We need to handle this on all events
// (press, repeat, release) because a press may perform a binding but // (press, repeat, release) because a press may perform a binding but
// a release should not encode if we consumed the press. // a release should not encode if we consumed the press.
if let Some(v) = self.maybe_handle_binding(&cfg, &event) { if let Some(v) = self.maybe_handle_binding(cfg, event, insp_ev.as_deref_mut()) {
return Ok(v); return Ok(v);
} }
@@ -359,7 +390,7 @@ impl Surface {
// highlight state. // highlight state.
let mouse_mods = self.mouse.borrow().mods; let mouse_mods = self.mouse.borrow().mods;
if !mouse_mods.equal(event.mods) { if !mouse_mods.equal(event.mods) {
self.refresh_mouse_for_mods_change(&event); self.refresh_mouse_for_mods_change(event);
} }
// Process the cursor state logic. This will update the cursor shape // Process the cursor state logic. This will update the cursor shape
@@ -381,7 +412,7 @@ impl Surface {
*pressed = 'event: { *pressed = 'event: {
// We need to unset the allocated fields that will become // We need to unset the allocated fields that will become
// invalid // invalid
let mut copy = OwnedKeyEvent::from_event(&event); let mut copy = OwnedKeyEvent::from_event(event);
copy.utf8.clear(); copy.utf8.clear();
// If we have a previous pressed key and we're releasing it // If we have a previous pressed key and we're releasing it
@@ -411,7 +442,7 @@ impl Surface {
// Encode and send our key. If we didn't encode anything, then we // Encode and send our key. If we didn't encode anything, then we
// return the effect as ignored. // return the effect as ignored.
let Some(write_req) = self.encode_key(&event) else { let Some(write_req) = self.encode_key(event, insp_ev) else {
// No valid request means that we didn't encode anything. // No valid request means that we didn't encode anything.
return Ok(InputEffect::Ignored); return Ok(InputEffect::Ignored);
}; };
@@ -491,6 +522,7 @@ impl Surface {
&self, &self,
cfg: &DerivedConfig, cfg: &DerivedConfig,
event: &KeyEvent<'_>, event: &KeyEvent<'_>,
mut insp_ev: Option<&mut inspector::KeyEvent>,
) -> Option<InputEffect> { ) -> Option<InputEffect> {
match event.action { match event.action {
// Release events never trigger a binding but we need to check // Release events never trigger a binding but we need to check
@@ -586,7 +618,7 @@ impl Surface {
// Store this event so that we can drain and encode on // Store this event so that we can drain and encode on
// invalid. We don't need to cap this because it is // invalid. We don't need to cap this because it is
// naturally capped by the config validation. // naturally capped by the config validation.
if let Some(req) = self.encode_key(event) { if let Some(req) = self.encode_key(event, insp_ev.as_deref_mut()) {
self.keyboard.borrow_mut().sequence_queued.push(req); self.keyboard.borrow_mut().sequence_queued.push(req);
} }
@@ -683,6 +715,9 @@ impl Surface {
// Store our last trigger so we don't encode the release event // Store our last trigger so we don't encode the release event
self.keyboard.borrow_mut().last_trigger = Some(event.binding_hash()); self.keyboard.borrow_mut().last_trigger = Some(event.binding_hash());
if let Some(ev) = insp_ev {
ev.binding = actions;
}
return Some(InputEffect::Consumed); return Some(InputEffect::Consumed);
} }
@@ -869,7 +904,11 @@ impl Surface {
/// Encodes the key event into a write request. Returns `None` if /// Encodes the key event into a write request. Returns `None` if
/// nothing was encoded. /// nothing was encoded.
fn encode_key(&self, event: &KeyEvent<'_>) -> Option<IoMessage> { fn encode_key(
&self,
event: &KeyEvent<'_>,
insp_ev: Option<&mut inspector::KeyEvent>,
) -> Option<IoMessage> {
// Build our encoding options, which requires the lock. // Build our encoding options, which requires the lock.
let opts = { let opts = {
let t = self.lock_terminal(); let t = self.lock_terminal();
@@ -887,6 +926,11 @@ impl Surface {
return None; return None;
} }
// Copy the encoded data into the inspector event if we have one.
if let Some(ev) = insp_ev {
ev.pty.clone_from(&data);
}
Some(IoMessage::write_req(&data)) Some(IoMessage::write_req(&data))
} }
+18 -1
View File
@@ -37,6 +37,7 @@
pub mod binding; pub mod binding;
pub mod clipboard; pub mod clipboard;
pub mod config; pub mod config;
pub mod inspector;
pub mod keyboard; pub mod keyboard;
pub mod mouse; pub mod mouse;
@@ -251,6 +252,9 @@ pub struct Surface {
/// Used to rate limit BEL handling. /// Used to rate limit BEL handling.
last_bell_time: Cell<Option<Instant>>, last_bell_time: Cell<Option<Instant>>,
/// The terminal inspector, if active (Zig: `inspector: ?*Inspector`).
pub(crate) inspector: RefCell<Option<Arc<ghostty_inspector::Shared>>>,
/// Debug screenshot state (see `debug_screenshot`). /// Debug screenshot state (see `debug_screenshot`).
created: Instant, created: Instant,
debug_shot_done: Cell<bool>, debug_shot_done: Cell<bool>,
@@ -474,6 +478,7 @@ impl Surface {
command_timer: Cell::new(None), command_timer: Cell::new(None),
search: RefCell::new(None), search: RefCell::new(None),
last_bell_time: Cell::new(None), last_bell_time: Cell::new(None),
inspector: RefCell::new(None),
created: Instant::now(), created: Instant::now(),
debug_shot_done: Cell::new(false), debug_shot_done: Cell::new(false),
}); });
@@ -1798,7 +1803,7 @@ impl Drop for Surface {
/// (`rt_surface.defaultTermioEnv()`, which already removed the variables it /// (`rt_surface.defaultTermioEnv()`, which already removed the variables it
/// does not want to leak); only if it provides nothing (the Zig error path) /// does not want to leak); only if it provides nothing (the Zig error path)
/// do we fall back to the process environment (`getEnvMap`). /// do we fall back to the process environment (`getEnvMap`).
fn rt_surface_env(rt: &dyn RtSurface) -> std::collections::HashMap<String, String> { fn rt_surface_env(rt: &dyn RtSurface) -> ghostty_os::EnvMap {
let rt_env = rt.default_termio_env(); let rt_env = rt.default_termio_env();
if rt_env.is_empty() { if rt_env.is_empty() {
return ghostty_os::env::get_env_map(); return ghostty_os::env::get_env_map();
@@ -1960,4 +1965,16 @@ impl CoreSurface for Surface {
ydpi: s.ydpi, ydpi: s.ydpi,
} }
} }
fn activate_inspector(&self) -> Result<(), CoreError> {
Surface::activate_inspector(self)
}
fn deactivate_inspector(&self) {
Surface::deactivate_inspector(self);
}
fn render_inspector(&self, ui: &ghostty_inspector::imgui::Ui) {
Surface::render_inspector(self, ui);
}
} }
+20
View File
@@ -975,6 +975,13 @@ impl Surface {
) -> Result<bool, CoreError> { ) -> Result<bool, CoreError> {
let cfg = self.cfg(); let cfg = self.cfg();
// If we have an inspector, we always queue a render
let _inspector_render = self
.inspector
.borrow()
.is_some()
.then(|| RenderOnDrop(self));
// Always record our latest mouse state // Always record our latest mouse state
let hidden = { let hidden = {
let mut m = self.mouse.borrow_mut(); let mut m = self.mouse.borrow_mut();
@@ -1915,6 +1922,10 @@ impl Surface {
// The mouse position in the viewport // The mouse position in the viewport
let pos_vp = self.pos_to_viewport(f64::from(pos.x), f64::from(pos.y)); let pos_vp = self.pos_to_viewport(f64::from(pos.x), f64::from(pos.y));
// If we have an inspector, we need to always record position
// information
self.inspector_record_cursor_pos(f64::from(pos.x), f64::from(pos.y));
// We always reset the over link status because it will be // We always reset the over link status because it will be
// reprocessed below. But we need the old value to know if we need to // reprocessed below. But we need the old value to know if we need to
// undo mouse shape changes. // undo mouse shape changes.
@@ -2217,6 +2228,15 @@ fn button_from_index(i: usize) -> Option<MouseButton> {
}) })
} }
/// Queues a render when dropped (Zig: `defer self.queueRender()`).
struct RenderOnDrop<'a>(&'a Surface);
impl Drop for RenderOnDrop<'_> {
fn drop(&mut self) {
self.0.queue_render();
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+4 -4
View File
@@ -143,12 +143,12 @@ impl RtSurface for FakeRt {
} }
} }
} }
fn default_termio_env(&self) -> Vec<(String, String)> { fn default_termio_env(&self) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
// Like GTK: the process environment minus what the runtime does not // Like GTK: the process environment minus what the runtime does not
// want to leak (here HOME stands in for GDK_DISABLE & co), plus extras. // want to leak (here HOME stands in for GDK_DISABLE & co), plus extras.
let mut env: Vec<(String, String)> = let mut env: Vec<(std::ffi::OsString, std::ffi::OsString)> =
std::env::vars().filter(|(k, _)| k != "HOME").collect(); std::env::vars_os().filter(|(k, _)| k != "HOME").collect();
env.push(("TERM_PROGRAM_TEST".to_owned(), "1".to_owned())); env.push(("TERM_PROGRAM_TEST".into(), "1".into()));
env env
} }
fn gl_proc_address(&self, name: &str) -> *const std::ffi::c_void { fn gl_proc_address(&self, name: &str) -> *const std::ffi::c_void {
+1
View File
@@ -6,6 +6,7 @@ license.workspace = true
[dependencies] [dependencies]
ghostty-vt.workspace = true ghostty-vt.workspace = true
ghostty-unicode.workspace = true
indexmap = "2.14.2" indexmap = "2.14.2"
thiserror = "2.0.21" thiserror = "2.0.21"
+15 -1
View File
@@ -1225,7 +1225,17 @@ impl ActionKey {
} }
} }
/// The doc comment of the action (Zig `help_strings.KeybindAction`). /// The Zig `help_strings.KeybindAction` string: [`ActionKey::docs`]
/// without the single trailing newline every doc literal carries. (A
/// trailing empty `///` line in the Zig source is a second `\n`.)
#[must_use]
pub fn help_string(self) -> &'static str {
let d = self.docs();
d.strip_suffix('\n').unwrap_or(d)
}
/// The doc comment of the action (Zig `help_strings.KeybindAction`),
/// each literal ending in one extra newline.
pub fn docs(self) -> &'static str { pub fn docs(self) -> &'static str {
match self { match self {
ActionKey::Ignore => { ActionKey::Ignore => {
@@ -1438,6 +1448,7 @@ Valid arguments are:
Adjust the selection to the beginning or the end of the line Adjust the selection to the beginning or the end of the line
respectively. respectively.
"## "##
} }
ActionKey::JumpToPrompt => { ActionKey::JumpToPrompt => {
@@ -1468,6 +1479,7 @@ Valid actions are:
The default OS editor is determined by using `open` on macOS The default OS editor is determined by using `open` on macOS
and `xdg-open` on Linux. and `xdg-open` on Linux.
"## "##
} }
ActionKey::WriteScreenFile => { ActionKey::WriteScreenFile => {
@@ -1574,6 +1586,7 @@ Valid arguments:
Creates a new split along the larger direction. Creates a new split along the larger direction.
For example, if the parent split is currently wider than it is tall, For example, if the parent split is currently wider than it is tall,
then a left-right split would be created, and vice versa. then a left-right split would be created, and vice versa.
"## "##
} }
ActionKey::GotoSplit => { ActionKey::GotoSplit => {
@@ -1940,6 +1953,7 @@ The value determines the crash location:
- `render` - `render`
Crash on the render thread for the focused surface. Crash on the render thread for the focused surface.
"## "##
} }
} }
+3 -22
View File
@@ -498,10 +498,8 @@ pub const BACKWARDS_COMPATIBLE_KEYS: &[(&str, TriggerKey)] = {
/// The codepoint we use for comparisons. Case folding can result /// The codepoint we use for comparisons. Case folding can result
/// in more codepoints so we need to use a 3 element array. /// in more codepoints so we need to use a 3 element array.
/// ///
/// DEVIATION: Zig uses uucode's full case folding table. Here we use /// Uses the full Unicode case folding table (uucode's `case_folding_full`,
/// `char::to_lowercase` (single-char results only) plus a few simple-folding /// generated by `ghostty-unicode`).
/// special cases; multi-codepoint foldings return the codepoint as-is exactly
/// like Zig does.
fn folded_codepoint(cp: u32) -> [u32; 3] { fn folded_codepoint(cp: u32) -> [u32; 3] {
// ASCII fast path // ASCII fast path
if let Some(c) = char::from_u32(cp) if let Some(c) = char::from_u32(cp)
@@ -511,24 +509,7 @@ fn folded_codepoint(cp: u32) -> [u32; 3] {
} }
// Unicode slow path. Case folding can result in more codepoints. // Unicode slow path. Case folding can result in more codepoints.
// If more codepoints are produced then we return the codepoint ghostty_unicode::case_folding::fold_full(cp)
// as-is which isn't correct but until we have a failing test
// then I don't want to handle this.
let Some(c) = char::from_u32(cp) else {
return [cp, 0, 0];
};
// Simple-fold differences from lowercase.
match c {
'\u{00B5}' => return [0x03BC, 0, 0],
'\u{017F}' => return ['s' as u32, 0, 0],
'\u{03C2}' => return [0x03C3, 0, 0],
_ => {}
}
let mut it = c.to_lowercase();
match (it.next(), it.next()) {
(Some(l), None) => [l as u32, 0, 0],
_ => [cp, 0, 0],
}
} }
impl Trigger { impl Trigger {
+14
View File
@@ -1507,6 +1507,20 @@ fn trigger_hash_folds_case() {
assert!(!uni('a').equal(&uni('A'))); assert!(!uni('a').equal(&uni('A')));
} }
#[test]
fn trigger_full_unicode_case_folding() {
// Simple folds beyond to_lowercase: micro sign, long s, final sigma.
assert!(uni('\u{00B5}').folded_equal(&uni('\u{03BC}')));
assert!(uni('\u{017F}').folded_equal(&uni('s')));
assert!(uni('\u{03C2}').folded_equal(&uni('\u{03C3}')));
// Multi-codepoint full folds: capital sharp s folds to "ss", as does
// U+00DF itself.
assert!(uni('\u{1E9E}').folded_equal(&uni('\u{00DF}')));
assert_eq!(uni('\u{1E9E}').hash(), uni('\u{00DF}').hash());
// Non-equivalent codepoints stay distinct.
assert!(!uni('\u{00DF}').folded_equal(&uni('s')));
}
#[test] #[test]
fn trigger_display_roundtrip() { fn trigger_display_roundtrip() {
for s in ["super+ctrl+alt+shift+a", "catch_all", "ctrl+arrow_up", "+"] { for s in ["super+ctrl+alt+shift+a", "catch_all", "ctrl+arrow_up", "+"] {
+1 -1
View File
@@ -94,7 +94,7 @@ pub fn generate(writer: &mut String, format: Format, show_docs: bool) {
} }
if show_docs { if show_docs {
let mut iter = key.docs().split('\n').peekable(); let mut iter = key.help_string().split('\n').peekable();
while let Some(s) = iter.next() { while let Some(s) = iter.next() {
// If it is the last line and empty, then skip it. // If it is the last line and empty, then skip it.
if iter.peek().is_none() && s.is_empty() { if iter.peek().is_none() && s.is_empty() {
+3 -2
View File
@@ -1,7 +1,8 @@
//! Key events and key codes (ZIG/src/input/key.zig). //! Key events and key codes (ZIG/src/input/key.zig).
//! //!
//! The Zig `imguiKey` helper (cimgui constants) is intentionally not ported; //! The Zig `imguiKey` helper (cimgui constants) lives in the inspector crate
//! the inspector is deferred (see notes). //! (`ghostty_inspector::keys::imgui_key`) so that this crate stays free of
//! the ImGui dependency.
pub use crate::key_mods::Mods; pub use crate::key_mods::Mods;
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "ghostty-onig"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Minimal safe wrapper for the Oniguruma match-param calls the `onig` crate does not expose"
[dependencies]
onig = "6.5.3"
onig_sys = "69.9.3"
# This crate exists solely to hold one tiny, SAFETY-commented FFI call
# (`onig_set_retry_limit_in_search_of_match_param`), which the `onig` crate
# does not expose. Everything else in the workspace keeps `unsafe_code`
# forbidden.
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
# Porting a low-level terminal emulator involves a lot of intentional
# numeric casts and long state-machine functions; these pedantic lints are
# noise for this codebase.
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_sign_loss = "allow"
cast_precision_loss = "allow"
cast_lossless = "allow"
module_name_repetitions = "allow"
too_many_lines = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
similar_names = "allow"
struct_excessive_bools = "allow"
fn_params_excessive_bools = "allow"
match_same_arms = "allow"
unreadable_literal = "allow"
many_single_char_names = "allow"
doc_markdown = "allow"
must_use_candidate = "allow"
return_self_not_must_use = "allow"
used_underscore_binding = "allow"
large_stack_arrays = "allow"
inline_always = "allow"
+61
View File
@@ -0,0 +1,61 @@
//! Oniguruma `MatchParam` extensions missing from the `onig` crate.
//!
//! Ghostty's `StringMap` bounds regex work with
//! `MatchParam.setRetryLimitInSearch` (a *whole-search* retry budget); the
//! `onig` crate only wraps the per-match limit, so the search limit is set
//! here through `onig_sys`.
use onig::MatchParam;
/// Set the retry limit in search (Oniguruma's aggregate backtracking-work
/// budget across the entire search, as opposed to per match attempt).
pub fn set_retry_limit_in_search(param: &mut MatchParam, limit: u32) {
#[allow(unsafe_code)]
// SAFETY: `param.as_raw()` is the live, non-null `OnigMatchParam*` owned
// by `param` (allocated by `onig_new_match_param` in
// `MatchParam::default`) and we hold `&mut` so nothing else uses it for
// the duration of the call; the function only writes an integer field.
unsafe {
onig_sys::onig_set_retry_limit_in_search_of_match_param(
param.as_raw(),
std::os::raw::c_ulong::from(limit),
);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use onig::{Regex, SearchOptions};
fn search(re: &Regex, text: &str, limit: Option<u32>) -> Result<Option<usize>, onig::Error> {
let mut param = MatchParam::default();
if let Some(l) = limit {
set_retry_limit_in_search(&mut param, l);
}
re.search_with_param(
text,
0,
text.len(),
SearchOptions::SEARCH_OPTION_NONE,
None,
param,
)
}
#[test]
fn small_limit_aborts_catastrophic_search() {
// Classic exponential backtracking pattern.
let re = Regex::new("(a+)+$").unwrap();
let text = format!("{}b", "a".repeat(40));
// With a tiny whole-search budget the search errors out.
assert!(search(&re, &text, Some(1000)).is_err());
}
#[test]
fn generous_limit_allows_normal_search() {
let re = Regex::new("b+").unwrap();
assert_eq!(search(&re, "aaabbb", Some(100_000)).unwrap(), Some(3));
}
}
+22
View File
@@ -9,6 +9,17 @@ pub fn iterator() -> std::env::ArgsOs {
std::env::args_os() std::env::args_os()
} }
/// Collect the process arguments as `String`s without ever panicking on
/// non-UTF-8 bytes (invalid sequences become U+FFFD). The CLI/config layers
/// are string based; `+action` dispatch only depends on the ASCII prefix.
pub fn collect_lossy() -> Vec<String> {
lossy(iterator())
}
fn lossy(it: impl Iterator<Item = std::ffi::OsString>) -> Vec<String> {
it.map(|a| a.to_string_lossy().into_owned()).collect()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -18,4 +29,15 @@ mod tests {
let mut iter = iterator(); let mut iter = iterator();
assert!(!iter.next().unwrap().is_empty()); assert!(!iter.next().unwrap().is_empty());
} }
#[test]
fn lossy_does_not_panic_on_non_utf8() {
use std::os::unix::ffi::OsStringExt;
let args = vec![
std::ffi::OsString::from("ghostty"),
std::ffi::OsString::from_vec(vec![b'+', b'x', 0xff]),
];
let out = lossy(args.into_iter());
assert_eq!(out, vec!["ghostty".to_owned(), "+x\u{fffd}".to_owned()]);
}
} }
+201 -11
View File
@@ -1,24 +1,153 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use crate::flatpak::is_flatpak; use crate::flatpak::is_flatpak;
/// PATH-style delimiter (`std.fs.path.delimiter` on POSIX). /// PATH-style delimiter (`std.fs.path.delimiter` on POSIX).
const DELIMITER: char = ':'; const DELIMITER: char = ':';
/// A byte-preserving environment map (Zig `std.process.EnvMap`): keys and
/// values are arbitrary OS strings, never required to be UTF-8.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EnvMap(HashMap<OsString, OsString>);
impl EnvMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Option<OsString> {
self.0
.insert(key.as_ref().to_owned(), value.as_ref().to_owned())
}
pub fn get(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> {
self.0.get(key.as_ref()).map(OsString::as_os_str)
}
pub fn remove(&mut self, key: impl AsRef<OsStr>) -> Option<OsString> {
self.0.remove(key.as_ref())
}
pub fn contains_key(&self, key: impl AsRef<OsStr>) -> bool {
self.0.contains_key(key.as_ref())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&OsStr, &OsStr)> {
self.0.iter().map(|(k, v)| (k.as_os_str(), v.as_os_str()))
}
}
impl<K: AsRef<OsStr>> std::ops::Index<K> for EnvMap {
type Output = OsStr;
fn index(&self, key: K) -> &OsStr {
self.get(key).expect("no entry found for key")
}
}
impl<K: Into<OsString>, V: Into<OsString>> FromIterator<(K, V)> for EnvMap {
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
Self(
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
impl IntoIterator for EnvMap {
type Item = (OsString, OsString);
type IntoIter = std::collections::hash_map::IntoIter<OsString, OsString>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Get the environment map. Inside Flatpak this is empty (the sandbox /// Get the environment map. Inside Flatpak this is empty (the sandbox
/// environment is not meaningful for host commands). /// environment is not meaningful for host commands). Bytes are preserved.
pub fn get_env_map() -> HashMap<String, String> { pub fn get_env_map() -> EnvMap {
if is_flatpak() { if is_flatpak() {
return HashMap::new(); return EnvMap::new();
}
std::env::vars_os().collect()
}
/// Filter out Snap-specific environment variables and path components
/// (port of `Surface.filterSnapPaths`): remove any `SNAP_*` variable and
/// drop `:`-separated components that start with one of the snap
/// directories. Variables we set ourselves (`TERMINFO`, `GHOSTTY*`) are
/// left alone.
pub fn filter_snap_paths(env: &mut EnvMap) {
use std::os::unix::ffi::{OsStrExt, OsStringExt};
const SNAP_VARS: [&str; 5] = [
"SNAP",
"SNAP_USER_COMMON",
"SNAP_USER_DATA",
"SNAP_DATA",
"SNAP_COMMON",
];
let mut to_remove: Vec<OsString> = Vec::new();
let mut to_update: Vec<(OsString, OsString)> = Vec::new();
for (key, value) in env.iter() {
let kb = key.as_bytes();
if kb == b"TERMINFO" || kb.starts_with(b"GHOSTTY") {
continue;
}
if kb.starts_with(b"SNAP_") {
to_remove.push(key.to_owned());
continue;
}
let mut filtered: Vec<&[u8]> = Vec::new();
let mut modified = false;
for path in value.as_bytes().split(|&b| b == b':') {
let mut include = true;
for k in SNAP_VARS {
if let Some(snap_path) = env.get(k) {
let snap_path = snap_path.as_bytes();
if snap_path.is_empty() {
continue;
}
if path.starts_with(snap_path) {
include = false;
modified = true;
break;
}
}
}
if include {
filtered.push(path);
}
}
if modified {
if filtered.is_empty() {
to_remove.push(key.to_owned());
} else {
to_update.push((key.to_owned(), OsString::from_vec(filtered.join(&b':'))));
}
}
}
for (k, v) in to_update {
env.insert(k, v);
}
for k in to_remove {
env.remove(k);
} }
std::env::vars_os()
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.to_string_lossy().into_owned(),
)
})
.collect()
} }
/// Append a value to an environment variable such as PATH. /// Append a value to an environment variable such as PATH.
@@ -39,6 +168,28 @@ pub fn append_env_always(current: &str, value: &str) -> String {
format!("{current}{DELIMITER}{value}") format!("{current}{DELIMITER}{value}")
} }
/// Byte-preserving [`prepend_env`].
pub fn prepend_env_os(current: &OsStr, value: &OsStr) -> OsString {
if current.is_empty() {
return value.to_owned();
}
let mut out = value.to_owned();
out.push(DELIMITER.to_string());
out.push(current);
out
}
/// Byte-preserving [`append_env`].
pub fn append_env_os(current: &OsStr, value: &OsStr) -> OsString {
if current.is_empty() {
return value.to_owned();
}
let mut out = current.to_owned();
out.push(DELIMITER.to_string());
out.push(value);
out
}
/// Prepend a value to an environment variable such as PATH. /// Prepend a value to an environment variable such as PATH.
pub fn prepend_env(current: &str, value: &str) -> String { pub fn prepend_env(current: &str, value: &str) -> String {
// If there is no prior value, we return it as-is // If there is no prior value, we return it as-is
@@ -64,6 +215,35 @@ pub fn getenv_not_empty(key: &str) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::os::unix::ffi::OsStringExt;
#[test]
fn env_map_preserves_non_utf8() {
let mut env = EnvMap::new();
let v = OsString::from_vec(vec![b'a', 0xff, b'b']);
env.insert("K", &v);
assert_eq!(env.get("K"), Some(v.as_os_str()));
}
#[test]
fn filter_snap_paths_filters() {
let mut env = EnvMap::new();
env.insert("SNAP", "/snap/ghostty/1");
env.insert("SNAP_NAME", "ghostty");
env.insert("GHOSTTY_X", "/snap/ghostty/1/a");
env.insert("TERMINFO", "/snap/ghostty/1/ti");
env.insert("PATH", "/snap/ghostty/1/bin:/usr/bin:/snap/ghostty/1/x");
env.insert("ONLY", "/snap/ghostty/1/lib");
env.insert("KEEP", "/usr/lib");
filter_snap_paths(&mut env);
assert!(!env.contains_key("SNAP"));
assert!(!env.contains_key("SNAP_NAME"));
assert_eq!(env["GHOSTTY_X"], *"/snap/ghostty/1/a");
assert_eq!(env["TERMINFO"], *"/snap/ghostty/1/ti");
assert_eq!(env["PATH"], *"/usr/bin");
assert!(!env.contains_key("ONLY"));
assert_eq!(env["KEEP"], *"/usr/lib");
}
#[test] #[test]
fn append_env_empty() { fn append_env_empty() {
@@ -75,6 +255,16 @@ mod tests {
assert_eq!(append_env("a:b", "foo"), "a:b:foo"); assert_eq!(append_env("a:b", "foo"), "a:b:foo");
} }
#[test]
fn os_variants_preserve_bytes() {
let cur = OsString::from_vec(vec![0xff, b'a']);
let out = prepend_env_os(&cur, OsStr::new("x"));
assert_eq!(out.into_vec(), vec![b'x', b':', 0xff, b'a']);
let out = append_env_os(&cur, OsStr::new("x"));
assert_eq!(out.into_vec(), vec![0xff, b'a', b':', b'x']);
assert_eq!(append_env_os(OsStr::new(""), OsStr::new("x")), "x");
}
#[test] #[test]
fn prepend_env_empty() { fn prepend_env_empty() {
assert_eq!(prepend_env("", "foo"), "foo"); assert_eq!(prepend_env("", "foo"), "foo");
+63 -10
View File
@@ -84,7 +84,7 @@ pub struct FlatpakHostCommand {
/// Environment variables for the child process. If this is None, this /// Environment variables for the child process. If this is None, this
/// does not send any environment variables. /// does not send any environment variables.
pub env: Option<HashMap<String, String>>, pub env: Option<crate::env::EnvMap>,
/// File descriptors to send to the child process. It is up to the /// File descriptors to send to the child process. It is up to the
/// caller to create the file descriptors and set them up. They are /// caller to create the file descriptors and set them up. They are
@@ -113,14 +113,6 @@ impl FlatpakHostCommand {
} }
} }
fn state(&self) -> State {
*self
.shared
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Spawn the command. This will start the host command. On return, /// Spawn the command. This will start the host command. On return,
/// the pid will be available. This must only be called with the /// the pid will be available. This must only be called with the
/// state in "init". /// state in "init".
@@ -128,7 +120,19 @@ impl FlatpakHostCommand {
let request = Request { let request = Request {
argv: self.argv.clone(), argv: self.argv.clone(),
cwd: self.cwd.clone(), cwd: self.cwd.clone(),
env: self.env.clone().unwrap_or_default(), // The Development.HostCommand DBus method takes `a{ss}`, which
// can only carry UTF-8, so non-UTF-8 bytes are replaced here.
env: self
.env
.iter()
.flat_map(crate::env::EnvMap::iter)
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.to_string_lossy().into_owned(),
)
})
.collect(),
fds: [ fds: [
self.stdin self.stdin
.try_clone() .try_clone()
@@ -213,6 +217,38 @@ impl FlatpakHostCommand {
Ok(()) Ok(())
} }
/// Send a signal to the started command. This does nothing if the
/// command is not in the started state.
pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> {
self.signal_handle().signal(sig, pg)
}
/// A cloneable handle that can signal this command even after the
/// command itself has been moved into an exit watcher
/// (`wait_callback`). Zig's `FlatpakHostCommand` is a pointer shared
/// between the exit watcher and `Exec.stop`; this is the equivalent.
pub fn signal_handle(&self) -> FlatpakSignalHandle {
FlatpakSignalHandle {
shared: Arc::clone(&self.shared),
}
}
}
/// Handle used to signal a host command independent of its owner.
#[derive(Clone)]
pub struct FlatpakSignalHandle {
shared: Arc<Shared>,
}
impl FlatpakSignalHandle {
fn state(&self) -> State {
*self
.shared
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Send a signal to the started command. This does nothing if the /// Send a signal to the started command. This does nothing if the
/// command is not in the started state. /// command is not in the started state.
pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> { pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> {
@@ -392,6 +428,23 @@ mod tests {
assert_eq!(cmd.signal(15, false), Ok(())); assert_eq!(cmd.signal(15, false), Ok(()));
} }
#[test]
fn signal_handle_survives_wait_callback() {
let (r, w) = crate::pipe::pipe().unwrap();
let w2 = w.try_clone().unwrap();
let cmd = FlatpakHostCommand::new(vec!["true".into()], r, w, w2);
let handle = cmd.signal_handle();
let (tx, rx) = std::sync::mpsc::channel();
cmd.wait_callback(move |r| {
let _ = tx.send(r);
})
.unwrap();
// The watcher owns the command; the handle still works (no-op
// since never started) and the watcher reports the state error.
assert_eq!(handle.signal(1, true), Ok(()));
assert_eq!(rx.recv().unwrap(), Err(Error::FlatpakMustBeStarted));
}
#[test] #[test]
fn exit_status_extraction() { fn exit_status_extraction() {
// Mirrors posix.W.EXITSTATUS on a raw wait status. // Mirrors posix.W.EXITSTATUS on a raw wait status.
+4 -1
View File
@@ -47,7 +47,10 @@ pub(crate) mod testutil;
// Functions and types (mirrors the flat exports of `os/main.zig`). // Functions and types (mirrors the flat exports of `os/main.zig`).
pub use dbus::launched_by_dbus_activation; pub use dbus::launched_by_dbus_activation;
pub use desktop::{DesktopEnvironment, desktop_environment, launched_from_desktop}; pub use desktop::{DesktopEnvironment, desktop_environment, launched_from_desktop};
pub use env::{append_env, append_env_always, get_env_map, getenv, getenv_not_empty, prepend_env}; pub use env::{
EnvMap, append_env, append_env_always, append_env_os, filter_snap_paths, get_env_map, getenv,
getenv_not_empty, prepend_env, prepend_env_os,
};
pub use file::{Rlimit, alloc_tmp_dir, fix_max_files, restore_max_files}; pub use file::{Rlimit, alloc_tmp_dir, fix_max_files, restore_max_files};
pub use flatpak::{FlatpakHostCommand, is_flatpak}; pub use flatpak::{FlatpakHostCommand, is_flatpak};
pub use homedir::{expand_home, home}; pub use homedir::{expand_home, home};
+102 -7
View File
@@ -2,6 +2,7 @@
//! additional [`ParseOptions`] from `os/uri.zig`. //! additional [`ParseOptions`] from `os/uri.zig`.
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct ParseOptions { pub struct ParseOptions {
@@ -47,15 +48,26 @@ impl Component<'_> {
} }
} }
/// The raw (percent-decoded) value (`toRaw`). Invalid escapes are left /// The raw (percent-decoded) value (`toRaw`) as bytes. Invalid escapes
/// as-is, matching `std.Uri.percentDecodeInPlace`. /// are left as-is, matching `std.Uri.percentDecodeInPlace`. The decoded
pub fn to_raw(&self) -> Cow<'_, str> { /// bytes are NOT required to be UTF-8 (e.g. non-UTF-8 file names).
pub fn to_raw_bytes(&self) -> Cow<'_, [u8]> {
match self { match self {
Component::Raw(s) => Cow::Borrowed(s.as_ref()), Component::Raw(s) => Cow::Borrowed(s.as_bytes()),
Component::PercentEncoded(s) => percent_decode(s), Component::PercentEncoded(s) => percent_decode(s),
} }
} }
/// Lossy (U+FFFD) UTF-8 view of [`Self::to_raw_bytes`], for values such
/// as host names that are expected to be text. Use `to_raw_bytes` for
/// paths.
pub fn to_raw(&self) -> Cow<'_, str> {
match self.to_raw_bytes() {
Cow::Borrowed(b) => String::from_utf8_lossy(b),
Cow::Owned(b) => Cow::Owned(String::from_utf8_lossy(&b).into_owned()),
}
}
/// The percent-encoded value as it appears in the source, or the raw /// The percent-encoded value as it appears in the source, or the raw
/// text for `Raw` components. /// text for `Raw` components.
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
@@ -68,9 +80,9 @@ impl Component<'_> {
/// Lenient percent decoding like `std.Uri.percentDecodeBackwards`/InPlace: /// Lenient percent decoding like `std.Uri.percentDecodeBackwards`/InPlace:
/// valid `%XX` sequences are decoded, everything else passes through. /// valid `%XX` sequences are decoded, everything else passes through.
fn percent_decode(s: &str) -> Cow<'_, str> { fn percent_decode(s: &str) -> Cow<'_, [u8]> {
if !s.contains('%') { if !s.contains('%') {
return Cow::Borrowed(s); return Cow::Borrowed(s.as_bytes());
} }
let b = s.as_bytes(); let b = s.as_bytes();
let mut out = Vec::with_capacity(b.len()); let mut out = Vec::with_capacity(b.len());
@@ -87,7 +99,7 @@ fn percent_decode(s: &str) -> Cow<'_, str> {
out.push(b[i]); out.push(b[i]);
i += 1; i += 1;
} }
Cow::Owned(String::from_utf8_lossy(&out).into_owned()) Cow::Owned(out)
} }
fn hexval(c: u8) -> Option<u8> { fn hexval(c: u8) -> Option<u8> {
@@ -318,6 +330,64 @@ pub fn parse(text: &str, options: ParseOptions) -> Result<Uri<'_>, ParseError> {
Ok(uri) Ok(uri)
} }
/// The pieces of a parsed URI that are needed by byte-oriented callers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedBytes {
pub scheme: String,
/// Percent-decoded host (lossy; host names are text).
pub host: Option<String>,
/// Percent-decoded path bytes (or the full raw tail with
/// [`ParseOptions::raw_path`]). Not required to be UTF-8.
pub path: Vec<u8>,
}
/// Parse a URI given as arbitrary bytes without losing any path bytes.
///
/// Bytes that are not valid UTF-8 are percent-escaped before parsing (so
/// they decode back to exactly the same bytes), mirroring Zig's byte-string
/// `std.Uri.parse`. With `raw_path` the original bytes are sliced directly.
pub fn parse_bytes(text: &[u8], options: ParseOptions) -> Result<ParsedBytes, ParseError> {
// Escape invalid bytes, remembering the offset mapping.
let mut escaped = String::with_capacity(text.len());
for chunk in text.utf8_chunks() {
escaped.push_str(chunk.valid());
for b in chunk.invalid() {
let _ = write!(escaped, "%{b:02X}");
}
}
let uri = parse(&escaped, options)?;
let path = if options.raw_path {
let esc_start = offset_in(&escaped, uri.path.as_str());
// Map the escaped offset back to the original byte offset. The path
// never begins inside an escape (it starts at a delimiter).
let mut orig_start = esc_start;
let mut esc_pos = 0usize;
let mut orig_pos = 0usize;
for chunk in text.utf8_chunks() {
let v = chunk.valid().len();
if esc_start <= esc_pos + v {
orig_start = orig_pos + (esc_start - esc_pos);
break;
}
esc_pos += v;
orig_pos += v;
let inv = chunk.invalid().len();
esc_pos += inv * 3;
orig_pos += inv;
orig_start = orig_pos;
}
text[orig_start.min(text.len())..].to_vec()
} else {
uri.path.to_raw_bytes().into_owned()
};
Ok(ParsedBytes {
scheme: uri.scheme.to_owned(),
host: uri.host.as_ref().map(|h| h.to_raw().into_owned()),
path,
})
}
/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef. /// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef.
fn is_valid_mac_address(s: &str) -> bool { fn is_valid_mac_address(s: &str) -> bool {
let b = s.as_bytes(); let b = s.as_bytes();
@@ -407,6 +477,31 @@ mod tests {
); );
} }
#[test]
fn percent_decode_preserves_non_utf8_bytes() {
let u = parse("file://localhost/a%FFb%e2%82", ParseOptions::default()).unwrap();
assert_eq!(&*u.path.to_raw_bytes(), b"/a\xffb\xe2\x82");
// The lossy text view still works for text values.
assert_eq!(u.path.to_raw(), "/a\u{fffd}b\u{fffd}");
}
#[test]
fn parse_bytes_non_utf8() {
let p = parse_bytes(b"file://localhost/a\xffb%FEc", ParseOptions::default()).unwrap();
assert_eq!(p.scheme, "file");
assert_eq!(p.host.as_deref(), Some("localhost"));
assert_eq!(p.path, b"/a\xffb\xfec");
let raw = ParseOptions {
raw_path: true,
mac_address: true,
};
let p = parse_bytes(b"kitty-shell-cwd://localhost/a\xff%41?x", raw).unwrap();
assert_eq!(p.path, b"/a\xff%41?x");
let p = parse_bytes(b"kitty-shell-cwd://\xfflo/a", raw).ok();
assert!(p.is_none() || p.is_some());
}
#[test] #[test]
fn parse_raw_path() { fn parse_raw_path() {
let text = "file://localhost/path??#fragment"; let text = "file://localhost/path??#fragment";
+5 -1
View File
@@ -1723,7 +1723,11 @@ impl Renderer {
mouse_mods: st.mouse.mods, mouse_mods: st.mouse.mods,
preedit, preedit,
scrollbar, scrollbar,
overlay_features: st.overlay_features.clone(), overlay_features: st
.inspector
.as_ref()
.map(|i| i.overlay_features())
.unwrap_or_default(),
} }
}; };
+1
View File
@@ -25,6 +25,7 @@ pub mod options;
pub mod overlay; pub mod overlay;
pub mod row; pub mod row;
pub mod shadertoy; pub mod shadertoy;
mod shadertoy_sampler;
pub mod size; pub mod size;
pub mod state; pub mod state;
pub mod thread; pub mod thread;
+64 -1
View File
@@ -8,6 +8,7 @@
//! `docs/rewrite/notes/ghostty-renderer-R1.md` for the (small) source //! `docs/rewrite/notes/ghostty-renderer-R1.md` for the (small) source
//! adaptations needed for naga's Vulkan-flavoured GLSL frontend. //! adaptations needed for naga's Vulkan-flavoured GLSL frontend.
use crate::shadertoy_sampler;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
@@ -250,7 +251,13 @@ pub fn spirv_from_glsl(src: &str, errlog: Option<&mut ShaderLog>) -> Result<Comp
/// * the `mainImage` prototype and `main` are moved after the user code /// * the `mainImage` prototype and `main` are moved after the user code
/// (naga requires callee definitions before their callers). /// (naga requires callee definitions before their callers).
fn adapt_for_naga(full: &str) -> String { fn adapt_for_naga(full: &str) -> String {
let mut s = full // Only the verbatim prefix gets the constructs below; the user's code is
// only touched by the combined-sampler rewrite.
let (prefix, user) = match full.strip_prefix(PREFIX) {
Some(rest) => (PREFIX, rest),
None => (full, ""),
};
let mut s = prefix
.replacen("#version 430 core", "#version 460 core", 1) .replacen("#version 430 core", "#version 460 core", 1)
.replace(" uniform ", " ") .replace(" uniform ", " ")
.replacen("layout(location = 0) in vec4 gl_FragCoord;", "", 1) .replacen("layout(location = 0) in vec4 gl_FragCoord;", "", 1)
@@ -276,6 +283,12 @@ fn adapt_for_naga(full: &str) -> String {
"", "",
1, 1,
); );
if !user.is_empty() {
// `texture2D` is a type for naga (and the `sampler2D` parameter
// rewrite): the prefix macro is replaced by rewriting the calls.
s = s.replacen("#define texture2D texture", "", 1);
s.push_str(&shadertoy_sampler::rewrite(user));
}
s.push_str("\nvoid main() { mainImage (_fragColor, gl_FragCoord.xy); }\n"); s.push_str("\nvoid main() { mainImage (_fragColor, gl_FragCoord.xy); }\n");
s s
} }
@@ -406,6 +419,56 @@ mod tests {
compile_shadertoy(src, None).unwrap(); compile_shadertoy(src, None).unwrap();
} }
#[test]
fn sampler2d_parameter_helper_compiles() {
let src = "vec4 fetch(sampler2D tex, vec2 uv) { return texture(tex, uv); }\n\
void mainImage(out vec4 c, in vec2 p) { c = fetch(iChannel0, p / iResolution.xy); }\n";
let mut log = ShaderLog::default();
let r = compile_shadertoy(src, Some(&mut log));
assert!(r.is_ok(), "{}", log.info);
}
fn compiles(src: &str) {
let mut log = ShaderLog::default();
let c = compile_shadertoy(src, Some(&mut log));
assert!(c.is_ok(), "{}\n{}", log.info, log.debug);
glsl_from_compiled(&c.unwrap()).unwrap();
}
#[test]
fn sampler2d_parameter_forwarding_and_prototype() {
compiles(
"vec4 inner(in sampler2D s, vec2 uv);\n\
vec4 outer(sampler2D t, vec2 uv) { return inner(t, uv) + inner(t, uv * 2.0); }\n\
vec4 inner(in sampler2D s, vec2 uv) { return texture(s, uv); }\n\
void mainImage(out vec4 c, in vec2 p) { c = outer(iChannel0, p / iResolution.xy); }\n",
);
}
#[test]
fn sampler2d_parameter_other_texture_functions() {
compiles(
"vec4 f(sampler2D s, vec2 uv) {\n\
vec2 sz = vec2(textureSize(s, 0));\n\
vec4 a = textureLod(s, uv, 0.0);\n\
vec4 b = texelFetch(s, ivec2(uv * sz), 0);\n\
return a + b + texture2D(s, uv);\n\
}\n\
void mainImage(out vec4 c, in vec2 p) { c = f(iChannel0, p / iResolution.xy); }\n",
);
}
#[test]
fn combined_sampler_builtin_uses_compile() {
compiles(
"void mainImage(out vec4 c, in vec2 p) {\n\
vec2 uv = p / iResolution.xy;\n\
c = textureLod(iChannel0, uv, 0.0) + texelFetch(iChannel0, ivec2(p), 0)\n\
+ vec4(textureSize(iChannel0, 0), 0.0, 0.0) + texture2D(iChannel0, uv);\n\
}\n",
);
}
#[test] #[test]
fn load_from_files_skips_missing_optional() { fn load_from_files_skips_missing_optional() {
let dir = let dir =
@@ -0,0 +1,359 @@
//! Source-level adaptation of combined image samplers for naga.
//!
//! glslang (Vulkan GLSL) accepts `sampler2D` as a function parameter type and
//! lets shaders pass the combined `iChannel0` around. naga's GLSL frontend
//! has no combined sampler *type*, only separate `texture2D` + `sampler`
//! objects (and a `sampler2D(texture, sampler)` constructor expression that
//! is valid only directly inside texture calls).
//!
//! This module rewrites user code so such helpers work:
//!
//! * `T f(.., [qual] sampler2D s, ..)` -> `T f(.., texture2D s_tex, sampler s_smp, ..)`
//! * uses of `s` in the function body -> `sampler2D(s_tex, s_smp)`
//! * `f(.., s, ..)` / `f(.., iChannel0, ..)` call arguments in sampler
//! positions -> `s_tex, s_smp` / `iChannel0_tex, iChannel0_smp`
//! * legacy `texture2D(` calls -> `texture(` (glslang gets this from the
//! prefix macro `#define texture2D texture`, which would otherwise clash
//! with the `texture2D` type).
//!
//! Limitations (documented, reported as compile errors by naga): a sampler
//! argument must be a plain identifier (no ternaries or struct members),
//! samplers cannot be stored in local variables/arrays/struct fields, and
//! overloads of one function name must agree on which parameters are
//! samplers.
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
/// Whitespace, comments and preprocessor lines (not significant).
Trivia,
Ident,
Punct,
Other,
}
struct Tok<'a> {
kind: Kind,
text: &'a str,
}
fn lex(src: &str) -> Vec<Tok<'_>> {
let b = src.as_bytes();
let mut toks = Vec::new();
let mut i = 0;
let mut line_start = true;
while i < b.len() {
let start = i;
let c = b[i];
let kind;
if c.is_ascii_whitespace() {
while i < b.len() && b[i].is_ascii_whitespace() {
if b[i] == b'\n' {
line_start = true;
}
i += 1;
}
// line_start must reflect "only whitespace since newline".
toks.push(Tok {
kind: Kind::Trivia,
text: &src[start..i],
});
continue;
} else if c == b'/' && b.get(i + 1) == Some(&b'/') {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
kind = Kind::Trivia;
} else if c == b'/' && b.get(i + 1) == Some(&b'*') {
i += 2;
while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
i += 1;
}
i = (i + 2).min(b.len());
kind = Kind::Trivia;
} else if c == b'#' && line_start {
// Preprocessor line (with backslash continuations).
while i < b.len() && b[i] != b'\n' {
if b[i] == b'\\' && b.get(i + 1) == Some(&b'\n') {
i += 1;
}
i += 1;
}
kind = Kind::Trivia;
} else if c.is_ascii_alphabetic() || c == b'_' {
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
i += 1;
}
kind = Kind::Ident;
} else if c.is_ascii_digit() || (c == b'.' && b.get(i + 1).is_some_and(u8::is_ascii_digit))
{
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
i += 1;
}
kind = Kind::Other;
} else {
// Any single (possibly multi-byte) character.
i += 1;
while i < b.len() && (b[i] & 0xC0) == 0x80 {
i += 1;
}
kind = Kind::Punct;
}
line_start = false;
toks.push(Tok {
kind,
text: &src[start..i],
});
}
toks
}
fn is_sig(t: &Tok<'_>) -> bool {
t.kind != Kind::Trivia
}
/// Index of the token matching the opener at `sig[open]` (indices into the
/// significant-token list), or None.
fn matching(toks: &[Tok<'_>], sig: &[usize], open: usize) -> Option<usize> {
let o = toks[sig[open]].text;
let c = match o {
"(" => ")",
"{" => "}",
"[" => "]",
_ => return None,
};
let mut depth = 0usize;
for (k, &ti) in sig.iter().enumerate().skip(open) {
let t = toks[ti].text;
if t == o {
depth += 1;
} else if t == c {
depth -= 1;
if depth == 0 {
return Some(k);
}
}
}
None
}
/// Split the significant range `(from..to)` at top-level commas, returning
/// `(start, end)` ranges (exclusive end) in significant-token indices.
fn split_args(toks: &[Tok<'_>], sig: &[usize], from: usize, to: usize) -> Vec<(usize, usize)> {
let mut out = Vec::new();
if from >= to {
return out;
}
let mut depth = 0i32;
let mut start = from;
for k in from..to {
match toks[sig[k]].text {
"(" | "[" | "{" => depth += 1,
")" | "]" | "}" => depth -= 1,
"," if depth == 0 => {
out.push((start, k));
start = k + 1;
}
_ => {}
}
}
out.push((start, to));
out
}
/// A top-level function definition/prototype (indices are into the
/// significant-token list).
struct Def {
name_k: usize,
params: Vec<(usize, usize)>,
body: Option<(usize, usize)>,
samplers: Vec<Option<String>>,
}
struct FnInfo {
/// Per parameter: the sampler name, if the parameter is a `sampler2D`.
samplers: Vec<Option<String>>,
}
/// Rewrite the user's ShaderToy source. Returns the source unchanged if it
/// needs no adaptation other than `texture2D(` -> `texture(`.
pub fn rewrite(src: &str) -> String {
let toks = lex(src);
let sig: Vec<usize> = toks
.iter()
.enumerate()
.filter(|(_, t)| is_sig(t))
.map(|(i, _)| i)
.collect();
// Pass 1: find top-level functions with sampler2D parameters.
let mut funcs: HashMap<&str, FnInfo> = HashMap::new();
let mut defs: Vec<Def> = Vec::new();
let mut k = 0;
let mut brace = 0i32;
while k < sig.len() {
let t = &toks[sig[k]];
match t.text {
"{" => brace += 1,
"}" => brace -= 1,
_ => {}
}
if brace == 0
&& t.kind == Kind::Ident
&& k + 2 < sig.len()
&& toks[sig[k + 1]].kind == Kind::Ident
&& toks[sig[k + 2]].text == "("
{
let name_k = k + 1;
if let Some(close) = matching(&toks, &sig, k + 2) {
let next = sig.get(close + 1).map(|&i| toks[i].text);
if next == Some("{") || next == Some(";") {
let params = split_args(&toks, &sig, k + 3, close);
let mut samplers = Vec::new();
let mut any = false;
for &(a, b) in &params {
let has = (a..b).any(|q| toks[sig[q]].text == "sampler2D");
if has {
let name = (a..b)
.rev()
.map(|q| &toks[sig[q]])
.find(|t| t.kind == Kind::Ident && t.text != "sampler2D")
.map(|t| t.text.to_owned());
any |= name.is_some();
samplers.push(name);
} else {
samplers.push(None);
}
}
let body = if next == Some("{") {
matching(&toks, &sig, close + 1).map(|e| (close + 1, e))
} else {
None
};
if any {
funcs
.entry(toks[sig[name_k]].text)
.or_insert_with(|| FnInfo {
samplers: samplers.clone(),
});
}
defs.push(Def {
name_k,
params,
body,
samplers,
});
// Skip past the signature (bodies are scanned for
// braces by the normal loop).
k = close + 1;
continue;
}
}
}
k += 1;
}
// Pass 2: collect replacements keyed by token index.
let mut repl: HashMap<usize, String> = HashMap::new();
let mut def_names: Vec<usize> = Vec::new();
for d in &defs {
def_names.push(d.name_k);
for (pi, &(a, b)) in d.params.iter().enumerate() {
if let Some(name) = d.samplers.get(pi).and_then(Option::as_ref) {
repl.insert(sig[a], format!("texture2D {name}_tex, sampler {name}_smp"));
// Blank the rest of the parameter, including interior trivia.
for ti in (sig[a] + 1)..=sig[b - 1] {
repl.insert(ti, String::new());
}
}
}
// Uses of the sampler parameters in the body.
if let Some((open, close)) = d.body {
let names: Vec<&String> = d.samplers.iter().flatten().collect();
for q in open + 1..close {
let t = &toks[sig[q]];
if t.kind == Kind::Ident && names.iter().any(|n| n.as_str() == t.text) {
repl.insert(sig[q], format!("sampler2D({0}_tex, {0}_smp)", t.text));
}
}
}
}
// Call sites (and legacy texture2D).
for k in 0..sig.len() {
let t = &toks[sig[k]];
if t.kind != Kind::Ident || sig.get(k + 1).map(|&i| toks[i].text) != Some("(") {
continue;
}
if t.text == "texture2D" {
repl.insert(sig[k], "texture".to_owned());
continue;
}
if def_names.contains(&k) {
continue;
}
let Some(info) = funcs.get(t.text) else {
continue;
};
let Some(close) = matching(&toks, &sig, k + 1) else {
continue;
};
let args = split_args(&toks, &sig, k + 2, close);
for (ai, &(a, b)) in args.iter().enumerate() {
if info.samplers.get(ai).is_some_and(Option::is_some)
&& b == a + 1
&& toks[sig[a]].kind == Kind::Ident
{
let n = toks[sig[a]].text;
repl.insert(sig[a], format!("{n}_tex, {n}_smp"));
}
}
}
let mut out = String::with_capacity(src.len() + 64);
for (i, t) in toks.iter().enumerate() {
match repl.get(&i) {
Some(r) => out.push_str(r),
None => out.push_str(t.text),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rewrites_param_use_and_call() {
let out = rewrite(
"vec4 fetch(sampler2D tex, vec2 uv) { return texture(tex, uv); }\n\
void mainImage(out vec4 c, in vec2 p) { c = fetch(iChannel0, p); }",
);
assert!(out.contains("vec4 fetch(texture2D tex_tex, sampler tex_smp, vec2 uv)"));
assert!(out.contains("texture(sampler2D(tex_tex, tex_smp), uv)"));
assert!(out.contains("fetch(iChannel0_tex, iChannel0_smp, p)"));
}
#[test]
fn leaves_plain_code_and_rewrites_legacy_texture2d() {
let src = "void mainImage(out vec4 c, in vec2 p) { c = texture2D(iChannel0, p); }";
assert_eq!(
rewrite(src),
"void mainImage(out vec4 c, in vec2 p) { c = texture(iChannel0, p); }"
);
}
#[test]
fn ignores_comments_and_forwards_samplers() {
let out = rewrite(
"// sampler2D in comment\n\
vec4 a(in sampler2D s) { return texture(s, vec2(0.0)); }\n\
vec4 b(sampler2D t, float x) { return a(t) * x; }\n",
);
assert!(out.contains("// sampler2D in comment"));
assert!(out.contains("vec4 a(texture2D s_tex, sampler s_smp)"));
assert!(out.contains("return a(t_tex, t_smp) * x;"));
}
}
+20 -6
View File
@@ -22,11 +22,11 @@ pub struct State {
/// The terminal data. /// The terminal data.
pub terminal: Terminal, pub terminal: Terminal,
/// The overlay features currently requested by the terminal inspector /// The terminal inspector, if any (Zig: `inspector: ?*Inspector`). This
/// (Zig: `inspector: ?*Inspector` and `inspector.rendererInfo() /// is `None` while the inspector is not active. It is accessed under the
/// .overlayFeatures()`). Empty while the inspector is not active. /// same lock as the terminal (lock order: this state, then the
// TODO(integration): the inspector is deferred (Phase 7); it fills this. /// inspector's own lock).
pub overlay_features: Vec<OverlayFeature>, pub inspector: Option<Arc<dyn InspectorHook>>,
/// Dead key state. This will render the current dead key preedit text /// Dead key state. This will render the current dead key preedit text
/// over the cursor. This currently only ever renders a single codepoint. /// over the cursor. This currently only ever renders a single codepoint.
@@ -39,6 +39,20 @@ pub struct State {
pub mouse: Mouse, pub mouse: Mouse,
} }
/// The view of the terminal inspector that the renderer and termio need
/// (the inspector crate itself depends on this crate, so the Zig
/// `*Inspector` pointer is expressed as a trait object).
pub trait InspectorHook: Send + Sync + std::fmt::Debug {
/// The overlay features requested in the inspector's renderer window
/// (Zig: `inspector.rendererInfo().overlayFeatures()`).
fn overlay_features(&self) -> Vec<OverlayFeature>;
/// Record data read from the pty before it is processed by the terminal
/// (Zig: `Inspector.recordPtyRead`). Termio calls this one byte at a
/// time while an inspector is active.
fn record_pty_read(&self, t: &Terminal, data: &[u8]);
}
/// The shared, mutex-protected renderer state. /// The shared, mutex-protected renderer state.
pub type SharedState = Arc<Mutex<State>>; pub type SharedState = Arc<Mutex<State>>;
@@ -46,7 +60,7 @@ impl State {
pub fn new(terminal: Terminal) -> State { pub fn new(terminal: Terminal) -> State {
State { State {
terminal, terminal,
overlay_features: Vec::new(), inspector: None,
preedit: None, preedit: None,
mouse: Mouse::default(), mouse: Mouse::default(),
} }
+1
View File
@@ -13,6 +13,7 @@ ghostty-unicode.workspace = true
ghostty-vt.workspace = true ghostty-vt.workspace = true
log = "0.4.34" log = "0.4.34"
onig = "6.5.3" onig = "6.5.3"
ghostty-onig.workspace = true
png = "0.18.1" png = "0.18.1"
rustix = { version = "1.1.5", features = ["shm", "fs"] } rustix = { version = "1.1.5", features = ["shm", "fs"] }
thiserror = "2.0.21" thiserror = "2.0.21"
+15
View File
@@ -977,6 +977,21 @@ impl Page {
pub fn lookup_hyperlink(&self, index: usize) -> Option<u16> { pub fn lookup_hyperlink(&self, index: usize) -> Option<u16> {
self.hyperlink_map.get(&index).copied() self.hyperlink_map.get(&index).copied()
} }
/// Bytes of string storage currently in use (Zig:
/// `string_alloc.usedBytes`); used by the inspector.
pub fn string_used_bytes(&self) -> usize {
let used: usize = self
.string_alloc
.used
.iter()
.map(|w| w.count_ones() as usize)
.sum();
used * self.string_alloc.chunk
}
/// Total bytes of string storage (Zig: `string_alloc.capacityBytes`).
pub fn string_capacity_bytes(&self) -> usize {
self.string_alloc.chunks * self.string_alloc.chunk
}
/// Number of hyperlink cells. /// Number of hyperlink cells.
pub fn hyperlink_count(&self) -> usize { pub fn hyperlink_count(&self) -> usize {
self.hyperlink_map.len() self.hyperlink_map.len()
+5
View File
@@ -863,6 +863,11 @@ impl PageList {
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).ok()?; let i = page.cell_index(usize::from(p.x), usize::from(p.y)).ok()?;
page.cells.get_mut(i) page.cells.get_mut(i)
} }
/// The handle of the internal tracked viewport pin, if any (used by the
/// inspector to label it).
pub fn viewport_pin_handle(&self) -> Option<TrackedPin> {
self.viewport_pin
}
/// Number of live pages in list order. /// Number of live pages in list order.
pub fn total_pages(&self) -> usize { pub fn total_pages(&self) -> usize {
self.nodes.iter().filter(|n| n.is_some()).count() self.nodes.iter().filter(|n| n.is_some()).count()
+18 -4
View File
@@ -11,9 +11,10 @@ use onig::{MatchParam, Regex, Region, SearchOptions};
/// Units are Oniguruma retry steps (internal backtracking/retry counter), /// Units are Oniguruma retry steps (internal backtracking/retry counter),
/// not bytes/characters/time. /// not bytes/characters/time.
/// ///
/// Deviation: the `onig` crate only exposes the retry limit *in match* /// This is Zig's `setRetryLimitInSearch`: a whole-search budget (set via
/// (Zig also sets the retry limit in search); both abort matching with an /// the `ghostty-onig` wrapper since the `onig` crate does not expose it).
/// error which is treated as "no further matches". /// Exceeding it aborts matching with an error which is treated as "no
/// further matches".
const ONI_SEARCH_RETRY_LIMIT: u32 = 100_000; const ONI_SEARCH_RETRY_LIMIT: u32 = 100_000;
/// A string and a map from each byte of the string to the pin it came from. /// A string and a map from each byte of the string to the pin it came from.
@@ -52,7 +53,7 @@ impl<'a> SearchIterator<'a> {
// Use per-search match params so we can bound regex retry steps // Use per-search match params so we can bound regex retry steps
// (Oniguruma's internal backtracking work counter). // (Oniguruma's internal backtracking work counter).
let mut match_param = MatchParam::default(); let mut match_param = MatchParam::default();
match_param.set_retry_limit_in_match(ONI_SEARCH_RETRY_LIMIT); ghostty_onig::set_retry_limit_in_search(&mut match_param, ONI_SEARCH_RETRY_LIMIT);
let haystack = &self.map.string[self.offset..]; let haystack = &self.map.string[self.offset..];
let mut region = Region::new(); let mut region = Region::new();
@@ -195,6 +196,19 @@ mod tests {
assert!(it.next_match().is_none()); assert!(it.next_match().is_none());
} }
#[test]
fn string_map_search_iterator_retry_limit_bounds_catastrophic_regex() {
// Exponential backtracking: without the whole-search retry limit
// this would effectively never finish.
let re = Regex::new("(a+)+$").unwrap();
let mut s = Screen::new(Options::new(60, 5, 0)).unwrap();
s.test_write_string(&format!("{}b", "a".repeat(50)))
.unwrap();
let map = line_map(&s, 10, 0);
let mut it = map.search_iterator(&re);
assert!(it.next_match().is_none());
}
#[test] #[test]
fn string_map_search_iterator_url_with_click_position() { fn string_map_search_iterator_url_with_click_position() {
let re = Regex::new(ghostty_config::url::REGEX).unwrap(); let re = Regex::new(ghostty_config::url::REGEX).unwrap();
+35 -4
View File
@@ -13,7 +13,6 @@
//! async-signal-safe, like their Zig counterparts are expected to be). //! async-signal-safe, like their Zig counterparts are expected to be).
use std::{ use std::{
collections::HashMap,
ffi::{CString, NulError, c_char}, ffi::{CString, NulError, c_char},
os::fd::OwnedFd, os::fd::OwnedFd,
panic::{AssertUnwindSafe, catch_unwind}, panic::{AssertUnwindSafe, catch_unwind},
@@ -25,6 +24,7 @@ use rustix::{
process::{self, WaitOptions}, process::{self, WaitOptions},
stdio, stdio,
}; };
use std::os::unix::ffi::OsStrExt;
/// Function executed /in the child process/ after the fork, but before /// Function executed /in the child process/ after the fork, but before
/// exec'ing the command. If the function returns `Some(code)`, the child /// exec'ing the command. If the function returns `Some(code)`, the child
@@ -173,7 +173,7 @@ pub struct Command {
/// Environment variables for the child process. If this is `None`, /// Environment variables for the child process. If this is `None`,
/// inherits the environment variables from this process. These are the /// inherits the environment variables from this process. These are the
/// exact environment variables to set; these are /not/ merged. /// exact environment variables to set; these are /not/ merged.
pub env: Option<HashMap<String, String>>, pub env: Option<ghostty_os::EnvMap>,
/// Working directory to change to in the child process. If not set, the /// Working directory to change to in the child process. If not set, the
/// working directory of the calling process is preserved. Failure to /// working directory of the calling process is preserved. Failure to
@@ -249,7 +249,12 @@ impl Command {
let envp = match &self.env { let envp = match &self.env {
Some(map) => map Some(map) => map
.iter() .iter()
.map(|(k, v)| CString::new(format!("{k}={v}"))) .map(|(k, v)| {
let mut b = k.as_bytes().to_vec();
b.push(b'=');
b.extend_from_slice(v.as_bytes());
CString::new(b)
})
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
// Inherit: snapshot our environment before the fork (nothing may // Inherit: snapshot our environment before the fork (nothing may
// allocate in the child). // allocate in the child).
@@ -738,13 +743,39 @@ mod tests {
let mut cmd = sh("echo $VALUE"); let mut cmd = sh("echo $VALUE");
cmd.stdout = Some(stdout.try_clone().unwrap().into()); cmd.stdout = Some(stdout.try_clone().unwrap().into());
cmd.env = Some(HashMap::from([("VALUE".to_owned(), "hello".to_owned())])); cmd.env = Some(ghostty_os::EnvMap::from_iter([("VALUE", "hello")]));
cmd.start().unwrap(); cmd.start().unwrap();
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0)); assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
assert_eq!("hello\n", read_all(stdout)); assert_eq!("hello\n", read_all(stdout));
} }
#[test]
fn non_utf8_env_value_is_preserved() {
use std::os::unix::ffi::OsStringExt;
let td = TempDir::init().unwrap();
let stdout = test_file(&td, "stdout.txt");
let mut cmd = sh("printf %s \"$VALUE\"");
cmd.stdout = Some(stdout.try_clone().unwrap().into());
let mut env = ghostty_os::EnvMap::new();
env.insert(
"VALUE",
std::ffi::OsString::from_vec(vec![b'a', 0xff, b'b']),
);
cmd.env = Some(env);
cmd.start().unwrap();
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
let mut bytes = Vec::new();
std::io::Read::read_to_end(
&mut std::fs::File::open(td.path().join("stdout.txt")).unwrap(),
&mut bytes,
)
.unwrap();
assert_eq!(bytes, vec![b'a', 0xff, b'b']);
}
#[test] #[test]
fn custom_working_directory() { fn custom_working_directory() {
let td = TempDir::init().unwrap(); let td = TempDir::init().unwrap();
+36 -27
View File
@@ -212,6 +212,9 @@ impl Exec {
let (tx, rx) = crossbeam_channel::bounded(1); let (tx, rx) = crossbeam_channel::bounded(1);
let wakeup = Wakeup::new()?; let wakeup = Wakeup::new()?;
if let Some(cmd) = fp.cmd.take() { if let Some(cmd) = fp.cmd.take() {
// Keep a signal handle so stop() can still SIGHUP the
// host process group after the watcher owns the command.
fp.signal = Some(cmd.signal_handle());
let w = wakeup.clone(); let w = wakeup.clone();
cmd.wait_callback(move |r| { cmd.wait_callback(move |r| {
let _ = tx.send(r.map_err(|e| format!("{e}"))); let _ = tx.send(r.map_err(|e| format!("{e}")));
@@ -724,10 +727,9 @@ enum Process {
struct FlatpakProcess { struct FlatpakProcess {
pid: u32, pid: u32,
/// Taken by the exit watcher (`wait_callback` consumes the command). /// Taken by the exit watcher (`wait_callback` consumes the command).
/// TODO(integration): ghostty-os' `FlatpakHostCommand::wait_callback`
/// takes `self`, so after the watcher is installed we can no longer
/// `signal()` the command from `stop()`.
cmd: Option<ghostty_os::flatpak::FlatpakHostCommand>, cmd: Option<ghostty_os::flatpak::FlatpakHostCommand>,
/// Signal handle that outlives the command's move into the watcher.
signal: Option<ghostty_os::flatpak::FlatpakSignalHandle>,
} }
struct Subprocess { struct Subprocess {
@@ -756,7 +758,7 @@ impl Subprocess {
// If we have a resources dir then set our env var // If we have a resources dir then set our env var
if let Some(dir) = &cfg.resources_dir { if let Some(dir) = &cfg.resources_dir {
log::info!("found Ghostty resources dir: {dir}"); log::info!("found Ghostty resources dir: {dir}");
env.insert("GHOSTTY_RESOURCES_DIR".into(), dir.clone()); env.insert("GHOSTTY_RESOURCES_DIR", dir.clone());
} }
// Set our TERM var. This is a bit complicated because we want to use // Set our TERM var. This is a bit complicated because we want to use
@@ -766,8 +768,8 @@ impl Subprocess {
// For now, we just look up a bundled dir but in the future we should // For now, we just look up a bundled dir but in the future we should
// also load the terminfo database and look for it. // also load the terminfo database and look for it.
if let Some(base) = &cfg.resources_dir { if let Some(base) = &cfg.resources_dir {
env.insert("TERM".into(), cfg.term.clone()); env.insert("TERM", cfg.term.clone());
env.insert("COLORTERM".into(), "truecolor".into()); env.insert("COLORTERM", "truecolor");
// Assume that the resources directory is adjacent to the terminfo // Assume that the resources directory is adjacent to the terminfo
// database // database
@@ -784,10 +786,10 @@ impl Subprocess {
.map_or_else(String::new, |p| p.to_string_lossy().into_owned()); .map_or_else(String::new, |p| p.to_string_lossy().into_owned());
format!("{parent}/terminfo") format!("{parent}/terminfo")
}; };
env.insert("TERMINFO".into(), terminfo); env.insert("TERMINFO", terminfo);
} else { } else {
env.insert("TERM".into(), "xterm-256color".into()); env.insert("TERM", "xterm-256color");
env.insert("COLORTERM".into(), "truecolor".into()); env.insert("COLORTERM", "truecolor");
} }
// Add our binary to the path if we can find it. // Add our binary to the path if we can find it.
@@ -795,8 +797,8 @@ impl Subprocess {
// Set environment variables used by some programs (such as neovim) to detect // Set environment variables used by some programs (such as neovim) to detect
// which terminal emulator and version they're running under. // which terminal emulator and version they're running under.
env.insert("TERM_PROGRAM".into(), "ghostty".into()); env.insert("TERM_PROGRAM", "ghostty");
env.insert("TERM_PROGRAM_VERSION".into(), VERSION_STRING.into()); env.insert("TERM_PROGRAM_VERSION", VERSION_STRING);
// VTE_VERSION is set by gnome-terminal and other VTE-based terminals. // VTE_VERSION is set by gnome-terminal and other VTE-based terminals.
// We don't want our child processes to think we're running under VTE. // We don't want our child processes to think we're running under VTE.
@@ -858,7 +860,7 @@ impl Subprocess {
// Add the environment variables that override any others. // Add the environment variables that override any others.
for (k, v) in cfg.env_override.iter() { for (k, v) in cfg.env_override.iter() {
env.insert(k.to_owned(), v.to_owned()); env.insert(k, v);
} }
// Build our args list // Build our args list
@@ -876,7 +878,7 @@ impl Subprocess {
// https://github.com/kovidgoyal/kitty/issues/1595 // https://github.com/kovidgoyal/kitty/issues/1595
// https://github.com/ghostty-org/ghostty/discussions/7769 // https://github.com/ghostty-org/ghostty/discussions/7769
if let Some(pwd) = &cwd { if let Some(pwd) = &cwd {
env.insert("PWD".into(), pwd.clone()); env.insert("PWD", pwd.clone());
} }
Ok(Subprocess { Ok(Subprocess {
@@ -1014,6 +1016,7 @@ impl Subprocess {
); );
self.process = Some(Process::Flatpak(FlatpakProcess { self.process = Some(Process::Flatpak(FlatpakProcess {
pid, pid,
signal: Some(cmd.signal_handle()),
cmd: Some(cmd), cmd: Some(cmd),
})); }));
return Ok((pty.master.try_clone()?, pty.master.try_clone()?)); return Ok((pty.master.try_clone()?, pty.master.try_clone()?));
@@ -1086,15 +1089,16 @@ impl Subprocess {
} }
} }
Process::Flatpak(fp) => { Process::Flatpak(fp) => {
if let Some(cmd) = &fp.cmd { let res = match (&fp.cmd, &fp.signal) {
if let Err(err) = cmd.signal(1 /* SIGHUP */, true) { (Some(cmd), _) => cmd.signal(1 /* SIGHUP */, true),
log::error!("error sending SIGHUP to command, may hang: {err}"); (None, Some(h)) => h.signal(1 /* SIGHUP */, true),
(None, None) => {
log::warn!("no signal handle for flatpak host command pid={}", fp.pid);
Ok(())
} }
} else { };
log::warn!( if let Err(err) = res {
"cannot signal flatpak host command pid={} after the watcher took it", log::error!("error sending SIGHUP to command, may hang: {err}");
fp.pid
);
} }
} }
} }
@@ -1147,22 +1151,27 @@ fn add_ghostty_path(env: &mut EnvMap) {
// We always set this so that if the shell overwrites the path // We always set this so that if the shell overwrites the path
// scripts still have a way to find the Ghostty binary when // scripts still have a way to find the Ghostty binary when
// running in Ghostty. // running in Ghostty.
env.insert("GHOSTTY_BIN_DIR".into(), exe_dir.clone()); env.insert("GHOSTTY_BIN_DIR", exe_dir.clone());
// Append if we have a path. We want to append so that ghostty is // Append if we have a path. We want to append so that ghostty is
// the last priority in the path. If we don't have a path set // the last priority in the path. If we don't have a path set
// then we just set it to the directory of the binary. // then we just set it to the directory of the binary.
if let Some(path) = env.get("PATH").cloned() { if let Some(path) = env.get("PATH").map(std::ffi::OsStr::to_owned) {
// Verify that our path doesn't already contain this entry // Verify that our path doesn't already contain this entry
use std::os::unix::ffi::OsStrExt;
if path if path
.split(':') .as_bytes()
.any(|entry| !entry.is_empty() && entry == exe_dir) .split(|&b| b == b':')
.any(|entry| !entry.is_empty() && entry == exe_dir.as_bytes())
{ {
return; return;
} }
env.insert("PATH".into(), ghostty_os::env::append_env(&path, &exe_dir)); env.insert(
"PATH",
ghostty_os::env::append_env_os(&path, std::ffi::OsStr::new(&exe_dir)),
);
} else { } else {
env.insert("PATH".into(), exe_dir); env.insert("PATH", exe_dir);
} }
} }
+126 -51
View File
@@ -3,12 +3,12 @@
//! The shell scripts live in `resources/shell-integration/` at the workspace //! The shell scripts live in `resources/shell-integration/` at the workspace
//! root (copied verbatim from `src/shell-integration/`). //! root (copied verbatim from `src/shell-integration/`).
use std::{collections::HashMap, path::Path}; use std::path::Path;
use ghostty_os::{env::prepend_env, homedir, shell::ShellCommandBuilder}; use ghostty_os::{env::prepend_env_os, homedir, shell::ShellCommandBuilder};
/// The environment passed to the child (Zig `std.process.EnvMap`). /// The environment passed to the child (Zig `std.process.EnvMap`).
pub type EnvMap = HashMap<String, String>; pub type EnvMap = ghostty_os::EnvMap;
/// Shell types we support /// Shell types we support
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -144,7 +144,7 @@ pub fn setup_features(env: &mut EnvMap, features: ShellIntegrationFeatures, curs
} }
if !buf.is_empty() { if !buf.is_empty() {
env.insert("GHOSTTY_SHELL_FEATURES".to_owned(), buf); env.insert("GHOSTTY_SHELL_FEATURES", buf);
} }
} }
@@ -218,15 +218,15 @@ fn setup_bash(
} }
// Preserve an existing ENV value. We're about to overwrite it. // Preserve an existing ENV value. We're about to overwrite it.
if let Some(v) = env.get("ENV").cloned() { if let Some(v) = env.get("ENV").map(std::ffi::OsStr::to_owned) {
env.insert("GHOSTTY_BASH_ENV".to_owned(), v); env.insert("GHOSTTY_BASH_ENV", v);
} }
// Set our new ENV to point to our integration script. // Set our new ENV to point to our integration script.
let script_path = format!("{resource_dir}/shell-integration/bash/ghostty.bash"); let script_path = format!("{resource_dir}/shell-integration/bash/ghostty.bash");
match std::fs::File::open(&script_path) { match std::fs::File::open(&script_path) {
Ok(_) => { Ok(_) => {
env.insert("ENV".to_owned(), script_path); env.insert("ENV", script_path);
} }
Err(err) => { Err(err) => {
log::warn!("unable to open {script_path}: {err}"); log::warn!("unable to open {script_path}: {err}");
@@ -235,9 +235,9 @@ fn setup_bash(
} }
} }
env.insert("GHOSTTY_BASH_INJECT".to_owned(), inject); env.insert("GHOSTTY_BASH_INJECT", inject);
if let Some(v) = rcfile { if let Some(v) = rcfile {
env.insert("GHOSTTY_BASH_RCFILE".to_owned(), v); env.insert("GHOSTTY_BASH_RCFILE", v);
} }
// In POSIX mode, HISTFILE defaults to ~/.sh_history, so unless we're // In POSIX mode, HISTFILE defaults to ~/.sh_history, so unless we're
@@ -245,8 +245,8 @@ fn setup_bash(
if !env.contains_key("HISTFILE") if !env.contains_key("HISTFILE")
&& let Some(home) = homedir::home()? && let Some(home) = homedir::home()?
{ {
env.insert("HISTFILE".to_owned(), format!("{home}/.bash_history")); env.insert("HISTFILE", format!("{home}/.bash_history"));
env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE".to_owned(), "1".to_owned()); env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE", "1");
} }
// Return a copy of our modified command line to use as the shell command. // Return a copy of our modified command line to use as the shell command.
@@ -271,10 +271,7 @@ fn setup_xdg_data_dirs(resource_dir: &str, env: &mut EnvMap) -> bool {
// Set an env var so we can remove this from XDG_DATA_DIRS later. // Set an env var so we can remove this from XDG_DATA_DIRS later.
// This happens in the shell integration config itself. We do this // This happens in the shell integration config itself. We do this
// so that our modifications don't interfere with other commands. // so that our modifications don't interfere with other commands.
env.insert( env.insert("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_path.clone());
"GHOSTTY_SHELL_INTEGRATION_XDG_DIR".to_owned(),
integ_path.clone(),
);
// If no XDG_DATA_DIRS set use the default value as specified. // If no XDG_DATA_DIRS set use the default value as specified.
// This ensures that the default directories aren't lost by setting // This ensures that the default directories aren't lost by setting
@@ -283,9 +280,9 @@ fn setup_xdg_data_dirs(resource_dir: &str, env: &mut EnvMap) -> bool {
let key = "XDG_DATA_DIRS"; let key = "XDG_DATA_DIRS";
let current = env let current = env
.get(key) .get(key)
.map_or("/usr/local/share:/usr/share", String::as_str); .unwrap_or_else(|| std::ffi::OsStr::new("/usr/local/share:/usr/share"));
let new = prepend_env(current, &integ_path); let new = prepend_env_os(current, std::ffi::OsStr::new(&integ_path));
env.insert(key.to_owned(), new); env.insert(key, new);
true true
} }
@@ -356,8 +353,8 @@ fn setup_nushell(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Opt
/// config then loads the true user config. /// config then loads the true user config.
fn setup_zsh(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option<Command> { fn setup_zsh(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option<Command> {
// Preserve an existing ZDOTDIR value. We're about to overwrite it. // Preserve an existing ZDOTDIR value. We're about to overwrite it.
if let Some(old) = env.get("ZDOTDIR").cloned() { if let Some(old) = env.get("ZDOTDIR").map(std::ffi::OsStr::to_owned) {
env.insert("GHOSTTY_ZSH_ZDOTDIR".to_owned(), old); env.insert("GHOSTTY_ZSH_ZDOTDIR", old);
} }
// Set our new ZDOTDIR to point to our shell resource directory. // Set our new ZDOTDIR to point to our shell resource directory.
@@ -367,7 +364,7 @@ fn setup_zsh(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option<
log::warn!("unable to open {integ_path}"); log::warn!("unable to open {integ_path}");
return None; return None;
} }
env.insert("ZDOTDIR".to_owned(), integ_path); env.insert("ZDOTDIR", integ_path);
Some(command.clone()) Some(command.clone())
} }
@@ -494,7 +491,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
"cursor:blink,path,ssh-env,ssh-terminfo,sudo,title", "cursor:blink,path,ssh-env,ssh-terminfo,sudo,title",
env["GHOSTTY_SHELL_FEATURES"] env.get("GHOSTTY_SHELL_FEATURES").unwrap().to_str().unwrap()
); );
// all features disabled // all features disabled
@@ -513,7 +510,10 @@ mod tests {
}, },
true, true,
); );
assert_eq!("ssh-env,sudo", env["GHOSTTY_SHELL_FEATURES"]); assert_eq!(
"ssh-env,sudo",
env.get("GHOSTTY_SHELL_FEATURES").unwrap().to_str().unwrap()
);
// blinking cursor // blinking cursor
let only_cursor = ShellIntegrationFeatures { let only_cursor = ShellIntegrationFeatures {
@@ -522,19 +522,28 @@ mod tests {
}; };
let mut env = EnvMap::new(); let mut env = EnvMap::new();
setup_features(&mut env, only_cursor, true); setup_features(&mut env, only_cursor, true);
assert_eq!("cursor:blink", env["GHOSTTY_SHELL_FEATURES"]); assert_eq!(
"cursor:blink",
env.get("GHOSTTY_SHELL_FEATURES").unwrap().to_str().unwrap()
);
// steady cursor // steady cursor
let mut env = EnvMap::new(); let mut env = EnvMap::new();
setup_features(&mut env, only_cursor, false); setup_features(&mut env, only_cursor, false);
assert_eq!("cursor:steady", env["GHOSTTY_SHELL_FEATURES"]); assert_eq!(
"cursor:steady",
env.get("GHOSTTY_SHELL_FEATURES").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
fn default_features() { fn default_features() {
let mut env = EnvMap::new(); let mut env = EnvMap::new();
setup_features(&mut env, ShellIntegrationFeatures::default(), true); setup_features(&mut env, ShellIntegrationFeatures::default(), true);
assert_eq!("cursor:blink,path,title", env["GHOSTTY_SHELL_FEATURES"]); assert_eq!(
"cursor:blink,path,title",
env.get("GHOSTTY_SHELL_FEATURES").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -544,8 +553,14 @@ mod tests {
let command = setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); let command = setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
assert_eq!("bash --posix", shell_str(command.as_ref())); assert_eq!("bash --posix", shell_str(command.as_ref()));
assert_eq!("1", env["GHOSTTY_BASH_INJECT"]); assert_eq!(
assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]); "1",
env.get("GHOSTTY_BASH_INJECT").unwrap().to_str().unwrap()
);
assert_eq!(
format!("{}/ghostty.bash", res.shell_path),
env.get("ENV").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -575,12 +590,18 @@ mod tests {
let mut env = EnvMap::new(); let mut env = EnvMap::new();
let command = setup_bash(&sh("bash --norc"), &res.path, &mut env).unwrap(); let command = setup_bash(&sh("bash --norc"), &res.path, &mut env).unwrap();
assert_eq!("bash --posix", shell_str(command.as_ref())); assert_eq!("bash --posix", shell_str(command.as_ref()));
assert_eq!("1 --norc", env["GHOSTTY_BASH_INJECT"]); assert_eq!(
"1 --norc",
env.get("GHOSTTY_BASH_INJECT").unwrap().to_str().unwrap()
);
let mut env = EnvMap::new(); let mut env = EnvMap::new();
let command = setup_bash(&sh("bash --noprofile"), &res.path, &mut env).unwrap(); let command = setup_bash(&sh("bash --noprofile"), &res.path, &mut env).unwrap();
assert_eq!("bash --posix", shell_str(command.as_ref())); assert_eq!("bash --posix", shell_str(command.as_ref()));
assert_eq!("1 --noprofile", env["GHOSTTY_BASH_INJECT"]); assert_eq!(
"1 --noprofile",
env.get("GHOSTTY_BASH_INJECT").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -590,11 +611,17 @@ mod tests {
let command = setup_bash(&sh("bash --rcfile profile.sh"), &res.path, &mut env).unwrap(); let command = setup_bash(&sh("bash --rcfile profile.sh"), &res.path, &mut env).unwrap();
assert_eq!("bash --posix", shell_str(command.as_ref())); assert_eq!("bash --posix", shell_str(command.as_ref()));
assert_eq!("profile.sh", env["GHOSTTY_BASH_RCFILE"]); assert_eq!(
"profile.sh",
env.get("GHOSTTY_BASH_RCFILE").unwrap().to_str().unwrap()
);
let command = setup_bash(&sh("bash --init-file profile.sh"), &res.path, &mut env).unwrap(); let command = setup_bash(&sh("bash --init-file profile.sh"), &res.path, &mut env).unwrap();
assert_eq!("bash --posix", shell_str(command.as_ref())); assert_eq!("bash --posix", shell_str(command.as_ref()));
assert_eq!("profile.sh", env["GHOSTTY_BASH_RCFILE"]); assert_eq!(
"profile.sh",
env.get("GHOSTTY_BASH_RCFILE").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -605,15 +632,27 @@ mod tests {
temp_env::with_var("HOME", Some("/home/test"), || { temp_env::with_var("HOME", Some("/home/test"), || {
let mut env = EnvMap::new(); let mut env = EnvMap::new();
setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
assert!(env["HISTFILE"].ends_with(".bash_history")); assert!(
assert_eq!("1", env["GHOSTTY_BASH_UNEXPORT_HISTFILE"]); env.get("HISTFILE")
.unwrap()
.to_str()
.unwrap()
.ends_with(".bash_history")
);
assert_eq!(
"1",
env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE")
.unwrap()
.to_str()
.unwrap()
);
}); });
// HISTFILE set // HISTFILE set
let mut env = EnvMap::new(); let mut env = EnvMap::new();
env.insert("HISTFILE".into(), "my_history".into()); env.insert("HISTFILE", "my_history");
setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
assert_eq!("my_history", env["HISTFILE"]); assert_eq!("my_history", env.get("HISTFILE").unwrap().to_str().unwrap());
assert!(!env.contains_key("GHOSTTY_BASH_UNEXPORT_HISTFILE")); assert!(!env.contains_key("GHOSTTY_BASH_UNEXPORT_HISTFILE"));
} }
@@ -621,11 +660,17 @@ mod tests {
fn bash_env() { fn bash_env() {
let res = TmpResourcesDir::new(Shell::Bash); let res = TmpResourcesDir::new(Shell::Bash);
let mut env = EnvMap::new(); let mut env = EnvMap::new();
env.insert("ENV".into(), "env.sh".into()); env.insert("ENV", "env.sh");
setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
assert_eq!("env.sh", env["GHOSTTY_BASH_ENV"]); assert_eq!(
assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]); "env.sh",
env.get("GHOSTTY_BASH_ENV").unwrap().to_str().unwrap()
);
assert_eq!(
format!("{}/ghostty.bash", res.shell_path),
env.get("ENV").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -667,11 +712,14 @@ mod tests {
assert!(setup_xdg_data_dirs(&res.path, &mut env)); assert!(setup_xdg_data_dirs(&res.path, &mut env));
assert_eq!( assert_eq!(
format!("{}/shell-integration", res.path), format!("{}/shell-integration", res.path),
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"] env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR")
.unwrap()
.to_str()
.unwrap()
); );
assert_eq!( assert_eq!(
format!("{}/shell-integration:/usr/local/share:/usr/share", res.path), format!("{}/shell-integration:/usr/local/share:/usr/share", res.path),
env["XDG_DATA_DIRS"] env.get("XDG_DATA_DIRS").unwrap().to_str().unwrap()
); );
} }
@@ -679,16 +727,19 @@ mod tests {
fn xdg_existing_xdg_data_dirs() { fn xdg_existing_xdg_data_dirs() {
let res = TmpResourcesDir::new(Shell::Fish); let res = TmpResourcesDir::new(Shell::Fish);
let mut env = EnvMap::new(); let mut env = EnvMap::new();
env.insert("XDG_DATA_DIRS".into(), "/opt/share".into()); env.insert("XDG_DATA_DIRS", "/opt/share");
assert!(setup_xdg_data_dirs(&res.path, &mut env)); assert!(setup_xdg_data_dirs(&res.path, &mut env));
assert_eq!( assert_eq!(
format!("{}/shell-integration", res.path), format!("{}/shell-integration", res.path),
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"] env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR")
.unwrap()
.to_str()
.unwrap()
); );
assert_eq!( assert_eq!(
format!("{}/shell-integration:/opt/share", res.path), format!("{}/shell-integration:/opt/share", res.path),
env["XDG_DATA_DIRS"] env.get("XDG_DATA_DIRS").unwrap().to_str().unwrap()
); );
} }
@@ -710,9 +761,18 @@ mod tests {
assert_eq!("nu --execute 'use ghostty *'", shell_str(command.as_ref())); assert_eq!("nu --execute 'use ghostty *'", shell_str(command.as_ref()));
assert_eq!( assert_eq!(
format!("{}/shell-integration", res.path), format!("{}/shell-integration", res.path),
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"] env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR")
.unwrap()
.to_str()
.unwrap()
);
assert!(
env.get("XDG_DATA_DIRS")
.unwrap()
.to_str()
.unwrap()
.starts_with(&format!("{}/shell-integration", res.path))
); );
assert!(env["XDG_DATA_DIRS"].starts_with(&format!("{}/shell-integration", res.path)));
} }
#[test] #[test]
@@ -747,7 +807,10 @@ mod tests {
let command = setup_zsh(&sh("zsh"), &res.path, &mut env); let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
assert_eq!("zsh", shell_str(command.as_ref())); assert_eq!("zsh", shell_str(command.as_ref()));
assert_eq!(res.shell_path, env["ZDOTDIR"]); assert_eq!(
res.shell_path,
env.get("ZDOTDIR").unwrap().to_str().unwrap()
);
assert!(!env.contains_key("GHOSTTY_ZSH_ZDOTDIR")); assert!(!env.contains_key("GHOSTTY_ZSH_ZDOTDIR"));
} }
@@ -755,12 +818,18 @@ mod tests {
fn zsh_zdotdir() { fn zsh_zdotdir() {
let res = TmpResourcesDir::new(Shell::Zsh); let res = TmpResourcesDir::new(Shell::Zsh);
let mut env = EnvMap::new(); let mut env = EnvMap::new();
env.insert("ZDOTDIR".into(), "$HOME/.config/zsh".into()); env.insert("ZDOTDIR", "$HOME/.config/zsh");
let command = setup_zsh(&sh("zsh"), &res.path, &mut env); let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
assert_eq!("zsh", shell_str(command.as_ref())); assert_eq!("zsh", shell_str(command.as_ref()));
assert_eq!(res.shell_path, env["ZDOTDIR"]); assert_eq!(
assert_eq!("$HOME/.config/zsh", env["GHOSTTY_ZSH_ZDOTDIR"]); res.shell_path,
env.get("ZDOTDIR").unwrap().to_str().unwrap()
);
assert_eq!(
"$HOME/.config/zsh",
env.get("GHOSTTY_ZSH_ZDOTDIR").unwrap().to_str().unwrap()
);
} }
#[test] #[test]
@@ -780,7 +849,13 @@ mod tests {
let mut env = EnvMap::new(); let mut env = EnvMap::new();
let r = setup(root, &sh("bash"), &mut env, None).unwrap().unwrap(); let r = setup(root, &sh("bash"), &mut env, None).unwrap().unwrap();
assert_eq!(r.shell, Shell::Bash); assert_eq!(r.shell, Shell::Bash);
assert!(env["ENV"].ends_with("shell-integration/bash/ghostty.bash")); assert!(
env.get("ENV")
.unwrap()
.to_str()
.unwrap()
.ends_with("shell-integration/bash/ghostty.bash")
);
assert!( assert!(
Path::new(root) Path::new(root)
.join("shell-integration/nushell/vendor/autoload/ghostty.nu") .join("shell-integration/nushell/vendor/autoload/ghostty.nu")
+12 -16
View File
@@ -933,19 +933,15 @@ impl StreamHandler<'_> {
return Ok(()); return Ok(());
} }
let Ok(url_str) = std::str::from_utf8(url) else {
log::warn!("invalid url in OSC 7: not utf-8");
return Ok(());
};
// Attempt to parse this file-style URI using options appropriate // Attempt to parse this file-style URI using options appropriate
// for this OSC 7 context (e.g. kitty-shell-cwd expects the full, // for this OSC 7 context (e.g. kitty-shell-cwd expects the full,
// unencoded path). // unencoded path). Parsing is byte based so non-UTF-8 path bytes
let uri = match ghostty_os::uri::parse( // survive percent-decoding.
url_str, let uri = match ghostty_os::uri::parse_bytes(
url,
ghostty_os::uri::ParseOptions { ghostty_os::uri::ParseOptions {
mac_address: true, mac_address: true,
raw_path: url_str.starts_with("kitty-shell-cwd://"), raw_path: url.starts_with(b"kitty-shell-cwd://"),
}, },
) { ) {
Ok(uri) => uri, Ok(uri) => uri,
@@ -967,12 +963,11 @@ impl StreamHandler<'_> {
log::warn!("OSC 7 uri must contain a hostname: UriMissingHost"); log::warn!("OSC 7 uri must contain a hostname: UriMissingHost");
return Ok(()); return Ok(());
}; };
let host = host.to_raw();
// OSC 7 is a little sketchy because anyone can send any value from // OSC 7 is a little sketchy because anyone can send any value from
// any host (such an SSH session). The best practice terminals follow // any host (such an SSH session). The best practice terminals follow
// is to valid the hostname to be local. // is to valid the hostname to be local.
let host_valid = match ghostty_os::hostname::is_local(&host) { let host_valid = match ghostty_os::hostname::is_local(host) {
Ok(v) => v, Ok(v) => v,
Err(err) => { Err(err) => {
log::warn!("failed to get hostname for OSC 7 validation: {err:?}"); log::warn!("failed to get hostname for OSC 7 validation: {err:?}");
@@ -985,17 +980,18 @@ impl StreamHandler<'_> {
} }
// We need the raw path, which might require unescaping. // We need the raw path, which might require unescaping.
let path = uri.path.to_raw().into_owned(); let path = uri.path;
log::debug!("terminal pwd: {path}"); log::debug!("terminal pwd: {}", String::from_utf8_lossy(&path));
self.terminal.set_pwd(path.as_bytes())?; self.terminal.set_pwd(&path)?;
// Report it to the surface. // Report it to the surface.
self.surface_message_writer(SurfaceMessage::PwdChange(WriteReq::init(path.as_bytes()))); self.surface_message_writer(SurfaceMessage::PwdChange(WriteReq::init(&path)));
// If we haven't seen a title, use our pwd as the title. // If we haven't seen a title, use our pwd as the title.
if !self.st.seen_title { if !self.st.seen_title {
self.window_title(&path); // Titles are text; only the title is lossy.
self.window_title(&String::from_utf8_lossy(&path));
self.st.seen_title = false; self.st.seen_title = false;
} }
Ok(()) Ok(())
+24 -3
View File
@@ -34,6 +34,7 @@ use ghostty_config::types_b::{ClipboardAccess, GraphemeWidthMethod, OSCColorRepo
use ghostty_datastruct::blocking_queue::Timeout; use ghostty_datastruct::blocking_queue::Timeout;
use ghostty_renderer::message::Message as RendererMessage; use ghostty_renderer::message::Message as RendererMessage;
use ghostty_renderer::size::Size; use ghostty_renderer::size::Size;
use ghostty_renderer::state::InspectorHook;
use ghostty_terminal::kitty::graphics_storage::TerminalInfo; use ghostty_terminal::kitty::graphics_storage::TerminalInfo;
use ghostty_terminal::point::{Coordinate, Point}; use ghostty_terminal::point::{Coordinate, Point};
use ghostty_terminal::screen::Scroll; use ghostty_terminal::screen::Scroll;
@@ -221,8 +222,9 @@ impl Output {
// the lock to grab our read data. // the lock to grab our read data.
let outbox = { let outbox = {
let mut term = self.state.lock_terminal(); let mut term = self.state.lock_terminal();
let inspector = term.state().inspector.clone();
let mut inner = self.inner(); let mut inner = self.inner();
self.process_output_locked(&mut term, &mut inner, buf); self.process_output_locked(&mut term, &mut inner, buf, inspector.as_deref());
// If our stream handling caused messages to be sent to the // If our stream handling caused messages to be sent to the
// mailbox thread, then we need to wake it up so that it // mailbox thread, then we need to wake it up so that it
@@ -241,7 +243,13 @@ impl Output {
} }
/// Process output from the pty but the lock is already held. /// Process output from the pty but the lock is already held.
fn process_output_locked(&self, term: &mut Terminal, inner: &mut OutputInner, buf: &[u8]) { fn process_output_locked(
&self,
term: &mut Terminal,
inner: &mut OutputInner,
buf: &[u8],
inspector: Option<&dyn InspectorHook>,
) {
// Schedule a render. We can call this first because we have the lock. // Schedule a render. We can call this first because we have the lock.
inner.stream.state.queue_render(); inner.stream.state.queue_render();
@@ -260,7 +268,20 @@ impl Output {
.push(RendererMessage::ResetCursorBlink, Timeout::Instant); .push(RendererMessage::ResetCursorBlink, Timeout::Instant);
} }
if let Err(err) = inner.stream.process(term, buf) { // If we have an inspector, we enter SLOW MODE because we need to
// process a byte at a time alternating between the inspector handler
// and the termio handler. This is very slow compared to our
// optimizations below but at least users only pay for it if they're
// using the inspector.
if let Some(insp) = inspector {
for &byte in buf {
insp.record_pty_read(term, &[byte]);
if let Err(err) = inner.stream.process_byte(term, byte) {
log::error!("error processing terminal data: {err:?}");
}
}
} else if let Err(err) = inner.stream.process(term, buf) {
log::error!("error processing terminal data: {err:?}"); log::error!("error processing terminal data: {err:?}");
} }
} }
@@ -287,9 +287,13 @@ fn large_write_with_backpressure_is_delivered_in_order() {
// queue (the pty buffer is far smaller) and the output reader at the // queue (the pty buffer is far smaller) and the output reader at the
// same time (cat would otherwise block on a full pty). // same time (cat would otherwise block on a full pty).
let mut f = start( let mut f = start(
sh("stty raw -echo; head -c 262144 | wc -c; echo done"), sh("stty raw -echo; echo ready; head -c 262144 | wc -c; echo done"),
|_| {}, |_| {},
); );
// Writing before `stty raw` applies would go through the cooked-mode
// line discipline, which silently drops bytes past MAX_CANON (4095)
// on a line without a newline.
f.wait_text("ready");
let chunk = vec![b'x'; 4096]; let chunk = vec![b'x'; 4096];
for _ in 0..64 { for _ in 0..64 {
f.write(&chunk); f.write(&chunk);
@@ -555,7 +559,7 @@ fn change_config_message_updates_palette_and_colors() {
fn process_output_manual_api() { fn process_output_manual_api() {
// Termio can also be fed manually (no child involved). // Termio can also be fed manually (no child involved).
let config = base_config(); let config = base_config();
let mut ecfg = exec::Config::new(std::collections::HashMap::new(), "xterm-256color"); let mut ecfg = exec::Config::new(ghostty_os::EnvMap::new(), "xterm-256color");
ecfg.command = Some(sh("true")); ecfg.command = Some(sh("true"));
let (tx, _rx) = unbounded(); let (tx, _rx) = unbounded();
let termio = Termio::init(Options { let termio = Termio::init(Options {
+50
View File
@@ -18,6 +18,7 @@ mod props;
mod ucd; mod ucd;
use std::env; use std::env;
use std::fmt::Write as _;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@@ -31,6 +32,7 @@ fn main() {
println!("cargo:rerun-if-changed=src/lut.rs"); println!("cargo:rerun-if-changed=src/lut.rs");
println!("cargo:rerun-if-changed=src/props.rs"); println!("cargo:rerun-if-changed=src/props.rs");
println!("cargo:rerun-if-changed=ucd"); println!("cargo:rerun-if-changed=ucd");
println!("cargo:rerun-if-changed=ucd/CaseFolding.txt");
let ucd = ucd::Ucd::load(&ucd_dir); let ucd = ucd::Ucd::load(&ucd_dir);
@@ -54,4 +56,52 @@ fn main() {
emoji.write_rust("bool", ToString::to_string), emoji.write_rust("bool", ToString::to_string),
) )
.unwrap(); .unwrap();
generate_case_folding(&ucd_dir, &out);
}
/// Generates `case_folding_full.rs` from `CaseFolding.txt`: the `C` (common)
/// and `F` (full) statuses, which is uucode's `case_folding_full`.
fn generate_case_folding(ucd_dir: &std::path::Path, out: &std::path::Path) {
let text = fs::read_to_string(ucd_dir.join("CaseFolding.txt")).unwrap();
let mut entries: Vec<(u32, Vec<u32>)> = Vec::new();
for line in text.lines() {
let line = line.split('#').next().unwrap().trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split(';').map(str::trim).collect();
if fields.len() < 3 || (fields[1] != "C" && fields[1] != "F") {
continue;
}
let cp = u32::from_str_radix(fields[0], 16).unwrap();
let mapping: Vec<u32> = fields[2]
.split_whitespace()
.map(|v| u32::from_str_radix(v, 16).unwrap())
.collect();
assert!(!mapping.is_empty() && mapping.len() <= 3);
entries.push((cp, mapping));
}
entries.sort_by_key(|e| e.0);
entries.dedup_by_key(|e| e.0);
let mut src = String::new();
writeln!(
src,
"static CASE_FOLDING_FULL: [(u32, [u32; 3]); {}] = [",
entries.len()
)
.unwrap();
for (cp, m) in &entries {
let mut a = [0u32; 3];
a[..m.len()].copy_from_slice(m);
writeln!(
src,
" ({cp:#x}, [{:#x}, {:#x}, {:#x}]),",
a[0], a[1], a[2]
)
.unwrap();
}
src.push_str("];\n");
fs::write(out.join("case_folding_full.rs"), src).unwrap();
} }
@@ -0,0 +1,63 @@
//! Full Unicode case folding (`CaseFolding.txt` statuses `C` + `F`, Unicode
//! 17.0.0), equivalent to uucode's `case_folding_full` field.
//!
//! The table is generated by `build.rs` from `ucd/CaseFolding.txt`.
include!(concat!(env!("OUT_DIR"), "/case_folding_full.rs"));
/// The full case folding of `cp`, or `None` if it folds to itself.
///
/// The result has 1 to 3 codepoints (unused trailing entries are `0`); use
/// [`fold_full`] for a slice.
#[must_use]
pub fn lookup(cp: u32) -> Option<[u32; 3]> {
CASE_FOLDING_FULL
.binary_search_by_key(&cp, |e| e.0)
.ok()
.map(|i| CASE_FOLDING_FULL[i].1)
}
/// The full case folding of `cp` as a zero-padded 3-element array. Like
/// uucode's `.with(&buffer, cp)`, a codepoint without a folding folds to
/// itself.
#[must_use]
pub fn fold_full(cp: u32) -> [u32; 3] {
lookup(cp).unwrap_or([cp, 0, 0])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_and_identity() {
assert_eq!(fold_full('A' as u32), ['a' as u32, 0, 0]);
assert_eq!(fold_full('a' as u32), ['a' as u32, 0, 0]);
assert_eq!(fold_full('1' as u32), ['1' as u32, 0, 0]);
assert_eq!(fold_full(0x00B5), [0x03BC, 0, 0]);
assert_eq!(fold_full(0x017F), ['s' as u32, 0, 0]);
assert_eq!(fold_full(0x03C2), [0x03C3, 0, 0]);
assert_eq!(fold_full(0x1E9E), [0x73, 0x73, 0]);
}
#[test]
fn multi_codepoint() {
// U+00DF LATIN SMALL LETTER SHARP S -> "ss"
assert_eq!(fold_full(0x00DF), [0x73, 0x73, 0]);
// U+0390 -> 03B9 0308 0301
assert_eq!(fold_full(0x0390), [0x03B9, 0x0308, 0x0301]);
// U+FB03 LATIN SMALL LIGATURE FFI -> "ffi"
assert_eq!(fold_full(0xFB03), [0x66, 0x66, 0x69]);
}
#[test]
fn table_sorted_and_nonempty() {
assert!(CASE_FOLDING_FULL.len() > 1500);
assert!(CASE_FOLDING_FULL.windows(2).all(|w| w[0].0 < w[1].0));
}
#[test]
fn dotted_capital_i_uses_full_folding() {
assert_eq!(fold_full(0x0130), [0x69, 0x307, 0]); // F entry for dotted I
}
}
+1
View File
@@ -8,6 +8,7 @@
#![cfg_attr(test, allow(clippy::unwrap_used))] #![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod case_folding;
pub mod emoji_table; pub mod emoji_table;
pub mod grapheme; pub mod grapheme;
pub mod lut; pub mod lut;
+1682
View File
@@ -0,0 +1,1682 @@
# CaseFolding-17.0.0.txt
# Date: 2025-07-30, 23:54:36 GMT
# © 2025 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use and license, see https://www.unicode.org/terms_of_use.html
#
# Unicode Character Database
# For documentation, see https://www.unicode.org/reports/tr44/
#
# Case Folding Properties
#
# This file is a supplement to the UnicodeData file.
# It provides a case folding mapping generated from the Unicode Character Database.
# If all characters are mapped according to the full mapping below, then
# case differences (according to UnicodeData.txt and SpecialCasing.txt)
# are eliminated.
#
# The data supports both implementations that require simple case foldings
# (where string lengths don't change), and implementations that allow full case folding
# (where string lengths may grow). Note that where they can be supported, the
# full case foldings are superior: for example, they allow "FUSS" and "Fuß" to match.
#
# All code points not listed in this file map to themselves.
#
# NOTE: case folding does not preserve normalization formats!
#
# For information on case folding, including how to have case folding
# preserve normalization formats, see the
# "Conformance" / "Default Case Algorithms" section of the core specification.
#
# ================================================================================
# Format
# ================================================================================
# The entries in this file are in the following machine-readable format:
#
# <code>; <status>; <mapping>; # <name>
#
# The status field is:
# C: common case folding, common mappings shared by both simple and full mappings.
# F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces.
# S: simple case folding, mappings to single characters where different from F.
# T: special case for uppercase I and dotted uppercase I
# - For non-Turkic languages, this mapping is normally not used.
# - For Turkic languages (tr, az), this mapping can be used instead of the normal mapping for these characters.
# Note that the Turkic mappings do not maintain canonical equivalence without additional processing.
# See the discussions of case mapping in the Unicode Standard for more information.
#
# Usage:
# A. To do a simple case folding, use the mappings with status C + S.
# B. To do a full case folding, use the mappings with status C + F.
#
# The mappings with status T can be used or omitted depending on the desired case-folding
# behavior. (The default option is to exclude them.)
#
# =================================================================
# Property: Case_Folding
# All code points not explicitly listed for Case_Folding
# have the value C for the status field, and the code point itself for the mapping field.
# =================================================================
0041; C; 0061; # LATIN CAPITAL LETTER A
0042; C; 0062; # LATIN CAPITAL LETTER B
0043; C; 0063; # LATIN CAPITAL LETTER C
0044; C; 0064; # LATIN CAPITAL LETTER D
0045; C; 0065; # LATIN CAPITAL LETTER E
0046; C; 0066; # LATIN CAPITAL LETTER F
0047; C; 0067; # LATIN CAPITAL LETTER G
0048; C; 0068; # LATIN CAPITAL LETTER H
0049; C; 0069; # LATIN CAPITAL LETTER I
0049; T; 0131; # LATIN CAPITAL LETTER I
004A; C; 006A; # LATIN CAPITAL LETTER J
004B; C; 006B; # LATIN CAPITAL LETTER K
004C; C; 006C; # LATIN CAPITAL LETTER L
004D; C; 006D; # LATIN CAPITAL LETTER M
004E; C; 006E; # LATIN CAPITAL LETTER N
004F; C; 006F; # LATIN CAPITAL LETTER O
0050; C; 0070; # LATIN CAPITAL LETTER P
0051; C; 0071; # LATIN CAPITAL LETTER Q
0052; C; 0072; # LATIN CAPITAL LETTER R
0053; C; 0073; # LATIN CAPITAL LETTER S
0054; C; 0074; # LATIN CAPITAL LETTER T
0055; C; 0075; # LATIN CAPITAL LETTER U
0056; C; 0076; # LATIN CAPITAL LETTER V
0057; C; 0077; # LATIN CAPITAL LETTER W
0058; C; 0078; # LATIN CAPITAL LETTER X
0059; C; 0079; # LATIN CAPITAL LETTER Y
005A; C; 007A; # LATIN CAPITAL LETTER Z
00B5; C; 03BC; # MICRO SIGN
00C0; C; 00E0; # LATIN CAPITAL LETTER A WITH GRAVE
00C1; C; 00E1; # LATIN CAPITAL LETTER A WITH ACUTE
00C2; C; 00E2; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX
00C3; C; 00E3; # LATIN CAPITAL LETTER A WITH TILDE
00C4; C; 00E4; # LATIN CAPITAL LETTER A WITH DIAERESIS
00C5; C; 00E5; # LATIN CAPITAL LETTER A WITH RING ABOVE
00C6; C; 00E6; # LATIN CAPITAL LETTER AE
00C7; C; 00E7; # LATIN CAPITAL LETTER C WITH CEDILLA
00C8; C; 00E8; # LATIN CAPITAL LETTER E WITH GRAVE
00C9; C; 00E9; # LATIN CAPITAL LETTER E WITH ACUTE
00CA; C; 00EA; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX
00CB; C; 00EB; # LATIN CAPITAL LETTER E WITH DIAERESIS
00CC; C; 00EC; # LATIN CAPITAL LETTER I WITH GRAVE
00CD; C; 00ED; # LATIN CAPITAL LETTER I WITH ACUTE
00CE; C; 00EE; # LATIN CAPITAL LETTER I WITH CIRCUMFLEX
00CF; C; 00EF; # LATIN CAPITAL LETTER I WITH DIAERESIS
00D0; C; 00F0; # LATIN CAPITAL LETTER ETH
00D1; C; 00F1; # LATIN CAPITAL LETTER N WITH TILDE
00D2; C; 00F2; # LATIN CAPITAL LETTER O WITH GRAVE
00D3; C; 00F3; # LATIN CAPITAL LETTER O WITH ACUTE
00D4; C; 00F4; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX
00D5; C; 00F5; # LATIN CAPITAL LETTER O WITH TILDE
00D6; C; 00F6; # LATIN CAPITAL LETTER O WITH DIAERESIS
00D8; C; 00F8; # LATIN CAPITAL LETTER O WITH STROKE
00D9; C; 00F9; # LATIN CAPITAL LETTER U WITH GRAVE
00DA; C; 00FA; # LATIN CAPITAL LETTER U WITH ACUTE
00DB; C; 00FB; # LATIN CAPITAL LETTER U WITH CIRCUMFLEX
00DC; C; 00FC; # LATIN CAPITAL LETTER U WITH DIAERESIS
00DD; C; 00FD; # LATIN CAPITAL LETTER Y WITH ACUTE
00DE; C; 00FE; # LATIN CAPITAL LETTER THORN
00DF; F; 0073 0073; # LATIN SMALL LETTER SHARP S
0100; C; 0101; # LATIN CAPITAL LETTER A WITH MACRON
0102; C; 0103; # LATIN CAPITAL LETTER A WITH BREVE
0104; C; 0105; # LATIN CAPITAL LETTER A WITH OGONEK
0106; C; 0107; # LATIN CAPITAL LETTER C WITH ACUTE
0108; C; 0109; # LATIN CAPITAL LETTER C WITH CIRCUMFLEX
010A; C; 010B; # LATIN CAPITAL LETTER C WITH DOT ABOVE
010C; C; 010D; # LATIN CAPITAL LETTER C WITH CARON
010E; C; 010F; # LATIN CAPITAL LETTER D WITH CARON
0110; C; 0111; # LATIN CAPITAL LETTER D WITH STROKE
0112; C; 0113; # LATIN CAPITAL LETTER E WITH MACRON
0114; C; 0115; # LATIN CAPITAL LETTER E WITH BREVE
0116; C; 0117; # LATIN CAPITAL LETTER E WITH DOT ABOVE
0118; C; 0119; # LATIN CAPITAL LETTER E WITH OGONEK
011A; C; 011B; # LATIN CAPITAL LETTER E WITH CARON
011C; C; 011D; # LATIN CAPITAL LETTER G WITH CIRCUMFLEX
011E; C; 011F; # LATIN CAPITAL LETTER G WITH BREVE
0120; C; 0121; # LATIN CAPITAL LETTER G WITH DOT ABOVE
0122; C; 0123; # LATIN CAPITAL LETTER G WITH CEDILLA
0124; C; 0125; # LATIN CAPITAL LETTER H WITH CIRCUMFLEX
0126; C; 0127; # LATIN CAPITAL LETTER H WITH STROKE
0128; C; 0129; # LATIN CAPITAL LETTER I WITH TILDE
012A; C; 012B; # LATIN CAPITAL LETTER I WITH MACRON
012C; C; 012D; # LATIN CAPITAL LETTER I WITH BREVE
012E; C; 012F; # LATIN CAPITAL LETTER I WITH OGONEK
0130; F; 0069 0307; # LATIN CAPITAL LETTER I WITH DOT ABOVE
0130; T; 0069; # LATIN CAPITAL LETTER I WITH DOT ABOVE
0132; C; 0133; # LATIN CAPITAL LIGATURE IJ
0134; C; 0135; # LATIN CAPITAL LETTER J WITH CIRCUMFLEX
0136; C; 0137; # LATIN CAPITAL LETTER K WITH CEDILLA
0139; C; 013A; # LATIN CAPITAL LETTER L WITH ACUTE
013B; C; 013C; # LATIN CAPITAL LETTER L WITH CEDILLA
013D; C; 013E; # LATIN CAPITAL LETTER L WITH CARON
013F; C; 0140; # LATIN CAPITAL LETTER L WITH MIDDLE DOT
0141; C; 0142; # LATIN CAPITAL LETTER L WITH STROKE
0143; C; 0144; # LATIN CAPITAL LETTER N WITH ACUTE
0145; C; 0146; # LATIN CAPITAL LETTER N WITH CEDILLA
0147; C; 0148; # LATIN CAPITAL LETTER N WITH CARON
0149; F; 02BC 006E; # LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
014A; C; 014B; # LATIN CAPITAL LETTER ENG
014C; C; 014D; # LATIN CAPITAL LETTER O WITH MACRON
014E; C; 014F; # LATIN CAPITAL LETTER O WITH BREVE
0150; C; 0151; # LATIN CAPITAL LETTER O WITH DOUBLE ACUTE
0152; C; 0153; # LATIN CAPITAL LIGATURE OE
0154; C; 0155; # LATIN CAPITAL LETTER R WITH ACUTE
0156; C; 0157; # LATIN CAPITAL LETTER R WITH CEDILLA
0158; C; 0159; # LATIN CAPITAL LETTER R WITH CARON
015A; C; 015B; # LATIN CAPITAL LETTER S WITH ACUTE
015C; C; 015D; # LATIN CAPITAL LETTER S WITH CIRCUMFLEX
015E; C; 015F; # LATIN CAPITAL LETTER S WITH CEDILLA
0160; C; 0161; # LATIN CAPITAL LETTER S WITH CARON
0162; C; 0163; # LATIN CAPITAL LETTER T WITH CEDILLA
0164; C; 0165; # LATIN CAPITAL LETTER T WITH CARON
0166; C; 0167; # LATIN CAPITAL LETTER T WITH STROKE
0168; C; 0169; # LATIN CAPITAL LETTER U WITH TILDE
016A; C; 016B; # LATIN CAPITAL LETTER U WITH MACRON
016C; C; 016D; # LATIN CAPITAL LETTER U WITH BREVE
016E; C; 016F; # LATIN CAPITAL LETTER U WITH RING ABOVE
0170; C; 0171; # LATIN CAPITAL LETTER U WITH DOUBLE ACUTE
0172; C; 0173; # LATIN CAPITAL LETTER U WITH OGONEK
0174; C; 0175; # LATIN CAPITAL LETTER W WITH CIRCUMFLEX
0176; C; 0177; # LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
0178; C; 00FF; # LATIN CAPITAL LETTER Y WITH DIAERESIS
0179; C; 017A; # LATIN CAPITAL LETTER Z WITH ACUTE
017B; C; 017C; # LATIN CAPITAL LETTER Z WITH DOT ABOVE
017D; C; 017E; # LATIN CAPITAL LETTER Z WITH CARON
017F; C; 0073; # LATIN SMALL LETTER LONG S
0181; C; 0253; # LATIN CAPITAL LETTER B WITH HOOK
0182; C; 0183; # LATIN CAPITAL LETTER B WITH TOPBAR
0184; C; 0185; # LATIN CAPITAL LETTER TONE SIX
0186; C; 0254; # LATIN CAPITAL LETTER OPEN O
0187; C; 0188; # LATIN CAPITAL LETTER C WITH HOOK
0189; C; 0256; # LATIN CAPITAL LETTER AFRICAN D
018A; C; 0257; # LATIN CAPITAL LETTER D WITH HOOK
018B; C; 018C; # LATIN CAPITAL LETTER D WITH TOPBAR
018E; C; 01DD; # LATIN CAPITAL LETTER REVERSED E
018F; C; 0259; # LATIN CAPITAL LETTER SCHWA
0190; C; 025B; # LATIN CAPITAL LETTER OPEN E
0191; C; 0192; # LATIN CAPITAL LETTER F WITH HOOK
0193; C; 0260; # LATIN CAPITAL LETTER G WITH HOOK
0194; C; 0263; # LATIN CAPITAL LETTER GAMMA
0196; C; 0269; # LATIN CAPITAL LETTER IOTA
0197; C; 0268; # LATIN CAPITAL LETTER I WITH STROKE
0198; C; 0199; # LATIN CAPITAL LETTER K WITH HOOK
019C; C; 026F; # LATIN CAPITAL LETTER TURNED M
019D; C; 0272; # LATIN CAPITAL LETTER N WITH LEFT HOOK
019F; C; 0275; # LATIN CAPITAL LETTER O WITH MIDDLE TILDE
01A0; C; 01A1; # LATIN CAPITAL LETTER O WITH HORN
01A2; C; 01A3; # LATIN CAPITAL LETTER OI
01A4; C; 01A5; # LATIN CAPITAL LETTER P WITH HOOK
01A6; C; 0280; # LATIN LETTER YR
01A7; C; 01A8; # LATIN CAPITAL LETTER TONE TWO
01A9; C; 0283; # LATIN CAPITAL LETTER ESH
01AC; C; 01AD; # LATIN CAPITAL LETTER T WITH HOOK
01AE; C; 0288; # LATIN CAPITAL LETTER T WITH RETROFLEX HOOK
01AF; C; 01B0; # LATIN CAPITAL LETTER U WITH HORN
01B1; C; 028A; # LATIN CAPITAL LETTER UPSILON
01B2; C; 028B; # LATIN CAPITAL LETTER V WITH HOOK
01B3; C; 01B4; # LATIN CAPITAL LETTER Y WITH HOOK
01B5; C; 01B6; # LATIN CAPITAL LETTER Z WITH STROKE
01B7; C; 0292; # LATIN CAPITAL LETTER EZH
01B8; C; 01B9; # LATIN CAPITAL LETTER EZH REVERSED
01BC; C; 01BD; # LATIN CAPITAL LETTER TONE FIVE
01C4; C; 01C6; # LATIN CAPITAL LETTER DZ WITH CARON
01C5; C; 01C6; # LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON
01C7; C; 01C9; # LATIN CAPITAL LETTER LJ
01C8; C; 01C9; # LATIN CAPITAL LETTER L WITH SMALL LETTER J
01CA; C; 01CC; # LATIN CAPITAL LETTER NJ
01CB; C; 01CC; # LATIN CAPITAL LETTER N WITH SMALL LETTER J
01CD; C; 01CE; # LATIN CAPITAL LETTER A WITH CARON
01CF; C; 01D0; # LATIN CAPITAL LETTER I WITH CARON
01D1; C; 01D2; # LATIN CAPITAL LETTER O WITH CARON
01D3; C; 01D4; # LATIN CAPITAL LETTER U WITH CARON
01D5; C; 01D6; # LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON
01D7; C; 01D8; # LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE
01D9; C; 01DA; # LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON
01DB; C; 01DC; # LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE
01DE; C; 01DF; # LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON
01E0; C; 01E1; # LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON
01E2; C; 01E3; # LATIN CAPITAL LETTER AE WITH MACRON
01E4; C; 01E5; # LATIN CAPITAL LETTER G WITH STROKE
01E6; C; 01E7; # LATIN CAPITAL LETTER G WITH CARON
01E8; C; 01E9; # LATIN CAPITAL LETTER K WITH CARON
01EA; C; 01EB; # LATIN CAPITAL LETTER O WITH OGONEK
01EC; C; 01ED; # LATIN CAPITAL LETTER O WITH OGONEK AND MACRON
01EE; C; 01EF; # LATIN CAPITAL LETTER EZH WITH CARON
01F0; F; 006A 030C; # LATIN SMALL LETTER J WITH CARON
01F1; C; 01F3; # LATIN CAPITAL LETTER DZ
01F2; C; 01F3; # LATIN CAPITAL LETTER D WITH SMALL LETTER Z
01F4; C; 01F5; # LATIN CAPITAL LETTER G WITH ACUTE
01F6; C; 0195; # LATIN CAPITAL LETTER HWAIR
01F7; C; 01BF; # LATIN CAPITAL LETTER WYNN
01F8; C; 01F9; # LATIN CAPITAL LETTER N WITH GRAVE
01FA; C; 01FB; # LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE
01FC; C; 01FD; # LATIN CAPITAL LETTER AE WITH ACUTE
01FE; C; 01FF; # LATIN CAPITAL LETTER O WITH STROKE AND ACUTE
0200; C; 0201; # LATIN CAPITAL LETTER A WITH DOUBLE GRAVE
0202; C; 0203; # LATIN CAPITAL LETTER A WITH INVERTED BREVE
0204; C; 0205; # LATIN CAPITAL LETTER E WITH DOUBLE GRAVE
0206; C; 0207; # LATIN CAPITAL LETTER E WITH INVERTED BREVE
0208; C; 0209; # LATIN CAPITAL LETTER I WITH DOUBLE GRAVE
020A; C; 020B; # LATIN CAPITAL LETTER I WITH INVERTED BREVE
020C; C; 020D; # LATIN CAPITAL LETTER O WITH DOUBLE GRAVE
020E; C; 020F; # LATIN CAPITAL LETTER O WITH INVERTED BREVE
0210; C; 0211; # LATIN CAPITAL LETTER R WITH DOUBLE GRAVE
0212; C; 0213; # LATIN CAPITAL LETTER R WITH INVERTED BREVE
0214; C; 0215; # LATIN CAPITAL LETTER U WITH DOUBLE GRAVE
0216; C; 0217; # LATIN CAPITAL LETTER U WITH INVERTED BREVE
0218; C; 0219; # LATIN CAPITAL LETTER S WITH COMMA BELOW
021A; C; 021B; # LATIN CAPITAL LETTER T WITH COMMA BELOW
021C; C; 021D; # LATIN CAPITAL LETTER YOGH
021E; C; 021F; # LATIN CAPITAL LETTER H WITH CARON
0220; C; 019E; # LATIN CAPITAL LETTER N WITH LONG RIGHT LEG
0222; C; 0223; # LATIN CAPITAL LETTER OU
0224; C; 0225; # LATIN CAPITAL LETTER Z WITH HOOK
0226; C; 0227; # LATIN CAPITAL LETTER A WITH DOT ABOVE
0228; C; 0229; # LATIN CAPITAL LETTER E WITH CEDILLA
022A; C; 022B; # LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON
022C; C; 022D; # LATIN CAPITAL LETTER O WITH TILDE AND MACRON
022E; C; 022F; # LATIN CAPITAL LETTER O WITH DOT ABOVE
0230; C; 0231; # LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON
0232; C; 0233; # LATIN CAPITAL LETTER Y WITH MACRON
023A; C; 2C65; # LATIN CAPITAL LETTER A WITH STROKE
023B; C; 023C; # LATIN CAPITAL LETTER C WITH STROKE
023D; C; 019A; # LATIN CAPITAL LETTER L WITH BAR
023E; C; 2C66; # LATIN CAPITAL LETTER T WITH DIAGONAL STROKE
0241; C; 0242; # LATIN CAPITAL LETTER GLOTTAL STOP
0243; C; 0180; # LATIN CAPITAL LETTER B WITH STROKE
0244; C; 0289; # LATIN CAPITAL LETTER U BAR
0245; C; 028C; # LATIN CAPITAL LETTER TURNED V
0246; C; 0247; # LATIN CAPITAL LETTER E WITH STROKE
0248; C; 0249; # LATIN CAPITAL LETTER J WITH STROKE
024A; C; 024B; # LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL
024C; C; 024D; # LATIN CAPITAL LETTER R WITH STROKE
024E; C; 024F; # LATIN CAPITAL LETTER Y WITH STROKE
0345; C; 03B9; # COMBINING GREEK YPOGEGRAMMENI
0370; C; 0371; # GREEK CAPITAL LETTER HETA
0372; C; 0373; # GREEK CAPITAL LETTER ARCHAIC SAMPI
0376; C; 0377; # GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA
037F; C; 03F3; # GREEK CAPITAL LETTER YOT
0386; C; 03AC; # GREEK CAPITAL LETTER ALPHA WITH TONOS
0388; C; 03AD; # GREEK CAPITAL LETTER EPSILON WITH TONOS
0389; C; 03AE; # GREEK CAPITAL LETTER ETA WITH TONOS
038A; C; 03AF; # GREEK CAPITAL LETTER IOTA WITH TONOS
038C; C; 03CC; # GREEK CAPITAL LETTER OMICRON WITH TONOS
038E; C; 03CD; # GREEK CAPITAL LETTER UPSILON WITH TONOS
038F; C; 03CE; # GREEK CAPITAL LETTER OMEGA WITH TONOS
0390; F; 03B9 0308 0301; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS
0391; C; 03B1; # GREEK CAPITAL LETTER ALPHA
0392; C; 03B2; # GREEK CAPITAL LETTER BETA
0393; C; 03B3; # GREEK CAPITAL LETTER GAMMA
0394; C; 03B4; # GREEK CAPITAL LETTER DELTA
0395; C; 03B5; # GREEK CAPITAL LETTER EPSILON
0396; C; 03B6; # GREEK CAPITAL LETTER ZETA
0397; C; 03B7; # GREEK CAPITAL LETTER ETA
0398; C; 03B8; # GREEK CAPITAL LETTER THETA
0399; C; 03B9; # GREEK CAPITAL LETTER IOTA
039A; C; 03BA; # GREEK CAPITAL LETTER KAPPA
039B; C; 03BB; # GREEK CAPITAL LETTER LAMDA
039C; C; 03BC; # GREEK CAPITAL LETTER MU
039D; C; 03BD; # GREEK CAPITAL LETTER NU
039E; C; 03BE; # GREEK CAPITAL LETTER XI
039F; C; 03BF; # GREEK CAPITAL LETTER OMICRON
03A0; C; 03C0; # GREEK CAPITAL LETTER PI
03A1; C; 03C1; # GREEK CAPITAL LETTER RHO
03A3; C; 03C3; # GREEK CAPITAL LETTER SIGMA
03A4; C; 03C4; # GREEK CAPITAL LETTER TAU
03A5; C; 03C5; # GREEK CAPITAL LETTER UPSILON
03A6; C; 03C6; # GREEK CAPITAL LETTER PHI
03A7; C; 03C7; # GREEK CAPITAL LETTER CHI
03A8; C; 03C8; # GREEK CAPITAL LETTER PSI
03A9; C; 03C9; # GREEK CAPITAL LETTER OMEGA
03AA; C; 03CA; # GREEK CAPITAL LETTER IOTA WITH DIALYTIKA
03AB; C; 03CB; # GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA
03B0; F; 03C5 0308 0301; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS
03C2; C; 03C3; # GREEK SMALL LETTER FINAL SIGMA
03CF; C; 03D7; # GREEK CAPITAL KAI SYMBOL
03D0; C; 03B2; # GREEK BETA SYMBOL
03D1; C; 03B8; # GREEK THETA SYMBOL
03D5; C; 03C6; # GREEK PHI SYMBOL
03D6; C; 03C0; # GREEK PI SYMBOL
03D8; C; 03D9; # GREEK LETTER ARCHAIC KOPPA
03DA; C; 03DB; # GREEK LETTER STIGMA
03DC; C; 03DD; # GREEK LETTER DIGAMMA
03DE; C; 03DF; # GREEK LETTER KOPPA
03E0; C; 03E1; # GREEK LETTER SAMPI
03E2; C; 03E3; # COPTIC CAPITAL LETTER SHEI
03E4; C; 03E5; # COPTIC CAPITAL LETTER FEI
03E6; C; 03E7; # COPTIC CAPITAL LETTER KHEI
03E8; C; 03E9; # COPTIC CAPITAL LETTER HORI
03EA; C; 03EB; # COPTIC CAPITAL LETTER GANGIA
03EC; C; 03ED; # COPTIC CAPITAL LETTER SHIMA
03EE; C; 03EF; # COPTIC CAPITAL LETTER DEI
03F0; C; 03BA; # GREEK KAPPA SYMBOL
03F1; C; 03C1; # GREEK RHO SYMBOL
03F4; C; 03B8; # GREEK CAPITAL THETA SYMBOL
03F5; C; 03B5; # GREEK LUNATE EPSILON SYMBOL
03F7; C; 03F8; # GREEK CAPITAL LETTER SHO
03F9; C; 03F2; # GREEK CAPITAL LUNATE SIGMA SYMBOL
03FA; C; 03FB; # GREEK CAPITAL LETTER SAN
03FD; C; 037B; # GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL
03FE; C; 037C; # GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL
03FF; C; 037D; # GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL
0400; C; 0450; # CYRILLIC CAPITAL LETTER IE WITH GRAVE
0401; C; 0451; # CYRILLIC CAPITAL LETTER IO
0402; C; 0452; # CYRILLIC CAPITAL LETTER DJE
0403; C; 0453; # CYRILLIC CAPITAL LETTER GJE
0404; C; 0454; # CYRILLIC CAPITAL LETTER UKRAINIAN IE
0405; C; 0455; # CYRILLIC CAPITAL LETTER DZE
0406; C; 0456; # CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I
0407; C; 0457; # CYRILLIC CAPITAL LETTER YI
0408; C; 0458; # CYRILLIC CAPITAL LETTER JE
0409; C; 0459; # CYRILLIC CAPITAL LETTER LJE
040A; C; 045A; # CYRILLIC CAPITAL LETTER NJE
040B; C; 045B; # CYRILLIC CAPITAL LETTER TSHE
040C; C; 045C; # CYRILLIC CAPITAL LETTER KJE
040D; C; 045D; # CYRILLIC CAPITAL LETTER I WITH GRAVE
040E; C; 045E; # CYRILLIC CAPITAL LETTER SHORT U
040F; C; 045F; # CYRILLIC CAPITAL LETTER DZHE
0410; C; 0430; # CYRILLIC CAPITAL LETTER A
0411; C; 0431; # CYRILLIC CAPITAL LETTER BE
0412; C; 0432; # CYRILLIC CAPITAL LETTER VE
0413; C; 0433; # CYRILLIC CAPITAL LETTER GHE
0414; C; 0434; # CYRILLIC CAPITAL LETTER DE
0415; C; 0435; # CYRILLIC CAPITAL LETTER IE
0416; C; 0436; # CYRILLIC CAPITAL LETTER ZHE
0417; C; 0437; # CYRILLIC CAPITAL LETTER ZE
0418; C; 0438; # CYRILLIC CAPITAL LETTER I
0419; C; 0439; # CYRILLIC CAPITAL LETTER SHORT I
041A; C; 043A; # CYRILLIC CAPITAL LETTER KA
041B; C; 043B; # CYRILLIC CAPITAL LETTER EL
041C; C; 043C; # CYRILLIC CAPITAL LETTER EM
041D; C; 043D; # CYRILLIC CAPITAL LETTER EN
041E; C; 043E; # CYRILLIC CAPITAL LETTER O
041F; C; 043F; # CYRILLIC CAPITAL LETTER PE
0420; C; 0440; # CYRILLIC CAPITAL LETTER ER
0421; C; 0441; # CYRILLIC CAPITAL LETTER ES
0422; C; 0442; # CYRILLIC CAPITAL LETTER TE
0423; C; 0443; # CYRILLIC CAPITAL LETTER U
0424; C; 0444; # CYRILLIC CAPITAL LETTER EF
0425; C; 0445; # CYRILLIC CAPITAL LETTER HA
0426; C; 0446; # CYRILLIC CAPITAL LETTER TSE
0427; C; 0447; # CYRILLIC CAPITAL LETTER CHE
0428; C; 0448; # CYRILLIC CAPITAL LETTER SHA
0429; C; 0449; # CYRILLIC CAPITAL LETTER SHCHA
042A; C; 044A; # CYRILLIC CAPITAL LETTER HARD SIGN
042B; C; 044B; # CYRILLIC CAPITAL LETTER YERU
042C; C; 044C; # CYRILLIC CAPITAL LETTER SOFT SIGN
042D; C; 044D; # CYRILLIC CAPITAL LETTER E
042E; C; 044E; # CYRILLIC CAPITAL LETTER YU
042F; C; 044F; # CYRILLIC CAPITAL LETTER YA
0460; C; 0461; # CYRILLIC CAPITAL LETTER OMEGA
0462; C; 0463; # CYRILLIC CAPITAL LETTER YAT
0464; C; 0465; # CYRILLIC CAPITAL LETTER IOTIFIED E
0466; C; 0467; # CYRILLIC CAPITAL LETTER LITTLE YUS
0468; C; 0469; # CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS
046A; C; 046B; # CYRILLIC CAPITAL LETTER BIG YUS
046C; C; 046D; # CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS
046E; C; 046F; # CYRILLIC CAPITAL LETTER KSI
0470; C; 0471; # CYRILLIC CAPITAL LETTER PSI
0472; C; 0473; # CYRILLIC CAPITAL LETTER FITA
0474; C; 0475; # CYRILLIC CAPITAL LETTER IZHITSA
0476; C; 0477; # CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT
0478; C; 0479; # CYRILLIC CAPITAL LETTER UK
047A; C; 047B; # CYRILLIC CAPITAL LETTER ROUND OMEGA
047C; C; 047D; # CYRILLIC CAPITAL LETTER OMEGA WITH TITLO
047E; C; 047F; # CYRILLIC CAPITAL LETTER OT
0480; C; 0481; # CYRILLIC CAPITAL LETTER KOPPA
048A; C; 048B; # CYRILLIC CAPITAL LETTER SHORT I WITH TAIL
048C; C; 048D; # CYRILLIC CAPITAL LETTER SEMISOFT SIGN
048E; C; 048F; # CYRILLIC CAPITAL LETTER ER WITH TICK
0490; C; 0491; # CYRILLIC CAPITAL LETTER GHE WITH UPTURN
0492; C; 0493; # CYRILLIC CAPITAL LETTER GHE WITH STROKE
0494; C; 0495; # CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK
0496; C; 0497; # CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER
0498; C; 0499; # CYRILLIC CAPITAL LETTER ZE WITH DESCENDER
049A; C; 049B; # CYRILLIC CAPITAL LETTER KA WITH DESCENDER
049C; C; 049D; # CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE
049E; C; 049F; # CYRILLIC CAPITAL LETTER KA WITH STROKE
04A0; C; 04A1; # CYRILLIC CAPITAL LETTER BASHKIR KA
04A2; C; 04A3; # CYRILLIC CAPITAL LETTER EN WITH DESCENDER
04A4; C; 04A5; # CYRILLIC CAPITAL LIGATURE EN GHE
04A6; C; 04A7; # CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK
04A8; C; 04A9; # CYRILLIC CAPITAL LETTER ABKHASIAN HA
04AA; C; 04AB; # CYRILLIC CAPITAL LETTER ES WITH DESCENDER
04AC; C; 04AD; # CYRILLIC CAPITAL LETTER TE WITH DESCENDER
04AE; C; 04AF; # CYRILLIC CAPITAL LETTER STRAIGHT U
04B0; C; 04B1; # CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE
04B2; C; 04B3; # CYRILLIC CAPITAL LETTER HA WITH DESCENDER
04B4; C; 04B5; # CYRILLIC CAPITAL LIGATURE TE TSE
04B6; C; 04B7; # CYRILLIC CAPITAL LETTER CHE WITH DESCENDER
04B8; C; 04B9; # CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE
04BA; C; 04BB; # CYRILLIC CAPITAL LETTER SHHA
04BC; C; 04BD; # CYRILLIC CAPITAL LETTER ABKHASIAN CHE
04BE; C; 04BF; # CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER
04C0; C; 04CF; # CYRILLIC LETTER PALOCHKA
04C1; C; 04C2; # CYRILLIC CAPITAL LETTER ZHE WITH BREVE
04C3; C; 04C4; # CYRILLIC CAPITAL LETTER KA WITH HOOK
04C5; C; 04C6; # CYRILLIC CAPITAL LETTER EL WITH TAIL
04C7; C; 04C8; # CYRILLIC CAPITAL LETTER EN WITH HOOK
04C9; C; 04CA; # CYRILLIC CAPITAL LETTER EN WITH TAIL
04CB; C; 04CC; # CYRILLIC CAPITAL LETTER KHAKASSIAN CHE
04CD; C; 04CE; # CYRILLIC CAPITAL LETTER EM WITH TAIL
04D0; C; 04D1; # CYRILLIC CAPITAL LETTER A WITH BREVE
04D2; C; 04D3; # CYRILLIC CAPITAL LETTER A WITH DIAERESIS
04D4; C; 04D5; # CYRILLIC CAPITAL LIGATURE A IE
04D6; C; 04D7; # CYRILLIC CAPITAL LETTER IE WITH BREVE
04D8; C; 04D9; # CYRILLIC CAPITAL LETTER SCHWA
04DA; C; 04DB; # CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS
04DC; C; 04DD; # CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS
04DE; C; 04DF; # CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS
04E0; C; 04E1; # CYRILLIC CAPITAL LETTER ABKHASIAN DZE
04E2; C; 04E3; # CYRILLIC CAPITAL LETTER I WITH MACRON
04E4; C; 04E5; # CYRILLIC CAPITAL LETTER I WITH DIAERESIS
04E6; C; 04E7; # CYRILLIC CAPITAL LETTER O WITH DIAERESIS
04E8; C; 04E9; # CYRILLIC CAPITAL LETTER BARRED O
04EA; C; 04EB; # CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS
04EC; C; 04ED; # CYRILLIC CAPITAL LETTER E WITH DIAERESIS
04EE; C; 04EF; # CYRILLIC CAPITAL LETTER U WITH MACRON
04F0; C; 04F1; # CYRILLIC CAPITAL LETTER U WITH DIAERESIS
04F2; C; 04F3; # CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE
04F4; C; 04F5; # CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS
04F6; C; 04F7; # CYRILLIC CAPITAL LETTER GHE WITH DESCENDER
04F8; C; 04F9; # CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS
04FA; C; 04FB; # CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK
04FC; C; 04FD; # CYRILLIC CAPITAL LETTER HA WITH HOOK
04FE; C; 04FF; # CYRILLIC CAPITAL LETTER HA WITH STROKE
0500; C; 0501; # CYRILLIC CAPITAL LETTER KOMI DE
0502; C; 0503; # CYRILLIC CAPITAL LETTER KOMI DJE
0504; C; 0505; # CYRILLIC CAPITAL LETTER KOMI ZJE
0506; C; 0507; # CYRILLIC CAPITAL LETTER KOMI DZJE
0508; C; 0509; # CYRILLIC CAPITAL LETTER KOMI LJE
050A; C; 050B; # CYRILLIC CAPITAL LETTER KOMI NJE
050C; C; 050D; # CYRILLIC CAPITAL LETTER KOMI SJE
050E; C; 050F; # CYRILLIC CAPITAL LETTER KOMI TJE
0510; C; 0511; # CYRILLIC CAPITAL LETTER REVERSED ZE
0512; C; 0513; # CYRILLIC CAPITAL LETTER EL WITH HOOK
0514; C; 0515; # CYRILLIC CAPITAL LETTER LHA
0516; C; 0517; # CYRILLIC CAPITAL LETTER RHA
0518; C; 0519; # CYRILLIC CAPITAL LETTER YAE
051A; C; 051B; # CYRILLIC CAPITAL LETTER QA
051C; C; 051D; # CYRILLIC CAPITAL LETTER WE
051E; C; 051F; # CYRILLIC CAPITAL LETTER ALEUT KA
0520; C; 0521; # CYRILLIC CAPITAL LETTER EL WITH MIDDLE HOOK
0522; C; 0523; # CYRILLIC CAPITAL LETTER EN WITH MIDDLE HOOK
0524; C; 0525; # CYRILLIC CAPITAL LETTER PE WITH DESCENDER
0526; C; 0527; # CYRILLIC CAPITAL LETTER SHHA WITH DESCENDER
0528; C; 0529; # CYRILLIC CAPITAL LETTER EN WITH LEFT HOOK
052A; C; 052B; # CYRILLIC CAPITAL LETTER DZZHE
052C; C; 052D; # CYRILLIC CAPITAL LETTER DCHE
052E; C; 052F; # CYRILLIC CAPITAL LETTER EL WITH DESCENDER
0531; C; 0561; # ARMENIAN CAPITAL LETTER AYB
0532; C; 0562; # ARMENIAN CAPITAL LETTER BEN
0533; C; 0563; # ARMENIAN CAPITAL LETTER GIM
0534; C; 0564; # ARMENIAN CAPITAL LETTER DA
0535; C; 0565; # ARMENIAN CAPITAL LETTER ECH
0536; C; 0566; # ARMENIAN CAPITAL LETTER ZA
0537; C; 0567; # ARMENIAN CAPITAL LETTER EH
0538; C; 0568; # ARMENIAN CAPITAL LETTER ET
0539; C; 0569; # ARMENIAN CAPITAL LETTER TO
053A; C; 056A; # ARMENIAN CAPITAL LETTER ZHE
053B; C; 056B; # ARMENIAN CAPITAL LETTER INI
053C; C; 056C; # ARMENIAN CAPITAL LETTER LIWN
053D; C; 056D; # ARMENIAN CAPITAL LETTER XEH
053E; C; 056E; # ARMENIAN CAPITAL LETTER CA
053F; C; 056F; # ARMENIAN CAPITAL LETTER KEN
0540; C; 0570; # ARMENIAN CAPITAL LETTER HO
0541; C; 0571; # ARMENIAN CAPITAL LETTER JA
0542; C; 0572; # ARMENIAN CAPITAL LETTER GHAD
0543; C; 0573; # ARMENIAN CAPITAL LETTER CHEH
0544; C; 0574; # ARMENIAN CAPITAL LETTER MEN
0545; C; 0575; # ARMENIAN CAPITAL LETTER YI
0546; C; 0576; # ARMENIAN CAPITAL LETTER NOW
0547; C; 0577; # ARMENIAN CAPITAL LETTER SHA
0548; C; 0578; # ARMENIAN CAPITAL LETTER VO
0549; C; 0579; # ARMENIAN CAPITAL LETTER CHA
054A; C; 057A; # ARMENIAN CAPITAL LETTER PEH
054B; C; 057B; # ARMENIAN CAPITAL LETTER JHEH
054C; C; 057C; # ARMENIAN CAPITAL LETTER RA
054D; C; 057D; # ARMENIAN CAPITAL LETTER SEH
054E; C; 057E; # ARMENIAN CAPITAL LETTER VEW
054F; C; 057F; # ARMENIAN CAPITAL LETTER TIWN
0550; C; 0580; # ARMENIAN CAPITAL LETTER REH
0551; C; 0581; # ARMENIAN CAPITAL LETTER CO
0552; C; 0582; # ARMENIAN CAPITAL LETTER YIWN
0553; C; 0583; # ARMENIAN CAPITAL LETTER PIWR
0554; C; 0584; # ARMENIAN CAPITAL LETTER KEH
0555; C; 0585; # ARMENIAN CAPITAL LETTER OH
0556; C; 0586; # ARMENIAN CAPITAL LETTER FEH
0587; F; 0565 0582; # ARMENIAN SMALL LIGATURE ECH YIWN
10A0; C; 2D00; # GEORGIAN CAPITAL LETTER AN
10A1; C; 2D01; # GEORGIAN CAPITAL LETTER BAN
10A2; C; 2D02; # GEORGIAN CAPITAL LETTER GAN
10A3; C; 2D03; # GEORGIAN CAPITAL LETTER DON
10A4; C; 2D04; # GEORGIAN CAPITAL LETTER EN
10A5; C; 2D05; # GEORGIAN CAPITAL LETTER VIN
10A6; C; 2D06; # GEORGIAN CAPITAL LETTER ZEN
10A7; C; 2D07; # GEORGIAN CAPITAL LETTER TAN
10A8; C; 2D08; # GEORGIAN CAPITAL LETTER IN
10A9; C; 2D09; # GEORGIAN CAPITAL LETTER KAN
10AA; C; 2D0A; # GEORGIAN CAPITAL LETTER LAS
10AB; C; 2D0B; # GEORGIAN CAPITAL LETTER MAN
10AC; C; 2D0C; # GEORGIAN CAPITAL LETTER NAR
10AD; C; 2D0D; # GEORGIAN CAPITAL LETTER ON
10AE; C; 2D0E; # GEORGIAN CAPITAL LETTER PAR
10AF; C; 2D0F; # GEORGIAN CAPITAL LETTER ZHAR
10B0; C; 2D10; # GEORGIAN CAPITAL LETTER RAE
10B1; C; 2D11; # GEORGIAN CAPITAL LETTER SAN
10B2; C; 2D12; # GEORGIAN CAPITAL LETTER TAR
10B3; C; 2D13; # GEORGIAN CAPITAL LETTER UN
10B4; C; 2D14; # GEORGIAN CAPITAL LETTER PHAR
10B5; C; 2D15; # GEORGIAN CAPITAL LETTER KHAR
10B6; C; 2D16; # GEORGIAN CAPITAL LETTER GHAN
10B7; C; 2D17; # GEORGIAN CAPITAL LETTER QAR
10B8; C; 2D18; # GEORGIAN CAPITAL LETTER SHIN
10B9; C; 2D19; # GEORGIAN CAPITAL LETTER CHIN
10BA; C; 2D1A; # GEORGIAN CAPITAL LETTER CAN
10BB; C; 2D1B; # GEORGIAN CAPITAL LETTER JIL
10BC; C; 2D1C; # GEORGIAN CAPITAL LETTER CIL
10BD; C; 2D1D; # GEORGIAN CAPITAL LETTER CHAR
10BE; C; 2D1E; # GEORGIAN CAPITAL LETTER XAN
10BF; C; 2D1F; # GEORGIAN CAPITAL LETTER JHAN
10C0; C; 2D20; # GEORGIAN CAPITAL LETTER HAE
10C1; C; 2D21; # GEORGIAN CAPITAL LETTER HE
10C2; C; 2D22; # GEORGIAN CAPITAL LETTER HIE
10C3; C; 2D23; # GEORGIAN CAPITAL LETTER WE
10C4; C; 2D24; # GEORGIAN CAPITAL LETTER HAR
10C5; C; 2D25; # GEORGIAN CAPITAL LETTER HOE
10C7; C; 2D27; # GEORGIAN CAPITAL LETTER YN
10CD; C; 2D2D; # GEORGIAN CAPITAL LETTER AEN
13F8; C; 13F0; # CHEROKEE SMALL LETTER YE
13F9; C; 13F1; # CHEROKEE SMALL LETTER YI
13FA; C; 13F2; # CHEROKEE SMALL LETTER YO
13FB; C; 13F3; # CHEROKEE SMALL LETTER YU
13FC; C; 13F4; # CHEROKEE SMALL LETTER YV
13FD; C; 13F5; # CHEROKEE SMALL LETTER MV
1C80; C; 0432; # CYRILLIC SMALL LETTER ROUNDED VE
1C81; C; 0434; # CYRILLIC SMALL LETTER LONG-LEGGED DE
1C82; C; 043E; # CYRILLIC SMALL LETTER NARROW O
1C83; C; 0441; # CYRILLIC SMALL LETTER WIDE ES
1C84; C; 0442; # CYRILLIC SMALL LETTER TALL TE
1C85; C; 0442; # CYRILLIC SMALL LETTER THREE-LEGGED TE
1C86; C; 044A; # CYRILLIC SMALL LETTER TALL HARD SIGN
1C87; C; 0463; # CYRILLIC SMALL LETTER TALL YAT
1C88; C; A64B; # CYRILLIC SMALL LETTER UNBLENDED UK
1C89; C; 1C8A; # CYRILLIC CAPITAL LETTER TJE
1C90; C; 10D0; # GEORGIAN MTAVRULI CAPITAL LETTER AN
1C91; C; 10D1; # GEORGIAN MTAVRULI CAPITAL LETTER BAN
1C92; C; 10D2; # GEORGIAN MTAVRULI CAPITAL LETTER GAN
1C93; C; 10D3; # GEORGIAN MTAVRULI CAPITAL LETTER DON
1C94; C; 10D4; # GEORGIAN MTAVRULI CAPITAL LETTER EN
1C95; C; 10D5; # GEORGIAN MTAVRULI CAPITAL LETTER VIN
1C96; C; 10D6; # GEORGIAN MTAVRULI CAPITAL LETTER ZEN
1C97; C; 10D7; # GEORGIAN MTAVRULI CAPITAL LETTER TAN
1C98; C; 10D8; # GEORGIAN MTAVRULI CAPITAL LETTER IN
1C99; C; 10D9; # GEORGIAN MTAVRULI CAPITAL LETTER KAN
1C9A; C; 10DA; # GEORGIAN MTAVRULI CAPITAL LETTER LAS
1C9B; C; 10DB; # GEORGIAN MTAVRULI CAPITAL LETTER MAN
1C9C; C; 10DC; # GEORGIAN MTAVRULI CAPITAL LETTER NAR
1C9D; C; 10DD; # GEORGIAN MTAVRULI CAPITAL LETTER ON
1C9E; C; 10DE; # GEORGIAN MTAVRULI CAPITAL LETTER PAR
1C9F; C; 10DF; # GEORGIAN MTAVRULI CAPITAL LETTER ZHAR
1CA0; C; 10E0; # GEORGIAN MTAVRULI CAPITAL LETTER RAE
1CA1; C; 10E1; # GEORGIAN MTAVRULI CAPITAL LETTER SAN
1CA2; C; 10E2; # GEORGIAN MTAVRULI CAPITAL LETTER TAR
1CA3; C; 10E3; # GEORGIAN MTAVRULI CAPITAL LETTER UN
1CA4; C; 10E4; # GEORGIAN MTAVRULI CAPITAL LETTER PHAR
1CA5; C; 10E5; # GEORGIAN MTAVRULI CAPITAL LETTER KHAR
1CA6; C; 10E6; # GEORGIAN MTAVRULI CAPITAL LETTER GHAN
1CA7; C; 10E7; # GEORGIAN MTAVRULI CAPITAL LETTER QAR
1CA8; C; 10E8; # GEORGIAN MTAVRULI CAPITAL LETTER SHIN
1CA9; C; 10E9; # GEORGIAN MTAVRULI CAPITAL LETTER CHIN
1CAA; C; 10EA; # GEORGIAN MTAVRULI CAPITAL LETTER CAN
1CAB; C; 10EB; # GEORGIAN MTAVRULI CAPITAL LETTER JIL
1CAC; C; 10EC; # GEORGIAN MTAVRULI CAPITAL LETTER CIL
1CAD; C; 10ED; # GEORGIAN MTAVRULI CAPITAL LETTER CHAR
1CAE; C; 10EE; # GEORGIAN MTAVRULI CAPITAL LETTER XAN
1CAF; C; 10EF; # GEORGIAN MTAVRULI CAPITAL LETTER JHAN
1CB0; C; 10F0; # GEORGIAN MTAVRULI CAPITAL LETTER HAE
1CB1; C; 10F1; # GEORGIAN MTAVRULI CAPITAL LETTER HE
1CB2; C; 10F2; # GEORGIAN MTAVRULI CAPITAL LETTER HIE
1CB3; C; 10F3; # GEORGIAN MTAVRULI CAPITAL LETTER WE
1CB4; C; 10F4; # GEORGIAN MTAVRULI CAPITAL LETTER HAR
1CB5; C; 10F5; # GEORGIAN MTAVRULI CAPITAL LETTER HOE
1CB6; C; 10F6; # GEORGIAN MTAVRULI CAPITAL LETTER FI
1CB7; C; 10F7; # GEORGIAN MTAVRULI CAPITAL LETTER YN
1CB8; C; 10F8; # GEORGIAN MTAVRULI CAPITAL LETTER ELIFI
1CB9; C; 10F9; # GEORGIAN MTAVRULI CAPITAL LETTER TURNED GAN
1CBA; C; 10FA; # GEORGIAN MTAVRULI CAPITAL LETTER AIN
1CBD; C; 10FD; # GEORGIAN MTAVRULI CAPITAL LETTER AEN
1CBE; C; 10FE; # GEORGIAN MTAVRULI CAPITAL LETTER HARD SIGN
1CBF; C; 10FF; # GEORGIAN MTAVRULI CAPITAL LETTER LABIAL SIGN
1E00; C; 1E01; # LATIN CAPITAL LETTER A WITH RING BELOW
1E02; C; 1E03; # LATIN CAPITAL LETTER B WITH DOT ABOVE
1E04; C; 1E05; # LATIN CAPITAL LETTER B WITH DOT BELOW
1E06; C; 1E07; # LATIN CAPITAL LETTER B WITH LINE BELOW
1E08; C; 1E09; # LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE
1E0A; C; 1E0B; # LATIN CAPITAL LETTER D WITH DOT ABOVE
1E0C; C; 1E0D; # LATIN CAPITAL LETTER D WITH DOT BELOW
1E0E; C; 1E0F; # LATIN CAPITAL LETTER D WITH LINE BELOW
1E10; C; 1E11; # LATIN CAPITAL LETTER D WITH CEDILLA
1E12; C; 1E13; # LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW
1E14; C; 1E15; # LATIN CAPITAL LETTER E WITH MACRON AND GRAVE
1E16; C; 1E17; # LATIN CAPITAL LETTER E WITH MACRON AND ACUTE
1E18; C; 1E19; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW
1E1A; C; 1E1B; # LATIN CAPITAL LETTER E WITH TILDE BELOW
1E1C; C; 1E1D; # LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE
1E1E; C; 1E1F; # LATIN CAPITAL LETTER F WITH DOT ABOVE
1E20; C; 1E21; # LATIN CAPITAL LETTER G WITH MACRON
1E22; C; 1E23; # LATIN CAPITAL LETTER H WITH DOT ABOVE
1E24; C; 1E25; # LATIN CAPITAL LETTER H WITH DOT BELOW
1E26; C; 1E27; # LATIN CAPITAL LETTER H WITH DIAERESIS
1E28; C; 1E29; # LATIN CAPITAL LETTER H WITH CEDILLA
1E2A; C; 1E2B; # LATIN CAPITAL LETTER H WITH BREVE BELOW
1E2C; C; 1E2D; # LATIN CAPITAL LETTER I WITH TILDE BELOW
1E2E; C; 1E2F; # LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE
1E30; C; 1E31; # LATIN CAPITAL LETTER K WITH ACUTE
1E32; C; 1E33; # LATIN CAPITAL LETTER K WITH DOT BELOW
1E34; C; 1E35; # LATIN CAPITAL LETTER K WITH LINE BELOW
1E36; C; 1E37; # LATIN CAPITAL LETTER L WITH DOT BELOW
1E38; C; 1E39; # LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON
1E3A; C; 1E3B; # LATIN CAPITAL LETTER L WITH LINE BELOW
1E3C; C; 1E3D; # LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW
1E3E; C; 1E3F; # LATIN CAPITAL LETTER M WITH ACUTE
1E40; C; 1E41; # LATIN CAPITAL LETTER M WITH DOT ABOVE
1E42; C; 1E43; # LATIN CAPITAL LETTER M WITH DOT BELOW
1E44; C; 1E45; # LATIN CAPITAL LETTER N WITH DOT ABOVE
1E46; C; 1E47; # LATIN CAPITAL LETTER N WITH DOT BELOW
1E48; C; 1E49; # LATIN CAPITAL LETTER N WITH LINE BELOW
1E4A; C; 1E4B; # LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW
1E4C; C; 1E4D; # LATIN CAPITAL LETTER O WITH TILDE AND ACUTE
1E4E; C; 1E4F; # LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS
1E50; C; 1E51; # LATIN CAPITAL LETTER O WITH MACRON AND GRAVE
1E52; C; 1E53; # LATIN CAPITAL LETTER O WITH MACRON AND ACUTE
1E54; C; 1E55; # LATIN CAPITAL LETTER P WITH ACUTE
1E56; C; 1E57; # LATIN CAPITAL LETTER P WITH DOT ABOVE
1E58; C; 1E59; # LATIN CAPITAL LETTER R WITH DOT ABOVE
1E5A; C; 1E5B; # LATIN CAPITAL LETTER R WITH DOT BELOW
1E5C; C; 1E5D; # LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON
1E5E; C; 1E5F; # LATIN CAPITAL LETTER R WITH LINE BELOW
1E60; C; 1E61; # LATIN CAPITAL LETTER S WITH DOT ABOVE
1E62; C; 1E63; # LATIN CAPITAL LETTER S WITH DOT BELOW
1E64; C; 1E65; # LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE
1E66; C; 1E67; # LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE
1E68; C; 1E69; # LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE
1E6A; C; 1E6B; # LATIN CAPITAL LETTER T WITH DOT ABOVE
1E6C; C; 1E6D; # LATIN CAPITAL LETTER T WITH DOT BELOW
1E6E; C; 1E6F; # LATIN CAPITAL LETTER T WITH LINE BELOW
1E70; C; 1E71; # LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW
1E72; C; 1E73; # LATIN CAPITAL LETTER U WITH DIAERESIS BELOW
1E74; C; 1E75; # LATIN CAPITAL LETTER U WITH TILDE BELOW
1E76; C; 1E77; # LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW
1E78; C; 1E79; # LATIN CAPITAL LETTER U WITH TILDE AND ACUTE
1E7A; C; 1E7B; # LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS
1E7C; C; 1E7D; # LATIN CAPITAL LETTER V WITH TILDE
1E7E; C; 1E7F; # LATIN CAPITAL LETTER V WITH DOT BELOW
1E80; C; 1E81; # LATIN CAPITAL LETTER W WITH GRAVE
1E82; C; 1E83; # LATIN CAPITAL LETTER W WITH ACUTE
1E84; C; 1E85; # LATIN CAPITAL LETTER W WITH DIAERESIS
1E86; C; 1E87; # LATIN CAPITAL LETTER W WITH DOT ABOVE
1E88; C; 1E89; # LATIN CAPITAL LETTER W WITH DOT BELOW
1E8A; C; 1E8B; # LATIN CAPITAL LETTER X WITH DOT ABOVE
1E8C; C; 1E8D; # LATIN CAPITAL LETTER X WITH DIAERESIS
1E8E; C; 1E8F; # LATIN CAPITAL LETTER Y WITH DOT ABOVE
1E90; C; 1E91; # LATIN CAPITAL LETTER Z WITH CIRCUMFLEX
1E92; C; 1E93; # LATIN CAPITAL LETTER Z WITH DOT BELOW
1E94; C; 1E95; # LATIN CAPITAL LETTER Z WITH LINE BELOW
1E96; F; 0068 0331; # LATIN SMALL LETTER H WITH LINE BELOW
1E97; F; 0074 0308; # LATIN SMALL LETTER T WITH DIAERESIS
1E98; F; 0077 030A; # LATIN SMALL LETTER W WITH RING ABOVE
1E99; F; 0079 030A; # LATIN SMALL LETTER Y WITH RING ABOVE
1E9A; F; 0061 02BE; # LATIN SMALL LETTER A WITH RIGHT HALF RING
1E9B; C; 1E61; # LATIN SMALL LETTER LONG S WITH DOT ABOVE
1E9E; F; 0073 0073; # LATIN CAPITAL LETTER SHARP S
1E9E; S; 00DF; # LATIN CAPITAL LETTER SHARP S
1EA0; C; 1EA1; # LATIN CAPITAL LETTER A WITH DOT BELOW
1EA2; C; 1EA3; # LATIN CAPITAL LETTER A WITH HOOK ABOVE
1EA4; C; 1EA5; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE
1EA6; C; 1EA7; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE
1EA8; C; 1EA9; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE
1EAA; C; 1EAB; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE
1EAC; C; 1EAD; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW
1EAE; C; 1EAF; # LATIN CAPITAL LETTER A WITH BREVE AND ACUTE
1EB0; C; 1EB1; # LATIN CAPITAL LETTER A WITH BREVE AND GRAVE
1EB2; C; 1EB3; # LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE
1EB4; C; 1EB5; # LATIN CAPITAL LETTER A WITH BREVE AND TILDE
1EB6; C; 1EB7; # LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW
1EB8; C; 1EB9; # LATIN CAPITAL LETTER E WITH DOT BELOW
1EBA; C; 1EBB; # LATIN CAPITAL LETTER E WITH HOOK ABOVE
1EBC; C; 1EBD; # LATIN CAPITAL LETTER E WITH TILDE
1EBE; C; 1EBF; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE
1EC0; C; 1EC1; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE
1EC2; C; 1EC3; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE
1EC4; C; 1EC5; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE
1EC6; C; 1EC7; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW
1EC8; C; 1EC9; # LATIN CAPITAL LETTER I WITH HOOK ABOVE
1ECA; C; 1ECB; # LATIN CAPITAL LETTER I WITH DOT BELOW
1ECC; C; 1ECD; # LATIN CAPITAL LETTER O WITH DOT BELOW
1ECE; C; 1ECF; # LATIN CAPITAL LETTER O WITH HOOK ABOVE
1ED0; C; 1ED1; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE
1ED2; C; 1ED3; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE
1ED4; C; 1ED5; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE
1ED6; C; 1ED7; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE
1ED8; C; 1ED9; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW
1EDA; C; 1EDB; # LATIN CAPITAL LETTER O WITH HORN AND ACUTE
1EDC; C; 1EDD; # LATIN CAPITAL LETTER O WITH HORN AND GRAVE
1EDE; C; 1EDF; # LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE
1EE0; C; 1EE1; # LATIN CAPITAL LETTER O WITH HORN AND TILDE
1EE2; C; 1EE3; # LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW
1EE4; C; 1EE5; # LATIN CAPITAL LETTER U WITH DOT BELOW
1EE6; C; 1EE7; # LATIN CAPITAL LETTER U WITH HOOK ABOVE
1EE8; C; 1EE9; # LATIN CAPITAL LETTER U WITH HORN AND ACUTE
1EEA; C; 1EEB; # LATIN CAPITAL LETTER U WITH HORN AND GRAVE
1EEC; C; 1EED; # LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE
1EEE; C; 1EEF; # LATIN CAPITAL LETTER U WITH HORN AND TILDE
1EF0; C; 1EF1; # LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW
1EF2; C; 1EF3; # LATIN CAPITAL LETTER Y WITH GRAVE
1EF4; C; 1EF5; # LATIN CAPITAL LETTER Y WITH DOT BELOW
1EF6; C; 1EF7; # LATIN CAPITAL LETTER Y WITH HOOK ABOVE
1EF8; C; 1EF9; # LATIN CAPITAL LETTER Y WITH TILDE
1EFA; C; 1EFB; # LATIN CAPITAL LETTER MIDDLE-WELSH LL
1EFC; C; 1EFD; # LATIN CAPITAL LETTER MIDDLE-WELSH V
1EFE; C; 1EFF; # LATIN CAPITAL LETTER Y WITH LOOP
1F08; C; 1F00; # GREEK CAPITAL LETTER ALPHA WITH PSILI
1F09; C; 1F01; # GREEK CAPITAL LETTER ALPHA WITH DASIA
1F0A; C; 1F02; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA
1F0B; C; 1F03; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA
1F0C; C; 1F04; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA
1F0D; C; 1F05; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA
1F0E; C; 1F06; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI
1F0F; C; 1F07; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI
1F18; C; 1F10; # GREEK CAPITAL LETTER EPSILON WITH PSILI
1F19; C; 1F11; # GREEK CAPITAL LETTER EPSILON WITH DASIA
1F1A; C; 1F12; # GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA
1F1B; C; 1F13; # GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA
1F1C; C; 1F14; # GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA
1F1D; C; 1F15; # GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA
1F28; C; 1F20; # GREEK CAPITAL LETTER ETA WITH PSILI
1F29; C; 1F21; # GREEK CAPITAL LETTER ETA WITH DASIA
1F2A; C; 1F22; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA
1F2B; C; 1F23; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA
1F2C; C; 1F24; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA
1F2D; C; 1F25; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA
1F2E; C; 1F26; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI
1F2F; C; 1F27; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI
1F38; C; 1F30; # GREEK CAPITAL LETTER IOTA WITH PSILI
1F39; C; 1F31; # GREEK CAPITAL LETTER IOTA WITH DASIA
1F3A; C; 1F32; # GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA
1F3B; C; 1F33; # GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA
1F3C; C; 1F34; # GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA
1F3D; C; 1F35; # GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA
1F3E; C; 1F36; # GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI
1F3F; C; 1F37; # GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI
1F48; C; 1F40; # GREEK CAPITAL LETTER OMICRON WITH PSILI
1F49; C; 1F41; # GREEK CAPITAL LETTER OMICRON WITH DASIA
1F4A; C; 1F42; # GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA
1F4B; C; 1F43; # GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA
1F4C; C; 1F44; # GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA
1F4D; C; 1F45; # GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA
1F50; F; 03C5 0313; # GREEK SMALL LETTER UPSILON WITH PSILI
1F52; F; 03C5 0313 0300; # GREEK SMALL LETTER UPSILON WITH PSILI AND VARIA
1F54; F; 03C5 0313 0301; # GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA
1F56; F; 03C5 0313 0342; # GREEK SMALL LETTER UPSILON WITH PSILI AND PERISPOMENI
1F59; C; 1F51; # GREEK CAPITAL LETTER UPSILON WITH DASIA
1F5B; C; 1F53; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA
1F5D; C; 1F55; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA
1F5F; C; 1F57; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI
1F68; C; 1F60; # GREEK CAPITAL LETTER OMEGA WITH PSILI
1F69; C; 1F61; # GREEK CAPITAL LETTER OMEGA WITH DASIA
1F6A; C; 1F62; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA
1F6B; C; 1F63; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA
1F6C; C; 1F64; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA
1F6D; C; 1F65; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA
1F6E; C; 1F66; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI
1F6F; C; 1F67; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI
1F80; F; 1F00 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI
1F81; F; 1F01 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI
1F82; F; 1F02 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI
1F83; F; 1F03 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI
1F84; F; 1F04 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI
1F85; F; 1F05 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI
1F86; F; 1F06 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI
1F87; F; 1F07 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI
1F88; F; 1F00 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI
1F88; S; 1F80; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI
1F89; F; 1F01 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI
1F89; S; 1F81; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI
1F8A; F; 1F02 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1F8A; S; 1F82; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1F8B; F; 1F03 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1F8B; S; 1F83; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1F8C; F; 1F04 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1F8C; S; 1F84; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1F8D; F; 1F05 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1F8D; S; 1F85; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1F8E; F; 1F06 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1F8E; S; 1F86; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1F8F; F; 1F07 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1F8F; S; 1F87; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1F90; F; 1F20 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI
1F91; F; 1F21 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI
1F92; F; 1F22 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI
1F93; F; 1F23 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI
1F94; F; 1F24 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI
1F95; F; 1F25 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI
1F96; F; 1F26 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI
1F97; F; 1F27 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI
1F98; F; 1F20 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI
1F98; S; 1F90; # GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI
1F99; F; 1F21 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI
1F99; S; 1F91; # GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI
1F9A; F; 1F22 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1F9A; S; 1F92; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1F9B; F; 1F23 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1F9B; S; 1F93; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1F9C; F; 1F24 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1F9C; S; 1F94; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1F9D; F; 1F25 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1F9D; S; 1F95; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1F9E; F; 1F26 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1F9E; S; 1F96; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1F9F; F; 1F27 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1F9F; S; 1F97; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1FA0; F; 1F60 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI
1FA1; F; 1F61 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI
1FA2; F; 1F62 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI
1FA3; F; 1F63 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI
1FA4; F; 1F64 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI
1FA5; F; 1F65 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI
1FA6; F; 1F66 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI
1FA7; F; 1F67 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI
1FA8; F; 1F60 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI
1FA8; S; 1FA0; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI
1FA9; F; 1F61 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI
1FA9; S; 1FA1; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI
1FAA; F; 1F62 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1FAA; S; 1FA2; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI
1FAB; F; 1F63 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1FAB; S; 1FA3; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI
1FAC; F; 1F64 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1FAC; S; 1FA4; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI
1FAD; F; 1F65 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1FAD; S; 1FA5; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI
1FAE; F; 1F66 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1FAE; S; 1FA6; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI
1FAF; F; 1F67 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1FAF; S; 1FA7; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI
1FB2; F; 1F70 03B9; # GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI
1FB3; F; 03B1 03B9; # GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI
1FB4; F; 03AC 03B9; # GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI
1FB6; F; 03B1 0342; # GREEK SMALL LETTER ALPHA WITH PERISPOMENI
1FB7; F; 03B1 0342 03B9; # GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI
1FB8; C; 1FB0; # GREEK CAPITAL LETTER ALPHA WITH VRACHY
1FB9; C; 1FB1; # GREEK CAPITAL LETTER ALPHA WITH MACRON
1FBA; C; 1F70; # GREEK CAPITAL LETTER ALPHA WITH VARIA
1FBB; C; 1F71; # GREEK CAPITAL LETTER ALPHA WITH OXIA
1FBC; F; 03B1 03B9; # GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI
1FBC; S; 1FB3; # GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI
1FBE; C; 03B9; # GREEK PROSGEGRAMMENI
1FC2; F; 1F74 03B9; # GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI
1FC3; F; 03B7 03B9; # GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI
1FC4; F; 03AE 03B9; # GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI
1FC6; F; 03B7 0342; # GREEK SMALL LETTER ETA WITH PERISPOMENI
1FC7; F; 03B7 0342 03B9; # GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI
1FC8; C; 1F72; # GREEK CAPITAL LETTER EPSILON WITH VARIA
1FC9; C; 1F73; # GREEK CAPITAL LETTER EPSILON WITH OXIA
1FCA; C; 1F74; # GREEK CAPITAL LETTER ETA WITH VARIA
1FCB; C; 1F75; # GREEK CAPITAL LETTER ETA WITH OXIA
1FCC; F; 03B7 03B9; # GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI
1FCC; S; 1FC3; # GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI
1FD2; F; 03B9 0308 0300; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA
1FD3; F; 03B9 0308 0301; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA
1FD3; S; 0390; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA
1FD6; F; 03B9 0342; # GREEK SMALL LETTER IOTA WITH PERISPOMENI
1FD7; F; 03B9 0308 0342; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI
1FD8; C; 1FD0; # GREEK CAPITAL LETTER IOTA WITH VRACHY
1FD9; C; 1FD1; # GREEK CAPITAL LETTER IOTA WITH MACRON
1FDA; C; 1F76; # GREEK CAPITAL LETTER IOTA WITH VARIA
1FDB; C; 1F77; # GREEK CAPITAL LETTER IOTA WITH OXIA
1FE2; F; 03C5 0308 0300; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA
1FE3; F; 03C5 0308 0301; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA
1FE3; S; 03B0; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA
1FE4; F; 03C1 0313; # GREEK SMALL LETTER RHO WITH PSILI
1FE6; F; 03C5 0342; # GREEK SMALL LETTER UPSILON WITH PERISPOMENI
1FE7; F; 03C5 0308 0342; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI
1FE8; C; 1FE0; # GREEK CAPITAL LETTER UPSILON WITH VRACHY
1FE9; C; 1FE1; # GREEK CAPITAL LETTER UPSILON WITH MACRON
1FEA; C; 1F7A; # GREEK CAPITAL LETTER UPSILON WITH VARIA
1FEB; C; 1F7B; # GREEK CAPITAL LETTER UPSILON WITH OXIA
1FEC; C; 1FE5; # GREEK CAPITAL LETTER RHO WITH DASIA
1FF2; F; 1F7C 03B9; # GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI
1FF3; F; 03C9 03B9; # GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI
1FF4; F; 03CE 03B9; # GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI
1FF6; F; 03C9 0342; # GREEK SMALL LETTER OMEGA WITH PERISPOMENI
1FF7; F; 03C9 0342 03B9; # GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI
1FF8; C; 1F78; # GREEK CAPITAL LETTER OMICRON WITH VARIA
1FF9; C; 1F79; # GREEK CAPITAL LETTER OMICRON WITH OXIA
1FFA; C; 1F7C; # GREEK CAPITAL LETTER OMEGA WITH VARIA
1FFB; C; 1F7D; # GREEK CAPITAL LETTER OMEGA WITH OXIA
1FFC; F; 03C9 03B9; # GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI
1FFC; S; 1FF3; # GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI
2126; C; 03C9; # OHM SIGN
212A; C; 006B; # KELVIN SIGN
212B; C; 00E5; # ANGSTROM SIGN
2132; C; 214E; # TURNED CAPITAL F
2160; C; 2170; # ROMAN NUMERAL ONE
2161; C; 2171; # ROMAN NUMERAL TWO
2162; C; 2172; # ROMAN NUMERAL THREE
2163; C; 2173; # ROMAN NUMERAL FOUR
2164; C; 2174; # ROMAN NUMERAL FIVE
2165; C; 2175; # ROMAN NUMERAL SIX
2166; C; 2176; # ROMAN NUMERAL SEVEN
2167; C; 2177; # ROMAN NUMERAL EIGHT
2168; C; 2178; # ROMAN NUMERAL NINE
2169; C; 2179; # ROMAN NUMERAL TEN
216A; C; 217A; # ROMAN NUMERAL ELEVEN
216B; C; 217B; # ROMAN NUMERAL TWELVE
216C; C; 217C; # ROMAN NUMERAL FIFTY
216D; C; 217D; # ROMAN NUMERAL ONE HUNDRED
216E; C; 217E; # ROMAN NUMERAL FIVE HUNDRED
216F; C; 217F; # ROMAN NUMERAL ONE THOUSAND
2183; C; 2184; # ROMAN NUMERAL REVERSED ONE HUNDRED
24B6; C; 24D0; # CIRCLED LATIN CAPITAL LETTER A
24B7; C; 24D1; # CIRCLED LATIN CAPITAL LETTER B
24B8; C; 24D2; # CIRCLED LATIN CAPITAL LETTER C
24B9; C; 24D3; # CIRCLED LATIN CAPITAL LETTER D
24BA; C; 24D4; # CIRCLED LATIN CAPITAL LETTER E
24BB; C; 24D5; # CIRCLED LATIN CAPITAL LETTER F
24BC; C; 24D6; # CIRCLED LATIN CAPITAL LETTER G
24BD; C; 24D7; # CIRCLED LATIN CAPITAL LETTER H
24BE; C; 24D8; # CIRCLED LATIN CAPITAL LETTER I
24BF; C; 24D9; # CIRCLED LATIN CAPITAL LETTER J
24C0; C; 24DA; # CIRCLED LATIN CAPITAL LETTER K
24C1; C; 24DB; # CIRCLED LATIN CAPITAL LETTER L
24C2; C; 24DC; # CIRCLED LATIN CAPITAL LETTER M
24C3; C; 24DD; # CIRCLED LATIN CAPITAL LETTER N
24C4; C; 24DE; # CIRCLED LATIN CAPITAL LETTER O
24C5; C; 24DF; # CIRCLED LATIN CAPITAL LETTER P
24C6; C; 24E0; # CIRCLED LATIN CAPITAL LETTER Q
24C7; C; 24E1; # CIRCLED LATIN CAPITAL LETTER R
24C8; C; 24E2; # CIRCLED LATIN CAPITAL LETTER S
24C9; C; 24E3; # CIRCLED LATIN CAPITAL LETTER T
24CA; C; 24E4; # CIRCLED LATIN CAPITAL LETTER U
24CB; C; 24E5; # CIRCLED LATIN CAPITAL LETTER V
24CC; C; 24E6; # CIRCLED LATIN CAPITAL LETTER W
24CD; C; 24E7; # CIRCLED LATIN CAPITAL LETTER X
24CE; C; 24E8; # CIRCLED LATIN CAPITAL LETTER Y
24CF; C; 24E9; # CIRCLED LATIN CAPITAL LETTER Z
2C00; C; 2C30; # GLAGOLITIC CAPITAL LETTER AZU
2C01; C; 2C31; # GLAGOLITIC CAPITAL LETTER BUKY
2C02; C; 2C32; # GLAGOLITIC CAPITAL LETTER VEDE
2C03; C; 2C33; # GLAGOLITIC CAPITAL LETTER GLAGOLI
2C04; C; 2C34; # GLAGOLITIC CAPITAL LETTER DOBRO
2C05; C; 2C35; # GLAGOLITIC CAPITAL LETTER YESTU
2C06; C; 2C36; # GLAGOLITIC CAPITAL LETTER ZHIVETE
2C07; C; 2C37; # GLAGOLITIC CAPITAL LETTER DZELO
2C08; C; 2C38; # GLAGOLITIC CAPITAL LETTER ZEMLJA
2C09; C; 2C39; # GLAGOLITIC CAPITAL LETTER IZHE
2C0A; C; 2C3A; # GLAGOLITIC CAPITAL LETTER INITIAL IZHE
2C0B; C; 2C3B; # GLAGOLITIC CAPITAL LETTER I
2C0C; C; 2C3C; # GLAGOLITIC CAPITAL LETTER DJERVI
2C0D; C; 2C3D; # GLAGOLITIC CAPITAL LETTER KAKO
2C0E; C; 2C3E; # GLAGOLITIC CAPITAL LETTER LJUDIJE
2C0F; C; 2C3F; # GLAGOLITIC CAPITAL LETTER MYSLITE
2C10; C; 2C40; # GLAGOLITIC CAPITAL LETTER NASHI
2C11; C; 2C41; # GLAGOLITIC CAPITAL LETTER ONU
2C12; C; 2C42; # GLAGOLITIC CAPITAL LETTER POKOJI
2C13; C; 2C43; # GLAGOLITIC CAPITAL LETTER RITSI
2C14; C; 2C44; # GLAGOLITIC CAPITAL LETTER SLOVO
2C15; C; 2C45; # GLAGOLITIC CAPITAL LETTER TVRIDO
2C16; C; 2C46; # GLAGOLITIC CAPITAL LETTER UKU
2C17; C; 2C47; # GLAGOLITIC CAPITAL LETTER FRITU
2C18; C; 2C48; # GLAGOLITIC CAPITAL LETTER HERU
2C19; C; 2C49; # GLAGOLITIC CAPITAL LETTER OTU
2C1A; C; 2C4A; # GLAGOLITIC CAPITAL LETTER PE
2C1B; C; 2C4B; # GLAGOLITIC CAPITAL LETTER SHTA
2C1C; C; 2C4C; # GLAGOLITIC CAPITAL LETTER TSI
2C1D; C; 2C4D; # GLAGOLITIC CAPITAL LETTER CHRIVI
2C1E; C; 2C4E; # GLAGOLITIC CAPITAL LETTER SHA
2C1F; C; 2C4F; # GLAGOLITIC CAPITAL LETTER YERU
2C20; C; 2C50; # GLAGOLITIC CAPITAL LETTER YERI
2C21; C; 2C51; # GLAGOLITIC CAPITAL LETTER YATI
2C22; C; 2C52; # GLAGOLITIC CAPITAL LETTER SPIDERY HA
2C23; C; 2C53; # GLAGOLITIC CAPITAL LETTER YU
2C24; C; 2C54; # GLAGOLITIC CAPITAL LETTER SMALL YUS
2C25; C; 2C55; # GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL
2C26; C; 2C56; # GLAGOLITIC CAPITAL LETTER YO
2C27; C; 2C57; # GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS
2C28; C; 2C58; # GLAGOLITIC CAPITAL LETTER BIG YUS
2C29; C; 2C59; # GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS
2C2A; C; 2C5A; # GLAGOLITIC CAPITAL LETTER FITA
2C2B; C; 2C5B; # GLAGOLITIC CAPITAL LETTER IZHITSA
2C2C; C; 2C5C; # GLAGOLITIC CAPITAL LETTER SHTAPIC
2C2D; C; 2C5D; # GLAGOLITIC CAPITAL LETTER TROKUTASTI A
2C2E; C; 2C5E; # GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE
2C2F; C; 2C5F; # GLAGOLITIC CAPITAL LETTER CAUDATE CHRIVI
2C60; C; 2C61; # LATIN CAPITAL LETTER L WITH DOUBLE BAR
2C62; C; 026B; # LATIN CAPITAL LETTER L WITH MIDDLE TILDE
2C63; C; 1D7D; # LATIN CAPITAL LETTER P WITH STROKE
2C64; C; 027D; # LATIN CAPITAL LETTER R WITH TAIL
2C67; C; 2C68; # LATIN CAPITAL LETTER H WITH DESCENDER
2C69; C; 2C6A; # LATIN CAPITAL LETTER K WITH DESCENDER
2C6B; C; 2C6C; # LATIN CAPITAL LETTER Z WITH DESCENDER
2C6D; C; 0251; # LATIN CAPITAL LETTER ALPHA
2C6E; C; 0271; # LATIN CAPITAL LETTER M WITH HOOK
2C6F; C; 0250; # LATIN CAPITAL LETTER TURNED A
2C70; C; 0252; # LATIN CAPITAL LETTER TURNED ALPHA
2C72; C; 2C73; # LATIN CAPITAL LETTER W WITH HOOK
2C75; C; 2C76; # LATIN CAPITAL LETTER HALF H
2C7E; C; 023F; # LATIN CAPITAL LETTER S WITH SWASH TAIL
2C7F; C; 0240; # LATIN CAPITAL LETTER Z WITH SWASH TAIL
2C80; C; 2C81; # COPTIC CAPITAL LETTER ALFA
2C82; C; 2C83; # COPTIC CAPITAL LETTER VIDA
2C84; C; 2C85; # COPTIC CAPITAL LETTER GAMMA
2C86; C; 2C87; # COPTIC CAPITAL LETTER DALDA
2C88; C; 2C89; # COPTIC CAPITAL LETTER EIE
2C8A; C; 2C8B; # COPTIC CAPITAL LETTER SOU
2C8C; C; 2C8D; # COPTIC CAPITAL LETTER ZATA
2C8E; C; 2C8F; # COPTIC CAPITAL LETTER HATE
2C90; C; 2C91; # COPTIC CAPITAL LETTER THETHE
2C92; C; 2C93; # COPTIC CAPITAL LETTER IAUDA
2C94; C; 2C95; # COPTIC CAPITAL LETTER KAPA
2C96; C; 2C97; # COPTIC CAPITAL LETTER LAULA
2C98; C; 2C99; # COPTIC CAPITAL LETTER MI
2C9A; C; 2C9B; # COPTIC CAPITAL LETTER NI
2C9C; C; 2C9D; # COPTIC CAPITAL LETTER KSI
2C9E; C; 2C9F; # COPTIC CAPITAL LETTER O
2CA0; C; 2CA1; # COPTIC CAPITAL LETTER PI
2CA2; C; 2CA3; # COPTIC CAPITAL LETTER RO
2CA4; C; 2CA5; # COPTIC CAPITAL LETTER SIMA
2CA6; C; 2CA7; # COPTIC CAPITAL LETTER TAU
2CA8; C; 2CA9; # COPTIC CAPITAL LETTER UA
2CAA; C; 2CAB; # COPTIC CAPITAL LETTER FI
2CAC; C; 2CAD; # COPTIC CAPITAL LETTER KHI
2CAE; C; 2CAF; # COPTIC CAPITAL LETTER PSI
2CB0; C; 2CB1; # COPTIC CAPITAL LETTER OOU
2CB2; C; 2CB3; # COPTIC CAPITAL LETTER DIALECT-P ALEF
2CB4; C; 2CB5; # COPTIC CAPITAL LETTER OLD COPTIC AIN
2CB6; C; 2CB7; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE
2CB8; C; 2CB9; # COPTIC CAPITAL LETTER DIALECT-P KAPA
2CBA; C; 2CBB; # COPTIC CAPITAL LETTER DIALECT-P NI
2CBC; C; 2CBD; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI
2CBE; C; 2CBF; # COPTIC CAPITAL LETTER OLD COPTIC OOU
2CC0; C; 2CC1; # COPTIC CAPITAL LETTER SAMPI
2CC2; C; 2CC3; # COPTIC CAPITAL LETTER CROSSED SHEI
2CC4; C; 2CC5; # COPTIC CAPITAL LETTER OLD COPTIC SHEI
2CC6; C; 2CC7; # COPTIC CAPITAL LETTER OLD COPTIC ESH
2CC8; C; 2CC9; # COPTIC CAPITAL LETTER AKHMIMIC KHEI
2CCA; C; 2CCB; # COPTIC CAPITAL LETTER DIALECT-P HORI
2CCC; C; 2CCD; # COPTIC CAPITAL LETTER OLD COPTIC HORI
2CCE; C; 2CCF; # COPTIC CAPITAL LETTER OLD COPTIC HA
2CD0; C; 2CD1; # COPTIC CAPITAL LETTER L-SHAPED HA
2CD2; C; 2CD3; # COPTIC CAPITAL LETTER OLD COPTIC HEI
2CD4; C; 2CD5; # COPTIC CAPITAL LETTER OLD COPTIC HAT
2CD6; C; 2CD7; # COPTIC CAPITAL LETTER OLD COPTIC GANGIA
2CD8; C; 2CD9; # COPTIC CAPITAL LETTER OLD COPTIC DJA
2CDA; C; 2CDB; # COPTIC CAPITAL LETTER OLD COPTIC SHIMA
2CDC; C; 2CDD; # COPTIC CAPITAL LETTER OLD NUBIAN SHIMA
2CDE; C; 2CDF; # COPTIC CAPITAL LETTER OLD NUBIAN NGI
2CE0; C; 2CE1; # COPTIC CAPITAL LETTER OLD NUBIAN NYI
2CE2; C; 2CE3; # COPTIC CAPITAL LETTER OLD NUBIAN WAU
2CEB; C; 2CEC; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI
2CED; C; 2CEE; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC GANGIA
2CF2; C; 2CF3; # COPTIC CAPITAL LETTER BOHAIRIC KHEI
A640; C; A641; # CYRILLIC CAPITAL LETTER ZEMLYA
A642; C; A643; # CYRILLIC CAPITAL LETTER DZELO
A644; C; A645; # CYRILLIC CAPITAL LETTER REVERSED DZE
A646; C; A647; # CYRILLIC CAPITAL LETTER IOTA
A648; C; A649; # CYRILLIC CAPITAL LETTER DJERV
A64A; C; A64B; # CYRILLIC CAPITAL LETTER MONOGRAPH UK
A64C; C; A64D; # CYRILLIC CAPITAL LETTER BROAD OMEGA
A64E; C; A64F; # CYRILLIC CAPITAL LETTER NEUTRAL YER
A650; C; A651; # CYRILLIC CAPITAL LETTER YERU WITH BACK YER
A652; C; A653; # CYRILLIC CAPITAL LETTER IOTIFIED YAT
A654; C; A655; # CYRILLIC CAPITAL LETTER REVERSED YU
A656; C; A657; # CYRILLIC CAPITAL LETTER IOTIFIED A
A658; C; A659; # CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS
A65A; C; A65B; # CYRILLIC CAPITAL LETTER BLENDED YUS
A65C; C; A65D; # CYRILLIC CAPITAL LETTER IOTIFIED CLOSED LITTLE YUS
A65E; C; A65F; # CYRILLIC CAPITAL LETTER YN
A660; C; A661; # CYRILLIC CAPITAL LETTER REVERSED TSE
A662; C; A663; # CYRILLIC CAPITAL LETTER SOFT DE
A664; C; A665; # CYRILLIC CAPITAL LETTER SOFT EL
A666; C; A667; # CYRILLIC CAPITAL LETTER SOFT EM
A668; C; A669; # CYRILLIC CAPITAL LETTER MONOCULAR O
A66A; C; A66B; # CYRILLIC CAPITAL LETTER BINOCULAR O
A66C; C; A66D; # CYRILLIC CAPITAL LETTER DOUBLE MONOCULAR O
A680; C; A681; # CYRILLIC CAPITAL LETTER DWE
A682; C; A683; # CYRILLIC CAPITAL LETTER DZWE
A684; C; A685; # CYRILLIC CAPITAL LETTER ZHWE
A686; C; A687; # CYRILLIC CAPITAL LETTER CCHE
A688; C; A689; # CYRILLIC CAPITAL LETTER DZZE
A68A; C; A68B; # CYRILLIC CAPITAL LETTER TE WITH MIDDLE HOOK
A68C; C; A68D; # CYRILLIC CAPITAL LETTER TWE
A68E; C; A68F; # CYRILLIC CAPITAL LETTER TSWE
A690; C; A691; # CYRILLIC CAPITAL LETTER TSSE
A692; C; A693; # CYRILLIC CAPITAL LETTER TCHE
A694; C; A695; # CYRILLIC CAPITAL LETTER HWE
A696; C; A697; # CYRILLIC CAPITAL LETTER SHWE
A698; C; A699; # CYRILLIC CAPITAL LETTER DOUBLE O
A69A; C; A69B; # CYRILLIC CAPITAL LETTER CROSSED O
A722; C; A723; # LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF
A724; C; A725; # LATIN CAPITAL LETTER EGYPTOLOGICAL AIN
A726; C; A727; # LATIN CAPITAL LETTER HENG
A728; C; A729; # LATIN CAPITAL LETTER TZ
A72A; C; A72B; # LATIN CAPITAL LETTER TRESILLO
A72C; C; A72D; # LATIN CAPITAL LETTER CUATRILLO
A72E; C; A72F; # LATIN CAPITAL LETTER CUATRILLO WITH COMMA
A732; C; A733; # LATIN CAPITAL LETTER AA
A734; C; A735; # LATIN CAPITAL LETTER AO
A736; C; A737; # LATIN CAPITAL LETTER AU
A738; C; A739; # LATIN CAPITAL LETTER AV
A73A; C; A73B; # LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR
A73C; C; A73D; # LATIN CAPITAL LETTER AY
A73E; C; A73F; # LATIN CAPITAL LETTER REVERSED C WITH DOT
A740; C; A741; # LATIN CAPITAL LETTER K WITH STROKE
A742; C; A743; # LATIN CAPITAL LETTER K WITH DIAGONAL STROKE
A744; C; A745; # LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE
A746; C; A747; # LATIN CAPITAL LETTER BROKEN L
A748; C; A749; # LATIN CAPITAL LETTER L WITH HIGH STROKE
A74A; C; A74B; # LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY
A74C; C; A74D; # LATIN CAPITAL LETTER O WITH LOOP
A74E; C; A74F; # LATIN CAPITAL LETTER OO
A750; C; A751; # LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER
A752; C; A753; # LATIN CAPITAL LETTER P WITH FLOURISH
A754; C; A755; # LATIN CAPITAL LETTER P WITH SQUIRREL TAIL
A756; C; A757; # LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER
A758; C; A759; # LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE
A75A; C; A75B; # LATIN CAPITAL LETTER R ROTUNDA
A75C; C; A75D; # LATIN CAPITAL LETTER RUM ROTUNDA
A75E; C; A75F; # LATIN CAPITAL LETTER V WITH DIAGONAL STROKE
A760; C; A761; # LATIN CAPITAL LETTER VY
A762; C; A763; # LATIN CAPITAL LETTER VISIGOTHIC Z
A764; C; A765; # LATIN CAPITAL LETTER THORN WITH STROKE
A766; C; A767; # LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER
A768; C; A769; # LATIN CAPITAL LETTER VEND
A76A; C; A76B; # LATIN CAPITAL LETTER ET
A76C; C; A76D; # LATIN CAPITAL LETTER IS
A76E; C; A76F; # LATIN CAPITAL LETTER CON
A779; C; A77A; # LATIN CAPITAL LETTER INSULAR D
A77B; C; A77C; # LATIN CAPITAL LETTER INSULAR F
A77D; C; 1D79; # LATIN CAPITAL LETTER INSULAR G
A77E; C; A77F; # LATIN CAPITAL LETTER TURNED INSULAR G
A780; C; A781; # LATIN CAPITAL LETTER TURNED L
A782; C; A783; # LATIN CAPITAL LETTER INSULAR R
A784; C; A785; # LATIN CAPITAL LETTER INSULAR S
A786; C; A787; # LATIN CAPITAL LETTER INSULAR T
A78B; C; A78C; # LATIN CAPITAL LETTER SALTILLO
A78D; C; 0265; # LATIN CAPITAL LETTER TURNED H
A790; C; A791; # LATIN CAPITAL LETTER N WITH DESCENDER
A792; C; A793; # LATIN CAPITAL LETTER C WITH BAR
A796; C; A797; # LATIN CAPITAL LETTER B WITH FLOURISH
A798; C; A799; # LATIN CAPITAL LETTER F WITH STROKE
A79A; C; A79B; # LATIN CAPITAL LETTER VOLAPUK AE
A79C; C; A79D; # LATIN CAPITAL LETTER VOLAPUK OE
A79E; C; A79F; # LATIN CAPITAL LETTER VOLAPUK UE
A7A0; C; A7A1; # LATIN CAPITAL LETTER G WITH OBLIQUE STROKE
A7A2; C; A7A3; # LATIN CAPITAL LETTER K WITH OBLIQUE STROKE
A7A4; C; A7A5; # LATIN CAPITAL LETTER N WITH OBLIQUE STROKE
A7A6; C; A7A7; # LATIN CAPITAL LETTER R WITH OBLIQUE STROKE
A7A8; C; A7A9; # LATIN CAPITAL LETTER S WITH OBLIQUE STROKE
A7AA; C; 0266; # LATIN CAPITAL LETTER H WITH HOOK
A7AB; C; 025C; # LATIN CAPITAL LETTER REVERSED OPEN E
A7AC; C; 0261; # LATIN CAPITAL LETTER SCRIPT G
A7AD; C; 026C; # LATIN CAPITAL LETTER L WITH BELT
A7AE; C; 026A; # LATIN CAPITAL LETTER SMALL CAPITAL I
A7B0; C; 029E; # LATIN CAPITAL LETTER TURNED K
A7B1; C; 0287; # LATIN CAPITAL LETTER TURNED T
A7B2; C; 029D; # LATIN CAPITAL LETTER J WITH CROSSED-TAIL
A7B3; C; AB53; # LATIN CAPITAL LETTER CHI
A7B4; C; A7B5; # LATIN CAPITAL LETTER BETA
A7B6; C; A7B7; # LATIN CAPITAL LETTER OMEGA
A7B8; C; A7B9; # LATIN CAPITAL LETTER U WITH STROKE
A7BA; C; A7BB; # LATIN CAPITAL LETTER GLOTTAL A
A7BC; C; A7BD; # LATIN CAPITAL LETTER GLOTTAL I
A7BE; C; A7BF; # LATIN CAPITAL LETTER GLOTTAL U
A7C0; C; A7C1; # LATIN CAPITAL LETTER OLD POLISH O
A7C2; C; A7C3; # LATIN CAPITAL LETTER ANGLICANA W
A7C4; C; A794; # LATIN CAPITAL LETTER C WITH PALATAL HOOK
A7C5; C; 0282; # LATIN CAPITAL LETTER S WITH HOOK
A7C6; C; 1D8E; # LATIN CAPITAL LETTER Z WITH PALATAL HOOK
A7C7; C; A7C8; # LATIN CAPITAL LETTER D WITH SHORT STROKE OVERLAY
A7C9; C; A7CA; # LATIN CAPITAL LETTER S WITH SHORT STROKE OVERLAY
A7CB; C; 0264; # LATIN CAPITAL LETTER RAMS HORN
A7CC; C; A7CD; # LATIN CAPITAL LETTER S WITH DIAGONAL STROKE
A7CE; C; A7CF; # LATIN CAPITAL LETTER PHARYNGEAL VOICED FRICATIVE
A7D0; C; A7D1; # LATIN CAPITAL LETTER CLOSED INSULAR G
A7D2; C; A7D3; # LATIN CAPITAL LETTER DOUBLE THORN
A7D4; C; A7D5; # LATIN CAPITAL LETTER DOUBLE WYNN
A7D6; C; A7D7; # LATIN CAPITAL LETTER MIDDLE SCOTS S
A7D8; C; A7D9; # LATIN CAPITAL LETTER SIGMOID S
A7DA; C; A7DB; # LATIN CAPITAL LETTER LAMBDA
A7DC; C; 019B; # LATIN CAPITAL LETTER LAMBDA WITH STROKE
A7F5; C; A7F6; # LATIN CAPITAL LETTER REVERSED HALF H
AB70; C; 13A0; # CHEROKEE SMALL LETTER A
AB71; C; 13A1; # CHEROKEE SMALL LETTER E
AB72; C; 13A2; # CHEROKEE SMALL LETTER I
AB73; C; 13A3; # CHEROKEE SMALL LETTER O
AB74; C; 13A4; # CHEROKEE SMALL LETTER U
AB75; C; 13A5; # CHEROKEE SMALL LETTER V
AB76; C; 13A6; # CHEROKEE SMALL LETTER GA
AB77; C; 13A7; # CHEROKEE SMALL LETTER KA
AB78; C; 13A8; # CHEROKEE SMALL LETTER GE
AB79; C; 13A9; # CHEROKEE SMALL LETTER GI
AB7A; C; 13AA; # CHEROKEE SMALL LETTER GO
AB7B; C; 13AB; # CHEROKEE SMALL LETTER GU
AB7C; C; 13AC; # CHEROKEE SMALL LETTER GV
AB7D; C; 13AD; # CHEROKEE SMALL LETTER HA
AB7E; C; 13AE; # CHEROKEE SMALL LETTER HE
AB7F; C; 13AF; # CHEROKEE SMALL LETTER HI
AB80; C; 13B0; # CHEROKEE SMALL LETTER HO
AB81; C; 13B1; # CHEROKEE SMALL LETTER HU
AB82; C; 13B2; # CHEROKEE SMALL LETTER HV
AB83; C; 13B3; # CHEROKEE SMALL LETTER LA
AB84; C; 13B4; # CHEROKEE SMALL LETTER LE
AB85; C; 13B5; # CHEROKEE SMALL LETTER LI
AB86; C; 13B6; # CHEROKEE SMALL LETTER LO
AB87; C; 13B7; # CHEROKEE SMALL LETTER LU
AB88; C; 13B8; # CHEROKEE SMALL LETTER LV
AB89; C; 13B9; # CHEROKEE SMALL LETTER MA
AB8A; C; 13BA; # CHEROKEE SMALL LETTER ME
AB8B; C; 13BB; # CHEROKEE SMALL LETTER MI
AB8C; C; 13BC; # CHEROKEE SMALL LETTER MO
AB8D; C; 13BD; # CHEROKEE SMALL LETTER MU
AB8E; C; 13BE; # CHEROKEE SMALL LETTER NA
AB8F; C; 13BF; # CHEROKEE SMALL LETTER HNA
AB90; C; 13C0; # CHEROKEE SMALL LETTER NAH
AB91; C; 13C1; # CHEROKEE SMALL LETTER NE
AB92; C; 13C2; # CHEROKEE SMALL LETTER NI
AB93; C; 13C3; # CHEROKEE SMALL LETTER NO
AB94; C; 13C4; # CHEROKEE SMALL LETTER NU
AB95; C; 13C5; # CHEROKEE SMALL LETTER NV
AB96; C; 13C6; # CHEROKEE SMALL LETTER QUA
AB97; C; 13C7; # CHEROKEE SMALL LETTER QUE
AB98; C; 13C8; # CHEROKEE SMALL LETTER QUI
AB99; C; 13C9; # CHEROKEE SMALL LETTER QUO
AB9A; C; 13CA; # CHEROKEE SMALL LETTER QUU
AB9B; C; 13CB; # CHEROKEE SMALL LETTER QUV
AB9C; C; 13CC; # CHEROKEE SMALL LETTER SA
AB9D; C; 13CD; # CHEROKEE SMALL LETTER S
AB9E; C; 13CE; # CHEROKEE SMALL LETTER SE
AB9F; C; 13CF; # CHEROKEE SMALL LETTER SI
ABA0; C; 13D0; # CHEROKEE SMALL LETTER SO
ABA1; C; 13D1; # CHEROKEE SMALL LETTER SU
ABA2; C; 13D2; # CHEROKEE SMALL LETTER SV
ABA3; C; 13D3; # CHEROKEE SMALL LETTER DA
ABA4; C; 13D4; # CHEROKEE SMALL LETTER TA
ABA5; C; 13D5; # CHEROKEE SMALL LETTER DE
ABA6; C; 13D6; # CHEROKEE SMALL LETTER TE
ABA7; C; 13D7; # CHEROKEE SMALL LETTER DI
ABA8; C; 13D8; # CHEROKEE SMALL LETTER TI
ABA9; C; 13D9; # CHEROKEE SMALL LETTER DO
ABAA; C; 13DA; # CHEROKEE SMALL LETTER DU
ABAB; C; 13DB; # CHEROKEE SMALL LETTER DV
ABAC; C; 13DC; # CHEROKEE SMALL LETTER DLA
ABAD; C; 13DD; # CHEROKEE SMALL LETTER TLA
ABAE; C; 13DE; # CHEROKEE SMALL LETTER TLE
ABAF; C; 13DF; # CHEROKEE SMALL LETTER TLI
ABB0; C; 13E0; # CHEROKEE SMALL LETTER TLO
ABB1; C; 13E1; # CHEROKEE SMALL LETTER TLU
ABB2; C; 13E2; # CHEROKEE SMALL LETTER TLV
ABB3; C; 13E3; # CHEROKEE SMALL LETTER TSA
ABB4; C; 13E4; # CHEROKEE SMALL LETTER TSE
ABB5; C; 13E5; # CHEROKEE SMALL LETTER TSI
ABB6; C; 13E6; # CHEROKEE SMALL LETTER TSO
ABB7; C; 13E7; # CHEROKEE SMALL LETTER TSU
ABB8; C; 13E8; # CHEROKEE SMALL LETTER TSV
ABB9; C; 13E9; # CHEROKEE SMALL LETTER WA
ABBA; C; 13EA; # CHEROKEE SMALL LETTER WE
ABBB; C; 13EB; # CHEROKEE SMALL LETTER WI
ABBC; C; 13EC; # CHEROKEE SMALL LETTER WO
ABBD; C; 13ED; # CHEROKEE SMALL LETTER WU
ABBE; C; 13EE; # CHEROKEE SMALL LETTER WV
ABBF; C; 13EF; # CHEROKEE SMALL LETTER YA
FB00; F; 0066 0066; # LATIN SMALL LIGATURE FF
FB01; F; 0066 0069; # LATIN SMALL LIGATURE FI
FB02; F; 0066 006C; # LATIN SMALL LIGATURE FL
FB03; F; 0066 0066 0069; # LATIN SMALL LIGATURE FFI
FB04; F; 0066 0066 006C; # LATIN SMALL LIGATURE FFL
FB05; F; 0073 0074; # LATIN SMALL LIGATURE LONG S T
FB05; S; FB06; # LATIN SMALL LIGATURE LONG S T
FB06; F; 0073 0074; # LATIN SMALL LIGATURE ST
FB13; F; 0574 0576; # ARMENIAN SMALL LIGATURE MEN NOW
FB14; F; 0574 0565; # ARMENIAN SMALL LIGATURE MEN ECH
FB15; F; 0574 056B; # ARMENIAN SMALL LIGATURE MEN INI
FB16; F; 057E 0576; # ARMENIAN SMALL LIGATURE VEW NOW
FB17; F; 0574 056D; # ARMENIAN SMALL LIGATURE MEN XEH
FF21; C; FF41; # FULLWIDTH LATIN CAPITAL LETTER A
FF22; C; FF42; # FULLWIDTH LATIN CAPITAL LETTER B
FF23; C; FF43; # FULLWIDTH LATIN CAPITAL LETTER C
FF24; C; FF44; # FULLWIDTH LATIN CAPITAL LETTER D
FF25; C; FF45; # FULLWIDTH LATIN CAPITAL LETTER E
FF26; C; FF46; # FULLWIDTH LATIN CAPITAL LETTER F
FF27; C; FF47; # FULLWIDTH LATIN CAPITAL LETTER G
FF28; C; FF48; # FULLWIDTH LATIN CAPITAL LETTER H
FF29; C; FF49; # FULLWIDTH LATIN CAPITAL LETTER I
FF2A; C; FF4A; # FULLWIDTH LATIN CAPITAL LETTER J
FF2B; C; FF4B; # FULLWIDTH LATIN CAPITAL LETTER K
FF2C; C; FF4C; # FULLWIDTH LATIN CAPITAL LETTER L
FF2D; C; FF4D; # FULLWIDTH LATIN CAPITAL LETTER M
FF2E; C; FF4E; # FULLWIDTH LATIN CAPITAL LETTER N
FF2F; C; FF4F; # FULLWIDTH LATIN CAPITAL LETTER O
FF30; C; FF50; # FULLWIDTH LATIN CAPITAL LETTER P
FF31; C; FF51; # FULLWIDTH LATIN CAPITAL LETTER Q
FF32; C; FF52; # FULLWIDTH LATIN CAPITAL LETTER R
FF33; C; FF53; # FULLWIDTH LATIN CAPITAL LETTER S
FF34; C; FF54; # FULLWIDTH LATIN CAPITAL LETTER T
FF35; C; FF55; # FULLWIDTH LATIN CAPITAL LETTER U
FF36; C; FF56; # FULLWIDTH LATIN CAPITAL LETTER V
FF37; C; FF57; # FULLWIDTH LATIN CAPITAL LETTER W
FF38; C; FF58; # FULLWIDTH LATIN CAPITAL LETTER X
FF39; C; FF59; # FULLWIDTH LATIN CAPITAL LETTER Y
FF3A; C; FF5A; # FULLWIDTH LATIN CAPITAL LETTER Z
10400; C; 10428; # DESERET CAPITAL LETTER LONG I
10401; C; 10429; # DESERET CAPITAL LETTER LONG E
10402; C; 1042A; # DESERET CAPITAL LETTER LONG A
10403; C; 1042B; # DESERET CAPITAL LETTER LONG AH
10404; C; 1042C; # DESERET CAPITAL LETTER LONG O
10405; C; 1042D; # DESERET CAPITAL LETTER LONG OO
10406; C; 1042E; # DESERET CAPITAL LETTER SHORT I
10407; C; 1042F; # DESERET CAPITAL LETTER SHORT E
10408; C; 10430; # DESERET CAPITAL LETTER SHORT A
10409; C; 10431; # DESERET CAPITAL LETTER SHORT AH
1040A; C; 10432; # DESERET CAPITAL LETTER SHORT O
1040B; C; 10433; # DESERET CAPITAL LETTER SHORT OO
1040C; C; 10434; # DESERET CAPITAL LETTER AY
1040D; C; 10435; # DESERET CAPITAL LETTER OW
1040E; C; 10436; # DESERET CAPITAL LETTER WU
1040F; C; 10437; # DESERET CAPITAL LETTER YEE
10410; C; 10438; # DESERET CAPITAL LETTER H
10411; C; 10439; # DESERET CAPITAL LETTER PEE
10412; C; 1043A; # DESERET CAPITAL LETTER BEE
10413; C; 1043B; # DESERET CAPITAL LETTER TEE
10414; C; 1043C; # DESERET CAPITAL LETTER DEE
10415; C; 1043D; # DESERET CAPITAL LETTER CHEE
10416; C; 1043E; # DESERET CAPITAL LETTER JEE
10417; C; 1043F; # DESERET CAPITAL LETTER KAY
10418; C; 10440; # DESERET CAPITAL LETTER GAY
10419; C; 10441; # DESERET CAPITAL LETTER EF
1041A; C; 10442; # DESERET CAPITAL LETTER VEE
1041B; C; 10443; # DESERET CAPITAL LETTER ETH
1041C; C; 10444; # DESERET CAPITAL LETTER THEE
1041D; C; 10445; # DESERET CAPITAL LETTER ES
1041E; C; 10446; # DESERET CAPITAL LETTER ZEE
1041F; C; 10447; # DESERET CAPITAL LETTER ESH
10420; C; 10448; # DESERET CAPITAL LETTER ZHEE
10421; C; 10449; # DESERET CAPITAL LETTER ER
10422; C; 1044A; # DESERET CAPITAL LETTER EL
10423; C; 1044B; # DESERET CAPITAL LETTER EM
10424; C; 1044C; # DESERET CAPITAL LETTER EN
10425; C; 1044D; # DESERET CAPITAL LETTER ENG
10426; C; 1044E; # DESERET CAPITAL LETTER OI
10427; C; 1044F; # DESERET CAPITAL LETTER EW
104B0; C; 104D8; # OSAGE CAPITAL LETTER A
104B1; C; 104D9; # OSAGE CAPITAL LETTER AI
104B2; C; 104DA; # OSAGE CAPITAL LETTER AIN
104B3; C; 104DB; # OSAGE CAPITAL LETTER AH
104B4; C; 104DC; # OSAGE CAPITAL LETTER BRA
104B5; C; 104DD; # OSAGE CAPITAL LETTER CHA
104B6; C; 104DE; # OSAGE CAPITAL LETTER EHCHA
104B7; C; 104DF; # OSAGE CAPITAL LETTER E
104B8; C; 104E0; # OSAGE CAPITAL LETTER EIN
104B9; C; 104E1; # OSAGE CAPITAL LETTER HA
104BA; C; 104E2; # OSAGE CAPITAL LETTER HYA
104BB; C; 104E3; # OSAGE CAPITAL LETTER I
104BC; C; 104E4; # OSAGE CAPITAL LETTER KA
104BD; C; 104E5; # OSAGE CAPITAL LETTER EHKA
104BE; C; 104E6; # OSAGE CAPITAL LETTER KYA
104BF; C; 104E7; # OSAGE CAPITAL LETTER LA
104C0; C; 104E8; # OSAGE CAPITAL LETTER MA
104C1; C; 104E9; # OSAGE CAPITAL LETTER NA
104C2; C; 104EA; # OSAGE CAPITAL LETTER O
104C3; C; 104EB; # OSAGE CAPITAL LETTER OIN
104C4; C; 104EC; # OSAGE CAPITAL LETTER PA
104C5; C; 104ED; # OSAGE CAPITAL LETTER EHPA
104C6; C; 104EE; # OSAGE CAPITAL LETTER SA
104C7; C; 104EF; # OSAGE CAPITAL LETTER SHA
104C8; C; 104F0; # OSAGE CAPITAL LETTER TA
104C9; C; 104F1; # OSAGE CAPITAL LETTER EHTA
104CA; C; 104F2; # OSAGE CAPITAL LETTER TSA
104CB; C; 104F3; # OSAGE CAPITAL LETTER EHTSA
104CC; C; 104F4; # OSAGE CAPITAL LETTER TSHA
104CD; C; 104F5; # OSAGE CAPITAL LETTER DHA
104CE; C; 104F6; # OSAGE CAPITAL LETTER U
104CF; C; 104F7; # OSAGE CAPITAL LETTER WA
104D0; C; 104F8; # OSAGE CAPITAL LETTER KHA
104D1; C; 104F9; # OSAGE CAPITAL LETTER GHA
104D2; C; 104FA; # OSAGE CAPITAL LETTER ZA
104D3; C; 104FB; # OSAGE CAPITAL LETTER ZHA
10570; C; 10597; # VITHKUQI CAPITAL LETTER A
10571; C; 10598; # VITHKUQI CAPITAL LETTER BBE
10572; C; 10599; # VITHKUQI CAPITAL LETTER BE
10573; C; 1059A; # VITHKUQI CAPITAL LETTER CE
10574; C; 1059B; # VITHKUQI CAPITAL LETTER CHE
10575; C; 1059C; # VITHKUQI CAPITAL LETTER DE
10576; C; 1059D; # VITHKUQI CAPITAL LETTER DHE
10577; C; 1059E; # VITHKUQI CAPITAL LETTER EI
10578; C; 1059F; # VITHKUQI CAPITAL LETTER E
10579; C; 105A0; # VITHKUQI CAPITAL LETTER FE
1057A; C; 105A1; # VITHKUQI CAPITAL LETTER GA
1057C; C; 105A3; # VITHKUQI CAPITAL LETTER HA
1057D; C; 105A4; # VITHKUQI CAPITAL LETTER HHA
1057E; C; 105A5; # VITHKUQI CAPITAL LETTER I
1057F; C; 105A6; # VITHKUQI CAPITAL LETTER IJE
10580; C; 105A7; # VITHKUQI CAPITAL LETTER JE
10581; C; 105A8; # VITHKUQI CAPITAL LETTER KA
10582; C; 105A9; # VITHKUQI CAPITAL LETTER LA
10583; C; 105AA; # VITHKUQI CAPITAL LETTER LLA
10584; C; 105AB; # VITHKUQI CAPITAL LETTER ME
10585; C; 105AC; # VITHKUQI CAPITAL LETTER NE
10586; C; 105AD; # VITHKUQI CAPITAL LETTER NJE
10587; C; 105AE; # VITHKUQI CAPITAL LETTER O
10588; C; 105AF; # VITHKUQI CAPITAL LETTER PE
10589; C; 105B0; # VITHKUQI CAPITAL LETTER QA
1058A; C; 105B1; # VITHKUQI CAPITAL LETTER RE
1058C; C; 105B3; # VITHKUQI CAPITAL LETTER SE
1058D; C; 105B4; # VITHKUQI CAPITAL LETTER SHE
1058E; C; 105B5; # VITHKUQI CAPITAL LETTER TE
1058F; C; 105B6; # VITHKUQI CAPITAL LETTER THE
10590; C; 105B7; # VITHKUQI CAPITAL LETTER U
10591; C; 105B8; # VITHKUQI CAPITAL LETTER VE
10592; C; 105B9; # VITHKUQI CAPITAL LETTER XE
10594; C; 105BB; # VITHKUQI CAPITAL LETTER Y
10595; C; 105BC; # VITHKUQI CAPITAL LETTER ZE
10C80; C; 10CC0; # OLD HUNGARIAN CAPITAL LETTER A
10C81; C; 10CC1; # OLD HUNGARIAN CAPITAL LETTER AA
10C82; C; 10CC2; # OLD HUNGARIAN CAPITAL LETTER EB
10C83; C; 10CC3; # OLD HUNGARIAN CAPITAL LETTER AMB
10C84; C; 10CC4; # OLD HUNGARIAN CAPITAL LETTER EC
10C85; C; 10CC5; # OLD HUNGARIAN CAPITAL LETTER ENC
10C86; C; 10CC6; # OLD HUNGARIAN CAPITAL LETTER ECS
10C87; C; 10CC7; # OLD HUNGARIAN CAPITAL LETTER ED
10C88; C; 10CC8; # OLD HUNGARIAN CAPITAL LETTER AND
10C89; C; 10CC9; # OLD HUNGARIAN CAPITAL LETTER E
10C8A; C; 10CCA; # OLD HUNGARIAN CAPITAL LETTER CLOSE E
10C8B; C; 10CCB; # OLD HUNGARIAN CAPITAL LETTER EE
10C8C; C; 10CCC; # OLD HUNGARIAN CAPITAL LETTER EF
10C8D; C; 10CCD; # OLD HUNGARIAN CAPITAL LETTER EG
10C8E; C; 10CCE; # OLD HUNGARIAN CAPITAL LETTER EGY
10C8F; C; 10CCF; # OLD HUNGARIAN CAPITAL LETTER EH
10C90; C; 10CD0; # OLD HUNGARIAN CAPITAL LETTER I
10C91; C; 10CD1; # OLD HUNGARIAN CAPITAL LETTER II
10C92; C; 10CD2; # OLD HUNGARIAN CAPITAL LETTER EJ
10C93; C; 10CD3; # OLD HUNGARIAN CAPITAL LETTER EK
10C94; C; 10CD4; # OLD HUNGARIAN CAPITAL LETTER AK
10C95; C; 10CD5; # OLD HUNGARIAN CAPITAL LETTER UNK
10C96; C; 10CD6; # OLD HUNGARIAN CAPITAL LETTER EL
10C97; C; 10CD7; # OLD HUNGARIAN CAPITAL LETTER ELY
10C98; C; 10CD8; # OLD HUNGARIAN CAPITAL LETTER EM
10C99; C; 10CD9; # OLD HUNGARIAN CAPITAL LETTER EN
10C9A; C; 10CDA; # OLD HUNGARIAN CAPITAL LETTER ENY
10C9B; C; 10CDB; # OLD HUNGARIAN CAPITAL LETTER O
10C9C; C; 10CDC; # OLD HUNGARIAN CAPITAL LETTER OO
10C9D; C; 10CDD; # OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG OE
10C9E; C; 10CDE; # OLD HUNGARIAN CAPITAL LETTER RUDIMENTA OE
10C9F; C; 10CDF; # OLD HUNGARIAN CAPITAL LETTER OEE
10CA0; C; 10CE0; # OLD HUNGARIAN CAPITAL LETTER EP
10CA1; C; 10CE1; # OLD HUNGARIAN CAPITAL LETTER EMP
10CA2; C; 10CE2; # OLD HUNGARIAN CAPITAL LETTER ER
10CA3; C; 10CE3; # OLD HUNGARIAN CAPITAL LETTER SHORT ER
10CA4; C; 10CE4; # OLD HUNGARIAN CAPITAL LETTER ES
10CA5; C; 10CE5; # OLD HUNGARIAN CAPITAL LETTER ESZ
10CA6; C; 10CE6; # OLD HUNGARIAN CAPITAL LETTER ET
10CA7; C; 10CE7; # OLD HUNGARIAN CAPITAL LETTER ENT
10CA8; C; 10CE8; # OLD HUNGARIAN CAPITAL LETTER ETY
10CA9; C; 10CE9; # OLD HUNGARIAN CAPITAL LETTER ECH
10CAA; C; 10CEA; # OLD HUNGARIAN CAPITAL LETTER U
10CAB; C; 10CEB; # OLD HUNGARIAN CAPITAL LETTER UU
10CAC; C; 10CEC; # OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG UE
10CAD; C; 10CED; # OLD HUNGARIAN CAPITAL LETTER RUDIMENTA UE
10CAE; C; 10CEE; # OLD HUNGARIAN CAPITAL LETTER EV
10CAF; C; 10CEF; # OLD HUNGARIAN CAPITAL LETTER EZ
10CB0; C; 10CF0; # OLD HUNGARIAN CAPITAL LETTER EZS
10CB1; C; 10CF1; # OLD HUNGARIAN CAPITAL LETTER ENT-SHAPED SIGN
10CB2; C; 10CF2; # OLD HUNGARIAN CAPITAL LETTER US
10D50; C; 10D70; # GARAY CAPITAL LETTER A
10D51; C; 10D71; # GARAY CAPITAL LETTER CA
10D52; C; 10D72; # GARAY CAPITAL LETTER MA
10D53; C; 10D73; # GARAY CAPITAL LETTER KA
10D54; C; 10D74; # GARAY CAPITAL LETTER BA
10D55; C; 10D75; # GARAY CAPITAL LETTER JA
10D56; C; 10D76; # GARAY CAPITAL LETTER SA
10D57; C; 10D77; # GARAY CAPITAL LETTER WA
10D58; C; 10D78; # GARAY CAPITAL LETTER LA
10D59; C; 10D79; # GARAY CAPITAL LETTER GA
10D5A; C; 10D7A; # GARAY CAPITAL LETTER DA
10D5B; C; 10D7B; # GARAY CAPITAL LETTER XA
10D5C; C; 10D7C; # GARAY CAPITAL LETTER YA
10D5D; C; 10D7D; # GARAY CAPITAL LETTER TA
10D5E; C; 10D7E; # GARAY CAPITAL LETTER RA
10D5F; C; 10D7F; # GARAY CAPITAL LETTER NYA
10D60; C; 10D80; # GARAY CAPITAL LETTER FA
10D61; C; 10D81; # GARAY CAPITAL LETTER NA
10D62; C; 10D82; # GARAY CAPITAL LETTER PA
10D63; C; 10D83; # GARAY CAPITAL LETTER HA
10D64; C; 10D84; # GARAY CAPITAL LETTER OLD KA
10D65; C; 10D85; # GARAY CAPITAL LETTER OLD NA
118A0; C; 118C0; # WARANG CITI CAPITAL LETTER NGAA
118A1; C; 118C1; # WARANG CITI CAPITAL LETTER A
118A2; C; 118C2; # WARANG CITI CAPITAL LETTER WI
118A3; C; 118C3; # WARANG CITI CAPITAL LETTER YU
118A4; C; 118C4; # WARANG CITI CAPITAL LETTER YA
118A5; C; 118C5; # WARANG CITI CAPITAL LETTER YO
118A6; C; 118C6; # WARANG CITI CAPITAL LETTER II
118A7; C; 118C7; # WARANG CITI CAPITAL LETTER UU
118A8; C; 118C8; # WARANG CITI CAPITAL LETTER E
118A9; C; 118C9; # WARANG CITI CAPITAL LETTER O
118AA; C; 118CA; # WARANG CITI CAPITAL LETTER ANG
118AB; C; 118CB; # WARANG CITI CAPITAL LETTER GA
118AC; C; 118CC; # WARANG CITI CAPITAL LETTER KO
118AD; C; 118CD; # WARANG CITI CAPITAL LETTER ENY
118AE; C; 118CE; # WARANG CITI CAPITAL LETTER YUJ
118AF; C; 118CF; # WARANG CITI CAPITAL LETTER UC
118B0; C; 118D0; # WARANG CITI CAPITAL LETTER ENN
118B1; C; 118D1; # WARANG CITI CAPITAL LETTER ODD
118B2; C; 118D2; # WARANG CITI CAPITAL LETTER TTE
118B3; C; 118D3; # WARANG CITI CAPITAL LETTER NUNG
118B4; C; 118D4; # WARANG CITI CAPITAL LETTER DA
118B5; C; 118D5; # WARANG CITI CAPITAL LETTER AT
118B6; C; 118D6; # WARANG CITI CAPITAL LETTER AM
118B7; C; 118D7; # WARANG CITI CAPITAL LETTER BU
118B8; C; 118D8; # WARANG CITI CAPITAL LETTER PU
118B9; C; 118D9; # WARANG CITI CAPITAL LETTER HIYO
118BA; C; 118DA; # WARANG CITI CAPITAL LETTER HOLO
118BB; C; 118DB; # WARANG CITI CAPITAL LETTER HORR
118BC; C; 118DC; # WARANG CITI CAPITAL LETTER HAR
118BD; C; 118DD; # WARANG CITI CAPITAL LETTER SSUU
118BE; C; 118DE; # WARANG CITI CAPITAL LETTER SII
118BF; C; 118DF; # WARANG CITI CAPITAL LETTER VIYO
16E40; C; 16E60; # MEDEFAIDRIN CAPITAL LETTER M
16E41; C; 16E61; # MEDEFAIDRIN CAPITAL LETTER S
16E42; C; 16E62; # MEDEFAIDRIN CAPITAL LETTER V
16E43; C; 16E63; # MEDEFAIDRIN CAPITAL LETTER W
16E44; C; 16E64; # MEDEFAIDRIN CAPITAL LETTER ATIU
16E45; C; 16E65; # MEDEFAIDRIN CAPITAL LETTER Z
16E46; C; 16E66; # MEDEFAIDRIN CAPITAL LETTER KP
16E47; C; 16E67; # MEDEFAIDRIN CAPITAL LETTER P
16E48; C; 16E68; # MEDEFAIDRIN CAPITAL LETTER T
16E49; C; 16E69; # MEDEFAIDRIN CAPITAL LETTER G
16E4A; C; 16E6A; # MEDEFAIDRIN CAPITAL LETTER F
16E4B; C; 16E6B; # MEDEFAIDRIN CAPITAL LETTER I
16E4C; C; 16E6C; # MEDEFAIDRIN CAPITAL LETTER K
16E4D; C; 16E6D; # MEDEFAIDRIN CAPITAL LETTER A
16E4E; C; 16E6E; # MEDEFAIDRIN CAPITAL LETTER J
16E4F; C; 16E6F; # MEDEFAIDRIN CAPITAL LETTER E
16E50; C; 16E70; # MEDEFAIDRIN CAPITAL LETTER B
16E51; C; 16E71; # MEDEFAIDRIN CAPITAL LETTER C
16E52; C; 16E72; # MEDEFAIDRIN CAPITAL LETTER U
16E53; C; 16E73; # MEDEFAIDRIN CAPITAL LETTER YU
16E54; C; 16E74; # MEDEFAIDRIN CAPITAL LETTER L
16E55; C; 16E75; # MEDEFAIDRIN CAPITAL LETTER Q
16E56; C; 16E76; # MEDEFAIDRIN CAPITAL LETTER HP
16E57; C; 16E77; # MEDEFAIDRIN CAPITAL LETTER NY
16E58; C; 16E78; # MEDEFAIDRIN CAPITAL LETTER X
16E59; C; 16E79; # MEDEFAIDRIN CAPITAL LETTER D
16E5A; C; 16E7A; # MEDEFAIDRIN CAPITAL LETTER OE
16E5B; C; 16E7B; # MEDEFAIDRIN CAPITAL LETTER N
16E5C; C; 16E7C; # MEDEFAIDRIN CAPITAL LETTER R
16E5D; C; 16E7D; # MEDEFAIDRIN CAPITAL LETTER O
16E5E; C; 16E7E; # MEDEFAIDRIN CAPITAL LETTER AI
16E5F; C; 16E7F; # MEDEFAIDRIN CAPITAL LETTER Y
16EA0; C; 16EBB; # BERIA ERFE CAPITAL LETTER ARKAB
16EA1; C; 16EBC; # BERIA ERFE CAPITAL LETTER BASIGNA
16EA2; C; 16EBD; # BERIA ERFE CAPITAL LETTER DARBAI
16EA3; C; 16EBE; # BERIA ERFE CAPITAL LETTER EH
16EA4; C; 16EBF; # BERIA ERFE CAPITAL LETTER FITKO
16EA5; C; 16EC0; # BERIA ERFE CAPITAL LETTER GOWAY
16EA6; C; 16EC1; # BERIA ERFE CAPITAL LETTER HIRDEABO
16EA7; C; 16EC2; # BERIA ERFE CAPITAL LETTER I
16EA8; C; 16EC3; # BERIA ERFE CAPITAL LETTER DJAI
16EA9; C; 16EC4; # BERIA ERFE CAPITAL LETTER KOBO
16EAA; C; 16EC5; # BERIA ERFE CAPITAL LETTER LAKKO
16EAB; C; 16EC6; # BERIA ERFE CAPITAL LETTER MERI
16EAC; C; 16EC7; # BERIA ERFE CAPITAL LETTER NINI
16EAD; C; 16EC8; # BERIA ERFE CAPITAL LETTER GNA
16EAE; C; 16EC9; # BERIA ERFE CAPITAL LETTER NGAY
16EAF; C; 16ECA; # BERIA ERFE CAPITAL LETTER OI
16EB0; C; 16ECB; # BERIA ERFE CAPITAL LETTER PI
16EB1; C; 16ECC; # BERIA ERFE CAPITAL LETTER ERIGO
16EB2; C; 16ECD; # BERIA ERFE CAPITAL LETTER ERIGO TAMURA
16EB3; C; 16ECE; # BERIA ERFE CAPITAL LETTER SERI
16EB4; C; 16ECF; # BERIA ERFE CAPITAL LETTER SHEP
16EB5; C; 16ED0; # BERIA ERFE CAPITAL LETTER TATASOUE
16EB6; C; 16ED1; # BERIA ERFE CAPITAL LETTER UI
16EB7; C; 16ED2; # BERIA ERFE CAPITAL LETTER WASSE
16EB8; C; 16ED3; # BERIA ERFE CAPITAL LETTER AY
1E900; C; 1E922; # ADLAM CAPITAL LETTER ALIF
1E901; C; 1E923; # ADLAM CAPITAL LETTER DAALI
1E902; C; 1E924; # ADLAM CAPITAL LETTER LAAM
1E903; C; 1E925; # ADLAM CAPITAL LETTER MIIM
1E904; C; 1E926; # ADLAM CAPITAL LETTER BA
1E905; C; 1E927; # ADLAM CAPITAL LETTER SINNYIIYHE
1E906; C; 1E928; # ADLAM CAPITAL LETTER PE
1E907; C; 1E929; # ADLAM CAPITAL LETTER BHE
1E908; C; 1E92A; # ADLAM CAPITAL LETTER RA
1E909; C; 1E92B; # ADLAM CAPITAL LETTER E
1E90A; C; 1E92C; # ADLAM CAPITAL LETTER FA
1E90B; C; 1E92D; # ADLAM CAPITAL LETTER I
1E90C; C; 1E92E; # ADLAM CAPITAL LETTER O
1E90D; C; 1E92F; # ADLAM CAPITAL LETTER DHA
1E90E; C; 1E930; # ADLAM CAPITAL LETTER YHE
1E90F; C; 1E931; # ADLAM CAPITAL LETTER WAW
1E910; C; 1E932; # ADLAM CAPITAL LETTER NUN
1E911; C; 1E933; # ADLAM CAPITAL LETTER KAF
1E912; C; 1E934; # ADLAM CAPITAL LETTER YA
1E913; C; 1E935; # ADLAM CAPITAL LETTER U
1E914; C; 1E936; # ADLAM CAPITAL LETTER JIIM
1E915; C; 1E937; # ADLAM CAPITAL LETTER CHI
1E916; C; 1E938; # ADLAM CAPITAL LETTER HA
1E917; C; 1E939; # ADLAM CAPITAL LETTER QAAF
1E918; C; 1E93A; # ADLAM CAPITAL LETTER GA
1E919; C; 1E93B; # ADLAM CAPITAL LETTER NYA
1E91A; C; 1E93C; # ADLAM CAPITAL LETTER TU
1E91B; C; 1E93D; # ADLAM CAPITAL LETTER NHA
1E91C; C; 1E93E; # ADLAM CAPITAL LETTER VA
1E91D; C; 1E93F; # ADLAM CAPITAL LETTER KHA
1E91E; C; 1E940; # ADLAM CAPITAL LETTER GBE
1E91F; C; 1E941; # ADLAM CAPITAL LETTER ZAL
1E920; C; 1E942; # ADLAM CAPITAL LETTER KPO
1E921; C; 1E943; # ADLAM CAPITAL LETTER SHA
#
# EOF
+11
View File
@@ -442,6 +442,17 @@ impl Parser {
Some(self.buffer.clone()) Some(self.buffer.clone())
} }
/// 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
}
/// Like [`Parser::data`] but without reserving room for the NUL /// Like [`Parser::data`] but without reserving room for the NUL
/// terminator (for parsers that, like Zig's color parsers, read /// terminator (for parsers that, like Zig's color parsers, read
/// `writer.buffered()` directly). /// `writer.buffered()` directly).
+63 -6
View File
@@ -11,25 +11,49 @@ fn parse_unsigned<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. /// 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(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
// Zig prologue: writer must exist. (The NUL-terminator capacity check is // Zig prologue: writer must exist. The NUL-terminator reservation is NOT
// folded into `data()`.) // done here: Zig only appends the NUL (`writer.writeByte(0)`) in the
let data = parser.data()?; // branches that return text, so full-buffer control commands (sleep,
// xterm emulation, progress, ...) still parse.
let data = parser.buffered()?;
// Check first to see if this is a ConEmu OSC // Check first to see if this is a ConEmu OSC
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
if let Some(cmd) = conemu(&data) { if let Some((cmd, needs_nul)) = conemu(&data) {
if needs_nul && !parser.reserve_nul() {
return None;
}
return Some(cmd); return Some(cmd);
} }
// If it's not a ConEmu OSC, it's an iTerm2 notification // If it's not a ConEmu OSC, it's an iTerm2 notification
if !parser.reserve_nul() {
return None;
}
Some(Command::ShowDesktopNotification { Some(Command::ShowDesktopNotification {
title: Vec::new(), title: Vec::new(),
body: data, body: data,
}) })
} }
/// Returns None where the Zig code does `break :conemu`. /// Returns None where the Zig code does `break :conemu`. The flag is true
fn conemu(data: &[u8]) -> Option<Command> { /// when the Zig branch appends a NUL terminator (needs buffer room).
fn conemu(data: &[u8]) -> Option<(Command, bool)> {
let r = conemu_inner(data)?;
let needs_nul = matches!(
r,
Command::ConemuComment(_)
| Command::ConemuShowMessageBox(_)
| Command::ConemuChangeTabTitle(_)
| Command::ConemuGuimacro(_)
| Command::ConemuRunProcess(_)
| Command::ConemuOutputEnvironmentVariable(_)
| Command::ReportPwd { .. }
);
Some((r, needs_nul))
}
fn conemu_inner(data: &[u8]) -> Option<Command> {
if data.is_empty() { if data.is_empty() {
return None; return None;
} }
@@ -218,6 +242,39 @@ mod tests {
); );
} }
fn run_opt(input: &str) -> Option<Command> {
let mut p = Parser::init(false);
for ch in input.bytes() {
p.next(ch);
}
p.end(Some(0x1b))
}
#[test]
fn full_buffer_control_commands_do_not_need_nul() {
let fill = Parser::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));
assert_eq!(
run_opt(&input),
Some(Command::ConemuSleep { duration_ms: 100 })
);
// OSC 9;5 wait for input.
let input = format!("9;5{}", "x".repeat(fill - 1));
assert_eq!(run_opt(&input), Some(Command::ConemuWaitInput));
}
#[test]
fn full_buffer_text_commands_are_invalid() {
let fill = Parser::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);
let input = format!("9;{}", "a".repeat(fill));
assert_eq!(run_opt(&input), None);
}
#[test] #[test]
fn osc_9_show_desktop_notification() { fn osc_9_show_desktop_notification() {
notif("9;Hello world", "Hello world"); notif("9;Hello world", "Hello world");
+2 -1
View File
@@ -21,6 +21,7 @@ WASM, Windows and the embedded C API (`libghostty`) are out of scope.
| `ghostty-renderer` | renderer/ generic, OpenGL (+ opengl/*), cell, cursor, image, link, size, State, Thread, Overlay, shadertoy, row, shaders/glsl | config, font, terminal | | `ghostty-renderer` | renderer/ generic, OpenGL (+ opengl/*), cell, cursor, image, link, size, State, Thread, Overlay, shadertoy, row, shaders/glsl | config, font, terminal |
| `ghostty-termio` | termio/*, pty.zig, Command.zig, shell-integration resources | config, terminal, os | | `ghostty-termio` | termio/*, pty.zig, Command.zig, shell-integration resources | config, terminal, os |
| `ghostty-cli` | cli/* actions (+list-themes, +show-config, +list-keybinds, ...) | config, input, os, font | | `ghostty-cli` | cli/* actions (+list-themes, +show-config, +list-keybinds, ...) | config, input, os, font |
| `ghostty-inspector` | inspector/* (Dear ImGui terminal inspector: widgets, key + VT event recording) | input, renderer, terminal, vt, font, `dear-imgui-rs` |
| `ghostty-core` | App.zig, Surface.zig, surface_mouse.zig, apprt/action.zig, apprt/structs.zig, apprt/surface.zig, global.zig | everything below | | `ghostty-core` | App.zig, Surface.zig, surface_mouse.zig, apprt/action.zig, apprt/structs.zig, apprt/surface.zig, global.zig | everything below |
| `ghostty-apprt` | apprt/action.zig, structs.zig, surface.zig, ipc.zig (core↔runtime contract types) | vt, input, config, datastruct | | `ghostty-apprt` | apprt/action.zig, structs.zig, surface.zig, ipc.zig (core↔runtime contract types) | vt, input, config, datastruct |
| `ghostty-apprt-gtk` | apprt/gtk/* | core, config, input | | `ghostty-apprt-gtk` | apprt/gtk/* | core, config, input |
@@ -75,7 +76,7 @@ WASM, Windows and the embedded C API (`libghostty`) are out of scope.
## Out of scope / deferred ## Out of scope / deferred
- macOS (Swift app, Metal, CoreText), Windows, WASM, libghostty C API. - macOS (Swift app, Metal, CoreText), Windows, WASM, libghostty C API.
- The ImGui inspector is deferred until everything else works (Phase 7). - (The ImGui inspector was deferred and is now ported: `docs/rewrite/notes/ghostty-inspector.md`.)
## Intentional omissions (decided by the orchestrator) ## Intentional omissions (decided by the orchestrator)
+16
View File
@@ -0,0 +1,16 @@
# Gap fixes X2 (partial ports)
| # | Item | Status |
|---|------|--------|
| 1 | Flatpak exit watcher consumed the command | Fixed. `ghostty_os::flatpak::FlatpakSignalHandle` (`FlatpakHostCommand::signal_handle()`, shares the `Arc<Shared>` state); `termio/exec.rs` keeps it so `stop()` still SIGHUPs the host process group after `wait_callback` takes the command. Test: `signal_handle_survives_wait_callback` (real DBus path not testable here). |
| 2 | Child env byte-preserving + `filterSnapPaths` | Fixed. New `ghostty_os::EnvMap` (OsString keys/values); `get_env_map()`, `RtSurface::default_termio_env`, core `rt_surface_env`, `shell_integration::EnvMap`, `Command.env`/`Subprocess.env`, `append_env_os`/`prepend_env_os` are all byte-preserving; GTK `default_termio_env` no longer uses `std::env::vars()`. `ghostty_os::filter_snap_paths` ported; applied when `SNAP` is set (Zig gates on the compile-time `snap` option). Flatpak `HostCommand` (`a{ss}`) can only carry UTF-8, so env is lossily converted at that DBus boundary only. `config.env` (RepeatableStringMap) remains String (config values). |
| 3 | Root `main.rs` argv | Fixed. `ghostty_os::args::collect_lossy()` (args_os based) used by `src/main.rs`, `ghostty_config::cli_args::args_iterator`, `ghostty_cli::action::detect_args`. Downstream CLI/config parsing is String based, so a non-UTF-8 arg becomes U+FFFD instead of panicking (not byte-exact end-to-end). |
| 4 | `os/uri.rs` percent-decoding | Fixed. `Component::to_raw_bytes()` (lossless), `to_raw()` is a lossy text view, new `uri::parse_bytes` (handles non-UTF-8 input incl. `raw_path`). OSC 7 (`termio/stream_handler.rs`) uses bytes for `set_pwd` and the pwd surface message; only the window-title fallback is lossy. |
| 5 | Keybinding Unicode case folding | Fixed. `ucd/CaseFolding.txt` (17.0.0, from uucode-0.2.0) vendored; `build.rs` generates `CASE_FOLDING_FULL` (status C+F); API `ghostty_unicode::case_folding::{lookup, fold_full}`; `input/binding::folded_codepoint` uses it (hash and `folded_equal`). |
| 6 | StringMap whole-search retry limit | Fixed. `onig` 6.5.3 only wraps retry-limit-in-match; `onig_sys` exposes `onig_set_retry_limit_in_search_of_match_param`. New crate `crates/ghostty-onig` (`unsafe_code = "deny"`, one SAFETY-commented call) provides `set_retry_limit_in_search`; `string_map.rs` now sets only that limit, like Zig. Tests in both crates (catastrophic regex aborts). |
| 7 | osc9 NUL reservation order | Fixed. `Parser::reserve_nul()`; osc9 uses `buffered()` and reserves only in branches where Zig calls `writeByte(0)` (11, 2, 3, 6, 7, 8, 9 and the notification fallback). Tests for full-buffer behaviour. |
| 8 | Wayland registry add/remove | Fixed in `winproto/wayland.rs` only: the registry `Dispatch` now binds newly advertised supported globals (blur, decoration manager, slide, xdg-activation; `xdg_wm_dialog_v1` flag) and clears/destroys removed ones, tracked by registry name (`bound_names`; Zig compares object id to name, which looks like an upstream quirk). Binds version 1 as the existing init does. No unit test (needs a compositor). |
| 9 | shadertoy sampler2D helpers | Fixed. `renderer/src/shadertoy_sampler.rs` token-level rewrite: `sampler2D` params -> `texture2D x_tex, sampler x_smp`, uses in bodies -> `sampler2D(x_tex, x_smp)`, call args `x`/`iChannel0` -> pairs, forwarding and prototypes work, `texture2D(` -> `texture(` (the prefix macro would clash with the type). Prefix-only rewrites no longer touch user text. Tests incl. textureSize/texelFetch/textureLod. Remaining limits: sampler args must be plain identifiers (no ternary/struct member), no sampler locals/arrays/struct fields, only `sampler2D` (iChannel1-3 are unused upstream). |
| 10 | `+list-themes` preview | Fixed. OSC 8 file:// hyperlinks (list entries, header name/path, error path) and double/dashed/dotted/curly underlines are recorded during draw and re-emitted as raw escapes after each ratatui flush (`Overlay`, `render_overlays`); popups drop covered overlays. Input moved from crossterm to `termina` (drawing still ratatui+crossterm backend) so `CSI ? 997 ; 1|2 n` color-scheme reports are seen; the driver sends `CSI ? 996 n` + `CSI ? 2031 h` unconditionally (vaxis gates on DECRQM 2031, which termina cannot parse). Verified with a pty smoke run; unit tests for overlays, key conversion and theme reports. |
Gate: build/test/clippy/fmt pass for ghostty-{os,termio,input,unicode,vt,terminal,onig,renderer,cli,config,core,apprt}. `ghostty-inspector` (another agent) currently fails clippy and blocks workspace-wide `clippy -D warnings`; it also makes clippy of crates depending on it unreliable (checked with warnings visible: none in X2 files).
+4 -11
View File
@@ -14,7 +14,8 @@ use ghostty_cli::build_info::{self, BuildInfo, Version};
use ghostty_core::global::{self, InitOptions}; use ghostty_core::global::{self, InitOptions};
fn main() -> ExitCode { fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect(); // Byte-safe argv (`std::env::args()` panics on non-UTF-8 arguments).
let args: Vec<String> = ghostty_os::args::collect_lossy();
// We first start by initializing our global state. This sets up // We first start by initializing our global state. This sets up
// process-level state we need to run the terminal. We detect whether a // process-level state we need to run the terminal. We detect whether a
@@ -92,17 +93,9 @@ fn build_info() -> BuildInfo {
let gtk = gtk_version::get_runtime_version(); let gtk = gtk_version::get_runtime_version();
let adw = adw_version::get_runtime_version(); let adw = adw_version::get_runtime_version();
BuildInfo { BuildInfo {
gtk_build: Some(Version { gtk_build: Some(version(gtk_version::COMPTIME_VERSION)),
major: 4,
minor: 14,
micro: 0,
}),
gtk_runtime: Some(version((gtk.0, gtk.1, gtk.2))), gtk_runtime: Some(version((gtk.0, gtk.1, gtk.2))),
adw_build: Some(Version { adw_build: Some(version(adw_version::COMPTIME_VERSION)),
major: 1,
minor: 5,
micro: 0,
}),
adw_runtime: Some(version((adw.0, adw.1, adw.2))), adw_runtime: Some(version((adw.0, adw.1, adw.2))),
..BuildInfo::default() ..BuildInfo::default()
} }