-
-
Notifications
You must be signed in to change notification settings - Fork 62
RUNTIME
What happens between "user logs in" and "shell is on screen", step by step.
User logs in
|
Display manager starts Niri (or Hyprland)
|
Compositor reaches graphical-session.target
|
systemd starts inir.service (wants link from compositor)
|
ExecStart calls: /usr/bin/inir run --session
|
inir script (bash):
- Validates QS/Qt ABI compatibility
- Sets QT_SCALE_FACTOR=1
- Suppresses noisy Qt log categories
- Bridges niri env vars to systemd session
- Launches: qs -c inir
|
Quickshell loads shell.qml
|
ShellRoot initialization:
1. Force-instantiate Idle and PowerProfilePersistence
2. Load FirstRunExperience (checks if first run)
3. Load ConflictKiller (kills conflicting trays/notification daemons)
4. Wait for Config.ready
|
Config.ready fires:
1. Apply current theme (ThemeService.applyCurrentTheme)
2. Initialize icon theme
3. Migrate enabledPanels if needed
4. Start shell entry timer (200ms delay for animation)
5. Schedule deferred init (500ms for non-critical services)
|
Panel loading:
- Selects ShellIiPanels or ShellWafflePanels based on panelFamily
- Each PanelLoader activates when its conditions are met
- Immediate panels load first (bar, background, OSD)
- Deferred panels load after GlobalStates.deferredPanelsReady
|
Shell entry animation completes
|
Deferred services load (GameMode, Weather, etc.)
|
Shell fully operational
The systemd service is the key piece. It does not use systemctl enable in the traditional sense because there's no [Install] section. Instead, iNiR creates a wants link from your compositor's service:
Niri: ~/.config/systemd/user/niri.service.wants/inir.service
Hyprland: ~/.config/systemd/user/wayland-wm@Hyprland.service.wants/inir.service
This means iNiR starts when your compositor starts and stops when it stops. It will never accidentally start under KDE or GNOME.
Managing the link:
inir service enable # create the wants link
inir service disable # remove it
inir service status # check current statescripts/inir is a 2400+ line bash script that wraps Quickshell. It's not the same as running qs -c inir directly:
inir run |
qs -c inir |
|
|---|---|---|
| Environment setup | Sets QT_SCALE_FACTOR, suppresses warnings, bridges niri env | Raw environment |
| Output | Backgrounded, logs to journal | Foreground, direct stdout |
| Crash recovery | systemd restarts on failure (max 3 in 30s) | None |
| ABI check | Validates Quickshell/Qt compatibility | None |
| Orphan cleanup | ExecStopPost cleans stale runtime | None |
For development and debugging, qs -c inir (direct mode) is usually better because you get stdout immediately. For daily use, the systemd service handles everything.
The launcher sets these before starting Quickshell:
| Variable | Value | Why |
|---|---|---|
QT_SCALE_FACTOR |
1 |
Shell handles its own scaling in QML |
QT_LOGGING_RULES |
(long list) | Suppress known-harmless Qt/QML warnings |
The --session flag (used by systemd) also runs ensure_systemd_graphical_env in the background, which bridges critical Niri environment variables to the systemd user session. Without this, apps launched from the shell wouldn't get WAYLAND_DISPLAY, NIRI_SOCKET, or ELECTRON_OZONE_PLATFORM_HINT.
Config.qml uses Quickshell's FileView to read the user's JSON config file. The loading sequence:
- FileView reads
~/.config/illogical-impulse/config.json - JsonAdapter parses the content
- Schema properties bind to parsed values (with fallbacks)
-
Config.readybecomes true - Everything that was waiting on config starts loading
If the config file doesn't exist (fresh install), Config creates it from defaults/config.json.
Hot-reload: if you edit config.json externally, FileView detects the change and re-parses within 50ms.
Each panel is wrapped in a PanelLoader:
PanelLoader {
identifier: "iiBar"
extraCondition: !(Config.options?.bar?.vertical ?? false)
component: Bar {}
}A panel loads when all three conditions are true:
-
Config.readyis true - The identifier exists in
Config.options.enabledPanels -
extraConditionevaluates to true
Panels are split into immediate (load at first frame) and deferred (load after deferredPanelsReady):
Immediate: bar, background, notification popup, OSD. These need to be visible right away.
Deferred: sidebars, overview, clipboard, lock screen, cheatsheet. These load after the shell is already on screen and responsive.
The systemd service has:
-
Restart=on-failurewithStartLimitBurst=3andStartLimitIntervalSec=30 - If iNiR crashes, systemd restarts it (up to 3 times in 30 seconds)
-
ExecStopPostrunsinir cleanup-orphansto clear stale Quickshell runtime entries - Exit code 143 (SIGTERM) is treated as success, not failure
Non-critical services load 500ms after the first frame to reduce boot contention:
- GameMode (fullscreen detection)
- WindowPreviewService (alt-tab previews)
- Weather (API polling)
- VoiceSearch (Gemini transcription)
- FontSyncService (GTK/KDE font sync)
- Hyprsunset (night light)
This keeps the initial frame fast. The bar and background appear immediately, everything else fills in shortly after.
If the shell won't start:
# Direct stdout (bypass systemd)
qs -c inir
# Verbose internal logging
qs -v -c inir
# Extra verbose
qs -vv -c inir
# Debug-level service logging
QS_DEBUG=1 qs -c inirCheck inir logs for recent journal output, or inir doctor for automated diagnostics.