OdyTTY Runtime Knobs

August 29, 2026 · View on GitHub

OdyTTY loads native runtime settings from built-in defaults, then odytty.conf, then environment variables. Environment variables always win and remain pinned for the session, so use odytty.conf for durable preferences and environment variables for one-off/dev overrides.

For the menu-driven workflow, including Settings search, theme and font pickers, the Layout section, and preservation-first saves, see Configuring OdyTTY. This document remains the exhaustive key-by-key reference.

Config path:

  • %APPDATA%\odytty\odytty.conf on Windows
  • $XDG_CONFIG_HOME/odytty/odytty.conf
  • ~/.config/odytty/odytty.conf on Unix when XDG_CONFIG_HOME is unset

The native app polls the resolved config file about once per second. Invalid rewrites, unknown keys, malformed values, unreadable files, and unresolved font families are non-fatal: OdyTTY keeps the last valid active settings and prints a warning. Deleting the config file also keeps the current settings until a later valid rewrite appears.

Contents

Config Format

odytty.conf is a dependency-free key = value file with # comments:

theme = odyssey-default
font_family = Victor Mono
font_size = 20.0
render_quality = high
min_contrast = 17.0

Blank lines are ignored. Duplicate keys are allowed; the last occurrence wins. If that value is invalid, OdyTTY warns and falls back to the setting's built-in default rather than an earlier occurrence. The in-app settings panel writes this same file with preservation-first writeback: comments, blank lines, ordering, and unknown/future keys stay in place, changed keys are rewritten, missing changed keys are appended, and saves use a same-directory temporary file plus rename.

Settings Reference

All settings except native_autoclose_ms are live-reloadable when their environment variable was not set at startup.

Config keyEnvironment variableValuesDefault
themeODYTTY_THEMEBuilt-in name, user theme name, .theme path, or systemodyssey-default
follow_os_themeODYTTY_FOLLOW_OS_THEMEon, offoff
os_theme_darkODYTTY_OS_THEME_DARKBuilt-in theme nameunset
os_theme_lightODYTTY_OS_THEME_LIGHTBuilt-in theme nameunset
visualODYTTY_VISUALoff, none, plain, ambient, scanlinesambient
fontODYTTY_FONT.ttf, .otf, or .ttc pathunset
font_familyODYTTY_FONT_FAMILYMonospace family name or font pathVictor Mono
font_weightODYTTY_FONT_WEIGHTWeight suffix such as Light, Medium, SemiBold, or emptyempty
font_sizeODYTTY_FONT_SIZEFloat, 6.0..=72.0 px20.0
line_heightODYTTY_LINE_HEIGHTFloat, 1.0..=2.01.0
text_gammaODYTTY_TEXT_GAMMAFloat, 0.5..=3.01.2
text_brightnessODYTTY_TEXT_BRIGHTNESSFloat, 1.0..=1.5 (step 0.05)1.0
stem_darkenODYTTY_STEM_DARKENFloat, 0.0..=1.00.7
min_contrastODYTTY_MIN_CONTRASTWCAG contrast ratio, 1.0..=21.017.0
focus_dimODYTTY_FOCUS_DIMFloat, 0.0..=1.00.0
inactive_pane_dimODYTTY_INACTIVE_PANE_DIMFloat, 0.0..=1.00.0
render_qualityODYTTY_RENDER_QUALITYplain, balanced, highhigh
window_paddingODYTTY_WINDOW_PADDINGFloat, 0.0..=64.0 px4.0
window_borderODYTTY_WINDOW_BORDERon, offoff
window_decorationsODYTTY_WINDOW_DECORATIONSon, offon
window_transparencyODYTTY_WINDOW_TRANSPARENCYon, offon
window_opacityODYTTY_WINDOW_OPACITYPercent, 20..=100 (step 5)80
selection_opacityODYTTY_SELECTION_OPACITYFloat, 0.0..=1.5 (step 0.05)1.0
colored_bg_opacityODYTTY_COLORED_BG_OPACITYFloat, 0.0..=1.0 (step 0.05)0.9
always_show_tab_barODYTTY_ALWAYS_SHOW_TAB_BARon, offoff
tab_bar_heightODYTTY_TAB_BAR_HEIGHTauto, or 1..=5 rowsauto
workspace_rail_sideODYTTY_WORKSPACE_RAIL_SIDEleft, rightleft
workspace_railODYTTY_WORKSPACE_RAILauto, alwaysauto
workspace_rail_widthODYTTY_WORKSPACE_RAIL_WIDTHauto, or 8..=32 cellsauto
workspace_rail_max_widthODYTTY_WORKSPACE_RAIL_MAX_WIDTH8..=32 cells24
workspace_rail_gapODYTTY_WORKSPACE_RAIL_GAP0..=3 rows1
workspace_rail_slot_rowsODYTTY_WORKSPACE_RAIL_SLOT_ROWS1, 2 rows2
tab_panel_strengthODYTTY_TAB_PANEL_STRENGTHFloat, 0.0..=1.0 (0 = panel off)0.8
tab_seamODYTTY_TAB_SEAMon, offoff
workspace_rail_autohideODYTTY_WORKSPACE_RAIL_AUTOHIDEon, offoff
workspace_rail_reveal_pxODYTTY_WORKSPACE_RAIL_REVEAL_PX1..=32 logical px16
background_treatmentODYTTY_BACKGROUND_TREATMENToff/color, gradient, vignette, imageimage
background_imageODYTTY_BACKGROUND_IMAGEPNG/JPEG/WebP path, default (bundled), or nonedefault (bundled)
cell_bg_opacityODYTTY_CELL_BG_OPACITYFloat, 0.0..=1.00.8
background_blur_radiusODYTTY_BACKGROUND_BLUR_RADIUSInteger, 0..=256 px0
background_image_scrimODYTTY_BACKGROUND_IMAGE_SCRIMauto, empty, or float 0.0..=1.00.5
bloomODYTTY_BLOOMon, offon
bloom_thresholdODYTTY_BLOOM_THRESHOLDFloat, 0.70..=1.25, or auto0.7
bloom_intensityODYTTY_BLOOM_INTENSITYFloat, 0.0..=1.00.7
bloom_radiusODYTTY_BLOOM_RADIUSFloat, 0.5..=8.0 px8.0
retroODYTTY_RETROon, offoff
crtODYTTY_CRTon, offon
crt_scanline_intensityODYTTY_CRT_SCANLINE_INTENSITYFloat, 0.0..=0.350.17
crt_scanline_periodODYTTY_CRT_SCANLINE_PERIODFloat, 2.0..=12.0 px7.0
crt_vignette_strengthODYTTY_CRT_VIGNETTE_STRENGTHFloat, 0.0..=0.450.45
crt_curvature 1ODYTTY_CRT_CURVATUREFloat, 0.0..=0.120.0
subpixelODYTTY_SUBPIXELoff, rgb, bgroff
synthetic_stylesODYTTY_SYNTHETIC_STYLESon, offon
ligaturesODYTTY_LIGATURESon, offon
ss01ODYTTY_LIGATURE_SS01on, offoff
ss02ODYTTY_LIGATURE_SS02on, offoff
kitty_named_transportsODYTTY_KITTY_NAMED_TRANSPORTSon, offoff
geometric_boxdrawODYTTY_GEOMETRIC_BOXDRAWon, offon
box_thicknessODYTTY_BOX_THICKNESSFloat, 0.5..=3.01.0
symbol_fallbackODYTTY_SYMBOL_FALLBACKon, offon
symbol_fontODYTTY_SYMBOL_FONT.ttf/.otf/.ttc path, empty, or autoauto
symbol_mapODYTTY_SYMBOL_MAPSemicolon-separated range=family entriesempty
themed_ui_rolesODYTTY_THEMED_UI_ROLESon, offon
cursor_styleODYTTY_CURSOR_STYLEblock, underline, barblock
cursor_blinkODYTTY_CURSOR_BLINKauto, on, offon
cursor_easingODYTTY_CURSOR_EASINGon, offon
cursor_motionODYTTY_CURSOR_MOTIONon, offon
cursor_glowODYTTY_CURSOR_GLOWon, offon
cursor_glow_intensityODYTTY_CURSOR_GLOW_INTENSITYFloat, 0.0..=1.00.5
cursor_trailODYTTY_CURSOR_TRAILon, offon
cursor_trail_strengthODYTTY_CURSOR_TRAIL_STRENGTHsubtle, balanced, expressivebalanced
reduced_motionODYTTY_REDUCED_MOTIONon, offoff
new_output_fadeODYTTY_NEW_OUTPUT_FADEon, offon
new_output_fade_msODYTTY_NEW_OUTPUT_FADE_MSFloat, 50..=1000 ms250
keybindsODYTTY_KEYBINDSchord=action listempty
pane_prefixODYTTY_PANE_PREFIXKey chord, or off to disablectrl+b
scroll_wheel_linesODYTTY_SCROLL_WHEEL_LINESFloat, 1.0..=10.0 lines6.0
scrollback_linesODYTTY_SCROLLBACK_LINESInteger lines, 0..=1000000 (0 = unlimited)10000
scroll_drag_speedODYTTY_SCROLL_DRAG_SPEEDramp, legacyramp
pixel_scrollODYTTY_PIXEL_SCROLLon, offon
scroll_pixel_speedODYTTY_SCROLL_PIXEL_SPEEDFloat, 0.25..=4.01.0
scroll_glideODYTTY_SCROLL_GLIDEon, offon
selection_drag_extendODYTTY_SELECTION_DRAG_EXTENDon, offon
scrollbar_dragODYTTY_SCROLLBAR_DRAGon, offon
wheel_zoomODYTTY_WHEEL_ZOOMon, offon
command_status_gutterODYTTY_COMMAND_STATUS_GUTTERon, offon
sh_clickODYTTY_SH_CLICKon, offon
buttonsODYTTY_BUTTONSon, offon
buttons_iterm_compatODYTTY_BUTTONS_ITERM_COMPATon, offon
buttons_stickyODYTTY_BUTTONS_STICKYon, offoff
shell_integrationODYTTY_SHELL_INTEGRATIONon, offon
shell_key_enhancementODYTTY_SHELL_KEY_ENHANCEMENTon, offoff
interactive_urlsODYTTY_INTERACTIVE_URLSon, offon
interactive_pathsODYTTY_INTERACTIVE_PATHSon, offoff
interactive_paths_barewordsODYTTY_INTERACTIVE_PATHS_BAREWORDSon, offon
interactive_paths_click_hintODYTTY_INTERACTIVE_PATHS_CLICK_HINTon, offon
interactive_paths_image_inlineODYTTY_INTERACTIVE_PATHS_IMAGE_INLINEon, offon
interactive_paths_editorODYTTY_INTERACTIVE_PATHS_EDITOReditor name or argv template(empty — use $EDITOR)
confirm_closeODYTTY_CONFIRM_CLOSEon, offon
shell_exit_closesODYTTY_SHELL_EXIT_CLOSESworkspace, appworkspace
bellODYTTY_BELLoff, visual, urgent, allurgent
ssh_config_hostsODYTTY_SSH_CONFIG_HOSTSon, offoff
remote_integrationODYTTY_REMOTE_INTEGRATIONon, offon
remote_reuseODYTTY_REMOTE_REUSEon, offon
remote_persistODYTTY_REMOTE_PERSISToff, 10m, 30m, 1h, 2h10m
remote_tmuxODYTTY_REMOTE_TMUXon, offoff
remote_image_pasteODYTTY_REMOTE_IMAGE_PASTEask, offask
session_replayODYTTY_SESSION_REPLAYon, offoff
restore_workspacesODYTTY_RESTORE_WORKSPACESon, offoff
osc52_writeODYTTY_OSC52_WRITEoff, ask, onask
osc52_readODYTTY_OSC52_READon, offoff
copy_on_selectODYTTY_COPY_ON_SELECTon, offoff
smart_ctrl_cODYTTY_SMART_CTRL_Coff, copy-or-interruptcopy-or-interrupt
cvd_modeODYTTY_CVD_MODEoff, protan, deutan, tritanoff
cvd_strengthODYTTY_CVD_STRENGTHFloat, 0.0..=1.01.0
native_autoclose_msODYTTY_NATIVE_AUTOCLOSE_MSPositive integer msunset

