Compare commits

...

8 Commits

11 changed files with 154 additions and 49 deletions

View File

@ -30,11 +30,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784476090, "lastModified": 1784907849,
"narHash": "sha256-flVUft590tsDX9z97O4DzVFg6zvOOmGeYhPeDdaP1DI=", "narHash": "sha256-wtypRfcfVvofMON6xemTObJCpPWLRyBoqLrvzgnLYlI=",
"owner": "aylur", "owner": "aylur",
"repo": "astal", "repo": "astal",
"rev": "fd94e333c8bd45557291c805f1df79d5f787d590", "rev": "9dac92f20e6c89b9373bbb238c49b1cb115724db",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -111,11 +111,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784546264, "lastModified": 1785958398,
"narHash": "sha256-jxVr5EHMYAOkFvS2k0abIvt6NqyshyWmiHHzV17sT2g=", "narHash": "sha256-5r/JgsoNMTx+qIh83WkxpujEKBR7PF5ssnh5vYCuSAw=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "e3dea8e4792b09a6c0eb5836b0284ad05b1acba0", "rev": "a7c70cc290290f373f50cd820403833d250459ac",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -163,11 +163,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1784497964, "lastModified": 1785967620,
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", "narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", "rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -42,6 +42,7 @@ in {
smile smile
podman-compose podman-compose
vesktop vesktop
sox
]; ];
programs.alacritty.settings.font.size = 13; programs.alacritty.settings.font.size = 13;

View File

