diff --git a/docs/rewrite/STATUS.md b/docs/rewrite/STATUS.md index d9b697d..c6e4e7c 100644 --- a/docs/rewrite/STATUS.md +++ b/docs/rewrite/STATUS.md @@ -22,7 +22,8 @@ Legend: ⬜ todo · 🟨 in progress · ✅ done (gate passed, committed) · ⚠ | A0 | apprt contract types | sonnet | ✅ | | | G1 | apprt-gtk (GTK4/libadwaita app) | sonnet | 🟨 | against CoreApp/CoreSurface traits | | T3 | terminal: Terminal + stream_readonly + StringMap | sonnet | 🟨 | | -| F2 | font: run.zig, SharedGridSet, config integration, terminal shaper tests | sonnet | 🟨 | | +| F2 | font: run.zig, SharedGridSet, config integration, terminal shaper tests | sonnet | ✅ | | +| K1 | terminal: kitty graphics image/storage/unicode/render/exec | sonnet | 🟨 | | ## Log - 2026-10-11: Phase 0 complete; Phase 1 launched. @@ -31,3 +32,4 @@ Legend: ⬜ todo · 🟨 in progress · ✅ done (gate passed, committed) · ⚠ - 2026-10-11: termio IO1 + font F1 committed. T1 partial (snapshot reflow, head-walking pins) → Codex round 2 in worktree. T2, R1 launched. - 2026-10-11: cli, apprt contract, renderer base committed. G1 (GTK) launched. - 2026-10-11: Screen committed. T3, F2 launched. +- 2026-10-11: F2 committed; K1 launched. diff --git a/docs/rewrite/notes/ghostty-apprt-gtk.md b/docs/rewrite/notes/ghostty-apprt-gtk.md new file mode 100644 index 0000000..bb34f72 --- /dev/null +++ b/docs/rewrite/notes/ghostty-apprt-gtk.md @@ -0,0 +1,126 @@ + +## F3: dialogs + config object (class/{dialog,config,config_errors_dialog,close_confirmation_dialog,clipboard_confirmation_dialog,title_dialog,debug_warning}.rs) + +Libadwaita >= 1.5 is assumed, so `Dialog` is always an `adw::AlertDialog` subclass (no `MessageDialog` fallback) and DebugWarning uses the `adw::Banner` (1.3) template; the clipboard dialog always uses the 1.4 template (remember switch). GType names, property names, signals and template children match the Zig classes. + +Public API (all `&self` methods on `glib::wrapper!` types; signals via `connect_*`): +- `config::Config` (GhosttyConfig; props `diagnostics-buffer`, `has-diagnostics` -> generated getters `diagnostics_buffer() -> Option`, `has_diagnostics() -> bool`): `new(&CoreConfig)` (clones), `from_arc(Arc)`, `new_default()`, `get() -> Arc`, `get_mut(|&mut CoreConfig| ..)` (Arc::make_mut), `clone_config() -> CoreConfig`. Test `ghostty_config` ported. +- `dialog::Dialog` (GhosttyDialog, parent adw::AlertDialog): `present(Option<&impl IsA>)`, `close()` (force_close); subclass via `DialogImpl: AdwAlertDialogImpl` (unsafe `IsSubclassable` impl, SAFETY-commented). Override `response` through `AdwAlertDialogImpl`. +- `config_errors_dialog::ConfigErrorsDialog`: `new(&Config)`, prop `config`, `present(Option<&W>)`, `close()`, `connect_reload_config(Fn(&Self))`. +- `close_confirmation_dialog::{CloseConfirmationDialog, Target{App,Tab,Window,Surface}}`: `new(Target)`, prop `target`, `present(Option<&W>)` (sets heading/body), `close()`, `connect_close_request(Fn(&Self))`, `connect_cancel(Fn(&Self))`. `Target::title()/body()` use ghostty_os::i18n::tr. +- `clipboard_confirmation_dialog::{ClipboardConfirmationDialog, ClipboardRequestBoxed(pub ClipboardRequest)}`: `new()`, props `request` (boxed, nullable), `clipboard-contents` (TextBuffer), `blur`, `can-remember` (generated getters/setters: `set_request(Option)`, `set_clipboard_contents(Option)`, `set_blur(bool)`, `set_can_remember(bool)`), helpers `set_request_value(ClipboardRequest)`, `get_request() -> Option`, `get_clipboard_contents()`, `present(Option<&W>)`, `connect_confirm(Fn(&Self, remember: bool))`, `connect_deny(Fn(&Self, bool))`. +- `title_dialog::{TitleDialog, Target{Surface,Tab}}` (parent adw::AlertDialog directly, as in Zig): `new(Target, Option<&str>)`, props `target`, `initial-value`, `present(&W)` (anchors to ancestor adw::ApplicationWindow/Window, async `choose`), `connect_set(Fn(&Self, &str))`. +- `debug_warning::DebugWarning`: `new()`. + +Status: all type-check and are clippy-clean; runtime (template instantiation, notify callbacks) NOT yet exercised because the crate did not fully compile when F3 finished. Also added `#![allow(clippy::unwrap_used)]` to build.rs. + +## F4: overlays, child-exited banner, scrolled window, css (done) + +Files: `src/css.rs`, `src/class/{resize_overlay,key_state_overlay,search_overlay,surface_child_exited,surface_scrolled_window}.rs`. +Verified in an isolated scratch crate (compiles, clippy -D warnings, instantiated on a live display with no GTK criticals). +All GType names match Zig (note `GhostttySurfaceScrolledWindow` keeps Zig's 3-t typo); templates from the bundled gresource. + +- `ResizeOverlay` (`GhosttyResizeOverlay`): props `duration`(u32, 750), `first-delay`(250), `label`(Option), `overlay-halign/valign`. API: `new()`, `set_label(Option<&str>)`, `label()`, `schedule()`. +- `KeyStateOverlay`: props `tables`/`sequence` are **GStrv (`Vec`)** instead of `ext.StringList`; read-only `has-tables`/`has-sequence`; `valign-target`. API: `set_tables(Vec)`, `set_sequence(Vec)`. +- `SearchOverlay`: props `active` (rw), `search-total`/`search-selected` (u64 ro), `has-search-total/selected`, `halign-target`, `valign-target`. API: `set_search_total(Option)`, `set_search_selected(Option)`, `set_search_contents(&str)`, `grab_focus_entry()` (Zig `grabFocus`; renamed to avoid clashing with `Widget::grab_focus`), `set_active(bool)`, `connect_{stop_search,search_changed(Fn(&Self, Option)),next_match,previous_match}`. +- `SurfaceChildExited`: prop `data` (boxed `ChildExitedBoxed`), signal `close-request`. API: `set_child_exited(Option<&ChildExited>)`, `connect_close_request`. The Zig Noop variant (adw < 1.3) is omitted (crate requires adw 1.5). +- `SurfaceScrolledWindow`: `SurfaceScrolledWindow::new(Option<&impl IsA>)`, props `surface` (typed `GtkWidget`, accepts the `Surface` class), `config` (typed `GObject`; the scrollbar-policy closure downcasts to `class::config::Config` and calls `.get().scrollbar` - needs `Config::get() -> impl Deref`). Binds `surface.config` -> own `config`, so Surface must expose a `config` property holding a `class::config::Config`. +- `css::CssProviders`: `new(&Display)` (installs runtime provider at APPLICATION+3), `load_all(&Config)`, `load_runtime`, `load_custom` (gtk-custom-css, 5MB limit, USER priority), `set_color_scheme(dark)` (sets `prefers-color-scheme` on providers, GTK >= 4.20, dynamic property). `css::runtime_css(&Config, gtk_416) -> String` is pure and tested. Static style*.css come from the gresource: the Application must set `resource_base_path` = `/com/mitchellh/ghostty` so AdwApplication loads them automatically. +- Template signal handlers receive the emitter (not the template) in gtk-rs, so shared handlers locate the overlay with `ancestor()`. +- build.rs fix: blueprint `.ui` files are bundled with `alias="/.ui"` so resource paths are `/com/mitchellh/ghostty/ui//.ui` (previously doubled `ui/ui/`). + +## F1: utilities, key, ipc (files: key, gtk_version, adw_version, weak_ref, ext/*, gsettings, cgroup, flatpak, pre_exec, post_fork, ipc/*) + +Ported (Zig -> Rust): key.zig -> key.rs (`accel_from_trigger`, `xdg_shortcut_from_trigger`, `label_from_trigger` return `Option`; `translate_mods`, `keyval_unicode_unshifted(&Widget, &gdk::KeyEvent, keycode) -> u32`, +`event_mods(&gdk::Event, Key, ModifierType, Action, winproto_event_mods: impl FnOnce(Option<&gdk::Device>, ModifierType)->Mods) -> Mods`, `key_from_keyval(u32)`, `keyval_from_key(Key)`; 132-entry KEYMAP). +gtk_version/adw_version -> `get_runtime_version() -> (u32,u32,u32)`, `at_least/runtime_at_least/runtime_until`, `log_version`, adw `supports_*`. "Comptime" version is the cargo feature level (4.14 / 1.5). +weak_ref.zig -> thin `WeakRef` over `glib::WeakRef`. ext -> `ext::actions::{Action, add, add_to_map, add_as_group}`, `ext::StringList` (glib Boxed `GhosttyStringList`), `get_ancestor`, `g_value_holds` (boxedCopy/boxedFree/listFind not needed). +gsettings -> `get_bool/get_int/get_string(Key)`. cgroup -> `fmt_scope(pid)`, `create_scope(&gio::DBusConnection, pid, Options)`. flatpak -> `resources_dir()`. +pre_exec/post_fork -> `PreExecInfo::new(&Config)`/`pre_exec(&info) -> Option`; `PostForkInfo::new(&Config)`/`post_fork(&info, Option) -> Result<(), PostForkError>` (uses `gio::Application::default()` dbus connection; ghostty-termio's Command also has its own copy of the cgroup wait loop). +ipc -> `ipc::perform_ipc(&Target, &Action) -> Result` (org.gtk.Actions.Activate, `new-window` / `new-window-command`), `ipc::dbus::DBus`. + +Tests: Zig ported 11 (key 3, gtk_version 2, adw_version 1, weak_ref 1, gsettings 4 (the comptime ones became 2), ext/actions 2, ext/slice 3); extra 8. The ext/actions GTK-display test and adw test are skipped at runtime when GTK cannot init (not main thread / no display). +Deviations: only unsafe is `FromGlib` for `gdk::Key` in key.rs (annotated). `many keycode` media keys remain a TODO as upstream. + +## winproto (F2) — src/winproto/{mod,noop,x11,wayland}.rs + +Ported winproto.zig, noop.zig, x11.zig, wayland.zig. Deps added: gdk4-x11 (x11, xlib; Xlib via its re-exported dlopen `x11` = x11-dl), gdk4-wayland (wayland_crate), wayland-client/-backend(client_system)/-protocols(client,staging)/-protocols-plasma(client). + +### Public API (consumed by class/window.rs, class/application.rs) +```rust +pub trait WindowHost { // implemented by window.rs using a *weak* ref to the Window gobject + fn gtk_window(&self) -> Option; // as(gtk.Window)/Native + fn is_quick_terminal(&self) -> bool; + fn window_decoration(&self) -> ghostty_config::types_b::WindowDecoration; + fn config(&self) -> Option>; // getConfig()?.get() +} +pub enum App { None(noop::App), Wayland(wayland::App), X11(x11::App) } +App::init(&gdk::Display, app_id:&str, &Config) -> Result // wayland, then x11, else None +App::protocol() -> Protocol; App::event_mods(Option<&gdk::Device>, gdk::ModifierType) -> Mods // falls back to crate::key::translate_mods +App::supports_quick_terminal() -> bool +App::init_quick_terminal(&mut self, >k::Window) -> Result<()> // call BEFORE the window is realized (layer-shell init) +pub enum Window { None, Wayland, X11 } +Window::init(&App, Rc) -> Result // window must be realized (needs gdk::Surface) +Window::{resize_event, sync_appearance, set_urgent(bool)}(&mut self) -> Result<()> +Window::client_side_decoration_enabled(&self) -> bool +Window::add_subprocess_env(&mut self, &mut Vec<(String,String)>) -> Result<()> // X11: WINDOWID +``` +Zig `deinit` = `Drop` (Wayland releases blur/decoration/slide/token objects, disconnects enter-monitor). +`noop::Window::update_config_event` (unused in Zig union) was not ported. + +### Behaviour notes / deviations +* Wayland uses gdk4-wayland's shared `wayland_client::Connection` (no unsafe). We keep a private event queue; blocking roundtrips at init; while an xdg-activation token is outstanding a 50 ms glib timeout dispatches the queue. KDE blur / server-decoration / slide / xdg-activation implemented; xdg_wm_dialog_v1 presence + layer-shell < 1.0.4 check ported. Registry changes after init (global_remove) are not tracked. +* Quick terminal via `gtk4-layer-shell` crate (`LayerShell` trait). Upstream requires libgtk4-layer-shell to be loaded before libwayland-client; Rust link order is not controllable, so the final binary should link it first or run with `LD_PRELOAD=libgtk4-layer-shell.so` (else `is_supported()` is false and the quick terminal is disabled). +* X11: Xlib (dlopen'd via x11-dl) for XGet/Change/DeleteWindowProperty, XEventsQueued/XPeekEvent; `XkbQueryExtension`/`XkbSelectEventDetails` declared in a small `extern "C"` block (`#[link(name="X11")]`). `unsafe` is confined to x11.rs (`#![allow(unsafe_code)]`, SAFETY comments). +* Tests: Zig has none for winproto; 2 added (noop defaults, X11 property struct layouts). Not runtime-tested on a live X11/Wayland compositor in this fork. +* Depends on `crate::key::translate_mods(gdk::ModifierType) -> Mods` (F1). + +## Application / App / GlobalShortcuts / demo (F8) + +Files: `src/app.rs` (App.zig), `src/class/application.rs` (application.zig), +`src/class/global_shortcuts.rs` (global_shortcuts.zig), `src/gl.rs` (GL proc loader), +`examples/demo.rs`. + +### Public API +* `app::App { pub app: Application }`: `App::new(Rc)`, `run()` (-> `Result<(), RunError>`), + `terminate()`, `wakeup()`, `perform_action(Target, Action) -> bool`, + `App::perform_ipc(&ipc::Target, &ipc::Action)` (static). `app::waker() -> Arc` + (thread-safe, `MainContext::wakeup`) - create it before the core app and give it to core. + `app::MUST_DRAW_FROM_APP_THREAD = true`. +* `class::application::Application` (GType `GhosttyApplication`, extends `AdwApplication`, property `config` + (read-only `Config` object, notify on change)): + `Application::default()`, `new(core_app)`, `core() -> Rc`, `rt() -> Rc`, + `get_config() -> class::config::Config` (object), `core_config() -> Arc`, + `winproto() -> Ref` / `winproto_mut()`, `saved_language()`, `surfaces() -> Vec`, + `perform_action(...)`, `new_window(parent: Option<&Surface>, WindowOverrides)`, `quit()`, `wakeup()`, + `run_loop()`, `deinit()`. `AppRt` is the `RtApp` impl (forwards to the default app; main thread only). +* The giant action switch is `Application::perform_action`. Surface targets are resolved with + `Surface::from_id(id)` (F5); actions use the Zig mechanisms (`activate_action("tab.close"/"win.close"/ + "split-tree.*")`, `ancestor::`). `inspector` actions go to `Surface::control_inspector` + (inspector deferred; no-op). Unimplemented actions (`secure_input`, `close_all_windows`, ...) log + return false. +* Startup mirrors Zig: log writer funnel (`glib::log_set_writer_func` -> `log`), version logging, `Config::load` + (errors become a diagnostic), LANG/`language`, i18n, `GDK_DEBUG`/`GDK_DISABLE` env (`set_gtk_env`), `adw::init`, + single-instance flag, app-id (`class`), winproto init (noop fallback), CSS via `css::CssProviders`, + `setlocale(LC_ALL,"")` (`init_locale`, closes the INTEGRATION_TODO item for locale), style-manager <-> core color scheme + sync (`color_scheme_event` + each `CoreSurface::color_scheme_callback`), `app.*` GActions + (`new-window`, `new-window-command` (`as`: `-e`, `--command=`, `--working-directory=`, `--title=`), `open-config`, + `present-surface` (u64 surface id), `quit`, `reload-config`), accelerator sync, config errors dialog, quit timer, + `run_loop` (own `MainContext` iteration + `CoreApp::tick` + quit conditions), `quit()` with close confirmation dialog. +* `SIGUSR2` reload: glib-rs has no `g_unix_signal_add`; a helper thread (`signal-hook`) hops to the main context. +* Global shortcuts: portal `CreateSession`/`BindShortcuts` over gio D-Bus (deprecated `signal_subscribe` allowed); + `GlobalShortcuts` signal `trigger(String)` carries the action in keybind syntax; `connect_trigger` parses it. +* `gl::proc_address(name)`: dlsym on libepoxy (works for EGL and GLX); use as the loader for glow / + `ghostty-renderer::prepare_context`. + +### Changes to `core.rs` (trait spec) +Added `AppMessage::NewWindow`, `CoreSurface::color_scheme_callback`, `CoreSurface::update_config`; +`CoreApp::perform_all_action` takes a `ghostty_input::binding::Action` (as in Zig). + +### Demo +`cargo run -p ghostty-apprt-gtk --example demo` (env: `DEMO_QUIT_AFTER_MS`, `DEMO_CONFIRM=1`). Dummy core: +keybindings from the user config drive window/tab/split/palette actions; `draw_frame` clears with `background`. + +### Known gaps +* `toggle_visibility`/`float_window`/`secure_input`/`close_all_windows` etc.: unimplemented like upstream. +* Open URL uses `xdg-open` (OpenURI portal is a TODO upstream too). +* `async-backend` (libxev) is meaningless in Rust and ignored. diff --git a/docs/rewrite/notes/ghostty-terminal-pagelist-requests.md b/docs/rewrite/notes/ghostty-terminal-pagelist-requests.md index d194bc8..5f22d10 100644 --- a/docs/rewrite/notes/ghostty-terminal-pagelist-requests.md +++ b/docs/rewrite/notes/ghostty-terminal-pagelist-requests.md @@ -9,3 +9,7 @@ 6. **Resize reflow / no-reflow fewer rows** must drop trailing blank rows, including rows holding only bg-color cells (Zig `resizeCols`/`resizeWithoutReflow`). Three Screen tests are `#[ignore = "pagelist: ..."]`: `screen_resize_no_reflow_less_rows_trims_blank_lines`, `screen_resize_less_cols_with_reflow_but_row_space`, `screen_resize_less_cols_to_wrap_wide_char`. 7. `PageList::reset` and `erase_rows` return `Result`; `erase_rows` with an empty history start point returns `InvalidCoordinate` (Zig would assert). Screen guards/ignores. +- (T3/p4) **`erase_row_bounded` must reset row flags on the blanked row**: after rotating, the final row is cleared with `Page::clear_cells`, which leaves `row.hyperlink`/`styled`/`grapheme` set. Zig's row is left with the flags cleared (test `Terminal: index bottom of scroll region clear hyperlinks` expects `!row.hyperlink`; ignored as `terminal_index_bottom_of_scroll_region_clear_hyperlinks`). +- (T3/p8) **Reflow to fewer cols with a pending-wrap cursor**: 4x2 screen printed "0123", `resize(2,2)` with reflow gives screen contents "01\n23" but the active area/viewport dump is only "23" (active top is one row too low; Zig yields "01\n23"). Ignored test: `terminal_resize_with_wraparound_on`. +- (T3/p1) **`Page::set_graphemes` / `Page::append_grapheme` must not mark the row dirty** (Zig `Page.appendGrapheme` leaves dirty untouched; callers like `Terminal.print` mark dirty explicitly). Terminal works around it for the zero-width path (test `terminal_ignored_vs16_doesnt_mark_dirty`). +- (T3/terminal) `PageList::clone_partial_row_between(src_pin, dst_pin, left, right)`: Terminal `insert_lines`/`delete_lines` need Zig's `Page.clonePartialRowFrom` between two different page nodes of one list (needs two page borrows). `terminal.rs::clone_partial_row_between` re-implements it via `export_cell`/`import_cell` (now `pub(crate)` use) plus the row-metadata merge from `Page::clone_partial_row_from`. diff --git a/docs/rewrite/notes/ghostty-terminal-terminal.md b/docs/rewrite/notes/ghostty-terminal-terminal.md new file mode 100644 index 0000000..23273cf --- /dev/null +++ b/docs/rewrite/notes/ghostty-terminal-terminal.md @@ -0,0 +1,42 @@ +# ghostty-terminal: Terminal, stream_readonly, StringMap + +## Ported +| Zig | Rust | tests | +|---|---|---| +| Terminal.zig | `terminal.rs` (+ `terminal/tests/p1..p8.rs`, helpers in `tests/mod.rs`) | 372 of 372 (370 pass, 2 `#[ignore = "pagelist: ..."]`) | +| stream_readonly.zig | `stream_readonly.rs` | 33 of 33 | +| StringMap.zig | `string_map.rs` (uses the `onig` crate; `ghostty-config` is a dev-dependency for the URL regex tests; `Screen`'s stand-in `StringMap` is now a re-export) | 3 of 3 | + +Ignored tests (PageList gaps, see `ghostty-terminal-pagelist-requests.md`): +`terminal_index_bottom_of_scroll_region_clear_hyperlinks` (erase_row_bounded keeps the row hyperlink flag) and +`terminal_resize_with_wraparound_on` (reflow to fewer cols loses a row in the active dump). + +Small non-test deviations: `glitch.txt` copied next to the tests (used by the glitch-text test); zero-width attach in +`print` restores the row dirty flag because `Page::append_grapheme` marks rows dirty (Zig does not; request in the pagelist file). + +## API overview (for termio / renderer / core) +```rust +use ghostty_terminal::terminal::{Terminal, Options}; +let mut t = Terminal::new(Options::new(80, 24))?; // Options{cols,rows,max_scrollback=10_000,colors,default_modes} +{ + let mut s = t.vt_stream(); // ghostty_vt::stream::Stream> (new_alloc) + s.next_slice(b"hello\r\n\x1b[31mred\x1b[0m")?; // applies every state-changing action; queries are ignored +} // s.handler.terminal gives &mut Terminal while the stream lives +t.resize(100, 30)?; // Zig resize(alloc, cols, rows); primary reflows (if wraparound mode), alt does not +t.print(cp)?; t.print_string("text\n")?; t.full_reset(); +let text = t.plain_string()?; // viewport as text (plain_string_unwrapped respects wrap) +``` +- `t.screens` is a `ScreenSet`: `t.screen()` / `t.screen_mut()` (= `screens.active()/active_mut()`), `t.screens.active_key`, `get(Key)`. +- Reading cells: `scr.pages.get_cell(Point::Viewport(Coordinate{x,y}))` -> `Cell` (Copy); rows/pages via `scr.pages.pin(pt)` + `pin.row_and_cell(&scr.pages)`; page data `scr.page(node)`; style of a cell `page.style(cell)`; graphemes `page.lookup_grapheme(page.cell_index(x,y)?)`. +- Cursor: `t.screen().cursor` (x, y, pending_wrap, style, `cursor_style`, hyperlink_id, semantic_content). Selection: `screen.select(..)`, `screen.selection_string(..)` (Screen API). +- State: `t.modes` (`ModeState`), `t.flags` (mouse_event/format, focused, `dirty` renderer flags, shell_redraws_prompt, ...), `t.colors` (DynamicRgb fg/bg/cursor + DynamicPalette), `t.scrolling_region`, `t.tabstops`, `t.pwd`/`get_pwd()`, `t.mouse_shape`, `t.status_display`, `t.width_px/height_px`. +- Viewport: `t.scroll_viewport(ScrollViewport::{Top,Bottom,Delta(n)})`. Screen switching: `switch_screen(Key) -> Option`, `switch_screen_mode(SwitchScreenMode::{M47,M1047,M1049}, enabled)`. +- `print_attributes()` returns a `String` (DECRQSS SGR reply body) instead of writing into a buffer; `set_pwd(&[u8])`. +- Termio's own handler can call the same `Terminal` methods; it should handle side-effect actions (queries, title, clipboard, bell, APC/DCS) itself and delegate the rest, mirroring `stream_readonly::Handler::vt`. +- Tests: Zig tests that mixed `s.nextSlice` and direct `t.` calls use `feed(&mut t, bytes)` (fresh stream per call) or `t.vt_stream()` + `s.handler.terminal`. + +## TODO(integration) +- `Terminal::kitty_graphics(cmd)` is a stub returning `None` (Zig `kitty.graphics.execute`); `kitty_images_delete_all` (erase display complete/scroll complete) only sets `kitty_images.dirty`; kitty image storage limit on lazily-created screens uses `Options::DEFAULT`. `kitty_images.dirty = true` hooks are in place at every Zig location (index, scroll_up, insert/delete lines, switch_screen, Screen::scroll). +- tmux control-mode viewer: not required by Terminal/stream_readonly; not ported here. +- `clone_partial_row_between` local helper (pagelist request). +- Oniguruma: `onig` crate lacks the retry-limit-in-search setter; only retry-limit-in-match is set.