TUI Device Experience Research
Summary: Evaluating approaches for a unified TUI experience on styrene fleet devices that can’t or shouldn’t run a full graphical desktop environment (COSMIC, GNOME, etc.). Recommends Approach D (Hybrid): Zellij as session shell with styrened.tui running as a dashboard pane — combining battle-tested multiplexing with custom styrene UI. Secondary recommendation: textual-web for browser-based access to headless devices.
Context
Styrene fleet devices span a resource spectrum:
| Tier | Example | RAM | Feasible |
|---|---|---|---|
| Headless | Pi Zero 2W | 512 MB | Console only, SSH target |
| Constrained | T100TA | 2 GB | Light GUI possible but wasteful |
| Capable | RPi 4B, x86 | 4-8 GB | Full DE or TUI by choice |
Tier 1 devices are SSH targets running styrened — the operator interacts via the TUI on their workstation. Tier 2-3 devices may boot into a local TUI experience for direct console interaction. All tiers benefit from a consistent operational shell that provides device identity, mesh status, and fleet context without requiring a graphical stack.
The question: what does a unified, lightweight operational environment look like for devices where a full NixOS desktop is either impossible or undesirable?
Landscape Survey
Prior Art: desktop-tui (Julien-cpsn)
desktop-tui is a Rust proof-of-concept (~1,300 LOC) using AppCUI-rs that renders a floating-window desktop metaphor inside a terminal. TOML-defined app shortcuts, taskbar, tiling modes.
Assessment: Experimental (author’s own warning: “made in 2 days”). The ANSI terminal parser covers basics (SGR colors, cursor movement, erase) but misses scroll regions, alternate screen buffer, mouse support — critical for hosting real TUI apps. No PTY allocation (uses pipes), no session persistence, no plugin system. The demo GIF looks impressive; the terminal emulation underneath is a sketch. Not suitable as a foundation, but validates the concept of a text-mode desktop metaphor.
vtm (Monotty Desktopio)
vtm — C++, 3.2k stars, actively maintained. The most complete text-based desktop environment available.
Capabilities:
- Floating and tiling window management within the terminal
- Infinite pannable workspace
- Collaborative session sharing — multiple users connect to same desktop
- Full binary serialization of UI state (detach/reattach)
- Session sharing over LAN (SSH tunnels, TCP, inetd)
- Wraps any console application, nestable indefinitely
Limitations:
- No extension/plugin system — customization requires forking the C++ codebase
- No Nix package currently (would need packaging work)
- No integration points for styrened without external status programs
- Less community adoption than tmux/Zellij
Zellij
Zellij — Rust, “terminal workspace with batteries included.” The modern tmux alternative.
Capabilities:
- Tiling and floating pane layouts defined in KDL
- Session persistence (detach/reattach)
- WASM plugin system — custom UI in any language compiling to WebAssembly
- Built-in status bar, tab management
- ~15MB RSS, minimal runtime overhead
- NixOS native:
programs.zellij.enable = true
Plugin system detail: Plugins render by printing to stdout within their allocated pane. They receive events (key presses, timer ticks, session state changes) and can call Zellij APIs (open panes, run commands, change layouts). Background worker threads available for async operations. Built-in plugins include the status bar, session manager, and config screen.
tmux
The incumbent. C, universally available, massive ecosystem. No plugin system beyond status bar scripting and external tools. Proven but static.
Textual + textual-web
Textual with textual-web can serve TUI applications to a web browser over WebSockets. This enables browser-based access to the TUI without SSH.
Relevant capability: textual-web can function as an SSH-like session in a browser tab. A fresh URL drops into a TUI application with full interactivity. This is unique among the options — no other approach offers browser-based access without additional infrastructure.
greetd + tuigreet
greetd is a minimal, agnostic login manager daemon. tuigreet is a console-based greeter. Together they provide TUI-native login on NixOS without any graphical stack. Both available in nixpkgs.
Cage (Wayland Kiosk)
Cage is a Wayland kiosk compositor that runs a single maximized application. An option if minimal Wayland is acceptable — runs a fullscreen terminal emulator (foot, alacritty) displaying the TUI session. Adds ~20MB overhead but gains proper font rendering and input handling. Worth considering for Tier 2-3 devices.
Architectural Approaches
Approach A: Zellij as Session Shell (Standalone)
greetd/tuigreet → auto-login → zellij --layout styrene.kdl
Custom Zellij WASM plugins provide fleet status, mesh indicators, and styrened IPC integration. Each pane is a native terminal — run any tool. Layouts shipped per device type.
What the user sees:
┌─ styrene: rpi-01 ────────────────────── MESH: ✓ ── 14:32 ─┐
│ ┌─ Status ──────────────┐ ┌─ Shell ──────────────────────┐ │
│ │ UPTIME: 14d 3h │ │ $ systemctl status styrened │ │
│ │ MEM: 128/512 MB │ │ ● styrened.service - Active │ │
│ │ DISK: 4.2/28 GB │ │ since Mon 2026-01-26... │ │
│ │ TEMP: 42°C │ │ │ │
│ │ RNS: CONNECTED │ │ $ │ │
│ └───────────────────────┘ └──────────────────────────────┘ │
│ <Alt+1> Status <Alt+2> Shell <Alt+3> Logs <Alt+N> New │
└────────────────────────────────────────────────────────────┘
| Pros | Cons |
|---|---|
| Battle-tested multiplexing | WASM plugins need Rust toolchain |
| Session persistence | Dashboard is separate process, not integrated |
| ~15MB RSS | No unified fleet view locally |
| NixOS native | Aesthetic limited to terminal colors |
| Identical over SSH | Separate codebase from the TUI |
Approach B: vtm as Text Desktop
greetd/tuigreet → auto-login → vtm
vtm provides the full desktop metaphor — floating windows, tiling, collaborative sessions. Console apps run in managed windows.
| Pros | Cons |
|---|---|
| Closest to “real DE” in text mode | No extension system (fork to customize) |
| Multi-user session sharing | Needs Nix packaging |
| Full state detach/reattach | No styrened integration without external tools |
| Cross-platform | Less community than Zellij/tmux |
Approach C: Textual TUI as Login Shell
greetd → auto-login → styrene --local-mode
Boot directly into styrened.tui as the session shell. The TUI is the operating environment with embedded terminal panes via pyte + ptyprocess.
| Pros | Cons |
|---|---|
| Unified codebase (Python/Textual) | Python terminal emulation bottleneck |
| Imperial CRT theme everywhere | No session persistence |
| Native styrened IPC | Single app — arbitrary tools only via embedded pane |
| textual-web enables browser access | Python import overhead on constrained hardware |
| Shared widgets/models with fleet TUI | Key-to-escape reversal problem |
Approach D: Hybrid — Zellij Session + Styrene TUI Pane (Recommended)
greetd → auto-login → zellij --layout styrene.kdl
├─ pane: styrene --dashboard
├─ pane: shell (bash/zsh)
└─ pane: journalctl -f -u styrened
Zellij provides session management and multiplexing. The TUI runs as one pane within it. An optional Zellij WASM status bar plugin shows mesh/RNS indicators.
| Pros | Cons |
|---|---|
| Battle-tested multiplexing + custom UI | Two technology stacks (Rust/WASM + Python) |
| Session persistence | Aesthetic seam between Zellij and the TUI |
| No Python terminal emulation needed | Zellij WASM API still evolving |
| The TUI focuses on dashboards | Additional config surface (KDL layouts) |
| Operator keeps full shell access | |
| Reproducible via NixOS config | |
| Identical experience over SSH |
Comparison Matrix
| Factor | A: Zellij Solo | B: vtm | C: Textual Shell | D: Hybrid |
|---|---|---|---|---|
| Resource overhead | Low (~15MB) | Medium | Medium (Python) | Low-Medium |
| Session persistence | Yes | Yes | No | Yes |
| Custom dashboard | WASM plugin | External proc | Native widget | Native widget |
| Shell access | Native | Native | Embedded (Python) | Native |
| Codebase unity | Separate (Rust) | Separate (C++) | Unified (Python) | Mixed |
| Imperial CRT theme | Terminal colors only | Terminal colors only | Full TCSS | Partial |
| SSH experience | Identical | Identical | Identical | Identical |
| textual-web support | No | No | Yes | Partial |
| NixOS integration | Excellent | Needs packaging | Good | Excellent |
| Maturity | High | Medium | Low (build it) | High foundation |
Recommendation
Approach D (Hybrid) is the pragmatic path.
Rationale
-
The TUI already exists. Adding
--local-mode/--dashboardshowing device-local status via styrened IPC is incremental work, not a new project. -
Zellij solves the hard problems. Session persistence, terminal emulation, multiplexing, resize handling — all identified as pain points in Python-based terminal emulation.
-
NixOS makes composition trivial. A NixOS module wires greetd + Zellij + styrened.tui into a single
enable = trueoption per device type. -
Operator workstation is unchanged. styrened.tui in full fleet management mode runs on the operator machine with or without Zellij wrapping it.
-
Browser access via textual-web is additive. Devices can additionally serve
styrene --dashboardvia textual-web for browser-based monitoring without SSH.
Where Approach C Wins
If browser-based access (textual-web) becomes the primary access pattern for headless devices, Approach C’s unified Python stack has an advantage. This can coexist: Zellij for local console/SSH, textual-web for browser access, both displaying the same styrened.tui dashboard.
Implementation Plan
Phase 1: NixOS Session Foundation
Goal: Devices boot into a Zellij session automatically. No desktop environment, no manual login on fleet devices.
Deliverables:
- NixOS module
styrene.sessionwiring greetd + tuigreet + Zellij - Per-device-type KDL layout files
- Imperial CRT terminal color scheme (16-color palette for terminal emulators)
- Auto-login configuration for fleet devices (operator workstations remain manual login)
Dependencies: None (foundation phase). Effort: M Benefits: All tiers. Tier 1 (headless) gets a consistent SSH landing. Tier 2-3 get a local console experience.
NixOS Module
# modules/styrene-session.nix
{ config, lib, pkgs, ... }:
let
cfg = config.styrene.session;
layoutFile = ./layouts + "/${cfg.deviceType}.kdl";
in {
options.styrene.session = {
enable = lib.mkEnableOption "Styrene TUI session";
deviceType = lib.mkOption {
type = lib.types.enum [ "headless" "constrained" "capable" ];
default = "constrained";
description = "Device tier — determines Zellij layout and resource allocation.";
};
autoLogin = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Auto-login to Zellij session (fleet devices). Disable for operator workstations.";
};
user = lib.mkOption {
type = lib.types.str;
default = "styrene";
description = "User account for the auto-login session.";
};
};
config = lib.mkIf cfg.enable {
# greetd as login manager
services.greetd = {
enable = true;
settings = {
default_session = if cfg.autoLogin then {
command = "${pkgs.zellij}/bin/zellij --layout ${layoutFile}";
user = cfg.user;
} else {
command = "${pkgs.greetd.tuigreet}/bin/tuigreet --cmd '${pkgs.zellij}/bin/zellij --layout ${layoutFile}' --theme 'text=green;border=darkgreen;prompt=lightgreen'";
};
};
};
# Zellij configuration
programs.zellij.enable = true;
# Imperial CRT terminal colors via console
console.colors = [
"0a0a0a" # black (background)
"ff6b6b" # red (danger)
"228b22" # green (dim phosphor)
"ffa94d" # yellow (warning)
"74c0fc" # blue (info)
"1a5c1a" # magenta (repurposed: dark green)
"69db7c" # cyan (repurposed: success)
"32cd32" # white (medium phosphor — standard text)
"0d2d0d" # bright black (surface)
"ff6b6b" # bright red
"39ff14" # bright green (bright phosphor — highlights)
"ffa94d" # bright yellow
"74c0fc" # bright blue
"228b22" # bright magenta (repurposed: dim phosphor)
"69db7c" # bright cyan
"39ff14" # bright white (accent)
];
};
}
KDL Layouts
Headless (headless.kdl) — SSH landing only, single pane:
layout {
pane
pane size=1 borderless=true {
plugin location="zellij:status-bar"
}
}
Constrained (constrained.kdl) — T100TA and similar, dashboard + shell:
layout {
pane split_direction="vertical" {
pane size="30%" command="styrene" {
args "--dashboard"
}
pane focus=true
}
pane size=1 borderless=true {
plugin location="zellij:status-bar"
}
}
Capable (capable.kdl) — RPi 4B, x86, full layout:
layout {
tab name="ops" focus=true {
pane split_direction="vertical" {
pane size="30%" command="styrene" {
args "--dashboard"
}
pane split_direction="horizontal" {
pane focus=true
pane size="30%" command="journalctl" {
args "-f" "-u" "styrened" "--no-hostname"
}
}
}
pane size=1 borderless=true {
plugin location="zellij:status-bar"
}
}
tab name="shell" {
pane
}
}
Imperial CRT Zellij Theme
// themes/imperial-crt.kdl
themes {
imperial-crt {
fg "#32cd32"
bg "#0a0a0a"
black "#0d2d0d"
red "#ff6b6b"
green "#39ff14"
yellow "#ffa94d"
blue "#74c0fc"
magenta "#228b22"
cyan "#69db7c"
white "#32cd32"
orange "#ffa94d"
}
}
Phase 2: TUI Dashboard Mode
Goal: The TUI gains a --dashboard flag that renders a device-local status view. Reads from styrened’s Unix socket IPC, not the mesh. Designed to run as a Zellij pane, not a fullscreen takeover.
Deliverables:
--dashboard/--local-modeCLI flag forstyrene- IPC client reading from styrened Unix socket (
/run/styrened/control.sock) - Dashboard widgets: uptime, memory, disk, temperature, RNS connectivity, LXMF queue depth
- Compact layout adapting to pane dimensions (min viable: 25 cols x 15 rows)
- Imperial CRT theme applied via existing TCSS
Dependencies: Phase 1 (layout references styrene --dashboard). Requires styrened IPC Control Server (implemented).
Effort: M
Benefits: Tier 2-3 (local console), Tier 1 via SSH if operator runs Zellij remotely.
CLI Entry Point
# styrened.tui __main__.py addition
@click.command()
@click.option("--dashboard", is_flag=True, help="Device-local dashboard mode (Zellij pane)")
def main(dashboard: bool):
if dashboard:
from styrene.screens.dashboard_local import LocalDashboardApp
LocalDashboardApp().run()
else:
from styrene.app import StyreneApp
StyreneApp().run()
IPC Data Contract
styrened’s IPC Control Server already exposes a Unix socket. The dashboard queries it for local device state:
| IPC Command | Response Fields | Widget |
|---|---|---|
status | uptime, load, memory, disk, temp | System vitals |
rns status | connected, interfaces, announce_count | RNS connectivity |
lxmf status | queue_depth, last_delivery, propagation_node | LXMF status |
identity | display_name, hash, address | Device identity header |
Dashboard Widget Layout
┌─ rpi-01 [3f8a...c21b] ──────┐
│ UP 14d 3h │ TEMP 42°C │
│ MEM 128/512 │ DISK 4.2/28G │
│ LOAD 0.12 │ │
├─ RETICULUM ──────────────────┤
│ STATUS: CONNECTED │
│ IFACE: WiFi ✓ LoRa ✓ │
│ PEERS: 7 known │
├─ LXMF ───────────────────────┤
│ QUEUE: 0 │ LAST: 2m ago │
│ PROP: styrene.hub [OK] │
└──────────────────────────────┘
At narrow widths (<40 cols), the dashboard drops to a single-column stack. At very narrow widths (<25 cols), it displays only the most critical metrics (uptime, RNS status, queue depth).
Refresh Strategy
- Poll IPC socket every 5 seconds (
set_interval) - No push mechanism needed — local socket is fast
- If styrened is unreachable, display
DAEMON: UNREACHABLEin danger color and retry with backoff
Phase 3: Zellij Status Bar Plugin (WASM)
Goal: A Zellij status bar plugin showing mesh identity, RNS connectivity, and clock. Always visible at the bottom of every Zellij layout, replacing or augmenting the default status bar.
Deliverables:
- Rust crate compiling to WASM (
styrene-zellij-plugin) - Status bar showing: device name, truncated RNS hash, mesh status indicator, UTC clock
- Reads from a lightweight status file (
/run/styrened/status.json) written by styrened - Imperial CRT colors via Zellij plugin API
- NixOS integration: plugin WASM binary included in Zellij config via Nix
Dependencies: Phase 1 (Zellij must be the session shell). styrened must write a status file (trivial addition to existing IPC server — write JSON sidecar on each status update). Effort: M Benefits: All tiers. The status bar is visible in every pane, every tab — persistent fleet awareness.
Plugin Architecture
styrened → writes /run/styrened/status.json (every 10s)
↓
Zellij plugin (WASM) → reads file on Timer event
↓
Renders status bar: [styrene: rpi-01] [3f8a] [MESH: ✓] [Q:0] [14:32 UTC]
The plugin does NOT connect to the Unix socket directly. WASM plugins in Zellij have limited I/O — file reads are supported, socket connections are not. styrened writes a JSON sidecar as a one-line file:
{"name":"rpi-01","hash":"3f8a","rns":"connected","lxmf_queue":0,"uptime":1234567}
Plugin Scaffold (Rust)
use zellij_tile::prelude::*;
#[derive(Default)]
struct StyreneStatusPlugin {
status: Option<DeviceStatus>,
}
#[derive(serde::Deserialize)]
struct DeviceStatus {
name: String,
hash: String,
rns: String,
lxmf_queue: u32,
uptime: u64,
}
impl ZellijPlugin for StyreneStatusPlugin {
fn load(&mut self, _config: BTreeMap<String, String>) {
set_timeout(10.0); // refresh every 10s
request_permission(&[
PermissionType::ReadApplicationState,
]);
}
fn update(&mut self, event: Event) -> bool {
match event {
Event::Timer(_) => {
// Read status file
if let Ok(data) = std::fs::read_to_string("/run/styrened/status.json") {
self.status = serde_json::from_str(&data).ok();
}
set_timeout(10.0);
true // re-render
}
_ => false,
}
}
fn render(&mut self, _rows: usize, cols: usize) {
match &self.status {
Some(s) => {
let mesh = if s.rns == "connected" { "✓" } else { "✗" };
let now = chrono::Utc::now().format("%H:%M");
print!(
" styrene: {} [{}] MESH:{} Q:{} {}",
s.name, &s.hash[..4.min(s.hash.len())], mesh, s.lxmf_queue, now
);
}
None => print!(" styrene: LOADING..."),
}
}
}
register_plugin!(StyreneStatusPlugin);
KDL Layout Integration
Replace the default status bar in layout files:
pane size=1 borderless=true {
plugin location="file:/run/current-system/sw/share/zellij/plugins/styrene-status.wasm"
}
Phase 4: textual-web Integration (Optional)
Goal: Serve the styrened.tui dashboard via textual-web for browser-based access. Headless Tier 1 devices become monitorable from any browser on the LAN without SSH.
Deliverables:
- systemd service running
textual-web serve styrene --dashboard - NixOS module option
styrene.session.textualWeb.enable - Bind to localhost by default; optional LAN exposure with explicit opt-in
- Basic security: listen address restriction, optional reverse proxy integration notes
Dependencies: Phase 2 (dashboard mode must exist). textual-web must be packaged for Nix (or run via nix run).
Effort: S
Benefits: All tiers, especially Tier 1 headless devices. Browser access eliminates SSH requirement for monitoring.
NixOS Module Extension
# Addition to styrene-session.nix
options.styrene.session.textualWeb = {
enable = lib.mkEnableOption "Serve dashboard via textual-web";
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Bind address. Set to 0.0.0.0 for LAN access (requires explicit opt-in).";
};
port = lib.mkOption {
type = lib.types.port;
default = 8501;
description = "Port for textual-web server.";
};
};
# systemd service
config.systemd.services.styrene-dashboard-web = lib.mkIf cfg.textualWeb.enable {
description = "Styrene Dashboard (textual-web)";
after = [ "network.target" "styrened.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.textual-web}/bin/textual-web serve styrene -- --dashboard --host ${cfg.textualWeb.listenAddress} --port ${toString cfg.textualWeb.port}";
User = cfg.user;
Restart = "on-failure";
RestartSec = 5;
};
};
Security Considerations
| Concern | Mitigation |
|---|---|
| Unauthenticated access | Default bind to localhost; LAN exposure requires explicit listenAddress = "0.0.0.0" |
| Data exposure | Dashboard is read-only (status display, no RPC commands) |
| Network attack surface | textual-web serves WebSocket + static assets only; no shell access |
| Production hardening | Place behind Traefik with Keycloak auth for non-localhost deployments |
textual-web currently has no built-in authentication. For LAN-exposed deployments, the recommendation is a reverse proxy (Traefik, Caddy) with SSO integration. The dashboard intentionally excludes command execution — it is a read-only status view.
Phase 5: Cage Wayland Kiosk (Optional, Tier 2-3)
Goal: For devices with physical screens where proper font rendering matters (VT323 monospace, Imperial CRT aesthetic at its best), run Cage as a minimal Wayland compositor displaying a fullscreen terminal emulator.
Deliverables:
- NixOS module option
styrene.session.wayland.enable(mutually exclusive with console-only mode) - Cage launching foot terminal with Zellij inside
- VT323 font configuration
- Overhead assessment: Cage + foot RAM usage on constrained hardware
Dependencies: Phase 1 (Zellij layout files reused). Font and theme configuration are additive. Effort: S Benefits: Tier 2-3 only. Tier 1 headless devices have no display — this phase is irrelevant for them.
NixOS Module Extension
# Addition to styrene-session.nix
options.styrene.session.wayland = {
enable = lib.mkEnableOption "Cage Wayland kiosk (for devices with screens)";
terminal = lib.mkOption {
type = lib.types.enum [ "foot" "alacritty" ];
default = "foot";
description = "Terminal emulator to run inside Cage. foot is lighter (~5MB RSS).";
};
};
config = lib.mkIf (cfg.enable && cfg.wayland.enable) {
# Override greetd to launch Cage instead of direct Zellij
services.greetd.settings.default_session.command = lib.mkForce
"${pkgs.cage}/bin/cage -s -- ${pkgs.${cfg.wayland.terminal}}/bin/${cfg.wayland.terminal} -e ${pkgs.zellij}/bin/zellij --layout ${layoutFile}";
# Font configuration
fonts = {
packages = [ pkgs.vt323 ];
fontconfig.defaultFonts.monospace = [ "VT323" "monospace" ];
};
};
foot Terminal Configuration
# /etc/xdg/foot/foot.ini (managed by NixOS)
[main]
font=VT323:size=14
pad=4x4
[colors]
background=0a0a0a
foreground=32cd32
regular0=0d2d0d
regular1=ff6b6b
regular2=228b22
regular3=ffa94d
regular4=74c0fc
regular5=1a5c1a
regular6=69db7c
regular7=32cd32
bright0=0a0a0a
bright1=ff6b6b
bright2=39ff14
bright3=ffa94d
bright4=74c0fc
bright5=228b22
bright6=69db7c
bright7=39ff14
Overhead Assessment
| Component | RSS (approx.) | Notes |
|---|---|---|
| Cage | ~8 MB | Minimal wlroots compositor |
| foot | ~5 MB | Wayland-native, GPU-accelerated text |
| Zellij | ~15 MB | Same as console mode |
| Total additional | ~13 MB | Cage + foot over bare console |
On a 2 GB T100TA, 13 MB additional overhead is negligible. On a 512 MB Pi Zero 2W, it is measurable but acceptable if the device has a display attached. Devices without displays should not enable this phase.
Phase Summary
| Phase | Effort | Dependencies | Tier 1 (Headless) | Tier 2 (Constrained) | Tier 3 (Capable) |
|---|---|---|---|---|---|
| 1: NixOS Session Foundation | M | None | SSH landing | Local console | Full workspace |
| 2: TUI Dashboard | M | Phase 1, styrened IPC | Via SSH | Local pane | Local pane |
| 3: Zellij Status Bar (WASM) | M | Phase 1, styrened status file | SSH sessions | Always visible | Always visible |
| 4: textual-web (Optional) | S | Phase 2 | Browser monitoring | Browser monitoring | Browser monitoring |
| 5: Cage Wayland (Optional) | S | Phase 1 | N/A (no display) | Proper fonts/rendering | Proper fonts/rendering |
Phases 1-3 are the core path. Phases 4-5 are additive enhancements that can be pursued independently once their dependencies are met. The entire stack is composable via NixOS module options — a single styrene.session.enable = true with device-type selection produces a working environment.
Sources
- desktop-tui — Proof-of-concept text desktop (Rust/AppCUI)
- vtm — Text-based desktop environment (C++)
- Zellij — Terminal workspace with WASM plugins (Rust)
- Zellij Plugin Documentation
- Textual — Python TUI framework
- textual-web — Serve Textual apps in browser
- greetd — Minimal login manager
- tuigreet — Console greeter for greetd
- Cage — Wayland kiosk compositor
- AppCUI-rs — Rust TUI framework (used by desktop-tui)