6 Commits

Author SHA1 Message Date
Miguel Palhas fda1bf2985 Merge remote-tracking branch 'origin/main' into eww-shell
CI / lint (pull_request) Successful in 30s
CI / eval (pull_request) Successful in 2m16s
2026-08-19 15:11:45 +01:00
Miguel Palhas 3e3c7f1b57 fix: match the real blueman window class
CI / lint (pull_request) Successful in 28s
CI / eval (pull_request) Successful in 1m48s
nixpkgs wraps blueman with --inherit-argv0, so the class is blueman-manager,
not the .blueman-manager-wrapped the rule expected. It never floated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:31:29 +01:00
Miguel Palhas 8e5feb8522 fix: make the audio sliders actually set volume
CI / lint (pull_request) Successful in 29s
CI / eval (pull_request) Successful in 1m48s
yuck interpolates ${...}, not {...}, so wpctl was being handed a literal
"{node}" and failing; the slider snapped back on the next poll.

Also drops the full-screen backdrop. It gave click-outside-to-close, but its
handler fired for clicks on the sliders too, closing the overlay mid-drag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:27:53 +01:00
Miguel Palhas db0e1aea32 feat: make the panel a centered dashboard with audio
CI / lint (pull_request) Successful in 29s
CI / eval (pull_request) Successful in 1m53s
Reshapes the corner strip into a centered card grid over a dimmed backdrop,
which also gives it click-outside-to-close.

Adds an Audio card: speaker and mic sliders driven by wpctl, each with a mute
toggle. Rows hide themselves when the node does not exist, same as battery.

custom.hyprland.panelScale multiplies every length in the stylesheet; konishi
sets 1.6 for its 4K outputs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:22:17 +01:00
Miguel Palhas 4a8e0241e4 fix: detach the panel's launched apps from eww
CI / lint (pull_request) Successful in 38s
CI / eval (pull_request) Successful in 2m10s
eww kills the onclick handler's process group when the window it came from
closes, so `eww close panel; <app>` never reached the app. Launch through
setsid first, close the panel after.

loginctl was also unqualified, which a systemd user unit's PATH does not cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:33:39 +01:00
Miguel Palhas 39cc3f2cd0 feat: replace noctalia with an eww panel
CI / lint (pull_request) Successful in 31s
CI / eval (pull_request) Successful in 2m19s
Drops the noctalia shell and its flake input. It was doing six jobs; each is
now a small binary-cached package, so nothing builds from source:

- control centre -> an eww overlay, SUPER+N, opening on the focused monitor
- launcher       -> fuzzel, SUPER+space
- wallpaper      -> wpaperd, with waypaper as the picker
- notifications  -> mako
- polkit agent   -> hyprpolkitagent
- qt/zathura theming -> static dark colours instead of generated ones

The panel shows clock, battery (hidden where there is none), network,
bluetooth, plus buttons for next/pick wallpaper, bluetooth power and lock.

