Compare commits
2 Commits
964ce103a0
...
d680730d30
| Author | SHA1 | Date |
|---|---|---|
|
|
d680730d30 | |
|
|
ed44b84afa |
|
|
@ -279,9 +279,13 @@ class Taskbar(Gtk.Box):
|
|||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
row.append(Gtk.Label(label="center-focused", xalign=0.0, hexpand=True))
|
||||
sw = Gtk.Switch(valign=Gtk.Align.CENTER)
|
||||
if ly["name"] == "columns":
|
||||
sw.set_tooltip_text("Keep the focused window centred in its column as "
|
||||
"it scrolls, instead of the minimal-movement default")
|
||||
else:
|
||||
sw.set_tooltip_text("Keep the focused column centred so the prev/next "
|
||||
"columns stay on-screen and tappable")
|
||||
sw.connect("state-set", self._on_fit_toggle)
|
||||
sw.connect("state-set", lambda s, state, name=ly["name"]: self._on_fit_toggle(s, state, name))
|
||||
row.append(sw)
|
||||
page.append(row)
|
||||
self._fit_sws[ly["name"]] = sw
|
||||
|
|
@ -325,6 +329,15 @@ class Taskbar(Gtk.Box):
|
|||
self._cols_lbl.set_label(str(new))
|
||||
_eval(f'hl.dispatch(hl.dsp.layout("cols {"+1" if delta > 0 else "-1"}"))')
|
||||
|
||||
def _read_center(self) -> bool:
|
||||
# columns.lua's own "center-focused" switch — global, published in the same
|
||||
# cache file as the columns stepper's count (see columns.lua's publish()).
|
||||
try:
|
||||
data = json.loads(_COLUMNS_STATE.read_text())
|
||||
return bool(data.get("_center", False))
|
||||
except (FileNotFoundError, json.JSONDecodeError, ValueError, TypeError):
|
||||
return False
|
||||
|
||||
# -- sync + handlers ---------------------------------------------------
|
||||
def _sync_layout_controls(self) -> None:
|
||||
# reflect the current per-ws layout / direction / focus_fit in the tabs+options
|
||||
|
|
@ -332,6 +345,11 @@ class Taskbar(Gtk.Box):
|
|||
run_json(["hyprctl", "getoption", "general:layout", "-j"], self._apply_tab_sel)
|
||||
run_json(["hyprctl", "getoption", "scrolling:direction", "-j"], self._apply_dir_sel)
|
||||
run_json(["hyprctl", "getoption", "scrolling:focus_fit_method", "-j"], self._apply_fit_sel)
|
||||
# columns' own center-focused state lives in columns-state.json, not a hyprctl
|
||||
# option — read it synchronously rather than round-tripping a subprocess.
|
||||
columns_sw = self._fit_sws.get("columns")
|
||||
if columns_sw is not None:
|
||||
columns_sw.set_active(self._read_center())
|
||||
|
||||
def _apply_tab_sel(self, ok, data) -> None:
|
||||
cur = data.get("str") if ok and isinstance(data, dict) else None
|
||||
|
|
@ -356,9 +374,13 @@ class Taskbar(Gtk.Box):
|
|||
self._syncing = False
|
||||
|
||||
def _apply_fit_sel(self, ok, data) -> None:
|
||||
# scrolling:focus_fit_method only describes the scrolling layout's own switch;
|
||||
# columns' switch is synced separately from columns-state.json (see
|
||||
# _sync_layout_controls) since it isn't backed by a hyprctl option at all.
|
||||
val = data.get("int") if ok and isinstance(data, dict) else 0
|
||||
sw = self._fit_sws.get("scrolling")
|
||||
if sw is not None:
|
||||
self._syncing = True
|
||||
for sw in self._fit_sws.values():
|
||||
sw.set_active(val == 1)
|
||||
self._syncing = False
|
||||
|
||||
|
|
@ -384,8 +406,11 @@ class Taskbar(Gtk.Box):
|
|||
if name:
|
||||
_eval(f'layouts.set("{self._active_ws}", "{name}", "{self._cur_dir()}")')
|
||||
|
||||
def _on_fit_toggle(self, _sw, state) -> bool:
|
||||
def _on_fit_toggle(self, _sw, state, name) -> bool:
|
||||
if not getattr(self, "_syncing", False):
|
||||
if name == "columns":
|
||||
_eval(f'hl.dispatch(hl.dsp.layout("center {"on" if state else "off"}"))')
|
||||
else:
|
||||
_eval(f"layouts.set_fit({1 if state else 0})")
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@
|
|||
-- vertical ws: k/j = −/+ columns, h/l = −/+ per-view.
|
||||
-- cols +/-/N — direct column-count set (the menu's stepper).
|
||||
-- orient h/v — orientation (set from the menu dropdown).
|
||||
-- center on/off/toggle — "center-focused" switch (menu): when a column is overfull
|
||||
-- and scrolls, keep the focused window centred in the column
|
||||
-- instead of the minimal-movement default (whichever edge of the
|
||||
-- focused window would otherwise go off-screen just barely comes
|
||||
-- back into view). Global (mirrors the scrolling layout's
|
||||
-- "Centered follow" toggle), persisted across reloads, OFF by
|
||||
-- default. Only ever repositions the column holding the focused
|
||||
-- window — every other column's pan is untouched, since there is
|
||||
-- only one focused window at a time.
|
||||
--
|
||||
-- State is per workspace; a window's column assignment, order, both weight tables and
|
||||
-- each column's pan offset persist across relayouts.
|
||||
|
|
@ -48,8 +57,22 @@ local function clampw(v) return math.max(RMIN, math.min(RMAX, v)) end
|
|||
|
||||
-- Publish per-workspace column count so the astal-menu can show a live stepper.
|
||||
local STATE_FILE = (os.getenv("HOME") or "") .. "/.cache/astal-menu/columns-state.json"
|
||||
|
||||
-- "center-focused" switch — global (like the scrolling layout's own fit-method
|
||||
-- toggle), so a single state, not per-workspace. Seeded once at module load from
|
||||
-- the last-published state, since a reload wipes this module's Lua state (same
|
||||
-- reason ncols/rows/orient are seeded — see seed() below).
|
||||
local center_focus = false
|
||||
do
|
||||
local f = io.open(STATE_FILE, "r")
|
||||
if f then
|
||||
local data = f:read("*a") or ""; f:close()
|
||||
if data:match('"_center"%s*:%s*true') then center_focus = true end
|
||||
end
|
||||
end
|
||||
|
||||
local function publish()
|
||||
local parts = {}
|
||||
local parts = { string.format('"_center":%s', tostring(center_focus)) }
|
||||
for wsid, s in pairs(state) do
|
||||
parts[#parts + 1] = string.format('"%s":{"cols":%d,"rows":%d,"orient":"%s"}',
|
||||
tostring(wsid), s.ncols, s.rows, s.orient)
|
||||
|
|
@ -258,15 +281,24 @@ local function recalculate(ctx)
|
|||
for r = 1, #rowlist do rh[r] = rh[r] * scale end
|
||||
pan = 0
|
||||
else
|
||||
-- overfull: keep natural heights and PAN so the focused row stays in view.
|
||||
-- overfull: keep natural heights and PAN so the focused row stays in view
|
||||
-- (center_focus: centred in the viewport instead of minimal movement).
|
||||
-- Only the column holding the focused window (frow ~= nil) is touched here;
|
||||
-- every other column keeps whatever pan it already had.
|
||||
pan = s.pan[c] or 0
|
||||
if frow then
|
||||
local top = 0
|
||||
for r = 1, frow - 1 do top = top + rh[r] end
|
||||
local bottom = top + rh[frow]
|
||||
if center_focus then
|
||||
pan = top + (rh[frow] - secLen) / 2
|
||||
else
|
||||
if top - pan < 0 then pan = top end
|
||||
if bottom - pan > secLen then pan = bottom - secLen end
|
||||
end
|
||||
end
|
||||
-- clamp to the column's actual scroll range either way — this is also what
|
||||
-- keeps "centred" from showing blank space past the first/last row.
|
||||
pan = math.max(0, math.min(pan, total - secLen))
|
||||
end
|
||||
s.pan[c] = pan
|
||||
|
|
@ -300,11 +332,23 @@ local function recalculate(ctx)
|
|||
end
|
||||
|
||||
local function layout_msg(ctx, cmd)
|
||||
local verb, arg = tostring(cmd or ""):match("^(%S+)%s*(%S*)$")
|
||||
if not verb then return end
|
||||
|
||||
-- "center-focused" switch: global, doesn't touch any window, so handled before the
|
||||
-- empty-workspace bail-out below (toggling it from the menu shouldn't require a
|
||||
-- window to be focused/present).
|
||||
if verb == "center" then
|
||||
if arg == "on" then center_focus = true
|
||||
elseif arg == "off" then center_focus = false
|
||||
else center_focus = not center_focus end
|
||||
publish()
|
||||
return
|
||||
end
|
||||
|
||||
local t = ctx.targets
|
||||
if type(t) ~= "table" or not t[1] then return end
|
||||
local s = ws_state(win_ws_id(t[1].window))
|
||||
local verb, arg = tostring(cmd or ""):match("^(%S+)%s*(%S*)$")
|
||||
if not verb then return end
|
||||
local cols, _, foc = columnize(s, t)
|
||||
local horizontal = (s.orient ~= "v")
|
||||
|
||||
|
|
@ -426,6 +470,6 @@ return {
|
|||
dirs = { "h", "v" },
|
||||
dir_labels = { "Horizontal", "Vertical" },
|
||||
default_dir = "h",
|
||||
fit_method = false,
|
||||
fit_method = true, -- menu shows the "center-focused" switch (routed to "center on/off")
|
||||
stepper = true, -- menu shows a "Columns: (-)[N](+)" count stepper
|
||||
}
|
||||
|
|
|
|||
151
sysupdate.sh
151
sysupdate.sh
|
|
@ -597,6 +597,75 @@ _fix_hypr_usr() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Offer to redeploy hypr/usr/ (device-specific lua) from the dotfiles template,
|
||||
# file by file. hypr/usr/ is excluded from the normal config-updater sync to
|
||||
# preserve per-device customisation (monitors.lua, wallpaper.conf), but feature/
|
||||
# bind updates in autostart.lua, binds.lua, windowrules.lua etc. still land in
|
||||
# the dotfiles template and need an explicit, reviewable opt-in to reach a
|
||||
# machine that already has its own hypr/usr/. Skipped entirely on first-time
|
||||
# setup — _migrate_hypr_usr already seeds usr/ from the template in that case.
|
||||
_redeploy_usr_configs() {
|
||||
local de_dir; de_dir=$(_de_source_dir)
|
||||
local src_usr="${de_dir:+$de_dir/hypr/usr}"
|
||||
local usr_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hypr/usr"
|
||||
|
||||
[[ -n "${src_usr:-}" && -d "$src_usr" ]] || return 0
|
||||
[[ -d "$usr_dir" ]] || return 0
|
||||
|
||||
ask "Redeploy user-local configs (hypr/usr/)?" || { log "Skipped user-local config redeploy"; return 0; }
|
||||
|
||||
local -a files=()
|
||||
while IFS= read -r -d '' item; do
|
||||
files+=("$(basename "$item")")
|
||||
done < <(find "$src_usr" -maxdepth 1 -mindepth 1 -type f ! -name '*.old' -print0 | sort -z)
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
warn "No template files found in $src_usr"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a selected=()
|
||||
if command -v dialog &>/dev/null; then
|
||||
local items=()
|
||||
for f in "${files[@]}"; do
|
||||
local tag="on"
|
||||
[[ "$f" == "monitors.lua" || "$f" == "wallpaper.conf" ]] && tag="off"
|
||||
local status="new"
|
||||
[[ -f "$usr_dir/$f" ]] && { diff -q "$usr_dir/$f" "$src_usr/$f" &>/dev/null && status="unchanged" || status="differs"; }
|
||||
items+=("$f" "($status)" "$tag")
|
||||
done
|
||||
local chosen
|
||||
chosen=$(dialog --stdout --separate-output \
|
||||
--title " Redeploy hypr/usr/ " \
|
||||
--checklist " SPACE = toggle | ENTER = confirm | ESC = cancel (monitors.lua / wallpaper.conf default OFF — device-specific) " \
|
||||
0 70 0 "${items[@]}") || { warn "Cancelled — no user-local configs redeployed."; return 0; }
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] && selected+=("$line")
|
||||
done <<< "$chosen"
|
||||
else
|
||||
warn "'dialog' not installed — redeploying all except monitors.lua/wallpaper.conf"
|
||||
for f in "${files[@]}"; do
|
||||
[[ "$f" == "monitors.lua" || "$f" == "wallpaper.conf" ]] && continue
|
||||
selected+=("$f")
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#selected[@]} -eq 0 ]]; then
|
||||
warn "Nothing selected — no user-local configs redeployed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
for f in "${selected[@]}"; do
|
||||
if cp -p "$src_usr/$f" "$usr_dir/$f"; then
|
||||
ok "redeployed hypr/usr/$f"
|
||||
else
|
||||
err "failed hypr/usr/$f"
|
||||
fi
|
||||
done
|
||||
|
||||
_fix_hypr_usr
|
||||
}
|
||||
|
||||
# Deploy root-owned greeter config (/etc/greetd) that config-updater can't reach.
|
||||
# Only when greetd is the active greeter on this machine. Uses the sudo credential
|
||||
# primed once at startup, so it doesn't re-prompt. PAM is intentionally left to the
|
||||
|
|
@ -613,13 +682,95 @@ _deploy_greetd() {
|
|||
ok "greetd config deployed" || warn "greetd config deploy had errors"
|
||||
}
|
||||
|
||||
# Ensure this machine has finished the Plymouth + greetd/ReGreet boot/login
|
||||
# migration. Delegates entirely to setup/tools/migrate-to-greetd.sh (idempotent,
|
||||
# already handles old-DM detection/teardown, PAM/FIDO, wallpaper sync, initramfs
|
||||
# + GRUB) rather than duplicating that logic here. Skips silently once the
|
||||
# machine is already on the target stack.
|
||||
_ensure_plymouth_regreet() {
|
||||
local migrate_tool="$DOTFILES/setup/tools/migrate-to-greetd.sh"
|
||||
[[ -x "$migrate_tool" ]] || return 0
|
||||
|
||||
local greetd_state; greetd_state=$(systemctl is-enabled greetd.service 2>/dev/null || true)
|
||||
local using_regreet=false
|
||||
grep -q 'regreet' /etc/greetd/config.toml 2>/dev/null && using_regreet=true
|
||||
local plymouth_ok=false
|
||||
pacman -Qq plymouth &>/dev/null && [[ -d /usr/share/plymouth/themes/m-archy ]] && plymouth_ok=true
|
||||
|
||||
if [[ "$greetd_state" == "enabled" && "$using_regreet" == true && "$plymouth_ok" == true ]]; then
|
||||
ok "Plymouth + greetd/ReGreet already active"
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "Not yet on Plymouth + greetd/ReGreet (currently: greetd=${greetd_state}, regreet=${using_regreet}, plymouth=${plymouth_ok})"
|
||||
if ask "Migrate now? (installs plymouth+regreet+cage, rebuilds initramfs; reboot needed after)"; then
|
||||
"$migrate_tool" --yes
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure the Astal GObject typelibs astal-menu's main.py imports at startup are
|
||||
# installed. config-updater only copies the astal-menu/ config directory — it
|
||||
# never installs packages — so a machine that's never run the AUR install is
|
||||
# missing these even after a full config sync.
|
||||
_ensure_astal_menu_deps() {
|
||||
local de_dir; de_dir=$(_de_source_dir)
|
||||
[[ "${de_dir:-}" == */hyprlua ]] || return 0
|
||||
[[ -d "$de_dir/astal-menu" ]] || return 0
|
||||
|
||||
local -a needed=(libastal-network-git libastal-bluetooth-git libastal-apps-git)
|
||||
local -a missing=()
|
||||
for pkg in "${needed[@]}"; do
|
||||
pacman -Qq "$pkg" &>/dev/null || missing+=("$pkg")
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -eq 0 ]]; then
|
||||
ok "astal-menu AUR deps already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "astal-menu is missing AUR deps: ${missing[*]}"
|
||||
if ask "Install them now?"; then
|
||||
if yay -S --noconfirm --needed "${missing[@]}"; then
|
||||
ok "astal-menu deps installed"
|
||||
else
|
||||
warn "Failed to install some astal-menu deps"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure hyprshutdown is installed. hypr/usr/binds.lua's power binds
|
||||
# (Super+Shift/Ctrl/Ctrl+Shift+O) call it directly; without it those binds
|
||||
# silently no-op. It's in the official extra repo, not AUR.
|
||||
_ensure_hyprshutdown() {
|
||||
local de_dir; de_dir=$(_de_source_dir)
|
||||
[[ "${de_dir:-}" == */hyprlua ]] || return 0
|
||||
|
||||
if pacman -Qq hyprshutdown &>/dev/null; then
|
||||
ok "hyprshutdown already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "hyprshutdown is missing — the power binds in hypr/usr/binds.lua will no-op without it"
|
||||
if ask "Install it now?"; then
|
||||
if sudo pacman -S --noconfirm --needed hyprshutdown; then
|
||||
ok "hyprshutdown installed"
|
||||
else
|
||||
warn "Failed to install hyprshutdown"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
sync_configs() {
|
||||
section "Config Sync"
|
||||
|
||||
# ── Migration: old flat layout → hypr/usr/ ───────────────────────────────
|
||||
_migrate_hypr_usr
|
||||
_fix_hypr_usr
|
||||
_redeploy_usr_configs
|
||||
_deploy_greetd
|
||||
_ensure_plymouth_regreet
|
||||
_ensure_astal_menu_deps
|
||||
_ensure_hyprshutdown
|
||||
|
||||
# ── Preferred: use the installed update-configs.sh ───────────────────────
|
||||
local cfg_script
|
||||
|
|
|
|||
Loading…
Reference in New Issue