This commit is contained in:
Thomas Avé 2026-03-26 16:34:56 +07:00
parent 5b86d1cd9b
commit b6e789c102
11 changed files with 261 additions and 197 deletions

View File

@ -30,11 +30,11 @@
]
},
"locked": {
"lastModified": 1769774308,
"narHash": "sha256-8Ve6VdUpcYbl8bS5oyDwVnnNobyPxdPmUHGgSDyOazQ=",
"lastModified": 1773507676,
"narHash": "sha256-UabDWdQTsGopWsC+RW0GDBRvAXF5KbkHwEcW4UQpd/A=",
"owner": "aylur",
"repo": "astal",
"rev": "eb235f8813bdea2a4a38ac228f2efc4e2a8a90af",
"rev": "d3fa2117d581b50e63eb3aefbf92d4883fa23f1b",
"type": "github"
},
"original": {
@ -132,11 +132,11 @@
]
},
"locked": {
"lastModified": 1773093840,
"narHash": "sha256-u/96NoAyN8BSRuM3ZimGf7vyYgXa3pLx4MYWjokuoH4=",
"lastModified": 1773810247,
"narHash": "sha256-6Vz1Thy/1s7z+Rq5OfkWOBAdV4eD+OrvDs10yH6xJzQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bb014746edb2a98d975abde4dd40fa240de4cf86",
"rev": "d47357a4c806d18a3e853ad2699eaec3c01622e7",
"type": "github"
},
"original": {
@ -184,11 +184,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"lastModified": 1773734432,
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"type": "github"
},
"original": {
@ -200,11 +200,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"lastModified": 1773734432,
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"type": "github"
},
"original": {
@ -252,11 +252,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1773125003,
"narHash": "sha256-EF89LT3zxm8dM2pgoAohUcdQoTVVv1v02Jq+z6j8u+0=",
"lastModified": 1773823796,
"narHash": "sha256-AYFcL50MSz2kdfEl1NgL5r5ukJ06MUos7l3ak2Rtn/M=",
"owner": "nix-community",
"repo": "NUR",
"rev": "eb59c1e77102b9f836cdea9f12568f191a9aff57",
"rev": "098c5b211d0e445773f57d8ecefb497af07e8767",
"type": "github"
},
"original": {
@ -299,11 +299,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1773099334,
"narHash": "sha256-ciiNQFZ2m2JOEDDg9ZZxOuxykl4Aw/2C1EQsOcq+F+E=",
"lastModified": 1773692852,
"narHash": "sha256-rVzZeR2gsPCgft+SHbxlxldejpOBI6bwj6gC24XpEIk=",
"owner": "vicinaehq",
"repo": "vicinae",
"rev": "2a427670116461a4ce0207db2c089aee9f40428e",
"rev": "f3dc9cb4696dda1218f00f2756cf25882fa5df9b",
"type": "github"
},
"original": {

View File

@ -47,11 +47,6 @@ in {
"DP-3,1920x1080,3840x-540,1"
"DP-1,1920x1080,3840x-540,1"
];
bind = [
", XF86PowerOff, exec, ${pkgs.rofi}/bin/rofi -show power-menu -modi power-menu:${
./hyprland/files/rofi-power-menu.sh
}"
];
general.gaps_out = 1;
};

View File

