Architecture
How iNiR is structured internally
Understanding how iNiR works under the hood. For developers, contributors, or the curious.
High-Level Overview
iNiR is a Quickshell configuration written in QML (Qt Modeling Language). It's not a standalone application - it runs inside Quickshell, which provides the Qt/QML runtime and Wayland integration.
User → Niri compositor → Quickshell runtime → iNiR (QML) → UI
Entry Point
File: shell.qml
This is where everything starts. It:
- Loads the config system
- Initializes services (singletons)
- Chooses which panel family to load (ii or waffle)
- Spawns the panel loaders
// Simplified
ShellRoot {
Config { id: config }
Loader {
source: config.panelFamily === "ii"
? "ShellIiPanels.qml"
: "ShellWafflePanels.qml"
}
}
Panel Families
Two distinct UI systems that share common components:
#ii Family (Material Design)
File: ShellIiPanels.qml
Loads panels conditionally using PanelLoader:
PanelLoader {
identifier: "iiBar"
extraCondition: !(Config.options?.bar?.vertical ?? false)
component: Bar {}
}
Panels only load if:
Config.readyis true- Identifier is in
enabledPanelsarray extraConditionis met (if specified)
Panels: iiBar, iiDock, iiSidebarLeft, iiSidebarRight, iiOverview, iiOverlay, iiBackground, iiNotificationPopup, iiMediaControls, iiClipboard, iiLock, iiSessionScreen, etc.
#waffle Family (Windows 11)
File: ShellWafflePanels.qml
Similar structure but different panels:
Panels: wBar (taskbar), wStartMenu, wActionCenter, wNotificationCenter, wWidgets, wTaskView, wBackground, wLock, etc.
Directory Structure
ii/
├── shell.qml # Entry point
├── ShellIiPanels.qml # ii panel loaders
├── ShellWafflePanels.qml # waffle panel loaders
├── GlobalStates.qml # UI state singleton
├── VERSION # Version string
├── CHANGELOG.md # Release history
├── defaults/config.json # Default config
│
├── modules/
│ ├── common/ # Shared across families
│ │ ├── Config.qml # Config system (singleton)
│ │ ├── Appearance.qml # Theming (singleton)
│ │ ├── Directories.qml # Paths (singleton)
│ │ ├── Persistent.qml # Runtime state (singleton)
│ │ ├── widgets/ # Reusable UI components
│ │ ├── functions/ # Utilities (StringUtils, etc.)
│ │ └── models/ # Data models
│ │
│ ├── bar/ # Top bar (ii)
│ ├── dock/ # Dock (ii)
│ ├── sidebarLeft/ # Left sidebar (ii)
│ ├── sidebarRight/ # Right sidebar (ii)
│ ├── overview/ # Workspace overview
│ ├── mediaControls/ # Media player overlay
│ ├── settings/ # Settings window
│ ├── notifications/ # Notification system
│ └── waffle/ # Windows 11 components
│
├── services/ # ~59 singletons
│ ├── Audio.qml # Audio control
│ ├── NiriService.qml # Niri IPC
│ ├── MprisController.qml # Media players
│ ├── Notifications.qml # Notification backend
│ ├── ThemeService.qml # Theme management
│ ├── GameMode.qml # Performance mode
│ └── ...
│
├── scripts/ # Shell scripts (Fish/Python)
│ ├── colors/ # Theming scripts
│ ├── setup/ # Installation
│ └── utils/ # Utilities
│
└── docs/ # Documentation
Config System
File: modules/common/Config.qml
Singleton that manages configuration. Uses Qt's JsonAdapter to sync between QML properties and JSON file.
#How it works
- Schema definition (Config.qml):
JsonAdapter {
property string panelFamily: "ii"
property list<string> enabledPanels: ["iiBar", "iiDock"]
property var appearance: ({
globalStyle: "material",
theme: "wallpaper"
})
}
-
File location:
~/.config/illogical-impulse/config.json -
Reading config (always null-safe):
Config.options?.bar?.vertical ?? false
- Writing config (must use setNestedValue):
Config.setNestedValue("bar.vertical", true)
Critical: Direct assignment doesn't persist. Always use setNestedValue.
#Config sync
Every option must exist in both:
Config.qmlschema (runtime)defaults/config.json(fresh install default)
If out of sync, fresh installs get different defaults than existing users.
Services (Singletons)
Services are QML singletons that provide system integration and shared state.
Examples:
#Audio.qml
- Wraps PipeWire/PulseAudio
- Exposes volume, mute state
- Emits signals on changes
#NiriService.qml
- Communicates with Niri compositor via IPC
- Provides window list, workspace info
- Handles Niri-specific features
#MprisController.qml
- Manages media players (MPRIS protocol)
- Deduplicates players by title/position
- Provides
displayPlayers(use this, notplayers)
#ThemeService.qml
- Applies themes (wallpaper-based or presets)
- Calls matugen to generate colors
- Updates terminal themes
#GameMode.qml
- Detects fullscreen apps
- Auto-disables effects/animations
- Can be toggled manually
Theming System
#Flow
- User changes wallpaper or selects preset
ThemeService.applyCurrentTheme()calledmatugengenerates Material You colors- Colors saved to
~/.local/state/quickshell/user/generated/colors.json MaterialThemeLoaderloads colors intoAppearance.m3colors- Terminal themes generated (foot, kitty, alacritty, etc.)
- GTK/Qt themes applied via matugen templates
#Design Tokens
Never hardcode values. Always use design tokens:
// ❌ Wrong
color: "#1a1a1a"
radius: 12
font.pixelSize: 16
// ✅ Correct
color: Appearance.colors.colLayer1
radius: Appearance.rounding.normal
font.pixelSize: Appearance.font.pixelSize.normal
#Five Global Styles
All ii components must support all five:
color: Appearance.inirEverywhere ? Appearance.inir.colLayer1
: Appearance.auroraEverywhere ? Appearance.aurora.colSubSurface
: Appearance.colors.colLayer1
- material: Solid backgrounds, Material Design
- cards: Subtle shadows, elevated surfaces
- aurora: Glass blur, translucent
- inir: TUI-inspired, minimal
- angel: Neo-brutalism glass
IPC System
Keybinds call shell functions via IPC.
#How it works
- Define IPC handler in QML:
IpcHandler {
target: "overview"
function toggle(): void { // Must have return type
GlobalStates.overviewOpen = !GlobalStates.overviewOpen
}
}
- Call from Niri config:
bind "Mod+Space" {
spawn "qs" "-c" "ii" "ipc" "call" "overview" "toggle";
}
- Or from terminal:
qs -c ii ipc call overview toggle
Critical: IPC functions MUST have explicit return type (: void, : string, etc.) or they won't register.
State Management
#GlobalStates.qml
Singleton for UI state that needs to be shared across components:
QtObject {
property bool barOpen: true
property bool sidebarLeftOpen: false
property bool overviewOpen: false
property bool screenLocked: false
// etc.
}
#Persistent.qml
Runtime state that survives hot-reloads but not restarts:
QtObject {
property var lastUsedTheme: null
property var cachedData: ({})
}
Hot-Reload vs Restart
Hot-reload works for:
- UI changes within existing components
- Config value changes
- Non-singleton QML files
Restart required for:
- Singleton changes (services, Config, Appearance)
- shell.qml changes
- Panel loader changes
- New modules added
- Config schema changes
Restart command:
qs kill -c ii; qs -c ii
Common Patterns
#Lazy Loading
LazyLoader {
active: GlobalStates.sidebarRightOpen && Config.ready
component: SidebarRight {}
}
Component only loads when active is true.
#Null-Safe Config Access
// Always use optional chaining + default
property string value: Config.options?.module?.option ?? "default"
#Animations
Behavior on opacity {
enabled: Appearance.animationsEnabled // Respects GameMode
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
#Process Commands
// Must use absolute paths
Process {
command: ["/usr/bin/fish", Directories.scriptPath + "/script.fish"]
}
Widget Property Names
Critical - wrong name = silent failure:
| Widget | Icon Property | NOT |
|---|---|---|
| ConfigSwitch | buttonIcon | |
| ConfigSpinBox | icon | |
| MaterialSymbol | text |
Performance Considerations
#Memory Usage
- Full shell: ~400MB
- Minimal (bar + background): ~80MB
- Each panel: ~20-60MB
- AI Chat: ~100MB
- Video wallpaper: ~150MB
#Optimization
- Disable unused panels in
enabledPanels - Use Material or iNiR style (no blur)
- Enable GameMode for auto-optimization
- Reduce animations in performance settings
- Disable heavy modules (AI chat, video wallpaper)
#GameMode Integration
When fullscreen app detected:
Appearance.effectsEnabled→ false (disables blur)Appearance.animationsEnabled→ false (disables animations)Appearance.gameModeMinimal→ true (hides non-essential UI)
Development Workflow
#Making Changes
- Edit QML files
- Changes hot-reload automatically (if not singleton)
- Check logs:
qs log -c ii -f - Test feature
- Restart if needed:
qs kill -c ii; qs -c ii
#Debugging
# View logs
qs log -c ii
# Follow logs live
qs log -c ii -f
# Check for errors
qs log -c ii | grep -iE "error|ReferenceError|TypeError"
# Test IPC
qs -c ii ipc show
qs -c ii ipc call overview toggle
#Common Issues
Config not saving:
- Using direct assignment instead of
setNestedValue
IPC not working:
- Missing return type on function
Crash on config read:
- Missing optional chaining (
?.)
Singleton not updating:
- Need to restart, hot-reload doesn't work for singletons
See Also
- Contributing - How to contribute code
- Config Options - All config options
- IPC Commands - All IPC targets
- CLAUDE.md - Complete technical reference