| name | getbyshell |
| description | GetByShell desktop shell — niri WM integration, panel system, calendar widget, popover components, modal patterns, and Nix-based configuration. |
getbyshell
prereqs: none
provides: getbyshell-architecture, getbyshell-nix, getbyshell-debugging
children: CHANGELOG.md, references/INDEX.md
governed-by: metaskill
update-strategy: re-derive from src/lib/getbyshell/AGENTS.md + nix/lib/getbyshell/SPEC.md + systemd unit files
update-trigger: surface topology changes, Tauri config changes, Nix lib changes, new widget added
Description
GetByShell is a multi-surface widget system for the TMNL desktop environment. Each "surface" is an independent Wayland layer-shell window backed by a Tauri + Vite + React stack, deployed as systemd user services via Nix/Home-Manager. Surfaces communicate via Tauri IPC, PubSub channels, and serialized data contracts.
Not a single app. Not a component library. An ecosystem of persistent UI surfaces that compose the user's desktop shell.
When to Load
- Working on any GetByShell surface (bar, panel, palette)
- Debugging systemd service failures or crash loops
- Modifying Nix modules under
nix/lib/getbyshell/ or nix/modules/getbyshell/
- Working in
src-shell/, src-panel-tauri/, src/lib/getbyshell/
- Adding a new surface to the ecosystem
- Diagnosing Wayland layer-shell, GTK, or WebKitGTK issues
Router
What are you doing?
│
├─ Debugging a crash / service not starting
│ → §1 Systemd Topology + §2 Common Failure Modes
│
├─ Working on the bar (48px strip)
│ → ref/bar.md + ref/popover.md + ref/modal.md
│
├─ Working on the panel surface
│ → ref/panel.md
│
├─ Working on calendar / Chronicle
│ → ref/calendar.md
│
├─ Modifying Nix service generation
│ → §3 Nix Library Architecture
│
├─ Adding a new surface
│ → §4 Adding a Surface
│
├─ Working on cross-surface communication
│ → §5 Shared Infrastructure
│
└─ Working on logging / observability
→ ref/logging.md
§1 Systemd Topology
getbyshell.target ← Groups all surfaces
├── tmnl-bar-vite.service ← :1421 Vite dev server
├── tmnl-bar.service ← Tauri layer-shell (Layer::Top)
├── tmnl-panel-vite.service ← :1422 Vite dev server
└── tmnl-panel.service ← Tauri layer-shell (Layer::Overlay)
Key relationships:
- Each surface = 2 services:
tmnl-{name}-vite (Vite) + tmnl-{name} (Tauri)
- Tauri service
Requires + After its Vite service
- Tauri service has
ExecStartPre health check (curl loop on Vite port)
- Tauri service has
After=graphical-session.target + ConditionEnvironment=WAYLAND_DISPLAY
getbyshell.target groups all surfaces, WantedBy=default.target
Diagnostic commands:
getbyshell-status
getbyshell-logs
getbyshell-bar-status
getbyshell-bar-logs
systemctl --user status tmnl-panel.service
journalctl --user -u tmnl-panel --since "5 min ago" --no-pager
Service files location: ~/.config/systemd/user/ (generated by Home-Manager)
§2 Common Failure Modes
Tauri Plugin Deserialization Panic
Symptom: PluginInitialization("log", "Error deserializing 'plugins.log' ... invalid type: map, expected unit")
Cause: tauri.conf.json has a plugins.log config object, but the Rust code initializes the plugin via Builder::new().build() (no config deserialization). Tauri tries to deserialize the JSON config into the plugin's config type, which expects unit {} not a map.
Fix: Either remove plugins.log from tauri.conf.json, or pass config through the Rust builder:
"plugins": { "log": { "level": "info" } }
"plugins": {}
Duplicate Webview Label Panic
Symptom: Failed to setup app: a webview with label 'panel' already exists
Cause: tauri.conf.json declares a window with label X, AND the Rust setup hook creates a WebviewWindowBuilder with the same label. Tauri creates config windows first, then runs setup — collision.
Fix: If the Rust code creates the window programmatically (e.g., for GTK layer-shell transplanting), remove the window from tauri.conf.json:
"windows": []
Systemd Dependency Cycle
Symptom: Found ordering cycle: getbyshell.target/start after tmnl-panel.service/start after graphical-session.target/start
Cause: getbyshell.target has WantedBy=graphical-session.target AND After=tmnl-{name}.service, but services have After=graphical-session.target. Creates: graphical-session → target → service → graphical-session.
Fix: Target uses WantedBy=default.target, NOT graphical-session.target. Individual services handle their own graphical-session dependency.
Vite Health Check Timeout
Symptom: ExecStartPre fails, Tauri never starts.
Cause: Vite dev server didn't start in time. Check tmnl-{name}-vite service logs.
Fix: Check port conflicts, increase healthCheckTimeout in surface config, check Vite config validity.
WebKitGTK Rendering Issues
Symptom: Blank/tiny/invisible content in the layer-shell window.
Cause: Compositor compositing bugs (especially WSLg).
Fix: Set WEBKIT_DISABLE_COMPOSITING_MODE=1 environment variable.
§3 Nix Library Architecture
Location: nix/lib/getbyshell/
nix/lib/getbyshell/
├── SPEC.md # Design specification
├── default.nix # Flake-parts module (mission-control + exports)
├── hm-module.nix # Home-Manager module (systemd services)
├── types.nix # Surface submodule type definition
├── surface.nix # mkSurface: generates service pair from config
├── shared-env.nix # mkSharedEnv: runtime PATH/PKG_CONFIG/LD_LIBRARY
├── health-check.nix # mkHealthCheck: curl loop per surface
├── mission-control.nix # mkSurfaceScripts + mkGlobalScripts
└── target.nix # mkTarget: getbyshell.target + legacy aliases
Consumer API (in ~/.config/nix/modules/home/getbyshell.nix):
gbg.getbyshell = {
enable = true;
projectDir = "/path/to/packages/tmnl";
surfaces = {
bar = { port = 1421; layer = "top"; viteConfig = "vite.config.shell.ts"; tauriDir = "src-shell-tauri"; };
panel = { port = 1422; layer = "overlay"; viteConfig = "vite.config.panel.ts"; tauriDir = "src-panel-tauri"; };
};
};
Adding a surface = add an attrset. All services, scripts, and health checks generate automatically.
§4 Adding a Surface
- Create
src-{name}-tauri/ with Cargo.toml, tauri.conf.json, src/lib.rs
- Create
vite.config.{name}.ts at package root
- Add surface to Nix config:
surfaces.newname = {
port = 1423;
layer = "overlay";
viteConfig = "vite.config.newname.ts";
tauriDir = "src-newname-tauri";
};
sudo nixos-rebuild switch to deploy systemd services
getbyshell-newname-start to bring it up
Critical: If Rust code creates the webview programmatically (layer-shell transplant), set "windows": [] in tauri.conf.json.
§5 Shared Infrastructure
Cross-Surface Communication
- Tauri IPC events:
listen() / emit() within a surface
- SIGUSR1: niri keybind →
pkill -USR1 tmnl-panel → toggle handler
- Future: Shared PubSub via NATS or Tauri deep links
Rust IPC Bridge (per surface)
update_input_region — pointer-event pass-through sync
set_surface_width — dynamic surface expansion for popovers/modals
shell_log_batch — structured log batching to journald
Architecture Principles
- Atom-as-State —
Atom.make() primary, services mutate via Atom.set(), React subscribes
- Schema-Backed Types — Effect Schema for all cross-boundary data
- Compound Components — Ark UI–inspired, context-scoped
- Layer-Shell Surface Model — Input regions synced to Rust, pointer pass-through
- Structured Logging — Effect Logger → batched Tauri IPC → Rust log → journald
§6 File Map
src/lib/getbyshell/ # Core lib (bar state, hooks, services)
├── types.ts # Schema: Workspace, NiriWindow, ConnectionStatus
├── atoms.ts # Runtime atom, workspace/window/clock state, operations
├── hooks.ts # useClockTick, useNiriSync, useWorkspaces, etc.
├── niri.ts # NiriService (Effect.Service → Tauri IPC)
├── popover/ # Floating panel compound component
├── modal/ # Full-overlay compound component
├── calendar/ # Month grid + Chronicle day entities
│ └── chronicle/ # Rich day entity (schemas, state, machines, services, atoms)
├── logging/ # Effect Logger → Tauri IPC bridge
└── index.ts # Public API barrel
src-shell/ # Bar surface entry points + components
├── App.tsx # Bar root
├── main.tsx # Vite entry
├── components/ # BarLayout, Clock, CalendarPanel, etc.
└── *.html # Multi-entry HTML files
src-panel-tauri/ # Panel surface (Rust + Tauri)
├── src/lib.rs # Layer-shell setup, SIGUSR1 handler, toggle logic
└── tauri.conf.json # Panel Tauri config (windows: [] — Rust creates them)
nix/lib/getbyshell/ # Nix library (service generation)
nix/modules/getbyshell/ # Monorepo flake-parts module
Known Issues (Active)
- Panel Rust code has a
function_casts_as_integer warning on signal handler cast — cosmetic, fix with handler as *const () as libc::sighandler_t
getbyshell.target dependency cycle if WantedBy=graphical-session.target — must use default.target
- Panel starts hidden by design — toggle via
pkill -USR1 tmnl-panel or bar IPC