{ config, lib, osConfig, pkgs, ... }: let host = osConfig.networking.hostName; isChest = config.home.username == "chest"; isRias = host == "Rias"; specialBindIds = [ "scratchpad" "scratchpad-stash" "special-music" "special-comms" "special-todo" "special-sysmon" "daemons-workspace" ]; specialSeed = pkgs.writeText "lucid-special-bindings.json" (builtins.toJSON { version = 1; binds = builtins.filter (entry: builtins.elem entry.id specialBindIds) (builtins.fromJSON (builtins.readFile ./lucid-keybinds-rias.json)).binds; }); vaultwardenBindSeed = pkgs.writeText "lucid-vaultwarden-binding.json" (builtins.toJSON { binds = [ { id = "vaultwarden-password"; keys = "SUPER + X"; desc = "Type a password from Vaultwarden"; category = "Apps"; type = "exec"; cmd = "qs ipc call -- launcher search '>pass '"; } ]; }); mergeVaultwardenBind = pkgs.writeText "lucid-merge-vaultwarden-bind.jq" '' def normkeys: (. // "") | ascii_upcase | gsub("\\s+"; "") | split("+") | map(if . == "WIN" or . == "LOGO" or . == "MOD4" then "SUPER" elif . == "CONTROL" then "CTRL" else . end) | sort | join("+"); if (.binds | type) != "array" then error("keybinds.json has no binds array") else . end | . as $old | ($seed[0].binds[0]) as $seed_bind | ($old.binds | map(select(.id == $seed_bind.id)) | first) as $existing | ($old.binds | map(select(.id != $seed_bind.id))) as $others | ($others | any(.enabled != false and ((.keys | normkeys) == ($seed_bind.keys | normkeys)))) as $conflict | .binds = $others + [ $seed_bind + (if $existing.enabled == false or ($existing == null and $conflict) then {enabled: false} else {} end) ] ''; chestBindingsScript = pkgs.writeText "lucid-chest-bindings.py" '' import hashlib import json import re import sys from collections import Counter source, reference, special, output = sys.argv[1:] binds = json.loads(source) refs = json.load(open(reference))["binds"] specials = json.load(open(special))["binds"] counts = Counter() result = [] for entry in binds: args = entry["_args"] if not 2 <= len(args) <= 3 or args[1].get("_type") != "lua-inline": raise ValueError(f"Unsupported Hyprland binding: {entry}") keys, expr = args[0], args[1]["expr"] opts = args[2] if len(args) == 3 else {} signature = json.dumps([keys.upper().replace(" ", ""), opts], sort_keys=True) counts[signature] += 1 identifier = hashlib.sha256(signature.encode()).hexdigest()[:12] b = {"id": f"chest-{identifier}-{counts[signature]}", "keys": keys} match = next((r for r in refs if r.get("keys", "").upper().replace(" ", "") == keys.upper().replace(" ", "") and r.get("opts", {}) == opts), None) b["desc"] = match.get("desc", keys) if match else keys b["category"] = match.get("category", "Other") if match else "Other" workspace = re.search(r"workspace = (\d+)", expr) if workspace and expr.startswith("hl.dsp.focus("): b.update(desc=f"Focus workspace {workspace.group(1)}", category="Workspaces") elif workspace and expr.startswith("hl.dsp.window.move("): b.update(desc=f"Move window to workspace {workspace.group(1)}", category="Workspaces") if keys.upper().replace(" ", "") == "SUPER+SHIFT+S": b.update(desc="Region screenshot and edit", category="Screenshots") m = re.fullmatch(r'hl\.dsp\.exec_cmd\(("(?:\\.|[^"\\])*")\)\s*', expr, re.S) if m: cmd = json.loads(m.group(1)) cmd = re.sub(r'/nix/store/[0-9a-z]{32}-[^/\s"\\]+/bin/([A-Za-z0-9_.+-]+)', r'\1', cmd) b.update(type="exec", cmd=cmd) else: b.update(type="lua", lua=expr) if opts: b["opts"] = opts result.append(b) json.dump({"version": 1, "binds": result + specials}, open(output, "w"), indent=2) ''; chestSeed = pkgs.runCommand "lucid-chest-bindings.json" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 ${chestBindingsScript} ${lib.escapeShellArg (builtins.toJSON config.lucidHyprlandBinds)} \ ${./lucid-keybinds-rias.json} ${specialSeed} "$out" ''; mergeChestBinds = pkgs.writeText "lucid-merge-chest-bindings.jq" '' def normlua: (. // "") | gsub("\\s+"; "") | gsub("\u0027"; "\""); def normkeys: (. // "") | ascii_upcase | gsub("\\s+"; "") | split("+") | map(if . == "WIN" or . == "LOGO" or . == "MOD4" then "SUPER" elif . == "CONTROL" then "CTRL" else . end) | sort | join("+"); def combos: if .each == "workspace" then .keys as $k | [range(1;11) as $i | ($k | gsub("\\{key\\}"; (($i % 10) | tostring)) | normkeys)] else [(.keys | normkeys)] end; def trigger: {click: (.opts.click // false), long_press: (.opts.long_press // false), release: (.opts.release // false), mouse: (.opts.mouse // false)}; if (.binds | type) != "array" then error("keybinds.json has no binds array") else . end | . as $old | .binds += [$seed[0].binds[] as $b | select([$old.binds[] | select( .id == $b.id or ((["scratchpad", "scratchpad-stash", "special-music", "special-comms", "special-todo", "special-sysmon", "daemons-workspace"] | index($b.id)) != null and .type == "lua" and $b.type == "lua" and ((.lua | normlua) == ($b.lua | normlua))) or ((trigger == ($b | trigger)) and ((combos - ($b | combos) | length) < (combos | length))) )] | length == 0) | $b] ''; mergeSpecialBinds = pkgs.writeText "lucid-merge-special-binds.jq" '' def normlua: (. // "") | gsub("\\s+"; "") | gsub("\u0027"; "\""); def normkeys: (. // "") | ascii_upcase | gsub("\\s+"; "") | split("+") | map(if . == "WIN" or . == "LOGO" or . == "MOD4" then "SUPER" elif . == "CONTROL" then "CTRL" else . end) | sort | join("+"); if (.binds | type) != "array" then error("keybinds.json has no binds array") else . end | . as $old | .binds += [ $seed[0].binds[] as $b | select([$old.binds[] | select(.id == $b.id or (.type == "lua" and ((.lua | normlua) == ($b.lua | normlua))))] | length == 0) | if ([$old.binds[] | select(.enabled != false and ((.keys | normkeys) == ($b.keys | normkeys)))] | length) > 0 then $b + { enabled: false } else $b end ] ''; mergeSpecialPrefs = pkgs.writeText "lucid-merge-special-prefs.jq" '' .specialCustom = ((.specialCustom // "[]" | fromjson) | if type != "array" then error("specialCustom is not an array") else . end | if any(.[]; .key == "daemons") then . else . + [{key: "daemons", label: "Daemons", glyph: "build", on: true, apps: ""}] end | tojson) ''; staticBindKeys = map (entry: builtins.elemAt entry._args 0) config.wayland.windowManager.hyprland.settings.bind; nonRiasBinds = pkgs.runCommand "lucid-nix-binds.lua" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 - ${source}/support/hypr/modules/binds.lua "$out" ${lib.escapeShellArg (builtins.toJSON staticBindKeys)} <<'PY' import json import pathlib import sys source = pathlib.Path(sys.argv[1]).read_text() keys = json.loads(sys.argv[3]) lua_keys = "{ " + ", ".join(json.dumps(key, ensure_ascii=True) for key in keys) + " }" guard = r""" local function normalize_nix_bind(keys) local mods, key = {}, "" for part in tostring(keys or ""):gmatch("[^+]+") do local token = (part:match("^%s*(.-)%s*$") or ""):upper() if token == "CONTROL" then token = "CTRL" end if token == "WIN" or token == "LOGO" or token == "MOD4" then token = "SUPER" end if token == "SUPER" or token == "CTRL" or token == "ALT" or token == "SHIFT" then mods[token] = true else key = token end end local out = {} for _, mod in ipairs({"ALT", "CTRL", "SHIFT", "SUPER"}) do if mods[mod] then out[#out + 1] = mod end end return table.concat(out, "+") .. "+" .. key end local nix_binds = {} for _, key in ipairs(NIX_BIND_KEYS) do nix_binds[normalize_nix_bind(key)] = true end """.replace("NIX_BIND_KEYS", lua_keys) marker = "-- several dispatchers on one key, in order" binding = ' if type(keys) ~= "string" or keys == "" then error("no keys", 0) end' fallback = ' for _, entry in ipairs(EMERGENCY) do apply(entry) end' fallback_source = ' status.source = "emergency"' fallback_text = 'Emergency binds are active - SUPER+S opens Settings.' for needle in (marker, binding, fallback, fallback_source, fallback_text): if source.count(needle) != 1: raise SystemExit("Lucid bind loader changed; refusing to skip conflict protection") source = source.replace(marker, guard + "\n" + marker, 1) source = source.replace(binding, binding + '\n if nix_binds[normalize_nix_bind(keys)] then error("Already bound by Nix: " .. keys, 0) end', 1) source = source.replace(fallback, ' -- Nix supplies the fallback keys on these hosts.', 1) source = source.replace(fallback_source, ' status.source = "nix"', 1) source = source.replace(fallback_text, 'Nix bindings remain active.', 1) pathlib.Path(sys.argv[2]).write_text(source) PY ''; chestBinds = pkgs.runCommand "lucid-chest-loader.lua" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 - ${source}/support/hypr/modules/binds.lua "$out" <<'PYCODE' import pathlib import sys source = pathlib.Path(sys.argv[1]).read_text() old = '{ id = "terminal", keys = "F9", type = "exec", cmd = "kitty" }' new = '{ id = "terminal", keys = "SUPER + RETURN", type = "exec", cmd = "ghostty" }' if source.count(old) != 1: raise SystemExit("Lucid emergency bindings changed") pathlib.Path(sys.argv[2]).write_text(source.replace(old, new, 1)) PYCODE ''; chestMonitors = pkgs.runCommand "lucid-chest-monitors.lua" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 - ${source}/support/hypr/modules/monitors.lua "$out" <<'PY' import pathlib import sys source = pathlib.Path(sys.argv[1]).read_text() default_rule = """hl.monitor({ output = "", mode = M.defaults.mode, position = M.defaults.position, scale = M.defaults.scale, }) """ if source.count(default_rule) != 1: raise SystemExit("Lucid monitor defaults changed; refusing to replace host monitor rules") pathlib.Path(sys.argv[2]).write_text(source.replace(default_rule, "", 1)) PY ''; chestBorders = pkgs.runCommand "lucid-chest-decorations.lua" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 - ${source}/support/hypr/modules/decorations.lua "$out" <<'PY' import pathlib import sys source = pathlib.Path(sys.argv[1]).read_text() marker = "local active_border, inactive_border = borders()" if source.count(marker) != 1: raise SystemExit("Lucid border module changed; refusing to replace host appearance settings") pathlib.Path(sys.argv[2]).write_text(source.split(marker, 1)[0]) PY ''; liveDecoder = if builtins.elem osConfig.networking.hostName [ "Rias" "Eclipse" ] then "cuda" else "vaapi"; vaultwardenLauncherPatch = pkgs.writeText "lucid-vaultwarden-launcher.py" '' import pathlib import sys face_path, dock_path = map(pathlib.Path, sys.argv[1:]) face = face_path.read_text() dock = dock_path.read_text() def replace_once(source, old, new, label): count = source.count(old) if count != 1: raise SystemExit(f"Lucid {label} anchor changed (found {count}); refusing an unsafe launcher patch") return source.replace(old, new, 1) face = replace_once(face, ' "clipboard": "Search clipboard history",\n', ' "clipboard": "Search clipboard history",\n "vaultwarden": "Search Vaultwarden",\n', "launcher placeholder") face = replace_once(face, ' "clipboard": "content_paste",\n', ' "clipboard": "content_paste",\n "vaultwarden": "key",\n', "launcher icon") face = replace_once(face, ' "icon": "content_paste"\n }, {\n "key": "wallpaper",', ' "icon": "content_paste"\n }, {\n "key": "vaultwarden",\n "label": "Vaultwarden",\n "icon": "key"\n }, {\n "key": "wallpaper",', "launcher mode") face = replace_once(face, ' emptyLabel: {\n', ' emptyLabel: {\n if (face.displayMode === "vaultwarden")\n return "No matching vault entries";\n', "launcher empty label") face = replace_once(face, ' emptyHint: {\n', ' emptyHint: {\n if (face.displayMode === "vaultwarden")\n return "Unlock the vault with rbw, then search by name, username, or folder.";\n', "launcher empty hint") dock = replace_once(dock, ' if (q.indexOf(">clip") === 0)\n return "clipboard";\n', ' if (q.indexOf(">clip") === 0)\n return "clipboard";\n\n if (q.indexOf(">pass") === 0)\n return "vaultwarden";\n', "launcher prefix") dock = replace_once(dock, ' if (mode === "clipboard")\n return ">clip";\n', ' if (mode === "clipboard")\n return ">clip ";\n\n if (mode === "vaultwarden")\n return ">pass ";\n', "launcher mode prefix") dock = replace_once(dock, ' if (mode === "wallpaper")\n return ">wallpaper";\n', ' if (mode === "wallpaper")\n return ">wallpaper ";\n', "wallpaper prefix spacing") dock = replace_once(dock, ' if (mode === "theme")\n return ">theme";\n', ' if (mode === "theme")\n return ">theme ";\n', "theme prefix spacing") dock = replace_once(dock, ' if (mode === "live")\n return ">live";\n', ' if (mode === "live")\n return ">live ";\n', "live prefix spacing") dock = replace_once(dock, ' if (mode === "commands")\n return ">";\n', ' if (mode === "commands")\n return "> ";\n', "command prefix spacing") dock = replace_once(dock, ' if (text === ">wallpaper" && !exact && Prefs.liveWallpapers && LiveWall.isVideo(dockWindow.appliedWallpaper))\n text = ">live";\n', ' if (text.trim() === ">wallpaper" && !exact && Prefs.liveWallpapers && LiveWall.isVideo(dockWindow.appliedWallpaper))\n text = ">live ";\n', "wallpaper live prefix spacing") dock = replace_once(dock, ' dockWindow.setSearchText(">wallpaper");\n } else if (id === "theme") {\n dockWindow.setSearchText(">theme");\n', ' dockWindow.setSearchText(">wallpaper ");\n } else if (id === "theme") {\n dockWindow.setSearchText(">theme ");\n', "menu prefix spacing") dock = replace_once(dock, ' } else if (id === "clipboard") {\n dockWindow.setSearchText(">clip");\n', ' } else if (id === "clipboard") {\n dockWindow.setSearchText(">clip ");\n', "clipboard prefix spacing") dock = replace_once(dock, ' dockWindow.setSearchText(">wallpaper", true);', ' dockWindow.setSearchText(">wallpaper ", true);', "wallpaper picker prefix spacing") dock = replace_once(dock, 'onPickerKindRequested: (kind) => dockWindow.setSearchText(kind === "live" ? ">live" : ">wallpaper", true)', 'onPickerKindRequested: (kind) => dockWindow.setSearchText(kind === "live" ? ">live " : ">wallpaper ", true)', "picker tab prefix spacing") dock = replace_once(dock, ' dockWindow.openLauncher(">wallpaper");\n }\n\n function live(): void {\n dockWindow.openLauncher(">live");', ' dockWindow.openLauncher(">wallpaper ");\n }\n\n function live(): void {\n dockWindow.openLauncher(">live ");', "wallpaper ipc prefix spacing") dock = replace_once(dock, 'onBackRequested: dockWindow.setSearchText(dockWindow.mode === "commands" ? "" : ">")', 'onBackRequested: dockWindow.setSearchText(dockWindow.mode === "commands" ? "" : "> ")', "back prefix spacing") dock = replace_once(dock, ' if (dockWindow.mode === "clipboard")\n Clip.refresh();\n', ' if (dockWindow.mode === "clipboard")\n Clip.refresh();\n if (dockWindow.mode === "vaultwarden")\n Vaultwarden.refresh();\n', "launcher mode activation") dock = replace_once(dock, ' readonly property var allCommands: [{', """ Connections { target: Vaultwarden function onEntriesChanged() { dockWindow.rebuildResults(); } function onLoadingChanged() { dockWindow.rebuildResults(); } function onErrorChanged() { dockWindow.rebuildResults(); } function onPasswordReady(password, targetAddress) { dockWindow.menuOpen = false; Vaultwarden.typePassword(password, targetAddress); } } readonly property var allCommands: [{""", "vaultwarden updates") dock = replace_once(dock, ' function rebuildResults() {', """ function vaultRows(query) { if (Vaultwarden.loading) return [dockWindow.headerRow("Loading Vaultwarden…")]; if (Vaultwarden.error !== "") return [dockWindow.headerRow(Vaultwarden.error)]; var rows = []; var entries = Vaultwarden.entries || []; for (var i = 0; i < entries.length; i++) { var entry = entries[i]; var haystack = (entry.name + " " + entry.user + " " + entry.folder).toLowerCase(); if (query !== "" && haystack.indexOf(query) === -1) continue; var detail = [entry.user, entry.folder].filter((part) => part !== "").join(" · "); rows.push(dockWindow.makeRow("vaultwarden", "vault-" + entry.id, entry.name, detail, { "glyph": "key", "payload": entry.id })); } return rows; } function rebuildResults() {""", "vaultwarden search") dock = replace_once(dock, ' } else if (mode === "clipboard") {\n var ents = Clip.entries;', ' } else if (mode === "vaultwarden") {\n rows = dockWindow.vaultRows(q);\n } else if (mode === "clipboard") {\n var ents = Clip.entries;', "vaultwarden results") dock = replace_once(dock, ' } else if (row.kind === "clip") {\n Clip.copy(row.payload);\n dockWindow.menuOpen = false;\n', ' } else if (row.kind === "clip") {\n Clip.copy(row.payload);\n dockWindow.menuOpen = false;\n } else if (row.kind === "vaultwarden") {\n Vaultwarden.getPassword(row.payload, Hyprland.activeToplevel ? Hyprland.activeToplevel.address : "");\n', "vaultwarden selection") face_path.write_text(face) dock_path.write_text(dock) ''; baseSource = pkgs.applyPatches { src = pkgs.fetchgit { url = "https://github.com/Sn3akyy1/lucid.git"; rev = "bdd7f79da18efd837f79dcecbe5d2ec178abc10c"; hash = "sha256-b6qylKjCGYy2Yo64i1w9g4oGrYgcXf502jY9qNNWhSI="; sparseCheckout = [ "/*" "!/wallpapers/*" "!/assets/*.webp" ]; nonConeMode = true; }; patches = [ (pkgs.writeText "lucid-scratchpad-target.patch" '' diff --git a/support/hypr/modules/specials.lua b/support/hypr/modules/specials.lua --- a/support/hypr/modules/specials.lua +++ b/support/hypr/modules/specials.lua @@ -176,8 +176,11 @@ function M.scratchpad() return function() if space(config(), "special").enabled == false then return end - local shown = hl.get_active_special_workspace() - local name = shown and (shown.name:gsub("^special:", "")) or "special" - hl.dispatch(hl.dsp.workspace.toggle_special(name)) + local shown = hl.get_active_special_workspace() + if shown and shown.name == "special:special" then + hl.dispatch(hl.dsp.workspace.toggle_special("special")) + else + hl.dispatch(hl.dsp.focus({ workspace = "special:special" })) + end end end '') (pkgs.writeText "lucid-lair-search.patch" '' --- a/luciddocks/Dock.qml +++ b/luciddocks/Dock.qml @@ -683,4 +683,8 @@ readonly property var searchEngines: ({ + "lair": { + "name": "Lair", + "url": "https://s.lair.moe/web?s=" + }, "duckduckgo": { "name": "DuckDuckGo", "url": "https://duckduckgo.com/?q=" --- a/lucidprefs/LauncherPage.qml +++ b/lucidprefs/LauncherPage.qml @@ -386,5 +386,8 @@ current: Prefs.launcherSearchEngine options: [{ + "key": "lair", + "label": "Lair" + }, { "key": "duckduckgo", "label": "DuckDuckGo" }, { '') (pkgs.writeText "lucid-shot-special-workspace-name.patch" '' diff --git a/lucidshot/ShotOverlay.qml b/lucidshot/ShotOverlay.qml --- a/lucidshot/ShotOverlay.qml +++ b/lucidshot/ShotOverlay.qml @@ -228,1 +228,1 @@ - const ws = mon.activeWorkspace ? mon.activeWorkspace.id : null; + const ws = mon.activeWorkspace ? mon.activeWorkspace.name : null; @@ -229,1 +229,1 @@ - const special = mon.specialWorkspace ? mon.specialWorkspace.id : 0; + const special = mon.specialWorkspace ? mon.specialWorkspace.name : ""; @@ -234,1 +234,1 @@ - return c.workspace.id === ws || (special !== 0 && c.workspace.id === special) || (c.pinned && c.monitor === mon.id); + return c.workspace.name === ws || (special && c.workspace.name === special) || (c.pinned && c.monitor === mon.id); @@ -246,1 +246,1 @@ - "special": special !== 0 && c.workspace.id === special, + "special": !!special && c.workspace.name === special, '') ]; }; source = pkgs.runCommand "lucid-vaultwarden-launcher-source" { nativeBuildInputs = [ pkgs.python3 ]; } '' cp -r ${baseSource} "$out" chmod -R u+w "$out" python3 ${vaultwardenLauncherPatch} "$out/luciddocks/LauncherFace.qml" "$out/luciddocks/Dock.qml" ''; wallpaperDir = "${config.home.homeDirectory}/Wallpapers"; wallpaperScript = "${config.home.homeDirectory}/.config/hypr/scripts/wallpaper/set-wallpaper.sh"; widgetLayout = if osConfig.networking.hostName == "Rias" then ./lucid-widgets-rias.json else ./lucid-widgets.json; defaultWallpaper = "${wallpaperDir}/bluehour.jpg"; hyprLua = import ../hyprland/lua.nix { inherit lib; }; colors = config.lib.stylix.colors.withHashtag; paletteFile = pkgs.runCommand "lucid-stylix-palette.json" { nativeBuildInputs = [ pkgs.python3 ]; } '' python3 - "$out" ${lib.escapeShellArg config.stylix.base16Scheme} \ ${lib.escapeShellArg "${source}/support/lucid"} \ ${lib.escapeShellArg config.stylix.accent} <<'PY' import json import runpy import sys from pathlib import Path output, scheme_path, scripts, accent_key = sys.argv[1:] sys.path.insert(0, scripts) from lucid_palette import at_tone importer = runpy.run_path(str(Path(scripts) / "add-theme.py")) scheme = importer["parse_base16"](scheme_path, Path(scheme_path).read_text()) if scheme is None: raise SystemExit("Stylix scheme is not valid Base16") bg, fg, accents, hints = importer["to_palette"](scheme) accent_key = accent_key.lower() hints["primary"] = scheme["colors"][accent_key] hints["secondary"] = scheme["colors"]["base0d" if accent_key != "base0d" else "base0e"] hints["tertiary"] = scheme["colors"]["base0c" if accent_key == "base0b" else "base0b"] palette = importer["build_palette"]( bg, fg, accents, hints, mode="dark" if importer["is_dark"](bg) else "light" ) if importer["is_dark"](bg): colors = scheme["colors"] # Keep the Base16 dark surface ladder. Lucid's built-in Mocha uses base00 # for its main panels, with base01/base02 as the raised surfaces; the # generic Material builder starts at base02 and makes the whole shell pale. background_tone = importer["tone"](bg) darkest = importer["hx"](at_tone(bg, background_tone - 4)) near_background = importer["hx"](at_tone(bg, background_tone - 2)) palette.update({ "surface": colors["base00"], "surface_dim": darkest, "surface_bright": importer["hx"](at_tone(bg, background_tone + 8)), "surface_container_lowest": darkest, "surface_container_low": near_background, "surface_container": colors["base00"], "surface_container_high": colors["base01"], "surface_container_highest": colors["base02"], "surface_variant": colors["base01"], "on_surface_variant": colors["base04"], "outline": colors["base03"], "outline_variant": colors["base01"], }) Path(output).write_text(json.dumps(palette, indent=2) + "\n") PY ''; themeMeta = pkgs.writeText "lucid-stylix-meta.json" (builtins.toJSON { id = "stylix"; name = "Stylix"; desc = "Active Stylix palette"; swatchBg = colors.base00; swatchAccent = colors.${config.stylix.accent}; source = "Home Manager"; detected = "base16"; user = true; }); themeName = pkgs.writeText "lucid-current-theme" "stylix\n"; chestOsdPatch = pkgs.writeText "lucid-chest-osd.patch" '' --- a/lucidosd/Osd.qml +++ b/lucidosd/Osd.qml @@ -26 +26 @@ - readonly property int brightnessPercent: osdWindow.maxBrightness > 0 ? Math.round((parseInt(brightnessFile.text()) / osdWindow.maxBrightness) * 100) : 0 + property int brightnessPercent: 0 @@ -378,2 +378,15 @@ onFileChanged: reload() + onLoaded: { + const value = parseInt(brightnessFile.text()); + if (osdWindow.maxBrightness > 0 && !isNaN(value)) + osdWindow.brightnessPercent = Math.round(value / osdWindow.maxBrightness * 100); + } } + + Timer { + id: chestBrightnessPoll + interval: 500 + repeat: true + running: osdWindow.backlightDevice !== "" + onTriggered: brightnessFile.reload() + } ''; themeAutoPatch = pkgs.writeText "lucid-theme-auto.patch" '' --- a/Prefs.qml +++ b/Prefs.qml @@ -455,5 +455,12 @@ function setColorMode(mode) { + if (mode === "auto") { + root.setAutoColorMode(true); + return; + } - if ((mode !== "dark" && mode !== "light") || mode === root.colorMode) + if (mode !== "dark" && mode !== "light") return; + root.setAutoColorMode(false); + if (mode === root.colorMode) + return; // set straight away so the control answers the click; the cache file is @@ -484,2 +491,4 @@ function setAutoColorMode(on) { + if (on && root.currentTheme !== "matugen" && root.currentTheme !== "pywal") + return; root.autoColorMode = on; @@ -1669,1 +1678,5 @@ - onLoaded: root.currentTheme = text().trim() || "matugen" + onLoaded: { + root.currentTheme = text().trim() || "matugen"; + if (root.currentTheme !== "matugen" && root.currentTheme !== "pywal" && root.autoColorMode) + root.setAutoColorMode(false); + } --- a/lucidprefs/ThemePage.qml +++ b/lucidprefs/ThemePage.qml @@ -11,9 +11,14 @@ readonly property string home: Quickshell.env("HOME") readonly property string currentTheme: Prefs.currentTheme - readonly property var modeOptions: [{ + readonly property bool wallpaperDerivedTheme: page.currentTheme === "matugen" || page.currentTheme === "pywal" + readonly property var modeOptions: [{ "key": "dark", "label": "Dark" }, { "key": "light", "label": "Light" }] + readonly property var autoModeOptions: page.modeOptions.concat([{ + "key": "auto", + "label": "Auto" + }]) @@ -22,1 +27,1 @@ - readonly property string modeHint: page.currentTheme === "matugen" || page.currentTheme === "pywal" ? "Re-derives the palette from your wallpaper in the mode you pick. Applications are asked to match." : (page.currentTheme === "colour" ? "Builds the palette from your colour again in the mode you pick. Applications are asked to match." : "Builds a light palette from this theme's own colours. Applications are asked to match.") + readonly property string modeHint: page.wallpaperDerivedTheme ? "Auto follows the wallpaper brightness whenever you change the wallpaper. Dark and Light stay fixed until you choose another mode." : (page.currentTheme === "colour" ? "Builds the palette from your colour again in the mode you pick. Applications are asked to match." : "Builds a light palette from this theme's own colours. Applications are asked to match.") @@ -273,6 +278,6 @@ M3Segmented { - width: Theme.dp(200) - current: Prefs.colorMode - options: page.modeOptions + width: Theme.dp(page.wallpaperDerivedTheme ? 300 : 200) + current: Prefs.autoColorMode && page.wallpaperDerivedTheme ? "auto" : Prefs.colorMode + options: page.wallpaperDerivedTheme ? page.autoModeOptions : page.modeOptions onChosen: (key) => { return Prefs.setColorMode(key); @@ -284,13 +289,0 @@ - SettingRow { - title: "Follow the wallpaper" - description: "Picks light for a bright wallpaper and dark for a dark one, every time the wallpaper changes. A hand-picked mode holds until the wallpaper changes next." - - M3Switch { - checked: Prefs.autoColorMode - onToggled: (v) => { - return Prefs.setAutoColorMode(v); - } - } - - } - ''; settingsScreenPatch = pkgs.writeText "lucid-settings-screen.patch" '' --- a/lucidprefs/Settings.qml +++ b/lucidprefs/Settings.qml @@ -9,2 +9,4 @@ FloatingWindow { id: win + property var preferredScreen: null + screen: win.preferredScreen || Monitors.mainScreen ''; wallpaperAutoPatch = pkgs.writeText "lucid-wallpaper-auto.patch" '' --- a/set-wallpaper.sh +++ b/set-wallpaper.sh @@ -32,4 +32,5 @@ MODE="''${2:-}" if [[ -z "$MODE" ]]; then - if grep -qE '"autoColorMode"[[:space:]]*:[[:space:]]*true' "$HOME/.config/quickshell/lucidprefs/prefs.json" 2>/dev/null; then + if [[ "$CURRENT_THEME" == matugen || "$CURRENT_THEME" == pywal ]] && \ + grep -qE '"autoColorMode"[[:space:]]*:[[:space:]]*true' "$HOME/.config/quickshell/lucidprefs/prefs.json" 2>/dev/null; then MODE=auto ''; kdeConnectBridgePython = pkgs.python3.withPackages (ps: [ ps.pygobject3 ]); applyTheme = pkgs.writeShellScript "lucid-apply-theme" '' case "''${1:-}" in --apps-only) exit 0 ;; --shell-only) shift ;; esac exec "$HOME/.config/lucid/apply-theme-upstream.sh" --shell-only "$@" ''; qmlPackages = with pkgs.qt6; [ qt5compat qtmultimedia ]; tesseract = pkgs.tesseract4.override { enableLanguages = [ "eng" "rus" ]; }; shellFile = if isChest then pkgs.writeText "lucid-chest-shell.qml" (builtins.replaceStrings [ ''import "./lucidlock"'' " Lock {\n }\n" " void Clip.probed;" ] [ "" "" " void Notifs.count;\n void KdeConnect.installed;\n void HyprConfig.moduleProbed;\n void Monitors.moduleProbed;\n void Glass.moduleProbed;\n void Startup.moduleProbed;\n void Clip.probed;" ] (builtins.readFile ./lucid-shell.qml)) else ./lucid-shell.qml; prefsFilter = if isChest then ''.barEnabled = true | .widgetOnTop = false | .widgetHideFullscreen = true | .fontFamily = $font | .liveWallpapers = false | .idleLock = false | .idleLockBeforeSleep = false | .launcherPowerButtons = "suspend,reboot,shutdown" | .powerModuleActions = "suspend,hibernate,logout,reboot,shutdown"'' else ''.barEnabled = true | .widgetOnTop = false | .widgetHideFullscreen = true | .fontFamily = $font | .wallpaperFolder = $wallpaperDir | .liveDecoder = $decoder''; lucid = pkgs.writeShellScript "lucid" '' export NIXPKGS_QT6_QML_IMPORT_PATH="${lib.makeSearchPath "lib/qt-6/qml" qmlPackages}:''${NIXPKGS_QT6_QML_IMPORT_PATH:-}" export QT_PLUGIN_PATH="${lib.makeSearchPath "lib/qt-6/plugins" qmlPackages}:''${QT_PLUGIN_PATH:-}" export PATH="${lib.makeBinPath (with pkgs; [ bash brightnessctl cliphist coreutils curl ffmpeg grim imagemagick jq libnotify pulseaudio kdeConnectBridgePython pywal16 wf-recorder wl-clipboard xdg-utils ])}:${tesseract}/bin:$PATH" exec ${pkgs.quickshell}/bin/qs --no-duplicate --path "${config.home.homeDirectory}/.config/quickshell" ''; startWallpaper = pkgs.writeShellScript "lucid-wallpaper-start" '' export PATH="${lib.makeBinPath (with pkgs; [ awww bash coreutils ffmpeg imagemagick jq python3 pywal16 util-linux ])}:$PATH" selected="${defaultWallpaper}" if [ -s "${config.home.homeDirectory}/.cache/current_live_wallpaper" ]; then saved=$(${pkgs.coreutils}/bin/head -n1 "${config.home.homeDirectory}/.cache/current_live_wallpaper") if [ -f "$saved" ]; then selected="$saved" fi fi if [ -s "${config.home.homeDirectory}/.cache/current_wallpaper" ]; then saved=$(${pkgs.coreutils}/bin/cat "${config.home.homeDirectory}/.cache/current_wallpaper") if [ "$selected" = "${defaultWallpaper}" ] && [ -f "$saved" ]; then selected="$saved" fi fi if [ ! -f "$selected" ]; then echo "Lucid startup wallpaper not found; leaving the current wallpaper unchanged" >&2 exit 0 fi exec "${wallpaperScript}" "$selected" ''; in { programs.waybar.enable = lib.mkForce false; services.mako.enable = lib.mkForce false; home.packages = with pkgs; [ cliphist ffmpeg grim imagemagick libnotify pywal16 wf-recorder tesseract ] ++ lib.optionals isChest [ brightnessctl playerctl slurp swappy ]; home.activation.installLucid = lib.hm.dag.entryAfter [ "writeBoundary" ] '' target="${config.home.homeDirectory}/.config/quickshell" $DRY_RUN_CMD mkdir -p "$target" $DRY_RUN_CMD ${pkgs.rsync}/bin/rsync -rltp --chmod=Du+rwx,Fu+rw \ --exclude='/shell.qml' \ --exclude='/LucidBar.qml' \ --exclude='/lucidprefs/prefs.json' \ --exclude='/lucidbar/blur.json' \ --exclude='/lucidbar/clock_reminders.json' \ --exclude='/lucidbar/mpris_shazam.json' \ --exclude='/luciddocks/pinned.json' \ --exclude='/luciddocks/usage.json' \ --exclude='/luciddocks/wallpaper.json' \ --exclude='/lucidmoji/config.json' \ --exclude='/lucidmoji/state.json' \ --exclude='/lucidkeys/state.json' \ --exclude='/lucidwidgets/widgets.json' \ "${source}/" "$target/" ${lib.optionalString isChest '' # Notifs refers to Lockscreen even without a lock surface. Keep its # singleton from taking over logind lock signals or its LockedHint. lock_qml="$target/Lockscreen.qml" if ! grep -Fq 'return; // Lucid lock disabled for chest' "$lock_qml"; then grep -Fq 'function lock(settle) {' "$lock_qml" && \ grep -Fq 'function pushLockedHint() {' "$lock_qml" && \ grep -Fq 'id: logindWatch' "$lock_qml" || { echo "Lucid lock integration changed; refusing to leave it active for chest" >&2 exit 1 } $DRY_RUN_CMD sed -i \ -e '/function lock(settle) {/a\ return; // Lucid lock disabled for chest' \ -e '/function pushLockedHint() {/a\ return; // Lucid lock disabled for chest' \ -e '/id: logindWatch/{n;n;s/running: true/running: false/;}' "$lock_qml" fi if ! grep -Fq 'id: chestBrightnessPoll' "$target/lucidosd/Osd.qml"; then $DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${chestOsdPatch} fi ''} $DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch -p1 -d "$target" < ${./idle-work.patch} if ! grep -Fq 'mode === "auto"' "$target/Prefs.qml"; then $DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${themeAutoPatch} fi if ! grep -Fq 'property var preferredScreen: null' "$target/lucidprefs/Settings.qml"; then $DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$target" < ${settingsScreenPatch} fi settings_qml="$target/lucidprefs/Settings.qml" if ! grep -Fq 'win.preferredScreen = Monitors.focusedScreen || Monitors.mainScreen;' "$settings_qml"; then $DRY_RUN_CMD sed -i '/if (win.visible) {/a\ win.preferredScreen = Monitors.focusedScreen || Monitors.mainScreen;' "$settings_qml" fi dock_qml="$target/luciddocks/Dock.qml" if grep -Fq '/var/lib/snapd/desktop/applications; do' "$dock_qml"; then $DRY_RUN_CMD sed -i \ 's#/var/lib/snapd/desktop/applications; do#/var/lib/snapd/desktop/applications $HOME/.nix-profile/share/applications $HOME/.local/state/nix/profile/share/applications /etc/profiles/per-user/$USER/share/applications /nix/profile/share/applications /nix/var/nix/profiles/default/share/applications /run/current-system/sw/share/applications; do#' \ "$dock_qml" elif ! grep -Fq '/etc/profiles/per-user/$USER/share/applications; do' "$dock_qml"; then echo "Lucid launcher application scan pattern changed; refusing to skip Nix profiles" >&2 exit 1 fi keyboard_focus='WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None' keyboard_focus_fixed='WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None' if grep -Fq "$keyboard_focus" "$dock_qml"; then $DRY_RUN_CMD sed -i \ 's/WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None/WlrLayershell.keyboardFocus: dockWindow.menuOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None/' \ "$dock_qml" elif ! grep -Fq "$keyboard_focus_fixed" "$dock_qml"; then echo "Lucid launcher keyboard focus pattern changed; refusing to skip focus fix" >&2 exit 1 fi if ! grep -Fq 'Nix focus workaround for Hyprland 0.57' "$dock_qml"; then if [ "$(grep -Fxc ' onMenuOpenChanged: {' "$dock_qml")" -ne 1 ]; then echo "Lucid menu-open handler changed; refusing to skip focus workaround" >&2 exit 1 fi $DRY_RUN_CMD sed -i '/ onMenuOpenChanged: {/a\ // Nix focus workaround for Hyprland 0.57: detach pointer and keyboard focus while menus are open.\ Quickshell.execDetached(["hyprctl", "eval", "hl.config({ input = { follow_mouse = " + (dockWindow.menuOpen ? 2 : 1) + " } })"]);' "$dock_qml" fi $DRY_RUN_CMD install -m644 ${shellFile} "$target/shell.qml" $DRY_RUN_CMD install -m644 ${./LucidBar.qml} "$target/LucidBar.qml" $DRY_RUN_CMD install -m644 ${./Vaultwarden.qml} "$target/Vaultwarden.qml" ${lib.optionalString (!isChest) '' $DRY_RUN_CMD install -m644 ${./Agenda.qml} "$target/Agenda.qml" $DRY_RUN_CMD install -m644 ${./CalendarSource.qml} "$target/CalendarSource.qml" $DRY_RUN_CMD install -m644 ${./ClockCalendar.qml} "$target/lucidbar/ClockCalendar.qml" $DRY_RUN_CMD install -m644 ${./thunderbird-calendar.py} "$target/thunderbird-calendar.py" ''} $DRY_RUN_CMD install -m644 ${./WidgetLayer.qml} "$target/lucidwidgets/WidgetLayer.qml" $DRY_RUN_CMD install -m644 ${./WidgetsPage.qml} "$target/lucidprefs/WidgetsPage.qml" ${lib.optionalString (!isChest) '' $DRY_RUN_CMD mkdir -p "${wallpaperDir}" \ "${config.home.homeDirectory}/.config/hypr/scripts/wallpaper" $DRY_RUN_CMD install -m755 "${source}/support/wallpaper/set-wallpaper.sh" \ "${wallpaperScript}" if ! grep -Fq 'CURRENT_THEME" == matugen' "${wallpaperScript}"; then $DRY_RUN_CMD ${pkgs.patch}/bin/patch --batch --forward -p1 -d "$(dirname ${lib.escapeShellArg wallpaperScript})" < ${wallpaperAutoPatch} fi ''} lucid_dir="${config.home.homeDirectory}/.config/lucid" $DRY_RUN_CMD mkdir -p "$lucid_dir/themes" if [ "${host}" = Rias ] && [ -z "''${DRY_RUN_CMD:-}" ]; then if [ ! -s "$lucid_dir/keybinds.json" ]; then install -m644 ${./lucid-keybinds-rias.json} "$lucid_dir/keybinds.json" fi binds_tmp=$(${pkgs.coreutils}/bin/mktemp "$lucid_dir/keybinds.json.XXXXXX") if ${pkgs.jq}/bin/jq --slurpfile seed ${vaultwardenBindSeed} -f ${mergeVaultwardenBind} \ "$lucid_dir/keybinds.json" > "$binds_tmp"; then chmod 644 "$binds_tmp" mv "$binds_tmp" "$lucid_dir/keybinds.json" else rm -f "$binds_tmp" echo "Could not merge the Vaultwarden Lucid binding" >&2 exit 1 fi fi ${lib.optionalString (!isRias) '' special_migration="$lucid_dir/special-workspaces-v1" if [ -z "''${DRY_RUN_CMD:-}" ] && [ ! -e "$special_migration" ]; then if [ -f "${config.home.homeDirectory}/.config/hypr/lucid-specials.lua" ] && \ [ ! -e "${config.home.homeDirectory}/.config/hypr/lucid-specials.before-special-workspaces-v1.lua" ]; then cp -p "${config.home.homeDirectory}/.config/hypr/lucid-specials.lua" \ "${config.home.homeDirectory}/.config/hypr/lucid-specials.before-special-workspaces-v1.lua" fi if [ ! -s "$lucid_dir/keybinds.json" ]; then install -m644 ${specialSeed} "$lucid_dir/keybinds.json" else binds_tmp=$(${pkgs.coreutils}/bin/mktemp "$lucid_dir/keybinds.json.XXXXXX") if ${pkgs.jq}/bin/jq --slurpfile seed ${specialSeed} -f ${mergeSpecialBinds} \ "$lucid_dir/keybinds.json" > "$binds_tmp"; then chmod 644 "$binds_tmp" mv "$binds_tmp" "$lucid_dir/keybinds.json" else rm -f "$binds_tmp" echo "Could not merge Lucid special workspace bindings" >&2 exit 1 fi fi fi ''} ${lib.optionalString isChest '' chest_migration="$lucid_dir/chest-keybinds-v1" if [ -z "''${DRY_RUN_CMD:-}" ] && [ ! -e "$chest_migration" ]; then if [ ! -s "$lucid_dir/keybinds.json" ]; then install -m644 ${chestSeed} "$lucid_dir/keybinds.json" else if [ ! -e "$lucid_dir/keybinds.before-chest-migration.json" ]; then cp -p "$lucid_dir/keybinds.json" "$lucid_dir/keybinds.before-chest-migration.json" fi binds_tmp=$(${pkgs.coreutils}/bin/mktemp "$lucid_dir/keybinds.json.XXXXXX") if ${pkgs.jq}/bin/jq --slurpfile seed ${chestSeed} -f ${mergeChestBinds} "$lucid_dir/keybinds.json" > "$binds_tmp"; then chmod 644 "$binds_tmp" mv "$binds_tmp" "$lucid_dir/keybinds.json" else rm -f "$binds_tmp" echo "Could not migrate chest keybindings to Lucid" >&2 exit 1 fi fi touch "$chest_migration" fi ''} monitor_file="${config.home.homeDirectory}/.config/hypr/lucid-monitors.lua" if [ "${host}" = Rias ] && { [ ! -s "$monitor_file" ] || ! grep -qE 'output[[:space:]]*=' "$monitor_file"; } && \ ! grep -qE 'workspace[[:space:]]*=' "$monitor_file" 2>/dev/null; then $DRY_RUN_CMD install -m644 ${./lucid-monitors-rias.lua} "$monitor_file" fi $DRY_RUN_CMD ${pkgs.rsync}/bin/rsync -rltp --chmod=Du+rwx,Fu+rw --ignore-existing \ "${source}/support/lucid/themes/" "$lucid_dir/themes/" for script in palette-edit.py lucid_palette.py add-theme.py scheme-gallery.py gen-light-palette.py; do $DRY_RUN_CMD install -m644 "${source}/support/lucid/$script" "$lucid_dir/$script" done for script in gen-pywal-palette.py set-mode.sh wallpaper-mode.sh; do $DRY_RUN_CMD install -m755 "${source}/support/lucid/$script" "$lucid_dir/$script" done $DRY_RUN_CMD install -m755 "${source}/support/lucid/apply-theme.sh" \ "$lucid_dir/apply-theme-upstream.sh" $DRY_RUN_CMD install -m755 ${applyTheme} \ "$lucid_dir/apply-theme.sh" if [ ! -e "$target/lucidprefs/prefs.json" ]; then $DRY_RUN_CMD install -m644 ${if isChest then "${source}/defaults/prefs.json" else ./lucid-prefs.json} "$target/lucidprefs/prefs.json" fi if [ -z "''${DRY_RUN_CMD:-}" ] && [ -f "$target/lucidprefs/prefs.json" ]; then prefs_tmp=$(${pkgs.coreutils}/bin/mktemp "$target/lucidprefs/prefs.json.XXXXXX") if ${pkgs.jq}/bin/jq --arg font ${lib.escapeShellArg config.stylix.fonts.sansSerif.name} \ --arg wallpaperDir ${lib.escapeShellArg wallpaperDir} \ --arg decoder ${lib.escapeShellArg liveDecoder} \ ${lib.escapeShellArg prefsFilter} \ "$target/lucidprefs/prefs.json" > "$prefs_tmp"; then ${pkgs.coreutils}/bin/chmod 644 "$prefs_tmp" ${pkgs.coreutils}/bin/mv "$prefs_tmp" "$target/lucidprefs/prefs.json" else ${pkgs.coreutils}/bin/rm -f "$prefs_tmp" exit 1 fi fi ${lib.optionalString (!isRias) '' if [ -z "''${DRY_RUN_CMD:-}" ] && [ ! -e "$special_migration" ]; then prefs_tmp=$(${pkgs.coreutils}/bin/mktemp "$target/lucidprefs/prefs.json.XXXXXX") if ${pkgs.jq}/bin/jq -f ${mergeSpecialPrefs} "$target/lucidprefs/prefs.json" > "$prefs_tmp"; then chmod 644 "$prefs_tmp" mv "$prefs_tmp" "$target/lucidprefs/prefs.json" touch "$special_migration" else rm -f "$prefs_tmp" echo "Could not merge Lucid special workspace settings" >&2 exit 1 fi fi ''} if [ ! -e "$target/luciddocks/pinned.json" ]; then $DRY_RUN_CMD install -m644 ${./lucid-pinned.json} "$target/luciddocks/pinned.json" fi if [ ! -e "$target/lucidwidgets/widgets.json" ]; then $DRY_RUN_CMD install -m644 ${widgetLayout} "$target/lucidwidgets/widgets.json" fi for pair in \ 'blur.json lucidbar/blur.json' \ 'clock_reminders.json lucidbar/clock_reminders.json' \ 'mpris_shazam.json lucidbar/mpris_shazam.json' \ 'usage.json luciddocks/usage.json' \ 'wallpaper.json luciddocks/wallpaper.json' \ 'moji-config.json lucidmoji/config.json' \ 'moji-state.json lucidmoji/state.json' \ 'keys-state.json lucidkeys/state.json'; do set -- $pair if [ ! -e "$target/$2" ]; then $DRY_RUN_CMD install -m644 "${source}/defaults/$1" "$target/$2" fi done ${lib.optionalString (isChest && host == "Impreza") '' # Import Eclipse's appearance once. Keep later Lucid edits and back up # existing laptop settings before changing them; monitor state is untouched. appearance_marker="$lucid_dir/impreza-appearance-v1" if [ -z "''${DRY_RUN_CMD:-}" ] && [ ! -e "$appearance_marker" ]; then mkdir -p "${config.home.homeDirectory}/.cache" "${config.home.homeDirectory}/.config/hypr" prefs="$target/lucidprefs/prefs.json" prefs_tmp=$(${pkgs.coreutils}/bin/mktemp "$prefs.XXXXXX") if ${pkgs.jq}/bin/jq --slurpfile appearance ${./lucid-prefs-impreza.json} \ '. + $appearance[0]' "$prefs" > "$prefs_tmp"; then cp --backup=numbered "$prefs" "$prefs.before-impreza-appearance" chmod 644 "$prefs_tmp" mv "$prefs_tmp" "$prefs" else rm -f "$prefs_tmp" exit 1 fi for pair in \ '${./lucid-widgets-impreza.json} lucidwidgets/widgets.json' \ '${./lucid-pinned-impreza.json} luciddocks/pinned.json'; do set -- $pair [ ! -e "$target/$2" ] || cp --backup=numbered "$target/$2" "$target/$2.before-impreza-appearance" install -m644 "$1" "$target/$2" done for pair in \ '${./lucid-settings-impreza.lua} lucid-settings.lua' \ '${./lucid-glass-impreza.lua} lucid-glass.lua'; do set -- $pair file="${config.home.homeDirectory}/.config/hypr/$2" [ ! -e "$file" ] || cp --backup=numbered "$file" "$file.before-impreza-appearance" install -m644 "$1" "$file" done if [ -e "${config.home.homeDirectory}/.cache/current_theme" ]; then cp --backup=numbered "${config.home.homeDirectory}/.cache/current_theme" \ "${config.home.homeDirectory}/.cache/current_theme.before-impreza-appearance" fi install -m644 ${themeName} "${config.home.homeDirectory}/.cache/current_theme" touch "$appearance_marker" fi ''} $DRY_RUN_CMD mkdir -p "${config.home.homeDirectory}/.config/lucid/themes/stylix" \ "${config.home.homeDirectory}/.cache/quickshell" $DRY_RUN_CMD install -m644 ${paletteFile} \ "${config.home.homeDirectory}/.config/lucid/themes/stylix/quickshell.json" $DRY_RUN_CMD install -m644 ${themeMeta} \ "${config.home.homeDirectory}/.config/lucid/themes/stylix/meta.json" if [ ! -e "${config.home.homeDirectory}/.cache/current_theme" ]; then $DRY_RUN_CMD install -m644 ${themeName} \ "${config.home.homeDirectory}/.cache/current_theme" fi if [ ! -e "${config.home.homeDirectory}/.cache/current_theme" ] || \ [ "$(${pkgs.coreutils}/bin/cat "${config.home.homeDirectory}/.cache/current_theme")" = stylix ]; then $DRY_RUN_CMD install -m644 ${paletteFile} \ "${config.home.homeDirectory}/.cache/quickshell/matugen.json" fi ''; systemd.user.services.lucid = { Unit = { Description = "Lucid bar, dock and desktop widgets"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; }; Service = { ExecStart = "${lucid}"; Restart = "on-failure"; RestartSec = 2; } // lib.optionalAttrs (!isChest) { ExecStartPre = "${startWallpaper}"; }; Install.WantedBy = [ "hyprland-session.target" ]; }; wayland.windowManager.hyprland = { settings = { on = [ (hyprLua.onStart [ "systemctl --user start lucid.service" ]) ]; bind = lib.mkIf (isRias || isChest) (lib.mkForce []); window_rule = lib.mkIf isChest [ { name = "float-lucid-settings"; match = { class = "org.quickshell"; title = "Lucid Settings"; }; float = true; size = [ 1180 800 ]; center = true; } { name = "float-lucid-choosers"; match = { class = "^(org\\.gnome\\.Nautilus|xdg-desktop-portal-gtk|xdg-desktop-portal-gnome|zenity|org\\.kde\\.kdialog|kdialog)$"; title = "^(Choose an account picture|Choose a template|Choose a colour scheme|Export the palette|Save received files to|Send files|Send to .+)$"; }; float = true; center = true; } ]; }; # Home Manager autoloads these before its generated settings. Lucid's # stateful UI modules are installed too, then required after the Nix base # config below so saved UI choices take effect without losing that base. extraLuaFiles = { "modules/binds.lua" = builtins.toPath (if isRias then "${source}/support/hypr/modules/binds.lua" else if isChest then "${chestBinds}" else "${nonRiasBinds}"); "modules/json.lua" = builtins.toPath "${source}/support/hypr/modules/json.lua"; "modules/specials.lua" = { content = builtins.toPath "${source}/support/hypr/modules/specials.lua"; autoLoad = false; }; } // lib.optionalAttrs isRias { "modules/decorations.lua" = builtins.toPath "${source}/support/hypr/modules/decorations.lua"; "modules/gestures.lua" = builtins.toPath "${source}/support/hypr/modules/gestures.lua"; "modules/input.lua" = builtins.toPath "${source}/support/hypr/modules/input.lua"; "modules/layout.lua" = builtins.toPath "${source}/support/hypr/modules/layout.lua"; "modules/layerrules.lua" = builtins.toPath "${source}/support/hypr/modules/layerrules.lua"; "modules/lumen.lua" = builtins.toPath "${source}/support/hypr/modules/lumen.lua"; "modules/misc.lua" = builtins.toPath "${source}/support/hypr/modules/misc.lua"; "modules/plugins.lua" = builtins.toPath "${source}/support/hypr/modules/plugins.lua"; "modules/windowrules.lua" = builtins.toPath "${source}/support/hypr/modules/windowrules.lua"; } // lib.optionalAttrs isChest { "modules/decorations.lua" = builtins.toPath "${chestBorders}"; } // lib.optionalAttrs (isRias || isChest) { "modules/glass.lua" = { content = builtins.toPath "${source}/support/hypr/modules/glass.lua"; autoLoad = false; }; "modules/monitors.lua" = { content = builtins.toPath (if isChest then "${chestMonitors}" else "${source}/support/hypr/modules/monitors.lua"); autoLoad = false; }; "modules/settings.lua" = { content = builtins.toPath "${source}/support/hypr/modules/settings.lua"; autoLoad = false; }; "modules/startup.lua" = { content = builtins.toPath "${source}/support/hypr/modules/startup.lua"; autoLoad = false; }; }; # Nix owns session startup and environment exports; don't load Lucid's # generic autostart/env defaults on top of the existing host setup. extraConfig = '' local config_home = os.getenv("XDG_CONFIG_HOME") or ${builtins.toJSON config.xdg.configHome} package.path = config_home .. "/hypr/?.lua;" .. config_home .. "/hypr/?/init.lua;" .. package.path require("modules.specials") LucidSpecials.apply() '' + lib.optionalString (isRias || isChest) '' require("modules.monitors") '' + lib.optionalString isRias '' local lucid_monitor_apply = LucidMonitors.apply LucidMonitors.apply = function(...) lucid_monitor_apply(...) hl.monitor({ output = "desc:Huawei Technologies Co. Inc. ZQE-CAA 0xC080F622", cm = "srgb", supports_hdr = -1 }) end LucidMonitors.apply() -- Lucid previously left empty workspaces 3-6 persistent on this host. -- Retire those stale rules after applying the monitor layout so they do -- not return on the next Hyprland reload. for _, workspace in ipairs({ "3", "4", "5", "6" }) do hl.workspace_rule({ workspace = workspace, persistent = false }) end '' + lib.optionalString (isRias || isChest) '' require("modules.settings") require("modules.glass") require("modules.startup") ''; }; }