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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Conservative setup integration for the ML4W network launcher, preserving custom scripts, existing backups, and write-free dry runs.
- Process-local `dreamcoder-nmtui` launcher with terminal-native inverse selection, Fish/Zsh routing, and a Waybar default entry point; explicit Newt style overrides remain supported.
- PEP 621 packaging: `pyproject.toml` with setuptools build backend, `__version__` in package.
- `dreamcoder-theme` CLI entry point (`dreamcoder-theme sync|doctor|paths`).
Expand Down
19 changes: 16 additions & 3 deletions docs/configuration/ml4w.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,23 @@ After every ML4W upgrade:

## What setup-hyprland.sh does

1. **Symlinks** wlogout + swaync `colors.css` → waybar (single theme toggle point)
1. **Symlinks** wlogout + swaync `colors.css` → `../waybar/colors.css`
(inside `~/.config`, a single theme toggle point). Regular files are backed
up before linking; existing incorrect links are repaired and current links
remain unchanged. `--dry-run` previews the same target without writing.
2. **Generates** `custom.lua` from JSON profile via `generate-custom-lua.sh`
3. **Installs** toggle script (`dreamcoder-toggle-theme.sh`) to `~/.config/hypr/scripts/`
4. **Applies** ML4W hooks (wallpaper, theme regeneration)
5. **Reloads** Hyprland
4. **Routes** the optional `~/.config/ml4w/scripts/ml4w-network` menu through
`~/.local/bin/dreamcoder-nmtui`, installed first. Only the two bare `nmtui`
calls in the recognized NetworkManager start/check script are replaced;
comments and formatting remain intact. Custom scripts, symlinks and conflicting
launchers are skipped with a warning. The original route is backed up to
`ml4w-network.bak` only when patched; existing backups are preserved.
Re-running setup repairs a missing launcher without rewriting an already
patched route. Re-run setup after an ML4W upgrade to restore the route.
`--dry-run` previews this installation without writes or network actions.
5. **Applies** ML4W hooks (wallpaper, theme regeneration)
6. **Reloads** Hyprland

## Theme Toggle

Expand Down Expand Up @@ -245,6 +257,7 @@ tests/ml4w/
├── generate_custom_lua.bats # generator, incl. the dreamcoder-colors loader
├── keybindings_variant.bats # dreamcoder.lua vs ML4W 2.16.1 and the profile
├── ml4w_managed.bats # ML4W ownership + colour-file predicates
├── nmtui_route.bats # safe optional network-menu route installation
├── setup_hyprland.bats # orchestrator
├── profile_validation.bats # JSON profiles
└── waybar_override.bats # Waybar accent override
Expand Down
87 changes: 87 additions & 0 deletions lib/nmtui_route.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Optional ML4W network route. Sourced: leave the caller's shell options alone.

_nmtui_route() {
# A strict known-body check protects arbitrary personal network scripts. Work
# in bytes so comments, indentation, line endings and final newlines survive.
python3 - "$1" "$2" <<'PY'
import pathlib
import re
import sys

path = pathlib.Path(sys.argv[1])
data = path.read_bytes()
launcher = b'"${HOME}/.local/bin/dreamcoder-nmtui"'
lines = [line.strip() for line in data.splitlines()
if line.strip() and not line.lstrip().startswith(b'#')]
expected = [b'if systemctl is-active --quiet NetworkManager; then', b'nmtui',
b'else', b'sudo systemctl start NetworkManager', b'if [ $? -eq 0 ]; then',
b'nmtui', b'else', b'echo "NetworkManager could not start"', b'fi', b'fi']
normalized = [b'nmtui' if line == launcher else line for line in lines]
if normalized != expected:
print('unknown')
elif sys.argv[2] == 'write':
path.write_bytes(re.sub(rb'(?m)^([ \t]*)nmtui([ \t]*\r?)$',
lambda m: m[1] + launcher + m[2], data))
else:
print('current' if lines.count(launcher) == 2 else 'pending')
PY
}

_nmtui_route_prerequisites() {
local route="$1" source_file="$2"
if [[ ! -f "${route}" && ! -L "${route}" ]]; then
warn "ML4W network script missing, skipped: ${route}"
return 1
fi
if [[ -L "${route}" || ! -f "${route}" ]]; then
warn "Unrecognized ML4W network script, skipped: ${route}"
return 1
fi
if ! command -v python3 >/dev/null || [[ ! -f "${source_file}" ]]; then
warn "nmtui route prerequisites missing, skipped: ${route}"
return 1
fi
}

