Compare commits
56
Commits
dev
...
3c9802ffc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c9802ffc7 | ||
|
|
5e87128f4b | ||
|
|
6c8449103c | ||
|
|
83836af9b1 | ||
|
|
52d956f169 | ||
|
|
14dc52b584 | ||
|
|
55fc4e90d1 | ||
|
|
fd4c3d9a3d | ||
|
|
6998f64d88
|
||
|
|
9a99024688
|
||
|
|
086d8651ff | ||
|
|
0b7dd8f725 | ||
|
|
4f572289a6 | ||
|
|
f7eaf5da47 | ||
|
|
208e7cb2b2
|
||
|
|
6e2601a39c | ||
|
|
07e14bf4e5 | ||
|
|
1657357da4 | ||
|
|
dfa4797354 | ||
|
|
95aa3ad114 | ||
|
|
6162403cdd | ||
|
|
cd42a892ee | ||
|
|
bb5b9930cd | ||
|
|
7f726d7d00 | ||
|
|
f4ab310e1b | ||
|
|
c4407f9b36 | ||
|
|
f1375b486b | ||
|
|
7137dab031 | ||
|
|
02b45cca9b | ||
|
|
000efcaf9d | ||
|
|
4967304641 | ||
|
|
7fdc462f62 | ||
|
|
17206de14a | ||
|
|
ed5b31b1f8 | ||
|
|
b976ba7ee7 | ||
|
|
fca7ea2527 | ||
|
|
2bfd6d60ac | ||
|
|
0494ce8fdc | ||
|
|
2a48d93511 | ||
|
|
5c79487715 | ||
|
|
13b0c2d2bd | ||
|
|
24ea6a440a | ||
|
|
0db15e30bc | ||
|
|
f96b5c09fe | ||
|
|
e1c8fb10d8 | ||
|
|
c9a5394095 | ||
|
|
d662ee6395 | ||
|
|
7211d885e8 | ||
|
|
e59fdce2e5 | ||
|
|
c8251c6f6d
|
||
|
|
98ac3a8844
|
||
|
|
bd109dceee
|
||
|
|
07b7e2bb05
|
||
|
|
b7cfc63310
|
||
|
|
3b688d50ef | ||
|
|
fe2b38e78f |
No files matched your search
@@ -12,14 +12,14 @@ jobs:
|
||||
build-nixos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: "Install Nix ❄️"
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
# - uses: DeterminateSystems/nix-installer-action@v4
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v13
|
||||
- uses: DeterminateSystems/flake-checker-action@v12
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v15
|
||||
- uses: DeterminateSystems/flake-checker-action@v14
|
||||
|
||||
- name: "Install Cachix ❄️"
|
||||
uses: cachix/cachix-action@v17
|
||||
|
||||
@@ -13,15 +13,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Install nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
uses: DeterminateSystems/nix-installer-action@v23
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- name: Update flake.lock
|
||||
uses: DeterminateSystems/update-flake-lock@v28
|
||||
uses: DeterminateSystems/update-flake-lock@v29
|
||||
with:
|
||||
token: ${{ secrets.GT_TOKEN_FOR_UPDATES }}
|
||||
pr-title: "[chore] Update flake.lock"
|
||||
|
||||
Generated
+28
-28
@@ -23,11 +23,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786845137,
|
||||
"narHash": "sha256-oQFip+v0luP8NIxJzmiW4Wu8bILsbFWom5l0zonl8hQ=",
|
||||
"lastModified": 1793418699,
|
||||
"narHash": "sha256-+RpTlcdh9KqZ+ZiLkNSTvibQWm935EcIdBzRPCTFsrQ=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "4cff07de74b50e64bdd68cd4e722ab5b6b35ee48",
|
||||
"rev": "ff0e21a414d4547183396da1b44a66f2577b65e9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -76,11 +76,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1788335262,
|
||||
"narHash": "sha256-3jBEq8avfzlrsY4UpW4d5TOmm7ocseZfnitEJhqauyc=",
|
||||
"lastModified": 1794597451,
|
||||
"narHash": "sha256-bUkc9dMpCFiJuuf5Efr7pnCDs5q18aA8zJah+MYZl9w=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "44d95795ee2d475b3d687325e26dcf4ca9104557",
|
||||
"rev": "eb3c90bd715f7a9a28719bb014f53d309ccb2fc8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -96,11 +96,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787377438,
|
||||
"narHash": "sha256-Sxu1NLTD/Ern6hFGLlZmtKCSct3YQXZI/lls8RE1XeM=",
|
||||
"lastModified": 1794251182,
|
||||
"narHash": "sha256-R2sZBXiOJo3HAnvTiVkbbHDJy//JMlAwUKEiPbfqPXk=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "65258d5c65a250189fde2e35f490d15e064c4c62",
|
||||
"rev": "6eb2d33d1d0c95a37ef6443c88e6c1686012ee8b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -131,11 +131,11 @@
|
||||
},
|
||||
"nixUnstable": {
|
||||
"locked": {
|
||||
"lastModified": 1788372231,
|
||||
"narHash": "sha256-7aqErvrAEz/5OcPA5p3M+tVOqeYc4oJXkNIPXfCqxAw=",
|
||||
"lastModified": 1794495034,
|
||||
"narHash": "sha256-E0hYoioqUtM2XmPPvYYpJgdADF5AW9oOoPSEiM9dTHQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9387b3fcc0c23c86661636da63faabad4235a0a6",
|
||||
"rev": "2074cc672349f14240fb7ea90d509e0b643e9dd4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -153,11 +153,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784642409,
|
||||
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=",
|
||||
"lastModified": 1794368463,
|
||||
"narHash": "sha256-ncpk0eCCyhU7M7U/PGbtwb1PXqobksBBYbJJ13abV2g=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-wsl",
|
||||
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e",
|
||||
"rev": "05721a428830972f025a8575b1a19c418d422d1b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -168,11 +168,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767892417,
|
||||
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
|
||||
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||
"lastModified": 1789546076,
|
||||
"narHash": "sha256-vWkSk5bbfTqdtMoSgD9FshACO8JCvXTFi+3cqEp0mH0=",
|
||||
"rev": "b1b875982b17dabde9b4a37f3e229e74913e6db3",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1074753.b1b875982b17/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -211,11 +211,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1788297115,
|
||||
"narHash": "sha256-Z+vUNbfd2FIKkWOTkcT7RYlh3oFCnig/d2eXD1SWf2E=",
|
||||
"lastModified": 1794552581,
|
||||
"narHash": "sha256-qXFIqA6FCcx7XeDzw0ZNlkDT9UiCzrNDwK8RuU8RcHM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a3116115851d68b8952a2a4221cc25a84e56b532",
|
||||
"rev": "f1e5c356f35d07e7b71a6b5e6c9338e0b029e0c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -245,11 +245,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1788337237,
|
||||
"narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
|
||||
"lastModified": 1794624949,
|
||||
"narHash": "sha256-3q/hxp/flW1PnV3Dx+H3p8kHGEHJ7251BqVMNsAAB9E=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
|
||||
"rev": "11e5885d241743ff0193465d533b20e754bc3ce0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -263,11 +263,11 @@
|
||||
"flake-parts": "flake-parts"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784312229,
|
||||
"narHash": "sha256-2uHCSUw341o3my1R0U0YCfbnMEazylxb58evWsjGL50=",
|
||||
"lastModified": 1794081290,
|
||||
"narHash": "sha256-vJCjuYXp06XZ9rufttvv/W3mkzQzefYxEvmmsZfUmvw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-vscode-server",
|
||||
"rev": "2f984dfbe7e5271b5c413d3e734374cc1306c921",
|
||||
"rev": "e8aac40ed8d76486859741b1608badeca3475943",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{ lib, config, pkgs, ... }: {
|
||||
imports = [ ./network-watchdog.nix ];
|
||||
|
||||
#environment.sessionVariables.SOPS_AGE_KEY_FILE = keyFilePath;
|
||||
systemd.user.startServices = lib.mkIf pkgs.stdenv.isLinux "sd-switch";
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{ lib, config, pkgs, ... }:
|
||||
let
|
||||
cfg = config.services.network-watchdog;
|
||||
watchdog = "${pkgs.custom.network-watchdog}/bin/network-watchdog";
|
||||
units = map (name: "${name}.service") cfg.units;
|
||||
in {
|
||||
# User services that only make sense while online: GUI clients that hold a
|
||||
# socket to a server (jellyfin-mpv-shim, nextcloud-client). Their upstream
|
||||
# units have no network ordering (network-online.target is a system target,
|
||||
# out of reach for user units), so they happily start while offline —
|
||||
# silently dead — and nothing notices a connection dropped by a network
|
||||
# blip. Gate each start on the network being up, restart on failure, and let
|
||||
# a watchdog catch the "up but not connected" state that systemd can't see.
|
||||
# See pkgs.custom.network-watchdog.
|
||||
options.services.network-watchdog.units = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "jellyfin-mpv-shim" ];
|
||||
description = "User services (no .service suffix) to gate on the network and keep connected.";
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.units != [ ]) {
|
||||
systemd.user.services = lib.genAttrs cfg.units (name: {
|
||||
Service = {
|
||||
ExecStartPre = "${watchdog} wait-online ${name}.service";
|
||||
TimeoutStartSec = "infinity"; # stay in start-pre until the network is back
|
||||
Restart = lib.mkDefault "on-failure";
|
||||
RestartSec = lib.mkDefault 5;
|
||||
};
|
||||
}) // {
|
||||
network-watchdog = {
|
||||
Unit = {
|
||||
Description = "Restart network services that are up but not connected";
|
||||
After = units;
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${watchdog} check ${lib.concatStringsSep " " units}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.timers.network-watchdog = {
|
||||
Unit.Description = "Periodic health check for network services";
|
||||
Timer = {
|
||||
OnActiveSec = "60s";
|
||||
OnUnitActiveSec = "60s";
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{ lib, config, pkgs, ... }:
|
||||
let
|
||||
configuredMpv = config.programs.mpv.finalPackage;
|
||||
shimWatchdog = pkgs.custom.jellyfin-mpv-shim-watchdog;
|
||||
in{
|
||||
|
||||
config = lib.mkIf (config.syscfg.make.gui) {
|
||||
@@ -83,36 +82,6 @@ in{
|
||||
|
||||
programs.yt-dlp.enable = true;
|
||||
|
||||
# The upstream jellyfin-mpv-shim service has no Restart and no network
|
||||
# ordering: it happily starts while offline (silently dead) and never
|
||||
# recovers a connection dropped by a network blip. Gate its start on the
|
||||
# network being up, and let a watchdog notice the "up but not connected"
|
||||
# state that systemd can't see. See pkgs.custom.jellyfin-mpv-shim-watchdog.
|
||||
systemd.user.services.jellyfin-mpv-shim.Service = {
|
||||
ExecStartPre = "${shimWatchdog}/bin/jellyfin-mpv-shim-watchdog wait-online";
|
||||
TimeoutStartSec = "infinity"; # stay in start-pre until the network is back
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
|
||||
systemd.user.services.jellyfin-mpv-shim-watchdog = {
|
||||
Unit = {
|
||||
Description = "Restart jellyfin-mpv-shim when it is up but not connected";
|
||||
After = [ "jellyfin-mpv-shim.service" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${shimWatchdog}/bin/jellyfin-mpv-shim-watchdog check";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.timers.jellyfin-mpv-shim-watchdog = {
|
||||
Unit.Description = "Periodic health check for jellyfin-mpv-shim";
|
||||
Timer = {
|
||||
OnActiveSec = "60s";
|
||||
OnUnitActiveSec = "60s";
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
services.network-watchdog.units = [ "jellyfin-mpv-shim" ];
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
{ lib, config, ... }: {
|
||||
|
||||
config = lib.mkIf (config.syscfg.make.gui) {
|
||||
services.easyeffects.enable = true;
|
||||
|
||||
xdg.configFile."pipewire/pipewire-pulse.conf.d/desktop.conf".text = ''
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
{ lib, config, pkgs, ... }: {
|
||||
|
||||
config = lib.mkIf (config.syscfg.make.gui) {
|
||||
services.nextcloud-client.enable = true;
|
||||
services.nextcloud-client = {
|
||||
enable = true;
|
||||
startInBackground = true;
|
||||
};
|
||||
services.network-watchdog.units = [ "nextcloud-client" ];
|
||||
|
||||
systemd.user.services.keepassxc = lib.mkIf (config.usercfg.wm != "X11") {
|
||||
Unit = {
|
||||
Description = "KeePassXC";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.keepassxc}/bin/keepassxc";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
home.packages = with pkgs; [
|
||||
thunar
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
@include border-radius;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.net-add-btn { margin-right: $popup-scale * 2pt; }
|
||||
|
||||
// Bluetooth device list
|
||||
.bt-device-row { margin-bottom: $popup-scale * 4pt; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.cpubar { color: $base0C; }
|
||||
.gpubar { color: $base0E; }
|
||||
.membar { color: $base08; }
|
||||
.batbar { color: $base0B; } // overridden by .bat-<state> below
|
||||
.batbar { color: $base0A; } // overridden by .bat-<state> below
|
||||
.cpubar, .gpubar, .membar, .batbar { @include background-base2; margin: $gaps-window 0; }
|
||||
|
||||
// Window chrome
|
||||
@@ -66,7 +66,9 @@
|
||||
.gpu-ring-label, .vram-usage-label, .swap-section-label { margin-top: 2pt; }
|
||||
|
||||
.cpu-core-label { font-size: 0.7em; @include color-active; }
|
||||
.bat-ring-label { font-size: 0.7em; @include color-body; }
|
||||
.bat-ring-label { font-size: 0.85em; font-weight: bold; @include color-body; }
|
||||
.bat-stat-value { font-size: 1.05em; font-weight: bold; @include color-body; }
|
||||
.bat-stat-label { font-size: 0.62em; margin-top: 2pt; @include color-active; }
|
||||
.ram-total-label { font-size: 0.72em; @include color-active; }
|
||||
|
||||
// GPU stats row
|
||||
@@ -92,7 +94,18 @@
|
||||
margin: $popup-scale * 4pt;
|
||||
}
|
||||
|
||||
// Battery colour by state (scripts/sys/battery → .bat-<state>)
|
||||
.batbar, .bat-ring { color: $base0B; }
|
||||
.bat-charging { color: $base0C; }
|
||||
// Battery colour by state (scripts/sys/battery → .bat-<state>):
|
||||
// light blue on mains (charging / full / idle), on battery green, orange below
|
||||
// 25 %, red at ≤15 %
|
||||
.batbar, .bat-ring { color: $base0A; }
|
||||
.bat-charging, .bat-full, .bat-idle { color: $base0B; }
|
||||
.bat-low { color: $base08; }
|
||||
.bat-critical { color: $base0F; }
|
||||
|
||||
// Battery history plot: one vertical bar per bucket, same colours as the ring
|
||||
.bat-hist { min-height: 44px; margin-top: $popup-scale * 8pt; }
|
||||
.bat-hist-bar { @include background-base2; margin: 0 2px; }
|
||||
.bat-hist-bar trough * { background-color: $base0A; }
|
||||
.hist-charging trough *, .hist-idle trough * { background-color: $base0B; }
|
||||
.hist-low trough * { background-color: $base08; }
|
||||
.hist-critical trough * { background-color: $base0F; }
|
||||
@@ -1,5 +1,5 @@
|
||||
(deflisten net
|
||||
:initial '{"wifi":{"connected":false,"enabled":false,"icon":"","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""},"wireguard":[],"vpn":{"up":false,"name":""}}'
|
||||
:initial '{"wifi":{"connected":false,"enabled":false,"icon":"","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""},"wireguard":[],"vpn":{"up":false,"full":false,"name":""}}'
|
||||
"scripts/net/net")
|
||||
|
||||
(deflisten bt
|
||||
@@ -23,7 +23,7 @@
|
||||
(label
|
||||
:visible {net.vpn.up}
|
||||
:class "net-icon net-active"
|
||||
:tooltip "VPN: ${net.vpn.name}"
|
||||
:tooltip {net.vpn.full ? "VPN: all traffic through ${net.vpn.name}" : "WireGuard: ${net.vpn.name}"}
|
||||
:text "")
|
||||
(label
|
||||
:class "net-icon ${net.wifi.connected ? 'net-active' : 'net-dim'}"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
(deflisten gpu :initial '{"gfx_pct":0,"mem_pct":0,"media_pct":0,"sclk":0,"mclk":0,"sclk_pct":0,"mclk_pct":0,"vclk":0,"vclk_pct":0,"temp":0,"power":0,"vram_used":0,"vram_total":1}' "scripts/sys/gpu")
|
||||
(deflisten memory :initial '{"total":1,"used":0,"cached":0,"human":{"total":"0G","used":"0G","cached":"0G"},"swap":{"total":0,"used":0,"human":{"total":"0G","used":"0G"}}}' "scripts/sys/memory")
|
||||
|
||||
; state: charging | discharging | low | full | idle | none — styled via .bat-<state>
|
||||
(deflisten battery :initial '{"visible":false,"percentage":0,"wattage":"","status":"","state":"none"}' "scripts/sys/battery")
|
||||
; state: charging | discharging | low (<25%) | critical (≤15%) | full | idle | none — styled via .bat-<state>
|
||||
; history: 6 h of {t, p, s} buckets (p = -1 when empty)
|
||||
(deflisten battery :initial '{"visible":false,"percentage":0,"wattage":"","status":"","time":"","state":"none","rate":"","history":[]}' "scripts/sys/battery")
|
||||
|
||||
(defwidget sys-mod []
|
||||
(module
|
||||
|
||||
@@ -39,6 +39,18 @@ detach() {
|
||||
systemd-run --user --collect --quiet "${unit[@]}" -- "$cmd" "$@"
|
||||
}
|
||||
|
||||
# eww kills a click handler that has not returned within the widget's
|
||||
# :timeout (200 ms) — less than the eww client calls most handlers make
|
||||
# (~50 ms each), let alone bluetoothctl or a station list fetch. Re-run
|
||||
# this script detached and return at once. Call it with the original
|
||||
# arguments, before anything with side effects.
|
||||
# detach_self "$@"
|
||||
detach_self() {
|
||||
[[ -n ${EWW_DETACHED-} ]] && return 0
|
||||
EWW_DETACHED=1 setsid -f "$0" "$@"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Event multiplexer for listener scripts. Producers run in the background
|
||||
# and write one line per event into a fifo; the main loop re-evaluates the
|
||||
# state on every event, coalescing bursts into a single refresh.
|
||||
|
||||
@@ -5,6 +5,7 @@ active() { rfkill list wifi 2>/dev/null | grep -q "Soft blocked: yes"; }
|
||||
case ${1:-} in
|
||||
status) active && echo true || echo false ;;
|
||||
*)
|
||||
detach_self "$@"
|
||||
if active; then
|
||||
rfkill unblock all
|
||||
state=false
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
case ${1:-} in
|
||||
status) dunstctl is-paused ;;
|
||||
*)
|
||||
detach_self "$@"
|
||||
dunstctl set-paused toggle
|
||||
state=$(dunstctl is-paused)
|
||||
eww update do-not-disturb="$state" 2>/dev/null
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Connect or disconnect the bluetooth device with MAC $1.
|
||||
mac=$1
|
||||
[[ -n $mac ]] || exit 64
|
||||
detach_self "$@"
|
||||
if bluetoothctl info "$mac" 2>/dev/null | grep -q "Connected: yes"; then
|
||||
bluetoothctl disconnect "$mac"
|
||||
else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Network state for the bar icons and the net panel: wifi, ethernet and USB
|
||||
# tethering. Refreshes on link/address changes and every 30 s (signal level).
|
||||
# tethering. Refreshes on link/address/rule changes and every 30 s (signal level).
|
||||
|
||||
wifi_iface() { awk 'NR > 2 { sub(":", "", $1); print $1; exit }' /proc/net/wireless; }
|
||||
eth_iface() { ip -o link 2>/dev/null | awk '$2 ~ /^en[op]/ { sub(":", "", $2); print $2; exit }'; }
|
||||
@@ -84,7 +84,7 @@ state() {
|
||||
wg_ifaces() {
|
||||
{
|
||||
systemctl list-units --all --plain --no-legend 'wireguard-*.service' 2>/dev/null \
|
||||
| awk '$1 !~ /-peer-/ { sub(/^wireguard-/, "", $1); sub(/\.service$/, "", $1); print $1 }'
|
||||
| awk '$1 !~ /-peer-|-vpn\.service$/ { sub(/^wireguard-/, "", $1); sub(/\.service$/, "", $1); print $1 }'
|
||||
ip -d -o link 2>/dev/null | awk '/ wireguard / { sub(":", "", $2); print $2 }'
|
||||
} | sort -u
|
||||
}
|
||||
@@ -97,10 +97,12 @@ human_bytes() {
|
||||
}
|
||||
|
||||
wg_json() { # prints the "wireguard" array and the "vpn" summary
|
||||
local name up ip rx tx peers endpoint peer_unit peer_script sep="" out="[" vpn_up=false vpn_name=""
|
||||
local name up ip rx tx peers endpoint peer_unit peer_script vpn_able vpn sep="" out="[" vpn_up=false vpn_name="" vpn_full=false
|
||||
while read -r name; do
|
||||
[[ -n $name ]] || continue
|
||||
up=false ip="" rx=0 tx=0 endpoint=""
|
||||
up=false ip="" rx=0 tx=0 endpoint="" vpn_able=false vpn=false
|
||||
[[ $(systemctl show -p LoadState --value "wireguard-$name-vpn.service" 2>/dev/null) == loaded ]] && vpn_able=true
|
||||
systemctl is-active --quiet "wireguard-$name-vpn.service" 2>/dev/null && vpn=true
|
||||
if [[ -d /sys/class/net/$name ]]; then
|
||||
ip -o link show "$name" 2>/dev/null | grep -q '<[^>]*UP' && up=true
|
||||
ip=$(ipv4 "$name")
|
||||
@@ -120,14 +122,15 @@ wg_json() { # prints the "wireguard" array and the "vpn" summary
|
||||
fi
|
||||
if [[ $up == true && $vpn_up == false ]]; then vpn_up=true vpn_name=$name; fi
|
||||
[[ -z $vpn_name ]] && vpn_name=$name
|
||||
out+="$sep{\"name\":\"$(jstr "$name")\",\"up\":$up,\"ip\":\"$ip\",\"rx\":\"$(human_bytes "$rx")\",\"tx\":\"$(human_bytes "$tx")\",\"peers\":\"$(jstr "$peers")\",\"endpoint\":\"$(jstr "$endpoint")\"}"
|
||||
[[ $vpn == true ]] && vpn_full=true vpn_name=$name
|
||||
out+="$sep{\"name\":\"$(jstr "$name")\",\"up\":$up,\"ip\":\"$ip\",\"rx\":\"$(human_bytes "$rx")\",\"tx\":\"$(human_bytes "$tx")\",\"peers\":\"$(jstr "$peers")\",\"endpoint\":\"$(jstr "$endpoint")\",\"vpn_able\":$vpn_able,\"vpn\":$vpn}"
|
||||
sep=,
|
||||
done < <(wg_ifaces)
|
||||
printf '"wireguard":%s],"vpn":{"up":%s,"name":"%s"}' "$out" "$vpn_up" "$(jstr "$vpn_name")"
|
||||
printf '"wireguard":%s],"vpn":{"up":%s,"full":%s,"name":"%s"}' "$out" "$vpn_up" "$vpn_full" "$(jstr "$vpn_name")"
|
||||
}
|
||||
|
||||
refresh() { emit "$(state)"; }
|
||||
_link_events() { exec ip monitor link address; }
|
||||
_link_events() { exec ip monitor link address rule; }
|
||||
|
||||
refresh
|
||||
mux_init
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Needs the polkit rule from modules/nixos/system/network/wireguard.
|
||||
name=$1
|
||||
[[ -n $name ]] || exit 64
|
||||
detach_self "$@"
|
||||
if [[ -d /sys/class/net/$name ]]; then
|
||||
systemctl stop "wireguard-$name.service"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Toggle VPN mode (all traffic through the tunnel) of WireGuard interface $1,
|
||||
# independently of the tunnel itself. Starting it brings the tunnel up.
|
||||
# Needs the polkit rule from modules/nixos/system/network/wireguard.
|
||||
name=$1
|
||||
[[ -n $name ]] || exit 64
|
||||
detach_self "$@"
|
||||
unit=wireguard-$name-vpn.service
|
||||
if systemctl is-active --quiet "$unit"; then
|
||||
systemctl stop "$unit"
|
||||
else
|
||||
systemctl start "$unit"
|
||||
fi
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pick a nearby WiFi network in wofi and connect to it through wpa_supplicant
|
||||
# (userControlled: the control socket is open to wheel). Networks added here
|
||||
# live in wpa_supplicant's memory only — the config file is a sops secret and
|
||||
# is never rewritten — so they are gone after a reboot.
|
||||
detach_self "$@"
|
||||
|
||||
eww close popup 2>/dev/null
|
||||
eww update active-panel= 2>/dev/null
|
||||
|
||||
iface=""
|
||||
for s in /run/wpa_supplicant/*; do
|
||||
[[ -S $s && ${s##*/} != p2p-* ]] && { iface=${s##*/}; break; }
|
||||
done
|
||||
|
||||
notify() { notify-send -a WiFi -i network-wireless "$@"; }
|
||||
[[ -n $iface ]] || { notify "WiFi" "wpa_supplicant is not running"; exit 1; }
|
||||
|
||||
wpa() { wpa_cli -g "/run/wpa_supplicant/$iface" "$@"; }
|
||||
field() { awk -F= -v k="$1" '$1 == k { print substr($0, length(k) + 2); exit }'; }
|
||||
|
||||
signal_icon() {
|
||||
if (($1 >= -50)); then echo ""
|
||||
elif (($1 >= -60)); then echo ""
|
||||
elif (($1 >= -70)); then echo ""
|
||||
elif (($1 >= -80)); then echo ""
|
||||
else echo ""; fi
|
||||
}
|
||||
|
||||
# wpa_cli prints SSIDs with printf-style escapes (\xNN, \\, \")
|
||||
unescape() { printf '%b' "$1"; }
|
||||
|
||||
saved_id() { # SSID → network id
|
||||
local id s
|
||||
while IFS=$'\t' read -r id s _; do
|
||||
[[ $(unescape "$s") == "$1" ]] && { echo "$id"; return; }
|
||||
done < <(wpa list_networks | tail -n +2)
|
||||
}
|
||||
|
||||
wpa scan >/dev/null
|
||||
sleep 2
|
||||
|
||||
current=$(unescape "$(wpa status | field ssid)")
|
||||
|
||||
declare -A ssid_of flags_of
|
||||
lines=()
|
||||
# strongest BSS per SSID, strongest first; hidden networks are skipped
|
||||
while IFS=$'\t' read -r sig flags raw; do
|
||||
ssid=$(unescape "$raw")
|
||||
[[ -n $ssid && $ssid != *$'\n'* ]] || continue
|
||||
line="$(signal_icon "$sig") $ssid"
|
||||
[[ $flags == *PSK* || $flags == *SAE* || $flags == *EAP* ]] && line+=" "
|
||||
[[ $ssid == "$current" ]] && line+=" "
|
||||
ssid_of[$line]=$ssid
|
||||
flags_of[$line]=$flags
|
||||
lines+=("$line")
|
||||
done < <(wpa scan_results | awk -F'\t' '
|
||||
NR > 1 && $5 != "" { if (!($5 in best) || $3 > best[$5]) { best[$5] = $3; flags[$5] = $4 } }
|
||||
END { for (s in best) printf "%s\t%s\t%s\n", best[s], flags[s], s }' | sort -t$'\t' -k1,1nr)
|
||||
|
||||
((${#lines[@]})) || { notify "WiFi" "No networks found"; exit 0; }
|
||||
|
||||
choice=$(printf '%s\n' "${lines[@]}" | wofi --dmenu -p "WiFi")
|
||||
[[ -n $choice && -n ${ssid_of[$choice]+x} ]] || exit 0
|
||||
ssid=${ssid_of[$choice]}
|
||||
flags=${flags_of[$choice]}
|
||||
[[ $ssid == "$current" ]] && exit 0
|
||||
|
||||
id=$(saved_id "$ssid")
|
||||
new=0
|
||||
if [[ -z $id ]]; then
|
||||
[[ $flags == *EAP* ]] && { notify "$ssid" "Enterprise (EAP) networks are not supported"; exit 1; }
|
||||
id=$(wpa add_network | tail -n 1)
|
||||
[[ $id =~ ^[0-9]+$ ]] || { notify "$ssid" "Could not add the network"; exit 1; }
|
||||
new=1
|
||||
wpa set_network "$id" ssid "$(printf '%s' "$ssid" | od -An -tx1 | tr -d ' \n')" >/dev/null
|
||||
if [[ $flags == *PSK* || $flags == *SAE* ]]; then
|
||||
psk=$(wofi --dmenu --password -p "Password for $ssid" </dev/null)
|
||||
if ((${#psk} < 8 || ${#psk} > 63)); then
|
||||
wpa remove_network "$id" >/dev/null
|
||||
[[ -n $psk ]] && notify "$ssid" "The password must be 8 to 63 characters"
|
||||
exit 1
|
||||
fi
|
||||
wpa set_network "$id" psk "\"$psk\"" >/dev/null
|
||||
if [[ $flags == *SAE* ]]; then
|
||||
wpa set_network "$id" key_mgmt WPA-PSK SAE >/dev/null
|
||||
wpa set_network "$id" ieee80211w 1 >/dev/null
|
||||
fi
|
||||
else
|
||||
wpa set_network "$id" key_mgmt NONE >/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# select_network disables every other network; re-enable them afterwards so
|
||||
# the saved ones still auto-connect later.
|
||||
wpa select_network "$id" >/dev/null
|
||||
for ((i = 0; i < 30; i++)); do
|
||||
sleep 0.5
|
||||
st=$(wpa status)
|
||||
if [[ $(field wpa_state <<<"$st") == COMPLETED && $(field id <<<"$st") == "$id" ]]; then
|
||||
wpa enable_network all >/dev/null
|
||||
notify "Connected" "$ssid"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
((new)) && wpa remove_network "$id" >/dev/null
|
||||
wpa enable_network all >/dev/null
|
||||
wpa reassociate >/dev/null
|
||||
notify "Could not connect to $ssid" "$( ((new)) && echo "Wrong password?" || echo "Timed out")"
|
||||
exit 1
|
||||
@@ -8,6 +8,7 @@ active() { systemctl --user is-active --quiet "$unit" || pgrep -x wlsunset >/dev
|
||||
case ${1:-} in
|
||||
status) active && echo true || echo false ;;
|
||||
*)
|
||||
detach_self "$@"
|
||||
if active; then
|
||||
systemctl --user stop "$unit" 2>/dev/null
|
||||
pkill -x wlsunset
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
# reply can never spawn a second daemon).
|
||||
panel=$1
|
||||
[[ -n $panel ]] || exit 64
|
||||
detach_self "$@"
|
||||
|
||||
# One toggle at a time; the lock dies with this process. fd 9 is closed for
|
||||
# every eww call so nothing can inherit it.
|
||||
|
||||
@@ -5,6 +5,7 @@ active() { [[ $(powerprofilesctl get 2>/dev/null) == power-saver ]]; }
|
||||
case ${1:-} in
|
||||
status) active && echo true || echo false ;;
|
||||
*)
|
||||
detach_self "$@"
|
||||
if active; then
|
||||
powerprofilesctl set balanced 2>/dev/null
|
||||
state=false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Toggle the full-screen power menu on the focused monitor. The open's exit
|
||||
# status is ignored: it exceeds the client's 100 ms reply deadline.
|
||||
detach_self "$@"
|
||||
eww close popup 2>/dev/null
|
||||
eww update active-panel= 2>/dev/null
|
||||
screen=$(eww-bar focused)
|
||||
|
||||
@@ -15,8 +15,9 @@ api="https://www.radiorecord.ru/api"
|
||||
unit=eww-radio
|
||||
state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/eww
|
||||
cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/eww
|
||||
img_dir=$cache_dir/radio-img
|
||||
id_file=$state_dir/radio-station
|
||||
mkdir -p "$state_dir" "$cache_dir"
|
||||
mkdir -p "$state_dir" "$cache_dir" "$img_dir"
|
||||
|
||||
station_ids='[507,522,523,536,537,42532,42602]'
|
||||
# Extra stations: {id (unique integer >= 1000000), title, stream_hls, icon_fill_white ("" for none)}
|
||||
@@ -47,6 +48,32 @@ stations() {
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
# GTK loads CSS url() images synchronously on the UI thread: a remote URL
|
||||
# freezes the whole daemon until the download finishes (or times out). eww
|
||||
# only ever gets file:// URIs of cached copies.
|
||||
local_img() { # URL → file:// URI, "" if unavailable
|
||||
local url=$1 f
|
||||
case $url in
|
||||
file://*) printf '%s' "$url"; return ;;
|
||||
/*) printf 'file://%s' "$url"; return ;;
|
||||
http://* | https://*) ;;
|
||||
*) return ;;
|
||||
esac
|
||||
f=$img_dir/$(cksum <<<"$url" | cut -d' ' -f1)
|
||||
if [[ ! -s $f ]]; then
|
||||
curl -sfL --max-time 5 -o "$f.tmp" "$url" 2>/dev/null && mv "$f.tmp" "$f" || { rm -f "$f.tmp"; return; }
|
||||
fi
|
||||
printf 'file://%s' "$f"
|
||||
}
|
||||
|
||||
localize_stations() {
|
||||
local url map='{}'
|
||||
while IFS= read -r url; do
|
||||
map=$(jq -c --arg k "$url" --arg v "$(local_img "$url")" '.[$k] = $v' <<<"$map")
|
||||
done < <(jq -r '.[].icon_fill_white // empty' <<<"$STATIONS" | sort -u)
|
||||
STATIONS=$(jq -c --argjson m "$map" 'map(.icon_fill_white = ($m[.icon_fill_white // ""] // ""))' <<<"$STATIONS")
|
||||
}
|
||||
|
||||
station() { # ID FIELD
|
||||
jq -r --argjson id "${1:-0}" --arg f "$2" 'first(.[] | select(.id == $id) | .[$f]) // empty' <<<"$STATIONS"
|
||||
}
|
||||
@@ -141,7 +168,8 @@ song_playing() { # ID → SONG from the station history (radiorecord) or the tit
|
||||
if [[ $(station "$id" radiorecord) == true ]]; then
|
||||
fetched=$(curl -sf --compressed --max-time 8 "$api/station/history/?id=$id" 2>/dev/null \
|
||||
| jq -c '.result.history[0] | {artist: (.artist // ""), song: (.song // ""), image600: (.image600 // "")}' 2>/dev/null)
|
||||
[[ -n $fetched && $fetched != null ]] && SONG=$fetched
|
||||
[[ -n $fetched && $fetched != null ]] \
|
||||
&& SONG=$(jq -c --arg i "$(local_img "$(jq -r .image600 <<<"$fetched")")" '.image600 = $i' <<<"$fetched")
|
||||
else
|
||||
SONG=$(jq -cn --arg t "$(station "$id" title)" '{artist: "", song: $t, image600: ""}')
|
||||
fi
|
||||
@@ -159,7 +187,7 @@ media_event() {
|
||||
if [[ -z $player ]]; then
|
||||
MEDIA=$default_media MEDIA_PLAYER=""
|
||||
elif [[ $player != mpv* ]]; then # the radio and jellyfin are mpv, not "external"
|
||||
MEDIA=$(media_json "$player" "$status" "$artist" "$title" "$art")
|
||||
MEDIA=$(media_json "$player" "$status" "$artist" "$title" "$(local_img "$art")")
|
||||
MEDIA_PLAYER=$player
|
||||
fi
|
||||
}
|
||||
@@ -206,7 +234,9 @@ refresh() {
|
||||
}
|
||||
|
||||
status_loop() {
|
||||
find "$img_dir" -type f -mtime +7 -delete 2>/dev/null
|
||||
STATIONS=$(stations)
|
||||
localize_stations
|
||||
SONG=$default_song MEDIA=$default_media MEDIA_PLAYER="" PLAYING="" LAST_SONG=0 LAST_VOL=""
|
||||
refresh
|
||||
mux_init
|
||||
@@ -216,10 +246,10 @@ status_loop() {
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
start) start "$2" ;;
|
||||
stop) stop ;;
|
||||
toggle) if playing; then stop; else start "$(current_id)"; fi ;;
|
||||
vol) set_volume "$2" ;;
|
||||
mute) toggle_mute ;;
|
||||
start) detach_self "$@"; start "$2" ;;
|
||||
stop) detach_self "$@"; stop ;;
|
||||
toggle) detach_self "$@"; if playing; then stop; else start "$(current_id)"; fi ;;
|
||||
vol) detach_self "$@"; set_volume "$2" ;;
|
||||
mute) detach_self "$@"; toggle_mute ;;
|
||||
*) status_loop ;;
|
||||
esac
|
||||
@@ -8,6 +8,7 @@ active() { hyprctl getoption decoration:screen_shader 2>/dev/null | grep -qF "$s
|
||||
case ${1:-} in
|
||||
status) active && echo true || echo false ;;
|
||||
*)
|
||||
detach_self "$@"
|
||||
if active; then
|
||||
hyprctl eval 'hl.config({ decoration = { screen_shader = "" } })' >/dev/null
|
||||
hyprctl eval 'hl.config({ render = { use_fp16 = 2 } })' >/dev/null
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Battery state from sysfs; refreshes on power_supply udev events and every
|
||||
# 30 s. Emits a `state` (charging / discharging / low / full / idle) that the
|
||||
# CSS maps to a colour, instead of hard-coding one here.
|
||||
# 30 s. Emits a `state` (charging / discharging / low / critical / full /
|
||||
# idle) that the CSS maps to a colour, instead of hard-coding one here.
|
||||
#
|
||||
# Every refresh also appends a sample to $XDG_STATE_HOME/eww/battery.log,
|
||||
# which feeds the drain rate (%/h) and the history plot.
|
||||
|
||||
bat=""
|
||||
for d in /sys/class/power_supply/*; do
|
||||
[[ -r $d/type && $(<"$d/type") == Battery ]] && { bat=$d; break; }
|
||||
done
|
||||
if [[ -z $bat ]]; then
|
||||
echo '{"visible":false,"percentage":0,"wattage":"","status":"","state":"none"}'
|
||||
echo '{"visible":false,"percentage":0,"wattage":"","status":"","time":"","state":"none","rate":"","history":[]}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log=${XDG_STATE_HOME:-$HOME/.local/state}/eww/battery.log
|
||||
hist_hours=6 buckets=24
|
||||
mkdir -p "${log%/*}" && touch "$log"
|
||||
|
||||
attr() { # first readable, non-empty attribute of the list, else 0
|
||||
local a v
|
||||
for a in "$@"; do
|
||||
@@ -22,39 +29,83 @@ attr() { # first readable, non-empty attribute of the list, else 0
|
||||
|
||||
hours_to_clock() { awk -v h="$1" 'BEGIN { printf "%d:%02d", int(h), int((h - int(h)) * 60 + 0.5) }'; }
|
||||
|
||||
# Append "epoch pct state" and, once the head is an hour past the plot window,
|
||||
# drop everything older than the window.
|
||||
log_sample() {
|
||||
local now=$1 first
|
||||
printf '%s %s %s\n' "$now" "$2" "$3" >> "$log"
|
||||
read -r first _ < "$log"
|
||||
((first < now - (hist_hours + 1) * 3600)) || return 0
|
||||
awk -v min=$((now - hist_hours * 3600)) '$1 >= min' "$log" > "$log.tmp" && mv "$log.tmp" "$log"
|
||||
}
|
||||
|
||||
# %/h over the last 30 min of samples in the current state, signed; empty
|
||||
# until there are 5 min of them.
|
||||
drain_rate() {
|
||||
awk -v now="$1" -v st="$2" '
|
||||
$1 >= now - 1800 && $3 == st { if (!n++) { t0 = $1; p0 = $2 } t1 = $1; p1 = $2 }
|
||||
END { if (n > 1 && t1 - t0 >= 300) printf "%.1f", (p1 - p0) * 3600 / (t1 - t0) }' "$log"
|
||||
}
|
||||
|
||||
# The plot window split into buckets: mean % of the bucket's samples (-1 when
|
||||
# it has none), the state of its last sample, and its start time as label.
|
||||
plot() {
|
||||
awk -v now="$1" -v hours="$hist_hours" -v n="$buckets" '
|
||||
BEGIN { w = hours * 3600 / n; t0 = now - hours * 3600 }
|
||||
$1 >= t0 { b = int(($1 - t0) / w); if (b >= n) b = n - 1; sum[b] += $2; cnt[b]++; st[b] = $3 }
|
||||
END {
|
||||
printf "["
|
||||
for (b = 0; b < n; b++) {
|
||||
p = -1; s = "none"
|
||||
if (cnt[b]) {
|
||||
p = sprintf("%.1f", sum[b] / cnt[b])
|
||||
s = st[b] == "c" ? "charging" : st[b] == "d" ? "discharging" : "idle"
|
||||
if (st[b] == "d" && p < 25) s = "low"
|
||||
if (st[b] == "d" && p <= 15) s = "critical"
|
||||
}
|
||||
printf "%s{\"t\":\"%s\",\"p\":%s,\"s\":\"%s\"}", (b ? "," : ""), strftime("%H:%M", t0 + b * w), p, s
|
||||
}
|
||||
printf "]"
|
||||
}' "$log"
|
||||
}
|
||||
|
||||
state() {
|
||||
local cap status rate now full power time="" wattage state
|
||||
local now cap status rate now_e full power pct st time="" wattage state
|
||||
now=$(printf '%(%s)T' -1)
|
||||
cap=$(attr capacity)
|
||||
status=$(attr status)
|
||||
rate=$(attr power_now current_now) # µW or µA
|
||||
now=$(attr energy_now charge_now) # µWh or µAh — same unit as rate, so now/rate = hours
|
||||
now_e=$(attr energy_now charge_now) # µWh or µAh — same unit as rate, so now_e/rate = hours
|
||||
full=$(attr energy_full charge_full)
|
||||
if [[ -r $bat/power_now ]]; then power=$rate; else power=$((rate * $(attr voltage_now) / 1000000)); fi
|
||||
wattage=$(awk -v p="$power" 'BEGIN { printf "%.1f W", p / 1000000 }')
|
||||
pct=$(awk -v n="$now_e" -v f="$full" -v c="$cap" 'BEGIN { printf "%.1f", (f > 0 ? 100 * n / f : c) }')
|
||||
if ((rate > 0)); then
|
||||
case $status in
|
||||
Discharging) time=$(hours_to_clock "$(awk -v n="$now" -v r="$rate" 'BEGIN { print n / r }')") ;;
|
||||
Charging) time=$(hours_to_clock "$(awk -v n="$now" -v f="$full" -v r="$rate" 'BEGIN { print (f - n) / r }')") ;;
|
||||
Discharging) time=$(hours_to_clock "$(awk -v n="$now_e" -v r="$rate" 'BEGIN { print n / r }')") ;;
|
||||
Charging) time=$(hours_to_clock "$(awk -v n="$now_e" -v f="$full" -v r="$rate" 'BEGIN { print (f - n) / r }')") ;;
|
||||
esac
|
||||
fi
|
||||
case $status in
|
||||
Charging)
|
||||
state=charging
|
||||
state=charging st=c
|
||||
status="Charging${time:+, $time left}"
|
||||
;;
|
||||
Discharging)
|
||||
state=discharging
|
||||
((cap <= 20)) && state=low
|
||||
state=discharging st=d
|
||||
((cap < 25)) && state=low
|
||||
((cap <= 15)) && state=critical
|
||||
status=${time:+$time left}
|
||||
;;
|
||||
Full)
|
||||
state=full
|
||||
state=full st=i
|
||||
status="Fully charged"
|
||||
;;
|
||||
*) state=idle ;;
|
||||
*) state=idle st=i ;;
|
||||
esac
|
||||
printf '{"visible":true,"percentage":%s,"wattage":"%s","status":"%s","state":"%s"}' \
|
||||
"$cap" "$wattage" "$(jstr "$status")" "$state"
|
||||
log_sample "$now" "$pct" "$st"
|
||||
printf '{"visible":true,"percentage":%s,"wattage":"%s","status":"%s","time":"%s","state":"%s","rate":"%s","history":%s}' \
|
||||
"$cap" "$wattage" "$(jstr "$status")" "$time" "$state" "$(drain_rate "$now" "$st")" "$(plot "$now")"
|
||||
}
|
||||
|
||||
refresh() { emit "$(state)"; }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(box :orientation "h" :space-evenly false :valign "center" :class "sys-section-header"
|
||||
(box :class "section-accent ${accent}")
|
||||
(label :class "sys-label" :text title :hexpand true :halign "start")
|
||||
(children)
|
||||
(button
|
||||
:class "net-toggle-btn ${enabled ? 'net-toggle-on' : 'net-toggle-off'}"
|
||||
:onclick onclick
|
||||
@@ -23,7 +24,13 @@
|
||||
:title "WiFi" :accent "wifi-accent"
|
||||
:enabled {net.wifi.enabled}
|
||||
:onclick {net.wifi.enabled ? "rfkill block wlan" : "rfkill unblock wlan"}
|
||||
:icon-on "" :icon-off "")
|
||||
:icon-on "" :icon-off ""
|
||||
(button
|
||||
:class "net-toggle-btn net-toggle-on net-add-btn"
|
||||
:visible {net.wifi.enabled}
|
||||
:onclick "scripts/net/wifi-connect"
|
||||
:tooltip "Connect to a network"
|
||||
(label :text "")))
|
||||
(box :orientation "v" :space-evenly false
|
||||
:visible {net.wifi.connected}
|
||||
(netinfo-row :label "SSID" :value {net.wifi.ssid})
|
||||
@@ -89,12 +96,19 @@
|
||||
(box :orientation "v" :space-evenly false :class "wg-row"
|
||||
(box :orientation "h" :space-evenly false :valign "center"
|
||||
(label :class "wg-name ${wg.up ? 'wg-up' : 'wg-down'}" :halign "start" :hexpand true :text {wg.name})
|
||||
(button
|
||||
:class "net-toggle-btn net-add-btn ${wg.vpn ? 'net-toggle-on' : 'net-toggle-off'}"
|
||||
:visible {wg.vpn_able}
|
||||
:onclick "scripts/net/wg-vpn-toggle ${wg.name}"
|
||||
:tooltip {wg.vpn ? "VPN: all traffic through ${wg.name}" : "VPN off: only the private subnet"}
|
||||
(label :text ""))
|
||||
(button
|
||||
:class "net-toggle-btn ${wg.up ? 'net-toggle-on' : 'net-toggle-off'}"
|
||||
:onclick "scripts/net/wg-toggle ${wg.name}"
|
||||
:tooltip {wg.up ? "Disconnect" : "Connect"}
|
||||
(label :text {wg.up ? "" : ""})))
|
||||
(box :orientation "v" :space-evenly false :visible {wg.up}
|
||||
(netinfo-row :label "Routing" :value {wg.vpn ? "All traffic" : "Private subnet"})
|
||||
(netinfo-row :label "IP" :value {wg.ip})
|
||||
(netinfo-row :label "Peer" :value {wg.endpoint != "" ? "${wg.peers} · ${wg.endpoint}" : wg.peers})
|
||||
(netinfo-row :label "Traffic" :value " ${wg.rx} ${wg.tx}"))
|
||||
|
||||
@@ -173,20 +173,46 @@
|
||||
|
||||
; --- Battery ---
|
||||
|
||||
(defwidget bat-stat [value label]
|
||||
(box :orientation "v" :halign "center" :valign "center" :space-evenly false
|
||||
(label :class "bat-stat-value" :text value)
|
||||
(label :class "bat-stat-label" :text label)))
|
||||
|
||||
; left: draw while on battery, charge rate while charging; right: time left / to full
|
||||
(defwidget bat-sys-win []
|
||||
(box :orientation "v" :space-evenly false :class "sys-section" :visible {battery.visible}
|
||||
(section-header :title "Battery" :accent "bat-accent")
|
||||
(overlay
|
||||
(circular-progress
|
||||
:width 60 :height 60
|
||||
:value {battery.percentage}
|
||||
:start-at 0
|
||||
:clockwise true
|
||||
:thickness 6
|
||||
:class "bat-ring bat-${battery.state}"
|
||||
:tooltip "Battery ${round(battery.percentage, 0)}%\n${battery.status} @ ${battery.wattage}")
|
||||
(label :class "bat-ring-label"
|
||||
:text "${round(battery.percentage, 0)}%"))
|
||||
(box :orientation "h" :space-evenly true :valign "center"
|
||||
(bat-stat
|
||||
:value {battery.state == "charging"
|
||||
? (battery.rate == "" ? "—" : "${battery.rate} %/h")
|
||||
: (battery.state == "discharging" || battery.state == "low" || battery.state == "critical"
|
||||
? battery.wattage : "—")}
|
||||
:label {battery.state == "charging" ? "charging" : "draw"})
|
||||
(overlay
|
||||
(circular-progress
|
||||
:width 72 :height 72
|
||||
:value {battery.percentage}
|
||||
:start-at 0
|
||||
:clockwise true
|
||||
:thickness 7
|
||||
:class "bat-ring bat-${battery.state}"
|
||||
:tooltip "Battery ${round(battery.percentage, 0)}%\n${battery.status} @ ${battery.wattage}")
|
||||
(label :class "bat-ring-label"
|
||||
:text "${round(battery.percentage, 0)}%"))
|
||||
(bat-stat
|
||||
:value {battery.time == "" ? "—" : battery.time}
|
||||
:label {battery.state == "charging" ? "to full" : "left"}))
|
||||
; 6 h history, one bar per 15 min bucket, coloured like the ring
|
||||
(box :orientation "h" :space-evenly true :class "bat-hist"
|
||||
(for b in {battery.history}
|
||||
(progress :orientation "v" :flipped true
|
||||
:value {b.p < 0 ? 0 : b.p}
|
||||
:class "bat-hist-bar hist-${b.s}"
|
||||
:tooltip {b.p < 0 ? b.t : "${b.t} ${round(b.p, 0)}%"})))
|
||||
(box :orientation "h" :space-evenly false
|
||||
(label :class "gpu-stat-label" :halign "start" :hexpand true :text "-6h")
|
||||
(label :class "gpu-stat-label" :halign "end" :text "now"))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ let
|
||||
"scripts/net/bt" = mkScript "bt" ./bar/scripts/net/bt [ jq glib ];
|
||||
"scripts/net/bt-toggle" = mkScript "bt-toggle" ./bar/scripts/net/bt-toggle [ bluez ];
|
||||
"scripts/net/wg-toggle" = mkScript "wg-toggle" ./bar/scripts/net/wg-toggle [ ];
|
||||
"scripts/net/wg-vpn-toggle" = mkScript "wg-vpn-toggle" ./bar/scripts/net/wg-vpn-toggle [ ];
|
||||
"scripts/net/wifi-connect" = mkScript "wifi-connect" ./bar/scripts/net/wifi-connect [ eww wpa_supplicant wofi libnotify ];
|
||||
"scripts/sys/gpu" = mkScript "gpu" ./bar/scripts/sys/gpu [ custom.amdgpu_top jq ];
|
||||
"scripts/sys/memory" = mkScript "memory" ./bar/scripts/sys/memory [ ];
|
||||
"scripts/sys/battery" = mkScript "battery" ./bar/scripts/sys/battery [ ];
|
||||
@@ -51,6 +53,12 @@ in {
|
||||
# graphical session, opens the bar once the daemon answers, and is
|
||||
# restarted on crash. `eww-bar` (packages/eww-bar) does the heavy
|
||||
# lifting; kanshi calls `eww-bar place` to move the bar between outputs.
|
||||
home.activation.ewwReload = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet eww.service; then
|
||||
run ${ewwBar} reload || true
|
||||
fi
|
||||
'';
|
||||
|
||||
systemd.user.services.eww = {
|
||||
Unit = {
|
||||
Description = "eww daemon and bar";
|
||||
|
||||
@@ -20,6 +20,8 @@ let
|
||||
in {
|
||||
|
||||
config = lib.mkIf (config.usercfg.wm == "Wayland") {
|
||||
systemd.user.services.kanshi.Unit.After = lib.mkForce [ config.services.kanshi.systemdTarget "awww.service" ];
|
||||
|
||||
services.kanshi = {
|
||||
enable = true;
|
||||
systemdTarget = "graphical-session.target";
|
||||
|
||||
@@ -24,23 +24,15 @@
|
||||
|
||||
|
||||
startupScript = pkgs.writeShellScriptBin "hyprland-start" ''
|
||||
# The eww bar is not started here: eww.service opens it with the
|
||||
# graphical session and kanshi places it per profile (see apps/eww).
|
||||
${pkgs.awww}/bin/awww-daemon &
|
||||
${pkgs.awww}/bin/awww restore &
|
||||
|
||||
sleep 2
|
||||
keepassxc &
|
||||
firefox &
|
||||
easyeffects --gapplication-service &
|
||||
|
||||
sleep 2
|
||||
nextcloud &
|
||||
# telegram-desktop &
|
||||
# discord &
|
||||
'';
|
||||
in {
|
||||
config = lib.mkIf (config.usercfg.wm == "Wayland") {
|
||||
services.awww.enable = true;
|
||||
|
||||
wayland.windowManager.hyprland = {
|
||||
enable = true;
|
||||
xwayland.enable = true;
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
bspc desktop -f 0
|
||||
|
||||
telegram-desktop &
|
||||
nextcloud &
|
||||
#flameshot &
|
||||
|
||||
sleep 2
|
||||
@@ -117,7 +116,6 @@
|
||||
|
||||
ibus-daemon -drx
|
||||
|
||||
easyeffects --gapplication-service &
|
||||
bspc desktop -f 0
|
||||
'';
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ in {
|
||||
# wireplumber.enable = true;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [ easyeffects alsa-utils ];
|
||||
programs.dconf.enable = true;
|
||||
environment.systemPackages = with pkgs; [ alsa-utils ];
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,37 @@
|
||||
(p.syscfg.net.wg.enable == true) &&
|
||||
(p.syscfg.net.wg.pubkey != config.syscfg.net.wg.pubkey);
|
||||
activePeers = builtins.filter isValidPeer config.syscfg.peers;
|
||||
|
||||
isServer = config.syscfg.server ? wireguard && config.syscfg.server.wireguard;
|
||||
vpnPeer = {
|
||||
name = "vpn-helcel";
|
||||
publicKey = "NFBJvYXZC+bd62jhrKnM7/pugidWhgR6+C5qIiUiq3Q=";
|
||||
allowedIPs = [ "10.10.1.0/24" "fd10:10:10::0/64" ];
|
||||
};
|
||||
|
||||
vpnTable = "51820";
|
||||
vpnDown = pkgs.writeShellScript "wg0-vpn-down" ''
|
||||
PATH=${lib.makeBinPath [ pkgs.iproute2 pkgs.wireguard-tools ]}
|
||||
for f in -4 -6; do
|
||||
ip $f rule del pref 31000 2>/dev/null
|
||||
ip $f rule del pref 31001 2>/dev/null
|
||||
ip $f route flush table ${vpnTable} 2>/dev/null
|
||||
done
|
||||
wg set wg0 fwmark off peer ${vpnPeer.publicKey} allowed-ips ${lib.concatStringsSep "," vpnPeer.allowedIPs} 2>/dev/null
|
||||
true
|
||||
'';
|
||||
vpnUp = pkgs.writeShellScript "wg0-vpn-up" ''
|
||||
set -e
|
||||
PATH=${lib.makeBinPath [ pkgs.iproute2 pkgs.wireguard-tools pkgs.procps ]}
|
||||
${vpnDown}
|
||||
sysctl -q net.ipv4.conf.all.src_valid_mark=1
|
||||
wg set wg0 fwmark ${vpnTable} peer ${vpnPeer.publicKey} allowed-ips 0.0.0.0/0,::/0
|
||||
for f in -4 -6; do
|
||||
ip $f route replace default dev wg0 table ${vpnTable}
|
||||
ip $f rule add table main suppress_prefixlength 0 pref 31000
|
||||
ip $f rule add not fwmark ${vpnTable} table ${vpnTable} pref 31001
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (config.syscfg.net.wg.enable) {
|
||||
@@ -18,20 +49,17 @@ in
|
||||
listenPort = 1515;
|
||||
mtu = 1340;
|
||||
peers =
|
||||
if (config.syscfg.server ? wireguard && config.syscfg.server.wireguard) then
|
||||
if isServer then
|
||||
map (p: {
|
||||
name = p.syscfg.hostname;
|
||||
publicKey = p.syscfg.net.wg.pubkey;
|
||||
allowedIPs = [ p.syscfg.net.wg.ip4 p.syscfg.net.wg.ip6 ];
|
||||
}) activePeers
|
||||
else
|
||||
[{
|
||||
allowedIPs = [ "10.10.1.0/24" "fd10:10:10::0/64" ];
|
||||
name = "vpn-helcel";
|
||||
[(vpnPeer // {
|
||||
endpoint = "vpn.helcel.net:1515";
|
||||
publicKey = "NFBJvYXZC+bd62jhrKnM7/pugidWhgR6+C5qIiUiq3Q=";
|
||||
persistentKeepalive = 30;
|
||||
}];
|
||||
})];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -61,5 +89,21 @@ in
|
||||
};
|
||||
startLimitIntervalSec = 0;
|
||||
};
|
||||
|
||||
systemd.services."wireguard-wg0-vpn" = lib.mkIf (!isServer) {
|
||||
description = "WireGuard wg0 - route all traffic";
|
||||
bindsTo = [ "wireguard-wg0.service" ];
|
||||
wants = [ "wireguard-wg0-peer-vpn-helcel.service" ];
|
||||
partOf = [ "wireguard-wg0-peer-vpn-helcel.service" ];
|
||||
after = [ "wireguard-wg0.service" "wireguard-wg0-peer-vpn-helcel.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = vpnUp;
|
||||
ExecStop = vpnDown;
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.checkReversePath = lib.mkIf (!isServer) "loose";
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.syscfg.server;
|
||||
wg = cfg.wireguard && config.syscfg.net.wg.enable;
|
||||
wgIp4 = builtins.head (lib.splitString "/" config.syscfg.net.wg.ip4);
|
||||
wgIp6 = builtins.head (lib.splitString "/" config.syscfg.net.wg.ip6);
|
||||
in {
|
||||
config = lib.mkIf (cfg.ipfw.enable) {
|
||||
boot.kernel.sysctl = {
|
||||
@@ -27,6 +30,11 @@ in {
|
||||
|
||||
iifname "${srcInt}" tcp dport ${srcPort} counter dnat ip6 to [${dstAddr6}]:${dstPort}
|
||||
iifname "${srcInt}" udp dport ${srcPort} counter dnat ip6 to [${dstAddr6}]:${dstPort}
|
||||
'' + lib.optionalString wg ''
|
||||
iifname "wg0" fib daddr type local ip daddr != ${wgIp4} tcp dport ${srcPort} counter dnat ip to ${dstAddr4}:${dstPort}
|
||||
iifname "wg0" fib daddr type local ip daddr != ${wgIp4} udp dport ${srcPort} counter dnat ip to ${dstAddr4}:${dstPort}
|
||||
iifname "wg0" fib daddr type local ip6 daddr != ${wgIp6} tcp dport ${srcPort} counter dnat ip6 to [${dstAddr6}]:${dstPort}
|
||||
iifname "wg0" fib daddr type local ip6 daddr != ${wgIp6} udp dport ${srcPort} counter dnat ip6 to [${dstAddr6}]:${dstPort}
|
||||
''
|
||||
) cfg.ipfw.ports}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
eww-bar = pkgs.callPackage ./eww-bar { };
|
||||
|
||||
jellyfin-mpv-shim-watchdog = pkgs.callPackage ./jellyfin-mpv-shim-watchdog { };
|
||||
network-watchdog = pkgs.callPackage ./network-watchdog { };
|
||||
}
|
||||
+49
-21
@@ -56,6 +56,9 @@ log() { printf 'eww-bar: %s\n' "$*" >&2; }
|
||||
ewwc() { timeout 5 eww --no-daemonize "$@" 9>&-; }
|
||||
|
||||
ping_daemon() { ewwc ping >/dev/null 2>&1; }
|
||||
# Empty when the daemon did not answer within the client's 100 ms deadline —
|
||||
# its main thread is busy (a popup takes seconds to build) — as well as when
|
||||
# nothing is open. Only a non-empty listing is authoritative.
|
||||
list_windows() { ewwc active-windows 2>/dev/null || true; }
|
||||
bar_open() { list_windows | grep -qx 'bar: bar'; }
|
||||
|
||||
@@ -69,6 +72,28 @@ wait_for_window() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# window_state NAME [SECONDS] → open | closed (listed without NAME) |
|
||||
# busy (nothing listed within SECONDS: main thread stuck, or nothing open)
|
||||
window_state() {
|
||||
local i n=$((${2:-5} * 5)) out
|
||||
for ((i = 0; i < n; i++)); do
|
||||
out=$(list_windows)
|
||||
if [[ -n $out ]]; then
|
||||
if grep -qx "$1: $1" <<<"$out"; then echo open; else echo closed; fi
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
echo busy
|
||||
}
|
||||
|
||||
# The compositor's view: does the daemon own any layer surface? Tells a
|
||||
# daemon that is merely not answering from one with nothing on screen.
|
||||
daemon_has_layer() {
|
||||
hyprctl -j layers 2>/dev/null \
|
||||
| jq -e --argjson pid "$(main_pid)" '[.. | objects | select(.pid? == $pid)] | length > 0' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- daemon ---
|
||||
|
||||
# nixpkgs wraps eww (wrapGAppsHook), so the running process is `.eww-wrapped`;
|
||||
@@ -126,13 +151,13 @@ wait_for_daemon() {
|
||||
|
||||
start_unit() {
|
||||
systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
systemctl --user start "$unit"
|
||||
systemctl --user start --no-block "$unit"
|
||||
}
|
||||
|
||||
restart_unit() {
|
||||
log "restarting $unit"
|
||||
systemctl --user reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
systemctl --user restart "$unit"
|
||||
systemctl --user restart --no-block "$unit"
|
||||
}
|
||||
|
||||
ensure() {
|
||||
@@ -194,7 +219,7 @@ close_popups() {
|
||||
|
||||
# open_bar [--force] [SPEC]
|
||||
open_bar() {
|
||||
local force=0 spec screen
|
||||
local force=0 spec screen state
|
||||
if [[ ${1:-} == --force ]]; then force=1; shift; fi
|
||||
spec=${1:-$(saved_screen)}
|
||||
spec=${spec:-$default_screen}
|
||||
@@ -210,17 +235,18 @@ open_bar() {
|
||||
# client deadline), so the result is verified instead.
|
||||
close_popups
|
||||
ewwc open bar --screen "$screen" >/dev/null 2>&1 || true
|
||||
if ! wait_for_window bar 5; then
|
||||
if [[ $screen != "$default_screen" ]]; then
|
||||
log "cannot open the bar on '$screen', using screen $default_screen"
|
||||
screen=$default_screen
|
||||
ewwc open bar --screen "$screen" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ! wait_for_window bar 5; then
|
||||
log "the bar does not come up, restarting $unit"
|
||||
restart_unit
|
||||
exit 1
|
||||
fi
|
||||
state=$(window_state bar 10)
|
||||
# Only an answered "no bar" means the screen was refused; busy just needs time.
|
||||
if [[ $state == closed && $screen != "$default_screen" ]]; then
|
||||
log "cannot open the bar on '$screen', using screen $default_screen"
|
||||
screen=$default_screen
|
||||
ewwc open bar --screen "$screen" >/dev/null 2>&1 || true
|
||||
state=$(window_state bar 10)
|
||||
fi
|
||||
if [[ $state != open ]]; then
|
||||
log "the bar does not come up ($state), restarting $unit"
|
||||
restart_unit
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$spec" >"$screen_file"
|
||||
printf '%s\n' "$screen" >"$current_file"
|
||||
@@ -301,14 +327,16 @@ check() {
|
||||
strike "daemon does not answer"
|
||||
return 0
|
||||
fi
|
||||
if wait_for_window bar 3; then
|
||||
rm -f "$fail_file"
|
||||
return 0
|
||||
fi
|
||||
# Either nothing is open or the main loop is frozen (empty reply). Try to
|
||||
# open the bar: a frozen daemon still won't list it afterwards.
|
||||
case $(window_state bar 10) in
|
||||
open) rm -f "$fail_file"; return 0 ;;
|
||||
# Nothing listed for 10 s while something is on screen: the main loop is
|
||||
# frozen or still building a popup. Leave the windows alone; a second
|
||||
# miss restarts the unit.
|
||||
busy) if daemon_has_layer; then strike "UI unresponsive"; return 0; fi ;;
|
||||
esac
|
||||
# Listed without the bar, or nothing on screen at all: it really is gone.
|
||||
open_bar || true
|
||||
if wait_for_window bar 3; then
|
||||
if wait_for_window bar 5; then
|
||||
rm -f "$fail_file"
|
||||
log "bar was missing, reopened"
|
||||
else
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{ writeShellScriptBin
|
||||
, coreutils
|
||||
, findutils
|
||||
, gnugrep
|
||||
, iproute2
|
||||
, systemd
|
||||
}:
|
||||
|
||||
# jellyfin-mpv-shim keeps a persistent socket to the Jellyfin server. When the
|
||||
# machine drops off the network that socket dies, but the process stays alive
|
||||
# and the unit stays "active" — running yet deaf to cast requests, the "up but
|
||||
# not working, restart it by hand" case. systemd can't see this: nothing
|
||||
# crashes and the shim speaks no health protocol, so we probe the real thing —
|
||||
# is its MainPID still holding a live (established, non-loopback) connection?
|
||||
#
|
||||
# check (default) one tick, run by a 60s timer: restart if online but
|
||||
# not connected, rate-limited to one restart per 5 min
|
||||
# wait-online block until a real address exists; used as ExecStartPre so the
|
||||
# unit only starts once online
|
||||
|
||||
writeShellScriptBin "jellyfin-mpv-shim-watchdog" ''
|
||||
set -u
|
||||
service="jellyfin-mpv-shim.service"
|
||||
stamp="''${XDG_RUNTIME_DIR:-/run/user/$(${coreutils}/bin/id -u)}/jellyfin-mpv-shim-watchdog.stamp"
|
||||
|
||||
# A global-scope address on a non-loopback interface — true for a LAN IP too,
|
||||
# so this needs no server URL and works whether Jellyfin is local or remote.
|
||||
network_up() { ${iproute2}/bin/ip -o addr show scope global up 2>/dev/null | ${gnugrep}/bin/grep -q .; }
|
||||
|
||||
if [ "''${1:-check}" = wait-online ]; then
|
||||
while ! network_up; do ${coreutils}/bin/sleep 5; done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Healthy: MainPID owns an established socket to something other than loopback.
|
||||
pid="$(${systemd}/bin/systemctl --user show -p MainPID --value "$service" 2>/dev/null)"
|
||||
if [ "''${pid:-0}" != 0 ] && ${iproute2}/bin/ss -tnpH state established 2>/dev/null \
|
||||
| ${gnugrep}/bin/grep "pid=$pid," \
|
||||
| ${gnugrep}/bin/grep -qv -e '127\.0\.0\.1' -e '\[::1\]'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Not connected: only worth restarting once the network is actually back...
|
||||
network_up || exit 0
|
||||
# ...and not if we already restarted within the last 5 minutes.
|
||||
[ -e "$stamp" ] && [ -n "$(${findutils}/bin/find "$stamp" -mmin -5 2>/dev/null)" ] && exit 0
|
||||
|
||||
: > "$stamp"
|
||||
echo "jellyfin-mpv-shim-watchdog: online but no live connection -> restarting $service"
|
||||
${systemd}/bin/systemctl --user restart "$service"
|
||||
''
|
||||
@@ -0,0 +1,69 @@
|
||||
{ writeShellScriptBin
|
||||
, coreutils
|
||||
, findutils
|
||||
, gnugrep
|
||||
, iproute2
|
||||
, systemd
|
||||
}:
|
||||
|
||||
# For user services that only make sense while online (jellyfin-mpv-shim,
|
||||
# nextcloud-client, ...). They keep a persistent socket to their server; when
|
||||
# the machine drops off the network that socket dies, but the process stays
|
||||
# alive and the unit stays "active" — running yet deaf, the "up but not
|
||||
# working, restart it by hand" case. systemd can't see this: nothing crashes
|
||||
# and the programs speak no health protocol, so we probe the real thing — is
|
||||
# the MainPID still holding a live (established, non-loopback) connection?
|
||||
#
|
||||
# wait-online UNIT block until a default route exists; used as ExecStartPre
|
||||
# so UNIT only starts once online. Also stamps the start
|
||||
# time so `check` leaves a freshly (re)started UNIT alone.
|
||||
# check UNIT... one tick, run by a 60s timer: restart every UNIT that is
|
||||
# active but not connected, at most once per 2 minutes.
|
||||
#
|
||||
# Wired up by the services.network-watchdog home-manager module.
|
||||
|
||||
writeShellScriptBin "network-watchdog" ''
|
||||
set -u
|
||||
stampdir="''${XDG_RUNTIME_DIR:-/run/user/$(${coreutils}/bin/id -u)}/network-watchdog"
|
||||
${coreutils}/bin/mkdir -p "$stampdir"
|
||||
|
||||
usage() { echo "usage: network-watchdog wait-online UNIT | check UNIT..." >&2; exit 64; }
|
||||
|
||||
# Online = a default route. An address alone is not enough: wg0 and virtual
|
||||
# bridges carry global-scope addresses whether or not there is a way out.
|
||||
network_up() { { ${iproute2}/bin/ip -4 route show default; ${iproute2}/bin/ip -6 route show default; } 2>/dev/null | ${gnugrep}/bin/grep -q .; }
|
||||
|
||||
# Healthy: MainPID owns an established socket to something other than loopback.
|
||||
connected() {
|
||||
pid="$(${systemd}/bin/systemctl --user show -p MainPID --value "$1" 2>/dev/null)"
|
||||
[ "''${pid:-0}" != 0 ] && ${iproute2}/bin/ss -tnpH state established 2>/dev/null \
|
||||
| ${gnugrep}/bin/grep "pid=$pid," \
|
||||
| ${gnugrep}/bin/grep -qv -e '127\.0\.0\.1' -e '\[::1\]'
|
||||
}
|
||||
|
||||
# Stamped within the last 2 minutes: still connecting, or already restarted.
|
||||
recently_started() { [ -n "$(${findutils}/bin/find "$stampdir/$1" -mmin -2 2>/dev/null)" ]; }
|
||||
|
||||
[ $# -ge 1 ] || usage
|
||||
cmd="$1"; shift
|
||||
case "$cmd" in
|
||||
wait-online)
|
||||
[ $# -eq 1 ] || usage
|
||||
while ! network_up; do ${coreutils}/bin/sleep 5; done
|
||||
: > "$stampdir/$1"
|
||||
;;
|
||||
check)
|
||||
# Nothing can be connected while offline, and restarting wouldn't help.
|
||||
network_up || exit 0
|
||||
for unit; do
|
||||
# Not "active" (inactive, failed, or still in wait-online): systemd's job, not ours.
|
||||
${systemd}/bin/systemctl --user is-active --quiet "$unit" || continue
|
||||
recently_started "$unit" && continue
|
||||
connected "$unit" && continue
|
||||
echo "network-watchdog: $unit is online but has no live connection -> restarting"
|
||||
${systemd}/bin/systemctl --user restart "$unit" # its wait-online refreshes the stamp
|
||||
done
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
''
|
||||
@@ -5,7 +5,7 @@
|
||||
[ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.kernelModules = [ "v4l2loopback" "kvm-amd" ];
|
||||
#boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
boot.extraModulePackages = with config.boot.kernelPackages;
|
||||
[ v4l2loopback ];
|
||||
boot.extraModprobeConfig = ''
|
||||
|
||||
Reference in new issue
Block a user