diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0c5ce2a..0e9799a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -88,19 +88,30 @@ how to override it. Items marked ✓ are shipped. green inputbar, yellow bottom-border). Remaining: author `.rasi` identities for the other four ported themes if/when they want one (the generated palette theme is the default and looks fine) -- **Visual theme picker (preview thumbnails):** replace the plain-text theme - list in `nomarchy-menu theme` with a rofi **icon grid of real desktop - previews** — each theme shown as a screenshot of its themed desktop (waybar - + a floating terminal), name beneath, the active theme marked, with the - `↩ Back` entry. **Workflow:** Bernardo captures the previews on real - hardware and commits them as `themes//preview.png`; a build-time - derivation downscales each to a small thumbnail (imagemagick), and the - picker loads it as the rofi element icon — rofi already renders image files, - which is how the Papirus menu icons work, so a `.png` path in the icon slot - Just Works. Grid layout via a per-invocation `-theme-str` (`listview { - columns; }` + a larger `element-icon`). Graceful fallback when a theme has - no `preview.png` yet: its wallpaper (`backgrounds/`), an accent swatch, or - the plain name — so it degrades cleanly as previews are added incrementally. +- ✓ **Visual theme picker (preview thumbnails):** `nomarchy-menu theme` is now + a rofi **icon grid of real desktop previews** instead of a plain-text list — + each theme a screenshot of its themed desktop (waybar + floating terminal), + pretty name beneath, **grouped dark-first then light** in one scrollable grid + (the previews make the mode obvious, so no light/dark submenu split), the + active theme marked `✓`, ending in `↩ Back`. The grid, the Name→slug map and + the active mark are all **generated at eval time** from the preset JSONs in + `rofi.nix` (`builtins.readDir` + `fromJSON`); "active" is just `t.slug`, since + every switch rebuilds the menu. Grid layout via a per-invocation `-theme-str` + (`listview { columns: 3; flow: horizontal; }` so Down scrolls row-by-row, + + vertical `element` cards, name centred below). **Sizing gotcha (learned the + hard way):** rofi's `element-icon` `size` is a **single value → a square + cell** (a two-value `WxH` is silently collapsed), and the icon is *contained* + in that square. So a 16:9 preview letterboxes (theme-coloured bands top/ + bottom), and a cell can't be shorter-than-square without the bands returning. + The fix: a build-time imagemagick step **centre-crops each preview to a + square** so it fills the cell edge-to-edge; the window width is derived so a + column is exactly the icon side (no slack margins). One knob, `themeGridIconW` + (240px), drives both icon and window. **Workflow:** Bernardo captures previews + on real hardware and commits them as `themes//preview.png`, **already + downscaled to 480×270** (~2.4 MB total for all 21, vs ~28 MB full-res) — the + source stays 16:9 and untouched; only the *displayed* thumb is squared at + build, so the crop is reversible. Graceful fallback when a theme has no + `preview.png`: a plain-name row, so it degrades cleanly. (A headless VM-render route — `runNixOSTest` + software-GL Hyprland (`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP framebuffer dump — was prototyped 2026-06-19 and **works** (themed waybar diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 2e6e39e..e6b614c 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -19,6 +19,92 @@ let rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi"; hasRasiOverride = builtins.pathExists rasiOverride; + # ── Visual theme picker ────────────────────────────────────────────── + # Every preset (themes/.json) is read at eval time to build a grid + # of real desktop previews. Grouped dark-first then light — the previews + # make the mode obvious at a glance, so no light/dark submenu split — and + # the active theme is marked ✓. "Active" is just t.slug: every switch + # rebuilds this script, so the baked-in mark always tracks the live theme. + themeFiles = lib.filterAttrs + (n: ty: ty == "regular" && lib.hasSuffix ".json" n) + (builtins.readDir cfg.themesDir); + themeData = lib.mapAttrsToList (fname: _: + let + j = builtins.fromJSON (builtins.readFile (cfg.themesDir + "/${fname}")); + slug = j.slug or (lib.removeSuffix ".json" fname); + in { + inherit slug; + name = j.name or slug; + mode = j.mode or "dark"; + hasPreview = builtins.pathExists (cfg.themesDir + "/${slug}/preview.png"); + }) themeFiles; + + byName = lib.sort (a: b: a.name < b.name); + orderedThemes = + byName (lib.filter (th: th.mode != "light") themeData) # dark group + ++ byName (lib.filter (th: th.mode == "light") themeData); # then light + + # rofi's element-icon cells are SQUARE, so a 16:9 preview would letterbox + # (theme-coloured bands top/bottom). Build-time crop each committed 480×270 + # preview to a centred square so it fills the cell edge-to-edge. The source + # preview.png stays 480×270 (untouched) — only the displayed thumb is square. + themeThumbs = pkgs.runCommand "nomarchy-theme-thumbs" + { nativeBuildInputs = [ pkgs.imagemagick ]; } '' + mkdir -p $out + ${lib.concatMapStringsSep "\n" + (th: lib.optionalString th.hasPreview '' + magick ${cfg.themesDir + "/${th.slug}/preview.png"} \ + -resize 360x360^ -gravity center -extent 360x360 -strip $out/${th.slug}.png + '') + orderedThemes} + ''; + + # One grid row per theme (square thumb + name, ✓ on the active one), and a + # Name→slug map so the picked label resolves back to the preset that + # nomarchy-theme-sync applies (pretty names ≠ slugs, hence the map). Themes + # with no preview degrade to a plain-name row. + themeRows = lib.concatMapStringsSep "\n" (th: + let label = th.name + lib.optionalString (th.slug == t.slug) " ✓"; + in if th.hasPreview + then "row ${lib.escapeShellArg label} ${themeThumbs}/${th.slug}.png" + else "printf '%s\\n' ${lib.escapeShellArg label}") + orderedThemes; + themeSlugMap = lib.concatMapStringsSep "\n" (th: + " [${lib.escapeShellArg th.name}]=${lib.escapeShellArg th.slug}") + orderedThemes; + + # Per-invocation grid layout (cards: a 16:9 preview above a centered name), + # overriding the single-column list theme just for this menu. + # · The icon box is sized 16:9 (rofi 2.0 takes a two-value `size`) to + # match the 480×270 previews exactly — they fill it with no letterbox. + # · The window is sized to the *content* (3 × icon + paddings), not a + # percentage, so a column is no wider than its card — otherwise the + # selection highlight balloons out around the image. + # · flow: horizontal lays cards out row-by-row (rofi's default Vertical + # fills column-by-column, so Down at a column's foot jumped to the top + # of the next column instead of scrolling the page). + # Grid dial — the previews are as big as the icon px allows; screen *height* + # caps it, so showing 2 rows (not 3) lets each card grow a lot. Scrolling + # reaches the rest. Tune themeGridIconW for size; the window resizes to fit. + # rofi's element-icon `size` is a SINGLE value (the manual only documents + # one) — a two-value "WxH" is silently collapsed, which is why the preview + # used to render tiny inside a too-wide column. So: size = the card *width*, + # rofi fits the 16:9 image tight to it (no square letterbox), and the window + # is sized so a column is exactly that width — the preview fills the cell. + themeGridCols = 3; + themeGridLines = 3; + themeGridIconW = 240; # square preview card side (px) — the size knob + themeGridPad = 0; # margin around the preview inside its cell (px) + themeGridGap = 8; # gap between cards (px) + themeGridThemeStr = lib.escapeShellArg (lib.concatStringsSep " " [ + # window width = cards + their padding + inter-card gaps + chrome (~20px) + "window { width: ${toString (themeGridCols * (themeGridIconW + 2 * themeGridPad) + (themeGridCols - 1) * themeGridGap + 20)}px; }" + "listview { columns: ${toString themeGridCols}; lines: ${toString themeGridLines}; spacing: ${toString themeGridGap}px; flow: horizontal; }" + "element { orientation: vertical; padding: ${toString themeGridPad}px; spacing: 2px; }" + "element-icon { size: ${toString themeGridIconW}px; }" + "element-text { horizontal-align: 0.5; }" + ]); + # Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built # from the SAME ./keybinds.nix that hyprland.nix binds, so it can never # drift from the live shortcuts. "$mod" reads as SUPER; rows are padded @@ -108,9 +194,20 @@ let [ -n "$choice" ] && powerprofilesctl set "$choice" ;; theme) - choice=$( { nomarchy-theme-sync list; printf '%s\n' "$BACK"; } | rofi -dmenu -p theme) || exit 0 + # Visual picker: a grid of real desktop previews (rows + Name→slug + # map generated from the presets at build time). Pick a card → apply + # that slug. The grid layout is a per-invocation -theme-str override. + declare -A THEME_SLUG=( +${themeSlugMap} + ) + choice=$( { +${themeRows} + back + } | rofi -dmenu -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0 [ "$choice" = "$BACK" ] && exec "$0" - [ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;; + choice="''${choice% ✓}" # drop the active marker if present + slug="''${THEME_SLUG[$choice]:-}" + [ -n "$slug" ] && exec nomarchy-theme-sync apply "$slug" ;; clipboard) sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi -dmenu -p clip) || exit 0 diff --git a/themes/catppuccin-latte/preview.png b/themes/catppuccin-latte/preview.png new file mode 100644 index 0000000..5fefd1f Binary files /dev/null and b/themes/catppuccin-latte/preview.png differ diff --git a/themes/catppuccin/preview.png b/themes/catppuccin/preview.png new file mode 100644 index 0000000..3fefec6 Binary files /dev/null and b/themes/catppuccin/preview.png differ diff --git a/themes/ethereal/preview.png b/themes/ethereal/preview.png new file mode 100644 index 0000000..e89c2f2 Binary files /dev/null and b/themes/ethereal/preview.png differ diff --git a/themes/everforest/preview.png b/themes/everforest/preview.png new file mode 100644 index 0000000..c931e6e Binary files /dev/null and b/themes/everforest/preview.png differ diff --git a/themes/flexoki-light/preview.png b/themes/flexoki-light/preview.png new file mode 100644 index 0000000..570f29b Binary files /dev/null and b/themes/flexoki-light/preview.png differ diff --git a/themes/gruvbox/preview.png b/themes/gruvbox/preview.png new file mode 100644 index 0000000..e90ce28 Binary files /dev/null and b/themes/gruvbox/preview.png differ diff --git a/themes/hackerman/preview.png b/themes/hackerman/preview.png new file mode 100644 index 0000000..8a8757a Binary files /dev/null and b/themes/hackerman/preview.png differ diff --git a/themes/kanagawa/preview.png b/themes/kanagawa/preview.png new file mode 100644 index 0000000..82734ba Binary files /dev/null and b/themes/kanagawa/preview.png differ diff --git a/themes/lumon/preview.png b/themes/lumon/preview.png new file mode 100644 index 0000000..8920f3f Binary files /dev/null and b/themes/lumon/preview.png differ diff --git a/themes/matte-black/preview.png b/themes/matte-black/preview.png new file mode 100644 index 0000000..9abd347 Binary files /dev/null and b/themes/matte-black/preview.png differ diff --git a/themes/miasma/preview.png b/themes/miasma/preview.png new file mode 100644 index 0000000..608db1d Binary files /dev/null and b/themes/miasma/preview.png differ diff --git a/themes/nord/preview.png b/themes/nord/preview.png new file mode 100644 index 0000000..b725dfa Binary files /dev/null and b/themes/nord/preview.png differ diff --git a/themes/osaka-jade/preview.png b/themes/osaka-jade/preview.png new file mode 100644 index 0000000..193c61f Binary files /dev/null and b/themes/osaka-jade/preview.png differ diff --git a/themes/retro-82/preview.png b/themes/retro-82/preview.png new file mode 100644 index 0000000..dcc35db Binary files /dev/null and b/themes/retro-82/preview.png differ diff --git a/themes/ristretto/preview.png b/themes/ristretto/preview.png new file mode 100644 index 0000000..76ff5a8 Binary files /dev/null and b/themes/ristretto/preview.png differ diff --git a/themes/rose-pine/preview.png b/themes/rose-pine/preview.png new file mode 100644 index 0000000..cfc2482 Binary files /dev/null and b/themes/rose-pine/preview.png differ diff --git a/themes/summer-day/preview.png b/themes/summer-day/preview.png new file mode 100644 index 0000000..b35f8f8 Binary files /dev/null and b/themes/summer-day/preview.png differ diff --git a/themes/summer-night/preview.png b/themes/summer-night/preview.png new file mode 100644 index 0000000..d86ea7a Binary files /dev/null and b/themes/summer-night/preview.png differ diff --git a/themes/tokyo-night/preview.png b/themes/tokyo-night/preview.png new file mode 100644 index 0000000..5e638dc Binary files /dev/null and b/themes/tokyo-night/preview.png differ diff --git a/themes/vantablack/preview.png b/themes/vantablack/preview.png new file mode 100644 index 0000000..021cb08 Binary files /dev/null and b/themes/vantablack/preview.png differ diff --git a/themes/white/preview.png b/themes/white/preview.png new file mode 100644 index 0000000..db936eb Binary files /dev/null and b/themes/white/preview.png differ