@ -2,9 +2,8 @@ import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
import { createBinding, createState, For, With, Accessor } from "ags";
import { createPoll } from "ags/time";
import { subprocess, execAsync } from "ags/process";
import Tray from "gi://AstalTray";
import { execAsync } from "ags/process";
import Hyprland from "gi://AstalHyprland";
import { getIconName } from "./utils";
import Wp from "gi://AstalWp";
import Battery from "gi://AstalBattery";
@ -16,6 +15,36 @@ const sensorsAvailable = await execAsync(["sensors"])
.catch(() => false);
const wirePlumber = Wp.get_default();
// --- NIRI IPC INTEGRATION ---
const [workspaces, setWorkspaces] = createState<any[]>([]);
const [windows, setWindows] = createState<any[]>([]);
async function updateNiriState() {
try {
const wsOut = await execAsync(["niri", "msg", "-j", "workspaces"]);
setWorkspaces(JSON.parse(wsOut) || []);
const winOut = await execAsync(["niri", "msg", "-j", "windows"]);
setWindows(JSON.parse(winOut) || []);
} catch (e) {
console.error("Failed to fetch Niri state:", e);
}
}
// Initialize state immediately
updateNiriState();
// Subscribe to Niri's native event stream
subprocess(
["niri", "msg", "-j", "event-stream"],
(line) => {
// A state change happened in Niri. Re-fetching guarantees perfect sync
updateNiriState();
},
(err) => console.error("Niri event stream error:", err)
);
// ----------------------------
function SysTray(): JSX.Element {
const tray = Tray.get_default();
let items = createBinding(tray, "items");
@ -52,18 +81,18 @@ function SysTray(): JSX.Element {
);
}
function Left(): JSX.Element {
function Left({ connector }: { connector: string }): JSX.Element {
return (
<box hexpand halign={Gtk.Align.START}>
<Clients />
<Clients connector={connector} />
</box>
);
}
function Center(): JSX.Element {
function Center({ connector }: { connector: string }): JSX.Element {
return (
<box>
<Workspaces />
<Workspaces connector={connector} />
</box>
);
}
@ -244,92 +273,49 @@ function Volume(): JSX.Element {
);
}
function Workspaces(): JSX.Element {
const hyprland = Hyprland.get_default();
let workspaces = createBinding(hyprland, "workspaces");
function Workspaces({ connector }: { connector: string }): JSX.Element {
return (
<box class="workspaces">
<With value={workspaces}>
{(wss: Array<Hyprland.Workspace>) => (
<box>
<With value={createBinding(hyprland, "focusedMonitor")}>
{(fm: Hyprland.Monitor) => {
let filtered_wss = new Accessor(() =>
wss
.sort((a, b) => a.id - b.id)
.filter(
(ws) =>
ws &&
ws.get_monitor() &&
ws.get_monitor().get_id() === fm.get_id(),
),
);
return (
<box>
<For each={filtered_wss}>
{(ws: Hyprland.Workspace, _index) => (
{/* Generate a derived binding by passing a transformation function into the `workspaces` state */}
<For each={workspaces((wss: any[]) =>
wss.filter((ws) => ws.output === connector).sort((a, b) => a.idx - b.idx)
)}>
{(ws: any) => (
<button
class={createBinding(hyprland, "focusedWorkspace").as(
(fw) => (ws === fw ? "focused" : ""),
)}
onClicked={() => ws.focus()}
class={ws.is_active ? "focused" : ""}
onClicked={() => execAsync(["niri", "msg", "action", "focus-workspace", ws.idx.toString()])}
>
{`${ws.id}`.slice(-1)}
{ws.name || ws.idx.toString()}
</button>
)}
</For>
</box>
);
}}
</With>
</box>
)}
</With>
</box>
);
}
function shorten(title: string) {
return title.length > 40 ? title.slice(0, 20) + "..." : title;
}
function Clients(): JSX.Element {
const hyprland = Hyprland.get_default();
let clients = createBinding(hyprland, "clients");
function Clients({ connector }: { connector: string }): JSX.Element {
return (
<box>
<With value={createBinding(hyprland, "focusedWorkspace")}>
{(fw: Hyprland.Workspace) => (
<box class="clients">
<With value={clients}>
{(cls: Array<Hyprland.Client>) => {
let filtered_clients = new Accessor(() =>
cls
.sort((a, b) => a.pid - b.pid)
.filter((cl) => !cl.title.includes("rofi"))
.filter(
(cl) => fw && cl.get_workspace().get_id() === fw.get_id(),
),
);
{/* Use With to react to workspace changes, so we know which WS is active */}
<With value={workspaces}>
{(wss: any[]) => {
let active_ws_for_monitor = wss.find((ws: any) => ws.is_active && ws.output === connector)?.id;
return (
<box>
<For each={filtered_clients}>
{(cl: Hyprland.Client, _index) => (
<box
class={createBinding(hyprland, "focusedClient").as(
(a) =>
a && a.address === cl.address
? "focused"
: "unfocused",
)}
>
<image iconName={getIconName(cl)} class="app-icon" />
<label
label={createBinding(cl, "title").as((title) =>
shorten(title),
)}
/>
{/* Generate a derived binding for windows, depending on the active workspace */}
<For each={windows((wins: any[]) =>
wins.filter((w: any) => !w.title?.includes("rofi"))
.filter((w: any) => w.workspace_id === active_ws_for_monitor)
)}>
{(win: any) => (
<box class={win.is_focused ? "focused" : "unfocused"}>
<image iconName={getIconName(win.app_id, win.title)} class="app-icon" />
<label label={shorten(win.title || "")} />
</box>
)}
</For>
@ -338,14 +324,13 @@ function Clients(): JSX.Element {
}}
</With>
</box>
)}
</With>
</box>
);
}
export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1) {
console.log("Creating Bar on monitor:", gdkmonitor);
console.log("Creating Bar on monitor:", gdkmonitor.get_connector());
const connector = gdkmonitor.get_connector() || "";
return (
<window
visible
@ -359,11 +344,11 @@ export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1) {
}
application={app}
class="Bar"
name="top-bar"
name={`top-bar-${connector}`}
>
<centerbox class="window-box">
<Left $type="start" />
<Center $type="center" />
<Left $type="start" connector={connector} />
<Center $type="center" connector={connector} />
<Right $type="end" />
</centerbox>
</window>

View File

@ -1,60 +1,19 @@
import app from "ags/gtk4/app";
import { Gdk } from "ags/gtk4";
import style from "./style.scss";
import Bar from "./Bar";
import Hyprland from "gi://AstalHyprland";
import NotificationPopups from "./notifications/NotificationPopups";
const hyprland = Hyprland.get_default();
function find_main_monitor(): Hyprland.Monitor {
let monitors = hyprland.get_monitors();
for (let j = 0; j < monitors.length; j++) {
for (const monitor of ["eDP", "DP", "HDMI-A"]) {
for (let i = 0; i < monitors.length; i++) {
console.log(
"Checking monitor:",
monitors[i].get_name(),
"against",
monitor + "-" + j,
);
if (monitors[i].get_name() == monitor + "-" + j) {
return monitors[i];
}
}
}
}
return monitors[0];
}
function register_windows(monitor: Hyprland.Monitor) {
let gtkMonitor = app.get_monitors()[0];
let scale = monitor.get_width() >= 3000 ? 1.2 : 1;
Bar(gtkMonitor, scale);
function register_windows(monitor: Gdk.Monitor) {
let scale = (monitor.get_geometry().width >= 3000) ? 1.2 : 1;
Bar(monitor, scale);
NotificationPopups();
}
function switch_to_best_monitor() {
let mainMonitor = find_main_monitor();
for (var wd of app.get_windows()) {
wd.destroy();
}
register_windows(mainMonitor);
}
hyprland.connect("monitor-added", (_, _monitor: Hyprland.Monitor) => {
switch_to_best_monitor();
});
hyprland.connect("monitor-removed", () => {
switch_to_best_monitor();
});
console.log("Trying to find the best monitor");
app.start({
css: style,
iconTheme: "Papirus",
main() {
switch_to_best_monitor();
register_windows(app.get_monitors()[0]);
},
});

View File

@ -1,5 +1,4 @@
import Apps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland";
const app_icons = new Apps.Apps().list.reduce(
(acc, app) => {
@ -16,57 +15,39 @@ const app_icons = new Apps.Apps().list.reduce(
{ classOrNames: {} as Record<string, string>, executables: {} as Record<string, string> },
);
export function getIconName(client: AstalHyprland.Client | null | undefined) {
if (!client) return "";
export function getIconName(app_id: string | null | undefined, title: string | null | undefined) {
if (!app_id && !title) return "";
// try a bunch of fields (snake_case and camelCase variants)
// try fields matching Niri outputs
const possibleKeys = [
// common client properties (snake_case)
(client as any).wm_class,
(client as any).initial_class,
(client as any).executable,
// camelCase variants (some bindings expose these)
(client as any).class,
(client as any).initialClass,
(client as any).initialTitle,
(client as any).title,
(client as any).name,
app_id,
title
].filter(Boolean) as string[];
// 1) direct exact match
for (const k of possibleKeys) {
const icon = app_icons.classOrNames[k] ?? app_icons.executables[k];
if (icon) {
// cache the mapping for this client's canonical class (prefer wm_class or class)
const cacheKey = (client as any).wm_class ?? (client as any).class ?? (client as any).name ?? k;
// cache the mapping
const cacheKey = app_id ?? title ?? k;
app_icons.classOrNames[cacheKey] = icon;
return icon;
}
}
// 2) fuzzy match: see if any stored key includes any of the client strings (title, name, etc.)
const searchStrings = [
(client as any).title,
(client as any).initialTitle,
(client as any).initial_class,
(client as any).initialClass,
(client as any).wm_class,
(client as any).class,
(client as any).name,
].filter(Boolean) as string[];
for (const s of searchStrings) {
for (const s of possibleKeys) {
const matchKey = Object.keys(app_icons.classOrNames).find((key) => key.includes(s));
if (matchKey) {
const icon = app_icons.classOrNames[matchKey];
const cacheKey = (client as any).wm_class ?? (client as any).class ?? (client as any).name ?? s;
const cacheKey = app_id ?? title ?? s;
app_icons.classOrNames[cacheKey] = icon;
return icon;
}
}
// nothing found — cache empty string so we don't repeat work
const cacheKey = (client as any).wm_class ?? (client as any).class ?? (client as any).name ?? "";
const cacheKey = app_id ?? title ?? "";
if (cacheKey) app_icons.classOrNames[cacheKey] = "";
return "";
}

View File

@ -21,7 +21,7 @@
pkgs.accountsservice
battery
hyprland
# hyprland
tray
powerprofiles
wireplumber

View File

@ -147,7 +147,6 @@ in {
"rounding 0, match:float 0, match:workspace f[1]"
];
# UPDATED: New Layer Rule Syntax (0.53.0)
# Syntax: effect value, match:namespace regex
layerrule = [
"no_anim on, match:namespace ags_bar_0"

142
home/niri/default.nix Normal file
View File

@ -0,0 +1,142 @@
{pkgs, ...}: let
terminal = "${pkgs.foot}/bin/footclient";
nautilus = "${pkgs.nautilus}/bin/nautilus";
in {
# Since native Home Manager doesn't have a programs.niri module yet,
# we generate the KDL configuration file directly.
services.hyprpaper = {
enable = true;
settings = {
ipc = "off";
splash = false;
wallpaper = {
path = "${../hyprland/files/wallpaper.png}";
monitor = "";
};
};
};
programs.hyprlock = {
enable = true;
settings = {
input-field = [
{
size = "200, 50";
position = "0, -80";
monitor = "";
dots_center = true;
fade_on_empty = true;
font_color = "rgb(202, 211, 245)";
inner_color = "rgb(91, 96, 120)";
outer_color = "rgb(24, 25, 38)";
outline_thickness = 3;
placeholder_text = "<span foreground=\"##cad3f5\">Password...</span>";
shadow_passes = 2;
}
];
general = {
no_fade_in = false;
};
background = [
{
path = "screenshot";
blur_passes = 3;
blur_size = 8;
}
];
};
};
xdg.configFile."niri/config.kdl".text = ''
spawn-at-startup "${pkgs.hyprpaper}/bin/hyprpaper"
spawn-at-startup "foot" "--server"
spawn-at-startup "ags" "run"
spawn-at-startup "${pkgs.mate-polkit}/bin/polkit-mate"
prefer-no-csd
layout {
gaps 5
border {
off
}
focus-ring {
width 4
active-color "rgba(0, 47, 95, 238)"
inactive-color "rgba(255, 255, 255, 0)"
}
}
input {
touchpad {
tap
}
focus-follows-mouse max-scroll-amount="10%"
warp-mouse-to-focus
}
output "eDP-1" {
scale 1.2
}
window-rule {
match app-id="vicinae"
open-floating true
}
binds {
// General Binds
Mod+Return { spawn "${terminal}"; }
Mod+Shift+Return { spawn "${terminal}"; }
Mod+Q { close-window; }
Mod+A { spawn "${nautilus}"; }
Mod+B { spawn "Helium"; }
Mod+M { maximize-column; }
Mod+F { fullscreen-window; }
// Vicinae Binds
Mod+C { spawn "vicinae" "vicinae://extensions/vicinae/clipboard/history"; }
Mod+N { spawn "vicinae" "vicinae://extensions/vicinae/wm/switch-windows"; }
Mod+Escape { spawn "vicinae" "open" "-q" "Power Management "; }
// Main Vicinae Toggle
Mod+Space { spawn "vicinae" "toggle"; }
Mod+Comma { consume-or-expel-window-left; }
Mod+Period { consume-or-expel-window-right; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// Movement
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+L { move-column-right; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
// Workspaces
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; }
// Move to workspace
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; }
}
'';
}

View File

@ -7,7 +7,8 @@
imports = [
(import ./common.nix {inherit inputs config pkgs;})
(import ./python.nix {inherit inputs config pkgs;})
(import ../hyprland {inherit inputs pkgs;})
# (import ../hyprland {inherit inputs pkgs;})
(import ../niri {inherit inputs config pkgs;})
(import ../ags {inherit inputs pkgs;})
../rofi
../vicinae

View File

@ -15,15 +15,17 @@
vulkan-extension-layer
];
};
programs.hyprland = {
enable = true;
withUWSM = true;
};
# programs.hyprland = {
# enable = true;
# withUWSM = true;
# };
programs.niri.enable = true;
services.greetd = {
enable = pkgs.lib.mkDefault true;
settings = rec {
initial_session = {
command = "uwsm start hyprland-uwsm.desktop";
# command = "uwsm start hyprland-uwsm.desktop";
command = "niri-session";
user = "user";
};
default_session = initial_session;