Add [tanin] repo build script; build 5 own packages + DB

- packaging/build-tanin-repo.sh: builds own pkgs + repo-add DB (-> ~/projects/tanin-repo)
- built: tanin-greet, tanin-libadwaita, tanin-setup, tanin-eww, tanin-desktop
- manifest: cliphist + cage are in extra (not AUR rebuilds); only eww/tiramisu/
  waypaper need rebuilding into [tanin]; record repo-build status

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:16:43 +02:00
parent a4569795e8
commit 857bd955c5
65 changed files with 12486 additions and 7 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,34 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = tanin-eww
pkgbase = tanin-eww
xdata = pkgtype=pkg
pkgver = 0.1.0-1
pkgdesc = TANINUX desktop shell — eww bar/dock/panels/spotlight + niri config (skel)
url = https://taninux.kgva.ch
builddate = 1781813719
packager = Unknown Packager
size = 188322
arch = any
license = GPL-3.0-or-later
depend = niri
depend = eww
depend = jq
depend = wl-clipboard
depend = tiramisu
depend = waypaper
depend = cliphist
depend = brightnessctl
depend = playerctl
depend = libqalculate
depend = grim
depend = slurp
depend = networkmanager
depend = bluez-utils
depend = wireplumber
depend = ttf-iosevka-nerd
depend = inter-font
depend = papirus-icon-theme
optdepend = wlsunset: night light (niri autostart)
optdepend = nm-applet: tray network applet (niri autostart)
optdepend = waybar: fallback bar if eww is missing
@@ -0,0 +1,5 @@
librewolf|librewolf|librewolf
nautilus|org.gnome.Nautilus|org.gnome.Nautilus
kitty|kitty|kitty
code|Code|code-oss
obsidian|obsidian|obsidian
@@ -0,0 +1,721 @@
// ===========================================================================
// eww theme — Mountain / "Fuji" palette, monochrome + soft violet accent
// Base bg/fg are the Fuji values; accent is Fuji's (bright) violet.
// ===========================================================================
$bg: #0f0f0f;
$bg-alt: #191919;
$bg-hi: #242424;
$fg: #f0f0f0;
$muted: #8a8a8a;
// Accent — $accent-dim mirrors TANINUX's gui.json `accent_hex` and is the only
// managed line (rewritten by scripts/accent.sh). Everything else derives from
// it, so the whole bar/panels/dock re-tint from one value.
$accent-dim: #ac8a8c; // accent_hex (managed by scripts/accent.sh)
$accent: lighten($accent-dim, 8%); // bright
// translucent accent washes (depth without shouting)
$accent-08: rgba($accent, 0.08);
$accent-14: rgba($accent, 0.14);
$accent-22: rgba($accent, 0.22);
$border: rgba(240, 240, 240, 0.06);
$shadow: rgba(0, 0, 0, 0.55);
* {
font-family: "Iosevka Aile", "Symbols Nerd Font", sans-serif;
font-size: 16px;
font-weight: 500; // Medium
// The GTK theme (adw-gtk3 / Adwaita) puts an "embossed" text-shadow on
// labels & buttons — on our dark bar it shows as a light, vertically
// offset ghost that makes text look doubled. Kill it everywhere.
text-shadow: none;
-gtk-icon-shadow: none;
}
// Kill the GTK window background — otherwise it shows through the panel margin
// as a fat light frame. GTK also puts a `.background` style class on toplevels
// that the theme fills with a colour; on a semi-transparent window (the dock)
// that fill shows through as an extra outer rectangle ("box in a box"), so kill
// it too.
window,
window.background,
.background,
decoration {
background-color: transparent;
background-image: none;
box-shadow: none; // kill the theme's CSD window shadow — on the translucent
border: none; // dock it shows as a faint outer box with its own shadow
}
// eventbox wrappers must not inherit a theme background, or they paint a second
// rectangle behind rounded content (the dock's "box in a box").
eventbox { background-color: transparent; background-image: none; }
// Strip GTK's default button/scale theming so our colours actually show.
button {
background-image: none;
background-color: transparent;
border: none;
box-shadow: none;
outline: none;
min-height: 0;
min-width: 0;
}
scale trough,
scale highlight,
scale slider {
background-image: none;
border: none;
box-shadow: none;
}
// ===========================================================================
// BAR
// ===========================================================================
.bar {
background-color: $bg;
color: $fg;
font-size: 8px; // was 12px
padding: 0 12px;
}
// (No bar shadow: the exclusive strip can't cast a real drop shadow onto the
// windows below — see HANDOVER. The .bar-shadow gradient hack was removed.)
// ---- workspaces -----------------------------------------------------------
.workspaces .ws {
color: $muted;
font-size: 7px; // was 11px
padding: 1px 9px;
margin: 1px 1px;
border-radius: 999px;
}
.workspaces .ws.occupied { color: $fg; }
.workspaces .ws.active {
background-color: $accent-14;
color: $accent;
}
.workspaces .ws:hover { background-color: $bg-alt; }
// ---- clock ----------------------------------------------------------------
.clock {
color: $fg;
padding: 1px 10px;
border-radius: 999px;
}
.clock:hover { background-color: $bg-alt; }
// ---- right modules --------------------------------------------------------
.module {
color: $fg;
padding: 1px 8px;
border-radius: 999px;
}
.module:hover {
background-color: $bg-alt;
color: $accent;
}
// ===========================================================================
// PANELS (depth: border + drop shadow inside the window margin)
// ===========================================================================
.panel {
background-color: $bg;
color: $fg;
border: 1px solid $accent-22;
border-radius: 18px;
padding: 18px;
// margin must exceed the shadow's reach, or it clips at the window edge and
// leaves a hard line instead of fading out. Same shadow as the dock.
margin: 12px 16px 16px 16px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.40);
}
.panel-title {
font-size: 18px;
font-weight: 700;
color: $fg;
margin-bottom: 4px;
}
// ---- control-center tiles -------------------------------------------------
.tile {
background-color: $bg-alt;
color: $muted;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 14px 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.05); // raised card feel
}
.tile:hover { background-color: $bg-hi; }
.tile.active {
background-color: $accent-14;
border: 1px solid $accent-22;
color: $accent;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); // flat — bg+border mark active
}
.tile.active:hover { background-color: $accent-08; }
.tile-icon { font-size: 24px; }
.tile-label { font-size: 12px; font-weight: 500; }
// ---- action buttons -------------------------------------------------------
.cc-actions { margin-top: 4px; }
.action {
background-color: $bg-alt;
color: $fg;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 12px 6px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.action:hover {
background-color: $accent;
color: $bg;
box-shadow: 0 2px 10px rgba(163, 158, 196, 0.35);
}
.action-icon { font-size: 20px; }
.action-label { font-size: 11px; font-weight: 500; }
// ---- control-center header (settings + power) -----------------------------
.cc-header { margin-bottom: 2px; }
.cc-iconbtn {
color: $muted;
font-size: 18px;
padding: 4px 8px;
border-radius: 10px;
}
.cc-iconbtn:hover { color: $fg; background-color: $bg-alt; }
.cc-iconbtn.active { color: $accent; background-color: $accent-14; }
.power-menu {
background-color: $bg-alt;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 6px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.power-item {
color: $fg;
font-size: 13px;
padding: 9px 12px;
border-radius: 10px;
}
.power-item:hover { background-color: $accent-14; color: $accent; }
.power-item-icon { font-size: 16px; }
// ---- sliders --------------------------------------------------------------
.slider-row { margin-top: 4px; }
.slider-icon { font-size: 18px; color: $fg; }
.slider-val { color: $muted; font-size: 13px; }
// ---- audio output dropdown ------------------------------------------------
.audio-caret {
color: $muted;
font-size: 13px;
padding: 0 4px;
background-color: transparent;
border: none;
}
.audio-caret:hover { color: $fg; }
.audio-caret.open { color: $accent; }
.audio-list { margin: 2px 0 2px 0; }
.audio-dev {
color: $muted;
background-color: $bg-alt;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 8px 12px;
}
.audio-dev:hover { background-color: $bg-hi; color: $fg; }
.audio-dev.active {
background-color: $accent-14;
border: 1px solid $accent-22;
color: $accent;
}
.audio-dev-icon { font-size: 15px; }
.audio-dev-check { color: $accent; font-size: 14px; }
scale trough {
background-color: $bg-alt;
border-radius: 999px;
min-height: 8px;
min-width: 60px;
}
scale highlight {
background-color: $accent;
border-radius: 999px;
}
scale slider {
background-color: $fg;
border-radius: 999px;
min-height: 15px;
min-width: 15px;
margin: -4px;
}
// progress-style bars (non interactive)
.metric-bar trough { min-height: 6px; }
.metric-bar highlight { background-color: $accent; }
.metric-bar slider {
background-color: transparent;
min-width: 0;
min-height: 0;
margin: 0;
}
// ---- music ----------------------------------------------------------------
.cover {
border-radius: 14px;
background-color: $bg-alt;
background-size: cover;
background-position: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}
.m-title { font-size: 17px; font-weight: 700; margin-top: 4px; }
.m-artist { color: $muted; font-size: 14px; }
.m-controls { margin-top: 6px; }
.m-btn {
font-size: 22px;
padding: 6px 12px;
border-radius: 12px;
color: $fg;
}
.m-btn:hover { background-color: $bg-alt; color: $accent; }
.m-btn.play { font-size: 28px; color: $accent; }
// ---- calendar -------------------------------------------------------------
.cal-time { font-size: 42px; font-weight: 700; }
.cal-date { color: $muted; font-size: 15px; margin-bottom: 10px; }
calendar {
padding: 4px;
background-color: transparent; // GTK theme draws a white box + border here otherwise
background-image: none;
border: none;
box-shadow: none;
outline: none;
color: $fg;
}
calendar.header { font-weight: 700; color: $fg; }
calendar.button { background: transparent; border: none; }
calendar:selected {
color: $bg;
background-color: $accent;
border-radius: 8px;
}
calendar:indeterminate { color: $muted; }
// ---- sysmon ---------------------------------------------------------------
.metric-icon { font-size: 20px; color: $accent; }
.metric { margin: 2px 0; }
.temp-row { margin-top: 2px; }
// ---- dock -----------------------------------------------------------------
// glassy: semi-transparent fill + Hyprland layer blur (set in launch.sh).
// No border -> avoids the "double frame" look; depth comes from a soft drop
// shadow plus a 1px inner top highlight (the Apple glass edge).
.dock-hot { background-color: transparent; }
// left-edge hover strip that opens the dock (niri has no cursor-pos IPC).
.dock-trigger { background-color: transparent; min-width: 6px; }
// the glassy box IS the eventbox (single styled element — no theme rectangle
// behind it). The inner layout box stays transparent.
.dock-evt { background-color: transparent; background-image: none; }
.dock {
background-color: rgba(15, 15, 15, 0.45);
border-radius: 22px;
padding: 10px 8px;
margin: 0;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.dock-inner { background-color: transparent; }
.dock-icon {
min-width: 42px;
min-height: 42px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.dock-item {
padding: 4px;
border-radius: 14px;
}
.dock-item:hover { background-color: rgba(255, 255, 255, 0.10); }
.dock-item.unpinned { opacity: 0.92; }
.dock-dot {
min-width: 4px;
min-height: 4px;
border-radius: 999px;
background-color: transparent; // reserve space so icons stay aligned
}
.dock-dot.on { background-color: $fg; } // white running indicator
.dock-dot-spacer { min-width: 4px; } // matches the dot, keeps icon centred
// ---- dock right-click menu ------------------------------------------------
.dock-menu-backdrop { background-color: transparent; }
.dock-catcher { background-color: transparent; }
.panel-backdrop { background-color: transparent; }
.spot-backdrop { background-color: transparent; }
// even opacity fade for the top panels (content pre-rendered at opacity 0, then
// only opacity animates → no size change, no re-layout → perfectly smooth)
.panel-fade { opacity: 0; transition: opacity 100ms ease-in-out; }
.panel-fade.shown { opacity: 1; }
.dock-menu {
background-color: rgba(16, 16, 20, 0.93);
border-radius: 16px;
padding: 6px;
margin: 6px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
.dock-menu-title {
color: $muted;
font-size: 11px;
font-weight: 700;
padding: 4px 12px 4px 12px;
}
.dock-menu-item {
color: $fg;
font-size: 13px;
padding: 8px 12px;
border-radius: 10px;
}
.dock-menu-item:hover { background-color: $accent-14; color: $accent; }
.dock-menu-item.danger:hover { background-color: rgba(220, 90, 90, 0.22); color: #ff7a7a; }
.dock-menu-ws { color: $accent; font-size: 11px; font-weight: 700; }
.dock-menu-sep {
min-height: 1px;
background-color: rgba(255, 255, 255, 0.08);
margin: 4px 8px;
}
// ---- notifications --------------------------------------------------------
.notif-list { margin-top: 6px; }
.notif-head { margin-bottom: 2px; }
.notif-clear {
color: $muted;
font-size: 12px;
padding: 2px 10px;
border-radius: 999px;
}
.notif-clear:hover { color: $accent; background-color: $bg-alt; }
.notif-empty { color: $muted; font-size: 13px; padding: 4px 2px; }
.notif-card {
background-color: $bg-alt;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 10px 12px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.notif-app { color: $accent; font-size: 11px; font-weight: 700; }
.notif-time { color: $muted; font-size: 11px; }
.notif-summary { color: $fg; font-size: 13px; font-weight: 600; }
.notif-body { color: $muted; font-size: 12px; }
.notif-x {
color: $muted;
font-size: 14px;
padding: 0 4px;
border-radius: 8px;
}
.notif-x:hover { color: $fg; background-color: $bg-hi; }
// the top-right toast: same card look, a touch more presence.
// Keep the shadow blur smaller than the margin below, otherwise GTK clips it
// to the window rectangle and the rounded corners look square.
.notif-popup {
border: 1px solid $accent-22;
margin: 10px 10px 14px 10px; // right = gaps_out (10) so it lines up with windows
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.4);
}
// ---- updates panel --------------------------------------------------------
.upd-head { margin-bottom: 2px; }
.upd-count {
color: $accent;
font-size: 13px;
font-weight: 700;
background-color: $accent-14;
padding: 1px 9px;
border-radius: 999px;
}
.upd-list { padding-right: 6px; }
.upd-row { padding: 5px 8px; border-radius: 8px; }
.upd-row:hover { background-color: $bg-alt; }
.upd-name { color: $fg; font-size: 13px; }
.upd-old { color: $muted; font-size: 12px; }
.upd-arrow { color: $muted; font-size: 12px; }
.upd-new { color: $accent; font-size: 12px; font-weight: 600; }
.upd-all { margin-top: 4px; }
// ---- control-center now-playing -------------------------------------------
.cc-music {
background-color: $bg-alt;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 10px 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.cc-cover {
min-width: 52px;
min-height: 52px;
border-radius: 12px;
background-color: $bg-hi;
background-size: cover;
background-position: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.cc-cover-fallback { font-size: 24px; color: $muted; }
.cc-m-title { font-size: 14px; font-weight: 700; color: $fg; }
.cc-m-artist { font-size: 12px; color: $muted; }
.cc-m-controls { margin-top: 4px; }
.cc-m-btn {
font-size: 18px;
color: $fg;
padding: 2px 4px;
border-radius: 8px;
}
.cc-m-btn:hover { color: $accent; background-color: $bg-hi; }
.cc-m-btn.play { color: $accent; }
// ===========================================================================
// LIGHT ("Shibui") — applied to panels/dock when GNOME is in light mode.
// The .light class is added to each panel/dock root; the bar never gets it.
// ===========================================================================
$bg-l: #f3f1ec; // soft warm white
$bg-alt-l: #e8e4dc; // cards / tiles
$bg-hi-l: #ddd8ce; // hover
$fg-l: #2a2926; // near-black ink
$muted-l: #6f6b62; // muted ink
$border-l: rgba(0, 0, 0, 0.10);
$acc-l: $accent-dim; // darker violet reads better on white
// ---- generic panel ---------------------------------------------------------
.panel.light {
background-color: $bg-l;
color: $fg-l;
border: 1px solid $border-l;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
}
.light .panel-title { color: $fg-l; }
// ---- control-center tiles / actions ---------------------------------------
.light .tile {
background-color: $bg-alt-l;
color: $muted-l;
border: 1px solid $border-l;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.light .tile:hover { background-color: $bg-hi-l; }
.light .tile.active {
background-color: $accent-14;
color: $acc-l;
border: 1px solid $accent-22;
}
.light .tile.active:hover { background-color: $accent-08; }
.light .action {
background-color: $bg-alt-l;
color: $fg-l;
border: 1px solid $border-l;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.light .action:hover { background-color: $accent; color: #fff; box-shadow: 0 2px 8px rgba(163, 158, 196, 0.4); }
// ---- sliders ---------------------------------------------------------------
.light .slider-icon { color: $fg-l; }
.light .slider-val { color: $muted-l; }
.light scale trough { background-color: $bg-hi-l; }
.light scale slider { background-color: $fg-l; }
.light .metric-bar trough { background-color: $bg-hi-l; }
// ---- audio dropdown --------------------------------------------------------
.light .audio-caret { color: $muted-l; }
.light .audio-caret:hover { color: $fg-l; }
.light .audio-dev { background-color: $bg-alt-l; color: $muted-l; border: 1px solid $border-l; }
.light .audio-dev:hover { background-color: $bg-hi-l; color: $fg-l; }
.light .audio-dev.active { background-color: $accent-14; color: $acc-l; border: 1px solid $accent-22; }
// ---- calendar --------------------------------------------------------------
.light calendar { color: $fg-l; }
.light calendar.header { color: $fg-l; }
.light calendar:indeterminate { color: $muted-l; }
// ---- music -----------------------------------------------------------------
.light .m-title { color: $fg-l; }
.light .m-artist { color: $muted-l; }
.light .cover { background-color: $bg-alt-l; }
.light .m-btn { color: $fg-l; }
.light .m-btn:hover { background-color: $bg-alt-l; color: $acc-l; }
// ---- sysmon ----------------------------------------------------------------
.light .metric-icon { color: $acc-l; }
// ---- notifications ---------------------------------------------------------
.light .notif-card {
background-color: $bg-alt-l;
border: 1px solid $border-l;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
}
.light .notif-app { color: $acc-l; }
.light .notif-time { color: $muted-l; }
.light .notif-summary { color: $fg-l; }
.light .notif-body { color: $muted-l; }
.light .notif-x { color: $muted-l; }
.light .notif-x:hover { color: $fg-l; background-color: $bg-hi-l; }
.light .notif-clear { color: $muted-l; }
.light .notif-clear:hover { color: $acc-l; background-color: $bg-alt-l; }
.light .notif-empty { color: $muted-l; }
// ---- dock (frosted white) --------------------------------------------------
.dock.light {
background-color: rgba(243, 241, 236, 0.55);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.55);
}
.dock.light .dock-item:hover { background-color: rgba(0, 0, 0, 0.06); }
.dock.light .dock-dot.on { background-color: $fg-l; } // dark dot on light glass
// ---- dock menu -------------------------------------------------------------
.dock-menu.light {
background-color: rgba(243, 241, 236, 0.96);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.dock-menu.light .dock-menu-title { color: $muted-l; }
.dock-menu.light .dock-menu-ws { color: $acc-l; }
.dock-menu.light .dock-menu-sep { background-color: rgba(0, 0, 0, 0.12); }
.dock-menu.light .dock-menu-item { color: $fg-l; }
.dock-menu.light .dock-menu-item:hover { background-color: $accent-14; color: $acc-l; }
// ---- updates panel (light) -------------------------------------------------
.light .upd-count { color: $acc-l; background-color: $accent-14; }
.light .upd-row:hover { background-color: $bg-alt-l; }
.light .upd-name { color: $fg-l; }
.light .upd-old { color: $muted-l; }
.light .upd-arrow { color: $muted-l; }
.light .upd-new { color: $acc-l; }
// ---- control-center header / power (light) --------------------------------
.light .cc-iconbtn { color: $muted-l; }
.light .cc-iconbtn:hover { color: $fg-l; background-color: $bg-alt-l; }
.light .cc-iconbtn.active { color: $acc-l; background-color: $accent-14; }
.light .power-menu { background-color: $bg-alt-l; border: 1px solid $border-l; }
.light .power-item { color: $fg-l; }
.light .power-item:hover { background-color: $accent-14; color: $acc-l; }
// ---- now-playing (light) ---------------------------------------------------
.light .cc-music { background-color: $bg-alt-l; border: 1px solid $border-l; }
.light .cc-cover { background-color: $bg-hi-l; }
.light .cc-m-title { color: $fg-l; }
.light .cc-m-artist { color: $muted-l; }
.light .cc-m-btn { color: $fg-l; }
.light .cc-m-btn:hover { color: $acc-l; background-color: $bg-hi-l; }
.light .cc-m-btn.play { color: $acc-l; }
// ===========================================================================
// SPOTLIGHT LAUNCHER
// ===========================================================================
.spot {
background-color: $bg;
border: 1px solid $accent-22;
border-radius: 18px;
padding: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.spot-search { padding: 8px 12px; }
.spot-search-icon { color: $muted; font-size: 19px; }
.spot-input {
color: $fg;
font-size: 19px;
background-color: transparent;
border: none;
box-shadow: none;
caret-color: $accent;
}
.spot-list { padding: 6px 4px 2px; }
.spot-row {
padding: 7px 10px;
border-radius: 12px;
background-color: transparent;
}
.spot-row:hover { background-color: $accent-14; }
.spot-icon { min-width: 36px; }
.spot-img {
min-width: 30px; min-height: 30px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.spot-glyph { color: $accent; font-size: 22px; }
.spot-title { color: $fg; font-size: 14px; }
.spot-sub { color: $muted; font-size: 11px; }
// ---- spotlight (light) ----------------------------------------------------
.spot.light {
background-color: $bg-l;
border: 1px solid $accent-22;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.50);
}
.spot.light .spot-search-icon { color: $muted-l; }
.spot.light .spot-input { color: $fg-l; caret-color: $acc-l; }
.spot.light .spot-row:hover { background-color: $accent-14; }
.spot.light .spot-glyph { color: $acc-l; }
.spot.light .spot-title { color: $fg-l; }
.spot.light .spot-sub { color: $muted-l; }
// ---- spotlight category pills ---------------------------------------------
.spot-pills { padding: 6px 6px 2px; }
.spot-pill {
padding: 4px 11px;
border-radius: 999px;
background-color: rgba(255, 255, 255, 0.05);
color: $muted;
}
.spot-pill:hover { background-color: $accent-14; color: $fg; }
.spot-pill.active { background-color: $accent-22; color: $fg; }
.spot-pill-icon { font-size: 13px; }
.spot-pill-label { font-size: 12px; }
.spot.light .spot-pill { background-color: rgba(0, 0, 0, 0.05); color: $muted-l; }
.spot.light .spot-pill:hover { color: $fg-l; }
.spot.light .spot-pill.active { color: $fg-l; }
// AUR badge in the updates panel
.upd-src.aur {
font-size: 9px;
color: $accent;
background-color: $accent-14;
border-radius: 5px;
padding: 0 4px;
}
.light .upd-src.aur { color: $acc-l; background-color: $accent-14; }
// pill sigil hint + keyboard focus (GTK Tab traversal)
.spot-pill-key {
font-size: 10px;
color: $muted;
background-color: rgba(255, 255, 255, 0.07);
border-radius: 5px;
padding: 0 4px;
margin-left: 2px;
}
.spot-pill:focus {
background-color: $accent-22;
color: $fg;
outline: none;
box-shadow: 0 0 0 2px $accent;
}
.spot-row:focus { background-color: $accent-22; outline: none; }
.spot.light .spot-pill-key { color: $muted-l; background-color: rgba(0,0,0,0.06); }
// in-panel update progress
.upd-prog {
background-color: $bg-alt;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 12px 14px;
}
.upd-prog-spin { color: $accent; font-size: 16px; }
.upd-prog-text { color: $muted; font-size: 11px; }
.light .upd-prog { background-color: $bg-alt-l; border: 1px solid $border-l; }
.light .upd-prog-spin { color: $acc-l; }
.light .upd-prog-text { color: $muted-l; }
@@ -0,0 +1,690 @@
;; ============================================================================
;; eww config — niri top bar + slide-in panels + dock
;; Monochrome / Fuji-accent theme.
;; Icons: Material Design Icons (nf-md-*) from Iosevka Nerd Font.
;; Repo lives in ~/eww and is symlinked to ~/.config/eww
;; ============================================================================
;; ---------------------------------------------------------------------------
;; DATA SOURCES (poll / listen)
;; ---------------------------------------------------------------------------
(deflisten workspaces :initial "[]"
"scripts/workspaces.sh")
(deflisten music
:initial {'{"status":"Stopped","title":"","artist":"","icon":"","cover":""}'}
"scripts/music.sh")
(defpoll volume :interval "1s"
:initial {'{"percent":0,"muted":false,"icon":""}'}
"scripts/volume.sh get")
;; audio output devices for the Control Center dropdown
(defpoll sinks :interval "3s" :initial "[]" "scripts/audio.sh sinks")
(defvar audio-menu false)
;; dock: pinned apps (real Papirus icons) + running state
;; 4s (not 2s): the poll re-renders the icon row, which briefly re-applies hover
;; state (a small flicker). 4s keeps running-dots fresh enough with less flicker.
(defpoll dock-apps :interval "4s" :initial "[]" "scripts/dock.sh apps")
;; dock visibility — drives the slide-in/out revealer (window stays mapped).
(defvar dock-open false)
;; panel + spotlight slide animation (one panel open at a time → one shared var).
(defvar panel-open false)
(defvar spot-open false)
;; live settings from TANINUX panel.json (pushed via eww update by panelcfg.sh)
(defvar cfg-icon-size 42)
(defvar cfg-modules {'{"music":true,"sys":true,"updates":true,"net":true,"bt":true,"vol":true}'})
;; GNOME light/dark — panels follow it (the bar stays dark). "light" = Shibui.
;; Polled (not a gsettings-monitor deflisten — that dies in eww's spawn env).
(defpoll colorscheme :interval "2s" :initial "dark" "scripts/colorscheme.sh get")
;; right-click context-menu target
(defvar dock-sel-class "")
(defvar dock-sel-exec "")
(defvar dock-sel-pinned false)
(defvar dock-sel-running false)
(defvar dock-sel-windows "[]") ; open windows of the selected app (multi-instance)
(defpoll network :interval "5s"
:initial {'{"icon":"","label":"…"}'}
"scripts/network.sh status")
(defpoll bluetooth :interval "5s"
:initial {'{"icon":"","label":"…","on":false}'}
"scripts/bluetooth.sh status")
(defpoll updates-list :interval "900s" :initial "[]" "scripts/updates.sh list")
(defvar updates-open true) ; dropdown state in the updates panel
(defvar upd-running false) ; a system update is in progress (panel shows progress)
(defvar upd-progress "") ; latest line of the running update
(defvar power-open false) ; power dropdown in the control center
(defpoll nightlight :interval "5s" :initial "true" "scripts/night.sh status")
;; DnD is our own flag now (mako is gone — replaced by tiramisu + eww).
(defpoll dnd :interval "2s" :initial "false" "scripts/notifications.sh dnd-get")
;; Notifications: pushed in by scripts/notifications.sh via `eww update`.
;; notifications — full history (newest first), rendered in the Control Center
;; popup — the single toast currently shown top-right
(defvar notifications "[]")
(defvar popup {'{"id":"0","app":"","summary":"","body":"","time":""}'})
;; Spotlight launcher — query text + active category + results
(defvar spot-q "")
(defvar spot-mode "apps")
(defvar spot-results "[]")
(defpoll sys :interval "2s"
:initial {'{"cpu":0,"mem":0,"disk":0,"temp":0}'}
"scripts/sysinfo.sh")
(defpoll time :interval "10s" :initial "" "scripts/clock.sh")
;; ---------------------------------------------------------------------------
;; THE BAR
;; ---------------------------------------------------------------------------
(defwidget bar []
(centerbox :class "bar" :vexpand true
(left)
(center)
(right)))
(defwidget left []
(box :halign "start" :valign "center" :space-evenly false
(workspaces)))
(defwidget center []
(box :halign "center" :valign "center" :space-evenly false
(clock)))
(defwidget right []
(box :halign "end" :valign "center" :space-evenly false :spacing 14
(sys-mini)
(updates-mod)
(net)
(bt)
(vol)))
;; --- workspaces -------------------------------------------------------------
(defwidget workspaces []
(eventbox
:onscroll "test {} = up && scripts/ws.sh cycle up || scripts/ws.sh cycle down"
(box :class "workspaces" :space-evenly false :spacing 6
(for ws in workspaces
(button
:class {ws.active ? "ws active" : (ws.windows > 0 ? "ws occupied" : "ws")}
:onclick "scripts/ws.sh switch ${ws.id}"
"${ws.id}")))))
;; --- clock (center) ---------------------------------------------------------
(defwidget clock []
(button :class "clock" :onclick "scripts/panel.sh toggle calendar"
"${time}"))
;; --- right-hand modules -----------------------------------------------------
(defwidget music-mini []
(revealer :transition "slideleft" :duration "200ms"
:reveal {cfg-modules.music && music.status != "Stopped" && music.title != ""}
(button :class "module music" :onclick "scripts/panel.sh toggle music"
(label :limit-width 28 :text "${music.icon} ${music.title}"))))
(defwidget sys-mini []
(button :class "module sys" :visible {cfg-modules.sys} :onclick "scripts/panel.sh toggle sysmon"
(label :text "󰓅 ${sys.cpu}% 󰍛 ${sys.mem}%")))
(defwidget updates-mod []
(revealer :transition "slideleft" :duration "200ms" :reveal {cfg-modules.updates && arraylength(updates-list) > 0}
(button :class "module updates" :onclick "scripts/panel.sh toggle updates"
(label :text "󰚰 ${arraylength(updates-list)}"))))
(defwidget net []
(button :class "module net" :visible {cfg-modules.net} :onclick "scripts/panel.sh toggle control-center"
:onrightclick ""
(label :text {network.icon})))
(defwidget bt []
(button :class "module bt" :visible {cfg-modules.bt} :onclick "scripts/panel.sh toggle control-center"
:onrightclick ""
(label :text {bluetooth.icon})))
(defwidget vol []
(eventbox
:onscroll "test {} = up && wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
(button :class "module vol" :visible {cfg-modules.vol} :onclick "scripts/panel.sh toggle control-center"
:onrightclick "scripts/volume.sh mute"
(label :text "${volume.icon} ${volume.percent}%"))))
;; ---------------------------------------------------------------------------
;; CONTROL CENTER (slides in top-right)
;; ---------------------------------------------------------------------------
(defwidget toggle-tile [icon label active onclick onrc]
(button :class {active ? "tile active" : "tile"} :onclick onclick :onrightclick onrc
(box :orientation "vertical" :space-evenly false :spacing 5
(label :class "tile-icon" :text icon)
(label :class "tile-label" :limit-width 14 :text label))))
(defwidget action-btn [icon label cmd]
(button :class "action" :onclick cmd
(box :orientation "vertical" :space-evenly false :spacing 4
(label :class "action-icon" :text icon)
(label :class "action-label" :text label))))
(defwidget vol-slider []
(box :orientation "vertical" :space-evenly false :spacing 8
(box :class "slider-row" :space-evenly false :spacing 10
;; dropdown caret on the LEFT — opens the output-device menu below
(button :class {audio-menu ? "audio-caret open" : "audio-caret"}
:onclick "eww update audio-menu=${!audio-menu}"
(label :text {audio-menu ? "▴" : "▾"}))
(button :class "slider-icon" :onclick "scripts/volume.sh mute"
(label :text {volume.icon}))
(scale :hexpand true :min 0 :max 100 :value {volume.percent}
:onchange "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ {}%")
(label :class "slider-val" :text "${volume.percent}%"))
(revealer :transition "slidedown" :duration "200ms" :reveal audio-menu
(box :class "audio-list" :orientation "vertical" :space-evenly false :spacing 4
(for s in sinks
(button :class {s.default ? "audio-dev active" : "audio-dev"}
:onclick "scripts/audio.sh set-default ${s.id} && eww update audio-menu=false"
(box :space-evenly false :spacing 8
(label :class "audio-dev-icon" :text "󰕾")
(label :halign "start" :hexpand true :limit-width 30 :text {s.name})
(label :class "audio-dev-check" :visible {s.default} :text "󰄬"))))))))
(defwidget cc-music []
(revealer :transition "slidedown" :duration "200ms"
:reveal {music.status != "Stopped" && music.title != ""}
(box :class "cc-music" :space-evenly false :spacing 12
(overlay
(box :class "cc-cover" :style "background-image: url('${music.cover}');")
(label :class "cc-cover-fallback" :visible {music.cover == ""} :text "󰝚"))
(box :orientation "vertical" :hexpand true :space-evenly false :spacing 2 :valign "center"
(label :class "cc-m-title" :halign "start" :limit-width 22 :text {music.title})
(label :class "cc-m-artist" :halign "start" :limit-width 26 :text {music.artist})
(box :class "cc-m-controls" :halign "start" :space-evenly false :spacing 16
(button :class "cc-m-btn" :onclick "playerctl previous" "󰒮")
(button :class "cc-m-btn play" :onclick "playerctl play-pause" {music.icon})
(button :class "cc-m-btn" :onclick "playerctl next" "󰒭"))))))
(defwidget power-item [icon label action]
(button :class "power-item" :onclick "scripts/power.sh ${action}"
(box :space-evenly false :spacing 12
(label :class "power-item-icon" :text icon)
(label :halign "start" :hexpand true :text label))))
;; generic slide-in wrapper for panels + spotlight (top-anchored → slide down).
;; Pure OPACITY fade (not a revealer): the content is always realized, so it
;; renders ONCE (during the realize wait, while still at opacity 0) and then only
;; the opacity animates via a CSS transition — perfectly even, no size change, no
;; per-frame re-layout. `open` just toggles the `.shown` class.
(defwidget reveal-box [open]
(box :class {open ? "panel-fade shown" : "panel-fade"}
(children)))
(defwidget control-center []
(box :class {colorscheme == "light" ? "panel cc light" : "panel cc"} :orientation "vertical" :space-evenly false :spacing 14
;; header: title (left) · settings gear · power
(box :class "cc-header" :space-evenly false :spacing 6
(label :class "panel-title" :halign "start" :hexpand true :text "Control Center")
(button :class "cc-iconbtn" :onclick "scripts/settings.sh && scripts/panel.sh close"
(label :text "󰒓"))
(button :class {power-open ? "cc-iconbtn active" : "cc-iconbtn"}
:onclick "eww update power-open=${!power-open}"
(label :text "󰐥")))
;; power dropdown (replaces the old block of action buttons)
(revealer :transition "slidedown" :duration "200ms" :reveal power-open
(box :class "power-menu" :orientation "vertical" :space-evenly false :spacing 2
(power-item :icon "󰌾" :label "Lock" :action "lock")
(power-item :icon "󰍃" :label "Logout" :action "logout")
(power-item :icon "󰒲" :label "Sleep" :action "sleep")
(power-item :icon "󰜉" :label "Reboot" :action "reboot")
(power-item :icon "󰐥" :label "Shutdown" :action "shutdown")))
(box :spacing 12 :homogeneous true
(toggle-tile :icon {network.icon} :label {network.label}
:active {network.label != "Offline"}
:onclick "scripts/network.sh toggle" :onrc "")
(toggle-tile :icon {bluetooth.icon} :label {bluetooth.label}
:active {bluetooth.on}
:onclick "scripts/bluetooth.sh toggle" :onrc ""))
(box :spacing 12 :homogeneous true
(toggle-tile :icon "󰖔" :label "Night Light" :active {nightlight}
:onclick "scripts/night.sh toggle" :onrc "")
(toggle-tile :icon "󰂛" :label "Do not Disturb" :active {dnd}
:onclick "scripts/notifications.sh dnd-toggle" :onrc ""))
(cc-music)
(vol-slider)
(notif-list)))
;; ---------------------------------------------------------------------------
;; NOTIFICATIONS (history list in the Control Center + top-right toast)
;; ---------------------------------------------------------------------------
(defwidget notif-card [n]
(box :class "notif-card" :space-evenly false :spacing 8
(box :orientation "vertical" :hexpand true :space-evenly false :spacing 2
(box :space-evenly false
(label :class "notif-app" :halign "start" :hexpand true :limit-width 24 :text {n.app})
(label :class "notif-time" :halign "end" :text {n.time}))
(label :class "notif-summary" :halign "start" :wrap true :limit-width 36 :text {n.summary})
(label :class "notif-body" :halign "start" :wrap true :limit-width 40 :show-truncated false
:visible {n.body != ""} :text {n.body}))
(button :class "notif-x" :valign "start" :onclick "scripts/notifications.sh dismiss ${n.id}" "󰅖")))
(defwidget notif-list []
(box :class "notif-list" :orientation "vertical" :space-evenly false :spacing 8
(box :class "notif-head" :space-evenly false
(label :class "panel-title" :halign "start" :hexpand true :text "Notifications")
(button :class "notif-clear" :visible {arraylength(notifications) > 0}
:onclick "scripts/notifications.sh clear" "Clear all"))
(box :visible {arraylength(notifications) == 0}
(label :class "notif-empty" :halign "start" :text "Nothing new"))
(scroll :vscroll true :height 220 :visible {arraylength(notifications) > 0}
(box :orientation "vertical" :space-evenly false :spacing 8
(for n in notifications
(notif-card :n n))))))
(defwidget notif-popup []
(box :class {colorscheme == "light" ? "panel notif-popup light" : "panel notif-popup"} :space-evenly false :spacing 8
(box :orientation "vertical" :hexpand true :space-evenly false :spacing 2
(box :space-evenly false
(label :class "notif-app" :halign "start" :hexpand true :text {popup.app})
(label :class "notif-time" :halign "end" :text {popup.time}))
(label :class "notif-summary" :halign "start" :wrap true :limit-width 36 :text {popup.summary})
(label :class "notif-body" :halign "start" :wrap true :limit-width 40
:visible {popup.body != ""} :text {popup.body}))
(button :class "notif-x" :valign "start" :onclick "scripts/notifications.sh popup-close" "󰅖")))
;; ---------------------------------------------------------------------------
;; CALENDAR (slides in top-center, opened by the clock)
;; ---------------------------------------------------------------------------
(defwidget cal []
(box :class {colorscheme == "light" ? "panel cal light" : "panel cal"} :orientation "vertical" :space-evenly false :spacing 6
(calendar :class "cal-widget")))
;; ---------------------------------------------------------------------------
;; MUSIC PANEL
;; ---------------------------------------------------------------------------
(defwidget music-panel []
(box :class {colorscheme == "light" ? "panel music-panel light" : "panel music-panel"} :orientation "vertical" :space-evenly false :spacing 12
(box :class "cover" :height 180
:style "background-image: url('${music.cover}');")
(label :class "m-title" :limit-width 30 :text {music.title})
(label :class "m-artist" :limit-width 34 :text {music.artist})
(box :class "m-controls" :halign "center" :space-evenly false :spacing 24
(button :class "m-btn" :onclick "playerctl previous" "󰒮")
(button :class "m-btn play" :onclick "playerctl play-pause" {music.icon})
(button :class "m-btn" :onclick "playerctl next" "󰒭"))))
;; ---------------------------------------------------------------------------
;; SYSTEM MONITOR PANEL
;; ---------------------------------------------------------------------------
(defwidget metric [icon label value]
(box :class "metric" :space-evenly false :spacing 12
(label :class "metric-icon" :text icon)
(box :orientation "vertical" :hexpand true :space-evenly false :spacing 4
(box :space-evenly false
(label :halign "start" :hexpand true :text label)
(label :halign "end" :text "${value}%"))
(scale :class "metric-bar" :min 0 :max 100 :value value :active false))))
(defwidget sysmon-panel []
(box :class {colorscheme == "light" ? "panel sysmon light" : "panel sysmon"} :orientation "vertical" :space-evenly false :spacing 16
(label :class "panel-title" :halign "start" :text "System")
(metric :icon "󰓅" :label "CPU" :value {sys.cpu})
(metric :icon "󰍛" :label "RAM" :value {sys.mem})
(metric :icon "󰋊" :label "Disk" :value {sys.disk})
(box :class "temp-row" :space-evenly false :spacing 12
(label :class "metric-icon" :text "󰔏")
(label :halign "start" :hexpand true :text "Temp")
(label :halign "end" :text "${sys.temp}°C"))))
;; ---------------------------------------------------------------------------
;; UPDATES PANEL (count + dropdown list, ~10 visible then scroll)
;; ---------------------------------------------------------------------------
(defwidget updates-panel []
(box :class {colorscheme == "light" ? "panel updates-panel light" : "panel updates-panel"}
:orientation "vertical" :space-evenly false :spacing 10
(box :class "upd-head" :space-evenly false :spacing 8
(label :class "panel-title" :halign "start" :text "Updates available")
(label :class "upd-count" :text {arraylength(updates-list)})
(box :hexpand true)
(button :class {updates-open ? "audio-caret open" : "audio-caret"}
:onclick "eww update updates-open=${!updates-open}"
(label :text {updates-open ? "▴" : "▾"})))
(revealer :transition "slidedown" :duration "200ms" :reveal updates-open
(scroll :vscroll true :height 260
(box :class "upd-list" :orientation "vertical" :space-evenly false :spacing 3
(for u in updates-list
(box :class "upd-row" :space-evenly false :spacing 8
(label :class "upd-src aur" :visible {u.src == "aur"} :text "AUR")
(label :class "upd-name" :halign "start" :hexpand true :limit-width 24 :text {u.name})
(label :class "upd-old" :text {u.old})
(label :class "upd-arrow" :text "→")
(label :class "upd-new" :text {u.new}))))))
(revealer :transition "crossfade" :duration "150ms" :reveal {!upd-running}
(button :class "action upd-all" :onclick "scripts/doupdate.sh &"
(label :text "Update all")))
(revealer :transition "crossfade" :duration "150ms" :reveal upd-running
(box :class "upd-prog" :space-evenly false :spacing 10 :valign "center"
(label :class "upd-prog-spin" :text "󰦖")
(label :class "upd-prog-text" :halign "start" :hexpand true
:limit-width 40 :show-truncated false :text upd-progress)))))
;; ---------------------------------------------------------------------------
;; DOCK (auto-hide, left edge, vertical — real app icons)
;; ---------------------------------------------------------------------------
(defwidget dock []
;; Auto-hide (niri has no cursor IPC): the dock + every icon write "show" on
;; hover (keepalive). "Leaving" is NOT detected by the dock's own onhoverlost
;; (GTK fires that spuriously when the pointer crosses onto a child icon, so it
;; would hide while you're still on the dock). Instead a separate `dock-catcher`
;; surface BELOW the dock fires "leave" the moment the pointer is anywhere off
;; the dock — a reliable signal. So while hovering the dock there is only ever
;; "show"; the dock stays put until you actually move off it.
(eventbox :class "dock-evt"
:onhover "scripts/dock.sh keepalive"
:onrightclick "scripts/dock.sh bg-menu"
(revealer :transition "slideright" :duration "200ms" :reveal dock-open
(box :class {colorscheme == "light" ? "dock light" : "dock"} :orientation "vertical" :space-evenly false :spacing 10 :valign "center"
(for a in dock-apps
(button :class {a.pinned ? "dock-item" : "dock-item unpinned"}
:onhover "scripts/dock.sh keepalive"
:onclick "scripts/dock.sh launch ${a.class} ${a.exec}"
:onrightclick "scripts/dock.sh menu-open '${a.class}' '${a.exec}' ${a.pinned} ${a.running} ${a.idx}"
(box :space-evenly false :spacing 4 :halign "center"
(box :class {a.running ? "dock-dot on" : "dock-dot"} :valign "center")
;; CSS background-image renders the SVG scale-aware (sharp at 1.25)
(box :class "dock-icon" :style "background-image: url('${a.icon}'); min-width: ${cfg-icon-size}px; min-height: ${cfg-icon-size}px;")
;; mirror of the dot's width on the right so the icon stays centred
(box :class "dock-dot-spacer"))))))))
;; thin always-present hover strip at the left edge — replaces the cursor poll.
;; Hovering it shows the dock; leaving is detected by the dock-catcher (below),
;; so the trigger only needs to fire "show".
(defwidget dock-trigger []
(eventbox :onhover "scripts/dock.sh show"
(box :class "dock-trigger")))
;; full-screen-ish catcher BELOW the dock (opened by the watcher while the dock is
;; visible). The pointer entering it = the pointer is off the dock → "leave"
;; (debounced hide). A click anywhere off the dock dismisses immediately. It
;; starts at x=6 so it never covers the left-edge trigger.
(defwidget dock-catcher []
(eventbox :hexpand true :vexpand true
:onhover "scripts/dock.sh hide"
:onclick "scripts/dock.sh hide-now"
:onrightclick "scripts/dock.sh hide-now"
(box :class "dock-catcher" :hexpand true :vexpand true)))
;; right-click on the EMPTY dock area (not an icon) -> jump to settings
(defwidget dock-bg-menu []
(box :class {colorscheme == "light" ? "dock-menu light" : "dock-menu"} :orientation "vertical" :space-evenly false :spacing 2
(label :class "dock-menu-title" :halign "start" :text "Dock & Bar")
(button :class "dock-menu-item"
:onclick "scripts/settings.sh --page panel-dock ; eww close dock-bg-menu dock-menu-backdrop"
(label :halign "start" :text " Panel & Dock Settings…"))
(button :class "dock-menu-item"
:onclick "scripts/panelcfg.sh toggle-autohide ; eww close dock-bg-menu dock-menu-backdrop"
(label :halign "start" :text " Toggle auto-hide"))
(button :class "dock-menu-item"
:onclick "eww close dock-bg-menu dock-menu-backdrop ; $HOME/projects/eww/launch.sh"
(label :halign "start" :text " Restart bar"))))
(defwidget dock-menu []
(box :class {colorscheme == "light" ? "dock-menu light" : "dock-menu"} :orientation "vertical" :space-evenly false :spacing 2
(label :class "dock-menu-title" :halign "start" :limit-width 18 :text {dock-sel-class})
;; window picker — only when more than one instance is open
(box :visible {arraylength(dock-sel-windows) > 1} :orientation "vertical" :space-evenly false :spacing 2
(for w in dock-sel-windows
(button :class "dock-menu-item dock-menu-win" :onclick "scripts/dock.sh focus-win ${w.address}"
(box :space-evenly false :spacing 8
(label :halign "start" :hexpand true :limit-width 20 :text {w.title})
(label :class "dock-menu-ws" :text " ${w.ws}"))))
(box :class "dock-menu-sep"))
(button :class "dock-menu-item" :onclick "scripts/dock.sh new ${dock-sel-exec}"
(label :halign "start" :text " New Window"))
(button :class "dock-menu-item" :visible {!dock-sel-pinned}
:onclick "scripts/dock.sh pin ${dock-sel-class}"
(label :halign "start" :text " Pin to Dock"))
(button :class "dock-menu-item" :visible {dock-sel-pinned}
:onclick "scripts/dock.sh unpin ${dock-sel-class}"
(label :halign "start" :text " Unpin"))
(button :class "dock-menu-item" :visible {dock-sel-pinned}
:onclick "scripts/dock.sh move-up ${dock-sel-class}"
(label :halign "start" :text " Move up"))
(button :class "dock-menu-item" :visible {dock-sel-pinned}
:onclick "scripts/dock.sh move-down ${dock-sel-class}"
(label :halign "start" :text " Move down"))
(button :class "dock-menu-item danger" :visible {dock-sel-running}
:onclick "scripts/dock.sh close ${dock-sel-class}"
(label :halign "start" :text " Close"))))
;; ---------------------------------------------------------------------------
;; SPOTLIGHT LAUNCHER
;; ---------------------------------------------------------------------------
(defwidget spot-row [r]
(button :class "spot-row" :onclick "scripts/spotlight.sh activate ${r.idx}"
(box :space-evenly false :spacing 12 :valign "center"
(box :class "spot-icon" :valign "center"
(box :visible {r.img != ""} :class "spot-img"
:style "background-image: url('${r.img}');")
(label :visible {r.img == ""} :class "spot-glyph" :text {r.icon}))
(box :orientation "vertical" :space-evenly false :valign "center" :hexpand true
(label :class "spot-title" :halign "start" :limit-width 64 :text {r.title})
(label :class "spot-sub" :halign "start" :limit-width 76 :show-truncated false :text {r.sub})))))
(defwidget spot-pill [mode icon label ?key]
(button :class {spot-mode == mode ? "spot-pill active" : "spot-pill"}
:onclick "eww update spot-mode='${mode}' spot-q='' && scripts/spotlight.sh qmode ${mode} ''"
(box :space-evenly false :spacing 6 :valign "center"
(label :class "spot-pill-icon" :text icon)
(label :class "spot-pill-label" :text label)
(label :class "spot-pill-key" :visible {(key ?: "") != ""} :text {key ?: ""}))))
(defwidget spotlight []
(box :class {colorscheme == "light" ? "spot light" : "spot"}
:orientation "vertical" :space-evenly false
;; The search box is OUTSIDE the slide revealer so the input is realized the
;; moment the window maps — GTK then auto-focuses it (it's the first focusable
;; widget) and, since the surface has exclusive keyboard focus, you can type
;; immediately without clicking. (Wrapping it in a collapsed revealer left the
;; input unrealized → keys went nowhere until you clicked it.)
(box :class "spot-search" :space-evenly false :spacing 12 :valign "center"
(label :class "spot-search-icon" :text "")
(input :class "spot-input" :hexpand true :value spot-q
:onchange "scripts/spotlight.sh type '{}'"
:onaccept "scripts/spotlight.sh activate 0"))
;; pills + results slide in (the animated part)
(revealer :transition "slidedown" :duration "200ms" :reveal spot-open
(box :orientation "vertical" :space-evenly false
(box :class "spot-pills" :space-evenly false :spacing 6 :halign "start"
(spot-pill :mode "apps" :icon "󰀻" :label "Apps")
(spot-pill :mode "clip" :icon "󰅍" :label "Pastebin" :key ";")
(spot-pill :mode "emoji" :icon "󰞅" :label "Emoji" :key ":")
(spot-pill :mode "calc" :icon "󰪚" :label "Rechner" :key "=")
(spot-pill :mode "web" :icon "󰖟" :label "Web" :key "?")
(spot-pill :mode "files" :icon "󰈔" :label "Dateien" :key "/"))
(revealer :transition "slidedown" :duration "120ms" :reveal {arraylength(spot-results) > 0}
(box :class "spot-list" :orientation "vertical" :space-evenly false :spacing 2
(for r in spot-results
(spot-row :r r))))))))
;; ---------------------------------------------------------------------------
;; WINDOWS
;; ---------------------------------------------------------------------------
;; NOTE: no outer reveal-box — the spotlight widget keeps its input always
;; realized (so it gets keyboard focus on open) and animates its body internally.
(defwindow spotlight
:monitor 0
:geometry (geometry :y "-10%" :width "640px" :anchor "center")
:stacking "overlay"
:focusable "exclusive"
(spotlight))
;; click-outside catcher for Spotlight — opened BEFORE spotlight so it stacks
;; below. A click anywhere outside the launcher dismisses it. This is the only
;; reliable dismiss in niri (no global Escape bind) and self-heals a stuck
;; spotlight surface — which, being :focusable, would otherwise hold the keyboard.
(defwindow spot-backdrop
:monitor 0
:geometry (geometry :width "100%" :height "100%" :anchor "center")
:stacking "overlay"
:focusable false
;; Animated dismiss: spotlight.sh close drops the backdrop instantly (lockout-
;; safe) then slides the body out + unmaps from a detached process.
(eventbox :hexpand true :vexpand true
:onclick "scripts/spotlight.sh close"
:onrightclick "scripts/spotlight.sh close"
(box :class "spot-backdrop" :hexpand true :vexpand true)))
(defwindow bar
:geometry (geometry :x "0%" :y "0%" :width "100%" :height "26px" :anchor "top center")
:stacking "fg"
:exclusive true
:focusable false
(bar))
(defwindow control-center
:monitor 0
:geometry (geometry :x "0px" :y "2px" :width "380px" :anchor "top right")
:stacking "overlay"
:focusable false
(reveal-box :open panel-open (control-center)))
(defwindow calendar
:monitor 0
:geometry (geometry :x "0px" :y "2px" :width "320px" :anchor "top center")
:stacking "overlay"
:focusable false
(reveal-box :open panel-open (cal)))
(defwindow music
:monitor 0
:geometry (geometry :x "0px" :y "2px" :width "320px" :anchor "top right")
:stacking "overlay"
:focusable false
(reveal-box :open panel-open (music-panel)))
(defwindow sysmon
:monitor 0
:geometry (geometry :x "0px" :y "2px" :width "340px" :anchor "top right")
:stacking "overlay"
:focusable false
(reveal-box :open panel-open (sysmon-panel)))
(defwindow updates
:monitor 0
:geometry (geometry :x "0px" :y "2px" :width "440px" :anchor "top right")
:stacking "overlay"
:focusable false
(reveal-box :open panel-open (updates-panel)))
;; click-outside catcher for the panels — covers everything BELOW the bar (so
;; bar symbols stay clickable to switch panels). A click closes the open panel.
(defwindow panel-backdrop
:monitor 0
:geometry (geometry :x "0px" :y "36px" :width "100%" :height "100%" :anchor "top left")
:stacking "overlay"
:focusable false
;; Animated dismiss: panel.sh close drops the backdrop instantly (lockout-safe)
;; then fades the panel out + unmaps from a detached process. (An instant close
;; here skipped the fade-out entirely.)
(eventbox :hexpand true :vexpand true
:onclick "scripts/panel.sh close"
:onrightclick "scripts/panel.sh close"
(box :class "panel-backdrop" :hexpand true :vexpand true)))
(defwindow notification-popup
:monitor 0
:geometry (geometry :x "0px" :y "8px" :width "360px" :anchor "top right")
:stacking "overlay"
:focusable false
(notif-popup))
;; the dock — content-sized; opened on hover of the trigger strip, hidden by the
;; dock's own hover/hoverlost (catcher-free; niri has no cursor IPC).
;; Opened only while visible. On open the revealer is collapsed (invisible), then
;; dock-open flips true and it slides in; on hide it slides out, then the window
;; closes. (niri doesn't animate layer map/unmap, so the revealer does it.)
;; NO fixed width — width is the axis `slideright` animates, so it must be free to
;; grow with the revealer (a fixed width makes the layer surface full-size at once
;; and the slide looks instant). Height is content-sized too. Mirrors how the
;; panels leave their animated axis (height, for slidedown) unfixed.
(defwindow dock
:monitor 0
:geometry (geometry :x "6px" :y "0px" :anchor "left center")
:stacking "overlay"
:focusable false
(dock))
;; leave-catcher: full-screen, opened by the watcher while the dock is visible.
;; It lives on the `fg` (top) layer-shell layer while the dock + trigger are on
;; `overlay` (which is ABOVE fg). So the dock/trigger are ALWAYS above the catcher
;; regardless of map order — hovering the dock never hits the catcher. The catcher
;; is still above normal app windows, so moving the pointer anywhere off the dock
;; enters it → reliable "leave" (debounced hide), in every direction.
(defwindow dock-catcher
:monitor 0
:geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "center")
:stacking "fg"
:focusable false
(dock-catcher))
;; always-on hover strip (6px) at the left edge that opens the dock.
(defwindow dock-trigger
:monitor 0
:geometry (geometry :x "0px" :y "0px" :width "6px" :height "60%" :anchor "left center")
:stacking "overlay"
:focusable false
(dock-trigger))
;; (no dock-backdrop window — auto-hide is catcher-free; see the dock widget.)
;; full-screen transparent catcher behind the menu — a click anywhere outside
;; the menu dismisses it. Opened BEFORE the menu so the menu stacks on top.
;; starts to the RIGHT of the dock (~104px) so the dock icons stay
;; right-clickable — right-clicking another icon switches the menu. A click
;; anywhere on the backdrop (left or right button) dismisses it.
(defwindow dock-menu-backdrop
:monitor 0
:geometry (geometry :x "104px" :y "0px" :width "100%" :height "100%" :anchor "top left")
:stacking "overlay"
:focusable false
;; onhover keeps the dock alive while the menu is up; clicking dismisses both
;; the menu and (debounced) the dock.
(eventbox :hexpand true :vexpand true :onhover "scripts/dock.sh show"
:onclick "eww close dock-menu dock-bg-menu dock-menu-backdrop && scripts/dock.sh hide"
:onrightclick "eww close dock-menu dock-bg-menu dock-menu-backdrop && scripts/dock.sh hide"
(box :class "dock-menu-backdrop" :hexpand true :vexpand true)))
;; right-click context menu — right of the dock, vertically centred. NO fixed
;; height: with a fixed height eww centres that tiny box and the real content
;; spills downward (looked vertically shifted). Content-sized + center-left
;; anchors the whole menu's middle to the screen middle. (niri exposes no cursor
;; Y, so we can't track the clicked icon's height; centred is the clean default.)
(defwindow dock-menu
:monitor 0
:geometry (geometry :x "104px" :y "0px" :anchor "center left")
:stacking "overlay"
:focusable false
(dock-menu))
;; vertically centred next to the dock (no fixed y/height — a fixed height made
;; eww centre the tiny box and the content spilled, and y=150 sat too high).
(defwindow dock-bg-menu
:monitor 0
:geometry (geometry :x "104px" :y "0px" :width "210px" :anchor "center left")
:stacking "overlay"
:focusable false
(dock-bg-menu))
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Launch the eww bar on every monitor. Safe to call from Hyprland autostart.
# If eww isn't installed yet, fall back to waybar so you're never bar-less.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v eww >/dev/null 2>&1; then
echo "eww not found — falling back to waybar" >&2
exec waybar
fi
# niri helpers — provides $WM (=niri), wm_outputs, wm_blur_layer, …
. "$DIR/scripts/wm.sh"
# Defensive: if GDK_SCALE is exported anywhere, unset it so eww uses native
# Wayland fractional scaling (GTK >= 3.24.49) and renders crisp at scale 1.25.
# (The niri config deliberately does NOT set GDK_SCALE.)
unset GDK_SCALE GDK_DPI_SCALE
# Make sure helper scripts are executable.
chmod +x "$DIR"/scripts/*.sh 2>/dev/null
# Stop waybar (we're replacing it).
pkill -x waybar 2>/dev/null
# HARD-restart the daemon. A plain `eww close-all` is NOT enough: across daemon
# hiccups/restarts the compositor keeps old layer surfaces mapped while the daemon
# forgets them — orphans that pile up (e.g. "5 header bars") and that eww can no
# longer close (it doesn't know their ids). Killing the daemon makes the
# compositor drop ALL its surfaces, so we always start from a truly clean slate.
eww kill 2>/dev/null
pkill -9 -f '[e]ww daemon' 2>/dev/null
sleep 0.5
# Give it time to initialise (cold start ~2s) before opening windows, or the open
# calls no-op against a not-yet-ready server.
eww daemon >/dev/null 2>&1 &
sleep 2
eww close-all 2>/dev/null
# Use the compositor's active-output count as 0-based eww --screen indices.
n=$(wm_outputs)
[ -z "$n" ] || [ "$n" -lt 1 ] 2>/dev/null && n=1
m=0
while [ "$m" -lt "$n" ]; do
eww open bar --id "bar-$m" --screen "$m" 2>/dev/null
m=$((m+1))
done
# Auto-hide dock + frosted-glass blur for the gtk-layer-shell namespace.
# (Blur/rounding/shadow live in ~/.config/niri/config.kdl as a layer-rule;
# wm_blur_layer is a no-op on niri.)
wm_blur_layer "gtk-layer-shell"
eww close dock dock-backdrop dock-menu 2>/dev/null
# Auto-hide: a thin left-edge hover trigger requests "show"; the dock keeps itself
# alive on hover and requests "leave" on hoverlost. The actual open/close + timing
# is done by the `dock.sh watch` DAEMON below — NOT in the eww hover handlers,
# because eww kills a handler the moment it sleeps (which would strand the dock /
# a backdrop and lock out the pointer). Handlers only write intent to a state file.
eww update dock-open=false 2>/dev/null
eww close dock dock-catcher 2>/dev/null # start hidden (mapped on hover)
printf 'hidden' > "$HOME/.cache/eww/.dock-hide-token" 2>/dev/null
eww open dock-trigger 2>/dev/null
pkill -f '[d]ock\.sh watch' 2>/dev/null
"$DIR/scripts/dock.sh" watch >/dev/null 2>&1 &
# Accent: mirror TANINUX's gui.json into eww's $accent and keep it in sync.
pkill -f "accent.sh watch" 2>/dev/null
"$DIR/scripts/accent.sh" watch >/dev/null 2>&1 &
# UI font: follow the GNOME/TANINUX appearance font (gsettings font-name).
pkill -f "font.sh watch" 2>/dev/null
"$DIR/scripts/font.sh" watch >/dev/null 2>&1 &
# Panel/Dock settings: mirror TANINUX's panel.json (icon size, modules, …).
pkill -f "panelcfg.sh watch" 2>/dev/null
"$DIR/scripts/panelcfg.sh" watch >/dev/null 2>&1 &
# Safety net: close orphaned backdrops that would otherwise swallow all clicks.
# Rewritten for niri — deterministic owner-pairing via `eww active-windows`
# (no niri geometry, no daemon kill), so it no longer over-fires. See guard.sh.
pkill -f "scripts/guard.sh" 2>/dev/null
"$DIR/scripts/guard.sh" >/dev/null 2>&1 &
# Spotlight: clipboard history watcher (cliphist) + warm the app index.
if command -v cliphist >/dev/null 2>&1; then
pkill -f "wl-paste --watch cliphist store" 2>/dev/null
wl-paste --type text --watch cliphist store >/dev/null 2>&1 &
wl-paste --type image --watch cliphist store >/dev/null 2>&1 &
fi
"$DIR/scripts/spotlight.sh" reindex >/dev/null 2>&1 &
# --- Notifications: tiramisu (headless) + our eww UI, replacing mako --------
# mako is D-Bus-activated, so we kill it and immediately let tiramisu grab the
# org.freedesktop.Notifications name — while tiramisu holds it, D-Bus won't
# re-activate mako. If tiramisu isn't installed yet, leave mako alone.
if command -v tiramisu >/dev/null 2>&1; then
pkill -x mako 2>/dev/null
[ -f "$HOME/.cache/eww/dnd" ] || echo false > "$HOME/.cache/eww/dnd"
pkill -f "notifications.sh listen" 2>/dev/null
"$DIR/scripts/notifications.sh" listen >/dev/null 2>&1 &
sleep 0.3
"$DIR/scripts/notifications.sh" refresh 2>/dev/null
fi
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Mirror TANINUX's accent into eww.
# get print the accent hex (from gui.json, fallback default)
# apply write it into eww.scss's $accent-dim line + eww reload (if changed)
# watch apply now, then re-apply whenever gui.json changes
#
# Contract: ~/.local/share/taninux/gui.json -> field accent_hex (#rrggbb).
# Only the $accent-dim line is managed; $accent + washes derive from it in SCSS.
set -u
SCSS="$HOME/.config/eww/eww.scss"
GUI="${XDG_DATA_HOME:-$HOME/.local/share}/taninux/gui.json"
DEFAULT="#8f8aac" # ume — same as the spec's fallback
get() {
local h=""
[ -f "$GUI" ] && h=$(jq -r '.accent_hex // empty' "$GUI" 2>/dev/null)
case "$h" in
\#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) printf '%s' "$h" ;;
*) printf '%s' "$DEFAULT" ;;
esac
}
current() {
grep -m1 -oE '^\$accent-dim:[[:space:]]*#[0-9a-fA-F]{6}' "$SCSS" 2>/dev/null | grep -oE '#[0-9a-fA-F]{6}'
}
apply() {
local hex; hex=$(get)
[ "$(current)" = "$hex" ] && return 0 # nothing changed
# awk handles the literal `$` in `$accent-dim` cleanly (no sed escaping pain)
awk -v hex="$hex" '
/accent_hex \(managed/ { print "$accent-dim: " hex "; // accent_hex (managed by scripts/accent.sh)"; next }
{ print }
' "$SCSS" > "$SCSS.tmp" && mv "$SCSS.tmp" "$SCSS"
eww reload 2>/dev/null
}
case "${1:-apply}" in
get) get; echo ;;
apply) apply ;;
watch)
apply
last=$(stat -c %Y "$GUI" 2>/dev/null || echo 0)
while :; do
sleep 2
now=$(stat -c %Y "$GUI" 2>/dev/null || echo 0)
if [ "$now" != "$last" ]; then last="$now"; apply; fi
done
;;
esac
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Audio output devices via wireplumber (wpctl).
# sinks JSON list of output devices [{id,name,default}]
# set-default <id> make <id> the default sink (and move running streams)
set -u
case "${1:-sinks}" in
sinks)
wpctl status | awk '
/^Audio/{sec="audio"} /^Video/{sec="video"}
/Sinks:/{insink=(sec=="audio")?1:0; next}
/Sources:|Filters:|Streams:/{insink=0}
insink && match($0,/[0-9]+\./){
def=($0 ~ /\*/)?"true":"false"
id=substr($0,RSTART,RLENGTH-1)
name=substr($0,RSTART+RLENGTH)
sub(/ *\[vol:.*$/,"",name)
gsub(/^[ \t]+|[ \t]+$/,"",name)
printf "%s\t%s\t%s\n",id,def,name
}' \
| jq -R -s -c 'split("\n")|map(select(length>0))|map(split("\t"))
|map({id:.[0],default:(.[1]=="true"),name:.[2]})'
;;
set-default)
id="${2:-}"
[ -n "$id" ] || exit 1
wpctl set-default "$id"
# move already-playing streams onto the new default too
def=$(pactl get-default-sink 2>/dev/null)
if [ -n "$def" ]; then
pactl list short sink-inputs 2>/dev/null | awk '{print $1}' | while read -r si; do
pactl move-sink-input "$si" "$def" 2>/dev/null
done
fi
;;
esac
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Bluetooth status / toggle via bluetoothctl.
powered() { bluetoothctl show 2>/dev/null | awk '/Powered:/{print $2; exit}'; }
case "${1:-status}" in
toggle)
if [ "$(powered)" = yes ]; then bluetoothctl power off
else bluetoothctl power on; fi
;;
status|*)
if [ "$(powered)" != yes ]; then
jq -nc '{icon:"󰂲",label:"Off",on:false}'
exit 0
fi
dev=$(bluetoothctl devices Connected 2>/dev/null | head -1 | cut -d' ' -f3-)
if [ -n "$dev" ]; then
jq -nc --arg d "$dev" '{icon:"󰂱",label:$d,on:true}'
else
jq -nc '{icon:"󰂯",label:"On",on:true}'
fi
;;
esac
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Clock for the bar — strftime format comes from TANINUX's panel.json.
fmt=$(jq -r '.bar.clock_format // empty' \
"${XDG_DATA_HOME:-$HOME/.local/share}/taninux/panel.json" 2>/dev/null)
[ -z "$fmt" ] && fmt='%H:%M · %A, %d %B %Y'
date "+$fmt"
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Reports GNOME's light/dark preference for eww.
# get print "light" or "dark" once
# listen print it now, then again on every change (for deflisten)
set -u
KEY="org.gnome.desktop.interface color-scheme"
emit() {
case "$(gsettings get $KEY 2>/dev/null)" in
*dark*) echo dark ;;
*) echo light ;; # 'prefer-light' and 'default' -> light
esac
}
case "${1:-get}" in
get) emit ;;
listen)
emit
gsettings monitor $KEY 2>/dev/null | while read -r _; do emit; done
;;
esac
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Close every eww overlay and reset the animation vars in one shot.
# Entry point for niri's panic bind (Mod+Escape) and any "close everything" need.
# Safe to call anytime — closing a not-open window is a no-op.
set -u
eww update panel-open=false dock-open=false spot-open=false 2>/dev/null
eww close \
spotlight spot-backdrop \
dock-menu dock-bg-menu dock-menu-backdrop \
control-center calendar music sysmon updates panel-backdrop \
dock 2>/dev/null
@@ -0,0 +1,308 @@
#!/usr/bin/env bash
# Dock backend (niri): pinned apps (real Papirus icons) + running
# apps, launch/focus, pin/unpin, new-window, close, and a hover-driven auto-hide.
#
# apps JSON [{exec,class,icon,running,pinned}] (pinned first)
# launch <class> <exec...> focus if running, else launch; then hide menus
# new <exec...> always launch a fresh instance
# close <class> close all windows of that class
# pin <class> add a running app to the pinned list
# unpin <class> remove an app from the pinned list
# show open the dock now (cancels a pending hide)
# hide hide the dock after HIDE_DELAY (debounced)
set -u
# ---- settings -------------------------------------------------------------
HIDE_DELAY=$(jq -r '.dock.hide_delay // 0.4' "${XDG_DATA_HOME:-$HOME/.local/share}/taninux/panel.json" 2>/dev/null)
case "$HIDE_DELAY" in ''|*[!0-9.]*) HIDE_DELAY="${DOCK_HIDE_DELAY:-0.4}";; esac
: "${HIDE_DELAY:=0.4}" # seconds the dock lingers after the
# pointer leaves it (decimals allowed)
ANIM=0.2 # slide duration — matches the revealer
dlog() { :; } # diagnostics off (flip to append-to-file if you need to trace)
CFG="$HOME/.config/eww"
PINS="$CFG/dock-pins" # one app per line: exec|class|icon
HTOK="$HOME/.cache/eww/.dock-hide-token"
STAMP="$HOME/.cache/eww/.dock-menu-stamp" # icon right-click marker (vs bg)
mkdir -p "$CFG" "$HOME/.cache/eww"
# seed the default pins on first run
if [ ! -f "$PINS" ]; then
cat > "$PINS" <<'EOF'
librewolf|librewolf|librewolf
kitty|kitty|kitty
nautilus|org.gnome.Nautilus|org.gnome.Nautilus
code|Code|code-oss
obsidian|obsidian|obsidian
EOF
fi
# ---- niri helpers ---------------------------------------------------------
. "$(dirname "$0")/wm.sh"
clients_json() { wm_clients; } # normalised [{id,pid,class,title,ws}]
# Hide is handled by the `watch` daemon (see bottom), NOT here — because eww
# kills an event-handler process the moment it lingers (a `sleep` for the
# slide-out gets killed → `eww close` never runs → the dock/backdrop sticks).
# So all the hover handlers do is write an instant intent to $HTOK; the daemon
# (a normal long-running process eww can't kill) does the timing + open/close.
#
# $HTOK states: show | leave:<epoch_ms> | hidden
dock_dismiss() { # instant: request hide + close menus
eww close dock-menu dock-menu-backdrop dock-bg-menu 2>/dev/null
printf 'hidden' > "$HTOK" 2>/dev/null
}
HIDE_MS=$(awk "BEGIN{print int($HIDE_DELAY*1000)}") # debounce in ms for the daemon
# On-screen Y (logical px) of the TOP of dock icon <idx>, derived from the
# vertically-centred dock layout (niri has no cursor IPC, but we know the index).
# Used to place the right-click menu at the icon's height.
menu_y() {
local idx="${1:-0}" H N isz itemH pitch content dtop y maxy
H=$(niri msg --json outputs 2>/dev/null | jq -r 'first(.[] | .logical.height // empty) // 1152' 2>/dev/null)
case "$H" in ''|*[!0-9]*) H=1152;; esac
N=$(eww get dock-apps 2>/dev/null | jq 'length' 2>/dev/null); case "$N" in ''|*[!0-9]*) N=1;; esac
isz=$(eww get cfg-icon-size 2>/dev/null); case "$isz" in ''|*[!0-9]*) isz=42;; esac
[ "$isz" -lt 42 ] && isz=42 # scss enforces a 42px min icon
itemH=$((isz + 8)); pitch=$((itemH + 10)) # +padding, +inter-item spacing
content=$((20 + N * itemH + (N - 1) * 10)) # +dock padding top/bottom
dtop=$(( (H - content) / 2 )); [ "$dtop" -lt 0 ] && dtop=0
y=$(( dtop + 10 + idx * pitch )) # top of icon <idx>
maxy=$(( H - 280 )); [ "$maxy" -lt 8 ] && maxy=8 # keep an ~270px menu on screen
[ "$y" -gt "$maxy" ] && y=$maxy
[ "$y" -lt 8 ] && y=8
printf '%s' "$y"
}
# ---- helpers --------------------------------------------------------------
resolve_icon() { # icon name -> best file path (Papirus svg preferred)
local name="$1" base sz ext f
[ -z "$name" ] && name="application-x-executable"
for base in /usr/share/icons/Papirus-Dark /usr/share/icons/Papirus; do
for sz in 64x64 48x48 32x32 24x24; do
for ext in svg png; do
[ -f "$base/$sz/apps/$name.$ext" ] && { printf '%s' "$base/$sz/apps/$name.$ext"; return; }
done
done
done
f=$(find /usr/share/icons/hicolor -type f \( -name "$name.svg" -o -name "$name.png" \) 2>/dev/null \
| sort -t/ -k6 -rn | head -1)
[ -n "$f" ] && { printf '%s' "$f"; return; }
for ext in svg png xpm; do
[ -f "/usr/share/pixmaps/$name.$ext" ] && { printf '%s' "/usr/share/pixmaps/$name.$ext"; return; }
done
# generic fallback so every tile has something
resolve_generic
}
resolve_generic() {
local p
for p in /usr/share/icons/Papirus-Dark/64x64/apps/application-x-executable.svg \
/usr/share/icons/Papirus/64x64/apps/application-x-executable.svg; do
[ -f "$p" ] && { printf '%s' "$p"; return; }
done
printf ''
}
lc() { printf '%s' "$1" | tr 'A-Z' 'a-z'; }
running_classes() { clients_json | jq -r '.[].class' | tr 'A-Z' 'a-z'; }
is_running() { running_classes | grep -qx "$(lc "$1")"; }
# open windows of a class -> [{address, title, ws}] (for the multi-instance menu)
win_list() {
clients_json \
| jq -c --arg c "$(lc "$1")" '
[ .[] | select((.class|ascii_downcase)==$c)
| { address: (.id|tostring),
title: ((.title // "") | if length>40 then .[0:40]+"…" else . end),
ws: .ws } ]'
}
emit_row() { # exec class iconname pinned
local exec="$1" class="$2" icon="$3" pinned="$4" run=false
is_running "$class" && run=true
printf '%s\t%s\t%s\t%s\t%s\n' "$exec" "$class" "$(resolve_icon "$icon")" "$run" "$pinned"
}
# ---- commands -------------------------------------------------------------
case "${1:-apps}" in
apps)
{
pinned_classes=""
while IFS='|' read -r exec class icon; do
[ -z "$exec" ] && continue
emit_row "$exec" "$class" "$icon" true
pinned_classes="$pinned_classes $(lc "$class")"
done < "$PINS"
# running apps that aren't pinned -> show them too (unpinned)
clients_json \
| jq -r '.[]|select(.class!="")|"\(.pid)\t\(.class)"' | sort -u \
| while IFS=$'\t' read -r pid class; do
case " $pinned_classes " in *" $(lc "$class") "*) continue;; esac
exe=$(basename "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" 2>/dev/null)
[ -z "$exe" ] && exe="$class"
emit_row "$exe" "$class" "$(lc "$class")" false
done
} | awk '!seen[$2]++' \
| jq -R -s -c 'split("\n")|map(select(length>0))|map(split("\t"))
|map({exec:.[0],class:.[1],icon:.[2],running:(.[3]=="true"),pinned:(.[4]=="true")})
|to_entries|map(.value + {idx: .key})'
;;
launch)
class="${2:-}"; shift 2 2>/dev/null || shift $#
# focus the running window (niri jumps to its workspace) else launch it
id=$(clients_json \
| jq -r --arg c "$(lc "$class")" '[.[]|select((.class|ascii_downcase)==$c)][0].id // empty')
if [ -n "$id" ]; then
wm_focus "$id"
else
setsid -f ${*} >/dev/null 2>&1
fi
dock_dismiss
;;
menu-open)
printf '%s' "$(date +%s%N)" > "$STAMP" 2>/dev/null # mark: an icon was right-clicked
class="${2:-}"; exe="${3:-}"; pinned="${4:-false}"; running="${5:-false}"; idx="${6:-}"
eww update dock-sel-class="$class" dock-sel-exec="$exe" \
dock-sel-pinned="$pinned" dock-sel-running="$running" 2>/dev/null
eww update "dock-sel-windows=$(win_list "$class")" 2>/dev/null
eww close dock-bg-menu 2>/dev/null # icon menu wins over the background menu
eww open dock-menu-backdrop 2>/dev/null
eww close dock-menu 2>/dev/null # re-open fresh so a switch re-renders
# niri has no cursor Y, but we KNOW the clicked icon's index — so compute its
# on-screen Y from the (vertically-centred) dock layout and open the menu
# there with --pos, instead of always centring it.
eww open dock-menu --anchor "top left" --pos "104x$(menu_y "${idx:-0}")" 2>/dev/null
;;
bg-menu)
# The icon's button onrightclick fires first (and stamps); this eventbox
# onrightclick lands here right after. If an icon was just clicked, suppress
# the background menu — only open it on a true empty-area right-click.
now=$(date +%s%N); last=$(cat "$STAMP" 2>/dev/null || echo 0)
[ "$(( (now - last) / 1000000 ))" -lt 250 ] && exit 0
eww close dock-menu 2>/dev/null
eww open dock-menu-backdrop 2>/dev/null
eww close dock-bg-menu 2>/dev/null
eww open dock-bg-menu 2>/dev/null
;;
new)
shift
setsid -f ${*} >/dev/null 2>&1
eww close dock-menu dock-menu-backdrop 2>/dev/null
;;
focus-win)
# focus one specific window by niri window id (jumps to its workspace)
id="${2:-}"
[ -n "$id" ] && wm_focus "$id"
dock_dismiss
;;
close)
# kill by PID — robust, independent of the IPC focus API
class="${2:-}"
for pid in $(clients_json \
| jq -r --arg c "$(lc "$class")" '.[]|select((.class|ascii_downcase)==$c)|.pid'); do
[ "$pid" -gt 0 ] 2>/dev/null && kill "$pid" 2>/dev/null
done
dock_dismiss
;;
pin)
class="${2:-}"
grep -qiE "^[^|]*\|${class}\|" "$PINS" 2>/dev/null && { eww close dock-menu dock-menu-backdrop 2>/dev/null; exit 0; }
pid=$(clients_json \
| jq -r --arg c "$(lc "$class")" '[.[]|select((.class|ascii_downcase)==$c)][0].pid // empty')
exe=""
[ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null && exe=$(basename "$(readlink -f "/proc/$pid/exe" 2>/dev/null)" 2>/dev/null)
[ -z "$exe" ] && exe="$class"
printf '%s|%s|%s\n' "$exe" "$class" "$(lc "$class")" >> "$PINS"
eww close dock-menu dock-menu-backdrop 2>/dev/null
;;
unpin)
class="${2:-}"
tmp=$(mktemp)
grep -viE "^[^|]*\|${class}\|" "$PINS" > "$tmp" 2>/dev/null
mv "$tmp" "$PINS"
eww close dock-menu dock-menu-backdrop 2>/dev/null
;;
move-up|move-down)
# reorder the pinned app in dock-pins (swap with its neighbour). Order in
# dock-pins == order in the dock. Menu stays open so you can move repeatedly.
class="${2:-}"
mapfile -t L < "$PINS" 2>/dev/null
idx=-1
for i in "${!L[@]}"; do case "${L[$i]}" in *"|${class}|"*) idx=$i; break;; esac; done
if [ "$idx" -ge 0 ]; then
if [ "$1" = move-up ]; then j=$((idx-1)); else j=$((idx+1)); fi
if [ "$j" -ge 0 ] && [ "$j" -lt "${#L[@]}" ]; then
t="${L[$idx]}"; L[$idx]="${L[$j]}"; L[$j]="$t"
printf '%s\n' "${L[@]}" > "$PINS"
eww update dock-apps="$("$0" apps)" 2>/dev/null # reflect immediately
fi
fi
;;
# --- INSTANT intent writes (safe inside eww handlers — no sleep to kill) ----
show) printf 'show' > "$HTOK" 2>/dev/null ;; # trigger/dock hover → want shown
keepalive) printf 'show' > "$HTOK" 2>/dev/null ;; # pointer on dock/icon → cancel hide
hide) printf 'leave:%s' "$(date +%s%3N)" > "$HTOK" 2>/dev/null ;; # pointer left at <ms>
hide-now) printf 'hidden' > "$HTOK" 2>/dev/null ;; # close immediately
# --- the auto-hide daemon: started once by launch.sh -----------------------
# eww can't kill THIS (it's not an event handler), so the timing is safe here.
#
# The dock window is mapped on show and CLOSED on hide (so nothing is visible
# when hidden — an always-mapped collapsed dock leaves a faint sliver). The
# revealer animates because the window has no fixed width (the slide axis is
# free to grow). All the open/sleep/close lives HERE in the daemon, never in an
# eww handler, so eww can't kill it mid-animation.
#
# Reconciles to $HTOK: show | leave:<epoch_ms> | hidden
watch)
# Start from a guaranteed-clean hidden state (closes any stale dock/catcher a
# crash or restart left mapped — otherwise a collapsed dock shows a sliver).
eww update dock-open=false 2>/dev/null
eww close dock dock-catcher 2>/dev/null
printf 'hidden' > "$HTOK" 2>/dev/null
vis=0
dlog "watch" "START (forced hidden)"
while :; do
st=$(cat "$HTOK" 2>/dev/null)
case "$st" in
show)
if [ "$vis" = 0 ]; then
dlog "watch" "SHOW open"
eww update dock-open=false 2>/dev/null # collapsed before map
eww open dock-catcher 2>/dev/null # catcher first (below)…
eww open dock 2>/dev/null # …dock after → on top
sleep 0.12 # let it realize collapsed
[ "$(cat "$HTOK" 2>/dev/null)" = show ] || { eww close dock dock-catcher 2>/dev/null; continue; }
eww update dock-open=true 2>/dev/null # slide in (revealer)
vis=1
fi
;;
leave:*)
if [ "$vis" = 1 ]; then
t=${st#leave:}; now=$(date +%s%3N)
if [ "$((now - t))" -ge "$HIDE_MS" ]; then
dlog "watch" "HIDE (leave aged $((now-t))ms)"
eww update dock-open=false 2>/dev/null # slide out
sleep "$ANIM"
if [ "$(cat "$HTOK" 2>/dev/null)" = show ]; then
eww update dock-open=true 2>/dev/null # came back mid-slide
else
eww close dock dock-catcher 2>/dev/null # unmap both → no sliver
vis=0
printf 'hidden' > "$HTOK" 2>/dev/null
fi
fi
fi
;;
hidden)
if [ "$vis" = 1 ]; then
dlog "watch" "HIDE (hidden state)"
eww update dock-open=false 2>/dev/null
sleep "$ANIM"
if [ "$(cat "$HTOK" 2>/dev/null)" = show ]; then eww update dock-open=true 2>/dev/null
else eww close dock dock-catcher 2>/dev/null; vis=0; fi
fi
;;
esac
sleep 0.05
done
;;
esac
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Full system update (repo + AUR) inside the eww Updates panel — no terminal.
# sudo auth via a zenity GUI password dialog; paru output streamed to eww vars:
# upd-running true while the update runs (panel swaps button → progress)
# upd-progress the latest output line
set -u
EWW(){ eww update "$@" 2>/dev/null; }
CACHE="$HOME/.cache/eww"; mkdir -p "$CACHE"
ASK="$CACHE/askpass.sh"
cat > "$ASK" <<'EOF'
#!/bin/sh
zenity --password --title="System-Update" --width=340 2>/dev/null
EOF
chmod +x "$ASK"
EWW upd-running=true upd-progress="Authentifiziere…"
# --sudoloop keeps the sudo timestamp warm after the single askpass prompt, so the
# AUR build steps (makepkg → sudo pacman) never re-prompt on a missing tty.
SUDO_ASKPASS="$ASK" stdbuf -oL paru -Syu --noconfirm --skipreview --sudoloop --sudoflags -A 2>&1 \
| while IFS= read -r line; do [ -n "$line" ] && EWW upd-progress="$line"; done
rc=${PIPESTATUS[0]}
if [ "$rc" -eq 0 ]; then EWW upd-progress="✓ System aktuell"; else EWW upd-progress="✗ Fehlgeschlagen (Code $rc)"; fi
EWW updates-list="$("$HOME/.config/eww/scripts/updates.sh" list)"
sleep 3
EWW upd-running=false upd-progress=""
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Mirror the GNOME UI font into eww's font-family (analogous to accent.sh).
# The system font is set in the TANINUX/GTK appearance settings; eww follows it.
# get print the resolved family
# apply rewrite the font-family line in eww.scss + reload
# watch apply now, then re-apply whenever gsettings font-name changes
set -u
DIR="$(cd "$(dirname "$0")/.." && pwd)"
SCSS="$DIR/eww.scss"
family() {
# Pango string e.g. "Iosevka Aile Semi-Bold 11" → family "Iosevka Aile".
local f
f=$(gsettings get org.gnome.desktop.interface font-name 2>/dev/null | sed "s/^'//; s/'$//")
f=$(printf '%s' "$f" | sed -E 's/ [0-9]+(\.[0-9]+)?$//') # drop size
f=$(printf '%s' "$f" | sed -E 's/ (Thin|Extra-?Light|Light|Semi-?Light|Regular|Book|Medium|Semi-?Bold|Demi-?Bold|Bold|Extra-?Bold|Ultra-?Bold|Black|Heavy|Italic|Oblique|Condensed|Expanded)$//Ig')
[ -n "$f" ] && printf '%s' "$f" || printf 'Iosevka Aile'
}
apply() {
local fam; fam=$(family)
sed -i -E "s|^(\s*font-family:).*|\1 \"$fam\", \"Symbols Nerd Font\", sans-serif;|" "$SCSS"
eww reload >/dev/null 2>&1
}
case "${1:-apply}" in
get) family; echo ;;
apply) apply ;;
watch)
apply
last=$(gsettings get org.gnome.desktop.interface font-name 2>/dev/null)
while :; do
sleep 3
now=$(gsettings get org.gnome.desktop.interface font-name 2>/dev/null)
[ "$now" != "$last" ] && { last="$now"; apply; }
done ;;
esac
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Safety net: never let an orphaned eww backdrop block all input.
#
# The lockout failure mode: a full-screen click-catcher (panel-backdrop /
# spot-backdrop / dock-menu-backdrop) stays mapped after its owner window is gone
# (a race, a crash, an interrupted close) — niri keeps the layer surface up and
# it swallows every click. (The dock is catcher-free, so it can't cause this.)
#
# Detection is DETERMINISTIC and niri-geometry-free: a backdrop is an orphan iff
# it is open while NONE of its owner windows are. We only ever close eww windows
# (never kill the daemon), and we require the orphan to persist across two polls
# (~1.4 s) so a normal open/close animation never trips it.
set -u
EWW=eww
INTERVAL=0.7 # poll period
STRIKES_NEEDED=2 # consecutive orphan readings before acting
# backdrop -> regex of windows that legitimise it being open
declare -A OWNER=(
[panel-backdrop]='^(control-center|calendar|music|sysmon|updates):'
[spot-backdrop]='^spotlight:'
[dock-menu-backdrop]='^(dock-menu|dock-bg-menu):'
[dock-catcher]='^dock:'
)
# var to reset when we force-close each backdrop (so the next open is clean)
declare -A RESET=(
[panel-backdrop]=panel-open
[spot-backdrop]=spot-open
)
declare -A strike
for b in "${!OWNER[@]}"; do strike[$b]=0; done
while :; do
sleep "$INTERVAL"
wins=$("$EWW" active-windows 2>/dev/null) || continue
[ -n "$wins" ] || continue
for b in "${!OWNER[@]}"; do
if printf '%s\n' "$wins" | grep -q "^${b}:"; then
# backdrop is open — orphan only if no owner is open
if printf '%s\n' "$wins" | grep -qE "${OWNER[$b]}"; then
strike[$b]=0 # legit (owner present)
else
strike[$b]=$(( strike[$b] + 1 ))
if [ "${strike[$b]}" -ge "$STRIKES_NEEDED" ]; then
strike[$b]=0
"$EWW" close "$b" 2>/dev/null
[ -n "${RESET[$b]:-}" ] && "$EWW" update "${RESET[$b]}=false" 2>/dev/null
fi
fi
else
strike[$b]=0 # backdrop not open
fi
done
# NOTE: the dock window is intentionally always mapped (collapsed when hidden,
# dock-open=false). That is the normal hidden state now — NOT an orphan — so the
# guard must never close it. The dock is catcher-free and tiny when collapsed,
# so it can't cause a lockout anyway.
done
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Emit current player state as JSON for eww (playerctl). Caches remote cover art.
cache="$HOME/.cache/eww"
mkdir -p "$cache"
last=""
last_art=""
emit() {
local status title artist art icon cover
status=$(playerctl status 2>/dev/null)
[ -z "$status" ] && status="Stopped"
title=$(playerctl metadata title 2>/dev/null)
artist=$(playerctl metadata artist 2>/dev/null)
art=$(playerctl metadata mpris:artUrl 2>/dev/null)
if [ "$status" = "Playing" ]; then icon="󰏤"; else icon="󰐊"; fi
cover=""
case "$art" in
file://*) cover="${art#file://}";;
http*://*)
if [ "$art" != "$last_art" ]; then
curl -sL --max-time 5 "$art" -o "$cache/cover.img" 2>/dev/null && cover="$cache/cover.img"
else
cover="$cache/cover.img"
fi
;;
esac
last_art="$art"
jq -nc --arg s "$status" --arg t "$title" --arg a "$artist" --arg i "$icon" --arg c "$cover" \
'{status:$s,title:$t,artist:$a,icon:$i,cover:$c}'
}
while true; do
cur=$(emit)
if [ "$cur" != "$last" ]; then
printf '%s\n' "$cur"
last="$cur"
fi
sleep 1
done
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Network status / toggle via NetworkManager.
case "${1:-status}" in
toggle)
if [ "$(nmcli radio wifi)" = enabled ]; then nmcli radio wifi off
else nmcli radio wifi on; fi
;;
status|*)
line=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null \
| grep -v ':loopback' | head -1)
name=$(printf '%s' "$line" | cut -d: -f1)
type=$(printf '%s' "$line" | cut -d: -f2)
icon="󰖪"; label="Offline"
if [ -n "$name" ]; then
case "$type" in
*wireless*) icon="󰖩"; label="$name";;
*ethernet*) icon="󰈀"; label="$name";;
*) icon="󰈀"; label="$name";;
esac
fi
jq -nc --arg i "$icon" --arg l "$label" '{icon:$i,label:$l}'
;;
esac
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Night light (wlsunset) status / toggle.
# Matches the args used in the niri config.kdl autostart.
ARGS="-l 47 -L 8.3 -d 600 -t 4000 -T 6500"
case "${1:-toggle}" in
status)
pgrep -x wlsunset >/dev/null && echo true || echo false
;;
toggle)
if pgrep -x wlsunset >/dev/null; then
pkill -x wlsunset
else
# shellcheck disable=SC2086
setsid wlsunset $ARGS >/dev/null 2>&1 &
fi
;;
esac
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Notification bridge: tiramisu (headless daemon) -> eww UI.
# Replaces mako. Keeps a JSON history, drives the control-center list and a
# top-right popup toast, and respects a Do-not-Disturb flag.
#
# listen run tiramisu and feed notifications into eww (the daemon)
# dismiss <id> drop one notification from the history
# clear empty the history
# refresh re-push the current history into eww (after `eww reload`)
# popup-close close the current toast now
# dnd-toggle flip Do-not-Disturb
# dnd-get print the DnD state (for the eww poll)
set -u
CACHE="$HOME/.cache/eww"
STORE="$CACHE/notifications.json"
DND="$CACHE/dnd"
PTIMER="$CACHE/.notif-popup-timer"
MAX=50 # keep at most this many in history
POPUP_SECS=5 # auto-dismiss the toast after N seconds
mkdir -p "$CACHE"
[ -f "$STORE" ] || echo '[]' > "$STORE"
[ -f "$DND" ] || echo 'false' > "$DND"
push() { eww update notifications="$(cat "$STORE")" 2>/dev/null; }
show_popup() { # $1 = a single notification JSON object
eww update popup="$1" 2>/dev/null
eww open notification-popup 2>/dev/null
[ -f "$PTIMER" ] && kill "$(cat "$PTIMER")" 2>/dev/null
( sleep "$POPUP_SECS"; eww close notification-popup 2>/dev/null; rm -f "$PTIMER" ) &
echo $! > "$PTIMER"
}
case "${1:-listen}" in
listen)
command -v tiramisu >/dev/null 2>&1 || { echo "tiramisu not installed" >&2; exit 1; }
push # restore whatever is already in the history on startup
# tiramisu -j emits one sanitized JSON object per notification.
# NOTE: field names (.source/.summary/.body/.id) are verified against a
# real `tiramisu -j` probe — adjust here if the probe shows other keys.
tiramisu -j | while IFS= read -r line; do
[ -z "$line" ] && continue
obj=$(jq -c --arg t "$(date '+%H:%M')" '
{ id: ((.id // "0")|tostring),
app: (.source // .app_name // .["app-name"] // "Notification"),
summary: (.summary // ""),
body: (.body // ""),
time: $t }' <<<"$line" 2>/dev/null) || continue
[ -z "$obj" ] && continue
tmp=$(jq -c --argjson n "$obj" --argjson max "$MAX" '[$n] + . | .[0:$max]' "$STORE") || continue
printf '%s' "$tmp" > "$STORE"
push
[ "$(cat "$DND" 2>/dev/null)" != "true" ] && show_popup "$obj"
done
;;
dismiss)
tmp=$(jq -c --arg id "${2:-}" 'map(select((.id|tostring) != $id))' "$STORE") && printf '%s' "$tmp" > "$STORE"
push
;;
clear)
echo '[]' > "$STORE"; push
;;
refresh)
push
;;
popup-close)
[ -f "$PTIMER" ] && kill "$(cat "$PTIMER")" 2>/dev/null
rm -f "$PTIMER"
eww close notification-popup 2>/dev/null
;;
dnd-toggle)
[ "$(cat "$DND" 2>/dev/null)" = "true" ] && echo false > "$DND" || echo true > "$DND"
;;
dnd-get)
cat "$DND" 2>/dev/null || echo false
;;
esac
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Toggle an eww panel with a click-outside backdrop. One panel open at a time.
# toggle <window> open it (closing others) or close it if already open
# close close the open panel + backdrop
#
# IMPORTANT (niri/eww): eww KILLS an event-handler process as soon as it lingers
# (a `sleep`). So the panel buttons / backdrop call this script, and we must NOT
# rely on a sleep surviving inside it: the **backdrop is always closed FIRST and
# instantly** (so a kill can never strand a full-screen click-eater → no lockout),
# and the only sleep (for the slide animation) runs in a DETACHED `setsid`
# process that eww can't kill.
set -u
PANELS="control-center calendar music sysmon updates"
# detached slide helper: $1 = panel-open target, rest = windows to close after anim
_defer_close() { # usage: _defer_close "win win2 ..."
# Wait the full fade (100ms) + a margin BEFORE unmapping, so the window never
# pops away while still partly visible (that read as a "hard" cut on close).
setsid -f sh -c "sleep 0.2; eww close $1" >/dev/null 2>&1
}
case "${1:-close}" in
toggle)
win="${2:-}"
[ -n "$win" ] || exit 0
if eww active-windows 2>/dev/null | grep -q "^${win}:"; then
# already open → dismiss (backdrop first, then animated close)
eww close panel-backdrop 2>/dev/null
eww update panel-open=false 2>/dev/null
_defer_close "$win"
else
eww close $PANELS panel-backdrop 2>/dev/null # only one at a time
eww update panel-open=false 2>/dev/null # start collapsed
eww open panel-backdrop 2>/dev/null # backdrop first (below)
eww open "$win" 2>/dev/null # panel on top (collapsed)
# reveal AFTER realize, in a detached process so an eww-killed handler
# can't leave the panel stuck collapsed under an open backdrop.
setsid -f sh -c 'sleep 0.12; eww update panel-open=true' >/dev/null 2>&1
fi
;;
close)
eww close panel-backdrop 2>/dev/null # kill any lockout instantly
eww update panel-open=false 2>/dev/null # slide out
_defer_close "$PANELS"
;;
esac
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Consume TANINUX's panel/dock settings (panel.json) into eww.
# apply push settings live (eww update + imperative autohide)
# watch apply now, then re-apply whenever panel.json changes
# get <jq-path> read a value (used by clock.sh / dock.sh)
#
# Contract file: ~/.local/share/taninux/panel.json (see docs/eww-panel-dock-integration.md)
set -u
CFG="${XDG_DATA_HOME:-$HOME/.local/share}/taninux/panel.json"
# seed defaults if missing, so eww always has something to read
if [ ! -f "$CFG" ]; then
mkdir -p "$(dirname "$CFG")"
cat > "$CFG" <<'EOF'
{
"version": 1,
"dock": { "autohide": true, "icon_size": 42, "hide_delay": 1.2 },
"bar": { "clock_format": "%H:%M %A, %d.%m.%Y",
"modules": { "music": true, "sys": true, "updates": true, "net": true, "bt": true, "vol": true } }
}
EOF
fi
. "$(dirname "$0")/wm.sh" # niri helpers
j() { jq -r "$1" "$CFG" 2>/dev/null; }
apply() {
# --- live vars (no reload) ---
local icon modules
icon=$(j '.dock.icon_size // 42'); case "$icon" in ''|*[!0-9]*) icon=42;; esac
[ "$icon" -lt 24 ] && icon=24; [ "$icon" -gt 64 ] && icon=64
modules=$(jq -c '.bar.modules // {}' "$CFG" 2>/dev/null); [ -z "$modules" ] && modules='{}'
eww update cfg-icon-size="$icon" "cfg-modules=$modules" 2>/dev/null
# --- autohide --- the dock window stays mapped; we flip dock-open (revealer)
# and the hover trigger. autohide off = parked open; on = hover to reveal.
eww open dock 2>/dev/null
if [ "$(j 'if .dock.autohide == false then "false" else "true" end')" = "false" ]; then
eww update dock-open=true 2>/dev/null; eww close dock-trigger 2>/dev/null # always visible
else
eww update dock-open=false 2>/dev/null; eww open dock-trigger 2>/dev/null # hover to reveal
fi
# clock_format + hide_delay are read on demand by clock.sh / dock.sh.
}
case "${1:-apply}" in
get) j "${2:-.}" ;;
apply) apply ;;
toggle-autohide)
cur=$(j 'if .dock.autohide == false then "false" else "true" end')
new=$([ "$cur" = "false" ] && echo true || echo false)
tmp=$(mktemp)
jq ".dock.autohide = $new" "$CFG" > "$tmp" 2>/dev/null && mv "$tmp" "$CFG"
apply
;;
watch)
apply
last=$(stat -c %Y "$CFG" 2>/dev/null || echo 0)
while :; do
sleep 2
now=$(stat -c %Y "$CFG" 2>/dev/null || echo 0)
[ "$now" != "$last" ] && { last="$now"; apply; }
done ;;
esac
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Power actions for the Control Center dropdown — compositor-agnostic.
set -u
. "$(dirname "$0")/wm.sh"
eww close control-center panel-backdrop 2>/dev/null
case "${1:-}" in
lock) wm_lock ;;
logout) wm_exit ;;
sleep) systemctl suspend ;;
reboot) systemctl reboot ;;
shutdown) systemctl poweroff ;;
esac
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Open the TANINUX GTK settings app. Single-instance (GTK app-id), so a second
# call just focuses the existing window. Runs with system python (needs `gi`,
# which the project venv lacks).
cd "$HOME/projects/taninux" 2>/dev/null || exit 1
# Args are passed through for deep-linking, e.g. `--page panel-dock` (the app may
# ignore unknown args and just open normally — best effort).
setsid -f env PYTHONPATH=src /usr/bin/python -m taninux.gui "$@" >/dev/null 2>&1
@@ -0,0 +1,275 @@
#!/usr/bin/env bash
# Spotlight launcher backend for eww.
# query <text> build results, push to eww (spot-results), cache for activate
# activate <idx> run the cached result's action, close the launcher
# open [clip] open + focus the launcher (optionally in clipboard mode)
# close
# reindex rebuild the .desktop app cache
#
# Sigils inside the query:
# =expr calc ?text web >cmd shell :name emoji
# /path ~path file ;text clipboard
# No sigil → fuzzy apps (plus an inline calc row if the text is an expression).
set -u
CACHE="$HOME/.cache/eww"; mkdir -p "$CACHE"
RES="$CACHE/spot-results.json"
APPTSV="$CACHE/spot-apps.tsv"
EMOJI="$(cd "$(dirname "$0")/.." && pwd)/data/emoji.tsv"
MAX=12
lc() { printf '%s' "$1" | tr 'A-Z' 'a-z'; }
# On Sway the launcher runs inside a one-key "launcher" mode (only Escape bound) so
# Escape handling is done inside eww (the spotlight window's onkey); nothing to
# reset at the compositor level on niri.
nav_reset() { :; }
resolve_icon() { # icon name -> file path (Papirus svg preferred)
local name="$1" base sz ext f
[ -z "$name" ] && name="application-x-executable"
case "$name" in /*) [ -f "$name" ] && { printf '%s' "$name"; return; };; esac
for base in /usr/share/icons/Papirus-Dark /usr/share/icons/Papirus; do
for sz in 64x64 48x48 32x32 24x24; do
for ext in svg png; do
[ -f "$base/$sz/apps/$name.$ext" ] && { printf '%s' "$base/$sz/apps/$name.$ext"; return; }
done
done
done
f=$(find /usr/share/icons/hicolor -type f \( -name "$name.svg" -o -name "$name.png" \) 2>/dev/null \
| sort -t/ -k6 -rn | head -1)
[ -n "$f" ] && { printf '%s' "$f"; return; }
for ext in svg png xpm; do
[ -f "/usr/share/pixmaps/$name.$ext" ] && { printf '%s' "/usr/share/pixmaps/$name.$ext"; return; }
done
printf '/usr/share/icons/Papirus-Dark/64x64/apps/application-x-executable.svg'
}
# ---- app index (cached TSV: id \t name \t iconpath \t haystack) -------------
reindex() {
local dirs=(/usr/share/applications "$HOME/.local/share/applications" \
/var/lib/flatpak/exports/share/applications \
"$HOME/.local/share/flatpak/exports/share/applications")
: > "$APPTSV.tmp"
local d f
for d in "${dirs[@]}"; do
[ -d "$d" ] || continue
for f in "$d"/*.desktop; do
[ -f "$f" ] || continue
awk -F= '
/^\[/ { grp = ($0=="[Desktop Entry]"); next }
grp && /^Name=/ && n=="" { n=substr($0,6) }
grp && /^Icon=/ && ic=="" { ic=substr($0,6) }
grp && /^NoDisplay=/ { nd=substr($0,11) }
grp && /^Hidden=/ { hd=substr($0,8) }
grp && /^Type=/ { ty=substr($0,6) }
grp && /^Comment=/ && cm=="" { cm=substr($0,9) }
grp && /^GenericName=/ && gn=="" { gn=substr($0,13) }
grp && /^Keywords=/ && kw=="" { kw=substr($0,10) }
END {
if (n=="" || ty!="Application" || nd=="true" || hd=="true") exit
printf "%s\t%s\t%s\t%s\n", n, ic, gn" "cm" "kw, ""
}' "$f" | while IFS=$'\t' read -r name icon extra _; do
[ -n "$name" ] || continue
local id; id=$(basename "$f" .desktop)
printf '%s\t%s\t%s\t%s\n' "$id" "$name" "$(resolve_icon "$icon")" "$(lc "$name $extra")" >> "$APPTSV.tmp"
done
done
done
sort -u -t$'\t' -k2 "$APPTSV.tmp" > "$APPTSV"; rm -f "$APPTSV.tmp"
}
ensure_index() {
[ -s "$APPTSV" ] || reindex
# rebuild if any app dir is newer than the cache
local newer
newer=$(find /usr/share/applications "$HOME/.local/share/applications" \
-name '*.desktop' -newer "$APPTSV" -print -quit 2>/dev/null)
[ -n "$newer" ] && reindex
}
# ---- result assembly --------------------------------------------------------
items=()
add() { # type iconOrPath title sub act
local img="" gly="$2"
case "$2" in /*) img="$2"; gly="";; esac
items+=("$(jq -nc --arg t "$1" --arg g "$gly" --arg im "$img" --arg ti "$3" --arg s "$4" --arg a "$5" \
'{type:$t,icon:$g,img:$im,title:$ti,sub:$s,act:$a}')")
}
emit() {
if [ ${#items[@]} -eq 0 ]; then printf '[]' > "$RES"
else printf '%s\n' "${items[@]}" | jq -s 'to_entries|map(.value + {idx:.key})' > "$RES"; fi
eww update spot-results="$(cat "$RES")" 2>/dev/null
}
# ---- modes ------------------------------------------------------------------
m_calc() { # inline=1 marks the auto row added before apps
local expr="$1" out
out=$(qalc -t -- "$expr" 2>/dev/null)
[ -z "$out" ] && return
case "$out" in *error*|*Error*) return;; esac
add calc "󰪚" "$out" "= $expr · Enter kopiert" "$out"
}
m_web() {
local q="$1" url
[ -n "$q" ] || return
url="https://duckduckgo.com/?q=$(jq -rn --arg q "$q" '$q|@uri')"
add web "󰖟" "$q" "Im Web suchen · DuckDuckGo" "$url"
}
m_shell() {
local c="$1"; [ -n "$c" ] || return
add shell "" "$c" "Shell-Befehl ausführen" "$c"
}
m_emoji() {
local q; q=$(lc "$1")
[ -f "$EMOJI" ] || return
local line
if [ -z "$q" ]; then line=$(head -n $MAX "$EMOJI")
else line=$(grep -i -- "$q" "$EMOJI" 2>/dev/null | head -n $MAX); fi
while IFS=$'\t' read -r ch name; do
[ -n "$ch" ] && add emoji "$ch" "$name" "Enter kopiert" "$ch"
done <<< "$line"
}
m_files() {
local q="$1" base="$HOME"
command -v fd >/dev/null 2>&1 || { add note "" "fd nicht installiert" "sudo pacman -S fd" ""; return; }
case "$q" in /*) base=/; ;; "~"*) q="${q#\~}"; q="${q#/}";; esac
[ -n "$q" ] || return
while IFS= read -r p; do
[ -n "$p" ] || continue
add file "󰈔" "$(basename "$p")" "${p/#$HOME/~}" "$p"
done < <(fd -H -E .git --max-results $MAX -- "$q" "$base" 2>/dev/null)
}
m_clip() {
local q="$1"
command -v cliphist >/dev/null 2>&1 || { add note "" "cliphist nicht installiert" "sudo pacman -S cliphist" ""; return; }
local line
if [ -z "$q" ]; then line=$(cliphist list 2>/dev/null | head -n $MAX)
else line=$(cliphist list 2>/dev/null | grep -i -- "$q" | head -n $MAX); fi
local id text
while IFS= read -r row; do
[ -n "$row" ] || continue
id=${row%%$'\t'*}; text=${row#*$'\t'}
add clip "󰅍" "$text" "Clipboard · Enter kopiert" "$id"
done <<< "$line"
}
m_apps() {
[ -z "$1" ] && return # empty query → no app rows (clean start)
local q; q=$(lc "$1")
ensure_index
local rows
rows=$(awk -F'\t' -v q="$q" '
function score(s, i,p) { p=index(s,q); return p }
{
hay=$4; name=tolower($2)
ok=1; n=split(q,toks," ")
for(i=1;i<=n;i++) if(index(hay,toks[i])==0){ok=0;break}
if(!ok) next
sc = (index(name,q)==1)?0 : (index(name,q)>0?1 : 2)
printf "%d\t%s\t%s\t%s\n", sc, $1, $2, $3
}' "$APPTSV" | sort -n -k1 | head -n $MAX)
local id name icon _sc
while IFS=$'\t' read -r _sc id name icon; do
[ -n "$id" ] && add app "$icon" "$name" "Anwendung" "$id"
done <<< "$rows"
}
# ---- dispatch ---------------------------------------------------------------
_build() { # sigil-aware build (apps mode / default); fills items[]
local raw="$*"
items=()
case "$raw" in
"="*) m_calc "${raw#=}" ;;
"?"*) m_web "${raw#\?}" ;;
">"*) m_shell "${raw#>}" ;;
":"*) m_emoji "${raw#:}" ;;
";"*) m_clip "${raw#;}" ;;
"/"*|"~"*) m_files "$raw" ;;
"") m_apps "" ;;
*)
if printf '%s' "$raw" | grep -qE '^[0-9.(][0-9.+*/^%() -]*$' \
&& printf '%s' "$raw" | grep -qE '[-+*/^%]'; then m_calc "$raw"; fi
m_apps "$raw"
;;
esac
}
query() { _build "$*"; emit; }
qmode() { # explicit category (from a pill); text is the raw search, no sigil
local mode="$1"; shift
items=()
case "$mode" in
clip) m_clip "$*" ;;
emoji) m_emoji "$*" ;;
calc) m_calc "$*" ;;
web) m_web "$*" ;;
files) m_files "$*" ;;
apps|*) _build "$*" ;;
esac
emit
}
activate() {
local idx="$1"
[ -s "$RES" ] || exit 0
local type act
type=$(jq -r --argjson i "$idx" '.[$i].type // empty' "$RES" 2>/dev/null)
act=$(jq -r --argjson i "$idx" '.[$i].act // empty' "$RES" 2>/dev/null)
[ -n "$type" ] || { spot_close; exit 0; }
case "$type" in
app) setsid -f gtk-launch "$act" >/dev/null 2>&1 ;;
web) setsid -f xdg-open "$act" >/dev/null 2>&1 ;;
shell) setsid -f sh -c "$act" >/dev/null 2>&1 ;;
file) setsid -f xdg-open "$act" >/dev/null 2>&1 ;;
calc|emoji) printf '%s' "$act" | wl-copy ;;
clip) cliphist decode "$act" 2>/dev/null | wl-copy ;;
esac
spot_close
}
# slide out, then unmap (niri doesn't animate layer map/unmap itself)
# Close spotlight with an animated slide-out, robustly: the full-screen
# spot-backdrop is closed FIRST and instantly (so a killed handler can never leave
# a click-eater → no lockout), then the body slides out (spot-open=false) and the
# window is unmapped from a DETACHED process eww can't kill.
spot_close() {
eww close spot-backdrop 2>/dev/null # catcher gone immediately (no lockout)
eww update spot-open=false 2>/dev/null # slide the body out
setsid -f sh -c 'sleep 0.2; eww close spotlight' >/dev/null 2>&1
nav_reset
}
open() {
local mode="${1:-apps}"
eww update spot-q="" spot-mode="$mode" 2>/dev/null
if [ "$mode" = clip ]; then qmode clip ""; else items=(); emit; fi
eww update spot-open=false 2>/dev/null # start collapsed
eww open spot-backdrop 2>/dev/null # click-outside catcher (stacks below)
eww open spotlight 2>/dev/null
# reveal in a DETACHED process so an eww-killed handler can't leave spotlight
# stuck collapsed under an open backdrop (lockout).
setsid -f sh -c 'sleep 0.12; eww update spot-open=true' >/dev/null 2>&1
}
# called on every keystroke: render results for the active category.
# A leading sigil always wins, regardless of which pill is active.
type_query() {
local text="$*"
eww update spot-q="$text" 2>/dev/null
case "$text" in
"="*|"?"*|">"*|":"*|";"*|"/"*|"~"*) query "$text"; return ;;
esac
local mode; mode=$(eww get spot-mode 2>/dev/null); [ -z "$mode" ] && mode=apps
qmode "$mode" "$text"
}
case "${1:-}" in
query) shift; query "$*" ;;
qmode) shift; mode="$1"; shift; qmode "$mode" "$*" ;;
type) shift; type_query "$*" ;;
activate) activate "${2:-0}" ;;
open) open "${2:-}" ;;
close) spot_close ;;
reindex) reindex ;;
*) echo "usage: spotlight.sh {query|qmode|type|cycle|activate|open|close|reindex}" >&2; exit 1 ;;
esac
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# CPU%, RAM%, Disk% (root), Temp(°C) as JSON for eww.
# --- CPU (delta over a short sample) ---
read -r _ a b c d _ < /proc/stat; t1=$((a+b+c+d)); idle1=$d
sleep 0.3
read -r _ a b c d _ < /proc/stat; t2=$((a+b+c+d)); idle2=$d
dt=$((t2 - t1)); di=$((idle2 - idle1))
cpu=0; [ "$dt" -gt 0 ] && cpu=$(( (100 * (dt - di)) / dt ))
# --- RAM ---
mt=$(awk '/MemTotal/{print $2}' /proc/meminfo)
ma=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
mem=0; [ "${mt:-0}" -gt 0 ] && mem=$(( (100 * (mt - ma)) / mt ))
# --- Disk (root) ---
disk=$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')
# --- Temp (sensors → fallback thermal zone) ---
temp=$(sensors -j 2>/dev/null | jq -r '
[.. | objects | to_entries[] | select(.key|test("temp.*_input")) | .value]
| map(select(. > 0)) | if length > 0 then (add/length) else 0 end' 2>/dev/null)
if [ -z "$temp" ] || [ "$temp" = "null" ] || [ "$temp" = "0" ]; then
temp=$(awk '{printf "%d", $1/1000}' /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
fi
temp=$(printf '%.0f' "${temp:-0}")
jq -nc --argjson cpu "${cpu:-0}" --argjson mem "${mem:-0}" \
--argjson disk "${disk:-0}" --argjson temp "${temp:-0}" \
'{cpu:$cpu,mem:$mem,disk:$disk,temp:$temp}'
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Pending updates: official repos (checkupdates) + AUR (paru -Qua).
# count number of pending updates (default)
# list JSON [{name, old, new, src}] src = repo|aur
set -u
repo(){ checkupdates 2>/dev/null; } # "name old -> new"
aur(){ paru -Qua 2>/dev/null; } # "name old -> new"
case "${1:-count}" in
count) echo $(( $(repo|wc -l) + $(aur|wc -l) )) ;;
list)
{ repo | awk '{print $1"\t"$2"\t"$4"\trepo"}'
aur | awk '{print $1"\t"$2"\t"$4"\taur"}'; } \
| jq -R -s -c 'split("\n")|map(select(length>0))|map(split("\t"))
|map({name:.[0],old:.[1],new:.[2],src:.[3]})' ;;
esac
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Volume via wireplumber (wpctl).
sink="@DEFAULT_AUDIO_SINK@"
case "${1:-get}" in
get)
raw=$(wpctl get-volume "$sink" 2>/dev/null) # e.g. "Volume: 0.95 [MUTED]"
vol=$(printf '%s' "$raw" | awk '{print $2}')
perc=$(awk -v v="${vol:-0}" 'BEGIN{printf "%d", v*100 + 0.5}')
muted=false
printf '%s' "$raw" | grep -q MUTED && muted=true
if [ "$muted" = true ] || [ "$perc" -eq 0 ]; then icon="󰖁"
elif [ "$perc" -lt 50 ]; then icon="󰕿"
else icon="󰕾"; fi
jq -nc --argjson p "$perc" --argjson m "$muted" --arg i "$icon" \
'{percent:$p,muted:$m,icon:$i}'
;;
set) wpctl set-volume -l 1 "$sink" "${2:-0}%";;
mute) wpctl set-mute "$sink" toggle;;
esac
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# niri helpers for the eww scripts. niri-only — the Hyprland/Sway abstraction
# (and its compositor detector) was removed; the old dual-compat version is in
# ~/eww-backup-dual-*. Source this: . "$(dirname "$0")/wm.sh"
#
# Provides:
# $WM always "niri" (kept so callers' $WM checks resolve)
# wm_clients normalised JSON [{id,pid,class,title,ws}]
# wm_focus <id> focus a window by id
# wm_goto_ws <idx> focus a workspace by index
# wm_cycle_ws <up|down> focus prev/next workspace
# wm_cursor_y empty (niri has no cursor-position IPC)
# wm_outputs number of active outputs
# wm_lock / wm_exit lock screen / quit the compositor
# wm_blur_layer <ns> no-op (niri layer effects live in config.kdl)
WM=niri
export WM
wm_clients() {
niri msg --json windows 2>/dev/null | jq -c '
[ .[] | { id: (.id|tostring),
pid: (.pid // 0),
class: (.app_id // ""),
title: (.title // ""),
ws: ((.workspace_id // "") | tostring) } ]'
}
wm_focus() { [ -n "${1:-}" ] && niri msg action focus-window --id "$1" >/dev/null 2>&1; }
wm_goto_ws() { niri msg action focus-workspace "$1" >/dev/null 2>&1; }
wm_cycle_ws() {
if [ "$1" = up ]; then niri msg action focus-workspace-up >/dev/null 2>&1
else niri msg action focus-workspace-down >/dev/null 2>&1; fi
}
wm_cursor_y() { printf ''; } # niri exposes no pointer position over IPC
wm_outputs() { niri msg --json outputs 2>/dev/null | jq '[.[]|select(.logical!=null)]|length'; }
wm_lock() { swaylock -f; }
wm_exit() { niri msg action quit --skip-confirmation >/dev/null 2>&1; }
# Blur/rounding/shadow for eww's layer surfaces is declarative in config.kdl
# (a layer-rule matched on the gtk-layer-shell namespace) — nothing to do live.
wm_blur_layer() { :; }
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Emit niri workspaces as JSON for eww:
# [{"id":<idx>,"active":<bool>,"windows":<n>}] sorted by idx ascending
# We display the per-monitor index (.idx) as the workspace number — that's also
# what `focus-workspace <idx>` takes (see scripts/ws.sh). niri workspaces are
# dynamic & per-output, so eww.yuck renders the live list, not a fixed 1..10 set.
. "$(dirname "$0")/wm.sh"
emit() {
local wins
wins=$(niri msg --json windows 2>/dev/null)
niri msg --json workspaces 2>/dev/null | jq -c --argjson wins "${wins:-[]}" '
[ .[] | .id as $wid
| { id: .idx,
active: .is_focused,
windows: ([ $wins[] | select(.workspace_id == $wid) ] | length) } ]
| sort_by(.id)'
}
last=""
print_if_new() {
local cur; cur=$(emit)
if [ -n "$cur" ] && [ "$cur" != "$last" ]; then
printf '%s\n' "$cur"; last="$cur"
fi
}
print_if_new
# Newline-delimited JSON event stream; the first lines are a full snapshot, then
# deltas. We just re-emit on every event — cheap and always correct.
niri msg --json event-stream 2>/dev/null | while read -r _; do
print_if_new
done
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -u
. "$(dirname "$0")/wm.sh"
case "${1:-}" in
switch) wm_goto_ws "${2:-}" ;;
cycle) wm_cycle_ws "${2:-}" ;;
esac
@@ -0,0 +1,195 @@
// niri config — ported from the Hyprland/eww setup.
// Docs: https://niri-wm.github.io/niri/ · validate with `niri validate`.
input {
keyboard {
xkb {
layout "ch"
}
numlock
}
touchpad {
tap
}
mouse {}
}
// Outputs — scale 1.25 like the Hyprland setup. Mode omitted on purpose so niri
// picks the highest refresh rate automatically (144 Hz on DP-3). Positions are
// left to niri's auto-placement.
output "DP-3" {
scale 1.25
}
output "HDMI-A-1" {
scale 1.25
}
layout {
gaps 10
center-focused-column "never"
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
default-column-width { proportion 0.5; }
// Fuji violet accent on the focus ring.
focus-ring {
width 3
active-color "#a39ec4"
inactive-color "#505050"
}
border {
off
}
// Subtle window shadow for depth.
shadow {
on
softness 20
spread 3
offset x=0 y=4
color "#00000070"
}
}
// ---- autostart ------------------------------------------------------------
// Bring up the systemd user session so portals (dark mode via
// org.freedesktop.portal.Settings) and TANINUX user services start.
spawn-at-startup "dbus-update-activation-environment" "--systemd" "--all"
spawn-at-startup "systemctl" "--user" "start" "tanin-session.target"
spawn-at-startup "/bin/sh" "-c" "$HOME/.config/eww/launch.sh"
spawn-at-startup "waypaper" "--restore"
spawn-at-startup "wlsunset" "-l" "47" "-L" "8.3" "-d" "600" "-t" "4000" "-T" "6500"
spawn-at-startup "nm-applet"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
spawn-at-startup "nextcloud" "--background"
hotkey-overlay {
skip-at-startup
}
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
animations {
// On by default. Uncomment to disable everything:
// off
}
// Ask apps to drop their client-side decorations (title/header bars); niri then
// draws only its own border/focus-ring. Apps that honour the protocol lose the
// top bar. (Some GTK apps ignore it and keep their headerbar — that's the app.)
prefer-no-csd
// Rounded corners for all windows.
window-rule {
geometry-corner-radius 14
clip-to-geometry true
}
// ---- eww frosted-glass / effects (enable after first boot) ----------------
// niri's layer-shell effects are version-sensitive; once niri runs we verify the
// real namespace with `niri msg --json layers` and enable blur/rounding/shadow.
// /-layer-rule {
// match namespace="^gtk-layer-shell$"
// geometry-corner-radius 14
// shadow { on; softness 8; spread 0; offset x=0 y=2; color "#00000088"; }
// }
binds {
Mod+Shift+Slash { show-hotkey-overlay; }
// --- launchers / apps (Hyprland muscle memory) ---
Mod+Backspace hotkey-overlay-title="Terminal: kitty" { spawn "kitty"; }
Mod+T hotkey-overlay-title="Terminal: kitty" { spawn "kitty"; }
Mod+E hotkey-overlay-title="Files: nautilus" { spawn "nautilus"; }
Mod+Space hotkey-overlay-title="Apps: Spotlight" { spawn-sh "$HOME/.config/eww/scripts/spotlight.sh open"; }
Mod+C hotkey-overlay-title="Calc: rofi" { spawn-sh "rofi -show calc"; }
// --- eww panels ---
Mod+A hotkey-overlay-title="Control Center" { spawn-sh "$HOME/.config/eww/scripts/panel.sh toggle control-center"; }
Mod+N hotkey-overlay-title="Calendar" { spawn-sh "$HOME/.config/eww/scripts/panel.sh toggle calendar"; }
Mod+Shift+R hotkey-overlay-title="Restart bar" { spawn-sh "$HOME/.config/eww/launch.sh"; }
// Panic-close: dismiss any open/stuck eww overlay (niri has no global Escape
// bind; a stuck :focusable spotlight would otherwise hold the keyboard).
Mod+Escape hotkey-overlay-title="Close overlays" { spawn-sh "$HOME/.config/eww/scripts/dismiss.sh"; }
// --- window management ---
Mod+W { close-window; }
Mod+F { toggle-window-floating; }
Mod+Shift+F { fullscreen-window; }
Mod+M { maximize-column; }
Mod+V { switch-focus-between-floating-and-tiling; }
Mod+R { switch-preset-column-width; }
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
// --- focus (arrows + hjkl) ---
Mod+Left { focus-column-left; }
Mod+Right { focus-column-right; }
Mod+Up { focus-window-up; }
Mod+Down { focus-window-down; }
Mod+H { focus-column-left; }
Mod+L { focus-column-right; }
Mod+K { focus-window-up; }
Mod+J { focus-window-down; }
// --- move columns/windows ---
Mod+Ctrl+Left { move-column-left; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Down { move-window-down; }
// --- workspaces (focus + move, Hyprland-style Shift to move) ---
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
// --- multi-monitor ---
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Right { focus-monitor-right; }
// --- screenshots (Mod+Y = region, like hyprshot) ---
Mod+Y { screenshot; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
// --- media / volume / brightness (work when locked) ---
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; }
XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; }
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; }
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; }
XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "set" "5%+"; }
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "set" "5%-"; }
// --- session ---
Super+Alt+L { spawn "swaylock" "-f"; }
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
Mod+Shift+P { power-off-monitors; }
}