custom.hyprland.verticalOutputs replaces noctalia's per-monitor wallpaper
handling; konishi sets it to DP-2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:24:55 +01:00
16 changed files with 565 additions and 180 deletions
Generated
-22
View File
@@ -910,27 +910,6 @@
"type": "github"
}
},
"noctalia": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785335804,
"narHash": "sha256-oN3I6Kr71lOCE3/ou4GgY2cQYh4B6xQxZXzomFgYn0s=",
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "aded31799eb0dffdbcf436243083f1ebce26f95d",
"type": "github"
},
"original": {
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "aded31799eb0dffdbcf436243083f1ebce26f95d",
"type": "github"
}
},
"nvchad-starter": {
"flake": false,
"locked": {
@@ -1006,7 +985,6 @@
"hyprland": "hyprland",
"nix-index-database": "nix-index-database",
"nixpkgs": "nixpkgs_6",
"noctalia": "noctalia",
"nvchad-starter": "nvchad-starter",
"nvchad4nix": "nvchad4nix",
"rose-pine-hyprcursor": "rose-pine-hyprcursor",
-7
View File
@@ -49,13 +49,6 @@
url = "https://github.com/hyprwm/Hyprland";
submodules = true;
};
# Pinned to a rev: noctalia publishes no binary cache, so every bump means
# building the Qt/QML app locally. quickshell itself comes from nixpkgs and
# is cached, so the follows stays. Bump deliberately, not via `flake update`.
noctalia = {
url = "github:noctalia-dev/noctalia-shell/aded31799eb0dffdbcf436243083f1ebce26f95d";
inputs.nixpkgs.follows = "nixpkgs";
};
spicetify-nix = {
url = "github:Gerg-L/spicetify-nix";
inputs.nixpkgs.follows = "nixpkgs";
-6
View File
@@ -27,9 +27,6 @@ in
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.dark} ${toString cursorSize}
'';
noctalia-theme = ''
noctalia msg theme-mode-set dark
'';
};
lightModeScripts = {
gtk-theme = ''
@@ -49,9 +46,6 @@ in
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.light} ${toString cursorSize}
'';
noctalia-theme = ''
noctalia msg theme-mode-set light
'';
};
};
+18 -19
View File
@@ -1,6 +1,5 @@
{
config,
lib,
pkgs,
...
}:
@@ -83,36 +82,36 @@
};
xdg.configFile = {
# zathura: include noctalia-generated theme
"zathura/zathurarc".text = ''
include noctaliarc
set default-bg "#2e3440"
set default-fg "#eceff4"
set statusbar-bg "#3b4252"
set statusbar-fg "#eceff4"
set inputbar-bg "#3b4252"
set inputbar-fg "#eceff4"
set highlight-color "#ebcb8b"
set highlight-active-color "#88c0d0"
set recolor-lightcolor "#2e3440"
set recolor-darkcolor "#eceff4"
'';
# gtk: include noctalia-generated css (mkForce to override gtk module)
"gtk-3.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
"gtk-4.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
# qt: use noctalia color scheme. Applied via QT_QPA_PLATFORMTHEME=qt6ct,
# set in the Hyprland env block (and imported into the dbus activation
# environment so the xdph screen-share picker inherits it too).
# custom_palette=true is required for qt5ct/qt6ct to actually apply the
# color scheme; Fusion is used because it fully honors a custom palette
# (Breeze/native styles partly ignore it).
# qt: dark color scheme shipped by qt5ct/qt6ct themselves. Applied via
# QT_QPA_PLATFORMTHEME=qt6ct, set in the Hyprland env block (and imported
# into the dbus activation environment so the xdph screen-share picker
# inherits it too). custom_palette=true is required for qt5ct/qt6ct to
# actually apply the color scheme; Fusion is used because it fully honors a
# custom palette (Breeze/native styles partly ignore it).
"qt5ct/qt5ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${config.home.homeDirectory}/.config/qt5ct/colors/noctalia.conf
color_scheme_path=${pkgs.libsForQt5.qt5ct}/share/qt5ct/colors/darker.conf
'';
"qt6ct/qt6ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${config.home.homeDirectory}/.config/qt6ct/colors/noctalia.conf
color_scheme_path=${pkgs.kdePackages.qt6ct}/share/qt6ct/colors/darker.conf
'';
};
}
+32 -12
View File
@@ -90,6 +90,21 @@ in
description = "Cursor theme darkman selects in light mode.";
};
};
panelScale = lib.mkOption {
type = lib.types.float;
default = 1.0;
example = 1.5;
description = "Multiplier for every length in the eww dashboard stylesheet.";
};
verticalOutputs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "DP-2" ];
description = ''
Outputs that get the portrait wallpaper set. Everything else gets the
landscape one.
'';
};
shareOutput = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@@ -104,8 +119,9 @@ in
imports = [
./cursor.nix
./eww
./kbptr.nix
./noctalia
./shell.nix
./wallpapers.nix
];
@@ -227,8 +243,8 @@ in
hl.env("HYPRCURSOR_THEME", "${cfg.cursorTheme.dark}")
hl.env("HYPRCURSOR_SIZE", "${toString cfg.cursorSize}")
-- Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so
-- the noctalia-generated color scheme actually applies. Without this
-- var the qt6ct.conf is never read.
-- the dark color scheme actually applies. Without this var the
-- qt6ct.conf is never read.
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
-- autostart
@@ -238,14 +254,15 @@ in
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME")
hl.exec_cmd("hyprctl setcursor ${cfg.cursorTheme.dark} ${toString cfg.cursorSize}")
hl.exec_cmd("hyprsunset")
hl.exec_cmd("noctalia")
-- eww, wpaperd, mako and the polkit agent are systemd user services
-- bound to graphical-session.target; nothing to start here.
-- kdeconnectd is only DBus-activated; nothing starts it at login,
-- so clipboard sync stays dead until the indicator runs.
hl.exec_cmd("kdeconnect-indicator")
end)
-- layer rules
hl.layer_rule({ match = { namespace = "noctalia-wallpaper" }, blur = true, ignore_alpha = 0.5 })
hl.layer_rule({ match = { namespace = "eww-panel" }, blur = true, ignore_alpha = 0.5 })
-- workspace rules
hl.workspace_rule({ workspace = "w[t1]", gaps_out = 0, gaps_in = 0 }) -- no gaps when only window
@@ -273,7 +290,11 @@ in
})
hl.window_rule({ match = { class = "thunar" }, float = true, size = "1800 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[\.blueman-manager-wrapped]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- nixpkgs wraps blueman with --inherit-argv0, so the class is
-- blueman-manager, not the .blueman-manager-wrapped the old rule expected.
hl.window_rule({ match = { class = [[.*[Bb]lueman.*]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "nm-connection-editor" }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "waypaper" }, float = true, size = "1600 1000", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[org\.pulseaudio\.pavucontrol]] }, float = true, size = "1200 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG).
@@ -296,7 +317,7 @@ in
hl.window_rule({ match = { workspace = "special:yazi" }, center = true })
-- keybinds
hl.bind(mod .. " + N", hl.dsp.exec_cmd("noctalia msg panel-toggle control-center"))
hl.bind(mod .. " + N", hl.dsp.exec_cmd("eww-panel"))
hl.bind(mod .. " + T", hl.dsp.exec_cmd("kitty"))
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mod .. " + Q", hl.dsp.window.close())
@@ -306,7 +327,7 @@ in
-- lock (routes through logind -> hypridle lock_cmd -> hyprlock)
hl.bind(mod .. " + CTRL + L", hl.dsp.exec_cmd("loginctl lock-session"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("fuzzel"))
-- printscreen
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"))
@@ -365,10 +386,9 @@ in
services.hyprpaper.enable = false;
# hyprlock: the lock screen, replacing noctalia's built-in locker (which is
# ugly and — unlike hyprlock — opaque to scripts/lock-state detection).
# Clean minimal dark look; tweak colours/clock/font to taste. hypridle below
# drives it (idle + before-sleep), and $mod CTRL+L locks manually.
# hyprlock: the lock screen. Clean minimal dark look; tweak colours/clock/
# font to taste. hypridle below drives it (idle + before-sleep), and
# $mod CTRL+L locks manually.
programs.hyprlock = {
enable = true;
settings = {
@@ -0,0 +1,292 @@
{
config,
lib,
pkgs,
inputs,
...
}:
let
cfg = config.custom.hyprland;
# Every length in the stylesheet goes through this, so one option resizes the
# whole dashboard for 4K hosts.
px = n: "${toString (builtins.ceil (n * cfg.panelScale))}px";
mkScript =
name: runtimeInputs:
pkgs.writeShellApplication {
inherit name runtimeInputs;
text = builtins.readFile (./scripts + "/${name}.sh");
};
battery = mkScript "battery" [
pkgs.coreutils
pkgs.findutils
pkgs.jq
];
network = mkScript "network" [
pkgs.networkmanager
pkgs.gawk
pkgs.gnused
pkgs.jq
];
bluetooth = mkScript "bluetooth" [
pkgs.bluez
pkgs.coreutils
pkgs.findutils
pkgs.gawk
pkgs.jq
];
audio = mkScript "audio" [
pkgs.gawk
pkgs.jq
pkgs.wireplumber
];
panelToggle = mkScript "eww-panel" [
config.programs.eww.package
pkgs.jq
inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland
];
eww = lib.getExe config.programs.eww.package;
bluetoothctl = "${pkgs.bluez}/bin/bluetoothctl";
wpctl = "${pkgs.wireplumber}/bin/wpctl";
wpaperctl = "${pkgs.wpaperd}/bin/wpaperctl";
# setsid, and before the close rather than after it: eww kills the onclick
# handler's process group when the window it came from goes away, so a bare
# `close panel; cmd` never reaches cmd.
act = cmd: "${pkgs.util-linux}/bin/setsid --fork ${cmd}; ${eww} close panel";
in
{
home.packages = [
panelToggle
pkgs.waypaper
];
programs.eww = {
enable = true;
systemd.enable = true;
yuckConfig = ''
;; Each script prints one JSON object; widgets read .icon / .label / .sub
;; off it.
(defpoll clock-time :interval "10s" :initial "--:--" `date +%H:%M`)
(defpoll clock-date :interval "1h" :initial "" `date "+%A, %d %B"`)
(defpoll battery
:interval "30s"
:initial '{"present":false,"icon":"","label":"","sub":""}'
`${lib.getExe battery}`)
(defpoll network
:interval "10s"
:initial '{"icon":"","label":"...","sub":""}'
`${lib.getExe network}`)
(defpoll bluetooth
:interval "10s"
:initial '{"powered":false,"icon":"","label":"...","sub":""}'
`${lib.getExe bluetooth}`)
(defpoll speaker
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} sink`)
(defpoll mic
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} source`)
(defwidget card [title]
(box :class "card" :orientation "v" :space-evenly false
(label :class "card-title" :halign "start" :text title :visible {title != ""})
(box :orientation "v" :space-evenly false
(children))))
(defwidget row [icon label sub onclick]
(eventbox :class "row" :cursor "pointer" :onclick onclick
(box :space-evenly false
(label :class "row-icon" :text icon)
(box :orientation "v" :space-evenly false :hexpand true
(label :class "row-label" :halign "start" :text label)
(label :class "row-sub" :halign "start" :text sub :visible {sub != ""})))))
;; Mute button, slider, readout. `state` is the speaker/mic json object.
(defwidget volume [state node]
(box :space-evenly false :visible {state.present}
(button :class {state.muted ? "mute muted" : "mute"}
:onclick "${wpctl} set-mute ''${node} toggle"
{state.icon})
(scale :class "slider" :hexpand true
:value {state.volume} :min 0 :max 101
:onchange "${wpctl} set-volume ''${node} {}%")
(label :class "row-sub" :text {"''${state.volume}%"})))
(defwidget action [icon tooltip onclick]
(button :class "action" :tooltip tooltip :onclick onclick icon))
(defwidget dashboard []
(box :class "dash" :orientation "v" :space-evenly false
(box :space-evenly false
(card :title ""
(label :class "clock-time" :halign "start" :text clock-time)
(label :class "clock-date" :halign "start" :text clock-date))
(card :title "System"
(box :visible {battery.present} :space-evenly false
(row :icon {battery.icon} :label {battery.label}
:sub {battery.sub} :onclick ""))
(row :icon {network.icon} :label {network.label} :sub {network.sub}
:onclick "${act "${pkgs.networkmanagerapplet}/bin/nm-connection-editor"}")
(row :icon {bluetooth.icon} :label {bluetooth.label} :sub {bluetooth.sub}
:onclick "${act "${pkgs.blueman}/bin/blueman-manager"}")))
(box :space-evenly false
(card :title "Audio"
(volume :state speaker :node "@DEFAULT_AUDIO_SINK@")
(volume :state mic :node "@DEFAULT_AUDIO_SOURCE@"))
(card :title "Actions"
(box :space-evenly false :halign "start"
(action :icon "󰑐" :tooltip "Next wallpaper"
:onclick "${wpaperctl} next")
(action :icon "󰸉" :tooltip "Pick wallpaper"
:onclick "${act "${lib.getExe pkgs.waypaper}"}")
(action :icon "󰂯" :tooltip "Toggle bluetooth"
:onclick {bluetooth.powered
? "${bluetoothctl} power off"
: "${bluetoothctl} power on"})
(action :icon "󰌾" :tooltip "Lock"
:onclick "${act "${pkgs.systemd}/bin/loginctl lock-session"}"))))))
;; Sized to its content. A full-screen backdrop would give click-outside-
;; to-close, but its handler also fires for clicks on the sliders.
(defwindow panel
:namespace "eww-panel"
:geometry (geometry :anchor "center")
:stacking "overlay"
(dashboard))
'';
scssConfig = ''
$bg: rgba(46, 52, 64, 0.96);
$card: rgba(59, 66, 82, 0.72);
$border: #4c566a;
$fg: #eceff4;
$muted: #81a1c1;
$accent: #88c0d0;
$red: #bf616a;
* {
all: unset;
font-family: "FiraCode Nerd Font", monospace;
}
window {
background-color: transparent;
}
.dash {
background-color: $bg;
border: ${px 1} solid $border;
border-radius: ${px 20};
padding: ${px 14};
color: $fg;
}
.card {
background-color: $card;
border-radius: ${px 14};
padding: ${px 16};
margin: ${px 6};
min-width: ${px 300};
}
.card-title {
font-size: ${px 11};
color: $muted;
margin-bottom: ${px 8};
}
.clock-time {
font-size: ${px 56};
font-weight: 600;
}
.clock-date {
font-size: ${px 14};
color: $muted;
}
.row {
border-radius: ${px 10};
padding: ${px 10} ${px 12};
&:hover {
background-color: rgba(136, 192, 208, 0.14);
}
}
.row-icon {
font-size: ${px 22};
color: $accent;
min-width: ${px 34};
}
.row-label {
font-size: ${px 14};
}
.row-sub {
font-size: ${px 11};
color: $muted;
}
.mute {
font-size: ${px 20};
color: $accent;
min-width: ${px 34};
&.muted {
color: $red;
}
}
.slider {
margin: 0 ${px 12};
min-width: ${px 180};
trough {
background-color: rgba(76, 86, 106, 0.6);
border-radius: ${px 8};
min-height: ${px 8};
highlight {
background-color: $accent;
border-radius: ${px 8};
}
}
}
.action {
background-color: rgba(76, 86, 106, 0.45);
border-radius: ${px 12};
padding: ${px 12} ${px 18};
margin-right: ${px 8};
font-size: ${px 20};
color: $fg;
&:hover {
background-color: $accent;
color: #2e3440;
}
}
'';
};
}
@@ -0,0 +1,37 @@
# One JSON object per audio node. $1 is "sink" or "source". wpctl prints
# "Volume: 0.45", with " [MUTED]" appended, and errors when there is no node.
case "$1" in
sink) node="@DEFAULT_AUDIO_SINK@" ;;
source) node="@DEFAULT_AUDIO_SOURCE@" ;;
*) echo "usage: audio sink|source" >&2; exit 2 ;;
esac
raw=$(wpctl get-volume "$node" 2>/dev/null || true)
case "$raw" in
*Volume:*) ;;
*)
jq -nc '{ present: false, volume: 0, muted: true, icon: "󰖁" }'
exit 0
;;
esac
volume=$(printf '%s' "$raw" | awk '{ printf "%d", $2 * 100 }')
muted=false
case "$raw" in *MUTED*) muted=true ;; esac
if [ "$1" = "source" ]; then
if [ "$muted" = true ]; then icon="󰍭"; else icon="󰍬"; fi
elif [ "$muted" = true ]; then icon="󰖁"
elif [ "$volume" -ge 66 ]; then icon="󰕾"
elif [ "$volume" -ge 33 ]; then icon="󰖀"
else icon="󰕿"
fi
jq -nc \
--argjson volume "$volume" \
--argjson muted "$muted" \
--arg icon "$icon" \
'{ present: true, volume: $volume, muted: $muted, icon: $icon }'
@@ -0,0 +1,32 @@
# One JSON object for the panel's battery row. `present: false` on the desktops,
# which have no BAT* at all — the row hides itself on that.
bat=$(find /sys/class/power_supply -maxdepth 1 -name 'BAT*' 2>/dev/null | sort | head -1)
if [ -z "$bat" ]; then
jq -nc '{ present: false, icon: "", label: "", sub: "" }'
exit 0
fi
capacity=$(cat "$bat/capacity")
status=$(cat "$bat/status")
if [ "$status" = "Charging" ] || [ "$status" = "Full" ]; then
icon="󰂄"
elif [ "$capacity" -ge 90 ]; then icon="󰁹"
elif [ "$capacity" -ge 80 ]; then icon="󰂂"
elif [ "$capacity" -ge 70 ]; then icon="󰂁"
elif [ "$capacity" -ge 60 ]; then icon="󰂀"
elif [ "$capacity" -ge 50 ]; then icon="󰁿"
elif [ "$capacity" -ge 40 ]; then icon="󰁾"
elif [ "$capacity" -ge 30 ]; then icon="󰁽"
elif [ "$capacity" -ge 20 ]; then icon="󰁼"
elif [ "$capacity" -ge 10 ]; then icon="󰁻"
else icon="󰁺"
fi
jq -nc \
--arg icon "$icon" \
--arg label "$capacity%" \
--arg sub "$status" \
'{ present: true, icon: $icon, label: $label, sub: $sub }'
@@ -0,0 +1,22 @@
# One JSON object for the panel's bluetooth row. bluetoothctl blocks forever
# instead of erroring when there is no adapter, hence the /sys check + timeouts.
if [ -z "$(find /sys/class/bluetooth -mindepth 1 -maxdepth 1 2>/dev/null)" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "No adapter", sub: "" }'
exit 0
fi
powered=$(timeout 2 bluetoothctl show 2>/dev/null | awk '/Powered:/ { print $2; exit }' || true)
if [ "$powered" != "yes" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "Off", sub: "" }'
exit 0
fi
connected=$(timeout 2 bluetoothctl devices Connected 2>/dev/null | cut -d' ' -f3- | paste -sd', ' - || true)
if [ -n "$connected" ]; then
jq -nc --arg sub "$connected" '{ powered: true, icon: "󰂱", label: "On", sub: $sub }'
else
jq -nc '{ powered: true, icon: "󰂯", label: "On", sub: "Nothing connected" }'
fi
@@ -0,0 +1,11 @@
# Toggle the panel on whichever monitor has focus. `eww open --toggle` can't do
# this: it ignores --screen when the window is already open elsewhere, so the
# panel would stay stuck on the monitor it first appeared on.
if eww active-windows | grep -q '^panel'; then
exec eww close panel
fi
screen=$(hyprctl -j monitors | jq -r 'map(select(.focused))[0].id // 0')
exec eww open panel --screen "$screen"
@@ -0,0 +1,43 @@
# One JSON object for the panel's network row. State must match "connected"
# exactly: NetworkManager reports docker0 and br-* as "connected (externally)".
status=$(nmcli -t -f TYPE,STATE,CONNECTION device status 2>/dev/null || true)
# nmcli's terse output backslash-escapes colons inside connection names, so
# rejoin fields 3..NF and unescape rather than taking $3.
pick() {
printf '%s\n' "$status" | awk -F: -v want="$1" '
$1 == want && $2 == "connected" {
name = $3
for (i = 4; i <= NF; i++) name = name ":" $i
print name
exit
}' | sed 's/\\:/:/g'
}
wifi=$(pick wifi)
ethernet=$(pick ethernet)
if [ -n "$wifi" ]; then
signal=$(nmcli -t -f IN-USE,SIGNAL dev wifi 2>/dev/null | awk -F: '$1 == "*" { print $2; exit }' || true)
if [ -z "$signal" ]; then icon="󰤨"
elif [ "$signal" -ge 75 ]; then icon="󰤨"
elif [ "$signal" -ge 50 ]; then icon="󰤥"
elif [ "$signal" -ge 25 ]; then icon="󰤢"
else icon="󰤟"
fi
if [ -n "$signal" ]; then
sub="Wi-Fi · $signal%"
else
sub="Wi-Fi"
fi
jq -nc --arg icon "$icon" --arg label "$wifi" --arg sub "$sub" \
'{ icon: $icon, label: $label, sub: $sub }'
elif [ -n "$ethernet" ]; then
jq -nc --arg label "$ethernet" '{ icon: "󰈀", label: $label, sub: "Ethernet" }'
else
jq -nc '{ icon: "󰤭", label: "Offline", sub: "" }'
fi
@@ -1,104 +0,0 @@
{
config,
lib,
inputs,
pkgs,
...
}:
let
avatar = pkgs.fetchurl {
url = "https://github.com/naps62.png";
sha256 = "1ck11xg4rwcvn8dcc06ax7i9yfzy21v1n6h5mcjrkrici5sznlsv";
};
in
{
imports = [ inputs.noctalia.homeModules.default ];
# Noctalia v5 rewrote the config schema (TOML, snake_case, flat sections).
# The v4 settings (appLauncher.*, bar.widgets.*, controlCenter.shortcuts.*, etc.)
# have no v5 equivalents — most must now be tweaked through the in-shell
# Settings UI. See docs.noctalia.dev/v5 and example.toml in the upstream repo.
programs.noctalia = {
enable = true;
settings = {
shell = {
font_family = "Sans Serif";
avatar_path = "${config.home.homeDirectory}/.face";
telemetry_enabled = false;
polkit_agent = true;
};
wallpaper = {
enabled = true;
fill_mode = "crop";
directory = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
transition = [ "fade" ];
transition_duration = 1500;
automation = {
enabled = true;
interval_minutes = 360;
order = "random";
recursive = true;
};
};
theme = {
mode = "dark";
source = "builtin";
builtin = "Nord";
templates = {
enable_builtin_templates = true;
builtin_ids = [
"btop"
"gtk3"
"gtk4"
"hyprland"
"qt"
];
};
};
notification.enable_daemon = true;
weather = {
enabled = true;
unit = "celsius";
};
location = {
auto_locate = false;
address = "Braga, Portugal";
};
nightlight = {
enabled = true;
temperature_day = 6500;
temperature_night = 4000;
};
bar.main = {
position = "top";
background_opacity = 0.93;
radius = 12;
start = [ "launcher" "wallpaper" "workspaces" ];
center = [ "media" "clock" ];
end = [ "tray" "notifications" "network" "bluetooth" "volume" "brightness" "battery" "control-center" "session" ];
};
};
};
home.file.".face".source = avatar;
# force overwrite files that noctalia may have turned into regular files
# (Hyprland 0.55+ is Lua-only, so the managed file is now hyprland.lua)
xdg.configFile."hypr/hyprland.lua".force = true;
xdg.configFile."gtk-3.0/gtk.css".force = true;
# NOTE: noctalia still writes its color theme to ~/.config/hypr/noctalia.conf
# in *hyprlang* syntax ($primary = rgb(...), general { col.active_border = ... }).
# Hyprland 0.55+ dropped the hyprlang parser entirely, so that file can no
# longer be `source`d — there is no hl.source, and the lua config can't parse
# hyprlang. Hyprland therefore uses the static border colors from
# ../default.nix instead of noctalia's themed ones. To restore live theming,
# noctalia would need to emit a Lua snippet we can require() here.
}
+48
View File
@@ -0,0 +1,48 @@
{ pkgs, ... }:
{
programs.fuzzel = {
enable = true;
settings = {
main = {
terminal = "${pkgs.kitty}/bin/kitty";
layer = "overlay";
font = "FiraCode Nerd Font:size=12";
icon-theme = "Tela black";
lines = 12;
width = 40;
inner-pad = 8;
};
border = {
radius = 12;
width = 1;
};
colors = {
background = "2e3440f0";
text = "eceff4ff";
match = "88c0d0ff";
selection = "434c5eff";
selection-text = "eceff4ff";
border = "4c566aff";
};
};
};
services.mako = {
enable = true;
settings = {
font = "FiraCode Nerd Font 11";
background-color = "#2e3440f0";
text-color = "#eceff4";
border-color = "#4c566a";
border-radius = 12;
border-size = 1;
padding = "14";
margin = "12";
default-timeout = 6000;
anchor = "top-right";
layer = "overlay";
};
};
services.hyprpolkitagent.enable = true;
}
+27 -1
View File
@@ -1,5 +1,10 @@
{ pkgs, ... }:
{ config, pkgs, ... }:
let
cfg = config.custom.hyprland;
horizontalDir = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
verticalDir = "${config.home.homeDirectory}/.cache/wallpapers/2160x3840";
# Horizontal wallpapers (3840x2160) for HDMI-A-1 and DP-1
# Just add any image URL you want here
horizontal = [
@@ -107,4 +112,25 @@ let
in
{
home.file.".local/bin/wallpaper-fetch".source = fetchScript;
# `any` covers every output not named in custom.hyprland.verticalOutputs,
# which get the portrait set instead.
services.wpaperd = {
enable = true;
settings =
{
default = {
duration = "6h";
sorting = "random";
mode = "center";
};
any.path = horizontalDir;
}
// builtins.listToAttrs (
map (output: {
name = output;
value.path = verticalDir;
}) cfg.verticalOutputs
);
};
}
+1 -1
View File
@@ -117,7 +117,7 @@ in
{
programs.kitty = {
enable = true;
# Fixed theme — noctalia no longer toggles kitty with dark/light mode.
# Fixed theme — nothing toggles kitty with dark/light mode.
themeFile = "Catppuccin-Mocha";
settings = {
confirm_os_window_close = 0;
+2 -8
View File
@@ -29,6 +29,8 @@
cursorSize = 42;
# The screen actually shared in every call; skips the xdph picker entirely.
shareOutput = "HDMI-A-1";
verticalOutputs = [ "DP-2" ];
panelScale = 1.6;
};
custom.gaming = {
@@ -47,7 +49,6 @@
-- session the moment Hyprland starts so a cold boot lands on hyprlock, not
-- an open desktop. A brief flash before it paints is possible.
hl.exec_cmd("hyprlock")
hl.exec_cmd("$HOME/.local/bin/hyprpaper-rotate")
end)
-- ethui-dev
@@ -110,12 +111,5 @@
hl.config({ render = { direct_scanout = false } })
'';
programs.noctalia.settings = {
shell.ui_scale = 1.25;
notification.monitors = [ "DP-1" ];
# NOTE: v5 has no per-monitor wallpaper directories; only directory_light /
# directory_dark. Re-add via Settings UI if upstream gains support.
};
programs.kitty.settings.font_size = 16;
}