Just ask! You don't need to run any special command. Simply say "update agents.md with..." and the agent will update this file directly.
CRITICAL: AI agents should NEVER run quickshell directly.
The user is ALWAYS running QuickShell with logs output to /home/nick/.config/dots/quickshell.log.
Because QuickShell is already running, you never need to start it yourself. Simply read the log file to see current output.
When debugging quickshell issues:
- Read the log file at
/home/nick/.config/dots/quickshell.log - Make code changes as needed
- The code changes are automatically applied
- Read the updated logs
The user captures quickshell output to a file for analysis:
quickshell -c nick 2>&1 | tee ~/.config/dots/quickshell.logThis allows:
- Real-time viewing in terminal
- Complete output capture including DEBUG, WARN, ERROR messages
- File accessible to AI agents via Read tool at
/home/nick/.config/dots/quickshell.log - Preserves ANSI color codes for readability
Use console.log() liberally during development:
Process {
id: myProcess
command: ["some", "command"]
onRunningChanged: {
if (running) {
console.log("Starting process with args:", command);
}
}
onExited: (code) => {
console.log("Process exited with code:", code);
}
}Important: Use arrow function syntax for signal handlers to avoid QML warnings:
- ✅ Good:
onExited: (code) => { ... } - ❌ Bad:
onExited: { console.log(exitCode); }(parameter injection warning)
When a Process fails silently:
-
Add logging at each stage:
Process { onRunningChanged: { if (running) console.log("Process starting..."); } stdout: SplitParser { onRead: data => { console.log("stdout:", data); } } stderr: SplitParser { onRead: data => { console.log("stderr:", data); } } onExited: (code) => { console.log("Exit code:", code); } }
-
Test command directly in terminal:
- Verify command works standalone
- Check exit codes
- Ensure full paths to binaries if needed
-
Common gotchas:
- Wrong command syntax for the tool
- Command uses different ID format than expected
- Need full path to binary (e.g.,
/etc/profiles/per-user/nick/bin/pulsemixer) - Shell features not available (pipes, redirects) without wrapping in bash -c
When parsing command output:
-
Log the raw buffer:
onExited: { console.log("Raw buffer:", buffer); // Then parse... }
-
Check for missing newlines:
- SplitParser without splitMarker may concatenate output
- Commands may not separate lines as expected
- Solution: Split by regex patterns instead of
\n
-
Example: Parsing concatenated output
// Bad: Assumes newlines exist let lines = buffer.split('\n'); // Good: Split by known delimiter in data let entries = buffer.split(/Sink:\s+/).filter(s => s.trim());
Use Hyprland.activeToplevel instead of Hyprland.focusedWindow for reactive window tracking:
readonly property int activeSpecialWsId: {
let wsId = Hyprland.activeToplevel?.workspace?.id ?? 0;
return (wsId < 0) ? wsId : 0;
}Process commands are evaluated when running becomes true, so you can reference reactive properties:
Process {
id: myProcess
// Command evaluated when running is set to true
command: ["wpctl", "set-default", sinkItem.modelData.id]
}
MouseArea {
onClicked: {
myProcess.running = true; // Command built here with current values
}
}For commands with multi-line output:
Process {
property string buffer: ""
onRunningChanged: {
if (running) buffer = ""; // Clear on start
}
stdout: SplitParser {
onRead: data => {
buffer += data;
}
}
onExited: {
// Parse complete buffer
}
}Different audio tools use different ID formats:
-
wpctl: Numeric IDs (48, 90, 100)
- Get sinks:
wpctl status | grep -A 100 'Sinks:' - Set default:
wpctl set-default 90
- Get sinks:
-
pulsemixer: String IDs (sink-4591)
- Get sinks:
pulsemixer --list-sinks - No built-in set-default command
- Use for getting volume:
pulsemixer --get-volume - Use for setting volume:
pulsemixer --change-volume +5
- Get sinks:
-
pactl: May not be available in all environments
Decision: Use wpctl for defaults, pulsemixer for volume control.
IMPORTANT: Always format QML files after editing using qmlformat:
qmlformat -i /path/to/file.qmlThe formatter is available via the qt6.qtdeclarative package which is installed with quickshell. Always run it after making changes to ensure consistent code style.
CRITICAL: The theme.nix file contains colors and configuration for the ENTIRE dots repository, not just quickshell. Changes here affect:
- Quickshell UI
- Hyprland colors
- Terminal colors (Ghostty, Alacritty, etc.)
- GTK/Qt themes
- macOS Aerospace/JankyBorders
- Any other application that reads from the theme config
The theme uses Tokyo Night color scheme with ANSI colors only (base16 colors have been removed):
theme.colors = {
background = "#1a1b26";
foreground = "#c0caf5";
# Standard ANSI colors
default = {
black = "#1b1d2b";
red = "#ff757f";
green = "#c3e88d";
yellow = "#ffc777";
blue = "#82aaff";
magenta = "#c099ff";
cyan = "#86e1fc";
white = "#828bb8";
};
bright = {
black = "#444a73";
red = "#ff757f";
green = "#c3e88d";
yellow = "#ffc777";
blue = "#82aaff";
magenta = "#c099ff";
cyan = "#86e1fc";
white = "#c8d3f5";
};
# Custom indexed colors for special use cases
indexed = {
one = "#ff966c"; # Orange
two = "#c53b53"; # Dark red
three = "#ffb3d9"; # Pastel retrowave pink
four = "#2f3549"; # Mid-tone background (for buttons, hover states)
};
};Migration Complete: All base16 color references have been migrated to ANSI equivalents across:
- GTK theme (
modules/home-manager/gtk.nix) - Qt theme (
modules/home-manager/qt.nix) - macOS Aerospace borders (
modules/darwin/aerospace.nix)
Quickshell reads colors from a JSON file generated by Nix at ~/.config/quickshell/theme-colors.json. The mapping is defined in quickshell.nix:
themeColors = builtins.toJSON {
# Semantic color names mapped to ANSI colors
primary = config.theme.colors.default.magenta; # Purple #c099ff
secondary = config.theme.colors.indexed.three; # Light pink #ffb3d9
tertiary = config.theme.colors.indexed.one; # Orange #ff966c
quaternary = config.theme.colors.default.yellow; # Yellow #ffc777
quinary = config.theme.colors.default.green; # Green #c3e88d
# Base colors
background = config.theme.colors.background;
foreground = config.theme.colors.foreground;
surface = config.theme.colors.default.black;
surfaceContainer = "#2f334f"; # Middle ground between default.black and bright.black
textOnBackground = config.theme.colors.foreground;
textOnSurface = config.theme.colors.bright.white;
textOnPrimary = config.theme.colors.background;
outline = config.theme.colors.bright.black;
shadow = config.theme.colors.default.black;
};Color Usage Philosophy:
- Use ONLY colors from the theme system - no hardcoded colors
- Use semantic names (primary, secondary, etc.) not specific colors
- Match retrowave/Tokyo Night aesthetic with pastels and neons
Current Color Assignments:
- CPU bars: Primary (purple)
- GPU bars: Tertiary (orange)
- RAM bars: Quaternary (yellow)
- Disk bars: Quinary (green)
- Clock/Volume: Secondary (light pink)
- Weather icon: Primary
- Weather condition: Secondary
- All bar icons/text match their bar colors
Colors are loaded from the JSON file via a singleton:
import "../../services"
Text {
color: Colours.primary // Use semantic names
}After changing theme.nix, rebuild with:
sudo nixos-rebuild switch --flake .#meraxesnick/
├── components/ # Reusable UI components
│ ├── workspaces/ # Workspace indicator (with fixed animations!)
│ └── Volume.qml # Volume widget with device detection
├── config/ # Theme and appearance
│ └── Appearance.qml # Font sizes, spacing, animation durations
├── modules/
│ └── bar/ # Main bar components
│ ├── Bar.qml # Bar content layout
│ ├── BarWindow.qml # Window management
│ ├── Menu.qml # Calendar, weather, performance stats, media controls
│ ├── VolumePopout.qml # Audio device selector
│ └── TrayPopout.qml # System tray menus
├── services/ # Singletons
│ ├── Colours.qml # Theme color singleton
│ ├── SystemUsage.qml # CPU/GPU/RAM/Disk monitoring
│ ├── Weather.qml # Weather data
│ └── qmldir # Service registration
└── shell.qml # Entry point
Components in subdirectories need qmldir files:
# modules/bar/qmldir
module bar
BarWindow 1.0 BarWindow.qml
VolumePopout 1.0 VolumePopout.qml
Without this, components won't be available even if imported.
- Fixed animation stuttering - Root cause was pill using
activeItem.height(animated) instead ofimplicitHeight(target) - Center-based positioning, simplified from ~100 lines to ~35 lines
- All animations 200ms with standard easing
- 15px workspace spacing
- Color fade animations on workspace text
- Increased icon/text sizes (icon: 32px, text: 18px)
- Device detection with icons (headset, bluetooth, soundbar, steam link, HDMI)
- Device icon positioned above percentage
- Hover effects match system tray
- Interactive Nix button with scale animation and color inversion
- Keyboard navigation (arrows, enter, escape) for power buttons
- 5 power buttons: Lock, Suspend, Logout, Restart, Power
- Calendar with month navigation that resets to current month on open
- Weather widget (40% width)
- Performance stats widget (60% width) with CPU/GPU/RAM/Disk bars
- Now playing media controls with album art
- 40px icon size with 4px spacing
- Rounded corner hover backgrounds (
Qt.alpha(Colours.textOnBackground, 0.08)) - Proper click handling with z-ordering fix
- Monitors CPU, GPU (NVIDIA/AMD/Intel), RAM, Disk usage and temps
- Uses
StdioCollectorwithonStreamFinishedpattern (from Caelestia) - Automatic GPU type detection
- Temperature fallback to
/sys/class/thermal/when sensors unavailable - Updates every 3 seconds
For pills/indicators that should appear in place:
Rectangle {
opacity: isActive ? 1 : 0
scale: isActive ? 1 : 0.8
visible: opacity > 0
// Instant position changes
Behavior on y { enabled: false }
Behavior on height { enabled: false }
// Smooth fade/scale
Behavior on opacity {
Anim { duration: Appearance.anim.small }
}
Behavior on scale {
Anim { duration: Appearance.anim.small }
}
}Store last valid position to avoid jumps during fade-out:
Rectangle {
property real lastY: 0
property var activeItem: null
onActiveItemChanged: {
if (activeItem !== null) {
lastY = activeItem.y;
}
}
y: lastY // Stays at last position even when activeItem becomes null
opacity: activeItem !== null ? 1 : 0
}