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
+1 -2
View File
@@ -61,8 +61,7 @@ AUR → OPT-IN only (paru = optdepend)
**Rebuilt AUR deps** — the desktop's hard requirements that aren't in the **Rebuilt AUR deps** — the desktop's hard requirements that aren't in the
official repos. We rebuild them into `[tanin]` so `tanin-desktop` can hard-depend official repos. We rebuild them into `[tanin]` so `tanin-desktop` can hard-depend
on them **without the AUR** (binary repos must never need it): `eww`, `tiramisu`, on them **without the AUR** (binary repos must never need it): `eww`, `tiramisu`,
`waypaper`, `cliphist`. ⚠️ Verify each before publishing — any that `waypaper`. (`cliphist` + `cage` are in `extra` — pulled from official repos.)
turn out to live in `extra` should be dropped from this list.
The full Arch package set (compositor, portals, audio, connectivity, fonts, …) The full Arch package set (compositor, portals, audio, connectivity, fonts, …)
is enumerated under `[packages.arch]` in `camel.toml` and is the seed for the is enumerated under `[packages.arch]` in `camel.toml` and is the seed for the
+3 -5
View File
@@ -140,10 +140,7 @@ name = "waypaper"
reason = "wallpaper setter (niri autostart: waypaper --restore)" reason = "wallpaper setter (niri autostart: waypaper --restore)"
verify = "AUR" verify = "AUR"
[[packages.rebuilt_aur]] # cliphist + cage are in `extra` (verified) — NOT rebuilt, pulled from official repos.
name = "cliphist"
reason = "clipboard history for Spotlight"
verify = "extra? else AUR"
# ============================================================================= # =============================================================================
# ARCH PACKAGE SET — hard depends of tanin-desktop, pulled from official repos. # ARCH PACKAGE SET — hard depends of tanin-desktop, pulled from official repos.
@@ -200,5 +197,6 @@ project = "taninux.kgva.ch" # homepage + [tanin] pacman Ser
name = "Final project/OS name: keep TANINUX, or split app=tanin / OS=<marketing name>?" name = "Final project/OS name: keep TANINUX, or split app=tanin / OS=<marketing name>?"
repo_hosting = "Gitea/Forgejo Arch registry vs static dir behind taninux.kgva.ch" repo_hosting = "Gitea/Forgejo Arch registry vs static dir behind taninux.kgva.ch"
signing = "Move [tanin] SigLevel to Required + distribute the packager key" signing = "Move [tanin] SigLevel to Required + distribute the packager key"
aur_repo_check = "Confirm which rebuilt_aur entries are actually in extra (drop those)" aur_repo_check = "DONE — cliphist+cage in extra; only eww/tiramisu/waypaper need AUR rebuild into [tanin]"
repo_build = "packaging/build-tanin-repo.sh builds the 5 own pkgs + DB at ~/projects/tanin-repo. Still need: taninux (sudo pacman -S python-build python-installer python-hatchling), the 3 AUR rebuilds (paru/makepkg + sudo), optional GPG signing key, then host."
blur_shadow = "niri layer-rule blur/shadow for eww layers (template commented in niri config)" blur_shadow = "niri layer-rule blur/shadow for eww layers (template commented in niri config)"
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Build the [tanin] binary repo from our own packages.
# ./build-tanin-repo.sh # build what's possible, refresh the DB
# TANIN_REPO_DIR=/path ./... # output elsewhere (default ~/projects/tanin-repo)
#
# Coverage:
# - own simple packages (file installs / metapackage): built here, no sudo.
# - taninux: needs python-build/installer/hatchling -> built only if present.
# - AUR rebuilds (eww, tiramisu, waypaper): NOT in official repos; build them
# separately (paru/makepkg) and drop the .pkg.tar.zst into the repo dir.
# (cliphist + cage are in extra -> no rebuild needed.)
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="${TANIN_REPO_DIR:-$HOME/projects/tanin-repo}"
DB="$OUT/tanin.db.tar.zst"
mkdir -p "$OUT"
OWN_SIMPLE=(tanin-greet tanin-libadwaita tanin-setup tanin-eww tanin-desktop)
OWN_BUILD=(taninux)
AUR_REBUILD=(eww tiramisu waypaper)
build() {
local p="$1"
echo "==> building $p"
( cd "$HERE/$p" && makepkg -d -f --noconfirm >/dev/null 2>&1 ) \
&& cp "$HERE/$p/"*.pkg.tar.zst "$OUT/" 2>/dev/null \
&& echo " ok" || { echo " FAILED ($p)"; return 1; }
}
for p in "${OWN_SIMPLE[@]}"; do build "$p"; done
for p in "${OWN_BUILD[@]}"; do
if pacman -Q python-build python-installer python-hatchling >/dev/null 2>&1; then
build "$p"
else
echo "** skip $p — needs: sudo pacman -S python-build python-installer python-hatchling"
fi
done
echo "** AUR rebuilds TODO (not in official repos): ${AUR_REBUILD[*]}"
echo " build each with paru/makepkg and copy the .pkg.tar.zst into $OUT, then re-run."
# refresh the DB (unsigned for now -> pacman.conf: SigLevel = Optional TrustAll)
shopt -s nullglob
pkgs=("$OUT"/*.pkg.tar.zst)
if (( ${#pkgs[@]} )); then
( cd "$OUT" && repo-add -q "$DB" ./*.pkg.tar.zst >/dev/null 2>&1 )
echo "== [tanin] repo refreshed at $OUT =="
ls -1 "$OUT" | grep -E '\.pkg\.tar\.zst$|tanin\.db'
else
echo "no packages built yet."
fi
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,55 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = tanin-desktop
pkgbase = tanin-desktop
xdata = pkgtype=pkg
pkgver = 0.2.0-1
pkgdesc = TANINUX desktop — metapackage pulling the full niri/eww environment
url = https://taninux.kgva.ch
builddate = 1781813721
packager = Unknown Packager
size = 0
arch = any
license = GPL-3.0-or-later
depend = taninux
depend = tanin-greet
depend = tanin-libadwaita
depend = tanin-eww
depend = tanin-setup
depend = niri
depend = greetd
depend = cage
depend = xdg-desktop-portal
depend = xdg-desktop-portal-gtk
depend = xdg-desktop-portal-gnome
depend = eww
depend = tiramisu
depend = waypaper
depend = cliphist
depend = foot
depend = jq
depend = wl-clipboard
depend = xdg-utils
depend = libqalculate
depend = grim
depend = slurp
depend = networkmanager
depend = bluez
depend = bluez-utils
depend = pipewire
depend = pipewire-pulse
depend = wireplumber
depend = brightnessctl
depend = playerctl
depend = flatpak
depend = pacman-contrib
depend = adw-gtk3
depend = adwaita-icon-theme
depend = papirus-icon-theme
depend = inter-font
depend = ttf-iosevka-nerd
optdepend = paru: AUR support in the Software Hub (opt-in)
optdepend = wlsunset: night light
optdepend = cups: printer management
optdepend = timeshift: system snapshots/backups
optdepend = waybar: fallback bar if eww is missing (launch.sh falls back to it)
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; }
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,23 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = tanin-greet
pkgbase = tanin-greet
xdata = pkgtype=pkg
pkgver = 0.1.0-1
pkgdesc = TANINUX login — custom GTK4 greeter for greetd (mirrors the topbar)
url = https://taninux.kgva.ch
builddate = 1781813715
packager = Unknown Packager
size = 18155
arch = any
license = GPL-3.0-or-later
backup = etc/greetd/config.toml
backup = etc/greetd/tanin-greet.css
depend = greetd
depend = cage
depend = gtk4
depend = python
depend = python-gobject
depend = adwaita-icon-theme
depend = inter-font
optdepend = tanin-backgrounds: default login wallpaper at /usr/share/backgrounds/tanin/login.jpg
@@ -0,0 +1,8 @@
# greetd config for tanin-greet (TANINUX custom GTK4 greeter).
# The greeter is a GTK app, so it's hosted by `cage` (one-window kiosk compositor).
[terminal]
vt = 1
[default_session]
command = "cage -s -- tanin-greet"
user = "greeter"
@@ -0,0 +1,176 @@
/* =========================================================================
* tanin-greet — stylesheet. Mirrors the eww topbar (eww.scss): Fuji palette,
* monochrome, pill shapes, Inter Medium. Accent is the brand Fuji violet.
* ========================================================================= */
@define-color bg #0f0f0f;
@define-color bg_alt #191919;
@define-color bg_hi #242424;
@define-color fg #f0f0f0;
@define-color yuki #f0f0f0; /* snow white — pill fill */
@define-color muted #8a8a8a;
@define-color accent #a39ec4;
/* No font-family override — inherit the system UI font. */
.greeter {
font-weight: 500;
color: @fg;
}
* { text-shadow: none; -gtk-icon-shadow: none; outline: none; }
/* dark scrim over the wallpaper — keeps text readable, keeps it moody */
.scrim { background-color: alpha(black, 0.55); }
/* ---- top bar: the eww bar look (opaque #0f0f0f strip) ------------------- *
* left = session dropdown · center = clock (time/day/date) · right = power. */
.topbar {
background-color: @bg;
min-height: 34px;
padding: 0 12px;
}
.topbar .clock {
color: @fg;
font-size: 13px;
}
/* session dropdown, top-left corner — quiet on the black bar, no box */
.session-dd,
.session-dd > button {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
min-height: 0;
padding: 2px 6px;
color: @muted;
font-size: 12px;
}
.session-dd > button:hover { color: @fg; }
/* power button — NO background at all, just the glyph */
.topbar .power,
.topbar .power > button {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
min-height: 0;
min-width: 0;
padding: 2px 6px;
color: @muted;
}
.topbar .power:hover,
.topbar .power > button:hover { color: @accent; background: transparent; }
/* TANINUX wordmark — bottom center, understated */
.brand {
color: @fg;
font-weight: 700;
font-size: 12px;
letter-spacing: 3px;
opacity: 0.8;
}
/* ---- centered login card (flat, no heavy frame — minimal) --------------- *
* NB: class is .logincard, never .card — libadwaita's built-in .card recipe
* paints an elevated background + shadow (the "solid box, too big" bug). */
.logincard { padding: 8px; background: transparent; box-shadow: none; }
.avatar {
color: @fg;
background-color: alpha(@accent, 0.14);
border-radius: 999px;
padding: 16px;
margin-bottom: 6px;
}
/* username row — plain name + chevron on the right, same width as the pill so
* the chevron sits where the password pill ends. No background (just the name). */
.namerow { min-width: 300px; }
.username {
color: @yuki;
font-size: 17px;
font-weight: 600;
}
.userswitch {
background: transparent;
border: none;
box-shadow: none;
min-height: 0;
min-width: 0;
padding: 2px 4px;
color: @muted;
}
.userswitch:hover { color: @fg; }
/* password pill — snow white (yuki), dark text, accent focus ring.
* border-radius 999px like the bar's workspaces/clock. */
.pill {
min-width: 300px;
min-height: 0;
background-color: alpha(@bg_alt, 0.45); /* greyish, slightly transparent */
color: @fg;
border: 1.5px solid alpha(@fg, 0.22); /* grey border (unfocused) */
border-radius: 999px;
padding: 11px 18px;
box-shadow: none;
}
entry.pill { caret-color: @yuki; }
entry.pill text { color: @fg; }
entry.pill text > placeholder { color: alpha(@fg, 0.45); }
entry.pill image { color: alpha(@fg, 0.6); } /* peek-eye icon */
/* clicked / focused → white border */
.pill:focus-within {
border-color: @yuki;
background-color: alpha(@bg_alt, 0.55);
box-shadow: none;
}
/* popup lists (session dropdown, user switcher).
* IMPORTANT: only style `popover > contents` — the outer `popover` node also
* covers the reserved shadow/margin area, so a solid bg there paints a huge
* solid box. Keep the outer node fully transparent. */
popover {
background: transparent;
background-image: none;
border: none;
box-shadow: none;
padding: 0;
}
popover > contents {
background-color: @bg_alt;
color: @fg;
border: 1px solid alpha(@fg, 0.06);
border-radius: 12px;
box-shadow: 0 8px 24px alpha(black, 0.45);
padding: 6px;
min-width: 0;
}
/* kill the inner list's OWN themed background (the lighter nested box) so the
* whole popover reads as one uniform surface. */
popover listview,
popover scrolledwindow,
popover scrolledwindow > viewport,
popover .view {
background: transparent;
background-color: transparent;
color: @fg;
}
popover listview > row,
popover .menuitem { border-radius: 8px; padding: 5px 10px; }
popover listview > row:selected,
popover listview > row:hover,
popover row:selected,
popover .menuitem:hover { background-color: alpha(@accent, 0.14); color: @accent; }
.error { color: #e0a3a3; font-size: 12px; margin-top: 4px; }
/* power popover buttons */
.menuitem {
background: transparent;
border: none;
box-shadow: none;
border-radius: 8px;
padding: 6px 14px;
color: @fg;
}
.menuitem:hover { background-color: alpha(@accent, 0.14); color: @accent; }
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""tanin-greet — TANINUX login greeter (GTK4 / PyGObject).
Custom layout: a black top bar like the eww topbar (brand · clock · power), a
wallpaper background, user + password centered as pills, and a small session
selector tucked in the bottom-right corner. Talks to greetd over GREETD_SOCK.
Run by greetd via cage: cage -s -- tanin-greet
Preview without greetd: just run it — the UI renders; only login needs the
socket (set GREETD_SOCK via a mock for a full dry-run).
Env overrides (handy for previewing from the repo):
TANIN_GREET_CSS path to the stylesheet (default /etc/greetd/tanin-greet.css)
TANIN_GREET_BG wallpaper image path (default /usr/share/backgrounds/tanin/login.jpg)
TANIN_GREET_SESSIONS wayland-sessions dir (default /usr/share/wayland-sessions)
"""
import gi, os, sys, json, socket, struct, shlex, glob, pwd
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, GLib, Gdk, Gio # noqa: E402
CSS_PATH = os.environ.get("TANIN_GREET_CSS", "/etc/greetd/tanin-greet.css")
WALLPAPER = os.environ.get("TANIN_GREET_BG", "/usr/share/backgrounds/tanin/login.jpg")
SESSION_DIR = os.environ.get("TANIN_GREET_SESSIONS", "/usr/share/wayland-sessions")
BRAND = os.environ.get("TANIN_GREET_BRAND", "TANINUX")
# --------------------------------------------------------------------------- #
# greetd IPC client — native-endian u32 length prefix + JSON payload.
# --------------------------------------------------------------------------- #
class Greetd:
def __init__(self):
path = os.environ.get("GREETD_SOCK")
self.sock = None
if path:
try:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
except OSError:
# No reachable greetd (e.g. preview without a daemon). Render the
# UI anyway; login() will report the missing socket on submit.
self.sock = None
def _recvn(self, n):
buf = b""
while len(buf) < n:
chunk = self.sock.recv(n - len(buf))
if not chunk:
raise RuntimeError("greetd closed the connection")
buf += chunk
return buf
def _send(self, obj):
data = json.dumps(obj).encode()
self.sock.sendall(struct.pack("=I", len(data)) + data)
def _recv(self):
n = struct.unpack("=I", self._recvn(4))[0]
return json.loads(self._recvn(n))
def login(self, username, password, cmd_argv):
"""Authenticate and start the session. Raises on failure."""
if self.sock is None:
raise RuntimeError("GREETD_SOCK not set (preview mode — cannot log in)")
self._send({"type": "create_session", "username": username})
while True:
r = self._recv()
t = r.get("type")
if t == "auth_message":
amt = r.get("auth_message_type")
if amt in ("secret", "visible"):
self._send({"type": "post_auth_message_response", "response": password})
else: # info / error — acknowledge with no response
self._send({"type": "post_auth_message_response"})
elif t == "success":
break
elif t == "error":
self._send({"type": "cancel_session"})
raise RuntimeError(r.get("description", "authentication failed"))
else:
raise RuntimeError(f"unexpected greetd reply: {r}")
# authenticated → launch the chosen session
self._send({"type": "start_session", "cmd": cmd_argv, "env": []})
r = self._recv()
if r.get("type") != "success":
self._send({"type": "cancel_session"})
raise RuntimeError(r.get("description", "failed to start session"))
# --------------------------------------------------------------------------- #
# System enumeration
# --------------------------------------------------------------------------- #
def list_users():
users = []
for p in pwd.getpwall():
if 1000 <= p.pw_uid < 65534 and p.pw_shell not in ("/usr/bin/nologin", "/sbin/nologin", "/bin/false"):
users.append(p.pw_name)
return users or [os.environ.get("USER", "user")]
def list_sessions():
sessions = [] # (name, exec_argv)
for f in sorted(glob.glob(os.path.join(SESSION_DIR, "*.desktop"))):
name = exec_ = None
try:
for line in open(f, encoding="utf-8"):
if line.startswith("Name=") and name is None:
name = line[5:].strip()
elif line.startswith("Exec=") and exec_ is None:
exec_ = line[5:].strip()
except OSError:
continue
if name and exec_:
sessions.append((name, shlex.split(exec_)))
return sessions or [("Niri", ["niri-session"])]
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
class Greeter(Gtk.Application):
def __init__(self):
super().__init__(application_id="ch.gabrielevarano.TaninGreet")
self.greetd = Greetd()
self.users = list_users()
self.sessions = list_sessions()
def do_activate(self):
self._load_css()
Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True)
win = Gtk.ApplicationWindow(application=self)
win.set_decorated(False)
win.fullscreen()
win.add_css_class("greeter")
overlay = Gtk.Overlay()
win.set_child(overlay)
# background image (falls back to the scrim colour if missing)
if os.path.exists(WALLPAPER):
bg = Gtk.Picture.new_for_filename(WALLPAPER)
bg.set_content_fit(Gtk.ContentFit.COVER)
overlay.set_child(bg)
else:
overlay.set_child(Gtk.Box())
# dark scrim so text stays readable over any wallpaper
scrim = Gtk.Box()
scrim.add_css_class("scrim")
scrim.set_hexpand(True)
scrim.set_vexpand(True)
overlay.add_overlay(scrim)
# main column: top bar + centered login
col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
col.append(self._topbar())
center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
center.set_vexpand(True)
center.set_valign(Gtk.Align.CENTER)
center.set_halign(Gtk.Align.CENTER)
center.append(self._login_card())
col.append(center)
col.append(self._footer()) # TANINUX wordmark, bottom center
overlay.add_overlay(col)
win.present()
self.password.grab_focus()
# debug: auto-open the session dropdown so previews can screenshot the
# popover without a click (TANIN_GREET_POPUP=1). No effect in production.
if os.environ.get("TANIN_GREET_POPUP"):
def _open():
btn = self.session_dd.get_first_child()
try:
btn.set_active(True)
except Exception: # noqa: BLE001
pass
return False
GLib.timeout_add(900, _open)
# ---- pieces ----------------------------------------------------------- #
def _topbar(self):
bar = Gtk.CenterBox()
bar.add_css_class("topbar")
# top-left corner: session dropdown
bar.set_start_widget(self._session_dd())
# center: clock — time · weekday, date
self.clock = Gtk.Label()
self.clock.add_css_class("clock")
bar.set_center_widget(self.clock)
self._tick()
GLib.timeout_add_seconds(1, self._tick)
# top-right corner: power
bar.set_end_widget(self._power_button())
return bar
def _session_dd(self):
self.session_dd = Gtk.DropDown.new_from_strings([s[0] for s in self.sessions])
self.session_dd.add_css_class("session-dd")
for i, (name, _) in enumerate(self.sessions):
if name.lower().startswith("niri"):
self.session_dd.set_selected(i)
break
return self.session_dd
def _footer(self):
box = Gtk.Box()
box.set_halign(Gtk.Align.CENTER)
box.set_valign(Gtk.Align.END)
box.set_margin_bottom(18)
lbl = Gtk.Label(label=BRAND)
lbl.add_css_class("brand")
box.append(lbl)
return box
def _power_button(self):
btn = Gtk.MenuButton()
btn.add_css_class("power")
btn.set_icon_name("system-shutdown-symbolic")
pop = Gtk.Popover()
pop.add_css_class("menu")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for label, argv in (("Reboot", ["systemctl", "reboot"]),
("Power Off", ["systemctl", "poweroff"])):
b = Gtk.Button(label=label)
b.add_css_class("menuitem")
b.connect("clicked", lambda _w, a=argv: self._run(a))
box.append(b)
pop.set_child(box)
btn.set_popover(pop)
return btn
def _login_card(self):
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14)
card.add_css_class("logincard") # NOT "card" — libadwaita's .card adds a bg+shadow
card.set_halign(Gtk.Align.CENTER)
avatar = Gtk.Image.new_from_icon_name("avatar-default-symbolic")
avatar.set_pixel_size(64)
avatar.add_css_class("avatar")
avatar.set_halign(Gtk.Align.CENTER)
card.append(avatar)
# user row — just the NAME as text, with a small dropdown chevron on the
# right edge (aligned with where the password pill ends). Not a pill.
name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
name_row.add_css_class("namerow")
name_row.set_halign(Gtk.Align.CENTER)
self.name_label = Gtk.Label(label=self.users[0])
self.name_label.add_css_class("username")
self.name_label.set_xalign(0.5) # centered
self.name_label.set_hexpand(True)
name_row.append(self.name_label)
self.user_btn = Gtk.MenuButton()
self.user_btn.add_css_class("userswitch")
self.user_btn.set_icon_name("pan-down-symbolic")
self.user_btn.set_visible(len(self.users) > 1) # no point if there's one user
upop = Gtk.Popover()
upop.add_css_class("menu")
ubox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for u in self.users:
b = Gtk.Button(label=u)
b.add_css_class("menuitem")
b.connect("clicked", lambda _w, name=u: (self.name_label.set_text(name),
self.user_btn.popdown()))
ubox.append(b)
upop.set_child(ubox)
self.user_btn.set_popover(upop)
name_row.append(self.user_btn)
card.append(name_row)
# password pill
self.password = Gtk.PasswordEntry()
self.password.set_show_peek_icon(True)
self.password.set_property("placeholder-text", "Password")
self.password.add_css_class("pill")
self.password.connect("activate", self._on_submit)
card.append(self.password)
self.error = Gtk.Label()
self.error.add_css_class("error")
self.error.set_visible(False)
card.append(self.error)
return card
# ---- behaviour -------------------------------------------------------- #
def _tick(self):
now = GLib.DateTime.new_now_local()
self.clock.set_text(now.format("%H:%M · %A, %d %B %Y"))
return True
def _on_submit(self, _entry):
user = self.name_label.get_text()
pw = self.password.get_text()
cmd = self.sessions[self.session_dd.get_selected()][1]
try:
self.greetd.login(user, pw, cmd)
self.quit() # greetd takes over from here
except Exception as e: # noqa: BLE001
self.password.set_text("")
self.error.set_text(str(e))
self.error.set_visible(True)
def _run(self, argv):
try:
Gio.Subprocess.new(argv, Gio.SubprocessFlags.NONE)
except GLib.Error as e:
self.error.set_text(str(e))
self.error.set_visible(True)
def _load_css(self):
if not os.path.exists(CSS_PATH):
return
provider = Gtk.CssProvider()
provider.load_from_path(CSS_PATH)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if __name__ == "__main__":
sys.exit(Greeter().run(None))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = tanin-libadwaita
pkgbase = tanin-libadwaita
xdata = pkgtype=pkg
pkgver = 0.1.0-1
pkgdesc = TANINUX Fuji recolour for GTK4/libadwaita + GTK3 — light + dark, pills
url = https://taninux.kgva.ch
builddate = 1781813716
packager = Unknown Packager
size = 13823
arch = any
license = GPL-3.0-or-later
depend = glib2
depend = adw-gtk3
optdepend = tanin-setup: seed the variant + start the color-scheme watcher on login
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# tanin-libadwaita {dark|light|auto|watch}
# Activates the Fuji recolour for BOTH GTK4/libadwaita and GTK3 by symlinking
# the chosen variant into ~/.config/gtk-{4.0,3.0}/gtk.css.
# dark|light set that mode (and the GNOME color-scheme + gtk-theme)
# auto follow the CURRENT color-scheme once (don't change it)
# watch follow color-scheme changes live (daemon) — the appearance
# toggle in settings then drives light/dark automatically
set -euo pipefail
SHARE="${TANIN_LIBADWAITA_DIR:-/usr/share/tanin/libadwaita}"
CFG="${XDG_CONFIG_HOME:-$HOME/.config}"
# Symlink both toolkits' gtk.css to the variant + pick the matching adw-gtk3.
# Does NOT touch color-scheme (so it's safe to call from a color-scheme watcher).
link_variant() {
local mode="$1" gtktheme
case "$mode" in
dark) gtktheme="adw-gtk3-dark" ;;
light) gtktheme="adw-gtk3" ;;
*) return 2 ;;
esac
local v src dst
for v in gtk-4.0 gtk-3.0; do
src="$SHARE/$v/$mode.css"; dst="$CFG/$v/gtk.css"
[ -f "$src" ] || continue
mkdir -p "$CFG/$v"
if [ -e "$dst" ] && [ ! -L "$dst" ] && [ ! -e "$dst.pre-tanin" ]; then
cp -a "$dst" "$dst.pre-tanin" # back up a real file once
fi
ln -sf "$src" "$dst"
done
gsettings set org.gnome.desktop.interface gtk-theme "$gtktheme" 2>/dev/null || true
gsettings set org.gnome.desktop.interface accent-color "purple" 2>/dev/null || true
echo "tanin-libadwaita: $mode"
}
current_mode() {
case "$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null)" in
*prefer-light*) echo light ;;
*) echo dark ;;
esac
}
case "${1:-auto}" in
dark|light)
link_variant "$1"
gsettings set org.gnome.desktop.interface color-scheme \
"$([ "$1" = light ] && echo prefer-light || echo prefer-dark)" 2>/dev/null || true
;;
auto)
link_variant "$(current_mode)"
;;
watch)
link_variant "$(current_mode)"
gsettings monitor org.gnome.desktop.interface color-scheme | while read -r _; do
link_variant "$(current_mode)"
done
;;
*)
echo "usage: tanin-libadwaita {dark|light|auto|watch}" >&2; exit 2 ;;
esac
@@ -0,0 +1,13 @@
[Unit]
Description=TANINUX libadwaita theme follower (Fuji light/dark on color-scheme change)
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/tanin-libadwaita watch
Restart=on-failure
RestartSec=2
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,66 @@
/* =========================================================================
* tanin-libadwaita Fuji DARK recolour for GTK3 (rides on adw-gtk3-dark).
* Covers the gaps the libadwaita/GTK4 recolour can't reach: GTK3 apps
* (pavucontrol, nm-connection-editor, ). Same palette as the GTK4 dark.
* ========================================================================= */
/* libadwaita-style names (adw-gtk3 honours these) */
@define-color accent_color #a39ec4;
@define-color accent_bg_color #a39ec4;
@define-color accent_fg_color #0f0f0f;
@define-color window_bg_color #0f0f0f;
@define-color window_fg_color #f0f0f0;
@define-color view_bg_color #141414;
@define-color view_fg_color #f0f0f0;
@define-color headerbar_bg_color #0f0f0f;
@define-color headerbar_fg_color #f0f0f0;
@define-color card_bg_color #191919;
@define-color card_fg_color #f0f0f0;
@define-color popover_bg_color #191919;
@define-color popover_fg_color #f0f0f0;
@define-color dialog_bg_color #141414;
@define-color dialog_fg_color #f0f0f0;
@define-color sidebar_bg_color #131313;
/* legacy GTK3 / Adwaita names */
@define-color theme_bg_color #0f0f0f;
@define-color theme_fg_color #f0f0f0;
@define-color theme_base_color #141414;
@define-color theme_text_color #f0f0f0;
@define-color theme_selected_bg_color #a39ec4;
@define-color theme_selected_fg_color #0f0f0f;
@define-color insensitive_bg_color #161616;
@define-color insensitive_fg_color #6a6a6a;
@define-color insensitive_base_color #121212;
@define-color borders rgba(240, 240, 240, 0.10);
@define-color destructive_color #e0a3a3;
@define-color success_color #9ec4a3;
@define-color warning_color #c4bd9e;
@define-color error_color #e0a3a3;
/* pill-shaped controls */
button,
entry,
combobox button,
spinbutton,
notebook > header > tabs > tab { border-radius: 999px; }
/* Force the Fuji accent. adw-gtk3 otherwise follows the GNOME accent-color
* gsetting (would stay blue), so we override the accented widgets explicitly. */
scale highlight,
scale trough progress,
progressbar progress,
levelbar block.filled { background-color: @accent_color; }
checkbutton check:checked,
radiobutton radio:checked,
check:checked, radio:checked { background-color: @accent_color; border-color: @accent_color; }
switch:checked { background-color: @accent_color; }
notebook > header tab:checked { box-shadow: inset 0 -3px @accent_color; }
notebook > header tab:checked label { color: @accent_color; }
row:selected, list row:selected,
treeview.view:selected, .view:selected,
menuitem:hover, menu menuitem:hover,
*:selected { background-color: @accent_color; color: @accent_fg_color; }
button.suggested-action { background-color: @accent_color; color: @accent_fg_color; }
entry:focus, entry:focus-within { border-color: @accent_color; }
@@ -0,0 +1,65 @@
/* =========================================================================
* tanin-libadwaita Fuji LIGHT recolour for GTK3 (rides on adw-gtk3).
* Same warm Fuji light palette as the GTK4 light variant.
* ========================================================================= */
/* libadwaita-style names (adw-gtk3 honours these) */
@define-color accent_color #6f67a8;
@define-color accent_bg_color #6f67a8;
@define-color accent_fg_color #ffffff;
@define-color window_bg_color #f3f1ec;
@define-color window_fg_color #2a2926;
@define-color view_bg_color #faf8f4;
@define-color view_fg_color #2a2926;
@define-color headerbar_bg_color #ece8e1;
@define-color headerbar_fg_color #2a2926;
@define-color card_bg_color #ffffff;
@define-color card_fg_color #2a2926;
@define-color popover_bg_color #ffffff;
@define-color popover_fg_color #2a2926;
@define-color dialog_bg_color #faf8f4;
@define-color dialog_fg_color #2a2926;
@define-color sidebar_bg_color #ece8e1;
/* legacy GTK3 / Adwaita names */
@define-color theme_bg_color #f3f1ec;
@define-color theme_fg_color #2a2926;
@define-color theme_base_color #faf8f4;
@define-color theme_text_color #2a2926;
@define-color theme_selected_bg_color #6f67a8;
@define-color theme_selected_fg_color #ffffff;
@define-color insensitive_bg_color #ebe7e0;
@define-color insensitive_fg_color #9b968c;
@define-color insensitive_base_color #f1eee8;
@define-color borders rgba(0, 0, 0, 0.12);
@define-color destructive_color #b04848;
@define-color success_color #4f7a57;
@define-color warning_color #8a7f3a;
@define-color error_color #b04848;
/* pill-shaped controls */
button,
entry,
combobox button,
spinbutton,
notebook > header > tabs > tab { border-radius: 999px; }
/* Force the Fuji accent. adw-gtk3 otherwise follows the GNOME accent-color
* gsetting, so we override the accented widgets explicitly. */
scale highlight,
scale trough progress,
progressbar progress,
levelbar block.filled { background-color: @accent_color; }
checkbutton check:checked,
radiobutton radio:checked,
check:checked, radio:checked { background-color: @accent_color; border-color: @accent_color; }
switch:checked { background-color: @accent_color; }
notebook > header tab:checked { box-shadow: inset 0 -3px @accent_color; }
notebook > header tab:checked label { color: @accent_color; }
row:selected, list row:selected,
treeview.view:selected, .view:selected,
menuitem:hover, menu menuitem:hover,
*:selected { background-color: @accent_color; color: @accent_fg_color; }
button.suggested-action { background-color: @accent_color; color: @accent_fg_color; }
entry:focus, entry:focus-within { border-color: @accent_color; }
@@ -0,0 +1,74 @@
/* =========================================================================
* tanin-theme Fuji recolour for libadwaita / GTK4.
* Drop-in at ~/.config/gtk-4.0/gtk.css. Overrides libadwaita's NAMED colors,
* which recolours every libadwaita app (tsettings/thub, Nautilus, ) without a
* full custom theme. Palette is 1:1 with eww.scss / the greeter.
* ========================================================================= */
/* ---- Fuji palette -------------------------------------------------------- */
@define-color fuji_bg #0f0f0f;
@define-color fuji_bg_alt #191919;
@define-color fuji_bg_hi #242424;
@define-color fuji_fg #f0f0f0;
@define-color fuji_muted #8a8a8a;
@define-color fuji_accent #a39ec4;
/* ---- accent -------------------------------------------------------------- */
@define-color accent_color #a39ec4;
@define-color accent_bg_color #a39ec4;
@define-color accent_fg_color #0f0f0f; /* dark text on the light violet */
/* ---- window / base surfaces --------------------------------------------- */
@define-color window_bg_color #0f0f0f;
@define-color window_fg_color #f0f0f0;
@define-color view_bg_color #141414;
@define-color view_fg_color #f0f0f0;
/* ---- headerbar ----------------------------------------------------------- */
@define-color headerbar_bg_color #0f0f0f;
@define-color headerbar_fg_color #f0f0f0;
@define-color headerbar_border_color #f0f0f0;
@define-color headerbar_backdrop_color #0f0f0f;
@define-color headerbar_shade_color rgba(0, 0, 0, 0.5);
/* ---- cards / dialogs / popovers / sidebar ------------------------------- */
@define-color card_bg_color #191919;
@define-color card_fg_color #f0f0f0;
@define-color card_shade_color rgba(0, 0, 0, 0.45);
@define-color dialog_bg_color #141414;
@define-color dialog_fg_color #f0f0f0;
@define-color popover_bg_color #191919;
@define-color popover_fg_color #f0f0f0;
@define-color sidebar_bg_color #131313;
@define-color sidebar_fg_color #f0f0f0;
@define-color sidebar_backdrop_color #0f0f0f;
@define-color sidebar_shade_color rgba(0, 0, 0, 0.45);
/* ---- semantic ------------------------------------------------------------ */
@define-color destructive_color #e0a3a3;
@define-color destructive_bg_color #b45c5c;
@define-color destructive_fg_color #0f0f0f;
@define-color success_color #9ec4a3;
@define-color warning_color #c4bd9e;
@define-color error_color #e0a3a3;
@define-color shade_color rgba(0, 0, 0, 0.4);
@define-color scrollbar_outline_color rgba(255, 255, 255, 0.06);
/* ---- pill-shaped controls (Fuji house style) --------------------------- *
* Buttons, entries and toggles become stadium pills like the bar/greeter. */
button,
button.toggle,
menubutton > button,
dropdown > button,
splitbutton > button,
entry,
spinbutton,
.pill {
border-radius: 999px;
min-height: 0;
}
/* containers stay softly rounded, not stadium-shaped */
.card,
popover > contents,
.osd { border-radius: 14px; }
@@ -0,0 +1,71 @@
/* =========================================================================
* tanin-libadwaita Fuji LIGHT recolour for libadwaita / GTK4.
* Warm "Yuki/Fuji" light palette (1:1 with eww.scss light tokens), deeper
* violet accent so it reads on white. Pair with color-scheme=prefer-light.
* ========================================================================= */
/* ---- Fuji light palette -------------------------------------------------- */
@define-color fuji_bg #f3f1ec; /* soft warm white */
@define-color fuji_bg_alt #e8e4dc; /* cards / tiles */
@define-color fuji_bg_hi #ddd8ce; /* hover */
@define-color fuji_fg #2a2926; /* near-black ink */
@define-color fuji_muted #6f6b62;
@define-color fuji_accent #6f67a8; /* deeper violet — readable on light */
/* ---- accent -------------------------------------------------------------- */
@define-color accent_color #6f67a8;
@define-color accent_bg_color #6f67a8;
@define-color accent_fg_color #ffffff;
/* ---- window / base surfaces --------------------------------------------- */
@define-color window_bg_color #f3f1ec;
@define-color window_fg_color #2a2926;
@define-color view_bg_color #faf8f4;
@define-color view_fg_color #2a2926;
/* ---- headerbar ----------------------------------------------------------- */
@define-color headerbar_bg_color #ece8e1;
@define-color headerbar_fg_color #2a2926;
@define-color headerbar_border_color #2a2926;
@define-color headerbar_backdrop_color #f3f1ec;
@define-color headerbar_shade_color rgba(0, 0, 0, 0.08);
/* ---- cards / dialogs / popovers / sidebar ------------------------------- */
@define-color card_bg_color #ffffff;
@define-color card_fg_color #2a2926;
@define-color card_shade_color rgba(0, 0, 0, 0.07);
@define-color dialog_bg_color #faf8f4;
@define-color dialog_fg_color #2a2926;
@define-color popover_bg_color #ffffff;
@define-color popover_fg_color #2a2926;
@define-color sidebar_bg_color #ece8e1;
@define-color sidebar_fg_color #2a2926;
@define-color sidebar_backdrop_color #f3f1ec;
@define-color sidebar_shade_color rgba(0, 0, 0, 0.07);
/* ---- semantic ------------------------------------------------------------ */
@define-color destructive_color #b04848;
@define-color destructive_bg_color #b04848;
@define-color destructive_fg_color #ffffff;
@define-color success_color #4f7a57;
@define-color warning_color #8a7f3a;
@define-color error_color #b04848;
@define-color shade_color rgba(0, 0, 0, 0.07);
@define-color scrollbar_outline_color rgba(0, 0, 0, 0.08);
/* ---- pill-shaped controls (Fuji house style) --------------------------- */
button,
button.toggle,
menubutton > button,
dropdown > button,
splitbutton > button,
entry,
spinbutton,
.pill {
border-radius: 999px;
min-height: 0;
}
.card,
popover > contents,
.osd { border-radius: 14px; }
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,16 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = tanin-setup
pkgbase = tanin-setup
xdata = pkgtype=pkg
pkgver = 0.1.0-1
pkgdesc = TANINUX first-run setup — seed user configs and enable the session portal
url = https://taninux.kgva.ch
builddate = 1781813718
packager = Unknown Packager
size = 2207
arch = any
license = GPL-3.0-or-later
depend = systemd
depend = xdg-desktop-portal
depend = xdg-desktop-portal-gtk
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# TANINUX first-run setup: seedet Default-Configs nach ~/.config und richtet
# das Session-Portal ein (damit libadwaita-Apps Hell/Dunkel übernehmen).
# Idempotent. --force überschreibt bestehende Configs (mit .bak-Backup).
set -euo pipefail
SKEL="/usr/share/tanin/skel"
UNIT="/usr/share/tanin/tanin-session.target"
CFG="${XDG_CONFIG_HOME:-$HOME/.config}"
FORCE=0
[ "${1:-}" = "--force" ] && FORCE=1
seed_configs() {
[ -d "$SKEL" ] || return 0
while IFS= read -r -d '' src; do
rel="${src#"$SKEL"/}"
dst="$HOME/$rel"
mkdir -p "$(dirname "$dst")"
if [ -e "$dst" ] && [ "$FORCE" -eq 0 ]; then
continue
fi
[ -e "$dst" ] && cp -a "$dst" "$dst.bak" 2>/dev/null || true
cp -a "$src" "$dst"
done < <(find "$SKEL" -type f -print0)
}
install_session_target() {
[ -f "$UNIT" ] || return 0
install -Dm644 "$UNIT" "$CFG/systemd/user/tanin-session.target"
systemctl --user daemon-reload 2>/dev/null || true
}
# Apply the Fuji GTK theme for the current light/dark mode and enable the
# follower service (tanin-libadwaita watch), so the appearance toggle in the
# TANINUX settings retints every GTK3/GTK4 app live. Best-effort.
enable_theme_follower() {
command -v tanin-libadwaita >/dev/null 2>&1 || return 0
tanin-libadwaita auto 2>/dev/null || true
systemctl --user enable --now tanin-libadwaita.service 2>/dev/null || true
}
seed_configs
install_session_target
enable_theme_follower
cat <<'EOF'
TANINUX setup complete.
• configs seeded into ~/.config (eww + niri)
• session target installed (~/.config/systemd/user/tanin-session.target)
• GTK theme applied + follower enabled (tanin-libadwaita.service)
The shipped niri config already autostarts the session bits:
spawn-at-startup "dbus-update-activation-environment" "--systemd" "--all"
spawn-at-startup "systemctl" "--user" "start" "tanin-session.target"
EOF
@@ -0,0 +1,7 @@
[Unit]
Description=TANINUX graphical session
# Startet (per Dependency-Request) graphical-session.target, an dem
# xdg-desktop-portal hängt — sonst kann das Portal nicht aktivieren und
# libadwaita-Apps (Nautilus …) bleiben im Hell-Fallback.
Requires=graphical-session.target
After=graphical-session.target