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:
+1
-2
@@ -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
@@ -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)"
|
||||||
|
|||||||
Executable
+52
@@ -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)
|
||||||
Binary file not shown.
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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||||
+105
@@ -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
|
||||||
+51
@@ -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
|
||||||
+36
@@ -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
|
||||||
+23
@@ -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
|
||||||
+6
@@ -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"
|
||||||
Executable
+21
@@ -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
|
||||||
+12
@@ -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
|
||||||
+308
@@ -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
|
||||||
+27
@@ -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=""
|
||||||
+37
@@ -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
|
||||||
+62
@@ -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
|
||||||
+43
@@ -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
|
||||||
+24
@@ -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
|
||||||
+18
@@ -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
|
||||||
Executable
+79
@@ -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
|
||||||
+47
@@ -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
|
||||||
+64
@@ -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
|
||||||
+12
@@ -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
|
||||||
+8
@@ -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
|
||||||
+275
@@ -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
|
||||||
+30
@@ -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}'
|
||||||
+15
@@ -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
|
||||||
+20
@@ -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
|
||||||
+45
@@ -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() { :; }
|
||||||
+33
@@ -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
|
||||||
+7
@@ -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; }
|
||||||
|
}
|
||||||
Binary file not shown.
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
@@ -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))
|
||||||
Binary file not shown.
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-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
|
||||||
+13
@@ -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
|
||||||
+66
@@ -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; }
|
||||||
+65
@@ -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; }
|
||||||
+74
@@ -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; }
|
||||||
+71
@@ -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; }
|
||||||
Binary file not shown.
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
|
||||||
@@ -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
|
||||||
Binary file not shown.
Reference in New Issue
Block a user