install_nmtui_route() {
# setup-hyprland has already checked ML4W ownership. Use its logging helpers
# and the same compare-before-copy / no-personal-symlink policy as Waybar.
local source_file="$1/DreamcoderShell/.local/bin/dreamcoder-nmtui"
local route="${HOME}/.config/ml4w/scripts/ml4w-network"
local launcher="${HOME}/.local/bin/dreamcoder-nmtui" state
_nmtui_route_prerequisites "${route}" "${source_file}" || return 0
state="$(_nmtui_route "${route}" probe)" || return 1
if [[ "${state}" == unknown ]]; then
warn "Unrecognized ML4W network script, skipped: ${route}"
return 0
fi
if [[ -L "${launcher}" ]] || { [[ -e "${launcher}" ]] &&
{ [[ ! -f "${launcher}" ]] || ! cmp -s "${source_file}" "${launcher}"; }; }; then
warn "Conflicting nmtui launcher, skipped: ${launcher}"
return 0
fi
if [[ "${state}" == current && -x "${launcher}" ]]; then
info "ML4W network route already current: ${route}"
return 0
fi
if [[ "${DRY_RUN}" == true ]]; then
step "Would install ML4W network route: ${route}"
return 0
fi
# Never point the network menu at a launcher that has not installed safely.
if [[ ! -f "${launcher}" ]]; then
mkdir -p "$(dirname "${launcher}")" || return 1
cp "${source_file}" "${launcher}" || return 1
fi
if [[ ! -x "${launcher}" ]]; then
chmod +x "${launcher}" || return 1
fi
if [[ "${state}" != current ]]; then
if [[ ! -e "${route}.bak" && ! -L "${route}.bak" ]]; then
cp -p "${route}" "${route}.bak" || return 1
fi
_nmtui_route "${route}" write || return 1
fi
info "ML4W network route installed: ${route}"
}
41 changes: 23 additions & 18 deletions scripts/setup-hyprland.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# 2. Symlinks swaync/colors.css → waybar/colors.css
# 3. Generates ~/.config/hypr/custom.lua from machine profile
# 4. Installs dreamcoder-toggle-theme.sh to ~/.config/hypr/scripts/
# 5. Applies wallpaper hooks (calls apply-ml4w-hooks.sh)
# 5. Installs the optional ML4W network route and applies wallpaper hooks
# 6. Reloads Hyprland configuration
# 7. Logs a summary of the integration
#
Expand All @@ -31,6 +31,7 @@ set -euo pipefail
DREAMCODER_DOTS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
[[ -f "${DREAMCODER_DOTS_DIR}/lib/ml4w.sh" ]] && source "${DREAMCODER_DOTS_DIR}/lib/ml4w.sh"
[[ -f "${DREAMCODER_DOTS_DIR}/lib/waybar.sh" ]] && source "${DREAMCODER_DOTS_DIR}/lib/waybar.sh"
[[ -f "${DREAMCODER_DOTS_DIR}/lib/nmtui_route.sh" ]] && source "${DREAMCODER_DOTS_DIR}/lib/nmtui_route.sh"
ASSETS_DIR="${DREAMCODER_DOTS_DIR}/ml4w_assets"
GENERATOR="${DREAMCODER_DOTS_DIR}/scripts/generate-custom-lua.sh"
ML4W_HOOKS="${DREAMCODER_DOTS_DIR}/scripts/apply-ml4w-hooks.sh"
Expand Down Expand Up @@ -109,7 +110,7 @@ else
fi

# ── pre-flight validation ──────────────────────────────────────────────────
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Dry-run mode — no files will be modified"
echo ""
fi
Expand Down Expand Up @@ -161,11 +162,11 @@ if command -v check-json >/dev/null; then
SCHEMA_VALID=true
fi
elif command -v python3 >/dev/null && python3 -c "import jsonschema" 2>/dev/null; then
if python3 -c "
if python3 - "${SCHEMA_FILE}" "${PROFILE_FILE}" <<'PY' 2>/dev/null
import json, sys
with open('${SCHEMA_FILE}') as f:
with open(sys.argv[1]) as f:
schema = json.load(f)
with open('${PROFILE_FILE}') as f:
with open(sys.argv[2]) as f:
profile = json.load(f)
import jsonschema
try:
Expand All @@ -174,12 +175,13 @@ try:
except jsonschema.ValidationError as e:
print(e.message, file=sys.stderr)
sys.exit(1)
" 2>/dev/null; then
PY
then
SCHEMA_VALID=true
fi
fi

