diff --git a/.config/ashell/config.toml##class.arch_desktop b/.config/ashell/config.toml##class.arch_desktop new file mode 100644 index 0000000..2401e44 --- /dev/null +++ b/.config/ashell/config.toml##class.arch_desktop @@ -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 diff --git a/.config/hypr/hyprland.lua b/.config/hypr/hyprland.lua new file mode 100644 index 0000000..ff5ee49 --- /dev/null +++ b/.config/hypr/hyprland.lua @@ -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) diff --git a/.config/hypr/machine.lua##class.arch_desktop b/.config/hypr/machine.lua##class.arch_desktop new file mode 100644 index 0000000..7456606 --- /dev/null +++ b/.config/hypr/machine.lua##class.arch_desktop @@ -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", + }, +} diff --git a/.config/hypr/machine.lua##class.arch_laptop b/.config/hypr/machine.lua##class.arch_laptop new file mode 100644 index 0000000..9bfa437 --- /dev/null +++ b/.config/hypr/machine.lua##class.arch_laptop @@ -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", + }, +} diff --git a/.config/noctalia/10-base.toml b/.config/noctalia/10-base.toml new file mode 100644 index 0000000..1a5084a --- /dev/null +++ b/.config/noctalia/10-base.toml @@ -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 diff --git a/.config/noctalia/90-machine.toml##class.arch_desktop b/.config/noctalia/90-machine.toml##class.arch_desktop new file mode 100644 index 0000000..1b0b618 --- /dev/null +++ b/.config/noctalia/90-machine.toml##class.arch_desktop @@ -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 diff --git a/.config/noctalia/90-machine.toml##class.arch_laptop b/.config/noctalia/90-machine.toml##class.arch_laptop new file mode 100644 index 0000000..8679416 --- /dev/null +++ b/.config/noctalia/90-machine.toml##class.arch_laptop @@ -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 diff --git a/.config/noctalia/palettes/Ember OLED.json b/.config/noctalia/palettes/Ember OLED.json new file mode 100644 index 0000000..1ac8dbc --- /dev/null +++ b/.config/noctalia/palettes/Ember OLED.json @@ -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" + } + } + } +} diff --git a/.local/share/noctalia/plugins/system-status/cpu.luau b/.local/share/noctalia/plugins/system-status/cpu.luau new file mode 100644 index 0000000..0e5a89e --- /dev/null +++ b/.local/share/noctalia/plugins/system-status/cpu.luau @@ -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 diff --git a/.local/share/noctalia/plugins/system-status/memory.luau b/.local/share/noctalia/plugins/system-status/memory.luau new file mode 100644 index 0000000..f1bcc88 --- /dev/null +++ b/.local/share/noctalia/plugins/system-status/memory.luau @@ -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 diff --git a/.local/share/noctalia/plugins/system-status/plugin.toml b/.local/share/noctalia/plugins/system-status/plugin.toml new file mode 100644 index 0000000..ec92f9d --- /dev/null +++ b/.local/share/noctalia/plugins/system-status/plugin.toml @@ -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"