@ -183,6 +183,32 @@ function Right() {
); );
} }
// Icon names to try for the current battery state, most specific first.
// Papirus does not ship every combination, so each entry needs a fallback:
// "-charged-symbolic" only exists at level 100, and "-charging-symbolic" only
// up to level 90 (above that the panel icon has to be named directly).
function batteryIconNames(): string[] {
const percentage = battery.percentage;
const thresholds = [...Array(11).keys()].map((i) => i * 10);
const level = thresholds.find((threshold) => threshold >= percentage * 100);
const plain = `battery-level-${level}-symbolic`;
// Being full is a state, not a 100% reading: with a charge limit set the
// battery stops well below 100 and sits there reporting FULLY_CHARGED.
if (battery.state === Battery.State.FULLY_CHARGED) {
return [`battery-level-${level}-charged-symbolic`, plain];
}
if (battery.charging) {
const padded = String(level).padStart(3, "0");
return [
`battery-level-${level}-charging-symbolic`,
`battery-${padded}-charging`,
plain,
];
}
return [plain];
}
// Papirus encodes the battery level as a 35%-opacity overlay inside its // Papirus encodes the battery level as a 35%-opacity overlay inside its
// *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every // *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every
// level renders as a solid, full battery. The Papirus *-symbolic icons are just // level renders as a solid, full battery. The Papirus *-symbolic icons are just
@ -190,13 +216,12 @@ function Right() {
// normally — so we resolve each name to that underlying file and load it // normally — so we resolve each name to that underlying file and load it
// directly (as a non-symbolic file icon), which renders the correct level. // directly (as a non-symbolic file icon), which renders the correct level.
function batteryGicon(): Gio.Icon { function batteryGicon(): Gio.Icon {
const percentage = battery.percentage; const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!);
const thresholds = [...Array(11).keys()].map((i) => i * 10); // lookup_icon() never fails: for an unknown name it hands back the
const icon = thresholds.find((threshold) => threshold >= percentage * 100); // "image-missing" paintable, so check availability before resolving.
const name = battery.charging const names = batteryIconNames();
? `battery-level-${icon}-${percentage >= 0.99 ? "charged" : "charging"}-symbolic` const name = names.find((n) => theme.has_icon(n)) ?? names[names.length - 1];
: `battery-level-${icon}-symbolic`; const symbolic = theme
const symbolic = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!)
.lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0) .lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0)
.get_file(); .get_file();
if (!symbolic) return Gio.ThemedIcon.new(name); if (!symbolic) return Gio.ThemedIcon.new(name);
@ -217,15 +242,21 @@ function batteryGicon(): Gio.Icon {
return new Gio.FileIcon({ file: symbolic }); return new Gio.FileIcon({ file: symbolic });
} }
function batteryTooltip(): string {
return `${Math.round(battery.percentage * 100)}%`;
}
function BatteryIcon(): JSX.Element { function BatteryIcon(): JSX.Element {
if (battery.get_state() == 0) return <box />; if (battery.get_state() == 0) return <box />;
// AstalBattery does not emit notify::percentage/charging/state on this // AstalBattery does not emit notify::percentage/charging/state on this
// system, so a signal binding stays frozen at its initial value (e.g. // system, so a signal binding stays frozen at its initial value (e.g.
// showing "charged" forever). Poll the live values instead. // showing "charged" forever). Poll the live values instead.
const gicon = createPoll<Gio.Icon>(batteryGicon(), 10000, batteryGicon); const gicon = createPoll<Gio.Icon>(batteryGicon(), 10000, batteryGicon);
const tooltip = createPoll<string>(batteryTooltip(), 10000, batteryTooltip);
return ( return (
<button <button
class="battery-item" class="battery-item"
tooltipText={tooltip}
onClicked={() => execAsync(["gnome-power-statistics"])} onClicked={() => execAsync(["gnome-power-statistics"])}
> >
<box> <box>

View File

@ -4,6 +4,16 @@ import style from "./style.scss";
import Bar from "./Bar"; import Bar from "./Bar";
import NotificationPopups from "./notifications/NotificationPopups"; import NotificationPopups from "./notifications/NotificationPopups";
const PREFERRED_MONITOR = "eDP-1";
function pick_monitor(): Gdk.Monitor | undefined {
const monitors = app.get_monitors();
return (
monitors.find((m: Gdk.Monitor) => m.get_connector() === PREFERRED_MONITOR) ??
monitors[0]
);
}
function register_windows(monitor: Gdk.Monitor) { function register_windows(monitor: Gdk.Monitor) {
let scale = (monitor.get_geometry().width >= 3000) ? 1.2 : 1; let scale = (monitor.get_geometry().width >= 3000) ? 1.2 : 1;
Bar(monitor, scale); Bar(monitor, scale);
@ -14,6 +24,11 @@ app.start({
css: style, css: style,
iconTheme: "Papirus", iconTheme: "Papirus",
main() { main() {
register_windows(app.get_monitors()[0]); const monitor = pick_monitor();
if (!monitor) {
console.error("No monitors available, not creating the bar.");
return;
}
register_windows(monitor);
}, },
}); });

View File

@ -18,9 +18,15 @@ const app_icons = new Apps.Apps().list.reduce(
export function getIconName(app_id: string | null | undefined, title: string | null | undefined) { export function getIconName(app_id: string | null | undefined, title: string | null | undefined) {
if (!app_id && !title) return ""; if (!app_id && !title) return "";
// Our foot terminals carry a unique per-window app-id ("footclient-<pid>") so the
// "duplicate window" keybind can tell them apart; strip that suffix back to the
// base app-id ("footclient") so the icon still resolves.
const norm = app_id ? app_id.replace(/-\d+$/, "") : app_id;
// try fields matching Niri outputs // try fields matching Niri outputs
const possibleKeys = [ const possibleKeys = [
app_id, app_id,
norm,
title title
].filter(Boolean) as string[]; ].filter(Boolean) as string[];

View File

@ -62,10 +62,10 @@
autoStageResolvedConflicts = false; autoStageResolvedConflicts = false;
}; };
git.overrideGpg = true; git.overrideGpg = true;
git.pagers = [ git.diffRenderers = [
{ {
colorArg = "always"; colorArg = "always";
pager = "delta --dark --color-only --paging=never"; command = "delta --dark --color-only --paging=never";
} }
]; ];
}; };

View File

@ -3,12 +3,21 @@
scale ? null, scale ? null,
... ...
}: let }: let
terminal = "${pkgs.foot}/bin/footclient";
nautilus = "${pkgs.nautilus}/bin/nautilus"; nautilus = "${pkgs.nautilus}/bin/nautilus";
playerctl = "${pkgs.playerctl}/bin/playerctl"; playerctl = "${pkgs.playerctl}/bin/playerctl";
wpctl = "${pkgs.wireplumber}/bin/wpctl"; wpctl = "${pkgs.wireplumber}/bin/wpctl";
brightnessctl = "${pkgs.brightnessctl}/bin/brightnessctl"; brightnessctl = "${pkgs.brightnessctl}/bin/brightnessctl";
terminal-name = "foot"; # Spawn a foot terminal that the "duplicate window" keybind can later trace back to
# its shell. foot's --server mode makes every window report the *server's* PID to the
# compositor, so niri can't distinguish windows by PID. Instead we give each window a
# unique app-id ("footclient-<pid>") and export the same value as FOOT_APPID into its
# shell; -E hands footclient's environment (incl. FOOT_APPID) to the server-spawned
# shell. launch_terminal then finds the focused window's shell by matching FOOT_APPID.
spawn_terminal = pkgs.writeShellScriptBin "spawn_terminal" ''
ID="footclient-$$"
export FOOT_APPID="$ID"
exec ${pkgs.foot}/bin/footclient -E --app-id="$ID" "$@"
'';
launch_terminal = pkgs.writeShellScriptBin "launch_terminal" '' launch_terminal = pkgs.writeShellScriptBin "launch_terminal" ''
function find_leaf_pid(){ function find_leaf_pid(){
local PID local PID
@ -29,28 +38,46 @@
fi fi
} }
LEAF_PID=$(find_leaf_pid "$1") # foot runs in --server mode, so niri reports the shared server PID for every foot
# window and cannot tell them apart. Resolve the focused window to its own shell by
# matching the FOOT_APPID marker that spawn_terminal exports into each window: scan
# the server's direct children for the one whose environment carries our app-id.
FOCUSED=$(niri msg --json focused-window)
SERVER_PID=$(echo "$FOCUSED" | ${pkgs.jq}/bin/jq -r '.pid // empty')
APP_ID=$(echo "$FOCUSED" | ${pkgs.jq}/bin/jq -r '.app_id // empty')
ROOT_PID=""
if [ -n "$APP_ID" ] && [ -n "$SERVER_PID" ]; then
for CHILD in $(pgrep -P "$SERVER_PID"); do
if grep -qzF "FOOT_APPID=$APP_ID" "/proc/$CHILD/environ" 2>/dev/null; then
ROOT_PID="$CHILD"
break
fi
done
fi
# Focused window isn't one of our tagged foot terminals: just open a plain terminal.
if [ -z "$ROOT_PID" ]; then
exec ${spawn_terminal}/bin/spawn_terminal
fi
LEAF_PID=$(find_leaf_pid "$ROOT_PID")
if [ "$(ps -p "$LEAF_PID" -o comm=)" == "ssh" ]; then if [ "$(ps -p "$LEAF_PID" -o comm=)" == "ssh" ]; then
SSH_COMMAND=$(ps -p "$LEAF_PID" -o args --no-headers) SSH_COMMAND=$(ps -p "$LEAF_PID" -o args --no-headers)
if [[ "$SSH_COMMAND" == *"waypipe"* ]]; then if [[ "$SSH_COMMAND" == *"waypipe"* ]]; then
CLIENT=$(echo "$SSH_COMMAND" | awk '{ print $5 }') CLIENT=$(echo "$SSH_COMMAND" | awk '{ print $5 }')
PREVIOUS_SESSION_ID=$(grep -z "SSH_SESSION_ID" "/proc/$LEAF_PID/environ" | xargs -0 -n 1 | grep -oP "[0-9]*") PREVIOUS_SESSION_ID=$(grep -z "SSH_SESSION_ID" "/proc/$LEAF_PID/environ" | xargs -0 -n 1 | grep -oP "[0-9]*")
SSH_SESSION_ID=$RANDOM SSH_SESSION_ID=$RANDOM
${terminal} -e zsh -c "SSH_SESSION_ID=$SSH_SESSION_ID waypipe ssh -t \"$CLIENT\" env SSH_SESSION_ID=\"$SSH_SESSION_ID\" PREVIOUS_SESSION_ID=\"$PREVIOUS_SESSION_ID\" \"zsh --login\"" ${spawn_terminal}/bin/spawn_terminal zsh -c "SSH_SESSION_ID=$SSH_SESSION_ID waypipe ssh -t \"$CLIENT\" env SSH_SESSION_ID=\"$SSH_SESSION_ID\" PREVIOUS_SESSION_ID=\"$PREVIOUS_SESSION_ID\" \"zsh --login\""
else else
${terminal} -e zsh -c "ssh -t \"$(echo "$SSH_COMMAND" | awk '{ print $2 }')\"" ${spawn_terminal}/bin/spawn_terminal zsh -c "ssh -t \"$(echo "$SSH_COMMAND" | awk '{ print $2 }')\""
fi fi
else # Not an ssh session: open a local terminal in the same directory.
else # Not an ssh session CWD=$(readlink -e /proc/"$LEAF_PID"/cwd)
if [ "$(ps -p "$1" -o comm=)" == "${terminal-name}" ]; then if [ "$CWD" != "" ]; then
CWD=$(readlink -e /proc/"$LEAF_PID"/cwd) ${spawn_terminal}/bin/spawn_terminal --working-directory "$CWD"
if [ "$CWD" != "" ]; then
${terminal} --working-directory "$CWD"
else
${terminal}
fi
else else
${terminal} ${spawn_terminal}/bin/spawn_terminal
fi fi
fi fi
''; '';
@ -156,8 +183,8 @@ in {
binds { binds {
// General Binds // General Binds
Mod+Return { spawn "sh" "-c" "${launch_terminal}/bin/launch_terminal $(niri msg --json focused-window | ${pkgs.jq}/bin/jq .pid)"; } Mod+Return { spawn "${launch_terminal}/bin/launch_terminal"; }
Mod+Shift+Return { spawn "${terminal}"; } Mod+Shift+Return { spawn "${spawn_terminal}/bin/spawn_terminal"; }
Mod+Q { close-window; } Mod+Q { close-window; }
Mod+A { spawn "${nautilus}"; } Mod+A { spawn "${nautilus}"; }
Mod+B { spawn "Helium"; } Mod+B { spawn "Helium"; }
@ -233,9 +260,10 @@ in {
XF86AudioMute { spawn "${wpctl}" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } XF86AudioMute { spawn "${wpctl}" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86AudioMicMute { spawn "${wpctl}" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } XF86AudioMicMute { spawn "${wpctl}" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
// Horizontal mouse scroll controls volume // Ctrl + horizontal mouse scroll controls volume
WheelScrollRight cooldown-ms=10 { spawn "${wpctl}" "set-volume" "@DEFAULT_AUDIO_SINK@" "1%+" "--limit" "1.0"; } // (unmodified WheelScroll binds capture the whole wheel in niri and break normal scrolling)
WheelScrollLeft cooldown-ms=10 { spawn "${wpctl}" "set-volume" "@DEFAULT_AUDIO_SINK@" "1%-"; } Ctrl+Alt+WheelScrollRight cooldown-ms=10 { spawn "${wpctl}" "set-volume" "@DEFAULT_AUDIO_SINK@" "1%+" "--limit" "1.0"; }
Ctrl+Alt+WheelScrollLeft cooldown-ms=10 { spawn "${wpctl}" "set-volume" "@DEFAULT_AUDIO_SINK@" "1%-"; }
// Mod + horizontal scroll moves between windows (columns) left/right // Mod + horizontal scroll moves between windows (columns) left/right
Mod+WheelScrollRight cooldown-ms=200 { focus-column-right; } Mod+WheelScrollRight cooldown-ms=200 { focus-column-right; }

View File

@ -17,6 +17,26 @@
name = "ayu-dark"; name = "ayu-dark";
}; };
}; };
providers = {
clipboard = {
entrypoints = {
history = {
preferences = {
defaultAction = "copy";
};
};
};
};
core = {
entrypoints = {
"search-emojis" = {
preferences = {
defaultAction = "copy";
};
};
};
};
};
launcher_window = { launcher_window = {
opacity = 0.95; opacity = 0.95;
client_side_decorations = { client_side_decorations = {

View File

@ -24,7 +24,17 @@ in {
services.logind.settings.Login = { services.logind.settings.Login = {
HandlePowerKey = "suspend"; HandlePowerKey = "suspend";
}; };
services.throttled.enable = true; # 'throttled' raises PL1/PL2 to 44W, but this machine is a Core Ultra 7 155U
# with a 15W spec sustained power. Holding it at 44W drives the chassis sensor
# (acpitz) past its 109C critical trip, which makes the kernel do a hardware
# protection shutdown. Leave the firmware's own power limits alone.
services.throttled.enable = false;
# thermald refuses to run on ThinkPads exposing thinkpad_acpi/dytc_lapmode,
# because the platform's DPTF firmware owns thermal management there. It exits
# immediately at boot, so enabling it only produces noise in the journal.
services.thermald.enable = lib.mkForce false;
services.thinkfan.enable = true; services.thinkfan.enable = true;
services.thinkfan.settings = { services.thinkfan.settings = {
sensors = [ sensors = [
@ -60,7 +70,8 @@ in {
[2 50 60] [2 50 60]
[3 56 68] [3 56 68]
[5 64 78] [5 64 78]
[7 76 95] [7 76 90]
["level disengaged" 85 32767]
]; ];
}; };

View File

@ -53,9 +53,6 @@
hardware.trackpoint.enable = lib.mkDefault true; hardware.trackpoint.enable = lib.mkDefault true;
hardware.trackpoint.emulateWheel = lib.mkDefault config.hardware.trackpoint.enable; hardware.trackpoint.emulateWheel = lib.mkDefault config.hardware.trackpoint.enable;
hardware.trackpoint.device = "TPPS/2 Synaptics TrackPoint"; hardware.trackpoint.device = "TPPS/2 Synaptics TrackPoint";
nixpkgs.config.packageOverrides = pkgs: {
zfs = pkgs.zfs_unstable;
};
boot.kernelPackages = pkgs.linuxPackages; boot.kernelPackages = pkgs.linuxPackages;
boot.zfs.forceImportRoot = false; boot.zfs.forceImportRoot = false;
hardware.firmware = with pkgs; [ hardware.firmware = with pkgs; [

View File

@ -1,8 +1,4 @@
{ {pkgs, ...}: {
inputs,
pkgs,
...
}: {
services.udev.packages = [pkgs.yubikey-personalization]; services.udev.packages = [pkgs.yubikey-personalization];
programs.appimage.enable = true; programs.appimage.enable = true;
programs.appimage.binfmt = true; programs.appimage.binfmt = true;