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:
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.
Reference in New Issue
Block a user