This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Always write zig 0.16.0 not older zig
Always use release builds (-Doptimize=ReleaseFast) for build/test commands; Debug builds can segfault in large transitive compiles
Always update unit test, benchmarks, readme and this document when making changes that require it
Follow data drive design and branchless programming pattern wherever possible
Performance is critical, if something is going to hurt performance flag it up
Never add Co-Authored-By or any co-author lines to git commits
Otter Shell is a Zig-based Wayland desktop shell monorepo. The primary applications are otter-bar (status bar), otter-launcher (application launcher), otter-files (native file manager), xdg-desktop-portal-otter (XDG desktop portal backend), otter-dock (application dock with magnification and window previews), otter-taskbar (Windows-style taskbar with grouped windows, Start panel, and hover previews), otter-term (Ghostty-based terminal), otter-monitor (XDG toplevel system monitor), otter-pkg (graphical apt package manager), otter-assistant (AI assistant toplevel backed by otter-assist), otter-assist (local text-to-text assistant daemon on a per-Wayland-display Unix socket), otter-notifications (notification daemon), otter-wallpaper (wallpaper daemon), otter-osd (on-screen display daemon), otter-transcribe (hotkey transcription daemon with layer-shell indicator), otter-jade (animated layer-shell otter pet), otter-logout (power menu overlay), otter-overview (workspace/window overview plus the otter-switcher Alt-Tab executable), otter-keybindhelp (searchable compositor shortcut overlay), otter-polkit (polkit authentication agent), otter-lock (session lockscreen), otter-greeter (Wayland display manager daemon and greeter UI), otter-idle (idle management daemon), otter-nightlight (gamma night-light daemon), otter-settings (graphical config editor and theme browser), otter-screenshot (region screenshot tool over ext_image_copy_capture_v1), otter-search (desktop search daemon and otter-searchctl CLI), otter-shot (Wayland product-shot composer), otter-theme-gen (wallpaper-reactive theme generator daemon), otter-hypr (Hyprland otter-float lua layout + otter-hypr-titlebar companion), and otter-bench (Otter HUD + Otter Bench performance suite), all using the shared component libraries.
New small-tool applications include otter-clip, otter-rec, otter-pick, otter-cal, otter-emoji, otter-timer, otter-note, otter-weather, otter-calc, otter-vox, and otter-clicker.
Non-application directories in the workspace: otter-examples (starter templates for Surface Description apps; otter-test0 is a scratch copy of it), otter-zenith (Debian packaging meta-repository that clones tagged releases), otter-site (SvelteKit website), zango (wlroots 0.19 tiling compositor — separate project, not built against the shell libraries), and zig-regex (vendored dependency).
Important: The workspace root (otter-shell/) is a thin git repo of package submodules. Each package folder (otter-bar/, otter-ui/, otter-conf/, etc.) remains its own git repo with its own remote — commits and pushes for code changes go per-package, not as normal files on the root. Root commits only update docs/skills/tools or submodule SHA pins. Cloud clones need git clone --recurse-submodules (or ./scripts/sync-workspace.sh).
# Build and run any app
cd <component> && zig build -Doptimize=ReleaseFast run
# Run tests for any component
cd <component> && zig build -Doptimize=ReleaseFast test
# Build only
zig build -Doptimize=ReleaseFast
# Send OSD command (client mode)
cd otter-osd && zig build -Doptimize=ReleaseFast run -- volume-up 5
# Build and run terminal
cd otter-term && zig build -Doptimize=ReleaseFast run
# Build and run file manager
cd otter-files && zig build -Doptimize=ReleaseFast run
# Build and verify desktop portal
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast test
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast integration
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast real-picker-integration
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast bench
cd xdg-desktop-portal-otter && zig build -Doptimize=ReleaseFast check-lines
# Benchmarks
cd otter-conf/benchmarks && zig build -Doptimize=ReleaseFast bench
cd otter-render/benchmarks && zig build -Doptimize=ReleaseFast -Dcpu=x86_64_v3 bench
cd otter-ui/benchmarks && zig build -Doptimize=ReleaseFast -Dcpu=x86_64_v3 bench
cd otter-desktop/benchmarks && zig build -Doptimize=ReleaseFast bench
cd otter-theme-gen/benchmarks && zig build -Doptimize=ReleaseFast bench
cd otter-search && zig build -Doptimize=ReleaseFast bench
cd otter-cue && zig build -Doptimize=ReleaseFast bench
cd otter-cue && zig build -Doptimize=ReleaseFast run -- --dry success| Library | Purpose |
|---|---|
| otter-conf | Comptime config parser (flat key-value format) |
| otter-theme | Four-layer visual theme (12 semantic colours, structural layout, complete spacing scale, globals, loader/migration) |
| otter-ui | Surface Description frame API (bounded layout, hit registry, overlays, UniformList), bar widget shell (geometry/motion state), drawing primitives, text input, icon caching, scroll state, VTE wrappers |
| otter-vte | Shared terminal rendering primitives: dynamic terminal command list, powerline/block glyph ops, custom cell glyphs, generic cell renderer for embeddable terminal views |
| otter-wayland | Wayland client (layer shell, xdg_shell, session lock, workspaces, toplevel, keyboard, IME, pointer, clipboard, damage tracking, capture selection overlay) |
| otter-config-types | Shared config struct definitions for all apps (bar, launcher, dock, taskbar, notifications, wallpaper, osd, transcribe, logout, overview, polkit, lock, idle) |
| otter-tools-core | Pure Zig bounded helpers for small tools: clipboard history, recorder encoder selection, color conversion, SplatHash sampling, semantic palette generation, calendar grids, emoji search, duration parsing, weather URL construction, calculator parsing, otter-search path/content indexing, assistant socket protocol helpers, vendored zidx knowledge retrieval, and Pika Search JSON parsing |
| otter-render | FreeType fonts, Text System boundary (HarfBuzz, itijah, Fontconfig), colors, image loading, sprite sheets, animation, CommandList + quad_renderer pipeline |
| otter-desktop | XDG icons, .desktop files, D-Bus services (UPower, MPRIS, SystemTray, NetworkManager, Logind, ScreenSaverService, etc.), SysInfo, PipeWire, PAM auth; libotter_desktop.so.0 exports BASU-backed services to plugin hosts |
| otter-cue | Native Cuelume interaction-sound palette (17 synthesized cues, Bind hover/press/release/toggle, PipeWire playback via otter-desktop) |
| otter-geo | Pure geometry types (Point, Rect, Padding, Transform) |
| otter-utils | BoundedArray, logging, FilePath utilities, the app-wide std.Io singleton, runtime-state paths |
D-Bus (basu), PipeWire, and PAM are compile-time optional via -Denable_dbus=false / -Denable_pipewire=false / -Denable_pam=false. C allocator overridable with -Dc_allocator=jemalloc or -Dc_allocator=mimalloc.
- Comptime Generic Parser (otter-conf):
Parser(comptime T: type)generates parsers at compile time. Nested struct flattening (prefix_field->config.prefix.field), custom value types withparse()/toStr(), SIMD string scanning. - Global
std.Io(otter-utils):otter_utils.io.install(io)is called once from each app'smainwith thestd.Iofromstd.process.Init; shared libraries read it back viaotter_utils.io.get(). This keepsioout of library signatures while still routing I/O through the app's realIo.get()falls back tostd.Options.debug_iowheninstallhas not run, so tests and one-off tools work unchanged. - Bar Widget Shell (otter-ui): Bar widgets embed a
Widgetstruct for shared geometry (area) and pointer state (last_motion,full_redraw). Layout, draw, and input call widget struct methods directly viawidget_registryand Surface Description specs — no vtable. - Bar widget specs (otter-ui): All 17 bar widgets emit frames via
otter-ui/src/widgets/specs/*.zig; widget files own state anddrawFrameemitters.FieldRegistrycalls structgetWidth/setArea/deinitdirectly (childWidgetshells hold area/motion only). - Bar layout (otter-bar):
bar_layout_engine.zigdivides the bar into three fixed columns (each up to one third of inner width). Left packs LTR, center centers its group in the middle third, right packs RTL.root_layout.zigdraws widgets and emits declarative SD rows for hits. - Bar input:
bar_input_dispatch.zigroutes bar-surface pointer/scroll/click throughUiState.dispatchplus hover-tooltip handling. Click popups (calendar, weather, volume, mpris, menus) stay until bar click or click-off overlay.bar_popup_input.zighandles LayerPopup surfaces. - Dim overlay teardown (otter-wayland): Destroy the layer role and surface before the buffer-release roundtrip. Null-buffer unmapping leaves configure callbacks alive after their serials are invalidated, disconnecting clients on Mango.
OTTER_TEST_WAYLAND=1enables the real-compositor lifecycle regression in the ReleaseFast test suite. - Capture selection overlay (otter-wayland):
selection_overlay.zigis the single shared frozen/live region selector forotter-screenshot,otter-shot, andotter-rec, and point selector forotter-clicker. Usescapture_overlay+ direct SHM pixel compositing — not Surface Description. Interactive selection/editor surfaces are double-buffered and coalesce missed redraws until compositor buffer release; never rewrite compositor-owned buffers. Apps passselection_overlay.preset.{screenshot,shot,rec,clicker}for layer namespace and visual mode (.frozen_unshadedfor screenshot CLI,.frozen_dimmedfor shot/rec, transparent.livefor clicker). - Capture backend selection (otter-wayland): Full-output capture prefers EXT image-copy capture plus an EXT output source, falling back to WLR screencopy v1–v3. Both raw paths normalize output transforms without changing packed pixel formats; WLR additionally handles
y_invertand row padding. Use immediate WLR copies for bounded requests on static desktops. DMA-BUF registry binding caps the advertised version at v4 and consumes legacy v3 format/modifier events. Test both capture paths and DMA-BUF v3/v4 negotiation in isolated VMs. - File-picker windows (otter-files): Only
--picker-serviceowns the picker socket. Dedicated picker windows close after the last completed/cancelled/disconnected request, while validation errors keep active requests open. Ordinary windows retain FileManager1 ownership and are not reused as portal pickers. Portal spawn coalescing resets after the final request completes. Socket cleanup compares the pathname inode/device recorded after bind; a Unix socket FD has a different identity. Picker filename filters support bounded glob matching with bracket classes (including Firefox/LibreWolf*.[zZ][iI][pP]filters). Folder requests with bothdirectory=trueandmultiple=trueuse the appended OTFPopen_directoriesmode; keep existing wire mode numbers stable. Watcher refreshes retain visible contents, coalesce changes during scans, and ignore hidden-entry changes when hidden files are off. Refresh file contents on writer close, retain watches for unchanged paths, and defer watcher refreshes during operations so a copy cannot cancel itself. Sidebar mount completion opens the UDisks-returned path; discovery and repeated clicks must preserve outstanding device actions. Navigation still invalidates picker acceptance. Picker initial folders do not constrain later navigation: accept only the completed current listing generation. Reserve the picker footer in both rendering and view materialization. Surface Description clips descendant hit regions together with their visual viewport. - Screenshot color handling:
otter-screenshotandotter-shotshareotter-wayland.capture.Capturer.captureAllqueries active output descriptions throughwp_color_manager_v1and normalizes supported PQ HDR outputs before stitching/cropping/annotation: preserve source precision, decode ST 2084, convert primaries, apply a BT.2390 shoulder from measured frame peak, and encode sRGB. Bit depth and EDID capability are not HDR detection. Toplevel captures and raw recording frames retain their compositor paths; Hyprland output captures are already SDR and bypass the query/conversion.--tonemap-hdris retired. Capture v1 lacks source color metadata: unknown descriptions remain unchanged, and other compositors returning SDR from HDR outputs require a documented exception. Packed 8-bit/10-bit HDR uses CPU-width SIMD plus per-code PQ/shoulder LUTs; frames at least 1920×1080 share one peak/LUT set across four existing Io workers. Other formats use the scalar reference. Differential checks cover packed formats, tails, strides, and alpha.zig build -Doptimize=ReleaseFast bench-capturecompares scalar/SIMD CPU conversion costs on gray and colored input. - Recorder timing and HDR (otter-rec): Submit each captured frame once with elapsed-time PTS; never fill missed intervals with duplicate encode calls. Retain the last frame to mark stop time, use atomic signal flags, and keep software x264 on
veryfast/zerolatencyso Stop does not flush a lookahead backlog. Native Wayland output/region frames use active color descriptions and shared HDR conversion before encoding. KMS protocol v2 carries active PQ metadata; NVIDIA performs region-peak reduction, BT.2390 and BT.2020-to-sRGB conversion on GL textures without CPU readback. Map GL/CUDA resources only around each copy. Files declare sRGB transfer, BT.709 primaries/matrix. Raw capture APIs remain raw; never infer HDR from bit depth.tests/recording_stop.pyruns in a VM;tests/gpu.shtests synthetic 4K60 GL/CUDA/NVENC frames without capturing the desktop. - Screenshot annotations and OCR:
otter-render/src/annotation.zigowns the bounded vector document, undo/redo, hit testing, and CPU rasterizer shared byotter-screenshot --annotateand theotter-shotGUI.selection_overlay.EditorCallbackscontinue on the same layer surfaces after region selection: editor buffers are transparent outside the frozen selection, keep its image at the captured coordinates, and provide tool-specific cursors. Both editors use the semantic Tabler annotation icon set with hover tooltips. The dedicatedotter-tools-coreotter_ocrmodule runs the systemtesseractCLI and copies recognized text to the clipboard; missing Tesseract disables OCR only. - Shared model packages: production model lookup is rooted at
/usr/share/otter-shell/models/{assist,transcribe,vox}.otter-zenithpackages those weights independently as architecture-independentotter-assist-model,otter-transcribe-model, andotter-vox-model; app-local paths remain source assets for packaging, not runtime fallbacks. Explicit development model flags still override the shared paths. - CommandList + Quad Renderer (otter-render): All apps buffer draw commands via
DefaultCommandList(solidRect, blendRect, roundedSolidRect, roundedBlendRect, roundedRectOutline, text, image, sprite, scissorPush/Pop, etc.), thenquad_renderer.rasterize()plays them back with scissor clipping and damage culling. Text measurement:font.measureTextAtScale(text, size, cmds.scale). - Terminal Rendering Boundary (otter-vte): Terminal views use
DynamicCommandList,PowerlineOps,BlockOps,cell_glyphs, andCellRenderer(...)fromotter-vte. Apps provide terminal/session state and comptime hooks, while terminal cell rendering stays in the shared package so other apps can embed terminal output without copyingotter-terminternals. - Terminal Scrollback Compression (otter-term):
app/compression.zigcaches each libghostty-vt activity token, waits 250 ms after terminal activity, then schedules one incremental compression step per event-loop pass until the API reports complete or unsupported. Scheduler state follows each tab/split; logical scrollback content and limits remain unchanged. - Terminal libghostty boundary (otter-term): libghostty-vt owns paste sanitizing, word/line selection semantics and tracked active selections, OSC 7/9/1337 PWD parsing, OSC 8 hyperlinks, Unicode grapheme widths, cursor visuals, palette generation, and terminal protocol reports/callbacks (focus, resize, color scheme, device attributes, XTVERSION, clipboard writes). Otter supplies Wayland policy/UI and sources terminal colors from
otter-term.conf; UI chrome usesotter-themetokens. - Text System Boundary (otter-render):
src/text.zigowns the backend boundary for HarfBuzz shaping, itijah bidi checks/layout, and Fontconfig translate-C.TextSystem.measurepreserves the current FreeType fast path for ASCII/simple LTR and uses HarfBuzz advances plus itijah layout for complex text.TextSystem.drawrenders shaped glyph IDs through cached FreeType glyph-index rasterization and routes.notdefclusters back through the existing fallback chain.itijahis vendored at latest main commita8a70fc73ea7bcfcffd260e390521c521c1e8490with a Zig 0.16 build wrapper. - Border Radius (otter-render/otter-theme): SDF-based rounded rect rendering via
sdf.zig.Theme.Layoutsuppliescontrol_radius,panel_radius, andjoined_radius; pill radius is derived. CommandList:roundedSolidRect,roundedBlendRect,roundedRectOutlinewithcorners: u4bitmask (bit0=TL, bit1=TR, bit2=BL, bit3=BR). Zero-radius fast-paths to existing sharp rect logic. Compositor border radius sent viaset_bordersprotocol request withradiusargument. - Damage Tracking (otter-wayland):
DamageTrackeraccumulates dirty rects with double-buffer support. Falls back to full redraw on overflow. - UI command storage (otter-ui):
Capacities.command_storage = falseis only for input/layout states whose application owns an external command list. Rendering entry points intentionally fail at compile time for those states. Rendering apps keep the default bounded command capacity unless measured frame maxima and overflow behavior prove a smaller capacity safe. - Monitor storage and aggregation (otter-desktop/otter-monitor): Process, previous-process, desktop catalog, and visible/sort storage retain dynamic capacity proportional to observed counts. Application aggregation uses the sorted PID index and compact desktop catalog indices; do not restore fixed maximum-sized tables or per-process desktop strings.
- Search index lifecycle (otter-tools-core/otter-search): File-index arrays grow geometrically within the configured memory budget; paths use one owned allocation per entry with relative/name aliases. Content trigram signatures are learned lazily from real queries, so startup does not read every indexed file. Content workers read the live index only while mutations are excluded; cancel and join them before watcher updates or index replacement. Inotify updates are incremental, with full rebuild reserved for queue overflow or watch invalidation.
- Animation (otter-render): Integer-only ease-out quadratic.
anim.tick(expanding)advances,anim.lerp(from, to)interpolates. Handles mid-animation reversal. - Pub/Sub (otter-wayland):
SubscriberListfor workspace, toplevel, and SysInfo state changes. - Keyboard/IME (otter-wayland):
Keyboardwraps xkbcommon with callbacks + timerfd repeat.TextInputwraps zwp_text_input_v3 for CJK/compose — checkisComposing()to suppress direct key handling. - Drawing Primitives (otter-ui):
drawing.zigis a re-export barrel overotter-ui/src/drawing/(core,text,frame,input,controls,select,dropdown,color_picker). Roughly 60 stateless helpers with no coupling to app config structs: border, inputBox, wrappingInputBox, toggle, checkbox, colorSwatch, dropdown, dropdownOverlay, tabBar, scrollbar, formRow, sectionHeader, button, numberInput, progressBar, iconButton, textTruncated, wrappedText, colorPickerPopup, passwordInputBox, authStatusLine, maskUtf8, etc. - Drag selection (otter-ui/otter-files):
otter_ui.DragSelectionowns viewport-clipped rubber-band geometry and visible-item selection baselines. Apps supply hit rectangles and applycontainsresults. Otter Files starts on blank pane space, supports additive Ctrl/Shift dragging, and leaves entry drags to file DnD. Pressing a selected entry preserves the full drag selection; an unmodified completed click collapses it. Stop on release, leave, navigation, keyboard actions, or scrolling; edge auto-scroll is not implemented. - ScrollState (otter-ui): Vertical scroll tracking with
scroll(delta),needsScrollbar(),thumbRect(),maxOffset(). - Theme Layers (otter-theme):
Themehas exactlycolors,layout,spacing, andglobals. Colours expose exactly 12 roles: background, surface, surface_alt, foreground, muted, accent, on_accent, selected, border, success, warning, danger. Built-in palettes inpresets.zigandcolors/*.confcontain only those roles. Current/Bank are layout files; compact/standard/spacious are spacing files. - Universal Theming: Widget configs use
?Color = nullfor theme-mapped fields, resolved viaconfig.field orelse theme.colors.<role>in root_container. Widget overrides win; otherwise every visual value comes from the resolved four-layer theme or its derived helpers. - Config Normalization: On startup, apps parse and re-save configs (strips unknown fields, adds missing defaults). Bar menu, power, settings, and clipboard buttons are static presets; custom buttons use
otter-conf.Dynamicindexed fields (button_N_name,button_N_enabled,button_N_font_icon,button_N_command, etc.) and appear in layouts asbutton_<name>. Legacybutton_<name>_*fields are migrated to the indexed form. Optional fields (?Color,?u16) are NOT emitted. Only writes to~/.config/otter-shell/, never system configs. - Dock and Taskbar layer geometry:
otter-dockkeeps compact bounds at rest, expands layer bounds for the magnification animation, waits for the matching configure before drawing, and detaches blur/buffers during signal-aware shutdown.otter-taskbaruses a bottom-anchored surface with a visible exclusive bar zone plus reserved overlay space; opt-in auto-hide releases that zone only when hidden and retains a two-pixel reveal target. Start, calendar, previews, and tray menus share the reserved Surface Description surface, with previews anchored to their app button and tray coordinates translated from the output origin. - Clipboard / DnD (otter-wayland):
Clipboardstruct wrapswl_data_devicefor paste and drag-and-drop support.getText()creates a pipe, callsoffer.receive("text/plain;charset=utf-8", fd), polls with timeout, reads result. DnD acceptstext/uri-listand text MIME offers, converts file URIs to shell-escaped paths, and invokes the registered drop callback. Used by otter-settings for Ctrl+V and otter-term for drop-to-paste. - Clipboard manager ownership (otter-clip): The daemon caches every advertised MIME representation byte-for-byte in source order, including GNOME/Nemo copied-file and KDE cut/URI metadata. Takeover after dispatch is allowed only for a complete supported selection; rejected formats (including portal handles), failed/truncated reads, and format/byte limits cancel capture and leave the original owner in place. Replay eligibility is separate from text/image history previews. Reordering formats changes toolkit negotiation; never prioritize binary or text formats. A newer selection in that dispatch cancels the pending takeover. Ext data-control is preferred with wlr data-control fallback. The built-in
button_clipbar button opens the launcher-backed history popup. - ConfigDoc (otter-settings): Preserves comments, field order, and custom fields through edits.
serialize()writes directly to disk without round-tripping through typed parser.removeFieldsWithPrefix()for bulk field deletion,removeFromLayouts()for layout cleanup (trims spaced names so widgets such asdndreturn to the General layout + picker).tabs.zigregisters one sidebar tab per app config file (includingotter-dock.confandotter-taskbar.conf). Bar layout chips list builtins fromeditor_widgets.available_widgets(dnd,nightlightincluded). Wallpaper path rows use the XDG portal directory chooser for global and per-display folders. Editable form values register exact text hit regions; active single-line editors keep fixed control bounds and horizontal-only padding so selection does not move or clip text. - Theme Loading (otter-theme):
theme.confparses onlyTheme.Globals, includingcolors_path,layout_path,spacing_path,icon_theme,icon_coloring,bar_islands,opacity, andblur; the loader composes the three layer files into oneTheme. First load migrates a legacy import graph totheme.conf.legacyplus three custom layers, writing the new root last. Four-layer themes withbar_islandsin the selected layout remain compatible until the setting is saved at the root. Normal reloads are read-only.ThemeWatcherwatches root and all three selected layers, including files created after startup; invalid reloads retain the active theme. - Sized Image Decode (zigimg/otter-render):
Image.loadFromFileSampleduses bounded JPEG/PNG statistical sampling for theme extraction. Render-facingloadFromFileAtSize/loadFromMemoryAtSizepreserve aspect ratio and use alpha-aware box filtering for every registered format, decoding the source once without the former second full-size RGBA staging copy. NanoSVG resolvescurrentColorwhile parsing instead of copying and rewriting SVG text. - Hot-Reload:
otter-conf.Watcher.watchConfigPathwatches the parent directory (atomic rename from otter-settings does not kill the watch). Event loops calldrainChanged()then reload once.IN_IGNOREDis not a change, so watch refresh cannot loop. Theme usesThemeWatcherwith the same drain. Bar, notifications, wallpaper, OSD, and idle poll watcher FDs for app config and theme. - Wallpaper State File:
otter-wallpaperwrites$XDG_RUNTIME_DIR/otter-shell/wallpaper-state(legacy/tmp/{uid}/otter-wallpaper-statestill read when present) withoutput_name=wallpaper_pathlines on every wallpaper change and removes it on normal exit or catchable shutdown signals. Path construction lives inotter-utils/src/runtime_state.zig. Used by lockscreen/greeter/shot/theme-gen to display or consume matching wallpapers while wallpaper is running.same_on_all_displaysdefaults to true so all non-override outputs show the same image. - Greeter compositor session:
otter-greeterlaunches its compositor through a dedicated root supervisor that owns thegreeterPAM/logind session, then forks the unprivileged compositor inside that session withLIBSEAT_BACKEND=logind. The daemon watches both the greeter IPC socket and supervisor PID so compositor failures before UI connection are reaped and retried. - Theme Gen Output Policy:
primary_outputselects which output wallpaper drives the single global generated colour layer. Empty/missing selections fall back to the lexicographically first output name, independent of compositor enumeration order. Per-output wallpaper rendering stays inotter-wallpaper; one global theme cannot carry simultaneous per-output palettes. Wallpaper-derived and manually selected palettes both map to the same 12 roles and drive every enabled external-app template. - Wallpaper SHM and catalog lifetime: Each wallpaper surface keeps one single-buffer SHM pool in steady state. Never force-release or rewrite a compositor-owned buffer: allocate a replacement, retire the old pool until
wl_buffer.release, then reclaim it after dispatch. Folder image lists store compact path references into one byte blob rather than fixed 4 KiB path slots. - Text file application matching (otter-desktop): Native XDG application enumeration and explicit launch share MIME matching.
text/plaineditors accepttext/*source files, including Lua and C; binary types remain excluded. - D-Bus (otter-desktop): UPower, PowerProfiles, MPRIS, SystemTray, NetworkManager, SCX Loader, Screensaver, ScreenSaverService, Logind, Notifications, Polkit Agent, Fprintd, switcheroo-control. All use sd-bus (vendored basu). Important:
sd_bus_process()handles one message per call — must loop to drain. - Portal account, secret, and lockdown: Account disclosure uses the shared native consent process and retains the displayed reply until approval. Secret derives per-app 32-byte values from a private atomically published master key under the XDG data directory; invalid storage never triggers key replacement. Lockdown combines user configuration with administrator constraints and emits property changes after edits; user values cannot clear administrator restrictions.
- XDG Desktop Portal (
xdg-desktop-portal-otter): Otter-only backend forxdg-desktop-portal. It advertises all 21 impl interfaces inotter.portalso no foreign backend can claim them, validates every call against its contract, and answers only what Otter owns: native Settings reads and watches the sharedotter-themegraph; native Wallpaper callsotter-wallpaper --set, so the daemon's watched config remains the only wallpaper state writer and lockscreen-only requests return failure; representable FileChooserOpenFile/SaveFile/SaveFilesrequests use theotter-filesOTFP v1 socket bridge with one bounded connect, one coalesced spawn, and one retry; Notification maps toorg.freedesktop.Notificationsdirectly; Screenshot delegates capture tootter-screenshotand PickColor tootter-pick; idle Inhibit maps toorg.freedesktop.ScreenSaver; basic Access grant/deny prompts use the native portal dialog; ScreenCast CreateSession/SelectSources/Start selects monitors or windows through thumbnail cards and exports real frames through PipeWire after consent (synthetic frames exist only in the integration build); RemoteDesktop CreateSession/SelectDevices/Start/ConnectToEIS uses system libeis with virtual-pointer/keyboard inject and Notify* until EIS connects. Additional native handlers cover Access, Account, AppChooser, Background, Clipboard, DynamicLauncher, Email, GlobalShortcuts, InputCapture, Lockdown, Print, Secret, and Usb. FileChooser supports bulk-save names and choices; notifications forward actions/icons; Inhibit supports sleep and session monitoring; Screenshot supports regions and PickColor; Wallpaper provides preview; combined RemoteDesktop/ScreenCast sessions share stream mapping and teardown. Unsupported platform capabilities and unrepresentable requests fail closed withorg.freedesktop.portal.Error.Failed; nothing forwards to GTK, GNOME, KDE, Hyprland, or keyring portals. Request and Session handles are caller-scoped and cancelled onClose, caller disconnect, or frontend disconnect. - Portal window capture: Detect EXT toplevel capture, Hyprland toplevel export with mapped WLR handles, or the Mutter-compatible ScreenCast D-Bus service (also provided by Niri). Never match windows by title or crop monitors to imitate window capture. Preserve stable identities/provider ownership across consent, renegotiate window sizes, and close sessions when their source disappears. Native thumbnails load on a worker; temporary preview streams stop after capture. Combined remote-desktop sessions remain monitor-only. CUPS is optional at runtime and
Suggestsin Zenith; missing print tools must not stop the portal or client-rendered print-to-file. - Portal compositor boundary: Detect Wayland protocol capabilities; never select a compositor by name, call compositor CLI IPC, or edit its bindings/configuration. The native Mutter-compatible ScreenCast service is a capture provider, not another portal backend. Optional vendor protocols must not prevent other portal interfaces from running. GlobalShortcuts registers actions and saves preferred triggers only; actual key assignment remains desktop-owned. InputCapture advertises zero capabilities when its protocol or output geometry is absent. RemoteDesktop Start requires native consent and enforces the approved device mask.
- Portal installation and activation:
xdg-desktop-portal-ottergenerates D-Bus/systemd executable paths from--prefixwithoutDESTDIRand keeps build-cache library paths out of the installed executable. Zenith packages the backend, activation files, and Otter selection with frontend/helper dependencies. Static user services activate through D-Bus; non-Otter sessions need an explicit desktop-specific~/.config/xdg-desktop-portal/*-portals.confpreference and the compositor must import its Wayland environment.sh tests/run-install-check.shverifies/usrand/usr/localstaging. The package includesotter-portal-dialog;tests/test_screencast_live.pyverifies real frames through the public portal FD. Theme layer loads opt intootter-confstrict custom-value parsing to retain active colours on invalid reloads. RemoteDesktop remains incomplete (consent, keymap, geometry, and combined capture); CreateSession or an EIS FD is not proof of working input. See the portal audit for incomplete native methods and remaining stubs. - Pika tiling desktop providers: Hyprland, Niri, and Mango select Otter apps and portal through desktop-specific defaults. Never use package-wide conflicts to remove other desktop environments or their apps during upgrades.
pika-shell-profile-common >= 0.1.4permits coexistence and avoids global XDG defaults. Legacy-app cleanup is confined to fresh Full ISO construction. Native onboarding helpers use private paths so GTK counterparts can remain installed. - Plugin desktop services:
otter-barandotter-launcherdynamically linklibotter_desktop.so.0; plugins call its stable C service ABI instead of linking systemd or embedding BASU. - XDG semantic icons:
otter-ui/src/icons.zigmaps every bundled semantic icon to an XDG icon name. Selected themes use-symbolicvariants with theme colouring or regular variants without it; app/content surfaces request regular variants first, with tinted symbolic fallback. Missing theme icons fall back to bundled SVGs.sd_bar_itemresolves arbitrary names through the same selected XDG theme. Icon-only buttons omit text gap so 20 px plugin slots remain centered and visible. - Icon Lookup (otter-desktop): Scans actual directories (preferred themes first, then all themes, pixmaps fallback, desktop file fallback). SVG preferred over raster. Works with any theme layout.
- Global Shared Services (otter-bar): SysInfo and PipeWire are lazily heap-allocated singletons. Clock timezone loaded once via zeit with global minute tracking.
| Purpose | Location |
|---|---|
| Config parser | otter-conf/src/parser.zig |
| Config imports / dynamic fields | otter-conf/src/import.zig, otter-conf/src/dynamic.zig |
| Theme definitions | otter-theme/src/theme.zig |
| Theme presets | otter-theme/src/presets.zig |
| Bar widget shell | otter-ui/src/widget.zig |
| Surface Description frame | otter-ui/src/ui_frame.zig |
| Drawing primitives | otter-ui/src/drawing.zig (barrel) + otter-ui/src/drawing/*.zig |
| Input buffer | otter-ui/src/input_buffer.zig |
Global std.Io singleton |
otter-utils/src/io.zig |
| Runtime state paths | otter-utils/src/runtime_state.zig |
| Command list | otter-render/src/command_list.zig |
| Terminal rendering primitives | otter-vte/src/command_list.zig, otter-vte/src/cell_renderer.zig, otter-vte/src/cell_glyphs.zig, otter-vte/src/ops.zig |
| Quad renderer | otter-render/src/quad_renderer.zig |
| Sprite sheets | otter-render/src/sprite.zig |
| Image loading and region drawing | otter-render/src/image.zig |
| SDF rendering | otter-render/src/sdf.zig |
| Font rendering | otter-render/src/font.zig |
| Text System boundary | otter-render/src/text.zig, otter-render/src/fontconfig.h |
| Wayland connection | otter-wayland/src/connection.zig |
| Wayland protocol XML | otter-wayland/protocols/ (stable/, staging/, unstable/, wlr/, plus dwl-ipc, otter-tag, otter-river) |
| Capture selection overlay | otter-wayland/src/selection_overlay.zig (shared by otter-screenshot, otter-shot, otter-rec, otter-clicker) |
| Hyprland handle → address map | otter-wayland/src/toplevel_map.zig |
| Compositor blur regions | otter-wayland/src/background_effect.zig |
| Keyboard handler | otter-wayland/src/keyboard.zig |
| Clipboard | otter-wayland/src/clipboard.zig |
| switcheroo-control GPU list | otter-desktop/src/switcheroo.zig |
| Bar main event loop | otter-bar/src/main.zig |
| Bar SD layout engine | otter-bar/src/components/bar_layout_engine.zig |
| Bar input dispatch / popup input | otter-bar/src/components/bar_input_dispatch.zig, otter-bar/src/components/bar_popup_input.zig |
| Bar config schema | otter-bar/src/config.zig |
| Dock entry point | otter-dock/src/main.zig |
| Dock config | otter-dock/src/config.zig |
| Dock state manager | otter-dock/src/dock.zig |
| Dock rendering | otter-dock/src/draw.zig |
| Dock magnification | otter-dock/src/magnification.zig |
| Dock layer surfaces | otter-dock/src/shell.zig |
| Dock window list sync | otter-dock/src/dock_sync.zig |
| Dock hover thumbnails (spec) | otter-dock/src/thumbnail.zig + ext_image_copy_capture / ext_foreign_toplevel_list (see docs/superpowers/specs/2026-03-30-otter-dock-design.md) |
| Dock theme roles | otter-theme/src/theme.zig (Theme.colors, Theme.layout, Theme.spacing, and derived dockBackground/dockBorder) |
| Taskbar entry point / layer surfaces / rendering | otter-taskbar/src/main.zig, otter-taskbar/src/shell.zig, otter-taskbar/src/draw.zig |
| Taskbar state and layout | otter-taskbar/src/state.zig, otter-taskbar/src/layout.zig |
| Taskbar Start panel | otter-taskbar/src/start_menu.zig |
| Taskbar hover previews | otter-taskbar/src/preview.zig + ext_image_copy_capture / ext_foreign_toplevel_list |
| Taskbar config | otter-config-types/src/taskbar.zig |
| Taskbar settings | otter-settings/src/tabs.zig, otter-settings/src/editor.zig |
| Terminal app | otter-term/build.zig (Zig 0.16 app linking installed libghostty-vt; Otter owns Wayland/PTY/render frontend) |
| Terminal scrollback compression | otter-term/src/app/compression.zig, otter-term/src/terminal/core.zig |
| File manager app / UI | otter-files/src/main.zig, otter-files/src/shell.zig, otter-files/src/model.zig |
| XDG desktop portal backend | xdg-desktop-portal-otter/src/main.zig, xdg-desktop-portal-otter/src/broker.zig, xdg-desktop-portal-otter/src/native.zig, xdg-desktop-portal-otter/src/native_settings.zig, xdg-desktop-portal-otter/src/native_wallpaper.zig, xdg-desktop-portal-otter/src/native_notification.zig, xdg-desktop-portal-otter/src/native_screenshot.zig, xdg-desktop-portal-otter/src/native_inhibit.zig, xdg-desktop-portal-otter/src/native_screencast.zig, xdg-desktop-portal-otter/src/screencast_feed.zig, xdg-desktop-portal-otter/src/native_remotedesktop.zig, xdg-desktop-portal-otter/src/eis_server.zig, xdg-desktop-portal-otter/src/picker.zig |
| Portal contracts, ownership, and routes | xdg-desktop-portal-otter/src/contracts.zig, xdg-desktop-portal-otter/src/ownership.zig, xdg-desktop-portal-otter/src/routes.zig |
| Portal integration tests and packaging | xdg-desktop-portal-otter/tests/, xdg-desktop-portal-otter/data/ |
| Transcribe daemon and CLI | otter-transcribe/src/main.zig, otter-transcribe/src/ctl.zig, otter-transcribe/src/config.zig, otter-transcribe/src/indicator.zig |
| Transcribe config types | otter-config-types/src/transcribe.zig |
| Launcher Surface Description migration | otter-launcher/src/draw.zig (LauncherUiState, UniformList, hit registry), otter-launcher/src/extensions.zig (fixed-buffer calc/emoji/clip rows backed by otter-tools-core and bounded cache reads). Right-click PopupMenu for .desktop Actions and switcheroo GPU pick. |
| Jade entry point | otter-jade/src/main.zig |
| Hypr layout+titlebar entry | otter-hypr/src/main.zig, lua/otter_hypr_layout/init.lua |
| Jade config/state/simulation | otter-jade/src/config.zig, otter-jade/src/state.zig, otter-jade/src/sim.zig |
| Jade drawing/input/sprites | otter-jade/src/draw.zig, otter-jade/src/input.zig, otter-jade/src/sprites.zig |
| WLR foreign toplevel | otter-wayland/src/protocols/foreign_toplevel.zig |
| Compositor-specific IPC | otter-wayland/src/protocols/hyprland.zig, niri.zig, dwl_ipc.zig, otter_tag.zig, ext_workspace.zig |
| Dock config types | otter-config-types/src/dock.zig |
| Widget container | otter-bar/src/components/root_container.zig |
| Theme loader | otter-theme/src/loader.zig |
| Settings entry point | otter-settings/src/main.zig |
| Assistant app entry | otter-assistant/src/main.zig |
| Assistant daemon | otter-assist/src/main.zig |
| Search daemon | otter-search/src/main.zig |
| Screenshot tool | otter-screenshot/src/main.zig |
| Screenshot annotation overlay | otter-screenshot/src/annotator.zig, otter-render/src/annotation.zig, otter-tools-core/src/ocr.zig |
| System monitor | otter-monitor/src/main.zig |
| Package manager app / helper | otter-pkg/src/main.zig, otter-pkg/src/app.zig, otter-pkg/src/helper.zig |
| Package manager model / UI | otter-pkg/src/backend.zig, otter-pkg/src/state.zig, otter-pkg/src/draw.zig |
| Otter HUD / Otter Bench suite | otter-bench/ (layers/, libs/, apps/; see otter-bench/CLAUDE.md) |
| Settings app struct | otter-settings/src/app.zig |
| Settings input handlers | otter-settings/src/input_handlers.zig |
| Settings config editor | otter-settings/src/editor.zig |
| Settings draw context | otter-settings/src/draw.zig |
| Settings form rendering | otter-settings/src/draw_form.zig |
| Settings chip rendering | otter-settings/src/draw_chips.zig |
| Settings theme browser | otter-settings/src/theme_browser.zig |
| Settings theme fields | otter-settings/src/theme_fields.zig |
| Theme Gen daemon / one-shot CLI | otter-theme-gen/src/main.zig, otter-theme-gen/src/cli.zig |
| Theme Gen config | otter-theme-gen/src/config.zig |
| Theme Gen palette | otter-theme-gen/src/palette.zig (re-export of otter-tools-core/src/theme_palette.zig) |
| Theme Gen template | otter-theme-gen/src/template.zig |
| Theme Gen SplatHash | otter-theme-gen/src/splathash.zig (re-export of otter-tools-core/src/splathash.zig) |
| Small tools shared core | otter-tools-core/src/root.zig, otter-tools-core/src/clip_history.zig, otter-tools-core/src/record.zig, otter-tools-core/src/color.zig, otter-tools-core/src/splathash.zig, otter-tools-core/src/theme_palette.zig, otter-tools-core/src/calendar.zig, otter-tools-core/src/emoji.zig, otter-tools-core/src/duration.zig, otter-tools-core/src/weather.zig, otter-tools-core/src/calc.zig, otter-tools-core/src/assist_client.zig, otter-tools-core/src/knowledge.zig, otter-tools-core/src/web_search.zig |
| Clipboard manager | otter-clip/src/main.zig, otter-clip/src/copy.zig, otter-clip/src/paste.zig |
| Screen recorder | otter-rec/src/main.zig, otter-rec/src/portal_recorder.zig (portal PipeWire stream capture) |
| Color picker | otter-pick/src/main.zig |
| Calendar popover | otter-cal/src/main.zig |
| Emoji picker | otter-emoji/src/main.zig |
| Timer | otter-timer/src/main.zig |
| Sticky notes | otter-note/src/main.zig |
| Weather helper | otter-weather/src/main.zig |
| Weather bar widget | otter-ui/src/widgets/weather.zig |
| DND bar widget | otter-ui/src/widgets/dnd.zig, otter-ui/src/widgets/specs/dnd.zig |
| Calculator | otter-calc/src/main.zig |
| Voice TTS CLI | otter-vox/src/main.zig, otter-vox/src/runtime.zig, otter-vox/src/tokenizer.zig, otter-vox/src/audio8_bridge.cpp (Audio8 Q5_0/FP16 GGUF, shipped pre-cloned voice codes, selectable Vulkan GPU with CPU fallback, chunked PipeWire playback) |
| Auto clicker | otter-clicker/src/main.zig, otter-clicker/src/virtual_pointer.zig, otter-clicker/src/uinput.zig (shared point overlay + Wayland virtual-pointer injection with uinput fallback) |
| PipeWire one-shot/streaming playback | otter-desktop/src/pipewire_playback.zig |
| Interaction cues | otter-cue/src/root.zig, otter-cue/src/recipe.zig, otter-cue/src/engine.zig, otter-cue/src/bind.zig, otter-cue/src/playback.zig |
| Config type definitions | otter-config-types/src/root.zig |
| Polkit agent entry | otter-polkit/src/main.zig |
| Overview / switcher overlay | otter-overview/src/main.zig, otter-overview/src/layout.zig, otter-overview/src/hypr.zig, otter-overview/src/sway.zig, otter-wayland/src/protocols/niri.zig (otter-switcher is the second installed executable and limits Alt-Tab to the active workspace; mouse hover selects, click activates, and wheel cycles. Sway uses native swaymsg tree/focus IPC. Hypr 0.56 export skips windows that miss the monitor; overflow columns use a temporary scrolling-tape nudge, not a window swap; bottom strip click-scrolls the matching workspace card and paints mini window tiles, centered on the card strip; Ctrl+1..9 / Ctrl+0 jump the same indices without feeding digits into search). Mapped overlay 1:1 3-finger swipe via zwp_pointer_gestures_v1; touchpad workspace scroll snaps on axis stop. Optional Hyprland 0.55+ lua otter_hypr_layout.overview_gesture.install() is compositor bind for opening from the desktop and accepts open_direction / close_direction options |
| Keybind help overlay | otter-keybindhelp/src/main.zig, otter-keybindhelp/src/parser.zig, otter-keybindhelp/src/normalize.zig, otter-keybindhelp/src/draw.zig, otter-keybindhelp/src/shell.zig |
| Lock entry point | otter-lock/src/main.zig |
| Lock config | otter-lock/src/config.zig |
| Lock drawing | otter-lock/src/draw.zig |
| Greeter daemon | otter-greeter/src/main_daemon.zig, otter-greeter/src/daemon.zig, otter-greeter/src/seat.zig |
| Greeter UI | otter-greeter/src/main_ui.zig, otter-greeter/src/ui_app.zig, otter-greeter/src/ui_draw.zig |
| Greeter IPC | otter-greeter/src/ipc.zig |
| Idle entry point | otter-idle/src/main.zig |
| Idle config | otter-idle/src/config.zig |
| Idle DPMS control | otter-idle/src/dpms.zig |
| Night Light daemon | otter-nightlight/src/main.zig |
| Gamma control | otter-wayland/src/gamma.zig |
| Shot app | otter-shot/src/main.zig, otter-shot/src/app.zig, otter-shot/src/state.zig |
| Shot composition/export | otter-shot/src/compose/shot_composer.zig, otter-shot/src/compose/screenshot_gradients.zig, otter-shot/src/export/file.zig, otter-shot/src/export/clipboard.zig |
| Shot capture/UI | otter-shot/src/capture/workflow.zig, otter-wayland/src/selection_overlay.zig, otter-shot/src/ui/draw.zig, otter-shot/src/ui/inspector.zig |
| Shot annotation GUI | otter-shot/src/annotation_editor.zig, otter-shot/src/ui/annotation_toolbar.zig |
| Logind D-Bus client | otter-desktop/src/logind.zig |
| ScreenSaver service | otter-desktop/src/screensaver_service.zig |
| Polkit D-Bus agent | otter-desktop/src/polkit_agent.zig |
| PAM authentication | otter-desktop/src/pam.zig |
| Fprintd D-Bus client | otter-desktop/src/fprintd.zig |
Each app follows the pattern: <app>/src/main.zig (entry), config.zig (schema), draw.zig (renderer).
- basu (
otter-desktop/vendor/basu/): Standalone sd-bus from systemd. No runtime libsystemd dependency. - PipeWire (
otter-desktop/vendor/pipewire/, fromallyourcodebase/pipewire): Zig bindings with dlopen shims. Statically linked. - itijah (
otter-render/vendor/itijah/): Zig-native Unicode Bidirectional Algorithm, vendored from latest main with a local Zig 0.16 build wrapper. - ggml (
otter-vox/third_party/ggml/): MIT C/C++ runtime boundary for Audio8 inference; Vulkan is preferred with native CPU fallback.
Flat key-value format with # comments. Nested struct flattening: clock_text_color maps to config.clock.text_color. All config struct fields must have defaults. Supported types: booleans, integers, floats, strings, enums, arrays, slices, optionals, custom parse/toStr types, nested structs. Unknown fields silently skipped. Dynamic(T, prefix) indexed entries are capped at indices 0..4095 (parser_support.max_dynamic_entries); import nesting is capped at depth 128 (import.max_import_depth) and total merged output is capped by LoadOptions.max_file_size.
Widget configs use ?Color = null for theme-mapped fields. Resolved at widget init: config.field orelse theme.token. Serializer omits null fields, keeping configs minimal.
Config: ~/.config/otter-shell/theme.conf (fallback: /etc/otter-shell/, then compiled-in defaults). Root contains only colors_path, layout_path, spacing_path, font_family, font_size, icon_theme, opacity, and blur. Selected files use flat keys matching Theme.Colors, Theme.Layout, or Theme.Spacing; no prefixed duplicate roles exist.
Theme resolution composes one selected Colours file, one Layout file, one Spacing file, and root Globals. Apps consume semantic roles and derived helpers; they do not branch on file names. Settings writes custom layers independently and exposes four matching editor tabs. Theme Gen writes only generated-colors.conf. Colour files cannot change structure, spacing, typography, icons, opacity, or blur.
Current is a dark continuous-plane system with one 4 px radius on standalone app controls, rows, panels, popups, decorations, and dock slots; only true joined edges are square. Its compact Bar has a feature/anatomy compatibility contract, not a pixel freeze: visual tokens may change its palette/radii, but preserve the rounded active workspace chip, real SVG/font icons, live widget content, dense packing, centered title, and lack of invented workspace/title indicators. Global bar_islands makes the Bar rail transparent and gives each left/center/right section its own surface; when global blur is enabled, the blur region must be clipped to those section surfaces so gaps remain clear. Current's 2 px line is reserved for focus, status, and progress surfaces. Bank is warm mineral and all-dark: opaque dark planes, with square markers and short baselines reserved for status, workspace, and progress; it must not introduce light panels, split light/dark surfaces, or required blur. Generic navigation/list selection uses tonal fill only in both profiles. Both profiles use CPU-renderable fills, lines, and rounded rectangles. A selected or focused control uses one thin outline/focus ring or one line/marker, never both.
Long-running apps use ThemeWatcher, watching theme.conf and all three selected paths, including a missing selected file created after startup. A layer parse/read failure retains the previous valid theme and logs the error; it must not publish partial/default state or rewrite files. Only one-shot legacy migration and explicit Settings/Theme Gen actions write theme files.
Path helpers honour XDG_CONFIG_HOME. Tests and visual captures set it to a temporary directory, so they neither read nor write the developer's session configuration. Never capture the real Wayland session: use isolated headless Sway/Grim or a direct-render fixture when a protocol is unavailable.
Shared fixture and comparison commands:
cd otter-ui
zig build -Doptimize=ReleaseFast visual-fixture -- --profile current --case default --scale 1 --out zig-out/visual/current-default@1x.png
zig build -Doptimize=ReleaseFast visual-compare -- --expected testdata/visual/current/default@1x.png --actual zig-out/visual/current-default@1x.png --diff zig-out/visual/current-default.diff.png
tools/headless-capture.sh --profile bank --out zig-out/visual/bank-headless.png -- <app> <args>ReleaseFast is required for build, test, fixture, compare, and benchmark evidence; Debug runs do not count.
Protocol XML lives under otter-wayland/protocols/:
- stable: xdg-shell, viewporter, tablet
- staging: alpha-modifier, content-type, cursor-shape, ext-background-effect, ext-foreign-toplevel-list, ext-idle-notify, ext-image-capture-source, ext-image-copy-capture, ext-session-lock, ext-workspace, single-pixel-buffer, tearing-control, xdg-activation, xdg-toplevel-tag
- unstable: idle-inhibit, linux-dmabuf, pointer-constraints, text-input, xdg-decoration, xdg-foreign, xdg-output
- wlr: layer-shell, foreign-toplevel-management, output-power-management, virtual-pointer, etc.
- compositor-specific:
dwl-ipc-unstable-v2,hyprland-toplevel-mapping-v1(ext/wlr handle → 64-bit Hypr address),otter-tag-v1, and theotter-river-*families (window/input/libinput/xkb management, layer shell)
BackgroundEffect owns compositor blur regions for translucent surfaces. XdgToplevel wrapper provides reusable toplevel window lifecycle with configure handling and HiDPI scale tracking.
Dynamic: Fontconfig, wayland-client, xkbcommon, PAM (optional), Vulkan loader (otter-vox). Zig-packaged/static: FreeType (allyourcodebase/freetype), HarfBuzz (allyourcodebase/harfbuzz). Vendored: basu (sd-bus), PipeWire, itijah, ggml. Pure Zig: zigimg (raster image formats), zeit (timezone/date).
All bar widgets embed a Widget shell for geometry/motion state and implement struct methods for draw/input. Key widgets:
- clock - Time display via zeit, left-click calendar popover (stays until click-off), global minute tracking
- workspaces - Workspace indicator/switcher via ext-workspace protocol
- battery - UPower D-Bus; click menu lists batteries plus power profiles; auto-hides when no battery
- brightness - Sysfs panel + keyboard LED + optional ddcutil DDC; click menu, scroll primary
- cpu_load/cpu_temp/memory - System metrics via SysInfo with threshold colors; cpu_temp click lists CPU/GPU hwmon sensors
- active_window - Focused window title via foreign_toplevel
- button - Clickable icon button with command execution
- power_profiles - Power profile selector via D-Bus
- network - NetworkManager D-Bus with WiFi, OpenVPN/WireGuard VPN, and Wi-Fi password prompt
- weather - Cached Open-Meteo current temperature with 7-day popup, popup refresh action, and optional public-IP location detection in
otter-weather - volume - PipeWire audio control with per-device popup
- falcond - Daemon status via inotify
- mpris - Media player via MPRIS D-Bus
- system_tray - StatusNotifierItem icons with animated expand/collapse, DBusMenu context menus
- dnd - Shared runtime Do Not Disturb state toggle with bell/bell-off SVG status icons
- nightlight - Night Light toggle (left-click
otter-nightlight; right-click temperature/schedule/suspend menu)
const log = otter_log.Scoped("component-name");
log.info("message", .{});
var counter = otter_log.Counter{}; // Performance counters
var reporter = otter_log.Reporter("otter-bar", 30).init(); // Periodic reporters- Daemons / long-running apps (bar, notifications, osd, wallpaper, polkit, settings, idle, overview):
std.heap.smp_allocatorin release builds,std.heap.DebugAllocatorin Debug (leak checking). Apps pick between them in a smallallocator()helper inmain.zig. - One-shot apps (launcher, logout):
ArenaAllocatorbacked bypage_allocatorfor bulk cleanup on exit. - BoundedArray for stack-allocated fixed-capacity collections throughout.
- IconImageCache:
IconImageCache(N)fixed-slot for bar,DynamicIconImageCachewith LRU eviction for launcher. Negative caching for missing icons. - Font glyph cache: Proactive culling at 8192 entries, reactive eviction on alloc failure. Fallback fonts from
/usr/share/fonts/otter-shell/via mmap. otter-waylandcallsmalloc_trim(0)every ~300 dispatches to reclaim glibc heap from libwayland churn.
The repo is fully on Zig 0.16 — every build.zig.zon declares .minimum_zig_version = "0.16.0" and all 37 app entry points use the 0.16 main signature. The notes below describe the idioms the code actually uses; do not reintroduce the 0.15 forms.
Most blocking/system APIs take an std.Io as their first argument. main receives a std.process.Init which provides the io:
pub fn main(init: std.process.Init) !void {
const io = init.io;
otter_utils.io.install(io); // shared libraries read it back via otter_utils.io.get()
const args = try init.minimal.args.toSlice(init.arena.allocator());
// ... no std.os.argv / std.os.environ globals
}Library code in this repo does not thread io through its own signatures; it calls otter_utils.io.get(). Apps must call install() before any library I/O runs.
std.ArrayList is unmanaged: it does not store an allocator, and the allocator is passed to each method. Initialize with .empty:
var buffer: std.ArrayListUnmanaged(u8) = .empty;
defer buffer.deinit(allocator);
try buffer.append(allocator, byte);
const slice = try buffer.toOwnedSlice(allocator);As a struct field: items: std.ArrayListUnmanaged(Item) = .empty, with deinit(self.allocator) in the struct's deinit.
std.fs.Dir / std.fs.File are std.Io.Dir / std.Io.File, and all methods take io:
file.close(io);
const n = try file.readStreaming(io, buf);
const len = try file.length(io);Names: readStreaming (not read), writeStreaming (not write), readPositional (not pread), writePositional (not pwrite), length (not getEndPos), setLength (not setEndPos). Absolute-path helpers are on the type: std.Io.Dir.createFileAbsolute(io, path, .{}), std.Io.Dir.deleteFileAbsolute(io, path), std.Io.Dir.realPathFile.
GenericReader, AnyReader, FixedBufferStream, CountingReader, and Io.Writer.null_writer do not exist. Use the concrete std.Io.Writer / std.Io.Reader:
const stdout = io.terminal.out;
try std.Io.Writer.print(stdout, "hello", .{});Reading a whole file goes through a reader interface, e.g. reader.interface.allocRemaining(allocator, .limited(max_file_size)).
var child = try std.process.spawn(io, .{ .argv = argv, .stdin = .pipe });Also: std.process.replace(io, ...) for exec, std.process.openExecutable for self-exe.
| Old | Current |
|---|---|
std.Thread.Mutex |
std.Io.Mutex |
std.Thread.Condition |
std.Io.Condition |
std.Thread.ResetEvent |
std.Io.Event |
std.Thread.WaitGroup |
std.Io.Group |
std.Thread.Semaphore |
std.Io.Semaphore |
std.Thread.RwLock |
std.Io.RwLock |
std.Thread.Pool |
std.Io.Group |
All take io and support cancelation. std.Thread.Pool and std.once do not exist.
std.time.Instant, std.time.Timer, and std.time.timestamp() are gone. Use std.Io.Clock:
const now_ns = std.Io.Clock.awake.now(io).nanoseconds; // i96otter-ui/src/widget.zig wraps this in a small Timer struct that replaces the removed std.time.Timer.
std.heap.ArenaAllocatoris lock-free and thread-safe.std.heap.ThreadSafeAllocatordoes not exist — pick a lock-free allocator directly (std.heap.smp_allocator).std.heap.GeneralPurposeAllocatoris nowstd.heap.DebugAllocator.
io.random(&buf); // may use cached state
io.randomSecure(&buf); // always syscalls (for keys etc.)Use the targeted builtins — relevant to the comptime parser code:
const T = @Int(.unsigned, 10);Also: @Struct, @Union, @Enum, @Tuple, @Pointer, @Fn.
- Small integer types implicitly coerce to floats when all values fit.
@intFromFloatis deprecated —@floor/@ceil/@round/@truncreturn integers directly.
packed unionrequires an explicit backing integer:packed union(u16) { ... }.- No pointers in
packed struct/packed union. - All fields must fit the backing integer (no unused bits).
- Extern contexts require explicit backing types.
Round-trip through an array:
const info = @typeInfo(@TypeOf(vector)).vector;
const arr: [info.len]info.child = vector;
const val = arr[runtime_index];std.fmt.Alt(wasstd.fmt.Formatter)std.fmt.Options(wasstd.fmt.FormatOptions)std.fmt.bufPrintSentinel(wasstd.fmt.bufPrintZ)
| Old | Current |
|---|---|
error.RenameAcrossMountPoints |
error.CrossDevice |
error.NotSameFileSystem |
error.CrossDevice |
error.SharingViolation |
error.FileBusy |
error.EnvironmentVariableNotFound |
error.EnvironmentVariableMissing |
C translation lives in the build system. Eleven packages wire a translate-c step: otter-bench, otter-clicker, otter-desktop, otter-greeter, otter-rec, otter-render, otter-term, otter-theme-gen, otter-utils, otter-vox, otter-wayland.
The only remaining @cImport call sites are in vendored third-party code (otter-desktop/vendor/pipewire/, otter-render/vendor/itijah/src/test/diff_oracle.zig) and in zango, which is a separate compositor project. New first-party code must not use @cImport.
Write a real .h file and wire the step in build.zig:
// src/c.h
#include <xkbcommon/xkbcommon.h>// build.zig
const translate_c = b.addTranslateC(.{
.root_source_file = b.path("src/c.h"),
.target = target,
.optimize = optimize,
});
translate_c.linkSystemLibrary("xkbcommon", .{});
const exe = b.addExecutable(.{
.name = "otter-foo",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "c", .module = translate_c.createModule() },
},
}),
});
// callsite
const c = @import("c");For advanced knobs, depend on the translate-c package explicitly.
std.SegmentedList— no replacement; build our own if needed.fs.realpathZ/.Wvariants →std.Io.Dir.realPathFile.- Windows
DynLib→ callLoadLibraryExWdirectly. builtin.subsystem→zig.Subsystem.
otter-installer, otter-first-setup, and otter-welcome are packaged by otter-zenith from explicit source refs. Installer automatic partitioning must always receive an explicitly selected drive and reject mounted/read-only disks; never restore implicit first-disk selection. The installer uses native catalog selectors and always defers account creation to First Setup. Running progress cannot be dismissed or marked complete before the helper exits successfully. images/pika-iso builds Minimal and Full through a manual Gitea workflow using Cockatiel packages. Successful main builds call release.sh to publish each ISO with matching basename-relative .md5 and .sha256 files to iso.pika-os.com, then retain downloadable Gitea artifacts. CDN publishing failures must fail the build.
Minimal ISO media uses explicit runtime packages, kernel-pika-runtime, and linux-firmware-installer, with nmtui for optional online installation. Keep PC network/storage firmware, omit GPU/audio/ARM SoC and switch/SmartNIC bundles, and boot its text console through the EFI framebuffer (nomodeset). Do not pull kernel headers/toolchains, Plymouth, or locales-all onto text media; generate the chosen locale during installation. Full desktop dependencies replace the installer firmware subset with linux-firmware. pikainstall schema 1 defaults modules_online to false; Full reads a mounted local package repository and may satisfy dependencies from packages already installed in the live filesystem. Pool pruning must match installed status, package name, version, and architecture exactly, after live cleanup. Size gates and a local-only module resolver check run before publishing ISO artifacts.
Pika installer frontends share native installation stages, Zig catalogs, and disk validation/preparation in pikainstall >= 3.5.0; the backend holds /run/pikainstall-install.lock across preparation and apply. Minimal uses a Zig-driven newt wizard with system locale/XKB/timezone catalogs and hidden passwords. --dry must never prepare or apply. pika-baseos-minimal and otter-greeter require login; both ISO recipes verify the live user PAM stack, including greeter autologin on Full, before image creation.
Pika keyboard settings use one canonical /etc/vconsole.conf containing both KEYMAP and the XKB fields. /usr/lib/tmpfiles.d/debian.conf forcibly recreates /etc/default/keyboard as an alias to it at boot; never split the settings between separate files. Verify selected keyboard settings after an installed-system reboot.
Installer raw Linux syscalls use std.os.linux.errno, including nonblocking progress reads; std.posix.errno follows libc when linked and cannot decode raw negative syscall returns. Live image detection uses /live/filesystem.squashfs; pool discovery supports /pool, /cdrom/pool, and /run/live/medium/pool. Factory Otter theme roots select generated colours plus installed layout/spacing files, and Hyprland ships a seed otter-theme.lua so the generated file is watched from first login.
Installer automatic storage defaults to XFS; ext4/Btrfs, encrypted root/home, home subvolumes, swap sizing, and rEFInd options share guarded backend validation. Manual assignments never format and must validate root, boot, and EFI before mounting. Finalization regenerates the initramfs after persistent storage configuration. Both backend and TUI builds explicitly target x86-64-v3. Native selector rendering must supply the frame allocator for shared chevron icons and deinitialize UI state caches.
First Setup and Welcome import otter-pkg's onboarding module for native APT previews, helper transactions, and driver selection. Keep one libapt context per process; close caches and transfer context ownership between serialized workers. Root account/completion operations run off the Wayland thread. pikainstall complete-task persists independent flags, with identity completion preserved when optional tasks are saved. Checkboxes select available CFHDB package profiles; installed profiles are read-only, and package scripts never come from profile data.
- Welcome shell content (
otter-welcome): Show shell docs, tips, and switching guidance only in Hyprland, Mango, or Niri sessions. Tips use a dismissible modal with keyboard navigation. Shell switching opens the guide; do not launchpika-shell-applyas a chooser because it only copies an already-installed profile. Packaging requirespika-shell-profile-commonplus an alternative dependency on the mutually exclusive Otter/Noctalia/Dank profiles, defaulting to Otter.
Live ISO graphics selection belongs to pika-iso and pika-live-booster-hooks: the standard Full entry permits Nouveau; RTX mounts a complete NVIDIA driver layer before switching root and blocks competing modules. Build modules for the exact live kernel, verify GSP firmware and runtime libraries, and wait for the selected driver before starting the greeter. Cached debs are not proof of a usable live driver. Fallback keeps nomodeset and blocks both drivers.
Native Pika installation must not hand schema-based apply requests to the legacy Python/host/chroot installer. Clear old filesystem signatures on automatically erased partitions, refresh device metadata, and mount known formats explicitly. Manual assignments preserve existing home and unrelated EFI files. Keep live, deferred first-boot, OEM, and base-only autologin intact. Suppress console diagnostics for the entire TUI while retaining kernel logs; remove live-only services and installer autostart from installed systems. Full includes Blivet for partition editing and a launcher entry for Otter Logout. Both ISO recipes include the official Ventoy runtime helper and compatibility marker so selected renamed/nested images resolve without separate retry delays.
Minimal must support offline base installation and carry full kernel-pika with matching headers/toolchain and systemd-cryptsetup. Desktop or NVIDIA selections add full linux-firmware; manually retain these metapackages for upgrades. Configure headers/toolchains before NVIDIA and verify modules for every installed kernel. Default installed boot entries retain modesetting; only explicit safe graphics uses nomodeset. Remove live-only service masks from installed systems.
TUI-created desktop users use normal login and omit the First Setup package; purge inherited live-image copies of First Setup. Welcome remains installed in every desktop mode. Deferred first-boot and OEM desktop modes retain First Setup. Preserve autologin for deferred first-boot identity, OEM, and base-only installs. Retired live accounts must lose admin groups and interactive login. Deferred accounts receive missing desktop defaults from /etc/skel; installed DNS uses NetworkManager’s runtime resolver, never the build-container resolver. Niri relies on XDG onboarding autostarts; Hyprland retains a guarded explicit onboarding sequence that launches Welcome even when First Setup is absent. Neither auto-opens the package manager; First Setup holds one per-user runtime lock. Polkit prefers the logged-in user only among identities offered by policy, keeping the displayed and authenticated identity identical.
-
Launcher recents:
recent_appsdefaults to false. When enabled, successful launcher requests persist up to 32 desktop IDs in the XDG cache and show them before alphabetical entries. Search ordering stays independent. -
Settings theme edits: Theme fields use the shared input buffer for selection and word deletion. String edits retain independent owned storage. Explicit layer paths survive Apply; selecting a preset or editing layer values replaces the corresponding path. Custom palette files serialize
Theme.Colors, matching their loader. Theme layer paths support~/expansion. -
Files mouse history: Side/extra and dedicated Back/Forward mouse buttons use the existing model history on press; modal input remains handled first.
-
Notes startup: Flush Wayland requests before the blocking poll, including the first buffer commit.
otter-note/tests/startup.pychecks mapping without input in a Hyprland VM. -
Application shortcut search: Shared application-name matching accepts case-insensitive ordered letter subsequences; desktop IDs and keywords retain substring matching.
Minimal TUI manual partitioning uses the shared safe drive catalog and GNU parted. Partition edits are immediate; selected filesystem formats occur only after final confirmation and validation. Omitted manual format fields preserve filesystems and legacy resume hashes. Keep existing EFI/home files by default. Both installer completion screens report elapsed time; TUI selects Reboot by default.
- XDG application discovery (otter-desktop): Empty XDG data/config variables use their standard defaults, including
/usr/local/share:/usr/share. Non-emptyXDG_DATA_DIRSremains authoritative.PathListowns appended path text but contains internal slices, so fill it in caller-owned storage rather than returning it by value.
Shared Pika desktop assets require pika-wallpapers, which supplies the wallpaper directory alias used by desktop defaults. Desktop settings postinst migrations must never leave background processes inside installer chroots; enable their existing boot service instead. Opening retained First Setup with no pending tasks shows a completed state, never another account-creation wizard.
-
File manager activation: Desktop-specific MIME files contain only Default Applications. Zenith ships
otter-files-session: when Otter is the selected directory handler, it installs a managed FileManager1 service in$XDG_RUNTIME_DIR/dbus-1/servicesand reloads D-Bus activation config. It preserves explicit user/runtime overrides and never rewrites user MIME preferences. XDG autostart and Otter compositor profile fragments invoke it at login. -
Package review and updates (otter-pkg): Cache architecture-aware hold confirmation counts per preview, never scan the catalog per draw. The Recommended tab compares real plans with recommends enabled/disabled; APT caches that policy, so config changes require reopening the cache with the caller's original lock mode. Rebuild stale package refs afterward. Held lists include upgradable packages absent from selective plans; kept-back/phased entries never require permission to change holds. Resolve dependency names before freeing their owning detail page; unavailable targets preserve that page and its navigation trail. Unmark all clears persisted marks from browse and review. Review navigation uses native tabs with overflow arrows. Check & apply updates refreshes first and only auto-applies when no saved selections, kept-back packages, removals, downgrades, or hold/essential confirmations require review.
otter-pkg-updatesowns one APT context on its polling worker, exports a single StatusNotifierItem, and reads metadata without elevation. The separately packagedotter-pkg-refresh.timerrefreshes repository metadata every six hours and never installs packages. Use ReleaseFastresolver-check,tray-check, and actual-tree review fixtures for regression coverage.