Setting Details

Pad The Content Away From Window And Chrome

window_padding inserts a pixel gap between the content grid and every hard boundary around it — not just the window edges. A pinned workspace rail's content-facing edge and the tab bar's bottom edge get the same gap, so content never touches chrome. Split-pane dividers count too: each pane keeps the same gap on every edge that faces a divider, so a pane's text never sits flush against the split. The grid gives up whatever whole cells the gap displaces, keeping text clear of every band and divider. A heavily padded or narrowed pane can temporarily have no drawable cell on one axis; its PTY backing model stays at the required one-cell minimum, while rendering and cell input remain clipped until the pane expands. Completed window resizes and divider drags reconcile every affected pane to the settled whole-cell geometry. The chrome bands themselves stay joined: where a pinned rail meets the tab bar, the bar band's background extends across the gap to the rail edge (the tabs keep their content-aligned inset), so the frame reads as one continuous piece. At 0 everything is flush, exactly as before. The auto-hidden rail is unaffected: its revealed overlay floats over full-bleed content without reflowing it.

Make The Window Transparent

window_transparency = on draws the terminal background at window_opacity so the desktop shows through. Text, the cursor, and every overlay remain opaque. The selection is fully opaque by default and has its own strength control, selection_opacity, independent of the window opacity.

Wayland and macOS support compositing natively, X11 requires a compositor, and Windows uses DWM. An environment without alpha compositing shows no visible change.

Keep Colored Blocks And Text Legible At Low Opacity

As window_opacity drops, colored cell backgrounds — prompt powerline segments, button chips, and highlighted status blocks — fade along with the window and can wash out. colored_bg_opacity sets a minimum background strength for any cell whose colour differs from the theme default, so those blocks stay solid while the plain background still shows the desktop through. The default is 0.9; 1.0 removes the window-opacity attenuation so those cells keep their full configured background opacity (literally opaque only where the cell background itself is opaque), and 0.0 disables the floor and restores the uniform behaviour. It has no effect at window_opacity = 100, where nothing is translucent. Default-background cells are never touched, and the selection, cursor, and window chrome are exempt.

text_brightness lifts glyph ink toward white in linear light so text stays readable over busy or colored backdrops. The default 1.0 keeps the exact theme colours; raising it toward 1.5 brightens strokes with a soft knee. The lift is applied after the min_contrast floor and leaves colour emoji unchanged.

Tune The Selection Strength

selection_opacity sets how strongly the text-selection highlight paints, from 0.0 (invisible) to 1.5 (strongest), independent of window_opacity, the theme colours, and min_contrast. The default is 1.0, the authored fully opaque selection. Lower it toward 0.0 for a translucent tint that lets a transparent or busy backdrop show through behind the selection; raise it toward 1.5 to keep the surface fully opaque while pushing the highlight colour stronger (bounded and in gamut). Text under the selection stays legible through the minimum-contrast floor.

Size The Tab Bar

tab_bar_height = auto uses one text row. A fixed value from 1 through 5 makes the band taller and centers its labels vertically.

Drag the bottom edge to set a manual height. Double-click that edge to return to auto.

Configure The Workspace Rail

workspace_rail_side chooses the left or right edge, while workspace_rail controls whether the rail appears automatically or stays pinned. Tabs remain on the top bar.

workspace_rail_width = auto sizes to the longest workspace name within the configured maximum. Drag the inner edge for a manual width, or double-click it to return to auto.

With autohide on, the pointer entering the configured edge zone reveals the rail as a floating overlay without reflowing terminal content. Workspace switch, create, and close shortcuts also reveal it briefly.

tab_panel_strength sets the opacity of the unified panel behind the rail and tab bar directly: 1.0 composes the chrome bands nearly fully opaque at any window opacity, 0.0 turns the panel off (bare labels over the window background), and the ramp between is linear. The default is 0.8 — a strong, clearly-present panel that still lets a hint of the window through. The revealed autohide overlay, the pinned rail, and the top bar all share one panel translucency, so toggling autohide never changes how solid the band looks. (Earlier releases scaled the panel from the window's remaining opacity headroom, which made even a maxed panel weak on a translucent window.)

A small chevron control pinned at the rail's bottom edge toggles auto-hide in place, so the escape hatch for the rail lives on the rail itself rather than only in settings. Clicking it flips tab_rail_autohide and writes the choice back to odytty.conf; on the revealed overlay the same control turns auto-hide back off. Cross-platform UI with no platform-specific behavior.

Legacy rail names remain accepted without warnings:

Canonical config keyCanonical environment variableLegacy config keyLegacy environment variable
workspace_rail_sideODYTTY_WORKSPACE_RAIL_SIDEtab_bar_placementODYTTY_TAB_BAR_PLACEMENT
workspace_rail_widthODYTTY_WORKSPACE_RAIL_WIDTHtab_rail_widthODYTTY_TAB_RAIL_WIDTH
workspace_rail_max_widthODYTTY_WORKSPACE_RAIL_MAX_WIDTHtab_rail_max_widthODYTTY_TAB_RAIL_MAX_WIDTH
workspace_rail_gapODYTTY_WORKSPACE_RAIL_GAPtab_rail_gapODYTTY_TAB_RAIL_GAP
workspace_rail_slot_rowsODYTTY_WORKSPACE_RAIL_SLOT_ROWStab_rail_slot_rowsODYTTY_TAB_RAIL_SLOT_ROWS
workspace_rail_autohideODYTTY_WORKSPACE_RAIL_AUTOHIDEtab_rail_autohideODYTTY_TAB_RAIL_AUTOHIDE
workspace_rail_reveal_pxODYTTY_WORKSPACE_RAIL_REVEAL_PXtab_rail_reveal_pxODYTTY_TAB_RAIL_REVEAL_PX

Choose Shell-Exit Behavior

shell_exit_closes = workspace closes an emptied workspace, with the final workspace still quitting OdyTTY. app quits OdyTTY whenever a shell exit would close a workspace, which pairs with restore_workspaces.

This setting only governs shell exits. Rail controls and close-tab, close-workspace, and close-pane bindings retain their surface-specific meaning.

Configure The Bell

urgent requests window attention when OdyTTY is unfocused, visual shows a brief screen flash, all combines them, and off ignores BEL. OdyTTY has no audible bell.

Tune Themes, Fonts, And Rendering

  • The accessibility-oriented knobs — min_contrast, cvd_mode / cvd_strength, and focus_dim — are covered in depth in accessibility.md, which explains the contrast floor, the OKLab color-vision-deficiency daltonization modes, and the focus/dim controls.

  • theme = system is a convenience alias. It enables OS dark/light following and maps dark to odyssey, light to odyssey-light, unless explicit os_theme_dark / os_theme_light values are set.

  • The vertical rail's geometry knobs carry a canonical workspace_rail_* family (workspace_rail_width, _max_width, _gap, _slot_rows, _autohide, _reveal_px) and matching ODYTTY_WORKSPACE_RAIL_* environment variables, since the rail lists workspaces rather than tabs. The older tab_rail_* config keys and ODYTTY_TAB_RAIL_* variables remain fully accepted as legacy aliases onto the same settings, so existing configs keep working unchanged. Each name is a pure alias — no separate field, default, or range. When both a workspace_rail_* name and its tab_rail_* twin are set for the same field, the canonical workspace_rail_* value wins.

    The master toggle workspace_rail / ODYTTY_WORKSPACE_RAIL is unchanged.

  • Rail side and visibility are separate settings. workspace_rail_side (ODYTTY_WORKSPACE_RAIL_SIDE, left|right) selects which side the rail sits on; workspace_rail (auto|always) selects whether it shows. For the side, when more than one source is set the precedence is canonical workspace_rail_side > legacy workspace_rail=left|right > tab_bar_placement, so the canonical key wins. A legacy workspace_rail=left|right both pins the rail (visibility always) and supplies the side when workspace_rail_side is absent.

    The default side resolves to the left. All legacy forms stay accepted with no warning.

  • ODYTTY_APPEARANCE=dark|light seeds the initial appearance for OS-theme following on X11, where the compositor never delivers a live light/dark signal. It is read directly from the environment rather than through the config file, so it is not a settings knob and has no odytty.conf key. Only dark and light are recognized; any other value is ignored and following falls back to its default seed.

  • visual = ambient (the default) and visual = scanlines are back-compat aliases for OdyTTY's scanline look, which is produced by the CRT post-process (crt / crt_scanline_*) — the legacy per-cell ambient wash was retired and folded into it. When no explicit crt value is set, an ambient visual turns the CRT pass on; an explicit crt setting always wins, so the two never stack. off, none, and plain opt out of the alias.

  • render_quality = plain is the hard direct-render fast path. It bypasses post-process effects and visual treatments even if individual effect knobs are enabled.

  • Bloom and CRT require filterable Rgba16Float render targets. Unsupported adapters fall back to the plain direct path with one stderr notice.

  • retro = on promotes effective bloom/CRT settings to a stronger phosphor profile without overwriting individual values: threshold 0.70, intensity 1.0, radius 8.0, scanlines 0.35, vignette 0.35. It does not force screen curvature; crt_curvature stays at its configured value (0.0 by default).

  • geometric_boxdraw = on renders supported box-drawing, block and shade elements, Braille (U+2800..=U+28FF), all four Powerline separators, and Symbols for Legacy Computing sextants and octants from cell geometry instead of relying on the active font. The procedural coverage tiles each cell edge exactly, so TUI borders and prompt separators stay seamless at any selected font or size. box_thickness adjusts the line weight without changing cell placement.

  • Text and UI colors are resolved in linear light before composition, and text_gamma adjusts glyph coverage independently of the color pipeline. An sRGB surface is preferred so antialiased edges retain their intended weight; optional subpixel text uses dual-source blending when the adapter supports it.

  • symbol_fallback = on backfills symbol/icon codepoints the body font lacks from a fallback chain (bundled Symbols Nerd Font v3+v2, an optional host * Nerd Font, plus a system tail). On macOS the tail is fixed system faces; on Windows it tries Segoe UI Symbol, Segoe MDL2 Assets, and Cambria Math; on Linux it uses installed broad-coverage symbol faces (Noto Sans Symbols / Symbols2, Symbola, DejaVu, Unifont) when present. When a symbol codepoint still misses the static chain on Linux, OdyTTY runs a per-codepoint, result-cached fontconfig :charset=<cp> query to find a monochrome host face that covers it (color/bitmap-only faces are rejected), which resolves standard symbols such as the playback triangle U+23F5 ⏵, the record bullet, and check/ballot marks that no bundled face carries. Candidates are tried in order — fc-match's preferred answer first, then the remaining providers fc-list reports — so a face that fails to load or proves bitmap-only costs a fallthrough rather than the glyph. The face index fontconfig reports is honored, which matters for a font collection: face 0 of a collection is arbitrary with respect to the request, so ignoring the index can rasterize symbols at the wrong weight beside the body font. A collection is read one face at a time rather than whole, so a multi-hundred-megabyte collection costs the face it provides rather than the file containing it. The query is local-only and read-only, runs at most once per distinct missing codepoint, and is never on the per-frame path; if fontconfig is absent (for example, in headless CI), the codepoint keeps the historical hollow-box glyph.

    Setting symbol_fallback = off disables the whole chain and the runtime query. Codepoints with Unicode Emoji_Presentation=Yes route to the color-emoji path when a color emoji font is available; text-default symbols in the same blocks, such as U+2731 and U+25CF, stay on the monochrome fallback path. If the color face does not cover a color-routed codepoint, OdyTTY emits no color run and falls through to the same monochrome coverage/symbol fallback renderer.

Tune Scrolling

  • pixel_scroll (default on) governs high-resolution, pixel-precise input — touchpads and hi-res wheels that emit pixel deltas. Such input scrolls the viewport by a continuous sub-row amount that tracks physical finger travel 1:1, rather than quantizing to whole notches. Continuous pixel input is tracked directly instead of eased, which avoids the sawtoothing that an easing catch-up produces on high-resolution devices. Classic detented wheels emit line deltas and are unaffected — they continue to use scroll_wheel_lines as the per-notch multiplier.

    The continuous direct-tracking pixel lane is per-pane: in a split it drives the pane under the pointer (without stealing focus), its overflowing partial row clipped to that pane so the sub-cell shift never smears across the divider.

  • scroll_pixel_speed (default 1.0, range 0.25..=4.0) is the sensitivity multiplier for the continuous pixel lane. 1.0 tracks finger travel exactly; higher scrolls faster than the finger, lower slower. It applies only to pixel-precise input, never to detented wheels.

  • scroll_glide (default on) animates scrollback between discrete wheel notches. Detented wheels emit whole notches with no sub-step data, so pixel tracking cannot help them; instead the integer viewport offset still jumps instantly per notch, but the rendered view eases toward it over a few frames — a forward-chase follower that only ever moves in the scroll direction, so a stream of notches cannot sawtooth. On by default; primary screen only. In a split, each pane glides independently as an eased follower with pixel-precise sub-cell smoothness — the pane under the pointer, without stealing focus — its overflowing partial row clipped to the pane so it never smears across the divider into a neighbour.

    High-resolution direct-tracking wheels and touchpads use pixel_scroll, which is likewise per-pane in a split.

  • scroll_wheel_lines sets how many rows one wheel notch advances the local scrollback viewport (default 6). The same count also drives alternate-scroll (DECSET 1007) arrow emulation, so classic pagers (less, man, git log) that enable alternate-scroll without full mouse tracking scroll at the same rows-per-notch as the viewport. Full mouse-reporting TUIs own the wheel — their report carries direction, not magnitude — so the multiplier does not apply there, and continuous (touchpad pixel) deltas are never multiplied.

Choose Cursor And Background Behavior

  • Fresh profiles use cursor_style = block and cursor_blink = on. auto also resolves to the conventional blinking terminal default. Applications can still select their requested DECSCUSR cursor shape and blink policy.

  • Keyboard and IME activity hold an application-requested blinking cursor visibly on. Blinking begins after a short 650 ms quiet period and parks solid on after 15 seconds without activity, so an idle terminal has no continuing blink wake. Steady application-requested shapes remain steady.

  • cursor_easing = on fades blinking cursor edges rather than switching them hard. cursor_motion = on glides eligible nearby moves while the logical cursor, selection, copy, and terminal input reach the destination immediately. First frames, resize, reflow, scrollback, focus loss, and other discontinuities remain exact-position presentation paths.

  • cursor_trail = on adds the low-alpha nearby echo. For stable jumps beyond the six-cell glide range, the same enabled trail presentation uses one cursor-shaped follower that stretches toward the destination and settles without delaying input. cursor_trail_strength selects subtle, balanced (the default), or expressive response for both forms. Turning motion or trail off removes its presentation work; settled effects add no idle wake.

  • cursor_glow = on draws one restrained analytic aura beneath the glyph, matched to the active Block, Bar, or Underline geometry. An unfocused Block cursor becomes a one-pixel hollow outline while its glyph keeps the normal foreground color; Bar and Underline remain their normal shapes.

  • cursor_glow_intensity scales that aura on a normalized 0.0..=1.0 range, independent of the whole-scene bloom_intensity. 0.0 shows no aura even while cursor_glow is on; the default 0.5 reproduces the calibrated restrained peak; 1.0 is stronger but stays bounded so nearby text remains readable and translucent backgrounds are not washed out. It is hot-reloadable and has no effect while cursor_glow is off.

  • Cursor slide, trail, glow, easing, blink activity, and large-jump follower presentation apply to the focused pane in a split and remain clipped to that pane. Idle and background panes do not receive animation wakes.

  • reduced_motion = on is the master static override for cursor slide, trail, glow, easing, and new-output fade. It preserves the stored settings while forcing those paths to their static or instant forms. new_output_fade is on by default and is one of the paths this override suppresses.

  • background_treatment = image draws a PNG, JPEG, or WebP behind the grid. Use cell_bg_opacity < 1.0 to show it through cells; otherwise it is only visible in transparent/padding areas. The settings panel presents this inverse as Wallpaper visibility, where higher values show more of the image. When cell backgrounds are translucent, OdyTTY applies the matching wallpaper wash to padding and non-grid edge regions so the image strength stays even across the full window.

  • background_image_scrim = auto is shown as Wallpaper readability in the settings panel. Lower explicit values keep the image clearer; higher values add more readability overlay.

  • Path settings, including background_image, open an inline file picker in Settings. Directories are enumerated off the UI path so keyboard and mouse navigation remain responsive while large folders load. The background_image picker also lists two entries at the top — Default (bundled) restores the shipped OdyTTY background and None (no image) clears it — so the bundled default is reachable from the GUI without editing the config.

Gate Terminal Clipboard And Named Graphics Authority

osc52_write = ask is the default policy for terminal-requested clipboard writes: a program that tries to set the system clipboard prompts for consent first, so an unattended write cannot silently replace what you have copied. The consent overlay offers allow once, allow for that session, deny for that session, or cancel; remembered choices disappear when the PTY closes.

Set osc52_write = on for the older compatibility behavior, which applies a focused write immediately with a compact, rate-limited notice that identifies only the clipboard target and byte count, never the copied content. off drains and discards all write requests. In every mode OdyTTY accepts a write only from the active PTY after the window has reported OS focus; requests are discarded while focus is absent or has not yet been observed. OSC 52 reads are independent and remain off by default through osc52_read = off. On Linux, selector p targets PRIMARY; macOS and Windows have no PRIMARY surface, so that target is a no-op there.

Kitty direct and chunked-inline image transfers remain available. File, temporary-file, and POSIX shared-memory transports named by terminal output require kitty_named_transports = on; the default rejects them before local file or shared-memory I/O. See Graphics Protocol Support before granting that authority to output from an SSH or other remote session.

Use Startup Smoke Timing

native_autoclose_ms is a smoke-test helper and is startup-only.

Key Binding Grammar

For every default chord, bindable action, overlay shortcut, and pane-prefix key, see keybindings.md. This section documents only the config grammar and Settings surface.

keybinds accepts comma- or semicolon-separated chord=action entries in odytty.conf:

keybinds = ctrl+shift+y=copy;ctrl+alt+v=paste;super+f=search;alt+pageup=scroll-up;alt+pagedown=scroll-down

For a one-off/dev override, pass the same list through ODYTTY_KEYBINDS; env wins for that session.

Chord modifiers are ctrl, shift, alt, and super. The key may be any single printable ASCII character except + and =, the word comma, f1-f24, or a named key: pageup, pagedown, home, end, enter, esc, backspace, delete, insert, tab, space, up, down, left, or right.

The in-app keybinding editor is opened from the Settings panel's Keybindings row. It covers every bindable action — the core workflow actions plus the overlay (command palette, connection manager, session replay, theme builder, session-attach / Manage Sessions), tab, and pane-management actions — writing through to keybinds; the ODYTTY_KEYBINDS env var can override the same setting for a session.

Pane And Interaction Details

Panes — multiplexer prefix (pane_prefix)

Pane / split management uses a tmux-style prefix model: press the prefix chord (default Ctrl+b), then a pane key. The prefix is captured only when the active tab has more than one pane; on a single-pane tab, Ctrl+b passes through to the shell unchanged, preserving the byte-identical default input path. Set pane_prefix=off (or none) to disable the pane prefix entirely and free Ctrl+b in multi-pane tabs too.

Ctrl+Shift+E and Ctrl+Shift+O create the first split because the prefix is inactive on a single-pane tab. Both direct chords continue to work after the tab has multiple panes.

After the prefixActionConfig name
%Split side-by-side (columns)split-columns
"Split stacked (rows)split-rows
/ / / Move focus to the neighbor panefocus-pane-left / -right / -up / -down
oCycle focus to the next panefocus-pane-next
xClose the focused paneclose-pane
zZoom / toggle-fullscreen the focused panezoom-pane
Space / =Equalize split sizesequalize-panes

The prefix itself is reconfigurable:

ODYTTY_PANE_PREFIX="ctrl+a" odytty   # use Ctrl+a instead
ODYTTY_PANE_PREFIX=off odytty        # disable; Ctrl+b is literal in multi-pane tabs too

Nested multiplexers. In a multi-pane tab, pressing the prefix twice (Ctrl+b Ctrl+b) sends a single literal prefix byte (e.g. 0x02) to the focused pane, so a tmux or screen running inside OdyTTY still receives its own prefix and works normally. In a single-pane tab, the first Ctrl+b already passes through literally.

Alternatively, change pane_prefix so the outer and inner prefixes differ. Individual pane actions are rebindable via keybinds (the chord is the second key, after the prefix), e.g. keybinds = ctrl+f=zoom-pane rebinds zoom to <prefix> Ctrl+f. ODYTTY_KEYBINDS provides the same syntax as a session-scoped override.

Zoom (<prefix> z) makes the focused pane fill the whole content area while the split layout underneath is preserved; press it again to restore the exact prior geometry. Splitting, closing a pane, or equalizing also clears zoom. Zoom is a no-op in a single-pane tab.

Inactive-pane dimming (inactive_pane_dim)

When a tab is split into multiple panes, inactive_pane_dim applies a subtle dim (in OKLab, so hue is preserved) to the non-focused panes so the focused one stands out. It accepts 0.0..=1.0; 0.0 (the default) is off — every pane renders undimmed and the multi-pane frame is byte-identical to before this knob existed. 0.150.30 is a subtle recede. The focused pane is never dimmed, single-pane tabs are never affected, and the minimum-contrast floor still applies so text stays legible.

The plain renderer profile forces it off.

Session output replay (session_replay)

session_replay = on (or ODYTTY_SESSION_REPLAY=on) turns on opt-in per-session output recording for the scrubbable replay overlay. It is off by default: while off, the PTY pump records nothing and the render/output path is byte-identical to before the feature existed. The same toggle is reachable in the Settings panel's Sessions section.

When on, each session keeps a bounded, in-memory ring of recent screen frames. The cap is fixed and bounded by both a frame count (600 frames) and a total byte budget (24 MiB); whichever binds first evicts the oldest frames, so memory never grows without bound. Turning the setting back off clears the ring immediately.

Recording is local-only: frames live only in process memory — they are never written to disk, logged, or sent anywhere, and they are dropped when the session closes or recording is turned off.

To scrub, press Ctrl+Shift+R (the default, or the right-click menu's "Session Replay" item) to open the replay overlay; rebind the session-replay action via keybinds if you prefer. ODYTTY_KEYBINDS provides the same syntax as a session-scoped override. / step one frame, PgUp/PgDn jump ten, Home/End go to the oldest/newest frame, and Esc closes it. Replay is presentation-only: the overlay scrubs a frozen, fully decoupled clone of the ring and never mutates the live terminal — the session keeps running underneath while you scrub.

The scrub view is a monochrome text preview of the recorded screen at each point.

Restore workspaces at launch (restore_workspaces)

restore_workspaces = on (or ODYTTY_RESTORE_WORKSPACES=on) reopens the previous window layout when odytty is launched with no arguments. It is off by default; the same toggle lives in the Settings panel's Sessions section.

What is restored is shape only: workspace names, tab titles and order, and each tab's pane split tree (axes + ratios), with every pane reopened at its captured working directory running a fresh interactive shell. What is never restored: terminal output, scrollback, environment, or the commands that were running — restore never re-runs a captured command. If a pane's saved directory no longer exists it opens at your home directory instead, with a single brief notice.

Rules:

  • Only a bare odytty restores. Any command-line argument — a flag, a path, --working-directory, -e COMMAND, an attach id — starts that launch fresh and suppresses restore.
  • The layout autosave runs regardless of this setting. A shape snapshot is written (debounced) as the layout changes and on a clean exit, so a snapshot is ready the moment you turn restore on. The snapshot is shape-only and lives in the state directory (workspaces.json).
  • One window owns the autosave. When several odytty windows are open only the first (primary) instance writes the snapshot and restores it, so a second window never clobbers the first window's saved layout.

Clickable URLs (interactive_urls)

interactive_urls = on is the default: a bare URL that a program printed as plain text — https://example.com, not wrapped in an OSC 8 hyperlink escape — gets the pointer (hand) cursor on hover, an armed underline while the platform modifier is held, and opens in your browser on Ctrl+click on Linux/Windows or Cmd+click on macOS. It is independent of interactive_paths: URL opening and filesystem-path detection toggle separately. The same toggle is in the Settings panel's Input section.

Security mirrors OSC 8 and interactive paths exactly: URLs are never auto-opened (always an explicit modifier+click), only an allowlisted scheme opens (http, https, file, mailtoftp/ssh/git are detected but not opened, and javascript: and friends never open), and the URL is passed as a direct argv vector to the platform opener with no shell interpolation. Detection is local-only and scans only the hovered row of the focused pane; an explicit OSC 8 hyperlink under the pointer always wins, so a cell is never double-decorated. Set interactive_urls = off (or ODYTTY_INTERACTIVE_URLS=off) to disable it — the off path never scans, so the hover frame is byte-identical.

Keyboard alternative, regardless of this setting: Ctrl+Shift+L (the hints action) labels every on-screen URL, path, and hash for keyboard quick-select and copy.

Smart Ctrl+C (smart_ctrl_c)

smart_ctrl_c controls what plain Ctrl+C does. Since v0.6.0 it defaults to copy-or-interrupt (the Windows-Terminal-style behavior): when text is selected, Ctrl+C copies the selection and clears it; when nothing is selected, Ctrl+C still sends the interrupt signal (^C). Set smart_ctrl_c = off (or ODYTTY_SMART_CTRL_C=off) to restore the plain terminal behavior where Ctrl+C always sends the interrupt. The toggle is in the Settings panel's Clipboard section, where its value reads copy-or-interrupt verbatim.

To still send an interrupt while text is selected, press Esc first (which clears the selection) and then Ctrl+C, or just press Ctrl+C twice — the first press copies and clears, so the second interrupts. Ctrl+Shift+C is always an unambiguous copy regardless of this setting, and a full-screen TUI never holds a local selection, so its Ctrl+C keeps interrupting.

Paste is unaffected: Ctrl+Shift+V pastes. There is deliberately no "smart Ctrl+V" — plain Ctrl+V stays the readline/vi verbatim-insert (^V). If you want plain Ctrl+V to paste anyway, bind it directly:

keybinds = ctrl+v=paste

Interactive file paths (interactive_paths)

interactive_paths = on (or ODYTTY_INTERACTIVE_PATHS=on) turns on opt-in detection of filesystem paths in terminal output. It is off by default: while off, the pointer path never scans terminal text for paths and the hover path is byte-identical to before the feature existed. The same toggle is reachable in the Settings panel's Input section.

When on, hovering a path-looking span — absolute (/etc/hosts), home-relative (~/notes.md), explicit-relative (./build.rs, ../Cargo.toml), or a bare relative path that contains a slash (src/main.rs) — that resolves to a real file or directory shows the pointer (hand) cursor, the same affordance as an OSC 8 hyperlink. Relative paths resolve against the shell's reported working directory (OSC 7); ~ expands against $HOME. A trailing :line[:col] suffix (src/main.rs:42:10) is recognized and carried through to the editor-open action below.

interactive_paths_barewords = on (the default) also treats basename-like tokens with an extension, such as carpet1.jpg in ls output, as candidates when the parent interactive_paths gate is on. Bareword candidates still go through the same cwd-aware filesystem check before they become interactive, so plain words, domains, versions, and non-existent filenames stay inert. Set interactive_paths_barewords = off for the older slash-required behavior.

interactive_paths_click_hint = on (the default) shows a transient, bottom-left Ctrl-click teaching chip, or Cmd-click on macOS, after two plain mis-clicks on a resolved path within a short window. It is purely presentational and rate-limited. Set interactive_paths_click_hint = off to suppress the chip entirely; it is also inert whenever the master interactive_paths gate is off.

The cursor affordance is the only frame-affecting change — there is no underline or other decoration, so with the feature on the rendered frame bytes are unchanged and only the mouse cursor shape reflects a hovered path. Detection is local-only: candidate spans are never logged, persisted, or sent anywhere, and the single filesystem stat happens only on a span actually under the pointer (the default, feature-off path makes zero stat calls). Hover detection runs on the focused pane only (a v1 bound shared with OSC 8 hyperlink hover).

Opening a path (modifier+click + context menu). With the feature on, Ctrl+click on Linux/Windows or Cmd+click on macOS opens a resolved span. This works even while a full mouse-tracking TUI has mouse reporting on: a modifier+click that lands on a resolved span opens it, while the same click anywhere else still reports to the app, so the program keeps its clicks. No extra Shift is needed. A right-click over a resolved span adds a file section to the context menu — Open, Open With…, Copy Path, Copy File, Reveal in File Manager ("Open With…" appears only on a regular file, not a directory).

Every open is an argv vector, never a shell string, so a path containing spaces, ;, $(), or backticks is inert. On Windows the launcher is explorer with the target as a single argv element, so a path or URI carrying cmd.exe metacharacters (&, |, %VAR%) never reaches a command line. The dispatch:

SpanAction
File, no :lineLinux: xdg-open <abs>; macOS: open <abs>; Windows: explorer <abs>
File, :line[:col]editor at that position (see below)
DirectoryLinux: xdg-open <abs>; macOS: open <abs>; Windows: explorer <abs>

"Copy Path" copies the absolute path; "Copy File" copies a file://<abs> URI as text (the clipboard is text-only — this pastes into file managers as a file reference); "Reveal in File Manager" opens the containing directory on Linux, uses open -R <abs> on macOS, and uses Explorer /select, on Windows.

Choosing an application ("Open With…"). On a regular-file span the file section gains an Open With… item that opens a type-to-filter picker overlay of the desktop applications registered to handle the file's MIME type. On Linux, the MIME type is detected first with a single read-only xdg-mime query filetype <abs> call. If that system probe is unavailable or empty, OdyTTY falls back to a small built-in magic-byte sniff for common file types (PNG, JPEG, GIF, PDF, WebP, BMP, TIFF). macOS asks NSWorkspace for registered applications directly. Windows does not enumerate applications yet, so its picker opens empty.

On Linux, candidate applications are read from the standard freedesktop locations (mimeapps.list defaults + added associations, then mimeinfo.cache, across the XDG_CONFIG_*/XDG_DATA_* directory ladders), honoring [Removed Associations]. Apps marked NoDisplay, Hidden, or Terminal=true, or without an Exec, are excluded; the list is capped and deduplicated (user entries override system ones). Selecting an app launches it on the file. The launch is built per the Desktop-Entry quoting rules, not a shell: the .desktop Exec is tokenized, %f/%F expand to the bare path and %u/%U to a file:// URI as a single argv element, and %i/%c/%k plus the deprecated field codes are stripped — so a path containing spaces, ;, $(), or backticks is one inert argument, never interpolated.

If the MIME type cannot be detected or no application handles it, the picker opens with an empty-state hint. Closed, the overlay is byte-identical to the live frame.

In-terminal image viewer ("Open in OdyTTY"). When the resolved span is an image file — extension .png, .jpg/.jpeg, or .webp (matching the built-in decoders; GIF/BMP/TIFF are not offered) — the file section gains an Open in OdyTTY item. It decodes the image and renders it centered, aspect-preserved, over a dimmed backdrop through the existing GPU graphics path; Esc (or a click away) dismisses it. The viewer is presentation-only: while it is closed the frame is byte-identical, and opening it never mutates the live terminal.

The decode is bounded before it runs (max 12000 px per axis, 256 MiB allocation), so a corrupt or decompression-bomb file is refused gracefully — it simply does not open, never crashes or hangs. The image type is confirmed by content (magic-byte sniff), not by trusting the file name. It is gated by the master interactive_paths setting plus interactive_paths_image_inline (default on): with the master gate on and interactive_paths_image_inline = on, platform-modifier clicking a resolved .png/.jpg/.jpeg/.webp span opens the in-app viewer and the Open in OdyTTY menu item appears. Set interactive_paths_image_inline = off to route image paths to the external default app (the same platform-opener path as any other file) instead of the in-app lightbox.

With the master gate off there is no image detection and no menu item at all.

Editor selection (interactive_paths_editor). A path:line:col span opens in an editor chosen by: the interactive_paths_editor setting (env ODYTTY_INTERACTIVE_PATHS_EDITOR) if non-empty, else $EDITOR/$VISUAL, else the platform opener (position lost). The value is either a known editor namevim, nvim, vi, code, emacs, emacsclient, helix/hx, sublime/subl, nano, micro (each mapped to its position-flag form, e.g. code --goto F:L:C, vim +call cursor(L,C) F, nano +L,C F) — or an argv template with {file}, {line}, {col} placeholders (e.g.

myeditor --line {line} {file}). The spec is always whitespace-tokenized into argv and never evaluated by a shell; a $EDITOR carrying args (code --wait) is split into argv too. Both the toggle and the editor knob live in the Settings panel's Input section.

Troubleshooting interactive paths. If modifier+click does nothing, confirm interactive_paths = on, the pointer is over the focused pane, and the span resolves to a real file or directory from the pane cwd. Bare filenames from plain ls output require interactive_paths_barewords = on. If Open or Reveal fails, Linux needs xdg-open available on PATH; macOS uses open; Windows uses Explorer directly.

If Open With is empty on Linux or macOS, the file type was not recognized or no matching application was registered. Windows application enumeration is not implemented, so its picker always shows the empty state.

Use The Native Settings UI

  • Ctrl+Shift+, opens Settings. / filters by name, key, description, or group. Esc clears the filter or closes the panel. Ctrl+S persists changes.

  • Theme, font, and path rows open pickers. Mouse wheel scrolls pickers; title back affordances return to Settings when launched from Settings.

  • Numeric rows use discrete steppers and click-to-type entry. The first typed key replaces the prefilled value.

  • Right-click opens the context menu. On OSC 133-aware prompts it can copy, cut, delete, clear input, open settings, and create, rename, or close tabs. A custom tab name is session-local; it overrides shell title updates until an empty rename clears it. The menu also offers Detach & switch, which spawns a fresh managed session in the focused pane's working directory and switches to it (a Swap / Keep both / Cancel prompt; this is a new session, not a live migration of the current one), and Manage Sessions (the session-attach overlay, default Ctrl+Shift+A).

    With interactive_paths on, right-clicking over a resolved path adds a file section (Open / Open With… / Copy Path / Copy File / Reveal in File Manager), plus Open in OdyTTY on an image file (in-terminal viewer). Open With… opens an app picker for the file's MIME type; the launch is argv-only (Desktop-Entry quoting, never a shell).

  • First launch without a config file shows an onboarding card. Set ODYTTY_ONBOARDING=1 to force it.

Examples

Use the plain renderer for compatibility or performance checks:

ODYTTY_RENDER_QUALITY=plain odytty

Follow the OS dark or light appearance:

ODYTTY_THEME=system odytty

Show a background image through translucent cells:

ODYTTY_BACKGROUND_TREATMENT=image \
ODYTTY_BACKGROUND_IMAGE=/tmp/background.jpg \
ODYTTY_CELL_BG_OPACITY=0.85 \
odytty

Use a non-blinking underline cursor:

ODYTTY_CURSOR_STYLE=underline ODYTTY_CURSOR_BLINK=off odytty

Close automatically during lifecycle smoke checks:

ODYTTY_NATIVE_AUTOCLOSE_MS=600 odytty

Bench Environment Variables

These affect cargo bench --bench perf only.

VariableValuesDefault
ODYTTY_PERF_PROFILEdefault, legacy, quickdefault
ODYTTY_PERF_GEOMETRY_ONLYAny non-empty valueunset
ODYTTY_PERF_SE_ONLYAny non-empty valueunset

ODYTTY_PERF_SE_ONLY=1 runs exact 64,000,000-byte SE1 and SE2 fixtures through the parser alone, the terminal model with one retained line, and the terminal model with the protocol's 100,000-line history bound. It isolates the core from the PTY, native event loop, geometry builder, and GPU presenter.

Launch CLI

The native window accepts launch-scoped title, directory, application identity, command, and command-exit handling:

odytty [--native] [--title TITLE] [--working-directory DIR] \
  [--app-id APP_ID | --class APP_ID] [--hold[=true|false]] [-e COMMAND...]

With no arguments, OdyTTY opens the native window and the user's default shell. --native is a compatibility alias for that same launch. --title sets the initial window title, and --working-directory sets the initial PTY directory; both also accept --flag=value. --working-dir is an accepted compatibility alias for --working-directory.

-e or --execute runs the remaining argv as the initial command instead of the default shell. A bare -- has the same command-boundary meaning. Anything after the command boundary belongs to the child, even when it resembles an OdyTTY option.

--app-id and --class are aliases. On Linux, their value becomes this window's Wayland app_id and the class half of X11 WM_CLASS; the X11 instance remains odytty. With neither flag, OdyTTY keeps io.unfinished_works.odytty. The override is per window: it does not change the desktop-file id, icon name, packaged StartupWMClass, or a future unrelated launch.

--hold, --hold=true, and --hold=false are accepted. The default is false, so an ordinary shell or command exit keeps the existing close behavior. When enabled, only the initial local command is retained after EOF; tabs, panes, and workspaces created later do not inherit it. OdyTTY renders a line containing Process exited with status N when a numeric code is available, or Process exited with unknown status (the process may have exited from a signal) otherwise, followed by Press any key to close. A key release does nothing. While the held pane is focused, the first press or repeat is consumed, closes it through normal cleanup, and either exits the window or focuses the surviving pane/tab/workspace. A dropped remote session's reconnect prompt still takes precedence over hold.

Introspection and shell-integration commands

These commands print and exit without opening a window:

odytty --version
odytty --help  # -h is an alias
odytty --list-themes
odytty --list-fonts
odytty --show-config
odytty --core-smoke
odytty shell-integration SHELL  # bash, zsh, fish, powershell, or pwsh

--list-themes and --list-fonts emit stable, machine-friendly inventories; --show-config emits the stable diagnostic subset of the effective settings; and --core-smoke prints a small owned-parser/terminal-model transcript. The shell-integration command prints the requested setup snippet to stdout (pwsh is accepted as an alias for powershell).

The Unix-only --interactive mode retains the original headless terminal frontend for diagnostics and development; it uses the caller's terminal for display and reserves Ctrl+Q as its local quit chord. It is not the normal native application path and is rejected cleanly on Windows.

Detached-Session CLI

Detached sessions have no odytty.conf keys in this slice. These commands are available on Unix; Windows rejects them with a clean unsupported-platform error:

odytty new --detached [-e COMMAND...] [--working-directory DIR] [--title TITLE] [--app-id APP_ID]
odytty list
odytty attach [ID]
odytty attach --diagnostic ID

new --detached starts a local session-host process and prints id=.... list prints one tab-separated row per live session: its title or id, pane count, humanized age, and a trailing id in parentheses when the title differs. It never prints scrollback or command output. attach [ID] reattaches a detached session in a live native window; without an id it attaches the sole live session or lists the choices. The window opens its normal initial local session, adds the hosted session as a focused tab repainted from the host snapshot, and streams live output.

new accepts --app-id / --class in both space and equals forms so shared launchers can pass a consistent argument surface. Detached creation has no window: the parsed value is not written to host metadata, and a later unrelated odytty attach uses the packaged default window identity.

If an explicitly requested id is dead, the window still opens and stderr reports odytty: attach session <id> failed: <err>. The headless script/CI form, attach --diagnostic <id>, prints a one-line status dump (`id=... state=attached mode=diagnostic columns=... rows=...

panes=1`) and exits without opening a window.

Host lifecycle is local-only and bounded. Each attach receives a current SnapshotEnvelope first, then future Output and Invalidate frames while it stays connected. Detach or socket close removes only that client; the hosted PTY and terminal model keep running with bounded scrollback until the child exits or the detached idle timeout (12 hours with no attached client) kills and reaps it. The idle bound is internal (--idle-timeout-ms); there is no user-facing flag or environment variable for it.

Scrollback is not printed by list and is not sent anywhere except over the per-user Unix-domain socket to an attaching local client.

The session-host socket lives under a per-user runtime directory. An explicitly-set XDG_RUNTIME_DIR always wins on supported Unix hosts (Linux uses its standard /run/user/<uid>). On macOS, which does not set XDG_RUNTIME_DIR, the host falls back to the per-user Darwin temp directory (std::env::temp_dir()confstr(_CS_DARWIN_USER_TEMP_DIR), e.g. /var/folders/.../T/).

In both cases the odytty/ socket subdirectory is created 0700 and validated owner-private, so both Unix resolution paths stay local-only and owner-private. No network service is opened. AF_UNIX socket paths are bounded (sun_path is 104 bytes on macOS, 108 on Linux); a runtime base long enough to overflow that limit is rejected with a clear error instead of an opaque bind() failure.

Command Palette

The command palette is exposed through the command-palette action in keybinds. It is bound by default to Ctrl+Shift+P (and a right-click menu "Command Palette" item); rebind it in odytty.conf as usual:

keybinds = ctrl+alt+p=command-palette

For a one-session override:

ODYTTY_KEYBINDS="ctrl+alt+p=command-palette" odytty

Environment values win for that session.

For palette behavior and its bounded action, history, and directory sources, see Search Actions, History, And Directories.

Connection Hosts

The SSH / connection manager reads its default saved hosts from an OdyTTY-owned local file:

  • %APPDATA%\odytty\hosts.conf on Windows
  • $XDG_CONFIG_HOME/odytty/hosts.conf
  • ~/.config/odytty/hosts.conf on Unix when XDG_CONFIG_HOME is unset

The file uses an OpenSSH-like block format:

Host web1
    HostName web1.example.invalid
    User deploy
    Port 2222
    Theme odyssey
    Font "Victor Mono"
    Title "Synthetic Web"
    IdentityFile ~/.ssh/id_ed25519

Host aliases are the quick-connect names. HostName, User, and Port drive the connect action. OdyTTY builds argv as ssh [-p PORT] -- [USER@]HOST and opens it in a new tab/session; -- keeps a saved host name from being interpreted as another ssh option. Theme, Font, and Title are optional per-host profile fields reserved for the overlay UI.

Integration on|off, Reuse on|off, and Tmux on|off are optional per-host overrides for remote shell integration, connection reuse, and tmux persistence (see below). Persist overrides the connection-persistence window for one host (any ssh ControlPersist value, e.g. off, 2h, 45m). IdentityFile names a path to an existing SSH private key; when set, the connect argv gains -i <path> so a key that is not in ~/.ssh/config still authenticates.

OdyTTY stores only the path — never any key material — and ssh-copy-id remains the once-and-done way off passwords entirely. A Protocol key is reserved (default and only accepted value ssh) so a future transport needs no file-format migration; any value is preserved across an edit.

You do not have to hand-edit this file to reach a new host. In the connection manager, typing a [user@]host[:port] that matches no saved host offers a Connect to: … row — Enter connects, and Shift+Enter (or Ctrl+S) connects and appends a Host block here for you. The append is atomic (temp-file-and-rename) and preserves the file's existing contents byte-for-byte; the new block reads Host <host> (no redundant HostName when the alias is the host) plus User/Port when supplied. An exact-alias collision skips the write and reports "already saved".

Typed input with embedded spaces, a leading -, or a port outside 1-65535 is rejected before any connect or write.

You can also add and edit hosts with an in-app form instead of typing directives by hand. In the connection manager, Tab opens a blank Add connection form and the right arrow (\u{2192}) opens an Edit form pre-filled from the selected OdyTTY-owned row (ssh-config-imported rows are read-only). The form carries Alias, HostName, User, and Port up front, with an Advanced section for IdentityFile, the three-way Integration / Reuse / Tmux overrides (inherit / on / off), and Theme / Font / Title. On the IdentityFile row, Enter (while the field is empty) — or a click on the always-visible [Browse] chip at the end of the row (whether the field is empty or already holds a path) — opens a browser of candidate private keys found under ~/.ssh — filename heuristics only (id_*, *.pem, *.key, and any file with a matching .pub sibling; *.pub, known_hosts, config, and authorized_keys are excluded).

The browser lists file names only and never reads key contents; picking one fills the path, and typing a path by hand stays fully supported (keys can live outside ~/.ssh). A focused-field help line at the bottom of the form explains each field as you move through it. Field validation matches the ad-hoc rules; an alias collision is refused inline with no write. Save (or Ctrl+S) appends a new block or edits the existing one in place — an edit re-renders only that block and leaves every other block, comment, and unknown field byte-for-byte untouched.

Test connection runs a non-interactive background probe (ssh -o BatchMode=yes -o ConnectTimeout=5 … exit) and reports a tri-state result: reachable with key/agent auth, reachable but interactive-auth (the expected state for a password host — the connect still works, interactively), a host-key mismatch, or unreachable. The probe carries no password and stores nothing credential-shaped.

Remote shell integration (remote_integration)

An SSH tab runs the system ssh as its local child, so by default the remote shell never sees OdyTTY's OSC 133 hooks and a remote session loses prompt marks, cwd titles, and the input boundaries those features need. With remote_integration = on (the default; ODYTTY_REMOTE_INTEGRATION=on), a connection injects OdyTTY's bash integration on the remote so a remote bash session behaves like a local one. The integration is delivered inline as a base64 blob decoded into a temporary rcfile that self-deletes on first read — nothing is persisted on the remote. Every failure path (no bash, no base64, undetectable shell) and any non-bash remote shell degrades silently to a plain ssh session, so the connection is never broken.

Turning it off globally, or setting Integration off for a single host in hosts.conf, makes that SSH launch byte-identical to a plain ssh invocation. The remote command is a fixed, inspectable POSIX-sh bootstrap plus OdyTTY's own public integration snippet; no local paths, usernames, or hostnames are embedded in it, and authentication stays entirely with the system ssh. A remote SSH tab titles itself user@host when no explicit per-host Title is set.

SSH connection reuse (remote_reuse)

With remote_reuse = on (the default; ODYTTY_REMOTE_REUSE=on), an integrated SSH tab adds OpenSSH ControlMaster=auto / ControlPersist multiplexing with a control socket OdyTTY owns under its state directory. The first tab to an effective SSH endpoint establishes a shared master connection; later tabs to that endpoint reuse it, so they open with no second authentication or handshake. If the shared master is gone, the tab degrades to an ordinary fresh connect. A per-host Reuse off line in hosts.conf opts a single host out, and remote_reuse = off disables it globally.

The socket template uses OpenSSH's %C connection hash. OpenSSH derives that identity from the effective local host, remote host, port, and remote user, so profiles for different endpoints cannot alias one multiplexed connection even when they share a textual hostname or an OdyTTY control directory.

Reuse layers onto integrated sessions only, so with remote_integration off the SSH argv stays byte-identical to a plain ssh launch regardless of this setting. Windows: OpenSSH for Windows has no connection multiplexing, so a Windows client never emits control options and reuse is a silent no-op there.

On Unix, the final ControlMaster directory must be a real, effective-UID-owned directory rather than a symlink or other object. OdyTTY validates it through a no-follow directory handle and repairs only its own permissions to 0700; a failed check disables reuse for that launch rather than emitting a control path.

SSH connection persistence window (remote_persist)

Connection reuse keeps an authenticated master alive for a while after the last tab to a host closes, so a daily-driver host is authenticated roughly once per boot rather than once per tab. remote_persist (ODYTTY_REMOTE_PERSIST) sets how long that master lingers: 10m (the default), 30m, 1h, 2h, or off. The default 10m maps to OpenSSH ControlPersist=600, which is the historical fixed window — so the default is a no-op change and existing behavior is unchanged. off maps to ControlPersist=no, tearing the master down with its last connection (the pre-persistence posture).

A per-host Persist line in hosts.conf overrides the global value for a single host and additionally accepts any raw ssh ControlPersist value (for example Persist 45m). This only takes effect with connection reuse on. Windows: OpenSSH for Windows has no connection multiplexing, so this knob is inert on a Windows client (no control options are ever emitted).

SSH session persistence (remote_tmux)

With remote_tmux = on (ODYTTY_REMOTE_TMUX=on; off by default), an integrated SSH tab wraps the remote shell in a persistent tmux session (tmux new-session -A -s odytty). A create-or-attach session means a link that drops and is reconnected reattaches the same remote session with its running programs and scrollback intact, rather than starting fresh. When the remote host has no tmux, the bootstrap degrades to a plain integrated bash session, so enabling this never breaks a connection. A per-host Tmux on line in hosts.conf opts a single host in (or Tmux off opts one out) regardless of the global default.

Persistence rides inside the integration bootstrap, so it only takes effect with remote_integration on; with integration off the SSH argv is byte-identical to a plain ssh launch regardless of this setting. Windows: the wrap is remote-side, so a Windows client drives it the same as any other, provided the remote host has tmux.

Dropped-connection reconnect

An integrated SSH tab whose link drops (the ssh client exits with its transport-failure status, 255) does not close silently. The tab is held open with an in-pane "connection dropped" prompt: press Enter to re-establish the connection in the same tab, or Esc / Ctrl+D to dismiss and close it. Reconnect re-runs the exact same argv, so with remote_tmux on it reattaches the persisted session. A clean logout (exit, status 0) and ordinary remote-command failures close the tab as before; only the transport-drop status offers reconnect.

Remote image paste-through (remote_image_paste)

With remote_image_paste = ask (the default; ODYTTY_REMOTE_IMAGE_PASTE=ask), pasting while the clipboard holds an image and the active tab is a remote integrated SSH session offers to upload it to the remote host. A confirm prompt appears in the pane — showing the encoded size and the target host — and nothing is uploaded until Enter confirms (Esc cancels). On confirmation the image is PNG-encoded and streamed over the tab's ssh connection (reusing the live ControlMaster when one is up) into a file created 0600 under an unguessable /tmp/odytty-paste-<random>.png name. On success a one-line notice — image uploaded <path> · copied to clipboard — is written into the pane and the remote path is copied to the local clipboard; the path is not typed into the shell (a bare path on an empty prompt would run on the next Enter and error).

Paste it (Ctrl+Shift+V) into a command wherever the file is wanted. Nothing is ever run remotely — it is an upload plus a clipboard copy, not a command. The feature also engages on reconnected and restored remote tabs.

remote_image_paste = off (ODYTTY_REMOTE_IMAGE_PASTE=off) disables the feature: an image paste on a remote tab does nothing. There is deliberately no silent auto-upload mode — confirm-first is the only enabled behavior. The feature only engages on a remote integrated tab; a local tab or an integration-off plain-ssh tab pastes exactly as before. Images larger than 10 MiB (PNG-encoded) are refused with a one-line notice rather than uploaded.

Uploaded files are cleaned up best-effort when the tab closes (an rm -f over the same connection). If the link has already dropped, cleanup cannot run and the file persists in the remote /tmp until the remote's own temp-file reaper removes it — OdyTTY never promises guaranteed remote deletion. Windows: the upload uses the bundled ssh.exe the same way (no ControlMaster reuse, as OpenSSH for Windows has none), so each upload does its own connect; the clipboard image is read through the platform clipboard backend. A Windows remote is out of scope (the /tmp path assumes a POSIX host).

OpenSSH config import is separate and default-off. ssh_config_hosts = on (or ODYTTY_SSH_CONFIG_HOSTS=on) lets the connection manager merge host names from a caller-resolved OpenSSH config path. The same toggle is reachable in the Settings panel's Connections section. While it is off, OdyTTY does not read OpenSSH config.

When enabled, the read is local, read-only, name-only, bounded, and ignores key material such as identity files. OdyTTY never handles SSH credentials, private keys, or passphrases; authentication remains with the system ssh binary and agent.

The saved hosts are browsed through the connection-manager overlay, opened by default with Ctrl+Shift+S (or the right-click menu's "Connection Manager" item). Rebind the connection-manager action in odytty.conf:

keybinds = ctrl+alt+h=connection-manager

For a one-session override:

ODYTTY_KEYBINDS="ctrl+alt+h=connection-manager" odytty

Environment values win for that session. The overlay lists the merged hosts (OdyTTY-owned first, then any opt-in OpenSSH-config names), with type-to-filter fuzzy matching over alias, host name, and user; / select, Enter quick-connects the highlighted host, and Esc dismisses. With ssh_config_hosts off, the overlay shows OdyTTY-owned hosts only and OdyTTY never references ~/.ssh at all. The overlay is presentation-only: it reads a frozen snapshot of the hosts list and never mutates live terminal state; accepting a host hands the connect action a name-only target to spawn.

Session-attach summon overlay (session-attach)

The in-window analogue of the odytty attach CLI: a session-attach overlay that lists the live, detached session-host sessions so you can reattach one without leaving the window. Open it with Ctrl+Shift+A by default (or the right-click menu's "Manage Sessions" item). Rebind the session-attach action in odytty.conf:

keybinds = ctrl+alt+a=session-attach

For a one-session override:

ODYTTY_KEYBINDS="ctrl+alt+a=session-attach" odytty

Environment values win for that session. The overlay lists each live session by its --title (falling back to the session id), with type-to-filter fuzzy matching over title and id; / select, Enter attaches the highlighted session into a new tab, and Esc dismisses. With no live sessions it opens to a hint rather than failing. The overlay is presentation-only: it reads a frozen snapshot of the live sessions on Unix. On Windows it opens with the empty-state hint and attach is unavailable. The overlay never attaches anything itself; accepting a row hands the App an attach request.

If the chosen session ended between listing and accepting, the attach fails gracefully (no panic) and the user can retry.

Footnotes

  1. crt_curvature is a config-file / environment-only knob and is not shown in the in-app settings panel. It still parses, clamps, and hot-reloads from odytty.conf or ODYTTY_CRT_CURVATURE as documented; the default 0.0 is flat and pixel-identical to the no-curvature path.