From 43e1a82096bb50140038a12db8ec96e792e843bc Mon Sep 17 00:00:00 2001 From: Drake Bott Date: Fri, 15 May 2026 21:58:46 -0500 Subject: [PATCH 1/3] quote: XP-style awesome desktop, Minecraft demo, and launcher Builds out the `quote` host: an XP-styled awesome WM desktop with a Start menu and Quick Launch wibar, Bliss wallpaper, demo-mode keybindings (Alt+M, Alt+F window mirroring), and native resolution handling. Adds the quote-mc launcher that builds the 3rd-brain pack via bb, runs it through Prism, and drives a nested Xephyr :2 demo session with firefox + ghostty. --- .gitignore | 4 + README.md | 19 - flake.lock | 14 + flake.nix | 6 + home/common/ghostty.nix | 19 +- home/common/neovim/rocks.toml | 6 +- home/common/zellij.nix | 19 +- home/linux/default.nix | 28 +- home/linux/games/minecraft.nix | 8 +- home/linux/plasma/default.nix | 7 +- home/linux/quote/_prism-files.nix | 54 ++ home/linux/quote/awesome.nix | 127 ++++ home/linux/quote/default.nix | 15 + home/linux/quote/desktop.nix | 14 + home/linux/quote/launcher.nix | 200 +++++ home/linux/quote/notifications.nix | 17 + home/linux/quote/picom.nix | 31 + home/linux/quote/prism.nix | 20 + home/linux/quote/rc.fnl | 742 +++++++++++++++++++ home/linux/quote/sandbox.nix | 11 + hosts/quote/configuration.nix | 20 + hosts/quote/default.nix | 7 + hosts/quote/hardware-configuration.nix | 6 + lib/default.nix | 2 +- outputs/default.nix | 2 + outputs/hosts.nix | 6 + outputs/packages.nix | 7 + outputs/vms.nix | 69 ++ pkgs/xfce-winxp-tc/default.nix | 138 ++++ pkgs/xfce-winxp-tc/pinned-start-menu.patch | 312 ++++++++ pkgs/xfce-winxp-tc/taskband-class-name.patch | 156 ++++ scripts/README.md | 10 + scripts/quote-pack.clj | 11 + system/nixOS/default.nix | 29 +- system/nixOS/greetd.nix | 10 +- system/nixOS/quote/awesome.nix | 46 ++ system/nixOS/quote/default.nix | 6 + system/nixOS/webcam.nix | 4 + system/users/default.nix | 2 +- 39 files changed, 2137 insertions(+), 67 deletions(-) create mode 100644 home/linux/quote/_prism-files.nix create mode 100644 home/linux/quote/awesome.nix create mode 100644 home/linux/quote/default.nix create mode 100644 home/linux/quote/desktop.nix create mode 100644 home/linux/quote/launcher.nix create mode 100644 home/linux/quote/notifications.nix create mode 100644 home/linux/quote/picom.nix create mode 100644 home/linux/quote/prism.nix create mode 100644 home/linux/quote/rc.fnl create mode 100644 home/linux/quote/sandbox.nix create mode 100644 hosts/quote/configuration.nix create mode 100644 hosts/quote/default.nix create mode 100644 hosts/quote/hardware-configuration.nix create mode 100644 outputs/packages.nix create mode 100644 outputs/vms.nix create mode 100644 pkgs/xfce-winxp-tc/default.nix create mode 100644 pkgs/xfce-winxp-tc/pinned-start-menu.patch create mode 100644 pkgs/xfce-winxp-tc/taskband-class-name.patch create mode 100644 scripts/README.md create mode 100644 scripts/quote-pack.clj create mode 100644 system/nixOS/quote/awesome.nix create mode 100644 system/nixOS/quote/default.nix create mode 100644 system/nixOS/webcam.nix diff --git a/.gitignore b/.gitignore index 8c4b5447..13d544ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ CLAUDE.md .pre-commit-config.yaml .clj-kondo .lsp + +# VM disk images — mutable per-boot, never commit +*.qcow2 +result diff --git a/README.md b/README.md index d379e6d0..6562b53a 100644 --- a/README.md +++ b/README.md @@ -33,25 +33,6 @@ My personal MacBook uses [nix-darwin](https://github.com/nix-darwin/nix-darwin): darwin-rebuild switch --flake .#macbook ``` -### Android - -My -[Pixel 9 Pro Fold](https://store.google.com/us/product/pixel_9_pro_fold?hl=en-US) -uses [nixos-avf](https://github.com/nix-community/nixos-avf) on Android's Native -Linux terminal: - -```bash -sudo nixos-rebuild switch --flake .#android -``` - -### Standalone - -Standalone home-manager configuration: - -```bash -home-manager switch --flake .#standalone -``` - ## Manual Setup ### Tresorit diff --git a/flake.lock b/flake.lock index 1ff67379..b7998806 100644 --- a/flake.lock +++ b/flake.lock @@ -774,6 +774,19 @@ "type": "github" } }, + "quote-mc": { + "flake": false, + "locked": { + "lastModified": 1778884491, + "narHash": "sha256-x9VaDJndcIx1X1frfjtWaE4gfRcNO1FGVw7p71SSiYg=", + "path": "/home/drakeb/workspace/quote-mc", + "type": "path" + }, + "original": { + "path": "/home/drakeb/workspace/quote-mc", + "type": "path" + } + }, "root": { "inputs": { "claude-code": "claude-code", @@ -790,6 +803,7 @@ "nixpkgs-unstable": "nixpkgs-unstable", "plasma-manager": "plasma-manager", "pre-commit-hooks": "pre-commit-hooks", + "quote-mc": "quote-mc", "spicetify-nix": "spicetify-nix", "steam-config-nix": "steam-config-nix", "stylix": "stylix", diff --git a/flake.nix b/flake.nix index 63132943..26212968 100644 --- a/flake.nix +++ b/flake.nix @@ -56,6 +56,12 @@ inputs.nixpkgs.follows = "nixpkgs"; }; claude-code.url = "github:sadjow/claude-code-nix"; + + # local working copy; switch to a git ref once the mod stabilizes + quote-mc = { + url = "path:/home/drakeb/workspace/quote-mc"; + flake = false; + }; }; outputs = inputs @ { flake-parts, ... }: diff --git a/home/common/ghostty.nix b/home/common/ghostty.nix index abdc8842..11cbd79e 100644 --- a/home/common/ghostty.nix +++ b/home/common/ghostty.nix @@ -7,14 +7,22 @@ fi ''; + xdg.configFile = lib.mkIf pkgs.stdenv.isLinux { + "ghostty/no-rounded.css".text = '' + window, .background, .titlebar, .top-bar { + border-radius: 0; + } + ''; + }; + programs.ghostty = { enable = true; package = if pkgs.stdenv.isLinux then pkgs.ghostty else null; settings = { - command = lib.mkIf pkgs.stdenv.isLinux "zellij"; + command = lib.mkIf pkgs.stdenv.isLinux "${nixpkgs-unstable.zellij}/bin/zellij"; desktop-notifications = false; - font-family = lib.mkForce "MonoLisa Variable"; + font-family = "MonoLisa Variable"; font-family-bold = "MonoLisa Variable Regular Bold"; font-family-italic = "MonoLisa Variable Italic Italic"; font-family-bold-italic = "MonoLisa Variable Italic Bold Italic"; @@ -30,7 +38,12 @@ background-opacity = 1.0; - gtk-titlebar = true; + gtk-titlebar = lib.mkIf pkgs.stdenv.isLinux false; + gtk-custom-css = lib.mkIf pkgs.stdenv.isLinux "no-rounded.css"; + + # The default "single-instance" scope strips PATH down to systemd + + # ghostty; "never" keeps spawned commands as direct ghostty children. + linux-cgroup = lib.mkIf pkgs.stdenv.isLinux "never"; macos-titlebar-style = "native"; diff --git a/home/common/neovim/rocks.toml b/home/common/neovim/rocks.toml index 17450b2c..f9bb8728 100644 --- a/home/common/neovim/rocks.toml +++ b/home/common/neovim/rocks.toml @@ -117,9 +117,9 @@ rev = "2337f109f51a09297596dd6b538b70ccba92b4e4" git = "A7Lavinraj/fyler.nvim" rev = "v2.0.0" -[plugins."nvim-java"] -git = "nvim-java/nvim-java" -rev = "v4.1.0" +# [plugins."nvim-java"] +# git = "nvim-java/nvim-java" +# rev = "v4.1.0" [bundles.neorg] items = [ diff --git a/home/common/zellij.nix b/home/common/zellij.nix index a6d3312b..8183eba2 100644 --- a/home/common/zellij.nix +++ b/home/common/zellij.nix @@ -1,7 +1,20 @@ { pkgs, lib, nixpkgs-unstable, ... }: +let + # zellij splits copy_command via shell-words, so route through a wrapper + # script that picks wl-copy or xclip based on whether a Wayland socket + # is present at runtime (awesome/quote is X11, sway/niri is Wayland). + linuxCopy = pkgs.writeShellScript "zellij-copy" '' + if [ -n "$WAYLAND_DISPLAY" ]; then + exec ${pkgs.wl-clipboard}/bin/wl-copy + else + exec ${pkgs.xclip}/bin/xclip -selection clipboard -in + fi + ''; +in { home.packages = lib.optionals pkgs.stdenv.isLinux (with pkgs; [ wl-clipboard + xclip ]); programs.zellij = { @@ -10,12 +23,12 @@ settings = { theme = "default"; - default_shell = "zsh"; + default_shell = if pkgs.stdenv.isLinux then "${pkgs.zsh}/bin/zsh" else "zsh"; mouse_mode = true; - rounded_corners = true; + rounded_corners = false; show_startup_tips = false; show_release_notes = false; - copy_command = if pkgs.stdenv.isLinux then "wl-copy" else "pbcopy"; + copy_command = if pkgs.stdenv.isLinux then "${linuxCopy}" else "pbcopy"; copy_clipboard = "system"; keybinds = { "locked" = { diff --git a/home/linux/default.nix b/home/linux/default.nix index b7fed5c6..93f61771 100644 --- a/home/linux/default.nix +++ b/home/linux/default.nix @@ -1,17 +1,21 @@ { features, hostName, lib, ... }: +let + de = features.desktopEnvironment; + selfContained = { + plasma = ./plasma; + sway = ./sway; + quote = ./quote; + }; + perHost = lib.optional + (de != null && !(selfContained ? ${de})) + ./${de}/host/${hostName}.nix; +in { imports = [ ./theme.nix - ] ++ lib.optionals features.gui [ - ./desktop.nix - ./mpv - ] ++ lib.optionals features.gaming [ - ./games - ] ++ lib.optionals (features.desktopEnvironment == "plasma") [ - ./plasma - ] ++ lib.optionals (features.desktopEnvironment == "sway") [ - ./sway - ] ++ lib.optionals (features.desktopEnvironment != null && features.desktopEnvironment != "sway") [ - ./${features.desktopEnvironment}/host/${hostName}.nix - ]; + ] + ++ lib.optionals features.gui [ ./desktop.nix ./mpv ] + ++ lib.optionals features.gaming [ ./games ] + ++ lib.optional (de != null && selfContained ? ${de}) selfContained.${de} + ++ perHost; } diff --git a/home/linux/games/minecraft.nix b/home/linux/games/minecraft.nix index c86daceb..9f1fc146 100644 --- a/home/linux/games/minecraft.nix +++ b/home/linux/games/minecraft.nix @@ -6,12 +6,10 @@ }) ]; + # JAVA_HOME pins temurin for general use; prismlauncher carries its own jdks. + # Not using `programs.java` because it adds temurin to home.packages, which + # collides with the openjdk propagated by clojure/leiningen/babashka. home.sessionVariables = { JAVA_HOME = "${pkgs.temurin-bin-21}"; }; - - programs.java = { - enable = true; - package = pkgs.temurin-bin-21; - }; } diff --git a/home/linux/plasma/default.nix b/home/linux/plasma/default.nix index f779a97e..82509317 100644 --- a/home/linux/plasma/default.nix +++ b/home/linux/plasma/default.nix @@ -1,9 +1,12 @@ -{ pkgs, inputs, theme, ... }: +{ pkgs, inputs, theme, hostName, ... }: let isLight = theme.appearance == "light"; in { - imports = [ inputs.plasma-manager.homeModules.plasma-manager ]; + imports = [ + inputs.plasma-manager.homeModules.plasma-manager + ./host/${hostName}.nix + ]; home.packages = with pkgs; [ exfatprogs hfsprogs diff --git a/home/linux/quote/_prism-files.nix b/home/linux/quote/_prism-files.nix new file mode 100644 index 00000000..c17b170c --- /dev/null +++ b/home/linux/quote/_prism-files.nix @@ -0,0 +1,54 @@ +{ pkgs, appearance ? "dark" }: +let + java = "${pkgs.temurin-bin-17}/bin/java"; + + # Prism's registered application theme IDs are "system", "dark", "bright" + # (no "light" — that was triggering ThemeWizardPage every launch because + # isValidApplicationTheme rejected it, regenerating the welcome wizard). + applicationTheme = if appearance == "light" then "bright" else "dark"; + iconTheme = if appearance == "light" then "pe_light" else "pe_dark"; + + # Merges our keys into prismlauncher.cfg without clobbering Prism-owned + # fields (accounts, last-used instance, window state, etc.). Idempotent. + prism-apply-launcher-config = pkgs.writeShellApplication { + name = "prism-apply-launcher-config"; + runtimeInputs = [ pkgs.crudini ]; + text = '' + file="''${1:-}" + [ -n "$file" ] || { echo "prism-apply-launcher-config: file path required" >&2; exit 1; } + [ -f "$file" ] || printf '[General]\n' > "$file" + set_kv() { crudini --ini-options=nospace --set "$file" General "$1" "$2"; } + set_kv ApplicationTheme '${applicationTheme}' + set_kv IconTheme '${iconTheme}' + set_kv BackgroundCat kitteh + set_kv JavaPath '${java}' + set_kv AutomaticJavaSwitch false + set_kv AutomaticJavaDownload false + ''; + }; + + # Merges our keys into an existing Prism instance.cfg without clobbering + # Prism-owned fields (Name, InstanceType, iconKey, etc.). Idempotent. + prism-apply-instance-config = pkgs.writeShellApplication { + name = "prism-apply-instance-config"; + runtimeInputs = [ pkgs.crudini ]; + text = '' + file="''${1:-}" + [ -f "$file" ] || { echo "prism-apply-instance-config: file not found: $file" >&2; exit 1; } + # Prism (QSettings) emits `key=value` with no spaces; nospace keeps us off + # crudini's default `key = value` so the file stays byte-identical when no + # value actually changes. + set_kv() { crudini --ini-options=nospace --set "$file" General "$1" "$2"; } + set_kv OverrideJavaLocation true + set_kv JavaPath '${java}' + set_kv OverrideMemory true + set_kv MinMemAlloc 512 + set_kv MaxMemAlloc 4096 + set_kv OverrideJavaArgs true + set_kv JvmArgs '-Ddirector.autoScreensaver=true' + ''; + }; +in +{ + inherit prism-apply-launcher-config prism-apply-instance-config; +} diff --git a/home/linux/quote/awesome.nix b/home/linux/quote/awesome.nix new file mode 100644 index 00000000..d05a7ec8 --- /dev/null +++ b/home/linux/quote/awesome.nix @@ -0,0 +1,127 @@ +{ config, lib, pkgs, inputs, ... }: +let + inherit (config.lib.stylix) colors; + wintc = inputs.self.packages.${pkgs.system}.xfce-winxp-tc; + + # Procedural Bliss-style wallpaper. The real "Bliss" jpeg (Charles O'Rear, + # 2001) is copyrighted by Microsoft, so we approximate: sky vertical gradient + # + a green hill drawn as a quadratic-bezier band, softened with a small + # blur. Sized to the monitor's native res so wallpaper.maximized doesn't + # rescale. + blissPng = pkgs.runCommand "bliss.png" + { + nativeBuildInputs = [ pkgs.imagemagick ]; + } '' + convert -size 2560x1440 gradient:'#3a78c8'-'#a8d4ee' \ + -fill '#5a8d2c' \ + -draw "path 'M -10,960 Q 640,790 1280,910 Q 1920,1030 2570,840 L 2570,1450 L -10,1450 Z'" \ + -fill '#79b03f' \ + -draw "path 'M -10,960 Q 640,790 1280,910 Q 1920,1030 2570,840 L 2570,852 Q 1920,1042 1280,922 Q 640,802 -10,972 Z'" \ + -blur 0x1.6 \ + "$out" + ''; + + # wintc ships the original Luna titlebar button PNGs (21x23, four states each) + # under its xfwm4 theme. Reuse them instead of redrawing — guarantees pixel + # parity with the bar/start menu. + xpButtonDir = ''${wintc}/share/themes/Windows XP style (Blue)/xfwm4''; + + # rc.fnl uses @placeholder@ markers for stylix-driven colors; replaceVars fills them in. + rcFnl = pkgs.replaceVars ./rc.fnl { + bgNormal = "#${colors.base00}"; + bgFocus = "#${colors.base0B}"; + bgUrgent = "#${colors.base0C}"; + fgNormal = "#${colors.base05}"; + fgFocus = "#${colors.base00}"; + borderNormal = "#${colors.base02}"; + borderFocus = "#${colors.base0B}"; + blissPath = "${blissPng}"; + inherit xpButtonDir; + }; + + # whitelist awesome's bare globals so fennel still catches typos + rcLua = pkgs.runCommand "awesome-rc.lua" { } '' + ${pkgs.luaPackages.fennel}/bin/fennel \ + --globals "client,screen,root,awesome,tag,mouse" \ + --compile ${rcFnl} > $out + ''; + + # OBS icon override; ship 48/128/256 so exact-size lookups don't fall back to upstream + obsRecordIcons = pkgs.runCommand "obs-record-icons" + { + nativeBuildInputs = [ pkgs.imagemagick ]; + } '' + mkdir -p $out + convert -size 256x256 xc:transparent \ + -fill '#dcdcdc' -draw 'circle 128,128 128,11' \ + -fill '#1e1e1e' -draw 'circle 128,128 128,33' \ + -fill '#b40f0f' -draw 'circle 128,128 128,58' \ + -fill 'rgba(255,255,255,0.55)' -draw 'ellipse 128,86 48,22 0,360' \ + "$out/256.png" + for size in 48 128; do + convert "$out/256.png" -resize "''${size}x''${size}" "$out/$size.png" + done + ''; +in +{ + xsession = { + enable = true; + windowManager.awesome = { + enable = true; + package = pkgs.awesome; + }; + # Pin the monitor mode before awesome starts so screen.geometry and the + # MC "wallpaper" placement see the native resolution. Hardware-specific + # to the quote host (single DP-1 panel at 2560x1440 @ 170Hz native). + initExtra = '' + ${pkgs.xorg.xrandr}/bin/xrandr --output DP-1 --mode 2560x1440 --rate 170.07 --primary || true + ''; + }; + + # XP cursors are not freely redistributable, so nixpkgs has nothing labelled + # "Windows XP". Vanilla-DMZ is the closest free family — it's the DMZ theme + # the Windows-style cursor packs derive from. + stylix.cursor = { + package = pkgs.vanilla-dmz; + name = "Vanilla-DMZ"; + size = 24; + }; + + xdg = { + configFile = { + "awesome/rc.lua".source = rcLua; + }; + dataFile = { + "icons/hicolor/48x48/apps/com.obsproject.Studio.png".source = + "${obsRecordIcons}/48.png"; + "icons/hicolor/128x128/apps/com.obsproject.Studio.png".source = + "${obsRecordIcons}/128.png"; + "icons/hicolor/256x256/apps/com.obsproject.Studio.png".source = + "${obsRecordIcons}/256.png"; + }; + }; + + home.packages = with pkgs; [ + rofi + xdotool + wmctrl + xorg.xprop + xorg.xwininfo + xorg.xset + xorg.xrandr + xcape # tap-Super alone → emits Super+Escape, our start-menu toggle + networkmanagerapplet # nm-applet for the systray + pasystray # XP-style volume/output icon in the tray, talks to pipewire-pulse + # corefonts ships Tahoma (and Verdana) under the MS EULA - the actual XP + # face the taskband and titlebars target. + corefonts + wintc + ]; + + # Reload the running awesome in-place after each switch; no-op without DISPLAY. + home.activation.reloadAwesome = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ -n "''${DISPLAY:-}" ]; then + run ${pkgs.awesome}/bin/awesome-client 'awesome.restart()' >/dev/null 2>&1 || true + fi + ''; +} diff --git a/home/linux/quote/default.nix b/home/linux/quote/default.nix new file mode 100644 index 00000000..ac698bef --- /dev/null +++ b/home/linux/quote/default.nix @@ -0,0 +1,15 @@ +{ lib, ... }: +{ + imports = [ + ./awesome.nix + ./picom.nix + ./launcher.nix + ./desktop.nix + ./notifications.nix + ./sandbox.nix + ./prism.nix + ]; + + # atuin panics on first launch in a fresh home (settings.rs:617) + programs.atuin.enable = lib.mkForce false; +} diff --git a/home/linux/quote/desktop.nix b/home/linux/quote/desktop.nix new file mode 100644 index 00000000..5b34a953 --- /dev/null +++ b/home/linux/quote/desktop.nix @@ -0,0 +1,14 @@ +_: +{ + xdg.desktopEntries.quote-mc = { + name = "Quote-MC"; + genericName = "Minecraft Beta 1.7.3 (Quote-MC pack)"; + comment = "Launch the Quote-MC modpack (Babric + StationAPI QoL + local quote-mc build)."; + exec = "quote-mc-launch"; + icon = "org.prismlauncher.PrismLauncher"; + terminal = false; + type = "Application"; + categories = [ "Game" ]; + startupNotify = false; + }; +} diff --git a/home/linux/quote/launcher.nix b/home/linux/quote/launcher.nix new file mode 100644 index 00000000..feaeedf6 --- /dev/null +++ b/home/linux/quote/launcher.nix @@ -0,0 +1,200 @@ +{ config, pkgs, theme, ... }: +let + prismDir = "${config.home.homeDirectory}/QuoteMC/prism"; + defaultPackRoot = "${config.home.homeDirectory}/workspace/3rd-brain"; + instanceId = "Quote-MC"; + prismFiles = import ./_prism-files.nix { inherit pkgs; inherit (theme) appearance; }; + + # avoids buildEnv collision with the same name auto-built by home/common/scripts.nix + inherit ((import ../../../scripts { inherit pkgs; })) quote-pack; + + # packwiz-installer materializes the local pack into a Prism instance's + # .minecraft/, replacing the hand-rolled .pw.toml regex parser + downloader + # that used to live in 3rd-brain/scripts/sync-mods.bb. We pin the bootstrap + # AND the main installer jar so launches stay offline-deterministic — the + # bootstrap's normal job is to self-update the installer from GitHub at run + # time, which `--bootstrap-no-update --bootstrap-main-jar` short-circuits. + packwizInstallerBootstrap = pkgs.fetchurl { + url = "https://github.com/packwiz/packwiz-installer-bootstrap/releases/download/v0.0.3/packwiz-installer-bootstrap.jar"; + hash = "sha256-qPuyTcYEJ46X9GiOgtPZGjGLmO/AjV2/y8vKtkQ9EWw="; + }; + packwizInstaller = pkgs.fetchurl { + url = "https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar"; + hash = "sha256-yfZGkI00DYR3OUipp9mLwdriUNNeEBbcbiuEWXYLVZg="; + }; + + quote-mc-launch = pkgs.writeShellApplication { + name = "quote-mc-launch"; + runtimeInputs = with pkgs; [ + prismlauncher + temurin-bin-21 + git + coreutils + libnotify + babashka + packwiz + quote-pack + prismFiles.prism-apply-instance-config + # MirrorCapture shells out to xdotool to resolve a window title to its + # screen geometry; without it on PATH the lookup fails silently and + # capture falls back to (0,0), grabbing whatever's at the screen origin. + xdotool + # Demo stage: Xephyr hosts a nested :2 X server whose framebuffer + # MirrorCapture grabs into MC monitor blocks; firefox + ghostty are + # the captured windows. MC stays on :0 fullscreen; :2 lives in a + # background window so occlusion can't break the capture. + xorg.xorgserver + firefox + ghostty + # While attached, Xephyr's grab masks awesome's :0 Alt+F. xbindkeys runs + # inside :2 and binds Alt+F to touch the detach-request flag rc.fnl polls. + xbindkeys + ]; + text = '' + set -euo pipefail + + pack_root="''${QUOTE_PACK_ROOT:-${defaultPackRoot}}" + export QUOTE_PACK_ROOT="$pack_root" + # Consumed by 3rd-brain/scripts/sync-mods.bb (`bb sync` task). + export PACKWIZ_INSTALLER_BOOTSTRAP_JAR='${packwizInstallerBootstrap}' + export PACKWIZ_INSTALLER_JAR='${packwizInstaller}' + instance_dir="${prismDir}/instances/${instanceId}" + instance_cfg="$instance_dir/instance.cfg" + instance_mods="$instance_dir/.minecraft/mods" + + if [ ! -d "$pack_root/modpack" ]; then + echo "quote-mc-launch: $pack_root/modpack not found." >&2 + notify-send -u critical "quote-mc-launch" "pack missing at $pack_root" || true + exit 1 + fi + + # first run: no .pw.toml means QoL mods aren't pinned yet + if [ -z "$(find "$pack_root/modpack/mods" -maxdepth 1 -name '*.pw.toml' -print -quit 2>/dev/null || true)" ]; then + quote-pack install-mods + fi + + quote-pack pack + + if [ ! -d "$instance_dir" ]; then + mrpack="$(find "$pack_root/modpack" -maxdepth 1 -name 'Quote-MC-*.mrpack' -print -quit || true)" + if [ -z "$mrpack" ]; then + echo "quote-mc-launch: no .mrpack found under $pack_root/modpack" >&2 + exit 1 + fi + notify-send "Quote-MC" "First-time import: click OK in the dialog, then close Prism. Quote-MC will launch automatically." || true + # No exec: we block on Prism, then continue once the user closes it. + prismlauncher --dir "${prismDir}" --import "$mrpack" || true + if [ ! -d "$instance_dir" ]; then + notify-send -u critical "Quote-MC" "Import wasn't completed. Re-run quote-mc-launch when ready." || true + exit 1 + fi + fi + + # Idempotent: stamps our Java/memory/JvmArgs over Prism's import defaults. + prism-apply-instance-config "$instance_cfg" + + quote-pack sync "$instance_mods" + + # Demo stage: ensure the nested :2 server and the captured windows are + # up before MC launches. Each block is a no-op if the process already + # exists, so re-running the launcher doesn't multiply. + # + # Capture geometry: the 3x2-block monitor wall captures 768x512 px + # (DEFAULT_MIRROR_PIXELS_PER_BLOCK = 256). The :2 server is exactly that + # size and firefox + ghostty are *stacked* — both sized to fill it, both + # at (0,0). The capture rect is the whole screen; the lever just raises + # whichever window it wants on top (MirrorCapture's resolveOffset raises + # the title it is handed). + cap_w=768 + cap_h=512 + + # The demo stack (Xephyr :2 + its windows + xbindkeys) is reused across + # launcher runs via the pgrep/search guards below. But a stack left over + # from before a launcher change silently masks the new config — stale + # Xephyr flags, stale window titles — because the guards see "already + # running" and skip recreation. Stamp the stack; if a running one carries + # a different stamp (or none), tear the whole thing down so it + # regenerates clean. Bump demo_stamp whenever the demo spec below changes. + director_dir="''${XDG_RUNTIME_DIR:-/tmp}/director" + mkdir -p "$director_dir" + demo_stamp_file="$director_dir/demo-stack.stamp" + demo_stamp='v3-stacked' + if pgrep -f 'Xephyr.*:2' >/dev/null \ + && [ "$(cat "$demo_stamp_file" 2>/dev/null || true)" != "$demo_stamp" ]; then + pkill -f 'Xephyr.*:2' || true + pkill -f 'xbindkeys.*xbindkeys-detach' || true + rm -f "$demo_stamp_file" + # firefox/ghostty on :2 exit on their own once the server is gone. + sleep 1 + fi + + # -sw-cursor: render the pointer into Xephyr's framebuffer. The X cursor + # is normally a separate sprite the server strips from XGetImage/x11grab + # results, so MirrorCapture would mirror everything *except* the cursor. + if ! pgrep -f 'Xephyr.*:2' >/dev/null; then + Xephyr :2 -screen "$cap_w"x"$cap_h" -ac -sw-cursor >/dev/null 2>&1 & + sleep 1 + fi + if ! DISPLAY=:2 xdotool search --name firefox >/dev/null 2>&1; then + # --no-remote + a dedicated profile: a plain `firefox` would hand the + # URL to the existing :0 instance instead of opening a window on :2. + # firefox errors ("profile cannot be loaded") if --profile points at a + # path that doesn't exist — it won't create it, so mkdir first. + mkdir -p "$HOME/.cache/quote-demo-ff" + # Firefox seeds the new-tab top-sites grid with a sponsored tile + # ("pinned from history" UI). A user.js in the profile disables it + # before the window paints — rewritten each run since the prefs are + # cheap and the profile may have been wiped. + printf '%s\n' \ + 'user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);' \ + 'user_pref("browser.newtabpage.activity-stream.showSponsored", false);' \ + > "$HOME/.cache/quote-demo-ff/user.js" + DISPLAY=:2 firefox --no-remote --profile "$HOME/.cache/quote-demo-ff" \ + 'https://www.youtube.com/watch?v=QmCfdA4Nz0w' >/dev/null 2>&1 & + fi + if ! DISPLAY=:2 xdotool search --name ghostty >/dev/null 2>&1; then + # Bare ghostty (no claude). --title pins the window title so the + # shell inside can't rewrite it out from under MirrorCapture's + # xdotool --name match. + DISPLAY=:2 ghostty --title=ghostty >/dev/null 2>&1 & + fi + + # :2 has no window manager, so windows spawn at their own default + # geometry — size each to fill the :2 screen and stack both at (0,0). + # They fully overlap; MirrorCapture raises whichever the lever selects. + place_demo_window() { + local name="$1" wid="" + for _ in $(seq 1 50); do + # `|| true` inside the subshell: xdotool exits 1 on no match, and + # `set -o pipefail` would otherwise propagate that and trip `set -e`. + wid="$(DISPLAY=:2 xdotool search --name "$name" 2>/dev/null | head -1 || true)" + [ -n "$wid" ] && break + sleep 0.2 + done + if [ -n "$wid" ]; then + DISPLAY=:2 xdotool windowsize "$wid" "$cap_w" "$cap_h" >/dev/null 2>&1 || true + DISPLAY=:2 xdotool windowmove "$wid" 0 0 >/dev/null 2>&1 || true + fi + } + place_demo_window firefox + place_demo_window ghostty + + # xbindkeys on :2 binds Alt+F to `touch detach-request`; rc.fnl polls + # that flag to detach (awesome's :0 Alt+F is masked by Xephyr's grab). + xbindkeys_cfg="$director_dir/xbindkeys-detach.conf" + printf '"touch %s/detach-request"\n Alt + f\n' "$director_dir" > "$xbindkeys_cfg" + if ! pgrep -f 'xbindkeys.*xbindkeys-detach' >/dev/null; then + DISPLAY=:2 xbindkeys -n -f "$xbindkeys_cfg" >/dev/null 2>&1 & + fi + + # Stack is fully up — record the stamp so the next launcher run reuses it + # instead of tearing it down. + printf '%s' "$demo_stamp" > "$demo_stamp_file" + + exec prismlauncher --dir "${prismDir}" --launch "${instanceId}" + ''; + }; +in +{ + home.packages = [ quote-mc-launch ]; +} diff --git a/home/linux/quote/notifications.nix b/home/linux/quote/notifications.nix new file mode 100644 index 00000000..7e95a241 --- /dev/null +++ b/home/linux/quote/notifications.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: +{ + services.dunst = { + enable = true; + settings.global = { + # font is set by stylix system-wide + frame_width = 1; + corner_radius = 2; + offset = "12x12"; + origin = "top-right"; + notification_limit = 5; + idle_threshold = 120; + }; + }; + + home.packages = [ pkgs.libnotify ]; +} diff --git a/home/linux/quote/picom.nix b/home/linux/quote/picom.nix new file mode 100644 index 00000000..a6955266 --- /dev/null +++ b/home/linux/quote/picom.nix @@ -0,0 +1,31 @@ +{ pkgs, ... }: +{ + services.picom = { + enable = true; + package = pkgs.picom; + # xrender is safer under virtio-vga; switch to glx once that's confirmed on metal + backend = "xrender"; + vSync = false; + + shadow = true; + fade = true; + fadeDelta = 4; + + settings = { + # shadows on xwinwrap's root drawable smear the wallpaper layer + shadow-exclude = [ + "class_g = 'xwinwrap'" + "name = 'Notification'" + ]; + + detect-rounded-corners = true; + detect-client-opacity = true; + use-damage = true; + + inactive-opacity = 0.95; + active-opacity = 1.0; + frame-opacity = 1.0; + inactive-opacity-override = false; + }; + }; +} diff --git a/home/linux/quote/prism.nix b/home/linux/quote/prism.nix new file mode 100644 index 00000000..934a3138 --- /dev/null +++ b/home/linux/quote/prism.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, theme, ... }: +let + prismDir = "${config.home.homeDirectory}/QuoteMC/prism"; + instanceCfg = "${prismDir}/instances/Quote-MC/instance.cfg"; + prismFiles = import ./_prism-files.nix { inherit pkgs; inherit (theme) appearance; }; +in +{ + home.packages = [ + prismFiles.prism-apply-launcher-config + prismFiles.prism-apply-instance-config + ]; + + home.activation.prismConfig = + lib.hm.dag.entryAfter [ "writeBoundary" "quoteSandbox" ] '' + run ${prismFiles.prism-apply-launcher-config}/bin/prism-apply-launcher-config '${prismDir}/prismlauncher.cfg' + if [ -f '${instanceCfg}' ]; then + run ${prismFiles.prism-apply-instance-config}/bin/prism-apply-instance-config '${instanceCfg}' + fi + ''; +} diff --git a/home/linux/quote/rc.fnl b/home/linux/quote/rc.fnl new file mode 100644 index 00000000..84a62739 --- /dev/null +++ b/home/linux/quote/rc.fnl @@ -0,0 +1,742 @@ +;;; quote awesome config (fennel, compiled to rc.lua by home-manager). +;;; minecraft b1.7.3 sits below as a click-through wallpaper. +;;; +;;; Bar + start menu come from xfce-winxp-tc (wintc-taskband), spawned at +;;; startup. We keep awesome responsible for titlebars, tags, demo mode, and +;;; MC framing — the taskband owns the bottom strip. + +(local gears (require :gears)) +(local awful (require :awful)) +(require :awful.autofocus) +(local beautiful (require :beautiful)) +(local naughty (require :naughty)) +(local wibox (require :wibox)) + +;;; surface errors as notifications +(when awesome.startup_errors + (naughty.notify {:preset naughty.config.presets.critical + :title "Awesome startup error" + :text awesome.startup_errors})) + +(var in-error false) +(awesome.connect_signal "debug::error" + (fn [err] + (when (not in-error) + (set in-error true) + (naughty.notify {:preset naughty.config.presets.critical + :title "Awesome runtime error" + :text (tostring err)}) + (set in-error false)))) + +;;; Theme +(beautiful.init (.. (gears.filesystem.get_themes_dir) :default/theme.lua)) +(set beautiful.font "Tahoma 8") +(set beautiful.bg_normal "@bgNormal@") +(set beautiful.bg_focus "@bgFocus@") +(set beautiful.bg_urgent "@bgUrgent@") +(set beautiful.fg_normal "@fgNormal@") +(set beautiful.fg_focus "@fgFocus@") +(set beautiful.border_width 1) +(set beautiful.border_normal "@borderNormal@") +(set beautiful.border_focus "@borderFocus@") +(set beautiful.useless_gap 4) + +(local terminal :ghostty) +(local modkey :Mod4) +(local altkey :Mod1) + +;; floating first; tile variants stay reachable via Mod+e/s/w +(set awful.layout.layouts [awful.layout.suit.floating + awful.layout.suit.tile + awful.layout.suit.tile.bottom + awful.layout.suit.max]) + +(local bliss-path "@blissPath@") + +(fn set-wallpaper [s] + ;; ignore_aspect=false: scale-to-fill with crop, no distortion. + (gears.wallpaper.maximized bliss-path s false)) + +(screen.connect_signal "property::geometry" set-wallpaper) + +;;; --- XP Luna titlebars --- + +(local xp-font-title "Tahoma Bold 9") +(local xp-text-white "#ffffff") + +;; XP Luna palette (titlebar gradients) +(local xp-luna-shine "#5d92e7") +(local xp-luna-top "#3674d6") +(local xp-luna-mid "#245ecb") +(local xp-luna-bot "#1741a3") +(local xp-titlebar-inactive-top "#7c91b3") +(local xp-titlebar-inactive-bot "#475377") + +(fn vgradient-h [stops h] + (gears.color {:type :linear :from [0 0] :to [0 h] : stops})) + +;; wintc ships hide/maximize/close PNGs (21x23, four variants each); the path +;; is filled in by replaceVars. See awesome.nix:xpButtonDir for rationale. +(local xp-button-dir "@xpButtonDir@") + +(fn xp-button-icon [name variant] + (.. xp-button-dir "/" name "-" variant :.png)) + +;; sized so the native 23-tall button PNGs fit with 1px of breathing room +(local xp-titlebar-height 24) +(local xp-titlebar-bg-focus + (vgradient-h [[0 xp-luna-shine] + [0.04 xp-luna-top] + [0.5 xp-luna-mid] + [1 xp-luna-bot]] xp-titlebar-height)) + +(local xp-titlebar-bg-normal + (vgradient-h [[0 xp-titlebar-inactive-top] [1 xp-titlebar-inactive-bot]] + xp-titlebar-height)) + +;; c.name is per-tab/world content; we want the app identity instead. +(fn xp-titlebar-title [c] + (let [w (wibox.widget.textbox) + update (fn [] (w:set_text (or c.class "")))] + (update) + (c:connect_signal "property::class" update) + w)) + +(fn xp-titlebar-button [c {: name : on-click}] + (let [active (xp-button-icon name :active) + inactive (xp-button-icon name :inactive) + prelight (xp-button-icon name :prelight) + pressed (xp-button-icon name :pressed) + idle (fn [] (if (= client.focus c) active inactive)) + img (wibox.widget {:widget wibox.widget.imagebox + :image (idle) + :forced_width 21 + :forced_height 23})] + (c:connect_signal :focus (fn [] (set img.image (idle)))) + (c:connect_signal :unfocus (fn [] (set img.image (idle)))) + (img:connect_signal "mouse::enter" (fn [] (set img.image prelight))) + (img:connect_signal "mouse::leave" (fn [] (set img.image (idle)))) + (img:connect_signal "button::press" (fn [] (set img.image pressed))) + ;; release-without-leave restores prelight (pointer still over button); + ;; the mouse::leave handler will swap to idle if the user moves away. + (img:connect_signal "button::release" (fn [] (set img.image prelight))) + (img:buttons (gears.table.join (awful.button [] 1 on-click))) + img)) + +(client.connect_signal "request::titlebars" + (fn [c] + (let [tb (awful.titlebar c + {:size xp-titlebar-height + :bg_focus xp-titlebar-bg-focus + :bg_normal xp-titlebar-bg-normal + :fg_focus xp-text-white + :fg_normal "#d9dde9" + :font xp-font-title}) + icon-widget (awful.titlebar.widget.iconwidget c) + title-widget (xp-titlebar-title c) + drag-buttons (gears.table.join (awful.button [] + 1 + (fn [] + (c:emit_signal "request::activate" + :titlebar + {:raise true}) + (awful.mouse.client.move c))) + (awful.button [] + 3 + (fn [] + (c:emit_signal "request::activate" + :titlebar + {:raise true}) + (awful.mouse.client.resize c))))] + (set title-widget.align :left) + (set title-widget.font xp-font-title) + (tb:setup {:layout wibox.layout.align.horizontal + 1 {:layout wibox.layout.fixed.horizontal + 1 {:widget wibox.container.margin + :left 4 + :right 6 + :top 3 + :bottom 3 + 1 icon-widget}} + 2 (let [w (wibox.widget {:widget wibox.container.margin + :top 2 + :bottom 2 + 1 title-widget})] + (w:buttons drag-buttons) + w) + 3 {:layout wibox.layout.fixed.horizontal + :spacing 2 + 1 (xp-titlebar-button c + {:name :hide + :on-click (fn [] + (set c.minimized + true))}) + 2 (xp-titlebar-button c + {:name :maximize + :on-click (fn [] + (set c.maximized + (not c.maximized)) + (c:raise))}) + 3 {:widget wibox.container.margin + :right 4 + 1 (xp-titlebar-button c + {:name :close + :on-click (fn [] + (c:kill))})}}})))) + +;;; Per-screen setup: wallpaper + tags. The taskband owns the bottom strip. + +(awful.screen.connect_for_each_screen (fn [s] + (set-wallpaper s) + (awful.tag [:1 :2 :3 :4] s + (. awful.layout.layouts 1)))) + +(local clientkeys + (gears.table.join (awful.key [modkey] :q (fn [c] (c:kill))) + (awful.key [modkey] :f + (fn [c] + (set c.fullscreen (not c.fullscreen)) + (c:raise))) + (awful.key [modkey :Shift] :space + awful.client.floating.toggle))) + +(local clientbuttons + (gears.table.join (awful.button [] 1 + (fn [c] + (when c.focusable + (c:emit_signal "request::activate" + :mouse_click + {:raise true})))) + (awful.button [modkey] 1 + (fn [c] + (c:emit_signal "request::activate" + :mouse_click + {:raise true}) + (awful.mouse.client.move c))) + (awful.button [modkey] 3 + (fn [c] + (c:emit_signal "request::activate" + :mouse_click + {:raise true}) + (awful.mouse.client.resize c))))) + +;;; Alt+m / Alt+Shift+m switch between desk mode and demo mode: +;;; - desk mode (default): other screen-1 windows visible, MC frozen in the +;;; back as a wallpaper-style cinematic (director's autoScreensaver on). +;;; - demo mode: MC focused with the pointer warped into it, MC unfrozen for +;;; live play. Alt+m enters soft demo, leaving overlay windows in place. +;;; Alt+Shift+m enters hard demo, also minimizing other screen-1 windows +;;; out of the way. Either key leaves demo mode. +;;; The director flag is a filesystem touchpoint the mod watches; toggling it +;;; flips the freeze state. We mirror that flip with WM focus + minimize state +;;; so a single keypress drives both layers. +(local director-flag (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) + :/director/toggle-screensaver)) + +;; toggle-screensaver flips the freeze state, but its result depends on which +;; screen is currently open — it can't *guarantee* an outcome. attach/detach +;; need determinism, so they use directional flags: freeze-screensaver always +;; pauses into the cinematic; resume-screensaver always returns to live play +;; (and can never strand the game paused, whatever screen was showing). +(local director-freeze-flag + (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) + :/director/freeze-screensaver)) + +(local director-resume-flag + (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) + :/director/resume-screensaver)) + +(fn touch-director-flag [] + (awful.spawn [:touch director-flag])) + +(fn touch-director-freeze [] + (awful.spawn [:touch director-freeze-flag])) + +(fn touch-director-resume [] + (awful.spawn [:touch director-resume-flag])) + +(fn find-mc-client [] + (var found nil) + (each [_ c (ipairs (client.get)) &until found] + (when (and c.class + (or (= c.class :Minecraft) + (= c.class :org-prismlauncher-EntryPoint))) + (set found c))) + found) + +(var demo-mode? false) +(var hidden-by-demo []) +(var pre-demo-focus nil) + +(fn warp-pointer-to-client [c] + (let [g (c:geometry) + cx (math.floor (+ g.x (/ g.width 2))) + cy (math.floor (+ g.y (/ g.height 2)))] + (mouse.coords {:x cx :y cy}))) + +;; MC (GLFW) only re-grabs the pointer on a real mouse-button event, so after +;; warping into demo mode we synthesize a left click via XTest. Deferred to the +;; next main-loop tick so the focus + warp have been flushed to the X server +;; before the click lands. +(fn synthesize-left-click [] + (gears.timer.delayed_call (fn [] + (root.fake_input :button_press 1) + (root.fake_input :button_release 1)))) + +(fn enter-demo-mode-soft [mc] + (set hidden-by-demo []) + (set pre-demo-focus client.focus) + (set client.focus mc) + (warp-pointer-to-client mc) + (synthesize-left-click) + (set demo-mode? true) + (touch-director-flag)) + +(fn enter-demo-mode [mc] + (set hidden-by-demo []) + (set pre-demo-focus client.focus) + (each [_ c (ipairs (client.get))] + ;; Skip wintc-taskband so the bottom bar stays visible in demo mode. + (when (and (= c.screen mc.screen) (not= c mc) (not c.minimized) + (not= c.class :Wintc-taskband)) + (table.insert hidden-by-demo c) + (set c.minimized true))) + (set client.focus mc) + (warp-pointer-to-client mc) + (synthesize-left-click) + (set demo-mode? true) + (touch-director-flag)) + +(fn leave-demo-mode [] + (each [_ c (ipairs hidden-by-demo)] + (when c.valid (set c.minimized false))) + (set hidden-by-demo []) + (when (and pre-demo-focus pre-demo-focus.valid (not pre-demo-focus.minimized)) + (set client.focus pre-demo-focus)) + (set pre-demo-focus nil) + (set demo-mode? false) + (touch-director-flag)) + +(fn director-toggle-soft [] + (if demo-mode? + (leave-demo-mode) + (let [mc (find-mc-client)] + (if mc + (enter-demo-mode-soft mc) + ;; MC not running yet (early startup or crash) — fall back to + ;; bare flag toggle so the keybind isn't dead. + (touch-director-flag))))) + +(fn director-toggle [] + (if demo-mode? + (leave-demo-mode) + (let [mc (find-mc-client)] + (if mc + (enter-demo-mode mc) + (touch-director-flag))))) + +;;; Alt+F: attach focus to the X11 window mirrored on the monitor block under +;;; the player's crosshair. Two-way IPC with quote-mc: +;;; awesome → quote-mc: touch attach-request +;;; quote-mc → awesome: write window title (or "" for no target) to +;;; attach-response, with a trailing newline so the file +;;; is always non-empty once written +;;; The mirrored windows (firefox, ghostty) run inside a nested Xephyr :2 +;;; server — invisible to awesome's client.get, which only sees :0. And MC +;;; sits fullscreen *above* the (background) Xephyr window, so simply focusing +;;; Xephyr can't route the pointer there. Instead, on attach we: +;;; 1. save focus + pointer, focus the Xephyr window (MC's GLFW releases its +;;; pointer grab on FocusOut), and touch the freeze flag; +;;; 2. DISPLAY=:2 xdotool focuses the specific window within :2 by the title +;;; quote-mc reported; +;;; 3. synthesize Ctrl+Shift to fire Xephyr's own grab, confining real input +;;; into :2 regardless of MC being on top. +;;; Xephyr's grab also holds the keyboard, so awesome's :0 Alt+F can't reach +;;; us while attached. An xbindkeys on :2 (started by the launcher) binds +;;; Alt+F to `touch detach-request`; awesome polls that flag while attached, +;;; and on seeing it drops Xephyr's grab (Ctrl+Shift), restores MC focus + +;;; pointer, unfreezes, and re-grabs MC's cursor — so Alt+F detaches too. +;;; Only fires while in demo mode — outside it, the crosshair has no meaning. +(local attach-request-flag + (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) :/director/attach-request)) + +(local attach-response-flag + (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) :/director/attach-response)) + +;; xbindkeys on :2 touches this when Alt+F is pressed inside Xephyr; rc.fnl +;; polls it to detach (awesome's :0 Alt+F can't reach us under Xephyr's grab). +(local detach-request-flag + (.. (or (os.getenv :XDG_RUNTIME_DIR) :/tmp) :/director/detach-request)) + +(var attached? false) +(var attach-target nil) +(var attach-saved-pointer nil) +(var attach-saved-focus nil) +(var attach-target-was-hidden? false) +(var detach-poll-timer nil) + +(fn trim [s] + (let [s2 (string.gsub s "^%s+" "") + s3 (string.gsub s2 "%s+$" "")] + s3)) + +(fn file-exists? [path] + (let [f (io.open path :r)] + (if f (do + (f:close) true) false))) + +(fn regex-escape [s] + ;; xdotool's `search --name` takes a POSIX extended regex; escape the + ;; metacharacters so a window title matches itself literally. + (pick-values 1 (string.gsub s "([%(%)%.%+%-%*%?%[%]%^%${}|\\])" "\\%1"))) + +(fn shell-quote [s] + (.. "'" (pick-values 1 (string.gsub s "'" "'\\''")) "'")) + +;; Xephyr doesn't reliably set WM_CLASS, so also match on its window name +;; ("Xephyr on :2.0 (ctrl+shift grabs ...)"). +(fn xephyr-client? [c] + (or (= c.class :Xephyr) (and c.name (string.find c.name :Xephyr 1 true) true))) + +(fn find-xephyr-client [] + (var found nil) + (each [_ c (ipairs (client.get)) &until found] + (when (xephyr-client? c) + (set found c))) + found) + +;; After MC loses focus its GLFW pointer grab is released; once that's flushed +;; we synthesize Ctrl+Shift to the (now focused) Xephyr window, firing its +;; built-in grab so real input is confined into the :2 server. The delay lets +;; MC's FocusOut land first — Xephyr's XGrabPointer fails if MC still holds one. +(fn synthesize-xephyr-grab [] + (gears.timer.start_new 0.2 (fn [] + (awful.spawn [:xdotool :key :ctrl+shift]) + false))) + +(fn stop-detach-poll [] + (when detach-poll-timer + (detach-poll-timer:stop) + (set detach-poll-timer nil))) + +(fn detach-from-window [] + (when attached? + (stop-detach-poll) + (os.remove detach-request-flag) + (when (and attach-target attach-target.valid) + (when attach-target-was-hidden? + ;; we un-minimized it on attach; re-hide so demo-mode invariants hold + (table.insert hidden-by-demo attach-target) + (set attach-target.minimized true))) + (when (and attach-saved-focus attach-saved-focus.valid + (not attach-saved-focus.minimized)) + (set client.focus attach-saved-focus)) + (when attach-saved-pointer + (mouse.coords {:x attach-saved-pointer.x :y attach-saved-pointer.y})) + ;; resume deterministically — detach must never leave the game paused + (touch-director-resume) + ;; pointer is back over MC but GLFW only re-grabs on a real button event + (synthesize-left-click) + (set attached? false) + (set attach-target nil) + (set attach-saved-pointer nil) + (set attach-saved-focus nil) + (set attach-target-was-hidden? false))) + +;; Alt+F pressed inside Xephyr: the xbindkeys on :2 has touched detach-request. +;; Xephyr still holds the host grab, so first toggle it off with Ctrl+Shift, +;; then once that release flushes hand off to detach-from-window. +(fn handle-detach-request [] + (stop-detach-poll) + (os.remove detach-request-flag) + (awful.spawn [:xdotool :key :ctrl+shift]) + (gears.timer.start_new 0.25 (fn [] (detach-from-window) false))) + +;; While attached, poll for the detach-request flag. The start_new callback +;; returning false stops the timer; true reschedules it. +(fn start-detach-poll [] + (stop-detach-poll) + (os.remove detach-request-flag) + (set detach-poll-timer + (gears.timer.start_new 0.15 + (fn [] + (if (not attached?) false + (file-exists? detach-request-flag) (do + (handle-detach-request) + false) + true))))) + +(fn complete-attach [title] + (let [xephyr (find-xephyr-client)] + (when xephyr + (let [coords (mouse.coords)] + (set attach-saved-pointer {:x coords.x :y coords.y})) + (set attach-saved-focus client.focus) + (set attach-target-was-hidden? xephyr.minimized) + (when xephyr.minimized + ;; pull it out of hidden-by-demo so leaving demo-mode doesn't try to + ;; restore it a second time + (let [filtered []] + (each [_ c (ipairs hidden-by-demo)] + (when (not= c xephyr) (table.insert filtered c))) + (set hidden-by-demo filtered)) + (set xephyr.minimized false)) + ;; Xephyr must be un-minimized (mapped) to be focusable and to grab, but + ;; MC is only *maximized*, not in the fullscreen layer — so raising Xephyr + ;; would lift it over MC onto the desktop. Lower it instead: focus + the + ;; Ctrl+Shift grab route input regardless of stacking, and it stays + ;; hidden behind MC. + (xephyr:lower) + (set client.focus xephyr) + (let [g (xephyr:geometry) + cx (math.floor (+ g.x (/ g.width 2))) + cy (math.floor (+ g.y (/ g.height 2)))] + (mouse.coords {:x cx :y cy})) + ;; firefox/ghostty are inside the :2 server, so awesome can't focus them + ;; directly. No WM runs on :2, so EWMH windowactivate is a no-op there — + ;; windowraise + windowfocus (XSetInputFocus) pick the target instead. + (awful.spawn.with_shell (.. "DISPLAY=:2 xdotool search --limit 1 --name " + (shell-quote (regex-escape title)) + " windowraise windowfocus")) + ;; freeze deterministically — never rely on toggle state here + (touch-director-freeze) + ;; confine real input into :2 — Ctrl+Shift toggles Xephyr's own grab + (synthesize-xephyr-grab) + (set attach-target xephyr) + (set attached? true) + ;; awesome's :0 Alt+F is now masked by Xephyr's grab — poll for the + ;; detach-request flag the :2 xbindkeys writes instead + (start-detach-poll)))) + +(fn try-attach [] + (when (and demo-mode? (not attached?)) + ;; Truncate the response, signal quote-mc, then poll for ~500ms. Quote-mc + ;; writes either "\n" (target found) or "\n" (no target). We trim + ;; trailing whitespace; empty = silent no-op. + (awful.spawn.easy_async_with_shell (.. "dir=\"${XDG_RUNTIME_DIR:-/tmp}/director\" +mkdir -p \"$dir\" +: > \"$dir/attach-response\" +touch \"$dir/attach-request\" +for _ in 1 2 3 4 5 6 7 8 9 10; do + sleep 0.05 + if [ -s \"$dir/attach-response\" ]; then + cat \"$dir/attach-response\" + exit 0 + fi +done") + (fn [stdout _ _ _] + (let [title (trim (or stdout ""))] + (when (not= title "") + (complete-attach title))))))) + +(fn attach-toggle [] + (if attached? (detach-from-window) (try-attach))) + +;; wintc-taskband --start signals the running instance over D-Bus to toggle +;; the Start menu. +(fn toggle-start-menu [] + (awful.spawn [:wintc-taskband :--start])) + +(var globalkeys + (gears.table.join (awful.key [modkey] :Return + (fn [] (awful.spawn terminal))) + (awful.key [modkey] :d + (fn [] (awful.spawn "rofi -show drun"))) + (awful.key [modkey :Shift] :e awesome.quit) + (awful.key [modkey :Shift] :c awesome.restart) + (awful.key [modkey] :h + (fn [] (awful.client.focus.bydirection :left))) + (awful.key [modkey] :j + (fn [] (awful.client.focus.bydirection :down))) + (awful.key [modkey] :k + (fn [] (awful.client.focus.bydirection :up))) + (awful.key [modkey] :l + (fn [] + (awful.client.focus.bydirection :right))) + (awful.key [modkey :Shift] :h + (fn [] (awful.client.swap.bydirection :left))) + (awful.key [modkey :Shift] :j + (fn [] (awful.client.swap.bydirection :down))) + (awful.key [modkey :Shift] :k + (fn [] (awful.client.swap.bydirection :up))) + (awful.key [modkey :Shift] :l + (fn [] (awful.client.swap.bydirection :right))) + (awful.key [modkey] :e + (fn [] + (awful.layout.set awful.layout.suit.tile))) + (awful.key [modkey] :s + (fn [] + (awful.layout.set awful.layout.suit.tile.bottom))) + (awful.key [modkey] :w + (fn [] + (awful.layout.set awful.layout.suit.max))) + (awful.key [modkey] :grave + (fn [] + (let [s (awful.screen.focused) + t (. s.tags 1)] + (when t (t:view_only))))) + (awful.key [modkey :Shift] :grave + awful.tag.history.restore) + (awful.key [altkey] :m director-toggle-soft) + (awful.key [altkey :Shift] :m director-toggle) + (awful.key [altkey] :f attach-toggle) + (awful.key [modkey] :Escape toggle-start-menu))) + +(for [i 1 4] + (let [label (tostring i)] + (set globalkeys + (gears.table.join globalkeys + (awful.key [modkey] label + (fn [] + (let [s (awful.screen.focused) + t (. s.tags i)] + (when t (t:view_only))))) + (awful.key [modkey :Shift] label + (fn [] + (when client.focus + (let [t (. client.focus.screen.tags i)] + (when t + (client.focus:move_to_tag t)))))))))) + +(root.keys globalkeys) + +(set awful.rules.rules + [{:rule {} + :properties {:border_width beautiful.border_width + :border_color beautiful.border_normal + :focus awful.client.focus.filter + :raise true + :keys clientkeys + :buttons clientbuttons + :screen awful.screen.preferred + :placement (+ awful.placement.no_overlap + awful.placement.no_offscreen) + :titlebars_enabled true}} + {:rule_any {:class [:Pavucontrol :Nm-connection-editor]} + :properties {:floating true}} + ;; Xephyr hosts the nested :2 X server whose framebuffer the Quote-MC + ;; monitor blocks capture (firefox / ghostty render there). It's + ;; plumbing, not a window to interact with — drop its chrome and keep it + ;; off the taskband. The `manage` handler parks it on the 2nd monitor (or + ;; minimizes it if there is only one), out of the maximized MC's way. + {:rule {:class :Xephyr} + :properties {:skip_taskbar true + :focusable false + :titlebars_enabled false + :border_width 0}} + ;; wintc-taskband's multi-monitor handling is broken: it places its DOCK + ;; window at virtual-screen (0,0) and sets _NET_WM_STRUT_PARTIAL bottom + ;; to (virt_height - DP-1_height). Anchor it to the bottom of screen 1 + ;; (DP-1) and set a sane 30px strut. Match :type "dock" so this rule + ;; doesn't fire for the start menu popup (which shares the class). + {:rule {:class :Wintc-taskband :type :dock} + :properties {:titlebars_enabled false + :border_width 0 + :focusable false + :focus false} + :callback (fn [c] + (let [s (. screen 1) + g s.geometry + h 30] + (set c.x g.x) + (set c.y (+ g.y g.height (- h))) + (set c.width g.width) + (set c.height h) + (c:struts {:bottom h :left 0 :right 0 :top 0})))} + ;; Start menu popup: type=popup_menu, class=Wintc-taskband. Wintc creates + ;; it as a GTK toplevel that hides itself on focus-out-event + ;; (shelldpa/api.c:152). If awesome's generic {:rule {}} catches it, the + ;; no_overlap placement, titlebar, and focus filter all interfere — and + ;; if awesome refuses to focus the popup at all, the focus-out handler + ;; fires immediately, so the menu dismisses the moment the pointer moves. + ;; Force the popup to be focus-eligible, no-op placement, drop chrome, + ;; and stay on top so wintc's own logic runs cleanly. The (fn [c] c) + ;; identity callbacks defeat the inherited :focus / :placement values + ;; without breaking awesome's rule merge — do not "clean up" to nil. + {:rule {:class :Wintc-taskband :type :popup_menu} + :properties {:titlebars_enabled false + :border_width 0 + :focusable true + :focus (fn [c] c) + :ontop true + :skip_taskbar true + :placement (fn [c] c)}} + ;; focusable stays true to align with LWJGL2's X11 grab (prevents stuck keys). + ;; size_hints_honor false: LWJGL2 / Prism's JVM reports its window's default + ;; size (e.g. 854x480) as a WM_NORMAL_HINTS program-specified size, which + ;; awesome would otherwise clamp our screen.geometry call back to. + ;; honor_workarea true: MC's bottom stops at the taskband's top edge instead + ;; of being covered by it; wintc-taskband sets _NET_WM_STRUT_PARTIAL. + ;; Prism's main window (class org-prismlauncher-EntryPoint) is the host that + ;; backgrounds the launched minecraft; treat both the same. + ;; placement override is needed because the generic {:rule {}} rule above sets + ;; placement = no_overlap + no_offscreen, which would re-clamp our resize. + {:rule_any {:class [:Minecraft :org-prismlauncher-EntryPoint]} + :properties {:floating true + :sticky true + :below true + :ontop false + :focusable true + :fullscreen false + :border_width 0 + :screen 1 + :size_hints_honor false + :skip_taskbar true + :titlebars_enabled false + :placement (fn [c] + (awful.placement.maximize c + {:honor_workarea true}))} + :callback (fn [c] + (let [apply (fn [] + (when c.valid + (set c.size_hints_honor false) + (awful.placement.maximize c + {:honor_workarea true})))] + (apply) + ;; Prism/LWJGL2 may resize after the GL context comes up; + ;; re-pin once the X event loop settles. + (gears.timer.delayed_call apply) + (gears.timer.start_new 1 (fn [] (apply) false))))}]) + +(client.connect_signal :manage + (fn [c] + (when (and awesome.startup + (not c.size_hints.user_position) + (not c.size_hints.program_position)) + (awful.placement.no_offscreen c)) + ;; The Xephyr :2 host window is plumbing (content is + ;; mirrored into MC, input goes via Alt+F) and must + ;; never sit over the maximized MC on screen 1. With a + ;; 2nd monitor, park it there — visible but out of the + ;; way. Single-monitor: minimize it (the awful.rules + ;; `minimized` doesn't reliably stick, and a delayed + ;; re-assert beats autofocus un-minimizing it). + (when (xephyr-client? c) + (set c.skip_taskbar true) + (let [s2 (. screen 2)] + (if s2 + (do + (set c.minimized false) + (set c.screen s2) + (set c.x s2.geometry.x) + (set c.y s2.geometry.y)) + (do + (set c.minimized true) + (gears.timer.delayed_call (fn [] + (when c.valid + (set c.minimized + true)))))))))) + +;;; xset r off: X11 fake KeyRelease autorepeat trips LWJGL2's pause-menu ESC. +;;; Spawn wintc-taskband first so its systray claims the slot before +;;; nm-applet / pasystray try to XEMBED. +;;; xcape: emit Super+Escape on a Super tap (Super held + released with no +;;; other key). Super+Escape is bound below to toggle-start-menu. +(when awesome.startup + ;; stylix sets a system-wide GTK theme; override so the taskband renders Luna. + (awful.spawn.with_shell "GTK_THEME='Windows XP style (Blue)' wintc-taskband") + (awful.spawn "xset r off") + (awful.spawn "xcape -e Super_L=Super_L|Escape") + (awful.spawn :nm-applet) + (awful.spawn :pasystray) + (awful.spawn.with_shell "quote-mc-launch >/tmp/quote-mc-launch.log 2>&1")) diff --git a/home/linux/quote/sandbox.nix b/home/linux/quote/sandbox.nix new file mode 100644 index 00000000..8d25e94a --- /dev/null +++ b/home/linux/quote/sandbox.nix @@ -0,0 +1,11 @@ +{ config, lib, ... }: +let + root = "${config.home.homeDirectory}/QuoteMC"; +in +{ + # umbrella dir for this profile's MC state; prism/ keeps Prism scoped to it + home.activation.quoteSandbox = + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run mkdir -p '${root}' '${root}/.cache' '${root}/prism' + ''; +} diff --git a/hosts/quote/configuration.nix b/hosts/quote/configuration.nix new file mode 100644 index 00000000..11f976e4 --- /dev/null +++ b/hosts/quote/configuration.nix @@ -0,0 +1,20 @@ +{ pkgs, ... }: +{ + imports = [ + ./hardware-configuration.nix + ../../system/common/linux + ]; + + networking.hostName = "quote"; + + services.openssh.enable = true; + + environment.systemPackages = with pkgs; [ + ghostty + temurin-bin-21 + git + ]; + + # quote shares the desktop host's home; pin UID so workspace files keep ownership + users.users.drakeb.uid = 1000; +} diff --git a/hosts/quote/default.nix b/hosts/quote/default.nix new file mode 100644 index 00000000..c7c879ba --- /dev/null +++ b/hosts/quote/default.nix @@ -0,0 +1,7 @@ +{ ... +}: { + imports = [ + ./configuration.nix + ../../system/users + ]; +} diff --git a/hosts/quote/hardware-configuration.nix b/hosts/quote/hardware-configuration.nix new file mode 100644 index 00000000..11de5fd8 --- /dev/null +++ b/hosts/quote/hardware-configuration.nix @@ -0,0 +1,6 @@ +# quote shares its physical hardware with the desktop host; +# the auto-generated config lives there and updates via nixos-generate-config. +{ ... }: +{ + imports = [ ../desktop/hardware-configuration.nix ]; +} diff --git a/lib/default.nix b/lib/default.nix index cc8c605f..71cfc5dd 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -21,7 +21,7 @@ let }; in assert builtins.elem t.appearance [ "light" "dark" ]; - assert builtins.elem f.desktopEnvironment [ null "plasma" "niri" "sway" "macos" ]; + assert builtins.elem f.desktopEnvironment [ null "plasma" "niri" "sway" "macos" "quote" ]; { inherit inputs username system versions; theme = t; diff --git a/outputs/default.nix b/outputs/default.nix index 70e19029..f5051bb1 100644 --- a/outputs/default.nix +++ b/outputs/default.nix @@ -9,6 +9,8 @@ ./home-manager.nix ./hosts.nix ./lib.nix + ./packages.nix ./systems.nix + ./vms.nix ]; } diff --git a/outputs/hosts.nix b/outputs/hosts.nix index 088b2890..bead50b3 100644 --- a/outputs/hosts.nix +++ b/outputs/hosts.nix @@ -46,6 +46,12 @@ in hostName = "pocket"; theme = (baseSystem.theme or { }) // { appearance = "light"; }; }) + // mkWithVariants "quote" (baseSystem // { + hostName = "quote"; + features = { desktopEnvironment = "quote"; gaming = true; }; + autologin = true; + theme = (baseSystem.theme or { }) // { appearance = "light"; scheme = "oxocarbon"; }; + }) // { android = mkSystem { hostName = "android"; diff --git a/outputs/packages.nix b/outputs/packages.nix new file mode 100644 index 00000000..5a48bca3 --- /dev/null +++ b/outputs/packages.nix @@ -0,0 +1,7 @@ +{ + perSystem = { pkgs, lib, system, ... }: { + packages = lib.optionalAttrs (lib.hasSuffix "linux" system) { + xfce-winxp-tc = pkgs.callPackage ../pkgs/xfce-winxp-tc { }; + }; + }; +} diff --git a/outputs/vms.nix b/outputs/vms.nix new file mode 100644 index 00000000..4f7990a0 --- /dev/null +++ b/outputs/vms.nix @@ -0,0 +1,69 @@ +{ self, inputs, lib, ... }: { + # Wrap each Linux nixosConfiguration as a runnable QEMU VM. `nix run .#vm-<host>` boots. + perSystem = { system, ... }: + let + isLinux = lib.hasSuffix "linux" system; + + # quote: mount the launcher's working set at the same paths it uses on metal, + # so live edits in ~/workspace flow through to the guest. + sharesFor = name: + if name == "quote" then { + quote-mc = { + source = "/home/drakeb/workspace/quote-mc"; + target = "/home/drakeb/workspace/quote-mc"; + securityModel = "passthrough"; + }; + director = { + source = "/home/drakeb/workspace/director"; + target = "/home/drakeb/workspace/director"; + securityModel = "passthrough"; + }; + quote-pack = { + source = "/home/drakeb/workspace/3rd-brain"; + target = "/home/drakeb/workspace/3rd-brain"; + securityModel = "passthrough"; + }; + } else { }; + + mkVm = name: cfg: + (cfg.extendModules { + modules = [ + "${inputs.nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" + (_: { + virtualisation = { + memorySize = 4096; + cores = 4; + diskSize = 8192; + graphics = true; + qemu.options = [ + # qxl gives more reliable early-KMS for Plymouth than virtio under qemu-vm + "-vga qxl" + "-qmp unix:/tmp/qmp-${name}.sock,server,nowait" + "-serial file:/tmp/${name}-serial.log" + ]; + sharedDirectories = sharesFor name; + }; + }) + ]; + }).config.system.build.vm; + + vms = lib.mapAttrs mkVm + (lib.filterAttrs + (_: cfg: cfg.config.nixpkgs.hostPlatform.system == system) + self.nixosConfigurations); + + vmApps = lib.mapAttrs' + (name: vm: lib.nameValuePair "vm-${name}" { + type = "app"; + program = "${vm}/bin/run-${name}-vm"; + meta.description = "Boot the ${name} NixOS host as a QEMU VM."; + }) + vms; + + vmPackages = lib.mapAttrs' (name: lib.nameValuePair "vm-${name}") vms; + in + lib.optionalAttrs isLinux { + apps = vmApps; + packages = vmPackages; + }; +} diff --git a/pkgs/xfce-winxp-tc/default.nix b/pkgs/xfce-winxp-tc/default.nix new file mode 100644 index 00000000..90a2935f --- /dev/null +++ b/pkgs/xfce-winxp-tc/default.nix @@ -0,0 +1,138 @@ +{ stdenv +, lib +, fetchFromGitHub +, cmake +, pkg-config +, python3 +, sass +, sassc +, xorg +, gettext +, wrapGAppsHook3 +, glib +, gtk3 +, xfce +, libcanberra-gtk3 +, libpulseaudio +, upower +, networkmanager +, libxml2 +, libwnck +}: + +stdenv.mkDerivation { + pname = "xfce-winxp-tc"; + version = "0-unstable-2026-05-11"; + + src = fetchFromGitHub { + owner = "rozniak"; + repo = "xfce-winxp-tc"; + rev = "2242a2b055da6a1c2036e23fa393d87ada670678"; + hash = "sha256-QQdcYHW5YEIoG6wZbnyZZuvyVZhYwXsro9ZmbDRDhlQ="; + }; + + patches = [ + ./pinned-start-menu.patch + ./taskband-class-name.patch + ]; + + nativeBuildInputs = [ + cmake + pkg-config + (python3.withPackages (ps: with ps; [ pillow packaging ])) + sass + sassc + xorg.xcursorgen + gettext + wrapGAppsHook3 + ]; + + buildInputs = [ + glib + gtk3 + xfce.garcon + xfce.libxfce4ui + libcanberra-gtk3 + libpulseaudio + upower + networkmanager + libxml2 + libwnck + ]; + + # shared/shelldpa dlopens "libwnck-3.so.0" by short name; under Nix it isn't + # on the default linker search path. Extend the gApps wrapper's runtime env. + preFixup = '' + gappsWrapperArgs+=( + --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [ libwnck ]} + ) + ''; + + # buildall.sh chains its own per-component cmake invocations; the default + # cmake hook would try to configure the top of the tree (which has no + # CMakeLists.txt) and fail. + dontUseCmakeConfigure = true; + + # icons/luna creates relative symlinks (32x32/actions/cut.png -> res/32x32/cut.png) + # that resolve against the source tree's res/ subdir. The mappings install + # appears to skip a chunk of res files in our setup; the taskband doesn't + # need these icons at runtime, so don't block the whole build on them. + dontCheckForBrokenSymlinks = true; + + postPatch = '' + substituteInPlace packaging/build.sh \ + --replace-fail 'dist_prefix="/usr"' "dist_prefix=\"$out\"" + + # distid.sh validates a -t target by probing for the matching pkg manager + # (pacman, dpkg, etc) which the Nix sandbox doesn't have. Trust the caller + # when DIST_ID is preset — we are caller, we know what we asked for. + substituteInPlace packaging/distid.sh \ + --replace-fail ' g_compare_against_env=1' \ + ' g_compare_against_env=1; g_compare_successful=1' + + # Replace the upstream targets list (40+ components incl. OOBE/IE/taskmgr + # that pull gstreamer, webkitgtk, etc) with the subset needed for the + # taskband + start menu and its visual assets. + printf '%s\n' \ + 'base/bldtag' \ + 'themes/luna/blue' \ + 'icons/luna' \ + 'fonts' \ + 'sounds' \ + 'shell/taskband' \ + > packaging/targets + + patchShebangs packaging tools + ''; + + # nixpkgs splits gio-unix-2.0 headers (gdesktopappinfo.h, etc) into a + # separate include dir from gio-2.0; upstream CMakeLists only asks + # pkg-config for glib-2.0. Expose the unix headers globally. + env.NIX_CFLAGS_COMPILE = "-I${lib.getDev glib}/include/gio-unix-2.0"; + + buildPhase = '' + runHook preBuild + + cd packaging + ./buildall.sh -z -t archpkg + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + while IFS= read -r mf; do + ( cd "$(dirname "$mf")" && make install ) + done < <(find build -mindepth 1 -name Makefile) + + runHook postInstall + ''; + + meta = with lib; { + description = "Windows XP Total Conversion for XFCE"; + homepage = "https://github.com/rozniak/xfce-winxp-tc"; + license = licenses.gpl2Only; + platforms = platforms.linux; + }; +} diff --git a/pkgs/xfce-winxp-tc/pinned-start-menu.patch b/pkgs/xfce-winxp-tc/pinned-start-menu.patch new file mode 100644 index 00000000..79155554 --- /dev/null +++ b/pkgs/xfce-winxp-tc/pinned-start-menu.patch @@ -0,0 +1,312 @@ +--- a/shell/taskband/src/start/personal.c ++++ b/shell/taskband/src/start/personal.c +@@ -62,9 +62,6 @@ + static void refresh_userpic( + WinTCToolbarStart* toolbar_start + ); +-static void update_personal_menu_mfu_items( +- WinTCToolbarStart* toolbar_start +-); + + static void clear_signal_tuple( + StartSignalTuple* tuple +@@ -104,10 +101,6 @@ + GtkMenuShell* self, + gpointer user_data + ); +-static void on_mfu_tracker_updated( +- WinTCStartMfuTracker* self, +- gpointer user_data +-); + static void on_personal_menu_hide( + GtkWidget* self, + gpointer user_data +@@ -795,116 +788,41 @@ + TOTAL_PROGRAMS_ITEM_COUNT + ); + +- // Add default items ++ // Add pinned items (replaces upstream's 2 MIME defaults + 6 MFU slots) + // +- GDesktopAppInfo* entry_internet = wintc_query_mime_handler( +- "x-scheme-handler/http", +- NULL +- ); +- GDesktopAppInfo* entry_email = wintc_query_mime_handler( +- "x-scheme-handler/mailto", +- NULL +- ); ++ static const gchar* const s_pinned_desktop_ids[] = { ++ "firefox.desktop", ++ "com.mitchellh.ghostty.desktop", ++ "vesktop.desktop", ++ "steam.desktop", ++ }; + StartSignalTuple* tuple; + +- tuple = +- &g_array_index( +- toolbar_start->personal.tuples_programs, +- StartSignalTuple, +- 0 +- ); +- tuple->toolbar_start = toolbar_start; +- +- gtk_menu_shell_append( +- GTK_MENU_SHELL(toolbar_start->personal.menubar_programs), +- create_personal_menu_item_from_desktop_entry( +- entry_internet, +- tuple, +- _("Opens your Internet browser."), +- _("Internet") +- ) +- ); +- +- tuple = +- &g_array_index( +- toolbar_start->personal.tuples_programs, +- StartSignalTuple, +- 1 +- ); +- tuple->toolbar_start = toolbar_start; +- +- gtk_menu_shell_append( +- GTK_MENU_SHELL(toolbar_start->personal.menubar_programs), +- create_personal_menu_item_from_desktop_entry( +- entry_email, +- tuple, +- _("Opens your e-mail program so you can send or read a message."), +- _("E-mail") +- ) +- ); +- +- g_clear_object(&entry_internet); +- g_clear_object(&entry_email); +- +- // Add separator between defaults & MFU +- // +- gtk_menu_shell_append( +- GTK_MENU_SHELL(toolbar_start->personal.menubar_programs), +- gtk_separator_menu_item_new() +- ); +- +- // Loop over to add the MFU items +- // +- for (gint i = 0; i < MAX_MFU_ITEM_COUNT; i++) ++ for (gsize i = 0; i < G_N_ELEMENTS(s_pinned_desktop_ids); i++) + { +- GtkWidget* personal_item = +- create_personal_menu_item( +- GTK_MENU_SHELL( +- toolbar_start->personal.menubar_programs +- ), +- NULL, +- NULL, +- NULL, +- NULL +- ); ++ GDesktopAppInfo* entry = ++ g_desktop_app_info_new(s_pinned_desktop_ids[i]); + + tuple = + &g_array_index( + toolbar_start->personal.tuples_programs, + StartSignalTuple, +- DEFAULT_ITEM_COUNT + i ++ i + ); +- +- tuple->is_action = FALSE; + tuple->toolbar_start = toolbar_start; + +- g_object_set_qdata( +- G_OBJECT(personal_item), +- S_QUARK_PERSONAL_ITEM_TUPLE, +- tuple +- ); +- +- g_signal_connect( +- personal_item, +- "activate", +- G_CALLBACK(on_menu_item_launcher_activate), +- tuple +- ); +- + gtk_menu_shell_append( + GTK_MENU_SHELL(toolbar_start->personal.menubar_programs), +- personal_item ++ create_personal_menu_item_from_desktop_entry( ++ entry, ++ tuple, ++ NULL, ++ NULL ++ ) + ); +- } + +- update_personal_menu_mfu_items(toolbar_start); +- +- g_signal_connect( +- wintc_start_mfu_tracker_get_default(), +- "mfu-updated", +- G_CALLBACK(on_mfu_tracker_updated), +- toolbar_start +- ); ++ g_clear_object(&entry); ++ } + + // Re-append All Programs items + // +@@ -970,135 +888,6 @@ + g_free(css); + } + +-static void update_personal_menu_mfu_items( +- WinTCToolbarStart* toolbar_start +-) +-{ +- // Find the first MFU item +- // +- GList* li_mfu = NULL; +- GList* list_children = +- gtk_container_get_children( +- GTK_CONTAINER(toolbar_start->personal.menubar_programs) +- ); +- +- for (GList* iter = list_children; iter; iter = iter->next) +- { +- if ( +- g_object_get_qdata( +- G_OBJECT(iter->data), +- S_QUARK_PERSONAL_ITEM_TUPLE +- ) +- ) +- { +- li_mfu = iter; +- break; +- } +- } +- +- if (!li_mfu) +- { +- g_critical("%s", "taskband: somehow found no mfu items"); +- g_list_free(list_children); +- return; +- } +- +- // Start updating the items +- // +- GList* list_mfu = +- wintc_start_mfu_tracker_get_mfu_list( +- wintc_start_mfu_tracker_get_default() +- ); +- +- for ( +- GList* iter = list_mfu; +- iter; +- iter = iter->next, li_mfu = li_mfu->next +- ) +- { +- StartSignalTuple* tuple = +- g_object_get_qdata( +- G_OBJECT(li_mfu->data), +- S_QUARK_PERSONAL_ITEM_TUPLE +- ); +- +- // Protect against the case where there could be more MFU items tracked +- // than personal menu items +- // +- if (!tuple) +- { +- break; +- } +- +- // Update the menu item and tuple +- // +- GarconMenuItem* garcon_item = GARCON_MENU_ITEM(iter->data); +- const gchar* icon_name = garcon_menu_item_get_icon_name( +- garcon_item +- ); +- GList* list_menu_children; +- GtkWidget* menu_item = GTK_WIDGET(li_mfu->data); +- GdkPixbuf* pixbuf_icon = NULL; +- +- list_menu_children = +- gtk_container_get_children( +- GTK_CONTAINER( +- gtk_bin_get_child(GTK_BIN(menu_item)) +- ) +- ); +- +- if (icon_name) +- { +- pixbuf_icon = +- gtk_icon_theme_load_icon( +- gtk_icon_theme_get_default(), +- icon_name, +- PROGRAM_ICON_SIZE, +- GTK_ICON_LOOKUP_FORCE_SIZE, +- NULL +- ); +- } +- +- if (!pixbuf_icon) +- { +- gtk_icon_theme_load_icon( +- gtk_icon_theme_get_default(), +- "application-x-generic", +- PROGRAM_ICON_SIZE, +- GTK_ICON_LOOKUP_FORCE_SIZE, +- NULL +- ); +- } +- +- gtk_image_set_from_pixbuf( +- GTK_IMAGE(list_menu_children->data), +- pixbuf_icon +- ); +- gtk_label_set_text( +- GTK_LABEL(list_menu_children->next->data), +- garcon_menu_item_get_name(garcon_item) +- ); +- gtk_widget_set_tooltip_text( +- menu_item, +- garcon_menu_item_get_comment(garcon_item) +- ); +- +- g_free(tuple->user_data); +- tuple->user_data = +- garcon_menu_item_get_command_expanded(garcon_item); +- +- if (pixbuf_icon) +- { +- g_object_unref(pixbuf_icon); +- } +- +- g_list_free(list_menu_children); +- } +- +- g_list_free(list_mfu); +- g_list_free(list_children); +-} +- + // + // CALLBACKS + // +@@ -1286,16 +1075,6 @@ + toolbar_start->sync_menu_should_close = TRUE; + } + +-static void on_mfu_tracker_updated( +- WINTC_UNUSED(WinTCStartMfuTracker* self), +- gpointer user_data +-) +-{ +- WinTCToolbarStart* toolbar_start = WINTC_TOOLBAR_START(user_data); +- +- update_personal_menu_mfu_items(toolbar_start); +-} +- + static void on_personal_menu_hide( + WINTC_UNUSED(GtkWidget* self), + gpointer user_data diff --git a/pkgs/xfce-winxp-tc/taskband-class-name.patch b/pkgs/xfce-winxp-tc/taskband-class-name.patch new file mode 100644 index 00000000..26442abe --- /dev/null +++ b/pkgs/xfce-winxp-tc/taskband-class-name.patch @@ -0,0 +1,156 @@ +diff --git a/shared/shelldpa/public/api.h b/shared/shelldpa/public/api.h +index db22546..701919f 100644 +--- a/shared/shelldpa/public/api.h ++++ b/shared/shelldpa/public/api.h +@@ -156,6 +156,17 @@ extern gchar* (*wintc_wndmgmt_window_get_name) ( + WinTCWndMgmtWindow* window + ); + ++/** ++ * Retrieves the WM_CLASS class group name of the specified window. May ++ * return NULL. ++ * ++ * @param window The window. ++ * @return The class group name, or NULL. ++ */ ++extern const gchar* (*wintc_wndmgmt_window_get_class_name) ( ++ WinTCWndMgmtWindow* window ++); ++ + /** + * Checks if the specified window is maximized. + * +diff --git a/shared/shelldpa/src/api.c b/shared/shelldpa/src/api.c +index d51a1ed..372e515 100644 +--- a/shared/shelldpa/src/api.c ++++ b/shared/shelldpa/src/api.c +@@ -42,6 +42,9 @@ GdkPixbuf* (*wintc_wndmgmt_window_get_mini_icon) ( + gchar* (*wintc_wndmgmt_window_get_name) ( + WinTCWndMgmtWindow* window + ) = NULL; ++const gchar* (*wintc_wndmgmt_window_get_class_name) ( ++ WinTCWndMgmtWindow* window ++) = NULL; + gboolean (*wintc_wndmgmt_window_is_maximized) ( + WinTCWndMgmtWindow* window + ) = NULL; +diff --git a/shared/shelldpa/src/dll/wnck.c b/shared/shelldpa/src/dll/wnck.c +index 9b21f62..84d6f71 100644 +--- a/shared/shelldpa/src/dll/wnck.c ++++ b/shared/shelldpa/src/dll/wnck.c +@@ -25,6 +25,9 @@ WinTCWndMgmtScreen* (*p_wnck_screen_get_default) (void) = NULL; + + void (*p_wnck_shutdown) (void) = NULL; + ++const gchar* (*p_wnck_window_get_class_group_name) ( ++ WinTCWndMgmtWindow* window ++) = NULL; + const gchar* (*p_wnck_window_get_class_instance_name) ( + WinTCWndMgmtWindow* window + ) = NULL; +@@ -103,6 +106,9 @@ gboolean init_dll_wnck() + p_wnck_window_close = + dlsym(dl_wnck, "wnck_window_close"); + ++ p_wnck_window_get_class_group_name = ++ dlsym(dl_wnck, "wnck_window_get_class_group_name"); ++ + p_wnck_window_get_class_instance_name = + dlsym(dl_wnck, "wnck_window_get_class_instance_name"); + +@@ -144,6 +150,7 @@ gboolean init_dll_wnck() + p_wnck_screen_get_default == NULL || + p_wnck_shutdown == NULL || + p_wnck_window_close == NULL || ++ p_wnck_window_get_class_group_name == NULL || + p_wnck_window_get_class_instance_name == NULL || + p_wnck_window_get_icon_is_fallback == NULL || + p_wnck_window_get_mini_icon == NULL || +diff --git a/shared/shelldpa/src/dll/wnck.h b/shared/shelldpa/src/dll/wnck.h +index be10c44..7da9efb 100644 +--- a/shared/shelldpa/src/dll/wnck.h ++++ b/shared/shelldpa/src/dll/wnck.h +@@ -33,6 +33,9 @@ extern void (*p_wnck_window_close) ( + WinTCWndMgmtWindow* window, + guint32 timestamp + ); ++extern const gchar* (*p_wnck_window_get_class_group_name) ( ++ WinTCWndMgmtWindow* window ++); + extern const gchar* (*p_wnck_window_get_class_instance_name) ( + WinTCWndMgmtWindow* Window + ); +diff --git a/shared/shelldpa/src/impl-wndmgmt-wnck.c b/shared/shelldpa/src/impl-wndmgmt-wnck.c +index e26d7b0..04c9ac0 100644 +--- a/shared/shelldpa/src/impl-wndmgmt-wnck.c ++++ b/shared/shelldpa/src/impl-wndmgmt-wnck.c +@@ -41,6 +41,7 @@ gboolean init_wndmgmt_wnck_impl(void) + wintc_wndmgmt_window_close = &wnck_window_close_real; + wintc_wndmgmt_window_get_mini_icon = &wnck_window_get_mini_icon_real; + wintc_wndmgmt_window_get_name = p_wnck_window_get_name; ++ wintc_wndmgmt_window_get_class_name = p_wnck_window_get_class_group_name; + wintc_wndmgmt_window_is_maximized = p_wnck_window_is_maximized; + wintc_wndmgmt_window_is_minimized = p_wnck_window_is_minimized; + wintc_wndmgmt_window_is_skip_tasklist = p_wnck_window_is_skip_tasklist; +diff --git a/shared/shelldpa/src/impl-wndmgmt-xfw.c b/shared/shelldpa/src/impl-wndmgmt-xfw.c +index 1b79bcd..7ef3fc4 100644 +--- a/shared/shelldpa/src/impl-wndmgmt-xfw.c ++++ b/shared/shelldpa/src/impl-wndmgmt-xfw.c +@@ -10,6 +10,9 @@ + // + // FORWARD DECLARATIONS + // ++static const gchar* xfw_window_get_class_name( ++ WinTCWndMgmtWindow* window ++); + static void xfw_shutdown(void); + static void xfw_window_close( + WinTCWndMgmtWindow* window, +@@ -51,6 +54,7 @@ gboolean init_wndmgmt_xfw_impl(void) + wintc_wndmgmt_window_close = &xfw_window_close; + wintc_wndmgmt_window_get_mini_icon = &xfw_window_get_mini_icon; + wintc_wndmgmt_window_get_name = p_xfw_window_get_name; ++ wintc_wndmgmt_window_get_class_name = &xfw_window_get_class_name; + wintc_wndmgmt_window_is_minimized = p_xfw_window_is_minimized; + wintc_wndmgmt_window_is_maximized = p_xfw_window_is_maximized; + wintc_wndmgmt_window_is_skip_tasklist = p_xfw_window_is_skip_tasklist; +@@ -64,6 +68,13 @@ gboolean init_wndmgmt_xfw_impl(void) + + // PRIVATE FUNCTIONS + // ++static const gchar* xfw_window_get_class_name( ++ WINTC_UNUSED(WinTCWndMgmtWindow* window) ++) ++{ ++ return NULL; ++} ++ + static void xfw_shutdown(void) + { + // Nothing to do! xfce4windowing doesn't expose any shutdown method +diff --git a/shell/taskband/src/taskbuttons/windowmonitor.c b/shell/taskband/src/taskbuttons/windowmonitor.c +index 7973de1..6ebb659 100644 +--- a/shell/taskband/src/taskbuttons/windowmonitor.c ++++ b/shell/taskband/src/taskbuttons/windowmonitor.c +@@ -471,16 +471,18 @@ static void window_manager_update_text( + WindowManagerSingle* window_manager + ) + { +- const gchar* new_text = ++ const gchar* class_text = ++ wintc_wndmgmt_window_get_class_name(window_manager->managed_window); ++ const gchar* title_text = + wintc_wndmgmt_window_get_name(window_manager->managed_window); + + gtk_label_set_text( + window_manager->button_text, +- new_text ++ (class_text && *class_text) ? class_text : title_text + ); + gtk_widget_set_tooltip_text( + GTK_WIDGET(window_manager->button), +- new_text ++ title_text + ); + } + diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..b60bc456 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,10 @@ +# scripts + +Standalone helpers wired into home-manager via per-host `.nix` modules. Look at +the consumer module for context on each script: + +- `quote-pack.clj` — dispatcher to `bb` tasks in the Quote-MC pack repo. + Consumed by `home/linux/quote/launcher.nix` (the `quote-mc-launch` wrapper + also exports `QUOTE_PACK_ROOT` so subshells resolve the same pack dir). +- `npm-clean-install.clj`, `rebuild.clj`, `tresorit-install.clj` — see + `scripts/default.nix` for how each is exposed. diff --git a/scripts/quote-pack.clj b/scripts/quote-pack.clj new file mode 100644 index 00000000..6159f60f --- /dev/null +++ b/scripts/quote-pack.clj @@ -0,0 +1,11 @@ +;; quote-pack <task ...> -> `bb <task ...>` in $QUOTE_PACK_ROOT +;; (default ~/workspace/3rd-brain) + +(require '[babashka.process :refer [shell]] + '[clojure.string :as str]) + +(let [env (System/getenv "QUOTE_PACK_ROOT") + pack-root (if (str/blank? env) + (str (System/getProperty "user.home") "/workspace/3rd-brain") + env)] + (apply shell {:dir pack-root} "bb" *command-line-args*)) diff --git a/system/nixOS/default.nix b/system/nixOS/default.nix index 117c203b..c9bd3dfe 100644 --- a/system/nixOS/default.nix +++ b/system/nixOS/default.nix @@ -1,4 +1,15 @@ { features, lib, ... }: +let + de = features.desktopEnvironment; + deModules = { + niri = ./niri; + plasma = ./plasma; + sway = ./sway; + quote = ./quote; + }; + # quote uses lightdm (awesome is X11, autologin via services.displayManager) + useGreetd = de != null && de != "quote"; +in { imports = [ ./audio.nix @@ -10,17 +21,9 @@ ./mullvad.nix ./printing.nix ./stylix.nix - ] ++ lib.optionals (features.desktopEnvironment != null) [ - ./greetd.nix - ] ++ lib.optionals features.gaming [ - ./gaming.nix - ] ++ ( - if features.desktopEnvironment == "niri" - then [ ./niri ] - else if features.desktopEnvironment == "plasma" - then [ ./plasma ] - else if features.desktopEnvironment == "sway" - then [ ./sway ] - else [ ] - ); + ./webcam.nix + ] + ++ lib.optional useGreetd ./greetd.nix + ++ lib.optionals features.gaming [ ./gaming.nix ] + ++ lib.optional (deModules ? ${de}) deModules.${de}; } diff --git a/system/nixOS/greetd.nix b/system/nixOS/greetd.nix index ee0f0ecc..d99546b3 100644 --- a/system/nixOS/greetd.nix +++ b/system/nixOS/greetd.nix @@ -1,11 +1,11 @@ { config, lib, pkgs, username, features, autologin, ... }: let de = features.desktopEnvironment; - sessionCommand = - if de == "plasma" then "${pkgs.kdePackages.plasma-workspace}/bin/startplasma-wayland" - else if de == "niri" then "${config.programs.niri.package}/bin/niri-session" - else if de == "sway" then "${pkgs.sway}/bin/sway" - else throw "greetd: unsupported desktopEnvironment: ${de}"; + sessionCommand = { + plasma = "${pkgs.kdePackages.plasma-workspace}/bin/startplasma-wayland"; + niri = "${config.programs.niri.package}/bin/niri-session"; + sway = "${pkgs.sway}/bin/sway"; + }.${de} or (throw "greetd: unsupported desktopEnvironment: ${de}"); in { services.greetd.enable = true; diff --git a/system/nixOS/quote/awesome.nix b/system/nixOS/quote/awesome.nix new file mode 100644 index 00000000..47ac5045 --- /dev/null +++ b/system/nixOS/quote/awesome.nix @@ -0,0 +1,46 @@ +{ pkgs, username, autologin, ... }: +{ + services = { + xserver = { + enable = true; + windowManager.awesome = { + enable = true; + package = pkgs.awesome; + luaModules = with pkgs.luaPackages; [ luarocks ]; + }; + + displayManager.lightdm.enable = true; + + # DP-1 stays primary at native landscape; DP-3 rotates 90° CW (portrait) + # and sits to the right at x=2560. Mirrors the KDE layout. + displayManager.sessionCommands = '' + ${pkgs.xorg.xrandr}/bin/xrandr \ + --output DP-1 --primary --mode 2560x1440 --pos 0x0 --rotate normal \ + --output DP-3 --mode 2560x1440 --pos 2560x0 --rotate right + ''; + }; + + displayManager = { + defaultSession = "none+awesome"; + autoLogin = { + enable = autologin; + user = username; + }; + }; + + # wintc-taskband's tray battery indicator segfaults if UPower's D-Bus name + # isn't registered. Run upower so it answers — the desktop has no battery + # to report on, but the daemon's presence is what the taskband needs. + upower.enable = true; + }; + + programs.dconf.enable = true; + + environment.systemPackages = with pkgs; [ + xclip + xterm + rofi + ]; + + fonts.fontconfig.enable = true; +} diff --git a/system/nixOS/quote/default.nix b/system/nixOS/quote/default.nix new file mode 100644 index 00000000..cfb700a7 --- /dev/null +++ b/system/nixOS/quote/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./awesome.nix + ]; +} diff --git a/system/nixOS/webcam.nix b/system/nixOS/webcam.nix new file mode 100644 index 00000000..645f0db5 --- /dev/null +++ b/system/nixOS/webcam.nix @@ -0,0 +1,4 @@ +{ pkgs, ... }: +{ + environment.systemPackages = with pkgs; [ v4l-utils ]; +} diff --git a/system/users/default.nix b/system/users/default.nix index f5ee7800..180053de 100644 --- a/system/users/default.nix +++ b/system/users/default.nix @@ -7,7 +7,7 @@ } (lib.mkIf pkgs.stdenv.isLinux { isNormalUser = true; - extraGroups = [ "networkmanager" "wheel" ]; + extraGroups = [ "networkmanager" "wheel" "video" ]; hashedPassword = "$6$RWnDqI6YSUzRNFcH$mYbS.1KQUPaNYRqK9C2So4oPy2hG7/sKCDDzDffv0jYkGk7g5O7uj8qWvMIRJi9kpmPOS5T3q49djmsYIhtyY."; }) (lib.mkIf pkgs.stdenv.isDarwin { }) From 17568eb56c815fe730700f4adeea12888fd50e5d Mon Sep 17 00:00:00 2001 From: Drake Bott <me@drake.dev> Date: Sun, 17 May 2026 10:48:11 -0500 Subject: [PATCH 2/3] cleanup --- .gitignore | 4 -- README.md | 10 ++++- flake.lock | 14 ------- flake.nix | 6 --- home/linux/quote/picom.nix | 1 - hosts/quote/configuration.nix | 4 +- outputs/default.nix | 1 - outputs/vms.nix | 69 ----------------------------------- 8 files changed, 10 insertions(+), 99 deletions(-) delete mode 100644 outputs/vms.nix diff --git a/.gitignore b/.gitignore index 13d544ed..8c4b5447 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ CLAUDE.md .pre-commit-config.yaml .clj-kondo .lsp - -# VM disk images — mutable per-boot, never commit -*.qcow2 -result diff --git a/README.md b/README.md index 6562b53a..d4b1dd5e 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,17 @@ My personal nix flake. ### NixOS (Linux) -I have a configuration for my gaming desktop: +My desktop has two configurations sharing the same hardware: -```bash +- `desktop` — my main config +- `quote` — an alternate `desktop` running an XP-themed awesome WM with an + embedded Minecraft game as the wallpaper +```bash sudo nixos-rebuild switch --flake .#desktop + +# or, the Minecraft desktop +sudo nixos-rebuild switch --flake .#quote ``` e-ink desktop using [Dasung Paperlike](https://shop.dasung.com/) diff --git a/flake.lock b/flake.lock index b7998806..1ff67379 100644 --- a/flake.lock +++ b/flake.lock @@ -774,19 +774,6 @@ "type": "github" } }, - "quote-mc": { - "flake": false, - "locked": { - "lastModified": 1778884491, - "narHash": "sha256-x9VaDJndcIx1X1frfjtWaE4gfRcNO1FGVw7p71SSiYg=", - "path": "/home/drakeb/workspace/quote-mc", - "type": "path" - }, - "original": { - "path": "/home/drakeb/workspace/quote-mc", - "type": "path" - } - }, "root": { "inputs": { "claude-code": "claude-code", @@ -803,7 +790,6 @@ "nixpkgs-unstable": "nixpkgs-unstable", "plasma-manager": "plasma-manager", "pre-commit-hooks": "pre-commit-hooks", - "quote-mc": "quote-mc", "spicetify-nix": "spicetify-nix", "steam-config-nix": "steam-config-nix", "stylix": "stylix", diff --git a/flake.nix b/flake.nix index 26212968..63132943 100644 --- a/flake.nix +++ b/flake.nix @@ -56,12 +56,6 @@ inputs.nixpkgs.follows = "nixpkgs"; }; claude-code.url = "github:sadjow/claude-code-nix"; - - # local working copy; switch to a git ref once the mod stabilizes - quote-mc = { - url = "path:/home/drakeb/workspace/quote-mc"; - flake = false; - }; }; outputs = inputs @ { flake-parts, ... }: diff --git a/home/linux/quote/picom.nix b/home/linux/quote/picom.nix index a6955266..f36ccd2a 100644 --- a/home/linux/quote/picom.nix +++ b/home/linux/quote/picom.nix @@ -3,7 +3,6 @@ services.picom = { enable = true; package = pkgs.picom; - # xrender is safer under virtio-vga; switch to glx once that's confirmed on metal backend = "xrender"; vSync = false; diff --git a/hosts/quote/configuration.nix b/hosts/quote/configuration.nix index 11f976e4..3c7d89e6 100644 --- a/hosts/quote/configuration.nix +++ b/hosts/quote/configuration.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ pkgs, username, ... }: { imports = [ ./hardware-configuration.nix @@ -16,5 +16,5 @@ ]; # quote shares the desktop host's home; pin UID so workspace files keep ownership - users.users.drakeb.uid = 1000; + users.users.${username}.uid = 1000; } diff --git a/outputs/default.nix b/outputs/default.nix index f5051bb1..2d0371af 100644 --- a/outputs/default.nix +++ b/outputs/default.nix @@ -11,6 +11,5 @@ ./lib.nix ./packages.nix ./systems.nix - ./vms.nix ]; } diff --git a/outputs/vms.nix b/outputs/vms.nix deleted file mode 100644 index 4f7990a0..00000000 --- a/outputs/vms.nix +++ /dev/null @@ -1,69 +0,0 @@ -{ self, inputs, lib, ... }: { - # Wrap each Linux nixosConfiguration as a runnable QEMU VM. `nix run .#vm-<host>` boots. - perSystem = { system, ... }: - let - isLinux = lib.hasSuffix "linux" system; - - # quote: mount the launcher's working set at the same paths it uses on metal, - # so live edits in ~/workspace flow through to the guest. - sharesFor = name: - if name == "quote" then { - quote-mc = { - source = "/home/drakeb/workspace/quote-mc"; - target = "/home/drakeb/workspace/quote-mc"; - securityModel = "passthrough"; - }; - director = { - source = "/home/drakeb/workspace/director"; - target = "/home/drakeb/workspace/director"; - securityModel = "passthrough"; - }; - quote-pack = { - source = "/home/drakeb/workspace/3rd-brain"; - target = "/home/drakeb/workspace/3rd-brain"; - securityModel = "passthrough"; - }; - } else { }; - - mkVm = name: cfg: - (cfg.extendModules { - modules = [ - "${inputs.nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" - (_: { - virtualisation = { - memorySize = 4096; - cores = 4; - diskSize = 8192; - graphics = true; - qemu.options = [ - # qxl gives more reliable early-KMS for Plymouth than virtio under qemu-vm - "-vga qxl" - "-qmp unix:/tmp/qmp-${name}.sock,server,nowait" - "-serial file:/tmp/${name}-serial.log" - ]; - sharedDirectories = sharesFor name; - }; - }) - ]; - }).config.system.build.vm; - - vms = lib.mapAttrs mkVm - (lib.filterAttrs - (_: cfg: cfg.config.nixpkgs.hostPlatform.system == system) - self.nixosConfigurations); - - vmApps = lib.mapAttrs' - (name: vm: lib.nameValuePair "vm-${name}" { - type = "app"; - program = "${vm}/bin/run-${name}-vm"; - meta.description = "Boot the ${name} NixOS host as a QEMU VM."; - }) - vms; - - vmPackages = lib.mapAttrs' (name: lib.nameValuePair "vm-${name}") vms; - in - lib.optionalAttrs isLinux { - apps = vmApps; - packages = vmPackages; - }; -} From 1f753d798da4167e7131a7de2f3a2399af9ec2d4 Mon Sep 17 00:00:00 2001 From: Drake Bott <me@drake.dev> Date: Sun, 17 May 2026 18:54:47 -0500 Subject: [PATCH 3/3] update --- flake.lock | 36 ++++++++++++++++---------------- home/common/claude-settings.json | 3 +++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index 1ff67379..63801581 100644 --- a/flake.lock +++ b/flake.lock @@ -361,11 +361,11 @@ ] }, "locked": { - "lastModified": 1778606796, - "narHash": "sha256-P2krpSkFVYJ89bgsnAZ9RtQiGwiTW77sfSJp9SEDscM=", + "lastModified": 1778905220, + "narHash": "sha256-ox/5IHc8uwy6UTw6N7Shp6uCHIgu/S2PsWeuXsOHSo8=", "owner": "nix-community", "repo": "home-manager", - "rev": "e1fd7350f4410972bcb8c42a697d8c924ffe642a", + "rev": "d1686dc7d36cbd1234cb226ad6ef97e882716acb", "type": "github" }, "original": { @@ -410,11 +410,11 @@ "xwayland-satellite-unstable": "xwayland-satellite-unstable" }, "locked": { - "lastModified": 1778862364, - "narHash": "sha256-O0qC3IOHRscJcGPuDlIS4cLboKJZq358KH3oVzBeQjo=", + "lastModified": 1778942403, + "narHash": "sha256-SPCWvqeVySTNUgX/shARpRl5fi/NnkObUgDGR/Aco4c=", "owner": "sodiboo", "repo": "niri-flake", - "rev": "9ab3f8b17e22ead80525c4572b74156acf870526", + "rev": "daefca3370581223fedc24d0101c4915a3689f9e", "type": "github" }, "original": { @@ -515,11 +515,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1778593042, - "narHash": "sha256-xYGrSg6354UK2K4WSQd4+TfyvfqmvFbSY+ZtGQUXK0c=", + "lastModified": 1779058144, + "narHash": "sha256-OFaHDx6EMVUfufD2cxkLHXT3AZdhLnjS+RChKPLIyAI=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "9bd7c80d43e258aaa607d83b43661df11444d808", + "rev": "8ea1d787af06d65d83885264cb5572d966bfa289", "type": "github" }, "original": { @@ -578,11 +578,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1778794387, - "narHash": "sha256-BL04pOS9453Awkeb9f90XBJXBSkWxN+vB7HIgnL0iMM=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8a1b0127302ea51e05bf4ea5a291743fac442406", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { @@ -642,11 +642,11 @@ }, "nixpkgs_5": { "locked": { - "lastModified": 1778443072, - "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { @@ -804,11 +804,11 @@ "systems": "systems_4" }, "locked": { - "lastModified": 1778793632, - "narHash": "sha256-HYHD6J64bAWB2iT00lyyTn0wWcb0POtV+nPshYvq6Uc=", + "lastModified": 1779000518, + "narHash": "sha256-wdtytSnzMe85J/qeXJALMzSLRFTZ1gBHwn81l1PtT8k=", "owner": "Gerg-L", "repo": "spicetify-nix", - "rev": "e175a8b634e06a1b0635ec3d4db2c72cdc41fd15", + "rev": "5dde76b38418892ccb3d99e99bed7f8a43ac294c", "type": "github" }, "original": { diff --git a/home/common/claude-settings.json b/home/common/claude-settings.json index 496fb14e..aac072be 100644 --- a/home/common/claude-settings.json +++ b/home/common/claude-settings.json @@ -6,6 +6,9 @@ "allow": ["mcp__svelte__*"] }, "model": "opus", + "attribution": { + "commit": "" + }, "enabledPlugins": { "elements-of-style@superpowers-marketplace": true, "superpowers-lab@superpowers-marketplace": true,