Compare commits
4 Commits
48505a5726
...
4c747221f2
| Author | SHA1 | Date |
|---|---|---|
|
|
4c747221f2 | |
|
|
48e32e7eea | |
|
|
de666acac7 | |
|
|
9d1ec69a11 |
|
|
@ -107,6 +107,14 @@ drawingarea {
|
||||||
.quad-action:active,
|
.quad-action:active,
|
||||||
button:checked.quad-action { background: @accent; color: @bg; border-color: @accent; }
|
button:checked.quad-action { background: @accent; color: @bg; border-color: @accent; }
|
||||||
|
|
||||||
|
/* Columns count stepper value (between the − / + pills) */
|
||||||
|
.stepper-value {
|
||||||
|
min-width: 34px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: @accent;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
}
|
||||||
|
|
||||||
/* A MenuButton (the settings cog) wraps an inner `button` node that would render a
|
/* A MenuButton (the settings cog) wraps an inner `button` node that would render a
|
||||||
* second ring inside the outer .quad-action pill — flatten it so only one border
|
* second ring inside the outer .quad-action pill — flatten it so only one border
|
||||||
* shows. */
|
* shows. */
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,8 @@ class QuadGrid(Gtk.Overlay):
|
||||||
self._takeover_reveal.set_valign(Gtk.Align.FILL)
|
self._takeover_reveal.set_valign(Gtk.Align.FILL)
|
||||||
self._takeover_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self._takeover_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self._takeover_holder.add_css_class("quad-expanded")
|
self._takeover_holder.add_css_class("quad-expanded")
|
||||||
self._takeover_reveal.set_child(bordered(self._takeover_holder, radius=16, fill_bg=True))
|
self._takeover_card = bordered(self._takeover_holder, radius=16, fill_bg=True)
|
||||||
|
self._takeover_reveal.set_child(self._takeover_card)
|
||||||
self.add_overlay(self._takeover_reveal)
|
self.add_overlay(self._takeover_reveal)
|
||||||
self._takeover_reveal.set_visible(False)
|
self._takeover_reveal.set_visible(False)
|
||||||
|
|
||||||
|
|
@ -80,8 +81,9 @@ class QuadGrid(Gtk.Overlay):
|
||||||
A Back row is prepended so the covered quads can be restored."""
|
A Back row is prepended so the covered quads can be restored."""
|
||||||
header = Gtk.CenterBox()
|
header = Gtk.CenterBox()
|
||||||
header.add_css_class("expanded-header")
|
header.add_css_class("expanded-header")
|
||||||
back = Gtk.Button(label=" Back")
|
back = Gtk.Button(label="") # nf-fa-compress (unfullscreen)
|
||||||
back.add_css_class("quad-action")
|
back.add_css_class("quad-action")
|
||||||
|
back.set_tooltip_text("Collapse")
|
||||||
back.connect("clicked", lambda *_: on_back())
|
back.connect("clicked", lambda *_: on_back())
|
||||||
header.set_start_widget(back)
|
header.set_start_widget(back)
|
||||||
self._takeover_holder.append(header)
|
self._takeover_holder.append(header)
|
||||||
|
|
@ -90,6 +92,9 @@ class QuadGrid(Gtk.Overlay):
|
||||||
|
|
||||||
def show_takeover(self) -> None:
|
def show_takeover(self) -> None:
|
||||||
self._takeover_reveal.set_visible(True)
|
self._takeover_reveal.set_visible(True)
|
||||||
|
self._takeover_card.add_css_class("quad-pop") # same bouncy scale as a quad
|
||||||
|
GLib.timeout_add(340, lambda: (self._takeover_card.remove_css_class("quad-pop"),
|
||||||
|
GLib.SOURCE_REMOVE)[1])
|
||||||
self._takeover_reveal.set_reveal_child(True)
|
self._takeover_reveal.set_reveal_child(True)
|
||||||
self.grid.add_css_class("dimmed")
|
self.grid.add_css_class("dimmed")
|
||||||
|
|
||||||
|
|
@ -156,8 +161,9 @@ class QuadGrid(Gtk.Overlay):
|
||||||
def _expanded_header(self, title: str) -> Gtk.Widget:
|
def _expanded_header(self, title: str) -> Gtk.Widget:
|
||||||
header = Gtk.CenterBox()
|
header = Gtk.CenterBox()
|
||||||
header.add_css_class("expanded-header")
|
header.add_css_class("expanded-header")
|
||||||
back = Gtk.Button(label=" Back") # nf arrow
|
back = Gtk.Button(label="") # nf-fa-compress (unfullscreen)
|
||||||
back.add_css_class("quad-action")
|
back.add_css_class("quad-action")
|
||||||
|
back.set_tooltip_text("Collapse")
|
||||||
back.connect("clicked", lambda *_: self.request_collapse())
|
back.connect("clicked", lambda *_: self.request_collapse())
|
||||||
header.set_start_widget(back)
|
header.set_start_widget(back)
|
||||||
lbl = Gtk.Label(label=title)
|
lbl = Gtk.Label(label=title)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from lib.proc import run_json, run_text
|
||||||
from paths import CACHE_DIR
|
from paths import CACHE_DIR
|
||||||
|
|
||||||
_LAYOUTS_MANIFEST = CACHE_DIR / "layouts.json"
|
_LAYOUTS_MANIFEST = CACHE_DIR / "layouts.json"
|
||||||
|
_COLUMNS_STATE = CACHE_DIR / "columns-state.json"
|
||||||
_LAYOUTS_STATE = CACHE_DIR / "layouts-state.json"
|
_LAYOUTS_STATE = CACHE_DIR / "layouts-state.json"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -284,11 +285,46 @@ class Taskbar(Gtk.Box):
|
||||||
row.append(sw)
|
row.append(sw)
|
||||||
page.append(row)
|
page.append(row)
|
||||||
self._fit_sws[ly["name"]] = sw
|
self._fit_sws[ly["name"]] = sw
|
||||||
if not ly.get("dirs") and not ly.get("fit_method"):
|
if ly.get("stepper"): # Columns: (-)[N](+)
|
||||||
|
page.append(self._cols_stepper())
|
||||||
|
if not ly.get("dirs") and not ly.get("fit_method") and not ly.get("stepper"):
|
||||||
page.append(Gtk.Label(label="No adjustable options", xalign=0.0,
|
page.append(Gtk.Label(label="No adjustable options", xalign=0.0,
|
||||||
css_classes=["net-ip"]))
|
css_classes=["net-ip"]))
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
# -- columns count stepper --------------------------------------------
|
||||||
|
def _cols_stepper(self) -> Gtk.Widget:
|
||||||
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
row.append(Gtk.Label(label="Columns", xalign=0.0, hexpand=True))
|
||||||
|
minus = Gtk.Button(label="−")
|
||||||
|
minus.add_css_class("quad-action")
|
||||||
|
self._cols_lbl = Gtk.Label(label=str(self._read_cols()))
|
||||||
|
self._cols_lbl.add_css_class("stepper-value")
|
||||||
|
plus = Gtk.Button(label="+")
|
||||||
|
plus.add_css_class("quad-action")
|
||||||
|
minus.connect("clicked", lambda *_a: self._step_cols(-1))
|
||||||
|
plus.connect("clicked", lambda *_a: self._step_cols(1))
|
||||||
|
row.append(minus)
|
||||||
|
row.append(self._cols_lbl)
|
||||||
|
row.append(plus)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def _read_cols(self) -> int:
|
||||||
|
try:
|
||||||
|
data = json.loads(_COLUMNS_STATE.read_text())
|
||||||
|
return int(data.get(str(self._active_ws), {}).get("cols", 2))
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, ValueError, TypeError):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def _step_cols(self, delta: int) -> None:
|
||||||
|
try:
|
||||||
|
cur = int(self._cols_lbl.get_label())
|
||||||
|
except ValueError:
|
||||||
|
cur = self._read_cols()
|
||||||
|
new = max(1, cur + delta)
|
||||||
|
self._cols_lbl.set_label(str(new))
|
||||||
|
_eval(f'hl.dispatch(hl.dsp.layout("cols {"+1" if delta > 0 else "-1"}"))')
|
||||||
|
|
||||||
# -- sync + handlers ---------------------------------------------------
|
# -- sync + handlers ---------------------------------------------------
|
||||||
def _sync_layout_controls(self) -> None:
|
def _sync_layout_controls(self) -> None:
|
||||||
# reflect the current per-ws layout / direction / focus_fit in the tabs+options
|
# reflect the current per-ws layout / direction / focus_fit in the tabs+options
|
||||||
|
|
|
||||||
|
|
@ -159,10 +159,13 @@ class MenuWindow(Gtk.ApplicationWindow):
|
||||||
|
|
||||||
# -- taskbar panel expansion ------------------------------------------
|
# -- taskbar panel expansion ------------------------------------------
|
||||||
def _on_taskbar_panel(self, expanded: bool) -> None:
|
def _on_taskbar_panel(self, expanded: bool) -> None:
|
||||||
# Cover the quads with the taskbar's workspace/window panel (the strip itself
|
# Cover the quads with the taskbar's workspace/window panel. Fully collapse the
|
||||||
# collapses inside the Taskbar). Collapse any expanded quad first.
|
# taskbar strip (header included) so the expanded panel reads as ONE module
|
||||||
|
# rather than a thin strip on top plus a detached card. Collapse any quad first.
|
||||||
if expanded and self.grid.is_expanded:
|
if expanded and self.grid.is_expanded:
|
||||||
self.grid.request_collapse()
|
self.grid.request_collapse()
|
||||||
|
self.taskbar_reveal.set_reveal_child(not expanded)
|
||||||
|
self.taskbar_reveal.set_visible(not expanded)
|
||||||
self.grid.show_takeover() if expanded else self.grid.hide_takeover()
|
self.grid.show_takeover() if expanded else self.grid.hide_takeover()
|
||||||
|
|
||||||
# -- appdrawer expansion ----------------------------------------------
|
# -- appdrawer expansion ----------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,41 @@
|
||||||
|
|
||||||
local state = {} -- [wsid] = { ncols, rows, orient, assign={addr=col}, seq={addr=n}, nextseq, pan={col=px} }
|
local state = {} -- [wsid] = { ncols, rows, orient, assign={addr=col}, seq={addr=n}, nextseq, pan={col=px} }
|
||||||
|
|
||||||
|
-- Publish per-workspace column/row counts so the astal-menu can show a live stepper.
|
||||||
|
local STATE_FILE = (os.getenv("HOME") or "") .. "/.cache/astal-menu/columns-state.json"
|
||||||
|
local function publish()
|
||||||
|
local parts = {}
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
pcall(function()
|
||||||
|
local f = io.open(STATE_FILE, "w")
|
||||||
|
if f then f:write("{" .. table.concat(parts, ",") .. "}\n"); f:close() end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function ws_state(wsid)
|
local function ws_state(wsid)
|
||||||
local s = state[wsid]
|
local s = state[wsid]
|
||||||
if not s then
|
if not s then
|
||||||
s = { ncols = 2, rows = 2, orient = "h", assign = {}, seq = {}, nextseq = 1, pan = {} }
|
s = { ncols = 2, rows = 2, orient = "h", assign = {}, seq = {}, nextseq = 1, pan = {} }
|
||||||
state[wsid] = s
|
state[wsid] = s
|
||||||
|
publish()
|
||||||
end
|
end
|
||||||
return s
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Reflow every window evenly across the current column count, by insertion order.
|
||||||
|
local function reflow(s, targets)
|
||||||
|
local addrs = {}
|
||||||
|
for _, tg in ipairs(targets) do
|
||||||
|
local ok, addr = pcall(function() return tg.window.address end)
|
||||||
|
if ok and addr then addrs[#addrs + 1] = addr end
|
||||||
|
end
|
||||||
|
table.sort(addrs, function(a, b) return (s.seq[a] or 0) < (s.seq[b] or 0) end)
|
||||||
|
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end
|
||||||
|
end
|
||||||
|
|
||||||
local function win_ws_id(win)
|
local function win_ws_id(win)
|
||||||
local ok, ws = pcall(function() return win.workspace end)
|
local ok, ws = pcall(function() return win.workspace end)
|
||||||
if ok and ws ~= nil then
|
if ok and ws ~= nil then
|
||||||
|
|
@ -185,18 +211,18 @@ local function layout_msg(ctx, cmd)
|
||||||
elseif arg == less_cols then s.ncols = math.max(1, s.ncols - 1); changed_cols = true
|
elseif arg == less_cols then s.ncols = math.max(1, s.ncols - 1); changed_cols = true
|
||||||
elseif arg == more_rows then s.rows = s.rows + 1
|
elseif arg == more_rows then s.rows = s.rows + 1
|
||||||
elseif arg == less_rows then s.rows = math.max(1, s.rows - 1) end
|
elseif arg == less_rows then s.rows = math.max(1, s.rows - 1) end
|
||||||
if changed_cols then
|
if changed_cols then reflow(s, t) end
|
||||||
-- reflow all windows evenly across the new column count (by insertion order)
|
publish()
|
||||||
local addrs = {}
|
elseif verb == "cols" then
|
||||||
for _, tg in ipairs(t) do
|
-- direct column-count change (the menu's (-)[N](+) stepper).
|
||||||
local ok, addr = pcall(function() return tg.window.address end)
|
if arg == "+" or arg == "+1" then s.ncols = s.ncols + 1
|
||||||
if ok and addr then addrs[#addrs + 1] = addr end
|
elseif arg == "-" or arg == "-1" then s.ncols = math.max(1, s.ncols - 1)
|
||||||
end
|
else local n = tonumber(arg); if n then s.ncols = math.max(1, math.floor(n)) end end
|
||||||
table.sort(addrs, function(a, b) return (s.seq[a] or 0) < (s.seq[b] or 0) end)
|
reflow(s, t)
|
||||||
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end
|
publish()
|
||||||
end
|
|
||||||
elseif verb == "orient" then
|
elseif verb == "orient" then
|
||||||
s.orient = (arg == "v") and "v" or "h"
|
s.orient = (arg == "v") and "v" or "h"
|
||||||
|
publish()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -216,4 +242,5 @@ return {
|
||||||
dir_labels = { "Horizontal", "Vertical" },
|
dir_labels = { "Horizontal", "Vertical" },
|
||||||
default_dir = "h",
|
default_dir = "h",
|
||||||
fit_method = false,
|
fit_method = false,
|
||||||
|
stepper = true, -- menu shows a "Columns: (-)[N](+)" count stepper
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ function M.write_manifest()
|
||||||
',"dirs":[', table.concat(dirs, ","), "]",
|
',"dirs":[', table.concat(dirs, ","), "]",
|
||||||
',"dir_labels":[', table.concat(dlabels, ","), "]",
|
',"dir_labels":[', table.concat(dlabels, ","), "]",
|
||||||
',"default_dir":', jstr(s.default_dir or ""),
|
',"default_dir":', jstr(s.default_dir or ""),
|
||||||
',"fit_method":', tostring(s.fit_method or false), "}",
|
',"fit_method":', tostring(s.fit_method or false),
|
||||||
|
',"stepper":', tostring(s.stepper or false), "}",
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
os.execute("mkdir -p '" .. MANIFEST:match("(.+)/[^/]+$") .. "' 2>/dev/null")
|
os.execute("mkdir -p '" .. MANIFEST:match("(.+)/[^/]+$") .. "' 2>/dev/null")
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@
|
||||||
-- * focus_fit_method 0/1 ("Centered follow") — see M.set_fit(); when centered the
|
-- * focus_fit_method 0/1 ("Centered follow") — see M.set_fit(); when centered the
|
||||||
-- focused column is centred and the previous/next columns peek in from the sides.
|
-- focused column is centred and the previous/next columns peek in from the sides.
|
||||||
--
|
--
|
||||||
-- column_width is kept < 1.0 (0.5) so the active column is narrower than the viewport
|
-- column_width is the default window size along the scroll axis, as a fraction of the
|
||||||
-- and the prev/next columns stay on-screen and clickable. fullscreen_on_one_column
|
-- monitor (0.76 = 76% of the monitor's width for a horizontal tape, or its height for a
|
||||||
-- lets a lone window still fill the screen, so the narrow width costs nothing alone.
|
-- vertical one — the layout applies it to whichever axis `direction` scrolls). Keeping it
|
||||||
|
-- < 1.0 leaves the prev/next windows peeking in on-screen and clickable;
|
||||||
|
-- fullscreen_on_one_column lets a lone window still fill the screen, so it costs nothing
|
||||||
|
-- when a window is alone.
|
||||||
return {
|
return {
|
||||||
name = "scrolling",
|
name = "scrolling",
|
||||||
label = "Scrolling",
|
label = "Scrolling",
|
||||||
|
|
@ -18,7 +21,7 @@ return {
|
||||||
config = {
|
config = {
|
||||||
scrolling = {
|
scrolling = {
|
||||||
direction = "down",
|
direction = "down",
|
||||||
column_width = 0.5,
|
column_width = 0.76, -- 76% of the monitor along the scroll axis
|
||||||
focus_fit_method = 1,
|
focus_fit_method = 1,
|
||||||
follow_focus = true,
|
follow_focus = true,
|
||||||
fullscreen_on_one_column = true,
|
fullscreen_on_one_column = true,
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,12 @@ hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.exec_cmd("hyprctl kill"))
|
||||||
|
|
||||||
hl.bind(mainMod .. " + CTRL + M", hl.dsp.exec_cmd("~/.config/scripts/toggle-layout.sh"))
|
hl.bind(mainMod .. " + CTRL + M", hl.dsp.exec_cmd("~/.config/scripts/toggle-layout.sh"))
|
||||||
|
|
||||||
-- lock/exit
|
-- lock/exit — hyprshutdown gracefully closes every app (with a UI) before it exits
|
||||||
|
-- Hyprland and runs its --post-cmd, instead of yanking the session out from under them.
|
||||||
hl.bind(mainMod .. " + O", hl.dsp.exec_cmd("hyprlock"))
|
hl.bind(mainMod .. " + O", hl.dsp.exec_cmd("hyprlock"))
|
||||||
hl.bind(mainMod .. " + SHIFT + O", hl.dsp.exec_cmd("hyprctl dispatch exit"))
|
hl.bind(mainMod .. " + SHIFT + O", hl.dsp.exec_cmd("hyprshutdown")) -- graceful logout
|
||||||
hl.bind(mainMod .. " + CTRL + O", hl.dsp.exec_cmd("systemctl poweroff"))
|
hl.bind(mainMod .. " + CTRL + O", hl.dsp.exec_cmd('hyprshutdown -p "systemctl poweroff"')) -- graceful power off
|
||||||
|
hl.bind(mainMod .. " + CTRL + SHIFT + O",hl.dsp.exec_cmd('hyprshutdown -p "systemctl reboot"')) -- graceful reboot
|
||||||
hl.bind(mainMod .. " + ALT + O", hl.dsp.exec_cmd("~/.config/scripts/pwr-dmenu.sh"))
|
hl.bind(mainMod .. " + ALT + O", hl.dsp.exec_cmd("~/.config/scripts/pwr-dmenu.sh"))
|
||||||
hl.bind(mainMod .. " + ALT + CTRL + SHIFT + END", hl.dsp.exit())
|
hl.bind(mainMod .. " + ALT + CTRL + SHIFT + END", hl.dsp.exit())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,8 +347,20 @@ class App:
|
||||||
def _init_colors(self):
|
def _init_colors(self):
|
||||||
curses.start_color()
|
curses.start_color()
|
||||||
curses.use_default_colors()
|
curses.use_default_colors()
|
||||||
# 1 = selected (cyan bold)
|
# Match the rest of the config (CyberQueer): accent red #E40046 for the
|
||||||
curses.init_pair(1, curses.COLOR_CYAN, -1)
|
# selection, electric violet #5018dd for headers/help. Define them as true
|
||||||
|
# RGB when the terminal allows (kitty does); otherwise fall back to the
|
||||||
|
# nearest ANSI colour so the tool still works on a basic console.
|
||||||
|
accent, violet = curses.COLOR_CYAN, curses.COLOR_GREEN
|
||||||
|
if curses.COLORS >= 256 and curses.can_change_color():
|
||||||
|
try:
|
||||||
|
curses.init_color(16, 894, 0, 275) # #E40046
|
||||||
|
curses.init_color(17, 314, 94, 867) # #5018dd
|
||||||
|
accent, violet = 16, 17
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
# 1 = selected (accent red, bold)
|
||||||
|
curses.init_pair(1, accent, -1)
|
||||||
# 2 = normal (white)
|
# 2 = normal (white)
|
||||||
curses.init_pair(2, curses.COLOR_WHITE, -1)
|
curses.init_pair(2, curses.COLOR_WHITE, -1)
|
||||||
# 3 = mirror target (yellow)
|
# 3 = mirror target (yellow)
|
||||||
|
|
@ -357,8 +369,8 @@ class App:
|
||||||
curses.init_pair(4, curses.COLOR_BLACK + 8 if curses.COLORS >= 16 else curses.COLOR_WHITE, -1)
|
curses.init_pair(4, curses.COLOR_BLACK + 8 if curses.COLORS >= 16 else curses.COLOR_WHITE, -1)
|
||||||
# 5 = status bar (reversed)
|
# 5 = status bar (reversed)
|
||||||
curses.init_pair(5, -1, -1)
|
curses.init_pair(5, -1, -1)
|
||||||
# 6 = help (green)
|
# 6 = help / headers (violet)
|
||||||
curses.init_pair(6, curses.COLOR_GREEN, -1)
|
curses.init_pair(6, violet, -1)
|
||||||
|
|
||||||
def _get_scale(self, pane_cols: int, pane_rows: int) -> float:
|
def _get_scale(self, pane_cols: int, pane_rows: int) -> float:
|
||||||
"""Return cached scale; recompute only on resize or when a monitor escapes the viewport."""
|
"""Return cached scale; recompute only on resize or when a monitor escapes the viewport."""
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ hide() { printf "${CSI}?25l"; }
|
||||||
show() { printf "${CSI}?25h"; }
|
show() { printf "${CSI}?25h"; }
|
||||||
|
|
||||||
b="${CSI}1m" ; d="${CSI}2m" ; r="${CSI}0m"
|
b="${CSI}1m" ; d="${CSI}2m" ; r="${CSI}0m"
|
||||||
cy="${CSI}96m"; gn="${CSI}92m"
|
# CyberQueer accents to match the rest of the config: red #E40046, violet #5018dd
|
||||||
|
# (24-bit truecolor; kitty renders these exactly).
|
||||||
|
cy="${CSI}38;2;228;0;70m" ; gn="${CSI}38;2;80;24;221m"
|
||||||
yl="${CSI}93m"; wh="${CSI}97m"; gy="${CSI}90m"
|
yl="${CSI}93m"; wh="${CSI}97m"; gy="${CSI}90m"
|
||||||
rv="${CSI}7m"
|
rv="${CSI}7m"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
#
|
#
|
||||||
# greetd runs this as the unprivileged `greeter` user. cage is a minimal
|
# greetd runs this as the unprivileged `greeter` user. cage is a minimal
|
||||||
# wlroots/Wayland compositor; `-s` allows VT switching (so Ctrl+Alt+F-keys still
|
# wlroots/Wayland compositor; `-s` allows VT switching (so Ctrl+Alt+F-keys still
|
||||||
# reach a console). Env here tunes the greeter's look before ReGreet starts.
|
# reach a console). Env here tunes the greeter's look/behaviour before ReGreet starts.
|
||||||
#
|
#
|
||||||
# Scaling: cage renders at output scale 1. Bump XCURSOR_SIZE for a larger cursor;
|
# Scaling: cage renders at output scale 1. Bump XCURSOR_SIZE for a larger cursor;
|
||||||
# uncomment GDK_SCALE to integer-scale the whole greeter (careful on mixed-DPI
|
# uncomment GDK_SCALE to integer-scale the whole greeter (careful on mixed-DPI
|
||||||
|
|
@ -13,4 +13,22 @@
|
||||||
export XCURSOR_SIZE=32
|
export XCURSOR_SIZE=32
|
||||||
# export GDK_SCALE=2
|
# export GDK_SCALE=2
|
||||||
|
|
||||||
exec cage -s -- regreet
|
# ── Multi-GPU: pin the greeter to a DRM card that actually drives a monitor ─────
|
||||||
|
# On a dual-GPU box (dGPU + iGPU) wlroots enumerates both cards and may pick the
|
||||||
|
# display-less one, giving a blank or input-frozen greeter. Hand it only the first
|
||||||
|
# card that has a connected output. Evaluated live, so it stays portable across
|
||||||
|
# boots and machines (no hard-coded PCI path).
|
||||||
|
for _status in /sys/class/drm/card[0-9]*-*/status; do
|
||||||
|
[ "$(cat "$_status" 2>/dev/null)" = "connected" ] || continue
|
||||||
|
_card="/dev/dri/$(basename "$(dirname "$_status")" | cut -d- -f1)"
|
||||||
|
[ -e "$_card" ] && { WLR_DRM_DEVICES="$_card"; export WLR_DRM_DEVICES; break; }
|
||||||
|
done
|
||||||
|
|
||||||
|
# amdgpu hardware cursors can freeze on this hardware (the cursor sticks and input
|
||||||
|
# feels dead); software cursors are reliable.
|
||||||
|
export WLR_NO_HARDWARE_CURSORS=1
|
||||||
|
|
||||||
|
# cage's default is "-m extend", which stretches one canvas across every monitor and
|
||||||
|
# leaves ReGreet's login card floating in the middle of the whole multi-monitor span.
|
||||||
|
# "-m last" renders on a single output instead, so the card is centred on one screen.
|
||||||
|
exec cage -s -m last -- regreet
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ HYPRLUA_PACKAGES=(
|
||||||
|
|
||||||
hyprsunset # blue-light filter / night-mode daemon
|
hyprsunset # blue-light filter / night-mode daemon
|
||||||
hypridle # idle daemon triggering lock/suspend after inactivity
|
hypridle # idle daemon triggering lock/suspend after inactivity
|
||||||
|
hyprshutdown # hyprtoolkit graphical power/shutdown dialog (Super+Alt+O)
|
||||||
ksshaskpass # Qt SSH passphrase dialog registered as ssh-askpass
|
ksshaskpass # Qt SSH passphrase dialog registered as ssh-askpass
|
||||||
|
|
||||||
nm-connection-editor # GTK editor for NetworkManager connection profiles
|
nm-connection-editor # GTK editor for NetworkManager connection profiles
|
||||||
|
|
|
||||||
30
sysupdate.sh
30
sysupdate.sh
|
|
@ -96,7 +96,17 @@ spin_stop() {
|
||||||
printf '\r%*s\r' "$(_cols)" ''
|
printf '\r%*s\r' "$(_cols)" ''
|
||||||
}
|
}
|
||||||
|
|
||||||
trap 'spin_stop; tput cnorm 2>/dev/null || true' EXIT
|
_SUDO_KEEPALIVE=""
|
||||||
|
trap 'spin_stop; [[ -n "${_SUDO_KEEPALIVE:-}" ]] && kill "$_SUDO_KEEPALIVE" 2>/dev/null; tput cnorm 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
# Prime sudo ONCE at startup and keep the credential fresh for the whole run, so no
|
||||||
|
# later step (config deploy, pacman -S per package, state-file write) re-prompts.
|
||||||
|
sudo_prime() {
|
||||||
|
printf " ${Y}?${RS} ${BO}Elevated access${RS} — enter your password once for this run.\n"
|
||||||
|
sudo -v || { err "sudo is required."; exit 1; }
|
||||||
|
( while true; do sudo -n true 2>/dev/null; sleep 50; kill -0 "$$" 2>/dev/null || exit; done ) &
|
||||||
|
_SUDO_KEEPALIVE=$!
|
||||||
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
# STATE FILE
|
# STATE FILE
|
||||||
|
|
@ -587,12 +597,29 @@ _fix_hypr_usr() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# setup module — a bad PAM file could lock out login.
|
||||||
|
_deploy_greetd() {
|
||||||
|
local src="$DOTFILES/etc-greetd"
|
||||||
|
[[ -d "$src" ]] || return 0
|
||||||
|
systemctl is-enabled greetd.service &>/dev/null || return 0
|
||||||
|
log "Deploying greetd / ReGreet config to ${BO}/etc/greetd${RS}..."
|
||||||
|
sudo install -Dm644 "$src/config.toml" /etc/greetd/config.toml && \
|
||||||
|
sudo install -Dm644 "$src/regreet.toml" /etc/greetd/regreet.toml && \
|
||||||
|
sudo install -Dm644 "$src/regreet.css" /etc/greetd/regreet.css && \
|
||||||
|
sudo install -Dm755 "$src/regreet-session.sh" /etc/greetd/regreet-session.sh && \
|
||||||
|
ok "greetd config deployed" || warn "greetd config deploy had errors"
|
||||||
|
}
|
||||||
|
|
||||||
sync_configs() {
|
sync_configs() {
|
||||||
section "Config Sync"
|
section "Config Sync"
|
||||||
|
|
||||||
# ── Migration: old flat layout → hypr/usr/ ───────────────────────────────
|
# ── Migration: old flat layout → hypr/usr/ ───────────────────────────────
|
||||||
_migrate_hypr_usr
|
_migrate_hypr_usr
|
||||||
_fix_hypr_usr
|
_fix_hypr_usr
|
||||||
|
_deploy_greetd
|
||||||
|
|
||||||
# ── Preferred: use the installed update-configs.sh ───────────────────────
|
# ── Preferred: use the installed update-configs.sh ───────────────────────
|
||||||
local cfg_script
|
local cfg_script
|
||||||
|
|
@ -694,6 +721,7 @@ declare -a FLATPAK_PKGS=()
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
header
|
header
|
||||||
|
sudo_prime # single password prompt up front; kept alive for the whole run
|
||||||
|
|
||||||
# ── Configs-only mode ────────────────────────────────────────────────────
|
# ── Configs-only mode ────────────────────────────────────────────────────
|
||||||
if [[ "$UPDATE_MODE" == "configs" ]]; then
|
if [[ "$UPDATE_MODE" == "configs" ]]; then
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue