A small Quickshell shell for Hyprland, packaged for Nix and exportable as a Home Manager module.
shell.qmlentrypoint, matching the standard Quickshell pattern from the official examplestheme/for shared color, spacing, and typography tokens plus semantic widget style presetsanimations/for shared motion presets, shader wrappers, and reusable transition assetsassets/for the shipped wallpaper and related visual assetsservices/for Quickshell singletons and pure JS service logiccomponents/for reusable UI primitives such asModuleChipandWidgetChipmodules/bar/widgets/for bar-specific widgetsmodules/bar/BarHoverPopupHost.qmlfor the bar-local hover popup controller that keepsTopBar.qmlfocused on compositionmodules/bar/popups/for lightweight popup surfaceslib/for pure JS helpers that are easy to unit testtests/qml/for headless Qt Quick testsnix/for packaging plus the Home Manager moduleflake.nixandnix/env.nixfor the development shell, nix-direnv integration, formatting, and git hooks
I did a quick survey of the official docs and a few larger public shells:
- Official Quickshell docs recommend named configs under
$XDG_CONFIG_HOME/quickshell/<name>for dotfiles and$XDG_CONFIG_DIRS/quickshell/<name>for packaged installs. - The official examples keep
shell.qmlas the entrypoint and place related QML files next to it. - Larger community shells such as Caelestia, Noctalia, DankMaterialShell, and Illogical Impulse commonly grow into
components/,modules/,services/, andassets/directories. - Quickshell 0.2 added root-relative imports and
.qmlls.inisupport for QMLLS, but those generated VFS files are system-dependent. For a sandbox-friendly project with strict headless linting, this repo sticks to explicit path imports. - For automated testing, the practical baseline is Qt Quick Test via
qmltestrunner, run with-platform offscreen. Wayland-specific behavior likePanelWindowplacement and Hyprland integration should still be treated as manual validation.
A longer note with links is in docs/research.md.
These docs are internal reuse guides for contributors and AI working in this repo.
docs/style-guide.mdfor the shell-wide visual language, naming rules, popup content guidance, and the AI implementation checklistdocs/shared-ui.mdfor pane, menu, form, and shared shell primitives inui/docs/components.mdfor bar chips, hover popup shells, and popup helper rows incomponents/docs/bar.mdfor global bar visibility, content unloading, and power-aware bar servicesdocs/panes.mdforPaneManagerand pane control flowdocs/notifications.mdforNotificationManagerand notification UI flowdocs/picker.mdfor the picker service and overlaydocs/animation-selector.mdfor shared animation usage
Use the docs in this order:
- visual language, naming, popup information selection:
docs/style-guide.md - bar widgets and bar hover popups:
docs/components.md - global bar visibility and bar-only service lifecycle:
docs/bar.md - panes, menus, forms, and general shell controls:
docs/shared-ui.md - shared controller state and overlays: the relevant subsystem guide in
docs/ - background repo research and external references:
docs/research.md
The migrated shell currently includes:
- fixed
1..5workspace indicators - active-workspace window chips
- CPU usage, RAM, and CPU temperature modules
- battery and audio modules
- system tray icons and a system glyph in the status cluster
- a status-cluster system popup with an idle inhibitor toggle
The shipped media defaults are:
- desktop wallpaper reference:
assets/coding-deck.png
Animation presets are shared through the top-level animations/ module. The current presets are HoverRevealAnimation, SmokeAnimation, and SlideIn. Surfaces can either instantiate a preset directly or use Animations.AnimationSelector with preset, or with separate openPreset / closePreset values when the enter and exit effects should differ. Window- and popup-level animated surfaces should keep their outer visible binding tied to the animation wrapper's lifecycle state (for example shouldShow) so close transitions can finish before the surface is torn down.
The Home Manager runtime configuration surface is intentionally small:
fonts.proseFamilyfonts.monoFamilyfonts.codeFamilyfonts.notificationToastFamilyfonts.sizefonts.aiPaneSizefonts.tasksPaneSizefonts.notificationToastSizefonts.packagestheme.flavorbar.disk.textPathbar.disk.warningThresholdGiB(default50)bar.disk.criticalThresholdGiB(default10)bar.disk.popupPathsbar.disk.btrfs.enable(defaultfalse)bar.disk.btrfs.pollInterval(default300seconds)bar.heightbar.temperature.enablebar.temperature.textPathbar.temperature.popupPathsbar.temperature.fanPaths
Bar ordering, spacing, and most theme defaults are kept in code rather than exposed as user options.
The shell palette is centrally managed in theme/Theme.qml and can be switched between Catppuccin flavors through Home Manager:
programs.couch-shell.theme.flavor = "mocha";Supported values are latte, frappe, macchiato, and mocha. The default is latte.
The top bar height is configurable through Home Manager:
programs.couch-shell.bar.height = 24;The default is 20 pixels, but the rendered bar grows when needed to keep larger configured fonts from clipping glyph icons. Bar modules use the same height so widget chips stay aligned with the effective bar size.
Font families are configured through the Home Manager module and substituted into the packaged QML config at build time.
fonts.proseFamilyis used by task notes, AI chat inputs, and markdown prose.fonts.monoFamilyis the shell's monospace/default font family.fonts.codeFamilyis used by markdown fenced code blocks.fonts.notificationToastFamilyis used by notification toasts.fonts.sizeis the base shell font size in pixels.fonts.aiPaneSizeis the AI pane font size in pixels.fonts.tasksPaneSizeis the tasks pane font size in pixels.fonts.notificationToastSizeis the notification toast font size in pixels.fonts.packagesadds font packages to the wrapper's fontconfig file so the selected families are visible when launching the packaged shell.
Defaults:
programs.couch-shell.fonts = {
proseFamily = "Inter";
monoFamily = "CommitMono Nerd Font";
codeFamily = "CommitMono Nerd Font";
notificationToastFamily = "CommitMono Nerd Font";
size = 12;
aiPaneSize = 12;
tasksPaneSize = 12;
notificationToastSize = 12;
packages = [ ];
};Example:
programs.couch-shell.fonts = {
proseFamily = "Inter";
monoFamily = "CommitMono Nerd Font";
codeFamily = "JetBrainsMono Nerd Font";
notificationToastFamily = "Inter";
size = 12;
aiPaneSize = 15;
tasksPaneSize = 13;
notificationToastSize = 16;
packages = [
pkgs.inter
pkgs.nerd-fonts.jetbrains-mono
];
};Install or list any font packages needed by the configured family names. If a family is not available through fontconfig, Qt will fall back according to the system font configuration.
The disk widget shows available space for textPath. Its hover popup lists free and total space for the containing filesystem of each named path, preserving names and ordering. These defaults also apply when running unpackaged QML:
programs.couch-shell.bar.disk = {
warningThresholdGiB = 50;
criticalThresholdGiB = 10;
textPath = "/persist";
popupPaths = [
{ name = "Persist"; path = "/persist"; }
{ name = "Ephemeral"; path = "/"; }
];
};textPath is independent of popupPaths. Custom lists replace the defaults; popupPaths = [ ]; disables the hover popup. Package users can pass the same bar.disk settings to lib.mkCouchShellPackage. Names and paths may contain spaces or quotes. Missing or unreadable paths show Unk in the widget and Unavailable in the popup, without falling back to another filesystem.
Disk data is shared across monitors and refreshed every 15 seconds, with one bounded query per distinct path. The widget path keeps polling while the bar is hidden; popup-only queries stop and are cancelled, then refresh immediately when shown. See docs/bar.md.
Disk warnings apply only to bar.disk.textPath: below warningThresholdGiB (default 50) the chip turns peach, and below criticalThresholdGiB (default 10) it turns red. Thresholds are positive integers in GiB, with critical lower than warning; comparisons use unrounded available bytes. Failed readings show Unk with normal tone and send no alerts.
“Low disk space” and “Critical disk space” notifications identify the path and remaining space and stay until dismissed. Each severity alerts once per session-local low-space episode; a direct critical drop sends only the critical alert. A successful reading at or above the warning threshold rearms both alerts. Partial recovery, failures, and dismissal do not rearm them, and recovery does not dismiss existing toasts. Notifications respect the existing do-not-disturb behavior.
For automatic Btrfs discovery, import the NixOS module at the system level:
imports = [ couch-shell.nixosModules.default ];Then enable the hover in the integrated Home Manager user's configuration:
programs.couch-shell.bar.disk.btrfs = {
enable = true;
pollInterval = 300; # seconds; five minutes by default
};The NixOS module automatically installs one shared, socket-activated root collector for enabled users. Log in again after activation to pick up the couch-shell-btrfs group. This hover replaces popupPaths, including an empty list, and shows mounted volumes, subvolume referenced/exclusive usage when accounting is valid, and snapshot counts. The chip still uses textPath at its existing 15-second interval. Hiding the bar stops Btrfs requests; the system service has no periodic timer.
Standalone Home Manager and direct package consumers must provision the root collector separately; enabling a user option cannot install a system service. See docs/bar.md for system provisioning, accounting states, access permissions, and diagnostics. Leaving Btrfs disabled preserves the manual hover.
Temperature sensor paths must be full sysfs paths, such as /sys/class/hwmon/hwmon7/temp1_input for temperatures and /sys/class/hwmon/hwmon7/fan1_input for fan speed sensors. When set, popupPaths and fanPaths replace the default sensor sets; use { } to show no popup temperatures or fans.
Example:
programs.couch-shell.bar.temperature = {
textPath = "/sys/class/hwmon/hwmon7/temp1_input";
popupPaths = {
gpu = "/sys/class/hwmon/hwmon7/temp2_input";
ram = "/sys/class/hwmon/hwmon6/temp1_input";
wifi = "/sys/class/hwmon/hwmon11/temp1_input";
nvme0 = "/sys/class/hwmon/hwmon0/temp1_input";
nvme1 = "/sys/class/hwmon/hwmon1/temp1_input";
};
fanPaths = {
fan1 = "/sys/class/hwmon/hwmon7/fan1_input";
fan2 = "/sys/class/hwmon/hwmon7/fan2_input";
};
};The bar and side panes expose Quickshell IPC commands that you can map from Hyprland. Bar visibility is global across monitors and resets to visible whenever the shell starts:
couch-shell ipc call bar showshows every barcouch-shell ipc call bar hidehides every barcouch-shell ipc call bar toggletoggles every bar
The current pane actions are:
couch-shell ipc call panes toggleCouchAitoggles the left pane onCouch AIcouch-shell ipc call panes toggleTaskstoggles the right pane onTaskscouch-shell ipc call panes openNewTaskopens a centered new-task window
Add matching Hyprland binds so those shortcuts execute the IPC commands.
For NixOS with Home Manager, this can look like:
{
wayland.windowManager.hyprland = {
enable = true;
settings = {
bind = [
"$mod, B, exec, couch-shell ipc call bar toggle"
"$mod SHIFT, A, exec, couch-shell ipc call panes toggleCouchAi"
"$mod SHIFT, T, exec, couch-shell ipc call panes toggleTasks"
"$mod SHIFT, Return, exec, couch-shell ipc call panes openNewTask"
];
};
};
}If you do not use $mod, replace it with the modifier string you use in Hyprland, such as SUPER.
Couch AI stores OpenAI/Codex OAuth credentials, local chat history, and the cached model catalog separately under the user data directory:
- credentials:
~/.local/share/couch-shell/auth.json - chat state:
~/.local/share/couch-shell/ai-state.json - model cache:
~/.local/share/couch-shell/ai-models.json
These files are written through the couch-ai-codex helper with a private data directory (0700), private JSON files (0600), and atomic temp-file renames. COUCH_SHELL_AUTH_FILE, COUCH_SHELL_AI_STATE_FILE, and COUCH_SHELL_AI_MODELS_FILE can override the default paths.
Every model refresh first discovers the latest stable Codex version from GitHub, then requests the model catalog using that version. Refresh requires network access to both GitHub and the model service; if either request fails, the existing model list and cache are preserved and an error is shown. Loading cached models remains offline. The cache records the successful client version so that a version change forces a fresh catalog request.
COUCH_SHELL_CODEX_CLIENT_VERSION is no longer used. A client_version query parameter in COUCH_SHELL_CODEX_MODELS_URL is replaced by the discovered version. Refresh does not fall back to a bundled, installed, or previously cached client version.
Chat state includes sessions, messages, drafts, selected model/settings, active session, and persisted reasoning text. It does not include OAuth tokens; those remain only in auth.json. Logging out removes credentials but intentionally keeps local chat history. Chat history is currently retained until the local state file is removed or a future clear-history UI is added.
See authentication guarantees for credential concurrency, logout, and OAuth callback handling.
direnv allowThat loads the flake-backed development shell through nix-direnv automatically.
./scripts/lint
./scripts/test
nix flake check
nix run .#couch-shell./scripts/lint runs qmllint in strict mode:
-W 0makes warnings fail immediatelyunused-importsandmultiline-stringsare promoted from info to warning- imports come from the current flake's Qt + upstream Quickshell environment
scripts/with-qml-envrefreshesQML_IMPORT_PATHandQT_PLUGIN_PATHon each run so stale shell variables do not break module resolution
Automated tests are intentionally limited to headless-safe checks:
qmllintfor static analysisqmltestrunnerwith-platform offscreenfor pure QML/JS logic tests
Manual tests are still required for:
- actual
PanelWindowplacement on Wayland - Hyprland monitor/workspace behavior
- compositor integration details
From the repo:
nix run .#couch-shellAfter installation:
couch-shellAdd the flake input and import the module:
{
inputs.couch-shell.url = "github:burnskp/couch-shell";
outputs = { self, nixpkgs, home-manager, couch-shell, ... }: {
homeConfigurations.me = home-manager.lib.homeManagerConfiguration {
pkgs = import nixpkgs { system = "x86_64-linux"; };
modules = [
couch-shell.homeManagerModules.default
{
programs.couch-shell = {
enable = true;
systemd.enable = true;
fonts = {
proseFamily = "Inter";
monoFamily = "CommitMono Nerd Font";
codeFamily = "JetBrainsMono Nerd Font";
notificationToastFamily = "Inter";
};
bar.temperature = {
textPath = "/sys/class/hwmon/hwmon10/temp1_input";
fanPaths = {
fan1 = "/sys/class/hwmon/hwmon10/fan1_input";
fan2 = "/sys/class/hwmon/hwmon10/fan2_input";
};
};
};
}
];
};
};
}If you use Home Manager through nixosConfigurations, add the same flake input and include the module in one of your Home Manager user modules:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
couch-shell.url = "github:burnskp/couch-shell";
couch-shell.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{ nixpkgs, home-manager, couch-shell, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.me = {
imports = [ couch-shell.homeManagerModules.default ];
programs.couch-shell = {
enable = true;
systemd.enable = true;
fonts = {
proseFamily = "Inter";
monoFamily = "CommitMono Nerd Font";
codeFamily = "JetBrainsMono Nerd Font";
notificationToastFamily = "Inter";
};
bar.temperature = {
textPath = "/sys/class/hwmon/hwmon10/temp1_input";
fanPaths = {
fan1 = "/sys/class/hwmon/hwmon10/fan1_input";
fan2 = "/sys/class/hwmon/hwmon10/fan2_input";
};
};
};
};
}
];
};
};
}The documented workflows should be validated with:
./scripts/lint./scripts/testnix flake checknix run .#couch-shell -- --help$(nix build .#couch-shell --no-link --print-out-paths)/bin/couch-shell --help
Manual Wayland validation is still required for actual panel placement and compositor integration.
Hooks are defined in flake.nix through git-hooks.nix and installed by the development shell.
- pre-commit: format + Nix hygiene + shell lint + QML lint
- pre-push: full
./scripts/test