if $SCHEMA_VALID; then
if [[ "${SCHEMA_VALID}" == true ]]; then
ok "Profile matches schema"
((++PASS))
else
Expand Down Expand Up @@ -209,17 +211,17 @@ echo " Pre-flight: ${PASS} passed"
# ── 1. Symlink wlogout → waybar colors ────────────────────────────────────
echo ""
echo "═══ Step 1: Symlink Wlogout colors → Waybar ═══"
if $DRY_RUN; then
step "Would symlink: ${WLOGOUT_DIR}/colors.css → ../../waybar/colors.css"
step "Would symlink: ${SWAYNC_DIR}/colors.css → ../../waybar/colors.css"
if [[ "${DRY_RUN}" == true ]]; then
step "Would symlink: ${WLOGOUT_DIR}/colors.css → ../waybar/colors.css"
step "Would symlink: ${SWAYNC_DIR}/colors.css → ../waybar/colors.css"
else
for target_dir in "${WLOGOUT_DIR}" "${SWAYNC_DIR}"; do
local_colors="${target_dir}/colors.css"
backup="${local_colors}.bak"

if [[ -L "${local_colors}" ]]; then
current_target=$(readlink "${local_colors}")
if [[ "${current_target}" == "../../waybar/colors.css" ]]; then
if [[ "${current_target}" == "../waybar/colors.css" ]]; then
ok "Already linked: ${local_colors} → ${current_target}"
continue
fi
Expand All @@ -232,15 +234,15 @@ else
fi

rm -f "${local_colors}"
ln -sf "../../waybar/colors.css" "${local_colors}"
ln -sf "../waybar/colors.css" "${local_colors}"
info "Linked: ${local_colors} → $(readlink "${local_colors}")"
done
fi

# ── 2. Generate custom.lua from profile ───────────────────────────────────
echo ""
echo "═══ Step 2: Generate custom.lua from profile ═══"
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
bash "${GENERATOR}" --profile "${PROFILE_NAME}" --dry-run 2>&1
else
if [[ ! -f "${GENERATOR}" ]]; then
Expand Down Expand Up @@ -273,7 +275,7 @@ KEYBIND_SELECTOR_TARGET="${HYPR_DIR}/conf/keybinding.lua"
KEYBIND_VARIANT_TARGET="${HYPR_DIR}/conf/keybindings/dreamcoder.lua"
echo ""
echo "═══ Step 3: Install keybindings variant (dreamcoder.lua) ═══"
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Would install keybindings selector + variant"
elif [[ -f "${KEYBIND_VARIANT}" ]] && grep -q "CONTRACT: This file only defines keybindings NOT managed" "${KEYBIND_VARIANT}" 2>/dev/null; then
mkdir -p "$(dirname "${KEYBIND_SELECTOR_TARGET}")" "$(dirname "${KEYBIND_VARIANT_TARGET}")"
Expand All @@ -286,7 +288,7 @@ fi

# ML4W's launch.sh prefers style-custom.css in the selected style theme dir.
# Optional: a machine without the ML4W selector must not fail setup.
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Would install Waybar accent override (style-custom.css)"
else
install_waybar_override "${DREAMCODER_DOTS_DIR}" || true
Expand All @@ -295,7 +297,7 @@ fi
# ── 4. Install dreamcoder-toggle-theme.sh ─────────────────────────────────
echo ""
echo "═══ Step 4: Install dreamcoder-toggle-theme.sh ═══"
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Would install: ${TOGGLE_TARGET}"
else
mkdir -p "${HYPR_SCRIPTS}"
Expand All @@ -309,10 +311,13 @@ else
fi
fi

# The network menu uses the terminal-aware launcher; custom routes are skipped.
install_nmtui_route "${DREAMCODER_DOTS_DIR}"

# ── 5. Apply ML4W wallpaper hooks ─────────────────────────────────────────
echo ""
echo "═══ Step 5: Apply ML4W wallpaper hooks ═══"
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Would run: ${ML4W_HOOKS}"
else
if [[ -f "${ML4W_HOOKS}" ]]; then
Expand All @@ -326,7 +331,7 @@ fi
# ── 6. Reload Hyprland ────────────────────────────────────────────────────
echo ""
echo "═══ Step 6: Reload Hyprland ═══"
if $DRY_RUN; then
if [[ "${DRY_RUN}" == true ]]; then
step "Would run: hyprctl reload"
else
if command -v hyprctl >/dev/null; then
Expand Down
Loading
Loading