Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions docs/configuration/ml4w.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ updates, so custom bindings live in a separate **variant**. `conf/keybinding.lua
If you add a keybinding, add it to the profile JSON and re-run the generator —
never to `dreamcoder.lua` unless it is a native-only bind.

### Tracking upstream `default.lua` (ML4W 2.16)

`dreamcoder.lua` follows upstream `default.lua` minus the profile-owned binds.
The ML4W 2.16 delta is ported as follows:

| Upstream 2.16 change | In `dreamcoder.lua` |
| --- | --- |
| Overview moved to `~/.local/share/quickshell-overview` | `SUPER + Tab` runs `qs -p ~/.local/share/quickshell-overview ipc call overview toggle` |
| `SUPER + ALT + B` statusbar autohide | Ported |
| `SUPER + ALT + D` dock autohide | Ported |
| Reload Dock moved to `SUPER + SHIFT + D` | Not bound — the profile owns that combo (theme toggle) |
| Rewritten AZERTY detection (`fr`, `be`) | Ported; binds the AZERTY keysyms only on AZERTY layouts, since the profile owns the digit workspace binds |

`tests/ml4w/keybindings_variant.bats` fails on any new collision between the
variant and the profile. The `SUPER + SHIFT + arrows` overlap (variant resize,
profile move) predates 2.16 and is listed there as a known exception.

## hyprctl dispatch is broken on Hyprland 0.55+ — native dispatchers used

Hyprland's Lua config parses `hyprctl dispatch <arg>` as Lua
Expand All @@ -97,6 +114,41 @@ translates the following `hyprctl dispatch` commands in profiles to native

Any other command still falls back to `hl.dsp.exec_cmd(...)`.

## Upgrade-proof hooks: Dreamcoder hooks live in Dreamcoder-owned files

ML4W upgrades overwrite every file ML4W ships (`hyprland.lua`, the
`ml4w-wallpaper` runner, shipped keybinding variants). A line injected into
one of those files silently disappears on the next upgrade. The rule:

- **Put hooks in files ML4W never ships.** `custom.lua` (generated from the
profile) loads `dreamcoder-colors`; `hyprland.lua` already requires
`custom.lua` when it exists, so nothing is injected into `hyprland.lua`.
`dreamcoder doctor` accepts the loader from `custom.lua` (a legacy require in
`hyprland.lua` is still recognised).
- **When a hook must live in an ML4W file, make it re-appliable.**
`scripts/apply-ml4w-hooks.sh` appends the wallpaper hook to
`~/.config/ml4w/scripts/ml4w-wallpaper` (the runner ML4W 2.16's Quickshell
wallpaper app calls with `$IMAGE_PATH`) between
`# >>> Dreamcoder wallpaper hook >>>` markers. Each run replaces the block, so
re-running it after an ML4W upgrade restores the hook without duplicates.
- **waypaper is optional.** ML4W 2.16 no longer installs it; its
`post_command` is hooked only when `~/.config/waypaper/config.ini` exists.
- **Colour files may be regular files.** ML4W 2.16 ships
`~/.config/hypr/colors.lua` / `colors.conf` as regular files and the theme
sync writes Dreamcoder colours through them (it only re-points files that are
already symlinks). `doctor.sh` and `verify-ml4w-setup.sh` accept either a
symlink into a Dreamcoder variant or a regular file whose bytes match a
`DreamcoderThemes/dreamcoder/hypr-colors-*` variant.

After every ML4W upgrade:

```bash
./scripts/generate-custom-lua.sh # custom.lua (keybinds + colour loader)
./scripts/apply-ml4w-hooks.sh # re-hook the wallpaper runner
./scripts/dreamcoder sync # rewrite colors.lua / colors.conf
./scripts/verify-ml4w-setup.sh
```

## What setup-hyprland.sh does

1. **Symlinks** wlogout + swaync `colors.css` → waybar (single theme toggle point)
Expand Down Expand Up @@ -148,8 +200,15 @@ DreamcoderProfiles/dreamcoder/
└── asus-vivobook15.json # ASUS VivoBook 15 profile (all Fn keys)

