Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally collapsed to a single commit; this repo mirrors only the latest state.
1017 lines
37 KiB
Nix
1017 lines
37 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
|
|
let
|
|
palette = import ./palette.nix;
|
|
in
|
|
# Per-user Hyprland + QuickShell configuration (home-manager).
|
|
# This is the Hyprland counterpart to home-manager/modules/desktop-user.nix —
|
|
# import this INSTEAD of desktop-user.nix when the user runs Hyprland.
|
|
{
|
|
imports = [
|
|
./kitty.nix
|
|
./firefox.nix
|
|
./tools/just.nix
|
|
];
|
|
|
|
home.activation.linkOnlyOfficeFonts = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
FONT_DIR="/run/current-system/sw/share/X11/fonts"
|
|
USER_FONTS="$HOME/.local/share/fonts"
|
|
NATIVE_TARGET="$HOME/.local/share/onlyoffice/desktopeditors/data/fonts"
|
|
FLATPAK_TARGET="$HOME/.var/app/org.onlyoffice.desktopeditors/data/onlyoffice/desktopeditors/data/fonts"
|
|
|
|
if [ -d "$FONT_DIR" ]; then
|
|
for TARGET_DIR in "$USER_FONTS" "$NATIVE_TARGET" "$FLATPAK_TARGET"; do
|
|
$DRY_RUN_CMD mkdir -p "$TARGET_DIR"
|
|
$DRY_RUN_CMD rm -f "$TARGET_DIR/AllFonts.js"* "$TARGET_DIR/fonts_thumbnail"* "$TARGET_DIR/font_selection.bin" "$TARGET_DIR/fonts.log"
|
|
$DRY_RUN_CMD cp -Lf "$FONT_DIR"/*.ttf "$FONT_DIR"/*.otf "$FONT_DIR"/*.ttc "$TARGET_DIR/" 2>/dev/null || true
|
|
done
|
|
fi
|
|
'';
|
|
|
|
# VS Code (same extensions set as the GNOME desktops).
|
|
programs.vscode = {
|
|
enable = true;
|
|
package = pkgs.vscode;
|
|
profiles.default = {
|
|
extensions =
|
|
with pkgs.vscode-extensions;
|
|
[
|
|
bbenoist.nix
|
|
ms-python.python
|
|
dart-code.flutter
|
|
]
|
|
++ [
|
|
(pkgs.vscode-utils.extensionFromVscodeMarketplace {
|
|
name = "opencode-go-for-copilot";
|
|
publisher = "DenizhanDaklr";
|
|
version = "0.1.18";
|
|
sha256 = "0igdkgv6dra3zpd80lhwymdh216dqn8m8kqwsxh258wyk9fzgfk0";
|
|
})
|
|
];
|
|
};
|
|
};
|
|
|
|
fonts.fontconfig.enable = true;
|
|
|
|
# XDG Desktop Portal backend config (per-user scope; silences the
|
|
# xdg-desktop-portal >= 1.17 loader warning in this HM profile).
|
|
xdg.portal.config.common.default = "hyprland";
|
|
|
|
home.packages = with pkgs; [
|
|
# Wayland user tools (screen recording)
|
|
wf-recorder
|
|
# QuickShell sci-fi theme fonts: Orbitron (display/clock) + Nerd Font
|
|
# symbol glyphs for status icons (wifi/battery/volume).
|
|
orbitron
|
|
nerd-fonts.symbols-only
|
|
];
|
|
|
|
# ============================ Hyprland ============================
|
|
wayland.windowManager.hyprland = {
|
|
enable = true;
|
|
# Hyprland 0.55+ uses Lua configs; hyprlang (.conf) is deprecated and
|
|
# will be removed in a future release.
|
|
configType = "lua";
|
|
package = null; # use the NixOS-system Hyprland (programs.hyprland)
|
|
systemd.enable = true;
|
|
systemd.variables = [ "QT_QPA_PLATFORM" ];
|
|
|
|
# All configuration lives in extraConfig as raw Lua — this matches the
|
|
# upstream Hyprland Lua API exactly and is easier to maintain than
|
|
# fighting Home Manager's settings abstraction for binds / rules.
|
|
settings = { };
|
|
extraConfig = ''
|
|
-- Auto-detect monitor
|
|
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 })
|
|
|
|
-- Environment variables
|
|
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
|
|
hl.env("XDG_SESSION_TYPE", "wayland")
|
|
hl.env("XDG_SESSION_DESKTOP", "Hyprland")
|
|
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
|
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
|
hl.env("XCURSOR_SIZE", "24")
|
|
hl.env("HYPRCURSOR_SIZE", "24")
|
|
|
|
-- Autostart
|
|
hl.on("hyprland.start", function()
|
|
hl.exec_cmd("quickshell")
|
|
hl.exec_cmd("hyprpaper")
|
|
-- Re-apply the wallpaper chosen in the SUPER+CTRL+A picker. hyprpaper
|
|
-- may not have its control socket up yet; qs-wall apply retries.
|
|
hl.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-wall apply'")
|
|
hl.exec_cmd("hypridle")
|
|
-- Launch the user's chosen autostart apps (toggled from the gear
|
|
-- quick-settings panel; qs-apps guards against double-starts).
|
|
hl.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-apps run'")
|
|
end)
|
|
|
|
-- General configuration
|
|
hl.config({
|
|
general = {
|
|
gaps_in = 6,
|
|
gaps_out = 12,
|
|
border_size = 2,
|
|
col = {
|
|
active_border = { colors = { "${palette.hyprRgba palette.blue "ee"}", "${palette.hyprRgba palette.neon "ee"}" }, angle = 45 },
|
|
inactive_border = "${palette.hyprRgba palette.bgDark "66"}",
|
|
},
|
|
layout = "dwindle",
|
|
resize_on_border = true,
|
|
},
|
|
decoration = {
|
|
rounding = 12,
|
|
blur = {
|
|
enabled = true,
|
|
size = 12,
|
|
passes = 4,
|
|
vibrancy = 0.2,
|
|
noise = 0.01,
|
|
popups = true,
|
|
},
|
|
shadow = {
|
|
enabled = true,
|
|
range = 16,
|
|
render_power = 3,
|
|
color = "${palette.hyprRgba palette.ink "66"}",
|
|
scale = 1.0,
|
|
},
|
|
active_opacity = 1.0,
|
|
inactive_opacity = 0.9,
|
|
},
|
|
animations = {
|
|
enabled = true,
|
|
},
|
|
input = {
|
|
kb_layout = "gb",
|
|
follow_mouse = 1,
|
|
sensitivity = -0.1,
|
|
touchpad = {
|
|
natural_scroll = true,
|
|
tap_to_click = true,
|
|
-- int 0..2 in 0.56 (no drag_lock_threshold key); 1 = lifting the
|
|
-- finger mid-drag keeps the drag alive (laptop text-selection QoL)
|
|
drag_lock = 1,
|
|
},
|
|
},
|
|
misc = {
|
|
-- no Hyprland logo / anime mascot on empty desktops (hyprpaper
|
|
-- paints immediately, but this kills the startup flash)
|
|
disable_hyprland_logo = true,
|
|
force_default_wallpaper = 0,
|
|
},
|
|
})
|
|
|
|
-- Scratchpad workspace: in the Lua config, workspace rules are set
|
|
-- via hl.workspace_rule (the top-level `workspace = ...` keyword from
|
|
-- hyprlang isn't a parser-level statement in 0.56.2). The
|
|
-- on_created_empty hook spawns a terminal in the special when it
|
|
-- opens empty; SUPER+` (toggle_special) reveals/hides it.
|
|
hl.workspace_rule({
|
|
workspace = "special:term",
|
|
on_created_empty = "kitty",
|
|
})
|
|
|
|
|
|
-- Animation curves (upstream example set + our custom bezier)
|
|
hl.curve("myBezier", { type = "bezier", points = { {0.05, 0.9}, {0.1, 1.05} } })
|
|
hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } })
|
|
hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1} } })
|
|
hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } })
|
|
|
|
-- Animations
|
|
hl.animation({ leaf = "windows", enabled = true, speed = 5, bezier = "myBezier" })
|
|
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.5, bezier = "easeOutQuint", style = "popin 87%" })
|
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "default", style = "popin 80%" })
|
|
hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" })
|
|
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" })
|
|
hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "quick" })
|
|
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
|
|
-- Gradient border: animate the 45° active_border angle on focus change
|
|
-- (style=once plays per focus switch; loop would burn refresh-rate
|
|
-- frames continuously and hurt battery on this laptop).
|
|
hl.animation({ leaf = "borderangle", enabled = true, speed = 3, bezier = "easeOutQuint", style = "once" })
|
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "default", style = "slidefade" })
|
|
|
|
-- Keybindings
|
|
local mod = "SUPER"
|
|
|
|
hl.bind(mod .. " + Return", hl.dsp.exec_cmd("kitty"))
|
|
hl.bind(mod .. " + Q", hl.dsp.window.close())
|
|
hl.bind(mod .. " + E", hl.dsp.exec_cmd("thunar"))
|
|
hl.bind(mod .. " + Space", hl.dsp.exec_cmd("wofi --show drun"))
|
|
-- Extended launcher modes (qs-launch → wofi): emoji picker, calculator
|
|
-- (qalc), DuckDuckGo search. Full .local/bin path: ~/.local/bin is not
|
|
-- on the Hyprland session PATH (same as the qs-wall/qs-apps autostart).
|
|
hl.bind(mod .. " + SHIFT + E", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-launch emoji'"))
|
|
hl.bind(mod .. " + SHIFT + C", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-launch calc'"))
|
|
hl.bind(mod .. " + SHIFT + D", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-launch search'"))
|
|
hl.bind(mod .. " + F", hl.dsp.window.fullscreen())
|
|
-- Float toggle + clamp into the monitor work-area + bring to front.
|
|
-- The QuickShell bar is a Top layer surface, always drawn above regular
|
|
-- windows, so a floating window that keeps fullscreen geometry tucks its
|
|
-- title bar/menus under the bar. Clamping into the reserved work-area
|
|
-- (below the bar) keeps the whole window visible.
|
|
hl.bind(mod .. " + V", function()
|
|
local wasFloating = (hl.get_active_window() or {}).floating == true
|
|
hl.dispatch(hl.dsp.window.float({ action = "toggle" }))
|
|
local w = hl.get_active_window()
|
|
if w ~= nil and w.floating and not wasFloating then
|
|
local m = w.monitor
|
|
if m ~= nil then
|
|
local wa = {
|
|
x = m.x + m.reserved.left,
|
|
y = m.y + m.reserved.top,
|
|
w = m.width - m.reserved.left - m.reserved.right,
|
|
h = m.height - m.reserved.top - m.reserved.bottom,
|
|
}
|
|
local x, y = w.at.x, w.at.y
|
|
local ww, wh = w.size.x, w.size.y
|
|
-- keep the window from sticking out below/right of the bar
|
|
if y + wh > wa.y + wa.h then wh = wa.y + wa.h - y end
|
|
if x + ww > wa.x + wa.w then ww = wa.x + wa.w - x end
|
|
-- and never let it ride up under the bar
|
|
if y < wa.y then
|
|
y = wa.y
|
|
if y + wh > wa.y + wa.h then wh = wa.y + wa.h - y end
|
|
end
|
|
if ww ~= w.size.x or wh ~= w.size.y then
|
|
hl.dispatch(hl.dsp.window.resize({ x = ww, y = wh }))
|
|
end
|
|
if x ~= w.at.x or y ~= w.at.y then
|
|
hl.dispatch(hl.dsp.window.move({ x = x, y = y }))
|
|
end
|
|
end
|
|
end
|
|
hl.dispatch(hl.dsp.window.bring_to_top())
|
|
end)
|
|
hl.bind(mod .. " + P", hl.dsp.window.pseudo())
|
|
hl.bind(mod .. " + M", hl.dsp.exit())
|
|
|
|
hl.bind(mod .. " + left", hl.dsp.focus({ direction = "left" }))
|
|
hl.bind(mod .. " + right", hl.dsp.focus({ direction = "right" }))
|
|
hl.bind(mod .. " + up", hl.dsp.focus({ direction = "up" }))
|
|
hl.bind(mod .. " + down", hl.dsp.focus({ direction = "down" }))
|
|
hl.bind(mod .. " + H", hl.dsp.focus({ direction = "left" }))
|
|
hl.bind(mod .. " + L", hl.dsp.focus({ direction = "right" }))
|
|
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" }))
|
|
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" }))
|
|
|
|
for i = 1, 9 do
|
|
hl.bind(mod .. " + " .. i, hl.dsp.focus({ workspace = i }))
|
|
hl.bind(mod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = i }))
|
|
end
|
|
|
|
hl.bind("Print", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-shot pick'"))
|
|
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-shot area'"))
|
|
hl.bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("sh -c '${config.home.homeDirectory}/.local/bin/qs-shot screen'"))
|
|
hl.bind(mod .. " + L", hl.dsp.exec_cmd("hyprlock"))
|
|
hl.bind(mod .. " + SHIFT + Q", hl.dsp.exec_cmd("wlogout"))
|
|
|
|
-- App switching + clipboard history + keybind cheatsheet + notif center + ws overview
|
|
-- (QuickShell popups via IPC)
|
|
-- Classic alt-tab window cycling (next) + bring to top, alt-shift-tab backwards.
|
|
hl.bind("ALT + Tab", function()
|
|
hl.dispatch(hl.dsp.window.cycle_next())
|
|
hl.dispatch(hl.dsp.window.bring_to_top())
|
|
end)
|
|
hl.bind("ALT + SHIFT + Tab", function()
|
|
hl.dispatch(hl.dsp.window.cycle_next({ direction = "prev" }))
|
|
hl.dispatch(hl.dsp.window.bring_to_top())
|
|
end)
|
|
hl.bind(mod .. " + Tab", hl.dsp.exec_cmd("wofi --show window"))
|
|
hl.bind(mod .. " + W", hl.dsp.exec_cmd("qs ipc call ws toggle"))
|
|
hl.bind(mod .. " + CTRL + V", hl.dsp.exec_cmd("qs ipc call clip toggle"))
|
|
hl.bind(mod .. " + CTRL + H", hl.dsp.exec_cmd("qs ipc call help toggle"))
|
|
hl.bind(mod .. " + CTRL + N", hl.dsp.exec_cmd("qs ipc call notif toggle"))
|
|
hl.bind(mod .. " + CTRL + C", hl.dsp.exec_cmd("qs ipc call cal toggle"))
|
|
hl.bind(mod .. " + CTRL + A", hl.dsp.exec_cmd("qs ipc call wall toggle"))
|
|
-- tablet on-screen keyboard toggle (wvkbd; see hyprland-tablet). Not a
|
|
-- qs IPC call because the keyboard is a Wayland surface, not Quickshell.
|
|
hl.bind(mod .. " + CTRL + K", hl.dsp.exec_cmd("hyprland-tablet keyboard-toggle"))
|
|
|
|
-- Scratchpad terminal (special workspace that follows you around)
|
|
hl.bind(mod .. " + grave", hl.dsp.workspace.toggle_special("term"))
|
|
hl.bind(mod .. " + SHIFT + grave", hl.dsp.window.move({ workspace = "special:term" }))
|
|
-- Pull the focused window OUT of the scratchpad back to the main
|
|
-- workspace. Open the scratchpad first (SUPER+grave) so the
|
|
-- scratchpad's window has focus, then SUPER+CTRL+grave ejects it.
|
|
hl.bind(mod .. " + CTRL + grave", hl.dsp.window.move({ workspace = "1" }))
|
|
|
|
-- Laptop media keys. locked = also work while hyprlock is up,
|
|
-- repeating = auto-repeat when held (volume ramps smoothly).
|
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("pamixer -i 5"), { locked = true, repeating = true })
|
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("pamixer -d 5"), { locked = true, repeating = true })
|
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pamixer -t"), { locked = true })
|
|
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("pamixer --default-source -t"), { locked = true })
|
|
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
|
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl 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 })
|
|
-- brightness changes are not observable from QML, so the key command
|
|
-- itself pokes the QuickShell OSD via IPC after setting the level.
|
|
-- NB function is named "bright" not "show" — the qs CLI treats "show"
|
|
-- as its own subcommand token and never reaches the handler.
|
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("sh -c 'brightnessctl set 5%+ && qs ipc call osd bright'"), { locked = true, repeating = true })
|
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("sh -c 'brightnessctl set 5%- && qs ipc call osd bright'"), { locked = true, repeating = true })
|
|
|
|
-- Resize submap: SUPER+R enters, arrows/HJKL resize the focused
|
|
-- window, Escape (or any reset) leaves. NOTE: 0.56.2's key parser only
|
|
-- splits on '+', so one key per hl.bind call (no "left, h" lists).
|
|
hl.define_submap("resize", function()
|
|
local function rsz(dx, dy, keys)
|
|
for _, k in ipairs(keys) do
|
|
hl.bind(k, hl.dsp.window.resize({ x = dx, y = dy, relative = true }))
|
|
end
|
|
end
|
|
rsz(-20, 0, { "left", "h" })
|
|
rsz(20, 0, { "right", "l" })
|
|
rsz(0, -20, { "up", "k" })
|
|
rsz(0, 20, { "down", "j" })
|
|
hl.bind("Escape", hl.dsp.submap("reset"))
|
|
end)
|
|
hl.bind(mod .. " + R", hl.dsp.submap("resize"))
|
|
|
|
-- Smart gaps: w[tv1] is a gapless "watch" workspace; parking a window
|
|
-- there (SUPER+CTRL+F) gives borderless fullscreen-ish video without
|
|
-- actually fullscreening. f[1] kills gaps under any real fullscreen.
|
|
hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 })
|
|
hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 })
|
|
hl.window_rule({ name = "no-gaps-wtv1", match = { float = false, workspace = "w[tv1]" }, border_size = 0, rounding = 0 })
|
|
hl.window_rule({ name = "no-gaps-f1", match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 })
|
|
|
|
local gapless = false
|
|
hl.bind(mod .. " + CTRL + F", function()
|
|
if gapless then
|
|
hl.dispatch(hl.dsp.window.move({ workspace = "previous" }))
|
|
else
|
|
hl.dispatch(hl.dsp.window.move({ workspace = "w[tv1]" }))
|
|
end
|
|
gapless = not gapless
|
|
end)
|
|
|
|
-- Flash focus: briefly override the border colour on window switch and
|
|
-- revert via a oneshot timer (hyprfocus as pure Lua, no plugin).
|
|
-- NB 0.56 set_prop accepts "active_border_color"/"inactive_border_color"
|
|
-- (NOT "bordercol"/"border_color" — the latter is a rule-effect name
|
|
-- only). Reverting to the config default colour is visually identical
|
|
-- to clearing the override, since the inactive border reads a
|
|
-- separate, un-overridden slot.
|
|
hl.on("window.active", function(w)
|
|
if w == nil then return end
|
|
hl.dispatch(hl.dsp.window.set_prop({ prop = "active_border_color", value = "rgba(00e5ffff)" }))
|
|
hl.timer(function()
|
|
hl.dispatch(hl.dsp.window.set_prop({ prop = "active_border_color", value = "rgba(7aa2f7ee)" }))
|
|
end, { timeout = 180, type = "oneshot" })
|
|
end)
|
|
|
|
-- Night light (blue-light filter) toggle: hyprsunset keeps the colour
|
|
-- temperature property alive while running; killing it restores normal.
|
|
local nightLight = false
|
|
hl.bind(mod .. " + SHIFT + N", function()
|
|
if nightLight then
|
|
hl.exec_cmd("pkill hyprsunset")
|
|
else
|
|
hl.exec_cmd("hyprsunset -t 4500")
|
|
end
|
|
nightLight = not nightLight
|
|
end)
|
|
|
|
-- Mouse binds
|
|
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
|
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
|
|
|
-- Window rules
|
|
hl.window_rule({ match = { class = "org.pulseaudio.pavucontrol" }, float = true })
|
|
hl.window_rule({ match = { title = "Picture%-in%-Picture" }, float = true })
|
|
hl.window_rule({ match = { title = "Firefox %-%- Picture%-in%-Picture" }, float = true })
|
|
|
|
-- Layer rules (frosted-glass blur for the QuickShell bar)
|
|
hl.layer_rule({ match = { namespace = "qs%-neon%-bar" }, blur = true })
|
|
hl.layer_rule({ match = { namespace = "qs%-neon%-bar" }, ignore_alpha = 0.3 })
|
|
'';
|
|
};
|
|
|
|
# ============================ QuickShell ============================
|
|
# QuickShell looks for ~/.config/quickshell/shell.qml by default.
|
|
# The pinned-flake info placeholder (@FLAKEINFO@ in the QML) is resolved at
|
|
# build time from flake.lock so the stats popup's system/badge chip shows
|
|
# the nixpkgs rev/date without needing network access at runtime. The
|
|
# @THEMECOLORS@ placeholder carries the build-time shared palette so the
|
|
# bar never flashes an unthemed frame before the wallpaper palette applies.
|
|
xdg.configFile."quickshell/shell.qml".text =
|
|
builtins.replaceStrings
|
|
[ "@FLAKEINFO@" "@THEMECOLORS@" ]
|
|
[
|
|
(builtins.toJSON (
|
|
let
|
|
lock = builtins.fromJSON (builtins.readFile ../../flake.lock);
|
|
nixpkgs = lock.nodes.nixpkgs.locked;
|
|
in
|
|
{
|
|
rev = nixpkgs.rev;
|
|
lastModified = nixpkgs.lastModified;
|
|
repo = nixpkgs.repo;
|
|
owner = nixpkgs.owner;
|
|
}
|
|
))
|
|
(builtins.toJSON {
|
|
glass = palette.hash palette.glass;
|
|
glassPanel = palette.hash palette.glassPanel;
|
|
surface = palette.hash palette.surface;
|
|
line = palette.hash palette.line;
|
|
text = palette.hash palette.text;
|
|
muted = palette.hash palette.muted;
|
|
ink = palette.hash palette.ink;
|
|
neon = palette.hash palette.neon;
|
|
violet = palette.hash palette.violet;
|
|
magenta = palette.hash palette.magenta;
|
|
danger = palette.hash palette.danger;
|
|
})
|
|
]
|
|
(builtins.readFile ./quickshell-shell.qml);
|
|
# Same-directory sibling component used by shell.qml (implicit QML import).
|
|
xdg.configFile."quickshell/SciSlider.qml".text = builtins.readFile ./quickshell-slider.qml;
|
|
# Keybind cheatsheet data rendered by the SUPER+CTRL+H help popup. Keep in
|
|
# sync with the hl.bind() list in extraConfig above.
|
|
xdg.configFile."quickshell/keybinds.json".text = builtins.toJSON [
|
|
{
|
|
cat = "SESSION";
|
|
items = [
|
|
{
|
|
keys = "SUPER + RETURN";
|
|
desc = "terminal (kitty)";
|
|
}
|
|
{
|
|
keys = "SUPER + SPACE";
|
|
desc = "app launcher (wofi)";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + E";
|
|
desc = "emoji picker";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + C";
|
|
desc = "calculator (qalc)";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + D";
|
|
desc = "DuckDuckGo search";
|
|
}
|
|
{
|
|
keys = "Print";
|
|
desc = "screenshot (area or full screen)";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + S";
|
|
desc = "screenshot (area)";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + R";
|
|
desc = "screenshot (full screen)";
|
|
}
|
|
{
|
|
keys = "SUPER + E";
|
|
desc = "file manager (thunar)";
|
|
}
|
|
{
|
|
keys = "ALT + TAB";
|
|
desc = "switch between windows (hold ALT, keep pressing TAB)";
|
|
}
|
|
{
|
|
keys = "ALT + SHIFT + TAB";
|
|
desc = "switch backwards through windows";
|
|
}
|
|
{
|
|
keys = "SUPER + TAB";
|
|
desc = "window switcher (list)";
|
|
}
|
|
{
|
|
keys = "SUPER + GRAVE";
|
|
desc = "scratchpad terminal (kitty auto-spawns); toggle";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + GRAVE";
|
|
desc = "send window to scratchpad";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + GRAVE";
|
|
desc = "pull window from scratchpad to main workspace (open scratchpad first)";
|
|
}
|
|
{
|
|
keys = "SUPER + L";
|
|
desc = "lock screen";
|
|
}
|
|
{
|
|
keys = "SUPER + M";
|
|
desc = "exit hyprland";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + Q";
|
|
desc = "power / logout menu";
|
|
}
|
|
];
|
|
}
|
|
{
|
|
cat = "WINDOWS";
|
|
items = [
|
|
{
|
|
keys = "SUPER + Q";
|
|
desc = "close window";
|
|
}
|
|
{
|
|
keys = "SUPER + ARROWS / H K L J";
|
|
desc = "focus neighbouring window";
|
|
}
|
|
{
|
|
keys = "SUPER + F";
|
|
desc = "fullscreen";
|
|
}
|
|
{
|
|
keys = "SUPER + V";
|
|
desc = "toggle floating";
|
|
}
|
|
{
|
|
keys = "SUPER + P";
|
|
desc = "pseudotile";
|
|
}
|
|
{
|
|
keys = "SUPER + R";
|
|
desc = "resize mode (arrows/HJKL, esc exits)";
|
|
}
|
|
{
|
|
keys = "SUPER + LEFT DRAG";
|
|
desc = "move window";
|
|
}
|
|
{
|
|
keys = "SUPER + RIGHT DRAG";
|
|
desc = "resize window";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + F";
|
|
desc = "watch mode: gapless media workspace / return";
|
|
}
|
|
];
|
|
}
|
|
{
|
|
cat = "WORKSPACES";
|
|
items = [
|
|
{
|
|
keys = "SUPER + 1..9";
|
|
desc = "switch workspace";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + 1..9";
|
|
desc = "move window to workspace";
|
|
}
|
|
];
|
|
}
|
|
{
|
|
cat = "MEDIA & SYSTEM";
|
|
items = [
|
|
{
|
|
keys = "PRINT / SUPER + SHIFT + S";
|
|
desc = "screenshot area to clipboard";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + R";
|
|
desc = "screenshot screen to clipboard";
|
|
}
|
|
{
|
|
keys = "VOL UP / VOL DOWN / MUTE";
|
|
desc = "volume (neon OSD, works locked)";
|
|
}
|
|
{
|
|
keys = "MIC MUTE";
|
|
desc = "toggle microphone";
|
|
}
|
|
{
|
|
keys = "PLAY / NEXT / PREV";
|
|
desc = "media playback (MPRIS)";
|
|
}
|
|
{
|
|
keys = "BRIGHT UP / DOWN";
|
|
desc = "backlight (neon OSD)";
|
|
}
|
|
{
|
|
keys = "SUPER + SHIFT + N";
|
|
desc = "night light (blue-light filter)";
|
|
}
|
|
];
|
|
}
|
|
{
|
|
cat = "INTERFACE";
|
|
items = [
|
|
{
|
|
keys = "SUPER + CTRL + V";
|
|
desc = "clipboard history (pick copies + pastes)";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + H";
|
|
desc = "this cheatsheet";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + N";
|
|
desc = "notification center (history, DND, clear)";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + C";
|
|
desc = "calendar + weather (Nextcloud events, Open-Meteo)";
|
|
}
|
|
{
|
|
keys = "SUPER + W";
|
|
desc = "workspace overview (jump to any workspace)";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + A";
|
|
desc = "wallpaper picker (arrows preview live, click to apply)";
|
|
}
|
|
{
|
|
keys = "SUPER + CTRL + K";
|
|
desc = "tablet on-screen keyboard toggle (wvkbd)";
|
|
}
|
|
{
|
|
keys = "GEAR button";
|
|
desc = "quick settings: volume, brightness, profile, keep-awake, power";
|
|
}
|
|
{
|
|
keys = "SPEAKER icon";
|
|
desc = "volume slider popup";
|
|
}
|
|
{
|
|
keys = "WI-FI button";
|
|
desc = "network menu";
|
|
}
|
|
{
|
|
keys = "BLUETOOTH button";
|
|
desc = "bluetooth devices (power, pair, connect, scan)";
|
|
}
|
|
{
|
|
keys = "BELL button";
|
|
desc = "notification center; MUTE icon means DND is on";
|
|
}
|
|
{
|
|
keys = "COFFEE icon";
|
|
desc = "keep-awake is on (no lock/suspend)";
|
|
}
|
|
];
|
|
}
|
|
];
|
|
|
|
# Clipboard history: a systemd user service tees every clipboard change
|
|
# into cliphist; the QuickShell bar renders the picker (SUPER+CTRL+V via
|
|
# `qs ipc call clip toggle`). Defaults to hyprland-session.target via
|
|
# wayland.systemd.target (systemd.enable above).
|
|
services.cliphist.enable = true;
|
|
|
|
# ============================ Desktop bits (Wayland) ============================
|
|
|
|
# GTK theme + cursors (matching the GNOME hosts for consistency)
|
|
gtk = {
|
|
enable = true;
|
|
gtk4.theme = config.gtk.theme;
|
|
theme = {
|
|
name = "catppuccin-frappe-blue-standard";
|
|
package = pkgs.catppuccin-gtk;
|
|
};
|
|
iconTheme = {
|
|
name = "Papirus-Dark";
|
|
package = pkgs.papirus-icon-theme;
|
|
};
|
|
cursorTheme = {
|
|
name = "Bibata-Modern-Classic";
|
|
package = pkgs.bibata-cursors;
|
|
};
|
|
};
|
|
|
|
home.pointerCursor = {
|
|
enable = true;
|
|
name = "Bibata-Modern-Classic";
|
|
package = pkgs.bibata-cursors;
|
|
gtk.enable = true;
|
|
x11.enable = true;
|
|
};
|
|
|
|
# Wallpaper for hyprpaper (Scifi.jpg = user-provided sci-fi artwork;
|
|
# tokyo-night and neon-horizon kept as fallback assets).
|
|
home.file."Pictures/wallpapers/tokyo-night.png".source = ../../assets/wallpapers/tokyo-night.png;
|
|
home.file."Pictures/wallpapers/neon-horizon.png".source = ../../assets/wallpapers/neon-horizon.png;
|
|
home.file."Pictures/wallpapers/Scifi.jpg".source = ../../assets/wallpapers/Scifi.jpg;
|
|
# hyprpaper >= 0.8 uses a hyprlang special-category block per wallpaper
|
|
# (`wallpaper { monitor = ... }`); the legacy `preload/wallpaper = ,img`
|
|
# key-value lines are silently ignored ("monitor has no target"). `*` is the
|
|
# wildcard monitor name. Note: `preload` is no longer a config keyword.
|
|
xdg.configFile."hypr/hyprpaper.conf".text = ''
|
|
ipc = 1
|
|
splash = 0
|
|
|
|
wallpaper {
|
|
monitor = *
|
|
path = ${config.home.homeDirectory}/Pictures/wallpapers/Scifi.jpg
|
|
fit_mode = cover
|
|
}
|
|
'';
|
|
|
|
# Wallpaper picker (SUPER+CTRL+A): backend script + PATH wrapper. The state
|
|
# file (not hyprpaper.conf) records the user's choice; the login autostart
|
|
# above re-applies it after hyprpaper starts.
|
|
xdg.configFile."quickshell/qs-wall.py".text = builtins.readFile ./quickshell-wall.py;
|
|
home.file.".local/bin/qs-wall" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
exec ${pkgs.python3}/bin/python3 \
|
|
${config.home.homeDirectory}/.config/quickshell/qs-wall.py "$@"
|
|
'';
|
|
};
|
|
|
|
# Bluetooth manager (BT status-bar button): backend script + PATH wrapper.
|
|
# $BLUECTL points at the bluez binary so the wrapper works regardless of PATH;
|
|
# `hardware.bluetooth` is enabled in modules/desktop/gui.nix.
|
|
xdg.configFile."quickshell/qs-bt.py".text = builtins.readFile ./quickshell-bt.py;
|
|
home.file.".local/bin/qs-bt" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
export BLUECTL=${pkgs.bluez}/bin/bluetoothctl
|
|
exec ${pkgs.python3}/bin/python3 \
|
|
${config.home.homeDirectory}/.config/quickshell/qs-bt.py "$@"
|
|
'';
|
|
};
|
|
|
|
# System-stats probe (stats status-bar chip + popup): backend script + PATH
|
|
# wrapper. The bar polls it slowly (15s) to stay battery-cheap (~250ms idle
|
|
# CPU sample); the stats popup refreshes every 3s while open. Reads /proc +
|
|
# /sys (kernel hwmon zones), so no lm_sensors runtime dependency.
|
|
xdg.configFile."quickshell/qs-stats.py".text = builtins.readFile ./quickshell-stats.py;
|
|
home.file.".local/bin/qs-stats" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
exec ${pkgs.python3}/bin/python3 \
|
|
${config.home.homeDirectory}/.config/quickshell/qs-stats.py "$@"
|
|
'';
|
|
};
|
|
|
|
# Screenshot capture (grimblast area/full screen) + QuickShell toast preview
|
|
# with Open / Copy actions: backend script + PATH wrapper. Binary paths
|
|
# injected via QS_* env vars (same pattern as qs-launch). "pick" shows a
|
|
# wofi chooser (select area / full screen), area/screen capture directly.
|
|
xdg.configFile."quickshell/qs-shot.py".text = builtins.readFile ./quickshell-shot.py;
|
|
home.file.".local/bin/qs-shot" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
export QS_GRIMBLAST=${pkgs.grimblast}/bin/grimblast
|
|
export QS_WOFI=${pkgs.wofi}/bin/wofi
|
|
export QS_NOTIFY=${pkgs.libnotify}/bin/notify-send
|
|
export QS_WLCOPY=${pkgs.wl-clipboard}/bin/wl-copy
|
|
export QS_XDGO=${pkgs.xdg-utils}/bin/xdg-open
|
|
exec ${pkgs.python3}/bin/python3 \
|
|
${config.home.homeDirectory}/.config/quickshell/qs-shot.py "$@"
|
|
'';
|
|
};
|
|
|
|
# Extended launcher (emoji / calculator / DuckDuckGo search via wofi):
|
|
# backend script + PATH wrapper. Binary paths injected via QS_* env vars so
|
|
# the modes work regardless of PATH (same pattern as qs-bt's $BLUECTL).
|
|
xdg.configFile."quickshell/qs-launch.py".text = builtins.readFile ./quickshell-launch.py;
|
|
home.file.".local/bin/qs-launch" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
export QS_WOFI=${pkgs.wofi}/bin/wofi
|
|
export QS_QALC=${pkgs.libqalculate}/bin/qalc
|
|
export QS_WLCOPY=${pkgs.wl-clipboard}/bin/wl-copy
|
|
export QS_NOTIFY=${pkgs.libnotify}/bin/notify-send
|
|
export QS_XDGO=${pkgs.xdg-utils}/bin/xdg-open
|
|
exec ${pkgs.python3}/bin/python3 \
|
|
${config.home.homeDirectory}/.config/quickshell/qs-launch.py "$@"
|
|
'';
|
|
};
|
|
|
|
# hyprlock media probe: prints MPRIS artist/title via playerctl, or nothing
|
|
# (empty label text = label hidden). playerctl's {{...}} format braces must
|
|
# live in a script — hyprlock parses cmd text as math expressions and chokes
|
|
# on a bare {{ }} (hyprwm/hyprlock#894). Colors from palette.
|
|
home.file.".local/bin/hyprlock-media" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
set -e
|
|
title=$(${pkgs.playerctl}/bin/playerctl metadata --format '{{ title }}' 2>/dev/null) || exit 0
|
|
artist=$(${pkgs.playerctl}/bin/playerctl metadata --format '{{ artist }}' 2>/dev/null || true)
|
|
printf '<span foreground="%s">♪</span> %s — %s\n' \
|
|
"${palette.hash palette.violet}" "$artist" "$title"
|
|
'';
|
|
};
|
|
|
|
# hyprlock battery probe: prints capacity + status, or nothing on machines
|
|
# without a battery (empty label text = label hidden).
|
|
home.file.".local/bin/hyprlock-battery" = {
|
|
executable = true;
|
|
text = ''
|
|
#!/bin/sh
|
|
set -e
|
|
cap=$(cat /sys/class/power_supply/BAT0/capacity 2>/dev/null) || exit 0
|
|
st=$(cat /sys/class/power_supply/BAT0/status 2>/dev/null)
|
|
case "$st" in
|
|
Charging) col=${palette.hash palette.neon} ;;
|
|
*) col=${palette.hash palette.muted} ;;
|
|
esac
|
|
[ "$cap" -le 15 ] && col=${palette.hash palette.danger}
|
|
printf '<span foreground="%s">%s%%</span> %s\n' "$col" "$cap" "$st"
|
|
'';
|
|
};
|
|
|
|
# hyprlock — matches the QuickShell sci-fi theme: blurred Scifi.jpg
|
|
# background, cyan-accented password field, Orbitron clock.
|
|
xdg.configFile."hypr/hyprlock.conf".text = ''
|
|
general {
|
|
ignore_empty_input = true
|
|
}
|
|
|
|
background {
|
|
monitor =
|
|
path = ${config.home.homeDirectory}/Pictures/wallpapers/Scifi.jpg
|
|
blur_size = 6
|
|
blur_passes = 3
|
|
noise = 0.01
|
|
vibrancy = 0.2
|
|
brightness = 0.35
|
|
}
|
|
|
|
input-field {
|
|
monitor =
|
|
size = 320, 80
|
|
outline_thickness = 2
|
|
dots_size = 0.15
|
|
dots_spacing = 0.6
|
|
dots_center = true
|
|
outer_color = ${palette.lockRgba palette.neon "1.0"}
|
|
inner_color = ${palette.lockRgba palette.panel "0.9"}
|
|
font_color = ${palette.lockRgba palette.danger "1.0"}
|
|
fade_on_empty = false
|
|
placeholder_text = <i>password //</i>
|
|
halign = center
|
|
valign = center
|
|
position = 0, -15%
|
|
}
|
|
|
|
label {
|
|
monitor =
|
|
text = $TIME
|
|
color = ${palette.lockRgba palette.neon "1.0"}
|
|
font_size = 64
|
|
font_family = Orbitron
|
|
halign = center
|
|
valign = center
|
|
position = 0, 12%
|
|
}
|
|
|
|
label {
|
|
monitor =
|
|
text = // SYSTEM LOCKED //
|
|
color = ${palette.lockRgba palette.muted "1.0"}
|
|
font_size = 14
|
|
font_family = Orbitron
|
|
halign = center
|
|
valign = center
|
|
position = 0, 4%
|
|
}
|
|
|
|
label {
|
|
monitor =
|
|
text = cmd[] ${config.home.homeDirectory}/.local/bin/hyprlock-media
|
|
color = ${palette.lockRgba palette.muted "1.0"}
|
|
font_size = 16
|
|
font_family = Orbitron
|
|
halign = center
|
|
valign = center
|
|
position = 0, 2%
|
|
}
|
|
|
|
label {
|
|
monitor =
|
|
text = cmd[] ${config.home.homeDirectory}/.local/bin/hyprlock-battery
|
|
color = ${palette.lockRgba palette.muted "1.0"}
|
|
font_size = 14
|
|
font_family = Orbitron
|
|
halign = center
|
|
valign = bottom
|
|
position = 0, 6%
|
|
}
|
|
'';
|
|
|
|
# hypridle — lock after 5 min idle, dim screen before locking
|
|
xdg.configFile."hypr/hypridle.conf".text = ''
|
|
general {
|
|
lock_cmd = pidof hyprlock || hyprlock
|
|
before_sleep_cmd = loginctl lock-session
|
|
# Under the Lua config, hyprctl dispatch evals the argument as Lua,
|
|
# so the legacy "dpms on" syntax errors out — use hl.dsp.dpms({on=true})
|
|
after_sleep_cmd = hyprctl dispatch "hl.dsp.dpms({on=true})"
|
|
}
|
|
|
|
listener {
|
|
timeout = 300
|
|
on-timeout = loginctl lock-session
|
|
}
|
|
|
|
listener {
|
|
timeout = 330
|
|
on-timeout = hyprctl dispatch "hl.dsp.dpms({off=true})"
|
|
on-resume = hyprctl dispatch "hl.dsp.dpms({on=true})"
|
|
}
|
|
|
|
# Suspend after 15 min idle (lock at 5 min). `sudo systemctl` because
|
|
# polkit does not recognise the greetd session as "active" — same reason
|
|
# the networkmanager group membership is needed (see desktop/hyprland.nix).
|
|
listener {
|
|
timeout = 900
|
|
on-timeout = sudo systemctl suspend
|
|
on-resume = hyprctl dispatch "hl.dsp.dpms({on=true})"
|
|
}
|
|
'';
|
|
|
|
# wofi — app launcher (sci-fi dark glass + neon, matches QuickShell theme).
|
|
# No `style=` line: wofi auto-loads $XDG_CONFIG_HOME/wofi/style.css, and its
|
|
# `style=` option does NOT expand `~` (loads nothing → unstyled black box).
|
|
xdg.configFile."wofi/config".text = ''
|
|
width=35%
|
|
height=45%
|
|
location=center
|
|
opacity=90
|
|
hide_scroll=true
|
|
allow_images=true
|
|
image_size=32
|
|
key_expand=Right
|
|
'';
|
|
xdg.configFile."wofi/style.css".text = ''
|
|
window {
|
|
background-color: ${palette.cssRgba palette.panel "0.92"};
|
|
border: 2px solid ${palette.hash palette.neon};
|
|
border-radius: 14px;
|
|
}
|
|
#input {
|
|
background-color: ${palette.cssRgba palette.ink "0.9"};
|
|
color: ${palette.hash palette.text};
|
|
border: 1px solid ${palette.hash palette.line};
|
|
border-radius: 10px;
|
|
margin: 10px;
|
|
padding: 8px;
|
|
}
|
|
#input:focus {
|
|
border: 1px solid ${palette.hash palette.neon};
|
|
}
|
|
#outer-box {
|
|
margin: 0;
|
|
}
|
|
#inner-box {
|
|
margin: 0 6px;
|
|
}
|
|
* {
|
|
font-family: "FiraCode Nerd Font", monospace;
|
|
font-size: 14px;
|
|
}
|
|
#entry {
|
|
padding: 8px 10px;
|
|
border-radius: 8px;
|
|
}
|
|
#entry:selected {
|
|
background-color: ${palette.cssRgba palette.neon "0.15"};
|
|
border: 1px solid ${palette.hash palette.neon};
|
|
border-radius: 8px;
|
|
}
|
|
#entry image {
|
|
margin-right: 10px;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
}
|
|
#entry label {
|
|
color: ${palette.hash palette.muted};
|
|
}
|
|
#entry:selected label {
|
|
color: ${palette.hash palette.neon};
|
|
}
|
|
#entry list {
|
|
margin-top: 4px;
|
|
}
|
|
#entry list row {
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
}
|
|
#entry list row:selected {
|
|
background-color: ${palette.cssRgba palette.neon "0.25"};
|
|
}
|
|
#scroll {
|
|
margin: 4px;
|
|
}
|
|
'';
|
|
}
|