feat(noctalia): add modular Hyprland profiles

This commit is contained in:
2026-07-21 13:42:48 +02:00
parent b79e8e3d08
commit 678ea8228d
11 changed files with 560 additions and 0 deletions
@@ -0,0 +1,14 @@
# Hyprland desktop status bar. Ashell reloads this file when it changes.
# Keep this intentionally lean while replacing the existing Waybar profile.
position = "Bottom"
layer = "Top"
[modules]
left = ["Workspaces", "KeyboardLayout"]
center = ["WindowTitle"]
right = ["SystemInfo", "MediaPlayer", ["Tray", "Tempo", "Settings"]]
[workspaces]
visibility_mode = "MonitorSpecific"
disable_special_workspaces = true
+135
View File
@@ -0,0 +1,135 @@
-- Shared Hyprland profile. yadm deploys machine.lua for the active class.
--
-- Scope: preserve launch, media, directional focus/move, workspace, scratchpad,
-- floating, fullscreen, and window-placement muscle memory. i3's split-tree,
-- stacking/tabbed, and resize-mode behavior is intentionally deferred.
local machine = dofile(os.getenv("HOME") .. "/.config/hypr/machine.lua")
for _, monitor in ipairs(machine.monitors or {}) do
hl.monitor(monitor)
end
local main_mod = "SUPER"
local terminal = "alacritty"
hl.config({
input = {
kb_layout = "us",
kb_variant = "intl",
numlock_by_default = true,
},
general = {
gaps_in = 8,
-- Keep a thin black perimeter so the bottom border remains on-screen.
gaps_out = 2,
border_size = 2,
col = {
-- Muted Ember tones: visible focus without a high-brightness OLED edge.
active_border = "rgba(c9782ed9)",
inactive_border = "rgba(2a190fa6)",
},
},
decoration = {
rounding = 0,
},
animations = {
enabled = false,
},
})
-- Application and session bindings.
local noctalia_ipc = "noctalia msg "
-- Tap and release Super on its own to open Noctalia's control-center home.
hl.bind(main_mod .. " + SUPER_L", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"), { release = true })
hl.bind(main_mod .. " + RETURN", hl.dsp.exec_cmd(terminal))
hl.bind(main_mod .. " + CTRL + RETURN", hl.dsp.exec_cmd(terminal .. " -e zellij"))
hl.bind(main_mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("rofi -show combi"))
hl.bind(main_mod .. " + SPACE", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle launcher"))
hl.bind(main_mod .. " + S", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"))
hl.bind(main_mod .. " + SHIFT + F", hl.dsp.exec_cmd(noctalia_ipc .. "bar-toggle main"))
hl.bind(main_mod .. " + SHIFT + R", hl.dsp.exec_cmd("hyprctl reload"))
hl.bind(main_mod .. " + CTRL + L", hl.dsp.exec_cmd("command -v hyprlock >/dev/null && hyprlock || loginctl lock-session"))
hl.bind(main_mod .. " + CTRL + SHIFT + ALT + S", hl.dsp.exec_cmd("systemctl suspend"))
hl.bind(main_mod .. " + CTRL + SHIFT + ALT + H", hl.dsp.exec_cmd("systemctl hibernate"))
hl.bind(main_mod .. " + SHIFT + C", hl.dsp.window.close())
hl.bind(main_mod .. " + CTRL + SHIFT + F", hl.dsp.exec_cmd("env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 flameshot gui"))
-- Audio, media, brightness, and calculator keys.
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true })
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
hl.bind(main_mod .. " + XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind(main_mod .. " + XF86AudioRaiseVolume", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind(main_mod .. " + XF86AudioLowerVolume", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd('light -A 5 && notify-send "Brightness set to $(light)"'), { locked = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd('light -U 5 && notify-send "Brightness set to $(light)"'), { locked = true })
hl.bind(main_mod .. " + XF86MonBrightnessDown", hl.dsp.exec_cmd('light -S 0.2 && notify-send "Brightness set to min"'), { locked = true })
hl.bind(main_mod .. " + XF86MonBrightnessUp", hl.dsp.exec_cmd('light -S 100 && notify-send "Brightness set to max"'), { locked = true })
hl.bind("XF86Calculator", hl.dsp.exec_cmd("qalculate-qt"), { locked = true })
-- Retain i3's hjkl and arrow-key directional workflow.
for _, direction in ipairs({ "left", "down", "up", "right" }) do
local key = ({ left = "H", down = "J", up = "K", right = "L" })[direction]
hl.bind(main_mod .. " + " .. key, hl.dsp.focus({ direction = direction }))
hl.bind(main_mod .. " + " .. direction, hl.dsp.focus({ direction = direction }))
end
for key, direction in pairs({ H = "l", J = "d", K = "u", L = "r", LEFT = "l", DOWN = "d", UP = "u", RIGHT = "r" }) do
hl.bind(main_mod .. " + SHIFT + " .. key, hl.dsp.window.move({ direction = direction }))
end
-- Numbered workspaces match the i3 profile. Shift moves; Ctrl moves and follows.
for workspace = 1, 9 do
local key = tostring(workspace)
hl.bind(main_mod .. " + " .. key, hl.dsp.focus({ workspace = workspace }))
hl.bind(main_mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = workspace }))
hl.bind(main_mod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = workspace, follow = true }))
end
-- Hyprland has no direct i3-style back_and_forth dispatcher; use numbered workspaces.
hl.bind(main_mod .. " + CTRL + RIGHT", hl.dsp.focus({ workspace = "+1" }))
hl.bind(main_mod .. " + CTRL + LEFT", hl.dsp.focus({ workspace = "-1" }))
-- Hyprland special workspaces are the Wayland analogue of the i3 scratchpad.
hl.bind(main_mod .. " + MINUS", hl.dsp.workspace.toggle_special("scratchpad"))
hl.bind(main_mod .. " + SHIFT + MINUS", hl.dsp.window.move({ workspace = "special:scratchpad" }))
-- Keep the i3 floating and fullscreen shortcuts. Layout-specific binds are deferred.
hl.bind(main_mod .. " + F", hl.dsp.window.fullscreen({ action = "toggle" }))
hl.bind(main_mod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" }))
hl.bind(main_mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(main_mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- i3 for_window/assign translations. Verify Wayland class/title values with `hyprctl clients`.
local function float_rule(name, match, border_size)
hl.window_rule({
name = name,
match = match,
float = true,
border_size = border_size,
})
end
hl.window_rule({ name = "thunderbird-workspace", match = { class = "^Thunderbird$" }, workspace = "9" })
float_rule("alsamixer", { title = "^alsamixer$" }, 1)
float_rule("qalculate", { title = "^Qalculate!$" })
float_rule("psensor", { class = "^Psensor$" })
float_rule("lxrandr", { title = "^lxrandr$" })
float_rule("blueman", { class = "^Blueman-manager$" })
float_rule("gparted", { class = "^GParted$" })
float_rule("pavucontrol", { class = "^pavucontrol$" })
float_rule("volume-control", { title = "^Volume Control$" })
float_rule("virtualbox", { class = ".*VirtualBox.*" })
hl.window_rule({ name = "virtualbox-manager-tiles", match = { class = ".*VirtualBox Manager.*" }, tile = true })
hl.window_rule({ name = "virtualbox-machine-tiles", match = { class = ".*VirtualBox Machine.*" }, tile = true })
-- Send music clients to the existing scratchpad without stealing startup focus.
hl.window_rule({ name = "spotify-music", match = { class = "^Spotify$" }, workspace = "special:scratchpad silent", no_initial_focus = true })
hl.window_rule({ name = "deezer-music", match = { class = "^Deezer$" }, workspace = "special:scratchpad silent", no_initial_focus = true })
hl.on("hyprland.start", function()
for _, command in ipairs(machine.startup or {}) do
hl.exec_cmd(command)
end
end)
@@ -0,0 +1,21 @@
-- Desktop-only display and startup profile.
return {
monitors = {
{
output = "HDMI-A-1",
mode = "3840x2160@120",
position = "0x0",
scale = 1,
},
},
startup = {
"noctalia",
-- Electron's tray icon registers only when the SNI watcher already exists.
"sh -c 'sleep 2; exec mullvad-vpn'",
"thunderbird",
"nextcloud",
"spotify",
"brave",
"alacritty -e tmux",
},
}
@@ -0,0 +1,11 @@
-- Laptop profile: leave monitor selection to Hyprland's hotplug support.
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
return {
monitors = {},
startup = {
"noctalia",
-- Start after Noctalia has claimed the StatusNotifier watcher.
"sh -c 'sleep 2; exec mullvad-vpn'",
"nextcloud",
},
}
+12
View File
@@ -0,0 +1,12 @@
# Shared Noctalia defaults. Keep GUI experiments in state; promote durable choices here.
[shell]
setup_wizard_enabled = false
offline_mode = true
external_ip_enabled = false
telemetry_enabled = false
[theme]
mode = "dark"
source = "custom"
custom_palette = "Ember OLED"
pure_black_dark = true
@@ -0,0 +1,110 @@
# Desktop-specific shell layout and hardware telemetry.
[bar.main]
position = "bottom"
layer = "overlay"
capsule_radius = 5
center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"]
font_weight = 400
margin_ends = 0
padding = 0
panel_overlap = 3
radius = 0
shadow = false
smart_auto_hide = true
# The bar may overlay the thin perimeter while visible; hidden bars release the space.
reserve_space = false
start = ["tray", "group:g4", "group:g2", "privacy"]
thickness = 39
widget_spacing = 10
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g1"
members = ["caffeine", "date", "clock"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g2"
members = ["network_rx", "network_tx", "network"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g3"
members = ["launcher", "media", "control-center", "clipboard"]
opacity = 1.0
padding = 6.0
radius = 5.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g4"
members = ["cpu", "temp", "ram", "gpu", "gpu_temp", "disk", "battery"]
opacity = 1.0
padding = 6.0
[widget.cpu]
type = "alex/system-status:cpu"
[widget.ram]
type = "alex/system-status:memory"
[widget.temp]
type = "sysmon"
stat = "cpu_temp"
display = "text"
[widget.gpu]
type = "sysmon"
stat = "gpu_usage"
display = "graph"
[widget.gpu_temp]
type = "sysmon"
stat = "gpu_temp"
display = "text"
[widget.disk]
type = "sysmon"
stat = "disk_pct"
display = "text"
path = "/"
[battery.device."/org/freedesktop/UPower/devices/battery_hidpp_battery_0"]
warning_threshold = 20
[control_center]
sidebar = "full"
sidebar_section = "full"
width = 880
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[system.monitor]
# GPU polling is disabled by Noctalia by default; enable it for the RTX 3090 widgets.
gpu_poll_seconds = 2.0
[plugins]
enabled = ["alex/system-status"]
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
@@ -0,0 +1,93 @@
# Laptop-specific Noctalia layout. Shared privacy/theme defaults live in 10-base.toml.
[bar.main]
position = "top"
layer = "overlay"
capsule_radius = 5
center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"]
font_weight = 400
margin_ends = 0
padding = 0
panel_overlap = 3
radius = 0
shadow = false
auto_hide = false
smart_auto_hide = false
reserve_space = true
start = ["tray", "group:g4", "group:g2", "privacy"]
thickness = 39
widget_spacing = 10
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g1"
members = ["caffeine", "date", "clock"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g2"
members = ["network_rx", "network_tx", "network"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g3"
members = ["launcher", "media", "control-center", "clipboard"]
opacity = 1.0
padding = 6.0
radius = 5.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g4"
members = ["cpu", "temp", "ram", "disk", "battery"]
opacity = 1.0
padding = 6.0
[widget.cpu]
type = "alex/system-status:cpu"
[widget.ram]
type = "alex/system-status:memory"
[widget.temp]
type = "sysmon"
stat = "cpu_temp"
display = "text"
[widget.disk]
type = "sysmon"
stat = "disk_pct"
display = "text"
path = "/"
[control_center]
sidebar = "full"
sidebar_section = "full"
width = 720
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[plugins]
enabled = ["alex/system-status"]
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
+48
View File
@@ -0,0 +1,48 @@
{
"dark": {
"mPrimary": "#ff8800",
"mOnPrimary": "#1a0d00",
"mSecondary": "#ffb05c",
"mOnSecondary": "#2b1600",
"mTertiary": "#ffcc99",
"mOnTertiary": "#2b1600",
"mError": "#ff6b4a",
"mOnError": "#2b0700",
"mSurface": "#050403",
"mOnSurface": "#fff1e6",
"mSurfaceVariant": "#1a110c",
"mOnSurfaceVariant": "#f4c7a1",
"mOutline": "#6e4a31",
"mShadow": "#000000",
"mHover": "#2a190f",
"mOnHover": "#fff1e6",
"terminal": {
"background": "#050403",
"foreground": "#fff1e6",
"cursor": "#fff1e6",
"cursorText": "#050403",
"selectionBg": "#3b220f",
"selectionFg": "#fff1e6",
"normal": {
"black": "#050403",
"red": "#ff6b4a",
"green": "#c98a45",
"yellow": "#ffb05c",
"blue": "#c97c45",
"magenta": "#d98a5c",
"cyan": "#e8a76a",
"white": "#fff1e6"
},
"bright": {
"black": "#6e4a31",
"red": "#ff8f72",
"green": "#e8ad68",
"yellow": "#ffca80",
"blue": "#e8a06a",
"magenta": "#f0ad82",
"cyan": "#ffd0a0",
"white": "#ffffff"
}
}
}
}
@@ -0,0 +1,59 @@
-- Sum each logical CPU's interval utilization. Unlike the built-in average,
-- this follows Unix tools by allowing the value to exceed 100%.
local command = [[awk '
/^cpu[0-9]+ / {
total = 0
for (i = 2; i <= NF; i++) total += $i
idle = $5 + $6
printf "%s %.0f %.0f\n", $1, total, idle
}' /proc/stat]]
local previous = {}
local request_in_flight = false
function update()
noctalia.setUpdateInterval(2000)
if request_in_flight then
return
end
request_in_flight = true
noctalia.runAsync(command, function(result)
request_in_flight = false
if result.exitCode ~= 0 then
barWidget.setText("CPU —")
return
end
local utilization = 0
local sampled = 0
for line in string.gmatch(result.stdout, "[^\r\n]+") do
local name, total, idle = string.match(line, "^(cpu%d+) (%d+) (%d+)$")
total = tonumber(total)
idle = tonumber(idle)
local prior = previous[name]
if prior ~= nil then
local total_delta = total - prior.total
local idle_delta = idle - prior.idle
if total_delta > 0 then
utilization = utilization + (100 * (total_delta - idle_delta) / total_delta)
sampled = sampled + 1
end
end
previous[name] = { total = total, idle = idle }
end
barWidget.setGlyph("cpu")
if sampled > 0 then
barWidget.setText(string.format("%.0f%%", utilization))
barWidget.setTooltip("Aggregate CPU utilization (100% per logical CPU)")
else
barWidget.setText("")
barWidget.setTooltip("Sampling CPU utilization…")
end
end)
end
@@ -0,0 +1,43 @@
-- Read the tiny kernel meminfo pseudo-file every five seconds. This avoids a
-- resident helper process and reuses Noctalia's lightweight widget lifecycle.
local command = [[awk '
$1 == "MemTotal:" { total = $2 }
$1 == "MemAvailable:" { available = $2 }
$1 == "SwapTotal:" { swap_total = $2 }
$1 == "SwapFree:" { swap_free = $2 }
END {
if (!total || !available) exit 1
used = total - available
if (swap_total > 0) {
swap_used = (swap_total - swap_free) * 100 / swap_total
printf "RAM %.1f/%.1f GiB S %.0f%%\n", used / 1048576, total / 1048576, swap_used
} else {
printf "RAM %.1f/%.1f GiB S —\n", used / 1048576, total / 1048576
}
}' /proc/meminfo]]
local request_in_flight = false
function update()
noctalia.setUpdateInterval(5000)
if request_in_flight then
return
end
request_in_flight = true
noctalia.runAsync(command, function(result)
request_in_flight = false
if result.exitCode ~= 0 then
barWidget.setText("RAM —")
barWidget.setTooltip("Unable to read /proc/meminfo")
return
end
local text = string.gsub(result.stdout, "%s+$", "")
barWidget.setGlyph("memory")
barWidget.setText(text)
barWidget.setTooltip("RAM used / total · S = swap used")
end)
end
@@ -0,0 +1,14 @@
id = "alex/system-status"
name = "System Status"
version = "1.0.0"
plugin_api = 3
author = "alex"
description = "Low-overhead memory and swap indicator for the bar."
[[widget]]
id = "cpu"
entry = "cpu.luau"
[[widget]]
id = "memory"
entry = "memory.luau"