A widget-engine desktop shell. A configurable bottom bar, popup windows,
toast command-runners with dialog-tree pipelines, an interactive floating
terminal, a StatusNotifierItem system tray, and a built-in
org.freedesktop.Notifications daemon — all driven by one Python
process on top of GTK 3 + PyGObject.
Styled out of the box with the INDIGO Cyberpunk palette: hot magenta, electric cyan, neon yellow, violet accent, deep blue-violet base.
- Bar — a transparent dock-strut at the screen edge with declarative
widget composition (
Box([…]),Spacer()). - Widgets — workspaces (urgent ring blink), system stat meters (CPU / RAM / temp), volume, network, media (cava equalizer background + beat-pulse), clock with battery underline, scrambling/beat-syncing lyrics, identity tag with pulsing corner brackets, SNI system tray.
- Popups — terminal-hosted popups (
fastfetch,sptlrx,spotify-player,nmtui), a network panel, hardware panel (CPU/RAM history, GPU live readouts), calendar, systray panel. - Interactive terminal —
indigoshell open terminalopens a top-right floating$SHELLpopup with the bar's theme; close it withexit/ Ctrl-D / mod+q. Single keybind to summon a full terminal that's always-on-top and tracks you across workspaces. - Chord menus — modal, seat-grabbed popups bound to keyboard chords
(
power-menu,display-menu,layout-menu,profile-menu,envy-menu). Press-to-arm, release-to-fire; unmapped keys flash red. - Toasts —
Daemon.toast(argv)spawns a one-shot top-right TermToast that runsargv, auto-grows its VTE to fit the output, then runs a perimeter-trace countdown animation and auto-closes (paused on hover). Used from menu actions (from .api import toast) for visible feedback on system changes (envycontrol -s …, etc). - Dialog pipelines — multi-stage cascades stacked top-right. Each
stage is a registered script that prints output (visible in a
TermToast) and writes a JSON manifest declaring the next-stage
options. The orchestrator routes 0-options → leaf-with-linger,
1-option → auto-advance, N-options → chord menu. Picked rows stay
lit as visual history; escape pulls a cancel branch if the script
declared one. See
helpers/flows/display/for the included example. - Notifications — full D-Bus notification daemon. Replaces dunst.
Renders toasts in the bar's visual language (corner brackets,
segmented progress meters when
valuehints arrive), supports actions, images, urgency styling, replace-by-id. - Theme — every color, spacing, font size, and per-widget preset
lives in one
theme.py. Includes 16-color ANSI palettes for the embedded VTE terminals and aNEWT_COLORSblock fornmtuiso they all match the bar. - Hot reload —
--watchre-execs the process on.pychange; or callindigoshell reloadover IPC.
indigoshell/
app.py ─ entry point (daemon vs client mode)
api.py ─ public helpers used in user config
(toggle, open_window, close_window, toast)
theme.py ─ palette, semantic tokens, per-widget presets,
terminal + newt + toast tokens
style.py ─ Style dataclass, CSS builder, hex helpers
config_default.py ─ default bar+windows+scripts+pipelines config
core/
daemon.py ─ GTK main loop, window registry, store, IPC,
toast(), start_pipeline(), pipelines routing
pipeline.py ─ dialog-tree orchestrator (cascade lifecycle,
manifest parsing, auto-grow reflow)
ipc.py ─ unix-socket command server
registry.py ─ merges built-in + user WindowKinds
client.py ─ CLI side of IPC
paths.py, store.py, singleton.py
helpers/
layout.py, profile.py, power.py
flows/ ─ dialog-tree pipeline definitions
display/ ─ pick-one-monitor pipeline
__init__.py ─ exports SCRIPTS + PIPELINES
query.py ─ list connected outputs
set.py ─ apply xrandr (or "cancel")
services/
proc.py ─ unified subprocess (run/fire/popen/subscribe)
sysinfo.py ─ 1Hz CPU/RAM sampler + rolling history,
direct-sysfs temperature
beat.py ─ aubio beat detector + cava bands broker
(lazy: only spawns when subscribed)
music.py ─ per-player playerctl status broker
notifications.py ─ org.freedesktop.Notifications D-Bus server
systray.py ─ org.kde.StatusNotifierWatcher + Host
dbusmenu.py ─ com.canonical.dbusmenu client
text_effects.py ─ scramble + other arrival animations
widgets/ ─ bar widgets
base.py ─ Widget + paint() / beveled_path() /
beveled_polyline() / stroke_partial()
layout.py ─ Box, Spacer
workspaces.py, systag.py, clock.py, volume.py, network.py, media.py,
stat_meter.py, battery_meter.py, stdout_text.py, network_panel.py,
hardware_panel.py, calendar.py, terminal.py, notification.py,
menu.py, systray.py, hud.py, line_graph.py, bar_meter.py,
term_toast.py ─ Toast / interactive shell (auto-grow VTE)
windows/ ─ top-level window kinds
base.py ─ WindowKind abstract class
bar.py ─ the bar itself
popup.py ─ click-anchored transient popups + PopupKind
(grab, glow, blur, bevel, type_hint)
notification.py ─ floating notification stack (bottom-right)
- All Cairo color painting goes through
paint(cr, hex_color, alpha=None)(widgets/base.py). - Perimeter geometry helpers
beveled_path,beveled_polyline,stroke_partialare shared by notifications, toasts, and menus. - All subprocess work goes through
services/proc.py:proc.run(cmd)— capture stdout, swallow missing-binary / timeoutproc.fire(cmd, *, detach=False)— fire-and-forgetproc.popen(cmd, *, text=False, bufsize=-1)— raw spawnproc.subscribe(cmd, on_line, ...)— line-streaming with reader
- Widgets center vertically by default in the bar; override
valign/vexpandif a draw widget should fill bar height (Media does this for its cava background). - Widget styling: every widget accepts
style=,hover_style=,active_style=, andchild_styles={}— overrides are CSS rules scoped to the widget's generated id. The theme file ships defaults.
indigoshell looks for a user config in:
~/.config/indigoshell/config.py./config.py(cwd)
A user config exports BAR = {...} with these keys:
BAR = {
"widgets": [...], # bar layout
"windows": WINDOWS, # name → WindowKind (PopupKind etc.)
"scripts": SCRIPTS, # name → script path (dialog flows)
"pipelines": PIPELINES, # name → initial command argv
}See indigoshell/config_default.py
for the shape and the available widgets.
A flow is a directory under helpers/flows/ with an __init__.py
that exports two dicts:
# helpers/flows/myflow/__init__.py
from pathlib import Path
_HERE = Path(__file__).parent
SCRIPTS = {
"myflow_step_a": str(_HERE / "step_a.py"),
"myflow_step_b": str(_HERE / "step_b.py"),
}
PIPELINES = {
"myflow-menu": ["myflow_step_a"], # entry-point
}In config_default.py, add the flow to _FLOWS. Then bind your WM to
indigoshell open myflow-menu and the first script runs.
Every dialog script:
-
Prints anything to stdout — visible in its TermToast.
-
Writes a JSON manifest to
$INDIGOSHELL_MANIFEST:{ "options": [ {"label": "First choice", "command": ["myflow_step_b", "arg1"]}, {"label": "Second choice", "command": ["myflow_step_b", "arg2"]} ], "cancel": {"command": ["myflow_step_b", "cancel"]} } -
Exits.
The orchestrator reads the manifest on child-exit and:
- 0 options → leaf node, linger trace fires, cascade closes
- 1 option → auto-advance, no menu, new toast stacked below
- N options → chord Menu below the cascade; user picks → next stage
- escape on menu → runs the
cancel.commandas the next stage if declared, otherwise the keypress is swallowed (flow menus never silently dismiss themselves)
Scripts can be any executable. SCRIPTS values may be a str path
(run under the daemon's Python) or a list[str] argv prefix (treated
as-is); the orchestrator appends the rest of the manifest command
list as argv.
# Install as a uv tool (editable)
uv tool install -e .
# Run the daemon
indigoshell # foreground
indigoshell --watch # re-exec on .py change
# Client commands
indigoshell open <window-name> # also triggers a pipeline if `name`
# is a registered pipeline entry
indigoshell close <window-name>
indigoshell toggle <window-name>
indigoshell list # registered kinds + open instances
indigoshell ping # health check
indigoshell reload # re-exec daemon in place
indigoshell kill # tear down + exit the daemonindigoshell claims org.freedesktop.Notifications on the session bus
on startup. To use it, stop and mask any existing notification daemon:
systemctl --user stop dunst.service
systemctl --user mask dunst.serviceTest:
notify-send "Hello" "from indigoshell"
notify-send -u critical "Critical" "stays until clicked"
notify-send -h int:value:65 "Download" "stable.iso — 65%"
notify-send --action="reply=Reply" --action="archive=Archive" "Message"from indigoshell.api import toast
# Use as a menu/click handler:
MenuItem("1", "PERF", toast(["pkexec", "envycontrol", "-s", "nvidia"]))
# Or imperatively from anywhere with a daemon handle:
get_daemon().toast(["ping", "-c", "10", "1.1.1.1"], cols=80, rows=4,
linger_ms=5000)The TermToast starts compact and grows its VTE rows as output writes
past the visible area (capped at max_rows). When the child exits, a
cyan perimeter trace runs around the popup border (paused on hover);
trace completion closes the popup.
indigoshell registers as org.kde.StatusNotifierWatcher + Host on
startup. Compatible with nm-applet --indicator, blueman-applet,
udiskie --tray, Discord/Spotify/Steam, etc. Pair the bar Systray
widget for the tray icon row; the systray-panel popup expands into
a per-item list with hover tooltips and right-click context menus
(via com.canonical.dbusmenu).
System:
- GTK 3, PyGObject (3.56+)
- VTE 2.91 (embedded terminals)
pactl,pulseaudio(volume)nmcli(network)cava(visualizer background)parec+ Pythonaubio(beat detection)playerctl(media status)psutil,python-xlib,watchdog(Python deps; declared inpyproject.toml)
Optional:
sptlrx(synced lyrics)spotify_player(terminal music player)fastfetch(system info popup)nmtui,envycontrol,pkexec(display / GPU flows)- A Nerd Font for the glyphs (
FiraCode Nerd Font Monoships in the default theme).
Personal project, single-author. Stable for daily-driver use; the API may still change.