tests/ml4w/
├── generate_custom_lua.bats # 13 tests for the generator
├── setup_hyprland.bats # 9 tests for the orchestrator
├── profile_validation.bats # 11 tests for JSON profiles
└── setup.bash # BATS test helper
├── apply_ml4w_hooks.bats # wallpaper hook (ML4W 2.16 runner fixture)
├── args.bats # script argument handling
├── generate_custom_lua.bats # generator, incl. the dreamcoder-colors loader
├── keybindings_variant.bats # dreamcoder.lua vs ML4W 2.16 and the profile
├── ml4w_managed.bats # ML4W ownership + colour-file predicates
├── setup_hyprland.bats # orchestrator
├── profile_validation.bats # JSON profiles
└── waybar_override.bats # Waybar accent override

tests/fixtures/ml4w/
└── ml4w-wallpaper-2.16 # upstream runner at tag 2.16 (3960570)
```
8 changes: 4 additions & 4 deletions docs/installation/linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ readlink ~/.config/btop/themes/dreamcoder.theme # → dreamcoder-light.them
readlink ~/.config/waybar/colors.css # → colors-light.css or colors-dark.css
readlink ~/.config/rofi/colors.rasi # → colors-light.rasi or colors-dark.rasi

# 3. Hyprland imports dreamcoder
grep "dreamcoder-colors" ~/.config/hypr/hyprland.lua # → require("dreamcoder-colors")
# 3. Hyprland loads dreamcoder colours (from the generated custom.lua)
grep "dreamcoder-colors" ~/.config/hypr/custom.lua # → require("dreamcoder-colors")

# 4. Timer is active
systemctl --user is-active dreamcoder-theme-auto.timer # → active
Expand Down Expand Up @@ -115,8 +115,8 @@ rm ~/.config/hypr/dreamcoder-colors.lua
rm ~/.config/btop/themes/dreamcoder.theme
sed -i 's/color_theme = "dreamcoder"/color_theme = "matugen"/' ~/.config/btop/btop.conf

# Remove dreamcoder import from hyprland.lua:
# Edit ~/.config/hypr/hyprland.lua and remove line: require("dreamcoder-colors")
# The dreamcoder-colors loader lives in the generated ~/.config/hypr/custom.lua;
# with dreamcoder-colors.lua removed, its guard skips the require.

# Disable timer
systemctl --user disable --now dreamcoder-theme-auto.timer
Expand Down
4 changes: 2 additions & 2 deletions docs/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ repository that ships the dotfiles. Gentleman.Dots is hosted at

| Upstream | Kind | Verified remote (HTTPS) | Pinned ref | Status |
| --- | --- | --- | --- | --- |
| ML4W (Hyprland desktop dotfiles) | Desktop base environment | <https://github.com/mylinuxforwork/dotfiles.git> | `46f2ca7f73fe98b16ce4ab6433a9ac29fa9fd033` | Pinned — verified against remote HEAD |
| Gentleman.Dots | Shell / editor / terminal base configuration | <https://github.com/Gentleman-Programming/Gentleman.Dots.git> | `02584500de6378ff5f54d252dc28fce8424b088a` | Pinned — verified against remote HEAD |
| ML4W (Hyprland desktop dotfiles) | Desktop base environment | <https://github.com/mylinuxforwork/dotfiles.git> | `3960570f47f4f691c424ff46b387d389a8e69bca` (tag `2.16`) | Pinned — verified against tag 2.16 and remote HEAD |
| Gentleman.Dots | Shell / editor / terminal base configuration | <https://github.com/Gentleman-Programming/Gentleman.Dots.git> | `6f44b797b016aea92772d8a6d81a5f1bc53a84bb` | Pinned — verified against remote HEAD |

### Pin mechanism

Expand Down
14 changes: 7 additions & 7 deletions docs/upstream-manifest.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"version": 1,
"provenance": {
"verified_on": "2026-08-10T01:27:49Z",
"method": "Resolved each upstream HTTPS remote HEAD with git ls-remote and recorded the exact returned commit; a second independent re-verification on the same day returned the same refs, so both stay pinned. No ref was ever filled without verification.",
"command": "git ls-remote https://github.com/mylinuxforwork/dotfiles.git HEAD; git ls-remote https://github.com/Gentleman-Programming/Gentleman.Dots.git HEAD"
"verified_on": "2026-09-28T05:42:13Z",
"method": "Resolved each upstream HTTPS remote with git ls-remote and recorded the exact returned commit. ML4W is pinned to tag 2.16 (lightweight tag, 3960570f47f4f691c424ff46b387d389a8e69bca), which was also the remote HEAD at verification; Gentleman.Dots is pinned to main HEAD. No ref was ever filled without verification.",
"command": "git ls-remote https://github.com/mylinuxforwork/dotfiles.git HEAD refs/tags/2.16; git ls-remote https://github.com/Gentleman-Programming/Gentleman.Dots.git HEAD refs/heads/main"
},
"upstreams": {
"ml4w": {
"name": "ML4W (Hyprland desktop dotfiles)",
"url": "https://github.com/mylinuxforwork/dotfiles.git",
"status": "pinned",
"pinned_ref": "46f2ca7f73fe98b16ce4ab6433a9ac29fa9fd033",
"verified_on": "2026-08-10T01:27:49Z"
"pinned_ref": "3960570f47f4f691c424ff46b387d389a8e69bca",
"verified_on": "2026-09-28T05:42:13Z"
},
"gentleman-dots": {
"name": "Gentleman.Dots (shell / editor / terminal base configuration)",
"url": "https://github.com/Gentleman-Programming/Gentleman.Dots.git",
"status": "pinned",
"pinned_ref": "02584500de6378ff5f54d252dc28fce8424b088a",
"verified_on": "2026-08-10T01:27:49Z"
"pinned_ref": "6f44b797b016aea92772d8a6d81a5f1bc53a84bb",
"verified_on": "2026-09-28T05:42:13Z"
}
},
"owned_paths": {}
Expand Down
33 changes: 30 additions & 3 deletions lib/ml4w.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# ============================================================================
# ml4w.sh — ML4W ownership predicates (pure; no side effects)
# ============================================================================
# This library deliberately omits `set -euo pipefail` (and <30 lines total):
# sourcing a library must not mutate the caller's shell options. Both callers
# set their own options before sourcing this file.
# This library deliberately omits `set -euo pipefail`: sourcing a library must
# not mutate the caller's shell options. Callers set their own options before
# sourcing this file.
#
# ML4W ownership is accepted from either supported layout:
# - old layout: individual config files symlinked from the ML4W dotfiles
Expand All @@ -25,3 +25,30 @@ waybar_is_ml4w_managed() {
[[ -f "${HOME}/.config/waybar/launch.sh" ]] && return 0
return 1
}

# Hyprland colour files (colors.lua / colors.conf) carry Dreamcoder colours when
# they are a symlink into a Dreamcoder variant, or a regular file byte-identical
# to one of the DreamcoderThemes hypr-colors-* variants. Regular files are the
# supported layout: ML4W 2.16 ships them as regular files and the theme sync
# writes through whatever sits at the path (it only flips existing symlinks).
hypr_colors_is_dreamcoder() {
local path="$1" variant
[[ -e "${path}" ]] || return 1
[[ -L "${path}" && "$(readlink "${path}")" == *dreamcoder* ]] && return 0
for variant in "${DREAMCODER_DOTS_DIR}"/DreamcoderThemes/dreamcoder/hypr-colors-*."${path##*.}"; do
[[ -f "${variant}" ]] && cmp -s "${path}" "${variant}" && return 0
done
return 1
}

# Waybar colors.css carries Dreamcoder colours when it is a symlink into a
# Dreamcoder variant, or a regular file written by the theme sync (identified by
# its generator header). The sync renders the active mode straight into the
# path, so a regular file is the supported layout on ML4W 2.16.
waybar_colors_is_dreamcoder() {
local path="$1"
[[ -e "${path}" ]] || return 1
[[ -L "${path}" && "$(readlink "${path}")" == *dreamcoder* ]] && return 0
[[ -L "${path}" ]] && return 1
head -n 5 "${path}" | grep -q 'Generated by Dreamcoder sync'
}
50 changes: 49 additions & 1 deletion ml4w_assets/hypr/conf/keybindings/dreamcoder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,64 @@ hl.bind(mainMod .. " + ALT + W", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-wa
hl.bind(mainMod .. " + CTRL + RETURN", hl.dsp.exec_cmd("~/.config/hypr/scripts/launcher.sh"), { description = "Open application launcher" })
hl.bind(mainMod .. " + SHIFT + B", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-reload-statusbar"), { description = "Reload Status Bar" })
hl.bind(mainMod .. " + CTRL + B", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-statusbar"), { description = "Toggle Status Bar" })
hl.bind(mainMod .. " + ALT + B", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-statusbar-autohide"), { description = "Toggle Status Bar Autohide" })
-- ML4W 2.16 moved "Reload Dock" to SUPER + SHIFT + D, which is profile-owned
-- here (Dreamcoder theme toggle), so the dock reload stays unbound.
hl.bind(mainMod .. " + ALT + D", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-dock-autohide"), { description = "Toggle Dock Autohide" })
hl.bind(mainMod .. " + SHIFT + R", hl.dsp.exec_cmd("~/.config/hypr/scripts/loadconfig.sh"), { description = "Reload hyprland config" })
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("~/.config/waybar/themeswitcher.sh"), { description = "Open waybar theme switcher" })
hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-toggle-theme"), { description = "Toggle between light and dark mode" })
hl.bind(mainMod .. " + ALT + G", hl.dsp.exec_cmd("~/.config/hypr/scripts/gamemode.sh"), { description = "Toggle game mode" })
hl.bind(mainMod .. " + CTRL + L", hl.dsp.exec_cmd("~/.config/ml4w/scripts/ml4w-power -l"), { description = "Lock Screen" })
-- Note: SHIFT + H is profile-owned (Move Window Left); hyprsunset is
-- profile-owned too via SHIFT + U (on) / SHIFT + I (off).
hl.bind(mainMod .. " + Tab", hl.dsp.exec_cmd("qs -p ~/.config/quickshell/overview ipc call overview toggle"), { description = "Open Select Window Menu" })
hl.bind(mainMod .. " + Tab", hl.dsp.exec_cmd("qs -p ~/.local/share/quickshell-overview ipc call overview toggle"), { description = "Open Select Window Menu" })
hl.bind("CTRL + ALT + T", hl.dsp.exec_cmd("~/.config/ml4w/themes/themes.sh"), { description = "Open Select Window Menu" })

-- AZERTY keyboard layout setup (ported from ML4W 2.16 default.lua)
-- The profile owns SUPER + [0-9] workspace binds. On AZERTY the number row
-- needs Shift, so Hyprland sees the unshifted keysyms instead of the digits;
-- bind those keysyms here only when an AZERTY layout is detected, so QWERTY
-- layouts never get duplicate workspace binds.
local azerty_keys = {
fr = { "ampersand", "eacute", "quotedbl", "apostrophe", "parenleft",
"minus", "egrave", "underscore", "ccedilla", "agrave" },
be = { "ampersand", "eacute", "quotedbl", "apostrophe", "parenleft",
"section", "egrave", "exclam", "ccedilla", "agrave" },
}

-- Variants of the layouts above that are not AZERTY
local non_azerty_variants = {
fr = { us = true, bepo = true, bepo_afnor = true, dvorak = true },
be = { wang = true },
}

local function detect_azerty()
local f = io.open(os.getenv("HOME") .. "/.config/hypr/input.lua", "r")
if not f then return nil end
local content = f:read("*all")
f:close()

-- kb_layout may be a list ("be,us"); the first entry is the primary one
local layout = content:match('kb_layout%s*=%s*"([^",]*)')
local variant = content:match('kb_variant%s*=%s*"([^",]*)') or ""
if not layout then return nil end
layout = layout:lower():gsub("%s", "")
variant = variant:lower():gsub("%s", "")

local excluded = non_azerty_variants[layout]
if excluded and excluded[variant] then return nil end
return azerty_keys[layout]
end

local ws_keys = detect_azerty()
if ws_keys then
for i = 1, 10 do
hl.bind(mainMod .. " + " .. ws_keys[i], hl.dsp.focus({ workspace = i }), { description = "Focus workspace " .. i })
hl.bind(mainMod .. " + SHIFT + " .. ws_keys[i], hl.dsp.window.move({ workspace = i }), { description = "Move window to workspace " .. i })
end
end

-- Special workspace (scratchpad)
-- Note: SHIFT + S is profile-owned (screenshot screen), so only plain S here.
hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("scratchpad"), { description = "Toggle special workspace scratchpad" })
Expand Down
Loading
Loading