diff --git a/Cargo.lock b/Cargo.lock index c26126e..c4b560a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,6 +664,51 @@ dependencies = [ "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]] name = "defmt" version = "1.1.1" @@ -1208,6 +1253,7 @@ dependencies = [ "ghostty-config", "ghostty-datastruct", "ghostty-input", + "ghostty-inspector", "ghostty-renderer", "ghostty-terminal", "ghostty-vt", @@ -1219,6 +1265,7 @@ name = "ghostty-apprt-gtk" version = "0.1.0" dependencies = [ "bitflags 2.13.2", + "dear-imgui-glow", "env_logger", "gdk4-wayland", "gdk4-x11", @@ -1226,6 +1273,7 @@ dependencies = [ "ghostty-config", "ghostty-datastruct", "ghostty-input", + "ghostty-inspector", "ghostty-os", "ghostty-vt", "glib-build-tools", @@ -1255,6 +1303,17 @@ dependencies = [ "log", ] +[[package]] +name = "ghostty-build-data" +version = "0.1.0" +dependencies = [ + "ghostty-cli", + "ghostty-config", + "ghostty-input", + "ghostty-os", + "ghostty-termio", +] + [[package]] name = "ghostty-cli" version = "0.1.0" @@ -1269,6 +1328,7 @@ dependencies = [ "log", "ratatui", "tempfile", + "termina", "thiserror 2.0.21", "unicode-width", ] @@ -1298,6 +1358,7 @@ dependencies = [ "ghostty-datastruct", "ghostty-font", "ghostty-input", + "ghostty-inspector", "ghostty-os", "ghostty-renderer", "ghostty-terminal", @@ -1341,11 +1402,33 @@ dependencies = [ name = "ghostty-input" version = "0.1.0" dependencies = [ + "ghostty-unicode", "ghostty-vt", "indexmap", "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]] name = "ghostty-os" version = "0.1.0" @@ -1392,6 +1475,7 @@ dependencies = [ "ghostty-cli", "ghostty-config", "ghostty-core", + "ghostty-os", "log", ] @@ -1404,6 +1488,7 @@ dependencies = [ "flate2", "ghostty-config", "ghostty-datastruct", + "ghostty-onig", "ghostty-unicode", "ghostty-vt", "log", @@ -2147,6 +2232,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + [[package]] name = "mio" version = "1.2.4" @@ -2955,6 +3046,19 @@ dependencies = [ "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]] name = "serde_repr" version = "0.1.21" @@ -4074,6 +4178,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + [[package]] name = "zvariant" version = "5.15.0" diff --git a/Cargo.toml b/Cargo.toml index 72737fc..3242474 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ ghostty-config.workspace = true ghostty-apprt-gtk.workspace = true ghostty-apprt.workspace = true ghostty-cli.workspace = true +ghostty-os.workspace = true log = "0.4" [lints] @@ -23,6 +24,11 @@ workspace = true resolver = "3" 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] version = "0.1.0" edition = "2024" @@ -38,12 +44,14 @@ ghostty-font = { path = "crates/ghostty-font" } ghostty-renderer = { path = "crates/ghostty-renderer" } ghostty-termio = { path = "crates/ghostty-termio" } ghostty-os = { path = "crates/ghostty-os" } +ghostty-onig = { path = "crates/ghostty-onig" } ghostty-datastruct = { path = "crates/ghostty-datastruct" } ghostty-core = { path = "crates/ghostty-core" } ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" } ghostty-apprt = { path = "crates/ghostty-apprt" } ghostty-cli = { path = "crates/ghostty-cli" } ghostty-bench = { path = "crates/ghostty-bench" } +ghostty-inspector = { path = "crates/ghostty-inspector" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/crates/ghostty-apprt-gtk/Cargo.toml b/crates/ghostty-apprt-gtk/Cargo.toml index f87b682..850db83 100644 --- a/crates/ghostty-apprt-gtk/Cargo.toml +++ b/crates/ghostty-apprt-gtk/Cargo.toml @@ -10,6 +10,7 @@ ghostty-apprt.workspace = true ghostty-config.workspace = true ghostty-input.workspace = true ghostty-datastruct.workspace = true +ghostty-inspector.workspace = true ghostty-os.workspace = true ghostty-vt.workspace = true log = "0.4" @@ -28,13 +29,14 @@ half = "2" libc = "0.2.190" signal-hook = "0.4.5" libloading = "0.9.0" +dear-imgui-glow = "0.19" +glow = "0.18" [build-dependencies] glib-build-tools = "0.22" [dev-dependencies] env_logger = "0.11" -glow = "0.18" [lints.rust] unsafe_code = "deny" diff --git a/crates/ghostty-apprt-gtk/src/class/mod.rs b/crates/ghostty-apprt-gtk/src/class/mod.rs index abff38b..b0e4f9b 100644 --- a/crates/ghostty-apprt-gtk/src/class/mod.rs +++ b/crates/ghostty-apprt-gtk/src/class/mod.rs @@ -9,6 +9,9 @@ pub mod config_errors_dialog; pub mod debug_warning; pub mod dialog; pub mod global_shortcuts; +pub mod imgui_widget; +pub mod inspector_widget; +pub mod inspector_window; pub mod key_state_overlay; pub mod resize_overlay; pub mod search_overlay; diff --git a/crates/ghostty-apprt-gtk/src/class/surface.rs b/crates/ghostty-apprt-gtk/src/class/surface.rs index f79101a..e69ac36 100644 --- a/crates/ghostty-apprt-gtk/src/class/surface.rs +++ b/crates/ghostty-apprt-gtk/src/class/surface.rs @@ -38,6 +38,7 @@ use gtk::{gdk, gio, glib}; use crate::class::application::Application; use crate::class::clipboard_confirmation_dialog::ClipboardConfirmationDialog; use crate::class::config::Config; +use crate::class::inspector_window::InspectorWindow; use crate::class::key_state_overlay::KeyStateOverlay; use crate::class::resize_overlay::ResizeOverlay; use crate::class::search_overlay::SearchOverlay; @@ -246,18 +247,66 @@ impl Surface { .is_ok() } - /// Show/hide the inspector. - /// - /// TODO(inspector): the inspector window is deferred; this is a no-op - /// that reports success like Zig does after (not) creating a window. + /// Show/hide the inspector (Zig: `controlInspector`). 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 } /// 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. pub fn key_sequence_action(&self, value: &KeySequence) { @@ -963,33 +1012,38 @@ impl Surface { 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 mut env: Vec<(String, String)> = std::env::vars().collect(); - let remove = |env: &mut Vec<(String, String)>, k: &str| env.retain(|(n, _)| n != k); + // Byte-preserving environment (Zig `internal_os.getEnvMap`). + let mut env = ghostty_os::env::get_env_map(); - remove(&mut env, "LANG"); 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. - remove(&mut env, "GDK_DEBUG"); - remove(&mut env, "GDK_DISABLE"); - remove(&mut env, "GSK_RENDERER"); + env.remove("GDK_DEBUG"); + env.remove("GDK_DISABLE"); + env.remove("GSK_RENDERER"); // Remove some environment variables that are set when Ghostty is // launched from a `.desktop` file, by D-Bus activation, or systemd. - remove(&mut env, "GIO_LAUNCHED_DESKTOP_FILE"); - remove(&mut env, "GIO_LAUNCHED_DESKTOP_FILE_PID"); - remove(&mut env, "DBUS_STARTER_ADDRESS"); - remove(&mut env, "DBUS_STARTER_BUS_TYPE"); - remove(&mut env, "INVOCATION_ID"); - remove(&mut env, "JOURNAL_STREAM"); - remove(&mut env, "NOTIFY_SOCKET"); + env.remove("GIO_LAUNCHED_DESKTOP_FILE"); + env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID"); + env.remove("DBUS_STARTER_ADDRESS"); + env.remove("DBUS_STARTER_BUS_TYPE"); + env.remove("INVOCATION_ID"); + env.remove("JOURNAL_STREAM"); + env.remove("NOTIFY_SOCKET"); - // Snap path filtering (`build_config.snap`) is a build-time option - // of the Zig build and is not supported here. + // Unset environment varies set by snaps if we're running in a snap. + // 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 // Window class. The right solution we should do is emit a signal @@ -999,16 +1053,20 @@ impl Surface { .ancestor(Window::static_type()) .and_then(|w| w.downcast::().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}"); } + for (k, v) in extra { + env.insert(k, v); + } if window.is_quick_terminal() { - env.retain(|(n, _)| n != "GHOSTTY_QUICK_TERMINAL"); - env.push(("GHOSTTY_QUICK_TERMINAL".to_owned(), "1".to_owned())); + env.insert("GHOSTTY_QUICK_TERMINAL", "1"); } } - env + env.into_iter().collect() } 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() .map(|s| s.default_termio_env()) .unwrap_or_default() @@ -2365,6 +2423,9 @@ mod imp { /// The source that handles setting our child property. pub idle_rechild: Cell>, + /// A weak reference to an inspector window. + pub inspector: glib::WeakRef, + pub action_group: RefCell>, pub vadj_handler: RefCell>, @@ -2457,6 +2518,7 @@ mod imp { precision_scroll: Cell::new(false), progress_bar_timer: Cell::new(None), idle_rechild: Cell::new(None), + inspector: glib::WeakRef::new(), action_group: RefCell::new(None), vadj_handler: RefCell::new(None), context: Cell::new(NewSurfaceContext::Window), @@ -2632,6 +2694,15 @@ mod imp { } 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() { adj.disconnect(id); } diff --git a/crates/ghostty-apprt-gtk/src/class/window.rs b/crates/ghostty-apprt-gtk/src/class/window.rs index 0839c24..be913bf 100644 --- a/crates/ghostty-apprt-gtk/src/class/window.rs +++ b/crates/ghostty-apprt-gtk/src/class/window.rs @@ -1733,10 +1733,10 @@ impl Window { /// Toggle the Ghostty inspector for the active surface. fn toggle_inspector(&self) { - // TODO(inspector): deferred; the inspector is not ported yet. - if let Some(surface) = self.get_active_surface() { - log::debug!("toggle-inspector requested for {surface:?} (inspector deferred)"); - } + let Some(surface) = self.get_active_surface() else { + return; + }; + surface.control_inspector(ghostty_apprt::action::Inspector::Toggle); } } diff --git a/crates/ghostty-apprt-gtk/src/debug_input.rs b/crates/ghostty-apprt-gtk/src/debug_input.rs index 494d512..1880fee 100644 --- a/crates/ghostty-apprt-gtk/src/debug_input.rs +++ b/crates/ghostty-apprt-gtk/src/debug_input.rs @@ -14,6 +14,10 @@ //! text hello world # IM commit of the rest of the line //! wait 200 # milliseconds //! 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 //! ``` //! @@ -34,12 +38,15 @@ use crate::class::surface::Surface; use crate::class::window::Window; /// One parsed command. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Step { Key(String), Text(String), Wait(u64), Screenshot(String), + InspectorScreenshot(String), + InspectorClick(f64, f64), + InspectorScroll(f64, f64, f64), Quit, } @@ -59,6 +66,31 @@ pub fn parse(script: &str) -> Result, String> { "text" if !rest.is_empty() => Step::Text(rest.to_owned()), "wait" => Step::Wait(rest.parse().map_err(|_| err("wait needs milliseconds"))?), "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 = rest + .split_whitespace() + .map(str::parse) + .collect::>() + .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 = rest + .split_whitespace() + .map(str::parse) + .collect::>() + .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, _ => return Err(err(&format!("bad command {line:?}"))), }); @@ -190,6 +222,30 @@ impl Runner { Some(s) => s.debug_screenshot(&path), 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] fn parse_script() { - let s = - "# c\n\nkey ctrl+shift+t\ntext hello world\nwait 200\nscreenshot /tmp/a.png\nquit\n"; + 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"; assert_eq!( parse(s).expect("valid script"), vec![ @@ -237,6 +292,9 @@ mod tests { Step::Text("hello world".into()), Step::Wait(200), 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 ] ); diff --git a/crates/ghostty-apprt-gtk/src/winproto/wayland.rs b/crates/ghostty-apprt-gtk/src/winproto/wayland.rs index eed0ea3..f679cd3 100644 --- a/crates/ghostty-apprt-gtk/src/winproto/wayland.rs +++ b/crates/ghostty-apprt-gtk/src/winproto/wayland.rs @@ -81,6 +81,69 @@ struct State { xdg_wm_dialog_present: bool, /// Activation tokens requested but not yet `done`. 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, + 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 { @@ -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( globals: &wayland_client::globals::GlobalList, qh: &QueueHandle, @@ -162,6 +233,22 @@ impl App { state.kde_decoration_manager = bind(&globals, &qh); state.kde_slide_manager = 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 queue @@ -530,14 +617,21 @@ fn entered_monitor(host: &dyn WindowHost, monitor: &gdk::Monitor) { impl Dispatch for State { fn event( - _: &mut Self, - _: &WlRegistry, - _: wayland_client::protocol::wl_registry::Event, + state: &mut Self, + registry: &WlRegistry, + event: wayland_client::protocol::wl_registry::Event, _: &GlobalListContents, _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, ) { - // 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), + _ => {} + } } } diff --git a/crates/ghostty-apprt/Cargo.toml b/crates/ghostty-apprt/Cargo.toml index d61fc5c..ccef9e7 100644 --- a/crates/ghostty-apprt/Cargo.toml +++ b/crates/ghostty-apprt/Cargo.toml @@ -9,6 +9,7 @@ ghostty-vt.workspace = true ghostty-input.workspace = true ghostty-config.workspace = true ghostty-datastruct.workspace = true +ghostty-inspector.workspace = true ghostty-terminal.workspace = true ghostty-renderer.workspace = true thiserror = "2.0.21" diff --git a/crates/ghostty-apprt/src/runtime_api.rs b/crates/ghostty-apprt/src/runtime_api.rs index 929cae7..4541d5c 100644 --- a/crates/ghostty-apprt/src/runtime_api.rs +++ b/crates/ghostty-apprt/src/runtime_api.rs @@ -242,11 +242,19 @@ pub trait CoreSurface { 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> { Ok(()) } + /// `deactivateInspector`: stop collecting inspection data. 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. @@ -278,7 +286,7 @@ pub trait RtSurface { fn set_clipboard(&self, clipboard: Clipboard, contents: &[(String, String)], confirm: bool); /// `defaultTermioEnv`: env vars to set for the child (GDK/GTK related /// 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) {} /// Resolve an OpenGL function pointer for the surface's GL context /// (GTK: dlsym on libepoxy). Called with the context current. Null if diff --git a/crates/ghostty-cli/Cargo.toml b/crates/ghostty-cli/Cargo.toml index 9bee436..19d292c 100644 --- a/crates/ghostty-cli/Cargo.toml +++ b/crates/ghostty-cli/Cargo.toml @@ -14,6 +14,7 @@ ghostty-os.workspace = true ghostty-vt.workspace = true log = "0.4.34" ratatui = "0.30.2" +termina = "0.3.3" thiserror = "2.0.21" unicode-width = "0.2.2" diff --git a/crates/ghostty-cli/src/action.rs b/crates/ghostty-cli/src/action.rs index 19f086c..a9c7049 100644 --- a/crates/ghostty-cli/src/action.rs +++ b/crates/ghostty-cli/src/action.rs @@ -53,7 +53,7 @@ pub trait ActionEnum: Sized + Copy { /// Detect the action from the process CLI args (Zig: `detectArgs`). pub fn detect_args() -> Result, DetectError> { - detect_iter(std::env::args()) + detect_iter(ghostty_os::args::collect_lossy()) } /// Detect the action from any iterator. Each iterator value should yield diff --git a/crates/ghostty-cli/src/build_info.rs b/crates/ghostty-cli/src/build_info.rs index f112245..489eb3c 100644 --- a/crates/ghostty-cli/src/build_info.rs +++ b/crates/ghostty-cli/src/build_info.rs @@ -49,15 +49,11 @@ pub struct BuildInfo { impl Default for BuildInfo { fn default() -> Self { BuildInfo { - version_string: "1.3.1".to_string(), - commit: None, - release_channel: "stable".to_string(), - build_mode: if cfg!(debug_assertions) { - ".Debug" - } else { - ".ReleaseFast" - } - .to_string(), + // Derived by `build.rs` (git describe / workspace app-version). + version_string: env!("GHOSTTY_VERSION_STRING").to_string(), + commit: Some(env!("GHOSTTY_COMMIT").to_string()).filter(|c| !c.is_empty()), + release_channel: env!("GHOSTTY_RELEASE_CHANNEL").to_string(), + build_mode: env!("GHOSTTY_BUILD_MODE").to_string(), app_runtime: ".gtk".to_string(), font_backend: ".fontconfig_freetype".to_string(), renderer: "renderer.generic.Renderer(renderer.OpenGL)".to_string(), diff --git a/crates/ghostty-cli/src/ghostty.rs b/crates/ghostty-cli/src/ghostty.rs index 0d6cf4c..c4c6bfd 100644 --- a/crates/ghostty-cli/src/ghostty.rs +++ b/crates/ghostty-cli/src/ghostty.rs @@ -112,6 +112,37 @@ impl Action { 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() -> 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::(), + Action::ListFonts => of::(), + Action::ListKeybinds => of::(), + Action::ListThemes => of::(), + Action::ListColors => of::(), + Action::ListActions => of::(), + Action::SshCache => of::(), + Action::ShowConfig => of::(), + Action::ValidateConfig => of::(), + Action::ShowFace => of::(), + // `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` /// function, i.e. `help_strings.Action.`), without trailing newline. #[must_use] diff --git a/crates/ghostty-cli/src/list_themes/preview.rs b/crates/ghostty-cli/src/list_themes/preview.rs index 8d99129..402e918 100644 --- a/crates/ghostty-cli/src/list_themes/preview.rs +++ b/crates/ghostty-cli/src/list_themes/preview.rs @@ -3,24 +3,29 @@ //! implementation with the same layout, key bindings and behaviour. //! //! Known differences from the vaxis version: -//! * hyperlinks (OSC 8) on theme names/paths are not emitted, -//! * double/dashed/dotted/curly underlines are drawn as single underlines, -//! * the terminal color scheme is not queried live: it defaults to light +//! * input is read with `termina` (not crossterm) so terminal responses +//! such as the color-scheme report are visible; drawing still goes +//! 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. use std::cell::RefCell; +use std::fmt::Write as _; use std::io::Write; use ghostty_config::config::Config; use ghostty_config::types_a::TerminalColor; +use ratatui::Frame; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{ - self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, - MouseEventKind, + Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }; use ratatui::layout::{Position, Rect}; use ratatui::style::{Color, Modifier, Style}; -use ratatui::{Frame, crossterm}; use super::{ ColorScheme, SMALL_LIST_THRESHOLD, ThemeListElement, absolute_path, should_include_theme, @@ -189,6 +194,146 @@ fn base64(data: &[u8]) -> String { 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, + ul: Option
    , +} + +/// 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, + pub ul: Option
      , +} + +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 = 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. enum Loaded { Ok(Box), @@ -222,6 +367,7 @@ pub struct Preview { /// Where `w` writes the auto theme file (overridable for tests). pub auto_theme_path: Option, effects: Vec, + overlays: Vec, } fn initial_scheme() -> Scheme { @@ -257,6 +403,7 @@ impl Preview { loaded: None, auto_theme_path: None, effects: vec![], + overlays: vec![], }; p.update_filtered(); p @@ -535,7 +682,12 @@ impl Preview { pub fn draw(&mut self, buf: &mut Buffer) -> Option { let area = buf.area; 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(); let theme_list = win.child(0, 0, LIST_WIDTH.min(area.width), area.height); @@ -607,7 +759,7 @@ impl Preview { if style == St::Selected { theme_list.print("\u{276f} ", self.ui_selected(), row, 0); } - theme_list.print( + theme_list.print_ex( &theme.theme, match style { St::Normal => self.ui_standard(), @@ -616,6 +768,8 @@ impl Preview { }, row, 2, + Some(&theme.to_uri()), + None, ); if style == St::Selected { 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::>(); 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> { let theme = self.selected()?.clone(); let stale = self.loaded.as_ref().is_none_or(|(p, _)| p != &theme.path); @@ -785,11 +954,13 @@ impl Preview { middle.saturating_sub(1), (child.area.width / 2).saturating_sub(gw(&text) / 2), ); - child.print( + child.print_ex( &theme.path, ui_err, middle, (child.area.width / 2).saturating_sub(gw(&theme.path) / 2), + Some(&theme.to_uri()), + None, ); child.print( err, @@ -821,8 +992,8 @@ impl Preview { let standard_bold = standard.add_modifier(Modifier::BOLD); let standard_italic = standard.add_modifier(Modifier::ITALIC); let standard_bold_italic = standard.add_modifier(Modifier::BOLD | Modifier::ITALIC); - // TODO: vaxis supports double/dashed/dotted/curly underlines. - let standard_underline = standard.add_modifier(Modifier::UNDERLINED); + // Single underline is a plain SGR 4; the other styles (double, + // dashed, dotted, curly) are re-emitted as raw `SGR 4:N` overlays. let color = |i: usize| { let c = config.palette.value[i]; @@ -833,17 +1004,22 @@ impl Preview { { let child = win.child(x_off, next_start, width, 4); child.fill(standard); - child.print( + let uri = theme.to_uri(); + child.print_ex( &theme.theme, standard_bold_italic, 1, (child.area.width / 2).saturating_sub(gw(&theme.theme) / 2), + Some(&uri), + None, ); - child.print( + child.print_ex( &theme.path, standard, 2, (child.area.width / 2).saturating_sub(gw(&theme.path) / 2), + Some(&uri), + None, ); next_start += child.area.height; } @@ -904,7 +1080,6 @@ impl Preview { (c(1), c(2), c(3), c(4), c(5), c(6)); let color6ul = color6.add_modifier(Modifier::UNDERLINED); let (color10, color12, color238) = (c(10), c(12), c(238)); - let _ = standard_underline; child.fill(standard); let rule = |row: u16, head: &str| { @@ -1174,19 +1349,32 @@ impl Preview { row += 1; col = 2; } + let mut ul = None; let style = match word { "ipsum" => Style::default().fg(color(2)).bg(bg), "consectetur" => standard_bold, "reprehenderit" => standard_italic, "Praesent" => standard_bold_italic, - "auctor" | "dui" | "erat" | "enim" | "odio" => { - // TODO: distinct underline styles (double, dashed, - // dotted, curly) are drawn as single underlines. - standard.add_modifier(Modifier::UNDERLINED) + "auctor" => standard.add_modifier(Modifier::UNDERLINED), + "dui" => { + ul = Some(Ul::Double); + standard + } + "erat" => { + ul = Some(Ul::Dashed); + standard + } + "enim" => { + ul = Some(Ul::Dotted); + standard + } + "odio" => { + ul = Some(Ul::Curly); + 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); } } @@ -1245,6 +1433,7 @@ fn gw(s: &str) -> u16 { #[derive(Clone, Copy)] struct Win<'a, 'b> { buf: &'a RefCell<&'b mut Buffer>, + decos: &'a RefCell>, area: Rect, } @@ -1267,6 +1456,12 @@ impl Win<'_, '_> { /// `Window.fill` with a blank cell of the given 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(); for y in self.area.top()..self.area.bottom() { for x in self.area.left()..self.area.right() { @@ -1287,6 +1482,7 @@ impl Win<'_, '_> { let h = height.min(self.area.bottom() - y); Win { buf: self.buf, + decos: self.decos, area: Rect::new(x, y, w, h), } } @@ -1323,6 +1519,21 @@ impl Win<'_, '_> { /// `Window.printSegment` without wrapping (text is clipped). Returns the /// column after the text. 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
        , + ) -> u16 { if row >= self.area.height || col >= self.area.width { return col; } @@ -1330,6 +1541,15 @@ impl Win<'_, '_> { let y = self.area.y + row; let max = usize::from(self.area.width - col); 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 } @@ -1345,37 +1565,158 @@ impl Win<'_, '_> { // ---- 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 { + 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. pub fn preview(themes: Vec, theme_filter: ColorScheme) -> Result<(), Error> { let mut app = Preview::new(themes, theme_filter); - let mut terminal = ratatui::try_init()?; - let result = run_loop(&mut terminal, &mut app); - ratatui::restore(); - result + run_loop(&mut app) } -fn run_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut Preview) -> Result<(), Error> { - use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; - use crossterm::terminal::SetTitle; +fn run_loop(app: &mut Preview) -> Result<(), Error> { + use ratatui::Terminal as RatTerminal; + use ratatui::backend::CrosstermBackend; + use termina::escape::csi::{Csi, DecPrivateMode, DecPrivateModeCode, Mode}; + use termina::{PlatformTerminal, Terminal as _}; - { - let out = terminal.backend_mut(); - crossterm::execute!( - out, - SetTitle("\u{1f47b} Ghostty Theme Preview \u{1f47b}"), - EnableMouseCapture - )?; - } + let mut tty = PlatformTerminal::new()?; + tty.enter_raw_mode()?; + // 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> { 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. - let first = event::read()?; - app.update(&first); - while event::poll(std::time::Duration::ZERO)? { - let ev = event::read()?; - app.update(&ev); + let first = tty.read(|_| true)?; + handle_terminal_event(app, first); + while tty.poll(|_| true, Some(std::time::Duration::ZERO))? { + let ev = tty.read(|_| true)?; + handle_terminal_event(app, ev); } for effect in app.take_effects() { match effect { @@ -1390,15 +1731,23 @@ fn run_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut Preview) -> Resul 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 } -fn draw_frame(app: &mut Preview, f: &mut Frame<'_>) { +fn draw_frame(app: &mut Preview, f: &mut Frame<'_>) -> Option { let cursor = app.draw(f.buffer_mut()); if let Some(pos) = cursor { f.set_cursor_position(pos); } + cursor } #[cfg(test)] @@ -1617,6 +1966,86 @@ mod tests { 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] fn draw_help_and_save_and_search_overlays() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ghostty-cli/src/version.rs b/crates/ghostty-cli/src/version.rs index 253f055..02beed8 100644 --- a/crates/ghostty-cli/src/version.rs +++ b/crates/ghostty-cli/src/version.rs @@ -92,7 +92,13 @@ mod tests { #[test] fn version_output_layout() { 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(); assert!(s.starts_with( "Ghostty 1.3.1\n\nVersion\n - version: 1.3.1\n - channel: stable\nBuild Config\n" diff --git a/crates/ghostty-config/src/cli_args.rs b/crates/ghostty-config/src/cli_args.rs index cfff20a..621a1bc 100644 --- a/crates/ghostty-config/src/cli_args.rs +++ b/crates/ghostty-config/src/cli_args.rs @@ -65,11 +65,39 @@ impl From 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. pub trait ConfigField: Clone + PartialEq + Default + fmt::Debug { /// For enum types, the list of valid values (used in error messages). 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 /// `self`. Repeatable types accumulate; scalar types overwrite. fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError>; @@ -424,6 +452,8 @@ pub fn parse_int_base0(s: &str) -> Option { // ConfigField impls for primitives impl ConfigField for bool { + const KIND: FieldKind = FieldKind::Bool; + fn parse_cli(&mut self, value: Option<&str>) -> Result<(), ArgError> { *self = parse_bool(value.unwrap_or("t"))?; Ok(()) @@ -481,6 +511,12 @@ impl ConfigField for String { } impl ConfigField for Option { + 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> { // For optional fields, we just treat it as the child type. if let Some(v) = self { @@ -550,8 +586,9 @@ impl> ArgIter for ArgsIterator { /// Create an args iterator for the process args. This will skip argv0. #[must_use] -pub fn args_iterator() -> ArgsIterator> { - ArgsIterator::new(std::env::args().skip(1)) +pub fn args_iterator() -> ArgsIterator>> { + // 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. diff --git a/crates/ghostty-config/src/macros.rs b/crates/ghostty-config/src/macros.rs index e0d0d5a..d6b1a98 100644 --- a/crates/ghostty-config/src/macros.rs +++ b/crates/ghostty-config/src/macros.rs @@ -31,6 +31,8 @@ pub struct FieldInfo { pub name: &'static str, /// Raw doc comment lines. 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. pub eq: fn(&T, &T) -> bool, /// Format this field as in `+show-config`. @@ -139,6 +141,7 @@ macro_rules! config_struct { $crate::macros::FieldInfo { name: $key, doc: &[$($doc),*], + kind: <$ty as $crate::cli_args::ConfigField>::KIND, eq: |a: &$name, b: &$name| a.$field == b.$field, format: |a: &$name, name: &str, w: &mut dyn ::core::fmt::Write| { let mut f = $crate::formatter::EntryFormatter::new(name, w); @@ -268,6 +271,9 @@ macro_rules! config_packed { } impl $crate::cli_args::ConfigField for $name { + const KIND: $crate::cli_args::FieldKind = + $crate::cli_args::FieldKind::Packed(Self::NAMES); + fn parse_cli( &mut self, value: ::core::option::Option<&str>, diff --git a/crates/ghostty-config/src/path.rs b/crates/ghostty-config/src/path.rs index 582f951..5321fa4 100644 --- a/crates/ghostty-config/src/path.rs +++ b/crates/ghostty-config/src/path.rs @@ -206,6 +206,8 @@ impl 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> { let Some(item) = Path::parse(input)? else { self.value.clear(); diff --git a/crates/ghostty-core/Cargo.toml b/crates/ghostty-core/Cargo.toml index 5f9b91c..8a45cac 100644 --- a/crates/ghostty-core/Cargo.toml +++ b/crates/ghostty-core/Cargo.toml @@ -11,6 +11,7 @@ ghostty-apprt.workspace = true ghostty-config.workspace = true ghostty-datastruct.workspace = true ghostty-font.workspace = true +ghostty-inspector.workspace = true ghostty-input.workspace = true ghostty-os.workspace = true ghostty-renderer.workspace = true diff --git a/crates/ghostty-core/src/surface/keyboard.rs b/crates/ghostty-core/src/surface/keyboard.rs index 56874ff..f1cb158 100644 --- a/crates/ghostty-core/src/surface/keyboard.rs +++ b/crates/ghostty-core/src/surface/keyboard.rs @@ -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::key_encode; use ghostty_input::{Action as KeyAction, Key, KeyEvent, Mods}; +use ghostty_inspector as inspector; use ghostty_terminal::terminal::{MouseEvents, ScrollViewport, Terminal}; use ghostty_termio::message::Message as IoMessage; use ghostty_vt::modes::Mode; @@ -331,10 +332,40 @@ impl Surface { // level. 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 { // Handle keybindings first. We need to handle this on all events // (press, repeat, release) because a press may perform a binding but // 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); } @@ -359,7 +390,7 @@ impl Surface { // highlight state. let mouse_mods = self.mouse.borrow().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 @@ -381,7 +412,7 @@ impl Surface { *pressed = 'event: { // We need to unset the allocated fields that will become // invalid - let mut copy = OwnedKeyEvent::from_event(&event); + let mut copy = OwnedKeyEvent::from_event(event); copy.utf8.clear(); // 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 // 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. return Ok(InputEffect::Ignored); }; @@ -491,6 +522,7 @@ impl Surface { &self, cfg: &DerivedConfig, event: &KeyEvent<'_>, + mut insp_ev: Option<&mut inspector::KeyEvent>, ) -> Option { match event.action { // 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 // invalid. We don't need to cap this because it is // 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); } @@ -683,6 +715,9 @@ impl Surface { // Store our last trigger so we don't encode the release event self.keyboard.borrow_mut().last_trigger = Some(event.binding_hash()); + if let Some(ev) = insp_ev { + ev.binding = actions; + } return Some(InputEffect::Consumed); } @@ -869,7 +904,11 @@ impl Surface { /// Encodes the key event into a write request. Returns `None` if /// nothing was encoded. - fn encode_key(&self, event: &KeyEvent<'_>) -> Option { + fn encode_key( + &self, + event: &KeyEvent<'_>, + insp_ev: Option<&mut inspector::KeyEvent>, + ) -> Option { // Build our encoding options, which requires the lock. let opts = { let t = self.lock_terminal(); @@ -887,6 +926,11 @@ impl Surface { 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)) } diff --git a/crates/ghostty-core/src/surface/mod.rs b/crates/ghostty-core/src/surface/mod.rs index 4623f88..eeff940 100644 --- a/crates/ghostty-core/src/surface/mod.rs +++ b/crates/ghostty-core/src/surface/mod.rs @@ -37,6 +37,7 @@ pub mod binding; pub mod clipboard; pub mod config; +pub mod inspector; pub mod keyboard; pub mod mouse; @@ -251,6 +252,9 @@ pub struct Surface { /// Used to rate limit BEL handling. last_bell_time: Cell>, + /// The terminal inspector, if active (Zig: `inspector: ?*Inspector`). + pub(crate) inspector: RefCell>>, + /// Debug screenshot state (see `debug_screenshot`). created: Instant, debug_shot_done: Cell, @@ -474,6 +478,7 @@ impl Surface { command_timer: Cell::new(None), search: RefCell::new(None), last_bell_time: Cell::new(None), + inspector: RefCell::new(None), created: Instant::now(), debug_shot_done: Cell::new(false), }); @@ -1798,7 +1803,7 @@ impl Drop for Surface { /// (`rt_surface.defaultTermioEnv()`, which already removed the variables it /// does not want to leak); only if it provides nothing (the Zig error path) /// do we fall back to the process environment (`getEnvMap`). -fn rt_surface_env(rt: &dyn RtSurface) -> std::collections::HashMap { +fn rt_surface_env(rt: &dyn RtSurface) -> ghostty_os::EnvMap { let rt_env = rt.default_termio_env(); if rt_env.is_empty() { return ghostty_os::env::get_env_map(); @@ -1960,4 +1965,16 @@ impl CoreSurface for Surface { 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); + } } diff --git a/crates/ghostty-core/src/surface/mouse.rs b/crates/ghostty-core/src/surface/mouse.rs index 8fcb158..157d440 100644 --- a/crates/ghostty-core/src/surface/mouse.rs +++ b/crates/ghostty-core/src/surface/mouse.rs @@ -975,6 +975,13 @@ impl Surface { ) -> Result { 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 let hidden = { let mut m = self.mouse.borrow_mut(); @@ -1915,6 +1922,10 @@ impl Surface { // The mouse position in the viewport 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 // reprocessed below. But we need the old value to know if we need to // undo mouse shape changes. @@ -2217,6 +2228,15 @@ fn button_from_index(i: usize) -> Option { }) } +/// 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)] mod tests { use super::*; diff --git a/crates/ghostty-core/tests/common/mod.rs b/crates/ghostty-core/tests/common/mod.rs index ad16441..d79364e 100644 --- a/crates/ghostty-core/tests/common/mod.rs +++ b/crates/ghostty-core/tests/common/mod.rs @@ -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 // want to leak (here HOME stands in for GDK_DISABLE & co), plus extras. - let mut env: Vec<(String, String)> = - std::env::vars().filter(|(k, _)| k != "HOME").collect(); - env.push(("TERM_PROGRAM_TEST".to_owned(), "1".to_owned())); + let mut env: Vec<(std::ffi::OsString, std::ffi::OsString)> = + std::env::vars_os().filter(|(k, _)| k != "HOME").collect(); + env.push(("TERM_PROGRAM_TEST".into(), "1".into())); env } fn gl_proc_address(&self, name: &str) -> *const std::ffi::c_void { diff --git a/crates/ghostty-input/Cargo.toml b/crates/ghostty-input/Cargo.toml index c54e64e..9b09bbf 100644 --- a/crates/ghostty-input/Cargo.toml +++ b/crates/ghostty-input/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] ghostty-vt.workspace = true +ghostty-unicode.workspace = true indexmap = "2.14.2" thiserror = "2.0.21" diff --git a/crates/ghostty-input/src/binding/action.rs b/crates/ghostty-input/src/binding/action.rs index 493edf7..7d2996f 100644 --- a/crates/ghostty-input/src/binding/action.rs +++ b/crates/ghostty-input/src/binding/action.rs @@ -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 { match self { ActionKey::Ignore => { @@ -1438,6 +1448,7 @@ Valid arguments are: Adjust the selection to the beginning or the end of the line respectively. + "## } ActionKey::JumpToPrompt => { @@ -1468,6 +1479,7 @@ Valid actions are: The default OS editor is determined by using `open` on macOS and `xdg-open` on Linux. + "## } ActionKey::WriteScreenFile => { @@ -1574,6 +1586,7 @@ Valid arguments: Creates a new split along the larger direction. For example, if the parent split is currently wider than it is tall, then a left-right split would be created, and vice versa. + "## } ActionKey::GotoSplit => { @@ -1940,6 +1953,7 @@ The value determines the crash location: - `render` Crash on the render thread for the focused surface. + "## } } diff --git a/crates/ghostty-input/src/binding/mod.rs b/crates/ghostty-input/src/binding/mod.rs index 2a4ff89..cc5f3e4 100644 --- a/crates/ghostty-input/src/binding/mod.rs +++ b/crates/ghostty-input/src/binding/mod.rs @@ -498,10 +498,8 @@ pub const BACKWARDS_COMPATIBLE_KEYS: &[(&str, TriggerKey)] = { /// The codepoint we use for comparisons. Case folding can result /// in more codepoints so we need to use a 3 element array. /// -/// DEVIATION: Zig uses uucode's full case folding table. Here we use -/// `char::to_lowercase` (single-char results only) plus a few simple-folding -/// special cases; multi-codepoint foldings return the codepoint as-is exactly -/// like Zig does. +/// Uses the full Unicode case folding table (uucode's `case_folding_full`, +/// generated by `ghostty-unicode`). fn folded_codepoint(cp: u32) -> [u32; 3] { // ASCII fast path 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. - // If more codepoints are produced then we return the codepoint - // 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], - } + ghostty_unicode::case_folding::fold_full(cp) } impl Trigger { diff --git a/crates/ghostty-input/src/binding/tests.rs b/crates/ghostty-input/src/binding/tests.rs index 79d9b92..961fa3f 100644 --- a/crates/ghostty-input/src/binding/tests.rs +++ b/crates/ghostty-input/src/binding/tests.rs @@ -1507,6 +1507,20 @@ fn trigger_hash_folds_case() { 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] fn trigger_display_roundtrip() { for s in ["super+ctrl+alt+shift+a", "catch_all", "ctrl+arrow_up", "+"] { diff --git a/crates/ghostty-input/src/helpgen_actions.rs b/crates/ghostty-input/src/helpgen_actions.rs index 30d6003..9725487 100644 --- a/crates/ghostty-input/src/helpgen_actions.rs +++ b/crates/ghostty-input/src/helpgen_actions.rs @@ -94,7 +94,7 @@ pub fn generate(writer: &mut String, format: Format, show_docs: bool) { } 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() { // If it is the last line and empty, then skip it. if iter.peek().is_none() && s.is_empty() { diff --git a/crates/ghostty-input/src/key.rs b/crates/ghostty-input/src/key.rs index ff42d88..1b4b76f 100644 --- a/crates/ghostty-input/src/key.rs +++ b/crates/ghostty-input/src/key.rs @@ -1,7 +1,8 @@ //! Key events and key codes (ZIG/src/input/key.zig). //! -//! The Zig `imguiKey` helper (cimgui constants) is intentionally not ported; -//! the inspector is deferred (see notes). +//! The Zig `imguiKey` helper (cimgui constants) lives in the inspector crate +//! (`ghostty_inspector::keys::imgui_key`) so that this crate stays free of +//! the ImGui dependency. pub use crate::key_mods::Mods; diff --git a/crates/ghostty-onig/Cargo.toml b/crates/ghostty-onig/Cargo.toml new file mode 100644 index 0000000..2a53b23 --- /dev/null +++ b/crates/ghostty-onig/Cargo.toml @@ -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" diff --git a/crates/ghostty-onig/src/lib.rs b/crates/ghostty-onig/src/lib.rs new file mode 100644 index 0000000..0fc8845 --- /dev/null +++ b/crates/ghostty-onig/src/lib.rs @@ -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) -> Result, 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)); + } +} diff --git a/crates/ghostty-os/src/args.rs b/crates/ghostty-os/src/args.rs index 2cf10ae..10abfe5 100644 --- a/crates/ghostty-os/src/args.rs +++ b/crates/ghostty-os/src/args.rs @@ -9,6 +9,17 @@ pub fn iterator() -> std::env::ArgsOs { 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 { + lossy(iterator()) +} + +fn lossy(it: impl Iterator) -> Vec { + it.map(|a| a.to_string_lossy().into_owned()).collect() +} + #[cfg(test)] mod tests { use super::*; @@ -18,4 +29,15 @@ mod tests { let mut iter = iterator(); 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()]); + } } diff --git a/crates/ghostty-os/src/env.rs b/crates/ghostty-os/src/env.rs index 937929d..f6fc7f9 100644 --- a/crates/ghostty-os/src/env.rs +++ b/crates/ghostty-os/src/env.rs @@ -1,24 +1,153 @@ use std::collections::HashMap; +use std::ffi::{OsStr, OsString}; use crate::flatpak::is_flatpak; /// PATH-style delimiter (`std.fs.path.delimiter` on POSIX). 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); + +impl EnvMap { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, key: impl AsRef, value: impl AsRef) -> Option { + self.0 + .insert(key.as_ref().to_owned(), value.as_ref().to_owned()) + } + + pub fn get(&self, key: impl AsRef) -> Option<&OsStr> { + self.0.get(key.as_ref()).map(OsString::as_os_str) + } + + pub fn remove(&mut self, key: impl AsRef) -> Option { + self.0.remove(key.as_ref()) + } + + pub fn contains_key(&self, key: impl AsRef) -> 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 { + self.0.iter().map(|(k, v)| (k.as_os_str(), v.as_os_str())) + } +} + +impl> std::ops::Index for EnvMap { + type Output = OsStr; + + fn index(&self, key: K) -> &OsStr { + self.get(key).expect("no entry found for key") + } +} + +impl, V: Into> FromIterator<(K, V)> for EnvMap { + fn from_iter>(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; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + /// Get the environment map. Inside Flatpak this is empty (the sandbox -/// environment is not meaningful for host commands). -pub fn get_env_map() -> HashMap { +/// environment is not meaningful for host commands). Bytes are preserved. +pub fn get_env_map() -> EnvMap { 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 = 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. @@ -39,6 +168,28 @@ pub fn append_env_always(current: &str, value: &str) -> String { 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. pub fn prepend_env(current: &str, value: &str) -> String { // If there is no prior value, we return it as-is @@ -64,6 +215,35 @@ pub fn getenv_not_empty(key: &str) -> Option { #[cfg(test)] mod tests { 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] fn append_env_empty() { @@ -75,6 +255,16 @@ mod tests { 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] fn prepend_env_empty() { assert_eq!(prepend_env("", "foo"), "foo"); diff --git a/crates/ghostty-os/src/flatpak.rs b/crates/ghostty-os/src/flatpak.rs index aab066c..2224407 100644 --- a/crates/ghostty-os/src/flatpak.rs +++ b/crates/ghostty-os/src/flatpak.rs @@ -84,7 +84,7 @@ pub struct FlatpakHostCommand { /// Environment variables for the child process. If this is None, this /// does not send any environment variables. - pub env: Option>, + pub env: Option, /// 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 @@ -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, /// the pid will be available. This must only be called with the /// state in "init". @@ -128,7 +120,19 @@ impl FlatpakHostCommand { let request = Request { argv: self.argv.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: [ self.stdin .try_clone() @@ -213,6 +217,38 @@ impl FlatpakHostCommand { 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, +} + +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 /// command is not in the started state. pub fn signal(&self, sig: u8, pg: bool) -> Result<(), Error> { @@ -392,6 +428,23 @@ mod tests { 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] fn exit_status_extraction() { // Mirrors posix.W.EXITSTATUS on a raw wait status. diff --git a/crates/ghostty-os/src/lib.rs b/crates/ghostty-os/src/lib.rs index 895c48d..eff4c7b 100644 --- a/crates/ghostty-os/src/lib.rs +++ b/crates/ghostty-os/src/lib.rs @@ -47,7 +47,10 @@ pub(crate) mod testutil; // Functions and types (mirrors the flat exports of `os/main.zig`). pub use dbus::launched_by_dbus_activation; 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 flatpak::{FlatpakHostCommand, is_flatpak}; pub use homedir::{expand_home, home}; diff --git a/crates/ghostty-os/src/uri.rs b/crates/ghostty-os/src/uri.rs index b3daf4a..711aa5e 100644 --- a/crates/ghostty-os/src/uri.rs +++ b/crates/ghostty-os/src/uri.rs @@ -2,6 +2,7 @@ //! additional [`ParseOptions`] from `os/uri.zig`. use std::borrow::Cow; +use std::fmt::Write as _; #[derive(Debug, Clone, Copy, Default)] pub struct ParseOptions { @@ -47,15 +48,26 @@ impl Component<'_> { } } - /// The raw (percent-decoded) value (`toRaw`). Invalid escapes are left - /// as-is, matching `std.Uri.percentDecodeInPlace`. - pub fn to_raw(&self) -> Cow<'_, str> { + /// The raw (percent-decoded) value (`toRaw`) as bytes. Invalid escapes + /// are left as-is, matching `std.Uri.percentDecodeInPlace`. The decoded + /// 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 { - Component::Raw(s) => Cow::Borrowed(s.as_ref()), + Component::Raw(s) => Cow::Borrowed(s.as_bytes()), 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 /// text for `Raw` components. pub fn as_str(&self) -> &str { @@ -68,9 +80,9 @@ impl Component<'_> { /// Lenient percent decoding like `std.Uri.percentDecodeBackwards`/InPlace: /// 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('%') { - return Cow::Borrowed(s); + return Cow::Borrowed(s.as_bytes()); } let b = s.as_bytes(); let mut out = Vec::with_capacity(b.len()); @@ -87,7 +99,7 @@ fn percent_decode(s: &str) -> Cow<'_, str> { out.push(b[i]); i += 1; } - Cow::Owned(String::from_utf8_lossy(&out).into_owned()) + Cow::Owned(out) } fn hexval(c: u8) -> Option { @@ -318,6 +330,64 @@ pub fn parse(text: &str, options: ParseOptions) -> Result, ParseError> { 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, + /// Percent-decoded path bytes (or the full raw tail with + /// [`ParseOptions::raw_path`]). Not required to be UTF-8. + pub path: Vec, +} + +/// 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 { + // 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. fn is_valid_mac_address(s: &str) -> bool { 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] fn parse_raw_path() { let text = "file://localhost/path??#fragment"; diff --git a/crates/ghostty-renderer/src/generic.rs b/crates/ghostty-renderer/src/generic.rs index db59d20..537e5ec 100644 --- a/crates/ghostty-renderer/src/generic.rs +++ b/crates/ghostty-renderer/src/generic.rs @@ -1723,7 +1723,11 @@ impl Renderer { mouse_mods: st.mouse.mods, preedit, scrollbar, - overlay_features: st.overlay_features.clone(), + overlay_features: st + .inspector + .as_ref() + .map(|i| i.overlay_features()) + .unwrap_or_default(), } }; diff --git a/crates/ghostty-renderer/src/lib.rs b/crates/ghostty-renderer/src/lib.rs index 9201892..577c72a 100644 --- a/crates/ghostty-renderer/src/lib.rs +++ b/crates/ghostty-renderer/src/lib.rs @@ -25,6 +25,7 @@ pub mod options; pub mod overlay; pub mod row; pub mod shadertoy; +mod shadertoy_sampler; pub mod size; pub mod state; pub mod thread; diff --git a/crates/ghostty-renderer/src/shadertoy.rs b/crates/ghostty-renderer/src/shadertoy.rs index 2947953..81399f7 100644 --- a/crates/ghostty-renderer/src/shadertoy.rs +++ b/crates/ghostty-renderer/src/shadertoy.rs @@ -8,6 +8,7 @@ //! `docs/rewrite/notes/ghostty-renderer-R1.md` for the (small) source //! adaptations needed for naga's Vulkan-flavoured GLSL frontend. +use crate::shadertoy_sampler; use std::io::Read; use std::path::Path; @@ -250,7 +251,13 @@ pub fn spirv_from_glsl(src: &str, errlog: Option<&mut ShaderLog>) -> Result 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) .replace(" uniform ", " ") .replacen("layout(location = 0) in vec4 gl_FragCoord;", "", 1) @@ -276,6 +283,12 @@ fn adapt_for_naga(full: &str) -> String { "", 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 } @@ -406,6 +419,56 @@ mod tests { 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] fn load_from_files_skips_missing_optional() { let dir = diff --git a/crates/ghostty-renderer/src/shadertoy_sampler.rs b/crates/ghostty-renderer/src/shadertoy_sampler.rs new file mode 100644 index 0000000..3e0756a --- /dev/null +++ b/crates/ghostty-renderer/src/shadertoy_sampler.rs @@ -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> { + 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 { + 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>, +} + +struct FnInfo { + /// Per parameter: the sampler name, if the parameter is a `sampler2D`. + samplers: Vec>, +} + +/// 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 = 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 = 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 ¶ms { + 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 = HashMap::new(); + let mut def_names: Vec = 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;")); + } +} diff --git a/crates/ghostty-renderer/src/state.rs b/crates/ghostty-renderer/src/state.rs index 1c020ef..109c980 100644 --- a/crates/ghostty-renderer/src/state.rs +++ b/crates/ghostty-renderer/src/state.rs @@ -22,11 +22,11 @@ pub struct State { /// The terminal data. pub terminal: Terminal, - /// The overlay features currently requested by the terminal inspector - /// (Zig: `inspector: ?*Inspector` and `inspector.rendererInfo() - /// .overlayFeatures()`). Empty while the inspector is not active. - // TODO(integration): the inspector is deferred (Phase 7); it fills this. - pub overlay_features: Vec, + /// The terminal inspector, if any (Zig: `inspector: ?*Inspector`). This + /// is `None` while the inspector is not active. It is accessed under the + /// same lock as the terminal (lock order: this state, then the + /// inspector's own lock). + pub inspector: Option>, /// Dead key state. This will render the current dead key preedit text /// over the cursor. This currently only ever renders a single codepoint. @@ -39,6 +39,20 @@ pub struct State { 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; + + /// 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. pub type SharedState = Arc>; @@ -46,7 +60,7 @@ impl State { pub fn new(terminal: Terminal) -> State { State { terminal, - overlay_features: Vec::new(), + inspector: None, preedit: None, mouse: Mouse::default(), } diff --git a/crates/ghostty-terminal/Cargo.toml b/crates/ghostty-terminal/Cargo.toml index f16a825..940eacd 100644 --- a/crates/ghostty-terminal/Cargo.toml +++ b/crates/ghostty-terminal/Cargo.toml @@ -13,6 +13,7 @@ ghostty-unicode.workspace = true ghostty-vt.workspace = true log = "0.4.34" onig = "6.5.3" +ghostty-onig.workspace = true png = "0.18.1" rustix = { version = "1.1.5", features = ["shm", "fs"] } thiserror = "2.0.21" diff --git a/crates/ghostty-terminal/src/page.rs b/crates/ghostty-terminal/src/page.rs index 4463120..d25ff64 100644 --- a/crates/ghostty-terminal/src/page.rs +++ b/crates/ghostty-terminal/src/page.rs @@ -977,6 +977,21 @@ impl Page { pub fn lookup_hyperlink(&self, index: usize) -> Option { 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. pub fn hyperlink_count(&self) -> usize { self.hyperlink_map.len() diff --git a/crates/ghostty-terminal/src/page_list.rs b/crates/ghostty-terminal/src/page_list.rs index d50e467..4c157de 100644 --- a/crates/ghostty-terminal/src/page_list.rs +++ b/crates/ghostty-terminal/src/page_list.rs @@ -863,6 +863,11 @@ impl PageList { let i = page.cell_index(usize::from(p.x), usize::from(p.y)).ok()?; 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 { + self.viewport_pin + } /// Number of live pages in list order. pub fn total_pages(&self) -> usize { self.nodes.iter().filter(|n| n.is_some()).count() diff --git a/crates/ghostty-terminal/src/string_map.rs b/crates/ghostty-terminal/src/string_map.rs index 5f5c307..d3a4dfc 100644 --- a/crates/ghostty-terminal/src/string_map.rs +++ b/crates/ghostty-terminal/src/string_map.rs @@ -11,9 +11,10 @@ use onig::{MatchParam, Regex, Region, SearchOptions}; /// Units are Oniguruma retry steps (internal backtracking/retry counter), /// not bytes/characters/time. /// -/// Deviation: the `onig` crate only exposes the retry limit *in match* -/// (Zig also sets the retry limit in search); both abort matching with an -/// error which is treated as "no further matches". +/// This is Zig's `setRetryLimitInSearch`: a whole-search budget (set via +/// the `ghostty-onig` wrapper since the `onig` crate does not expose it). +/// Exceeding it aborts matching with an error which is treated as "no +/// further matches". 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. @@ -52,7 +53,7 @@ impl<'a> SearchIterator<'a> { // Use per-search match params so we can bound regex retry steps // (Oniguruma's internal backtracking work counter). 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 mut region = Region::new(); @@ -195,6 +196,19 @@ mod tests { 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] fn string_map_search_iterator_url_with_click_position() { let re = Regex::new(ghostty_config::url::REGEX).unwrap(); diff --git a/crates/ghostty-termio/src/command.rs b/crates/ghostty-termio/src/command.rs index 91b68a0..9165d6d 100644 --- a/crates/ghostty-termio/src/command.rs +++ b/crates/ghostty-termio/src/command.rs @@ -13,7 +13,6 @@ //! async-signal-safe, like their Zig counterparts are expected to be). use std::{ - collections::HashMap, ffi::{CString, NulError, c_char}, os::fd::OwnedFd, panic::{AssertUnwindSafe, catch_unwind}, @@ -25,6 +24,7 @@ use rustix::{ process::{self, WaitOptions}, stdio, }; +use std::os::unix::ffi::OsStrExt; /// Function executed /in the child process/ after the fork, but before /// 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`, /// inherits the environment variables from this process. These are the /// exact environment variables to set; these are /not/ merged. - pub env: Option>, + pub env: Option, /// Working directory to change to in the child process. If not set, the /// working directory of the calling process is preserved. Failure to @@ -249,7 +249,12 @@ impl Command { let envp = match &self.env { Some(map) => map .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::>()?, // Inherit: snapshot our environment before the fork (nothing may // allocate in the child). @@ -738,13 +743,39 @@ mod tests { let mut cmd = sh("echo $VALUE"); 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(); assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0)); 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] fn custom_working_directory() { let td = TempDir::init().unwrap(); diff --git a/crates/ghostty-termio/src/exec.rs b/crates/ghostty-termio/src/exec.rs index 6f1d9ba..9010c32 100644 --- a/crates/ghostty-termio/src/exec.rs +++ b/crates/ghostty-termio/src/exec.rs @@ -212,6 +212,9 @@ impl Exec { let (tx, rx) = crossbeam_channel::bounded(1); let wakeup = Wakeup::new()?; 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(); cmd.wait_callback(move |r| { let _ = tx.send(r.map_err(|e| format!("{e}"))); @@ -724,10 +727,9 @@ enum Process { struct FlatpakProcess { pid: u32, /// 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, + /// Signal handle that outlives the command's move into the watcher. + signal: Option, } struct Subprocess { @@ -756,7 +758,7 @@ impl Subprocess { // If we have a resources dir then set our env var if let Some(dir) = &cfg.resources_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 @@ -766,8 +768,8 @@ impl Subprocess { // For now, we just look up a bundled dir but in the future we should // also load the terminfo database and look for it. if let Some(base) = &cfg.resources_dir { - env.insert("TERM".into(), cfg.term.clone()); - env.insert("COLORTERM".into(), "truecolor".into()); + env.insert("TERM", cfg.term.clone()); + env.insert("COLORTERM", "truecolor"); // Assume that the resources directory is adjacent to the terminfo // database @@ -784,10 +786,10 @@ impl Subprocess { .map_or_else(String::new, |p| p.to_string_lossy().into_owned()); format!("{parent}/terminfo") }; - env.insert("TERMINFO".into(), terminfo); + env.insert("TERMINFO", terminfo); } else { - env.insert("TERM".into(), "xterm-256color".into()); - env.insert("COLORTERM".into(), "truecolor".into()); + env.insert("TERM", "xterm-256color"); + env.insert("COLORTERM", "truecolor"); } // 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 // which terminal emulator and version they're running under. - env.insert("TERM_PROGRAM".into(), "ghostty".into()); - env.insert("TERM_PROGRAM_VERSION".into(), VERSION_STRING.into()); + env.insert("TERM_PROGRAM", "ghostty"); + env.insert("TERM_PROGRAM_VERSION", VERSION_STRING); // 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. @@ -858,7 +860,7 @@ impl Subprocess { // Add the environment variables that override any others. for (k, v) in cfg.env_override.iter() { - env.insert(k.to_owned(), v.to_owned()); + env.insert(k, v); } // Build our args list @@ -876,7 +878,7 @@ impl Subprocess { // https://github.com/kovidgoyal/kitty/issues/1595 // https://github.com/ghostty-org/ghostty/discussions/7769 if let Some(pwd) = &cwd { - env.insert("PWD".into(), pwd.clone()); + env.insert("PWD", pwd.clone()); } Ok(Subprocess { @@ -1014,6 +1016,7 @@ impl Subprocess { ); self.process = Some(Process::Flatpak(FlatpakProcess { pid, + signal: Some(cmd.signal_handle()), cmd: Some(cmd), })); return Ok((pty.master.try_clone()?, pty.master.try_clone()?)); @@ -1086,15 +1089,16 @@ impl Subprocess { } } Process::Flatpak(fp) => { - if let Some(cmd) = &fp.cmd { - if let Err(err) = cmd.signal(1 /* SIGHUP */, true) { - log::error!("error sending SIGHUP to command, may hang: {err}"); + let res = match (&fp.cmd, &fp.signal) { + (Some(cmd), _) => cmd.signal(1 /* SIGHUP */, true), + (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!( - "cannot signal flatpak host command pid={} after the watcher took it", - fp.pid - ); + }; + if let Err(err) = res { + log::error!("error sending SIGHUP to command, may hang: {err}"); } } } @@ -1147,22 +1151,27 @@ fn add_ghostty_path(env: &mut EnvMap) { // We always set this so that if the shell overwrites the path // scripts still have a way to find the Ghostty binary when // 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 // 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. - 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 + use std::os::unix::ffi::OsStrExt; if path - .split(':') - .any(|entry| !entry.is_empty() && entry == exe_dir) + .as_bytes() + .split(|&b| b == b':') + .any(|entry| !entry.is_empty() && entry == exe_dir.as_bytes()) { 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 { - env.insert("PATH".into(), exe_dir); + env.insert("PATH", exe_dir); } } diff --git a/crates/ghostty-termio/src/shell_integration.rs b/crates/ghostty-termio/src/shell_integration.rs index 7ec2f3c..2f60432 100644 --- a/crates/ghostty-termio/src/shell_integration.rs +++ b/crates/ghostty-termio/src/shell_integration.rs @@ -3,12 +3,12 @@ //! The shell scripts live in `resources/shell-integration/` at the workspace //! 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`). -pub type EnvMap = HashMap; +pub type EnvMap = ghostty_os::EnvMap; /// Shell types we support #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -144,7 +144,7 @@ pub fn setup_features(env: &mut EnvMap, features: ShellIntegrationFeatures, curs } 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. - if let Some(v) = env.get("ENV").cloned() { - env.insert("GHOSTTY_BASH_ENV".to_owned(), v); + if let Some(v) = env.get("ENV").map(std::ffi::OsStr::to_owned) { + env.insert("GHOSTTY_BASH_ENV", v); } // Set our new ENV to point to our integration script. let script_path = format!("{resource_dir}/shell-integration/bash/ghostty.bash"); match std::fs::File::open(&script_path) { Ok(_) => { - env.insert("ENV".to_owned(), script_path); + env.insert("ENV", script_path); } Err(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 { - 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 @@ -245,8 +245,8 @@ fn setup_bash( if !env.contains_key("HISTFILE") && let Some(home) = homedir::home()? { - env.insert("HISTFILE".to_owned(), format!("{home}/.bash_history")); - env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE".to_owned(), "1".to_owned()); + env.insert("HISTFILE", format!("{home}/.bash_history")); + env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE", "1"); } // 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. // This happens in the shell integration config itself. We do this // so that our modifications don't interfere with other commands. - env.insert( - "GHOSTTY_SHELL_INTEGRATION_XDG_DIR".to_owned(), - integ_path.clone(), - ); + env.insert("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_path.clone()); // If no XDG_DATA_DIRS set use the default value as specified. // 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 current = env .get(key) - .map_or("/usr/local/share:/usr/share", String::as_str); - let new = prepend_env(current, &integ_path); - env.insert(key.to_owned(), new); + .unwrap_or_else(|| std::ffi::OsStr::new("/usr/local/share:/usr/share")); + let new = prepend_env_os(current, std::ffi::OsStr::new(&integ_path)); + env.insert(key, new); true } @@ -356,8 +353,8 @@ fn setup_nushell(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Opt /// config then loads the true user config. fn setup_zsh(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option { // Preserve an existing ZDOTDIR value. We're about to overwrite it. - if let Some(old) = env.get("ZDOTDIR").cloned() { - env.insert("GHOSTTY_ZSH_ZDOTDIR".to_owned(), old); + if let Some(old) = env.get("ZDOTDIR").map(std::ffi::OsStr::to_owned) { + env.insert("GHOSTTY_ZSH_ZDOTDIR", old); } // 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}"); return None; } - env.insert("ZDOTDIR".to_owned(), integ_path); + env.insert("ZDOTDIR", integ_path); Some(command.clone()) } @@ -494,7 +491,7 @@ mod tests { ); assert_eq!( "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 @@ -513,7 +510,10 @@ mod tests { }, 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 let only_cursor = ShellIntegrationFeatures { @@ -522,19 +522,28 @@ mod tests { }; let mut env = EnvMap::new(); 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 let mut env = EnvMap::new(); 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] fn default_features() { let mut env = EnvMap::new(); 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] @@ -544,8 +553,14 @@ mod tests { let command = setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); assert_eq!("bash --posix", shell_str(command.as_ref())); - assert_eq!("1", env["GHOSTTY_BASH_INJECT"]); - assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]); + assert_eq!( + "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] @@ -575,12 +590,18 @@ mod tests { let mut env = EnvMap::new(); let command = setup_bash(&sh("bash --norc"), &res.path, &mut env).unwrap(); 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 command = setup_bash(&sh("bash --noprofile"), &res.path, &mut env).unwrap(); 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] @@ -590,11 +611,17 @@ mod tests { 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!("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(); 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] @@ -605,15 +632,27 @@ mod tests { temp_env::with_var("HOME", Some("/home/test"), || { let mut env = EnvMap::new(); setup_bash(&sh("bash"), &res.path, &mut env).unwrap(); - assert!(env["HISTFILE"].ends_with(".bash_history")); - assert_eq!("1", env["GHOSTTY_BASH_UNEXPORT_HISTFILE"]); + assert!( + 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 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(); - 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")); } @@ -621,11 +660,17 @@ mod tests { fn bash_env() { let res = TmpResourcesDir::new(Shell::Bash); 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(); - assert_eq!("env.sh", env["GHOSTTY_BASH_ENV"]); - assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]); + assert_eq!( + "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] @@ -667,11 +712,14 @@ mod tests { assert!(setup_xdg_data_dirs(&res.path, &mut env)); assert_eq!( format!("{}/shell-integration", res.path), - env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"] + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR") + .unwrap() + .to_str() + .unwrap() ); assert_eq!( 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() { let res = TmpResourcesDir::new(Shell::Fish); 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_eq!( format!("{}/shell-integration", res.path), - env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"] + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR") + .unwrap() + .to_str() + .unwrap() ); assert_eq!( 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!( 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] @@ -747,7 +807,10 @@ mod tests { let command = setup_zsh(&sh("zsh"), &res.path, &mut env); 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")); } @@ -755,12 +818,18 @@ mod tests { fn zsh_zdotdir() { let res = TmpResourcesDir::new(Shell::Zsh); 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); assert_eq!("zsh", shell_str(command.as_ref())); - assert_eq!(res.shell_path, env["ZDOTDIR"]); - assert_eq!("$HOME/.config/zsh", env["GHOSTTY_ZSH_ZDOTDIR"]); + assert_eq!( + 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] @@ -780,7 +849,13 @@ mod tests { let mut env = EnvMap::new(); let r = setup(root, &sh("bash"), &mut env, None).unwrap().unwrap(); 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!( Path::new(root) .join("shell-integration/nushell/vendor/autoload/ghostty.nu") diff --git a/crates/ghostty-termio/src/stream_handler.rs b/crates/ghostty-termio/src/stream_handler.rs index 4cddbf9..1621a7d 100644 --- a/crates/ghostty-termio/src/stream_handler.rs +++ b/crates/ghostty-termio/src/stream_handler.rs @@ -933,19 +933,15 @@ impl StreamHandler<'_> { 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 // for this OSC 7 context (e.g. kitty-shell-cwd expects the full, - // unencoded path). - let uri = match ghostty_os::uri::parse( - url_str, + // unencoded path). Parsing is byte based so non-UTF-8 path bytes + // survive percent-decoding. + let uri = match ghostty_os::uri::parse_bytes( + url, ghostty_os::uri::ParseOptions { mac_address: true, - raw_path: url_str.starts_with("kitty-shell-cwd://"), + raw_path: url.starts_with(b"kitty-shell-cwd://"), }, ) { Ok(uri) => uri, @@ -967,12 +963,11 @@ impl StreamHandler<'_> { log::warn!("OSC 7 uri must contain a hostname: UriMissingHost"); return Ok(()); }; - let host = host.to_raw(); // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // 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, Err(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. - let path = uri.path.to_raw().into_owned(); + let path = uri.path; - log::debug!("terminal pwd: {path}"); - self.terminal.set_pwd(path.as_bytes())?; + log::debug!("terminal pwd: {}", String::from_utf8_lossy(&path)); + self.terminal.set_pwd(&path)?; // 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 !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; } Ok(()) diff --git a/crates/ghostty-termio/src/termio.rs b/crates/ghostty-termio/src/termio.rs index 2a566d9..7e19eb9 100644 --- a/crates/ghostty-termio/src/termio.rs +++ b/crates/ghostty-termio/src/termio.rs @@ -34,6 +34,7 @@ use ghostty_config::types_b::{ClipboardAccess, GraphemeWidthMethod, OSCColorRepo use ghostty_datastruct::blocking_queue::Timeout; use ghostty_renderer::message::Message as RendererMessage; use ghostty_renderer::size::Size; +use ghostty_renderer::state::InspectorHook; use ghostty_terminal::kitty::graphics_storage::TerminalInfo; use ghostty_terminal::point::{Coordinate, Point}; use ghostty_terminal::screen::Scroll; @@ -221,8 +222,9 @@ impl Output { // the lock to grab our read data. let outbox = { let mut term = self.state.lock_terminal(); + let inspector = term.state().inspector.clone(); 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 // 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. - 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. inner.stream.state.queue_render(); @@ -260,7 +268,20 @@ impl Output { .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:?}"); } } diff --git a/crates/ghostty-termio/tests/exec_integration.rs b/crates/ghostty-termio/tests/exec_integration.rs index 0d70f2f..b1d58a3 100644 --- a/crates/ghostty-termio/tests/exec_integration.rs +++ b/crates/ghostty-termio/tests/exec_integration.rs @@ -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 // same time (cat would otherwise block on a full pty). 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]; for _ in 0..64 { f.write(&chunk); @@ -555,7 +559,7 @@ fn change_config_message_updates_palette_and_colors() { fn process_output_manual_api() { // Termio can also be fed manually (no child involved). 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")); let (tx, _rx) = unbounded(); let termio = Termio::init(Options { diff --git a/crates/ghostty-unicode/build.rs b/crates/ghostty-unicode/build.rs index 3a69d7a..bcb38f7 100644 --- a/crates/ghostty-unicode/build.rs +++ b/crates/ghostty-unicode/build.rs @@ -18,6 +18,7 @@ mod props; mod ucd; use std::env; +use std::fmt::Write as _; use std::fs; use std::path::PathBuf; @@ -31,6 +32,7 @@ fn main() { println!("cargo:rerun-if-changed=src/lut.rs"); println!("cargo:rerun-if-changed=src/props.rs"); println!("cargo:rerun-if-changed=ucd"); + println!("cargo:rerun-if-changed=ucd/CaseFolding.txt"); let ucd = ucd::Ucd::load(&ucd_dir); @@ -54,4 +56,52 @@ fn main() { emoji.write_rust("bool", ToString::to_string), ) .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)> = 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 = 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(); } diff --git a/crates/ghostty-unicode/src/case_folding.rs b/crates/ghostty-unicode/src/case_folding.rs new file mode 100644 index 0000000..9ede328 --- /dev/null +++ b/crates/ghostty-unicode/src/case_folding.rs @@ -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 + } +} diff --git a/crates/ghostty-unicode/src/lib.rs b/crates/ghostty-unicode/src/lib.rs index 07b6058..09b46e5 100644 --- a/crates/ghostty-unicode/src/lib.rs +++ b/crates/ghostty-unicode/src/lib.rs @@ -8,6 +8,7 @@ #![cfg_attr(test, allow(clippy::unwrap_used))] +pub mod case_folding; pub mod emoji_table; pub mod grapheme; pub mod lut; diff --git a/crates/ghostty-unicode/ucd/CaseFolding.txt b/crates/ghostty-unicode/ucd/CaseFolding.txt new file mode 100644 index 0000000..a0b0f07 --- /dev/null +++ b/crates/ghostty-unicode/ucd/CaseFolding.txt @@ -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: +# +# ; ; ; # +# +# 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 diff --git a/crates/ghostty-vt/src/osc/mod.rs b/crates/ghostty-vt/src/osc/mod.rs index 76f4cf5..c417bb9 100644 --- a/crates/ghostty-vt/src/osc/mod.rs +++ b/crates/ghostty-vt/src/osc/mod.rs @@ -442,6 +442,17 @@ impl Parser { 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 /// terminator (for parsers that, like Zig's color parsers, read /// `writer.buffered()` directly). diff --git a/crates/ghostty-vt/src/osc/parsers/osc9.rs b/crates/ghostty-vt/src/osc/parsers/osc9.rs index 68d902c..898dd7d 100644 --- a/crates/ghostty-vt/src/osc/parsers/osc9.rs +++ b/crates/ghostty-vt/src/osc/parsers/osc9.rs @@ -11,25 +11,49 @@ fn parse_unsigned(bytes: &[u8]) -> Option { /// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. pub fn parse(parser: &mut Parser, _terminator_ch: Option) -> Option { - // Zig prologue: writer must exist. (The NUL-terminator capacity check is - // folded into `data()`.) - let data = parser.data()?; + // Zig prologue: writer must exist. The NUL-terminator reservation is NOT + // done here: Zig only appends the NUL (`writer.writeByte(0)`) in the + // branches that return text, so full-buffer control commands (sleep, + // xterm emulation, progress, ...) still parse. + let data = parser.buffered()?; // Check first to see if this is a ConEmu 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); } // If it's not a ConEmu OSC, it's an iTerm2 notification + if !parser.reserve_nul() { + return None; + } Some(Command::ShowDesktopNotification { title: Vec::new(), body: data, }) } -/// Returns None where the Zig code does `break :conemu`. -fn conemu(data: &[u8]) -> Option { +/// Returns None where the Zig code does `break :conemu`. The flag is true +/// when the Zig branch appends a NUL terminator (needs buffer room). +fn conemu(data: &[u8]) -> Option<(Command, bool)> { + 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 { if data.is_empty() { return None; } @@ -218,6 +242,39 @@ mod tests { ); } + fn run_opt(input: &str) -> Option { + 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] fn osc_9_show_desktop_notification() { notif("9;Hello world", "Hello world"); diff --git a/docs/rewrite/ARCHITECTURE.md b/docs/rewrite/ARCHITECTURE.md index e7d7b8a..d835bef 100644 --- a/docs/rewrite/ARCHITECTURE.md +++ b/docs/rewrite/ARCHITECTURE.md @@ -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-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-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-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 | @@ -75,7 +76,7 @@ WASM, Windows and the embedded C API (`libghostty`) are out of scope. ## Out of scope / deferred - 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) diff --git a/docs/rewrite/notes/gaps-X2.md b/docs/rewrite/notes/gaps-X2.md new file mode 100644 index 0000000..f81f7f4 --- /dev/null +++ b/docs/rewrite/notes/gaps-X2.md @@ -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` 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). diff --git a/src/main.rs b/src/main.rs index 1d26221..e916e70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,8 @@ use ghostty_cli::build_info::{self, BuildInfo, Version}; use ghostty_core::global::{self, InitOptions}; fn main() -> ExitCode { - let args: Vec = std::env::args().collect(); + // Byte-safe argv (`std::env::args()` panics on non-UTF-8 arguments). + let args: Vec = ghostty_os::args::collect_lossy(); // We first start by initializing our global state. This sets up // 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 adw = adw_version::get_runtime_version(); BuildInfo { - gtk_build: Some(Version { - major: 4, - minor: 14, - micro: 0, - }), + gtk_build: Some(version(gtk_version::COMPTIME_VERSION)), gtk_runtime: Some(version((gtk.0, gtk.1, gtk.2))), - adw_build: Some(Version { - major: 1, - minor: 5, - micro: 0, - }), + adw_build: Some(version(adw_version::COMPTIME_VERSION)), adw_runtime: Some(version((adw.0, adw.1, adw.2))), ..BuildInfo::default() }