Project: NothingLess
Version: 1.0.0
Framework: QtQuick / Quickshell
Primary Languages: QML, JavaScript, Python, Bash, Nix
Compositor: Hyprland (via axctl abstraction)
Target Platforms: Arch Linux, Fedora, NixOS
NothingLess is a highly customizable Wayland shell built on Quickshell. It provides a unified desktop environment layer including a status bar, dynamic notch ("dynamic island"), app dock, dashboard, lockscreen, desktop widgets, notification popups, and an AI assistant sidebar. The shell is driven by a reactive JSON configuration system and supports multi-monitor setups via per-screen Variants.
The project was forked from Ambxst and maintains the same upstream license. All NothingLess-specific modifications are provided under that same license.
- 130+ compositor settings across 11 categories (vs. ~40 upstream)
- Hardware-accelerated video wallpapers via QtMultimedia + FFmpeg (instead of mpv)
- Custom MangoHud integration for real-time FPS display in the notch
- Configurable rendering backend: OpenGL (default) or Vulkan with threaded render loop
- Ndot dot-matrix typography and monochrome-with-red-accents design language
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | Qt 6 (QtQuick, QtQuick.Controls, QtQuick.Effects, QtQuick.Layouts) | Rendering, animations, controls |
| Shell Runtime | Quickshell (qs) |
Wayland panel/surface manager, QML engine, IPC |
| Compositor Bridge | axctl (Go binary, external repo) |
Hyprland abstraction: window focus, workspace dispatch, config persistence |
| Configuration | JSON on disk + Quickshell.Io.FileView / JsonAdapter |
Reactive, file-backed persistent config |
| Backend Scripts | Python 3, Bash | System monitoring, clipboard, OCR, screenshots, wallpaper thumbs |
| Color Generation | matugen |
Material You color extraction from wallpapers |
| Packaging | Nix Flake (flake.nix) |
Reproducible builds, NixOS module, dev shells |
| Install Script | install.sh (Bash) |
Arch / Fedora dependency install, repo clone, launcher setup |
- Core:
quickshell,qt6-base,qt6-declarative,qt6-wayland,qt6-svg,qt6-multimedia,qt6-shadertools,kf6-syntax-highlighting,kf6-breeze-icons - Compositor:
hyprland,axctl - System:
brightnessctl,grim,slurp,wl-clipboard,wlsunset,wtype,upower,power-profiles-daemon,NetworkManager,bluetooth - Media:
playerctl,ffmpeg,gpu-screen-recorder,wf-recorder - Fonts:
ttf-phosphor-icons,ttf-ndot(custom),ttf-roboto,noto-fonts,noto-fonts-emoji - Tools:
kitty,tmux,fuzzel,matugen,tesseract,zenity,jq,sqlite - Python packages (installed via pipx where applicable): script dependencies are runtime-checked
./
├── shell.qml # Entry point: ShellRoot, Variants per screen, service init
├── cli.sh # Launch wrapper & IPC controller (brightness, lock, install, etc.)
├── install.sh # Distribution-aware installer (Arch, Fedora, NixOS)
├── flake.nix # Nix flake: packages, devShells, apps, NixOS module
├── version # Single-line version string (e.g., "1.0.0")
│
├── config/ # Central configuration system
│ ├── Config.qml # >3700 lines. Singleton. FileView + JsonAdapter persistence
│ ├── ConfigValidator.js # Deep-merge validation against defaults
│ ├── KeybindActions.js # Keybind action dispatch table
│ └── defaults/*.js # 14 default blueprints: bar.js, theme.js, ai.js, compositor.js, etc.
│
├── modules/ # All QML code organized by domain
│ ├── bar/ # Panel widgets: clock, systray, workspaces, battery, volume
│ ├── components/ # Reusable UI primitives + GLSL shaders (55 files)
│ ├── corners/ # Rounded screen-corners overlay
│ ├── desktop/ # Desktop background + icon grid
│ ├── dock/ # App dock (standalone or integrated)
│ ├── frame/ # Screen border / glow effect
│ ├── globals/ # GlobalStates.qml — transient runtime state (non-persistent)
│ ├── lockscreen/ # WlSessionLock + PAM authentication
│ ├── notch/ # Dynamic island UI (launcher, dashboard, notifications)
│ ├── notifications/ # Popup system + delegate + history
│ ├── services/ # 43+ backend singletons (Battery, AI, Network, AxctlService, etc.)
│ ├── shell/ # UnifiedShellPanel + ReservationWindows + OSD
│ ├── sidebar/ # AI assistant sidebar
│ ├── theme/ # Colors, Icons, Styling singletons + app config generators
│ ├── tools/ # Screenshot, screen recording, mirror, color picker
│ └── widgets/ # Complex overlays
│ ├── config/ # Standalone settings window
│ ├── dashboard/ # Main hub: controls, metrics, assistant, clipboard, notes
│ ├── defaultview/ # Notch idle content (compact player, notification indicator)
│ ├── launcher/ # App search + multi-tab launcher
│ ├── overview/ # Mission Control workspace overview
│ ├── powermenu/ # Lock, logout, shutdown actions
│ ├── presets/ # Theme/layout preset switcher
│ └── tools/ # Quick utility access (OCR, recording, etc.)
│
├── scripts/ # Python & Bash backends invoked by QML via Quickshell.Io.Process
│ ├── system_monitor.py # CPU/RAM/GPU/disk/temp JSON output
│ ├── clipboard_watch.sh # wl-paste --watch wrapper
│ ├── thumbgen.py # Wallpaper thumbnail generation
│ ├── lockwall.py # Lockscreen wallpaper blur preprocessing
│ ├── colorpicker.py # hyprpicker wrapper
│ ├── ocr.sh # Screenshot → OCR text extraction
│ ├── wf-record.sh # Screen recording wrapper
│ ├── weather.sh # Weather data fetching
│ └── ...
│
├── assets/ # Wallpapers, color presets, AI provider configs, sounds, fonts
│ ├── presets/ # Default theme/layout presets (copied to ~/.config on first run)
│ ├── nothingless/ # Brand assets (logo, animations)
│ ├── aiproviders/ # Per-provider config templates
│ ├── colors/ # Color preset JSONs
│ └── sound/ # UI sounds
│
└── nix/ # Nix-specific packaging
├── lib.nix # `forAllSystems` helper
├── modules/default.nix # NixOS module (enables services, fonts)
└── packages/ # Granular package sets: core, apps, tools, media, fonts, tesseract
Quickshell uses a VFS import prefix qs. rather than physical qmldir files for most modules. A few directories (e.g., modules/bar/, modules/widgets/launcher/) contain qmldir files for legacy compatibility, but the primary resolution mechanism is Quickshell's built-in module system.
# Direct Quickshell launch (requires qs in PATH)
qs -p shell.qml
# Or via the CLI wrapper (sets up QML import paths, config presets, etc.)
./cli.sh# One-liner installer (Arch / Fedora / NixOS)
curl -sL https://github.com/Leriart/NothingLess/raw/main/install.sh | sh
# Manual clone + symlink
git clone https://github.com/Leriart/NothingLess.git ~/.local/src/nothingless
sudo ln -s ~/.local/src/nothingless/cli.sh /usr/local/bin/nothingless# Enter a shell with all dependencies and QML_IMPORT_PATH set
nix develop
# Run directly from the flake
nix run github:Leriart/NothingLessnothingless install hyprland # Auto-detect config mode
nothingless install hyprland --conf # Force .conf mode (safe default)
nothingless install hyprland --lua # Force Lua mode (Hyprland >= 0.48)
nothingless remove hyprland # Remove integrationThere is currently no automated test suite. The project relies on:
- Manual runtime testing on Hyprland
- Visual regression testing via screenshots in PRs (see
.github/pull_request_template.md) - Nix flake evaluation (
nix flake check) for packaging correctness
When modifying UI, authors are expected to provide before/after screenshots in pull requests.
ShellRootinitializes a globalContextMenuand per-screenVariants.- Per-screen layers (stacked bottom to top):
Wallpaper(per screen)Desktopicon grid (if enabled)UnifiedShellPanel— contains Bar, Notch, Dock, FrameScreenCornersrounded overlay (if enabled)ReservationWindows— Wayland exclusive-zone reservations for bar/dock/sidebarOverviewPopup,PresetsPopup(conditional)
- Global overlays (single instance):
WlSessionLock→LockScreen(secure lockscreen)ScreenshotTool,ScreenshotOverlay,ScreenrecordTool,MirrorWindowSettingsWindow,OSD
- Service initialization is deferred:
- Critical services (
CaffeineService,IdleService,GlobalShortcuts,BatteryAlertService) init on next tick viaQt.callLater - Non-critical services (
NightLightService,GameModeService) deferred 2s
- Critical services (
- Boot splash (
assets/nothingless/NOTHING_splash.webp) auto-fades after ~5.3s.
Config.qmlwatches~/.config/nothingless/config/*.jsonviaFileView- Missing files are bootstrapped from
assets/presets/NothingLess Default/ JsonAdaptercreates bidirectional QML property bindings- Changes auto-persist to disk; use
Config.pauseAutoSavefor batch updates Config.initialLoadCompletegates components that need fully initialized config
Colors.qmlwatches~/.cache/nothingless/colors.json(generated bymatugen)- On change, it regenerates app configs: Qt6ct, GTK, Pywal, Kitty, NvChad, Discord
Config.resolveColor(name)maps semantic names (e.g.,"surface","primary") to actual colors
AxctlService.qmlis the single point of contact with Hyprland- It reads/writes
~/.local/share/nothingless/axctl.toml - Dispatches workspace/window/monitor commands via the
axctlCLI daemon CompositorConfig.qmlapplies shell theme colors to Hyprland decoration settingsCompositorKeybinds.qmlmanages dynamic keybind injection
All services use pragma Singleton and expose Singleton { id: root }.
| Symbol | File | Responsibility |
|---|---|---|
Config |
config/Config.qml |
Central reactive config store |
GlobalStates |
modules/globals/GlobalStates.qml |
Transient runtime state (visibility flags, wallpaper manager, layout) |
Visibilities |
modules/services/Visibilities.qml |
Per-screen UI visibility/layering manager |
Colors |
modules/theme/Colors.qml |
Dynamic color palette from matugen output |
Styling |
modules/theme/Styling.qml |
Shared style utilities: radius(), fontSize(), getStyledRectConfig() |
Anim |
modules/theme/Anim.qml |
Unified animation system: 12 profiles, duration(), easing(), spring helpers, listAddConfig, global speed scale, compositor sync |
Icons |
modules/theme/Icons.qml |
Phosphor-Bold icon font character map |
AxctlService |
modules/services/AxctlService.qml |
Compositor abstraction (focus, dispatch, state sync) |
PerMonitorConfig |
modules/services/PerMonitorConfig.qml |
Per-monitor config overrides (bar/notch/dock position) |
StateService |
modules/services/StateService.qml |
JSON persistence for session state (layout, presets) |
FocusGrabManager |
modules/services/FocusGrabManager.qml |
Input focus coordination for popups |
GradientCache |
modules/components/GradientCache.qml |
GPU texture sharing optimization for gradients |
| Component | File | Usage |
|---|---|---|
StyledRect |
modules/components/StyledRect.qml |
Base themed container (300+ usages). Supports gradient, halftone, border, shadow variants |
StateLayer |
modules/components/StateLayer.qml |
M3 interaction overlay: ripple + hover/press/focus state opacity |
Surface |
modules/components/Surface.qml |
M3 elevated surface wrapper (StyledRect + StateLayer). Elevation 0-4 mapping |
BarPopup |
modules/components/BarPopup.qml |
Popup anchored to bar items |
AnimatedBehavior |
modules/components/AnimatedBehavior.qml |
Reusable NumberAnimation that follows the active Anim profile |
AnimatedPopup |
modules/components/AnimatedPopup.qml |
Wrapper with opacity + scale entrance/exit animations |
AnimatedListView |
modules/components/AnimatedListView.qml |
ListView with unified add/remove/displaced/populate transitions |
SearchInput |
modules/components/SearchInput.qml |
Universal search field |
PaneRect |
modules/components/PaneRect.qml |
Pane variant container |
Always use one of these string values for the variant property:
"transparent", "bg", "popup", "internalbg", "barbg", "pane", "common", "focus", "primary", "primaryfocus", "overprimary", "secondary", "secondaryfocus", "oversecondary", "tertiary", "tertiaryfocus", "overtertiary", "error", "errorfocus", "overerror"
- Indentation: 4 spaces
- Imports: Use
qs.modules.<domain>namespace. Example:import qs.modules.services - Null safety: Always null-check nested properties. QML configs may be undefined during load.
- Async safety: Use
Qt.callLater()when modifying lists inside process handlers. - Raw JS objects: Results from
JSON.parse()have NO QML signals. Never use them inConnectionstargets. - Focus management: Never call
forceActiveFocus()directly on popups. UseFocusGrabManager.requestGrab(item)/releaseGrab(item). - Color resolution: Never hardcode colors. Use
Config.resolveColor(name)or bind toColors.*.
All UI motion must go through the unified animation system in Anim.qml.
-
Use
AnimatedBehaviorfor everyBehaviorwhen possible:Behavior on opacity { AnimatedBehavior { type: "standard"; size: "normal" } }
Valid
typevalues:"standard","emphasized","spatial","spring".
Validsizevalues:"small","normal","large","extraLarge"(standard);"fast","default","slow"(spatial);"small","normal","large"(emphasized/spring).
Optional:variant("enter" | "exit" | "expand" | "collapse"),useSpring: true,springName: "snappy" | "expressive",speedMultiplier. -
When
AnimatedBehavioris not enough, useAnim.duration(type, size)andAnim.easing(type, variant).type/.bezierCurve:NumberAnimation { duration: Anim.emphasizedNormal easing.type: Anim.easing("emphasized").type easing.bezierCurve: Anim.easing("emphasized").bezierCurve || [] }
-
List transitions: use
AnimatedListViewas a drop-in replacement forListView, or build transitions fromAnim.listAddConfig,Anim.listRemoveConfig,Anim.listDisplacedConfig. -
Popup/window open & close: animate
opacity+scaleviaAnimatedPopupor theBarPopup/OverviewPopup/PresetsPopuppattern (popupOpacity/popupScale+AnimatedBehavior). -
Floating windows (
FloatingWindow,SettingsWindow,MirrorWindow, etc.): the root element must remain the window type. Animate an inner container, not the window root. InitializepopupOpacity/popupScalefrom the global visibility state so the window is not transparent on first open:property real popupOpacity: GlobalStates.settingsWindowVisible ? 1.0 : 0.0 property real popupScale: GlobalStates.settingsWindowVisible ? 1.0 : 0.96 visible: popupOpacity > 0 || GlobalStates.settingsWindowVisible
Subsequent visibility changes should be driven by a
Connectionshandler that assigns target values (breaking the initial binding is acceptable). -
Game mode / reduced motion: every animation must respect
Anim.animationsEnabled.AnimatedBehaviordoes this automatically; manualBehaviorblocks must setenabled: Anim.animationsEnabled. -
Decorative long-loop animations (weather effects, marquee, disc rotation) may keep fixed durations, but their easing curves must still come from
Anim.easing("linear").
- Atomic defaults: Every new config key MUST have a corresponding entry in
config/defaults/<domain>.js. - Bulk updates: Wrap multi-property config changes in
Config.pauseAutoSave = true...Config.pauseAutoSave = false. - Bind to Config: UI elements should bind to
Config.<module>.<property>. Avoid local state for persistent settings. - Validation: Add type constraints in
ConfigValidator.jswhen introducing new config shapes.
- Hardcoding: NEVER hardcode colors, sizes, or durations. Use
Config.theme.*,Config.bar.*,Colors.*,Styling.*,Anim.*. - Direct Config Props: AVOID modifying
Configproperties directly outside theJsonAdapterbinding system. - Global Pollution: Do not add properties to
rootinshell.qml. UseGlobalStatesfor shared transient state. - Missing Defaults: NEVER add a config key without updating
config/defaults/*.js. - StyledRect bypass: NEVER create raw
Rectanglecontainers. UseStyledRectwith an appropriate variant. - Animation bypass: NEVER use raw
Easing.OutCubic,Easing.InOutQuad, or hardcodedduration: 200in UI animations. Route everything throughAnimorAnimatedBehavior. - Missing popup transitions: NEVER make a popup/window appear or disappear instantly. Add
opacity+scaleanimations. - Orphaned
Behavior: NEVER leave aBehaviorwithoutenabled: Anim.animationsEnabled(unless it usesAnimatedBehavior, which handles it internally). - Floating window animated with the wrong root: NEVER replace a
FloatingWindowroot with a genericItem/AnimatedPopup. KeepFloatingWindowas the root and animate inner content. Also NEVER startpopupOpacityat0unconditionally when the window may already be visible — initialize it from the visibility state.
All AI provider integrations live under modules/services/ai/strategies/ and MUST implement the ApiStrategy interface.
- Base helpers: use the shared helpers in
ApiStrategy.qml:formatMessages(messages)— converts internal messages with attachments to OpenAI-compatible content parts.formatTools(tools)— converts internal tool definitions to OpenAI function-calling format.normalizeChatEndpoint(base)— safely appends/v1/chat/completions.
- OpenAI-compatible providers MUST subclass
OpenAiCompatibleStrategyinstead of duplicating SSE/tool-call parsing. Currently used by: OpenAI, Mistral, Groq, DeepSeek. - DeepSeek specifics: set
supportsReasoning: trueandreasoningField: "reasoning_content"so the chat UI can display chain-of-thought andAi.qmlcan stream reasoning deltas. - Images in attachments: always use
att.base64+att.mimeType. The oldatt.urlfield does not exist in the attachment model. - Tool calls:
- Always propagate
tool_call_idfrom assistant responses into the follow-uprole: "tool"/role: "function"message. - For Anthropic/Gemini (no OpenAI-style IDs), continue using
nameas the identifier.
- Always propagate
- Agent connections: external agents connect via
AgentManager+AgentToolRegistry. Supported types:http-bridge— generic REST agent (OpenClaw gateway, Odysseus API, custom server). Discovers tools viaGET <endpoint>/tools, invokes viaPOST <endpoint>/invoke.command— stateless command wrapper; the binary receives a JSON payload as its last argument and prints a JSON response.
- Tool safety: shell commands are gated by
Config.ai.enabledTools(must include"shell") andConfig.ai.toolAllowlist. Empty allowlist means manual approval; non-empty allowlist +toolAutoApprove: trueruns allowlisted commands without confirmation. - New config keys for AI (e.g.
enabledTools,agents,toolAllowlist) MUST be added to bothconfig/defaults/ai.jsandconfig/Config.qmladapter.
- Lockscreen: Uses
WlSessionLock(Wayland secure session lock protocol) + PAM authentication viaQuickshell.Services.Pam. The PAM config is inconfig/pam/. - Credential storage: API keys (AI providers) are stored via
KeyStore.qmlwhich delegates to a Python script (scripts/keystore.py). No credentials are committed to the repo. - Process execution: QML spawns external processes via
Quickshell.Io.Process. All shell commands are constructed internally; no user input is passed directly to shell interpreters without sanitization. - Compositor config:
axctl.tomlis written to the user's data directory (~/.local/share/nothingless/). Theaxctldaemon runs as the user and communicates over IPC, not network sockets. - File paths: Avoid traversing outside
~/.config/nothingless/and~/.local/share/nothingless/for user-facing file operations.
- The version is stored in the plain-text file
versionat the repo root. flake.nixreads this file at evaluation time.
nix/packages/default.nixbuilds abuildEnvnamedNothingLess-${version}.- The launcher script (
nothingless) wrapscli.shwith:NOTHINGLESS_QSpointing to the Quickshell binaryQML2_IMPORT_PATH/QML_IMPORT_PATHset for Nix store Qt modulesFONTCONFIG_PATHfor bundled fonts
- Detects distro (Arch, Fedora, NixOS, or unknown)
- Installs dependencies via
pacman/yay(Arch),dnf(Fedora), ornix profile(NixOS) - Clones or updates the repo to
~/.local/src/nothingless - Builds Quickshell from source if not available (on unsupported distros)
- Creates
/usr/local/bin/nothinglesslauncher - Configures systemd services: disables
iwd, enablesNetworkManagerandbluetooth
- Template is in
.github/pull_request_template.md - Required: description of changes, screenshots for UI changes, behavior impact statement
- No CI/CD workflows are configured; reviewers rely on Nix flake evaluation and manual testing
| Task | Primary Location | Notes |
|---|---|---|
| Add config key | config/defaults/<domain>.js + config/Config.qml |
Both MUST be updated |
| Change bar layout | modules/bar/BarContent.qml |
Auto-hide, horizontal/vertical, widget groups |
| Change notch behavior | modules/notch/Notch.qml, modules/notch/NotchContent.qml |
StackView navigation, animations |
| Add AI provider | modules/services/ai/strategies/ |
Implement ApiStrategy interface |
| Theme / colors | modules/theme/Colors.qml, modules/theme/Styling.qml |
Watches ~/.cache/nothingless/colors.json |
| Animations (M3) | modules/theme/Anim.qml |
Standard / Emphasized / Spatial curves with global speed scale |
| Interaction states | modules/components/StateLayer.qml, modules/components/Surface.qml |
Ripple + M3 elevation surfaces |
| System monitoring | modules/services/SystemResources.qml |
Reads scripts/system_monitor.py JSON output |
| Clipboard | modules/services/ClipboardService.qml |
Interacts with scripts/clipboard_*.sh |
| Lockscreen | modules/lockscreen/LockScreen.qml |
WlSessionLockSurface + PAM |
| Screenshots | modules/tools/ScreenshotTool.qml |
Uses grim/slurp |
| Screen recording | modules/tools/ScreenrecordTool.qml |
Uses wf-recorder / gpu-screen-recorder |
| Add new widget/tab to dashboard | modules/widgets/dashboard/ |
Lazy-loaded LRU tabs |
| Overview / Mission Control | modules/widgets/overview/ |
Workspace window overview |
| Notifications | modules/notifications/ |
Popup system + history |
| Compositor settings | modules/services/CompositorConfig.qml |
Live apply to Hyprland via axctl |
Config.qmlis >3700 lines. Modify with extreme care; usepauseAutoSavefor bulk edits.- Large files (>1000 lines):
ClipboardTab,NotesTab,TmuxTab,BindsPanel,ShellPanel,PresetsTab,ThemePanel,LauncherView,AssistantTab,Ai.qml. - The
qs.import prefix is a Quickshell VFS construct, not a physical directory. screenshotToolModeinGlobalStates.qmlis DEPRECATED.- Gemini AI provider does not support the
systemrole; this is handled inmodules/services/ai/strategies/GeminiApiStrategy.qml. axctlis maintained in a separate repository (github:Leriart/axctl). When changes are made there, a manual build and install is required (the daemon runs in the user's session environment and cannot be tested by this agent directly).- Changelog entries for the project website are stored in a separate repo at
/home/adriano/Repos/Leriart/web/content/NothingLess/changelog/as Zola markdown files. Only write a changelog when explicitly asked.