diff --git a/config/README.md b/config/README.md
new file mode 100644
index 0000000..572dee4
--- /dev/null
+++ b/config/README.md
@@ -0,0 +1,16 @@
+# `config`
+
+My tool and machine configuration ranging from `~/.config` symlinks to libraries which support my daily workflows.
+
+The [`setup`](../setup) and [`scwrypts`](../scwrypts) handle the setup / maintenance details, but contained herein:
+
+
+| Submodule | Description |
+| ------------------------------------ | -------------------------------------------------------------------- |
+| [`bin`](./bin) | goes on my `${PATH}`; used in limited cases |
+| [`colorschemes`](./colorschemes) | my colorscheme YAML definitions which style all my desktop apps; uses the [spec established in `dotwryn/scwrypts`](../scwrypts/dotwryn/desktop/colorscheme/coloscheme.module.zsh) |
+| [`desktop`](./desktop) | utilities which configure, support, or launch a desktop environment |
+| [`etc`](./etc) | config files linked back to `/etc` on the local system |
+| [`host-overrides`](./host-overrides) | hostname-based values overrides for `etc`, `user`, and `bin` |
+| [`user`](./user) | config files linked back to `${XDG_CONFIG_HOME:-${HOME}/.config` |
+
diff --git a/config/bin/README.md b/config/bin/README.md
new file mode 100644
index 0000000..2ba2aba
--- /dev/null
+++ b/config/bin/README.md
@@ -0,0 +1,48 @@
+# config : bin
+
+This directory contains shell one-offs which solve focused problems.
+Generally, `scwrypts` are a better source for long-term maintenance, but, for a _few selected scenarios_, a raw shell script is preferred.
+This is typically due to:
+
+1. The simplicity of the script outweighs the maintenance overhead for an equivalent `scwrypt`
+2. The overhead of `scwrypts` causes excessive slowdowns for quick, simple tasks
+
+## Reference / Usage
+
+All scripts in this directory support and can be used in one of two ways:
+
+1. Direct reference through `${DOTWRYN}/config/bin/${script_name}`
+2. Symlinked on the user's `${PATH}`
+
+### Direct Reference
+
+Anytime a raw script is referenced throughout the `dotwryn` repository, it should be referenced with _direct reference_.
+`dotwryn`-internal operations must not assume the user has a symlink to any of these functions.
+
+### Symlink
+
+An API for human users!
+All the scripts in this directory allow for symlinking.
+
+```zsh
+# you can decide where it goes
+ln -s "${DOTWRYN}/config/bin/${script_name}" "${somewhere_on_users_path}/${local_script_name}"
+
+# since ~/.local/bin is on MY ${PATH} (and I want it to have the same name)
+cd ~/.local/bin
+ln -s "${DOTWRYN}/config/bin/${script_name}"
+```
+
+
diff --git a/config/desktop/hyprland/config.lua b/config/desktop/hyprland/config.lua
new file mode 100644
index 0000000..3996874
--- /dev/null
+++ b/config/desktop/hyprland/config.lua
@@ -0,0 +1,41 @@
+--
+-- dotwryn hyprland configuration library
+--
+
+local DOTWRYN = os.getenv("DOTWRYN")
+local dotwryn = {
+ DOTWRYN = DOTWRYN,
+
+ colorscheme = dofile(DOTWRYN .. "/scwrypts/dotwryn/desktop/colorscheme/colorscheme.lua").load(),
+ scwrypts = dofile(DOTWRYN .. "/config/desktop/hyprland/scwrypts.lua"),
+
+ font = {
+ family = table.concat({
+ "Pokémoji Color",
+ "Monaspace Argon",
+ }, ", "),
+
+ size = 11,
+ },
+}
+
+---------------------------------------------------------------------
+
+local config_filepaths = {}
+
+local config_dir = (DOTWRYN .. "/config/desktop/hyprland/config.lua.d")
+local config_dir_quoted = config_dir:gsub("'", "'\\''")
+
+assert(io.open(config_dir, "r"), "error reading hyprland config.lua.d : " .. config_dir)
+
+local find = io.popen("find '" .. config_dir_quoted .. "' -mindepth 1 -maxdepth 1 -type f -name '*.lua'")
+for config_filepath in find:lines() do
+ table.insert(config_filepaths, config_filepath)
+end
+find:close()
+
+table.sort(config_filepaths)
+
+for _, config_filepath in ipairs(config_filepaths) do
+ assert(loadfile(config_filepath))(dotwryn)
+end
diff --git a/config/desktop/hyprland/config.lua.d/00-hyprland.lua b/config/desktop/hyprland/config.lua.d/00-hyprland.lua
new file mode 100644
index 0000000..fd1229a
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/00-hyprland.lua
@@ -0,0 +1,37 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- hyprland-base configuration
+--
+
+hl.config({
+ ecosystem = {
+ no_update_news = true,
+ no_donation_nag = true,
+ },
+
+ input = {
+ kb_layout = "us",
+ },
+
+ misc = {
+ font_family = dotwryn.font.family,
+ force_default_wallpaper = 0,
+ disable_hyprland_logo = true,
+ disable_splash_rendering = true,
+ },
+})
+
+hl.monitor({
+ output = "",
+ mode = "preferred",
+ position = "auto",
+ scale = "auto",
+})
+
+------------------------------------------
+
+hl.bind("SUPER + SHIFT + e", hl.dsp.exec_cmd("qs ipc call logout show"))
+hl.bind("CTRL + ALT + Delete", hl.dsp.exit())
+hl.bind("SUPER + SHIFT + r", hl.dsp.exec_cmd("hyprctl reload"))
+hl.bind("SUPER + SHIFT + c", hl.dsp.exec_cmd("hyprctl reload"))
diff --git a/config/desktop/hyprland/config.lua.d/01-hyprland-high-dpi-scaling.lua b/config/desktop/hyprland/config.lua.d/01-hyprland-high-dpi-scaling.lua
new file mode 100644
index 0000000..27cc798
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/01-hyprland-high-dpi-scaling.lua
@@ -0,0 +1,25 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- high-dpi scaling : XWayland apps render at 1x and scale themselves
+--
+
+hl.config({
+ xwayland = {
+ force_zero_scaling = true,
+ },
+})
+
+-- force_zero_scaling leaves steam at 1x; match the active monitor's scale instead
+local set_steam_scaling = function()
+ local monitor = hl.get_active_monitor()
+ if monitor == nil or monitor.scale == nil or monitor.scale <= 0 then return end
+
+ hl.env("STEAM_FORCE_DESKTOPUI_SCALING", tostring(monitor.scale))
+end
+
+set_steam_scaling()
+
+for _, event in ipairs({ "monitor.added", "monitor.removed", "monitor.layout_changed" }) do
+ hl.on(event, set_steam_scaling)
+end
diff --git a/config/desktop/hyprland/config.lua.d/09-hyprland-animations.lua b/config/desktop/hyprland/config.lua.d/09-hyprland-animations.lua
new file mode 100644
index 0000000..73ee702
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/09-hyprland-animations.lua
@@ -0,0 +1,12 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+
+hl.config({ animations = { enabled = true } })
+
+hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } })
+
+hl.animation({ leaf = "global", enabled = false })
+hl.animation({ leaf = "windows", enabled = true, speed = 3, bezier = "easeOutQuint", style = "slide" })
+hl.animation({ leaf = "layers", enabled = true, speed = 3, bezier = "easeOutQuint", style = "popin" })
+hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "easeOutQuint", style = "slide" })
+hl.animation({ leaf = "border", enabled = true, speed = 3, bezier = "easeOutQuint" })
diff --git a/config/desktop/hyprland/config.lua.d/10-dwindle.lua b/config/desktop/hyprland/config.lua.d/10-dwindle.lua
new file mode 100644
index 0000000..ea36c13
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/10-dwindle.lua
@@ -0,0 +1,79 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- as a long-time i3wm user, my dwindle configuration contains tries to
+-- make the transition feel smooth
+--
+--
+--
+
+local colors = {
+ active = {
+ border = dotwryn.colorscheme.get_hex(".dwindle.colors.active.border", ".i3.focused.border", ".material.secondary.500", ".ansi.green.bright"),
+ text = dotwryn.colorscheme.get_hex(".dwindle.colors.active.text", ".i3.focused.text", ".material.foreground.secondary.500", ".foreground"),
+ locked = dotwryn.colorscheme.get_hex(".dwindle.colors.active.locked", ".i3.urgent.border", ".material.error.500", ".ansi.red.bright"),
+ },
+
+ inactive = {
+ border = dotwryn.colorscheme.get_hex(".dwindle.colors.inactive.border", ".i3.unfocused.border", ".material.primary.700", ".ansi.yellow.bright"),
+ text = dotwryn.colorscheme.get_hex(".dwindle.colors.inactive.text", ".i3.unfocused.text", ".material.foreground.primary.700", ".foreground"),
+ locked = dotwryn.colorscheme.get_hex(".dwindle.colors.inactive.locked", ".i3.inactive.border", ".material.primary.700", ".ansi.yellow.bright"),
+ },
+}
+
+hl.config({
+ general = {
+ layout = "dwindle",
+
+ border_size = 4,
+ gaps_in = 0,
+ gaps_out = 0,
+
+ resize_on_border = false,
+
+ col = {
+ active_border = colors.active.border,
+ inactive_border = colors.inactive.border,
+ },
+ },
+
+ dwindle = { preserve_split = true },
+ input = { follow_mouse = 1 },
+ binds = { movefocus_cycles_groupfirst = true },
+
+ decoration = {
+ rounding = 0,
+ shadow = { enabled = false },
+ blur = { enabled = false },
+ },
+
+ -- currently treating dwindle "groups" like an i3 "tabbed"
+ group = {
+ col = {
+ border_active = colors.active.border,
+ border_inactive = colors.inactive.border,
+ border_locked_active = colors.active.locked,
+ border_locked_inactive = colors.inactive.locked,
+ },
+
+ groupbar = {
+ font_family = dotwryn.font.family,
+ font_size = dotwryn.font.size,
+ height = 22,
+ gradients = true,
+ rounding = 0,
+ gradient_rounding = 0,
+ render_titles = true,
+
+ text_color = colors.active.text,
+ text_color_inactive = colors.inactive.text,
+
+ col = {
+ active = colors.active.border,
+ inactive = colors.inactive.border,
+ locked_active = colors.active.locked,
+ locked_inactive = colors.inactive.locked,
+ },
+ },
+ },
+})
diff --git a/config/desktop/hyprland/config.lua.d/11-dwindle-bindings.lua b/config/desktop/hyprland/config.lua.d/11-dwindle-bindings.lua
new file mode 100644
index 0000000..e98a1de
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/11-dwindle-bindings.lua
@@ -0,0 +1,44 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- i3wm & vim-style window navigation
+--
+
+-- moves a window through groups to recreate i3wm tab behavior
+local move_window = function(direction)
+ return function()
+ local window = hl.get_active_window()
+ local group = window ~= nil and window.group or nil
+
+ if group ~= nil and group.size > 1 and (direction == "left" or direction == "right") then
+ local at_edge = (direction == "left" and group.current_index <= 1)
+ or (direction == "right" and group.current_index >= group.size)
+ if not at_edge then
+ hl.dispatch(hl.dsp.group.move_window({ forward = direction == "right" }))
+ return
+ end
+ end
+
+ hl.dispatch(hl.dsp.window.move({ direction = direction, group_aware = true }))
+ end
+end
+
+------------------------------------------
+
+for key, direction in pairs({ h = "left", j = "down", k = "up", l = "right" }) do
+ hl.bind("SUPER + " .. key, hl.dsp.focus({ direction = direction }))
+ hl.bind("SUPER + SHIFT + " .. key, move_window(direction))
+end
+
+hl.bind("SUPER + f", hl.dsp.window.fullscreen({ mode = "fullscreen" }))
+
+hl.bind("SUPER + q", hl.dsp.window.close())
+
+-- i3 'layout toggle split'
+hl.bind("SUPER + e", hl.dsp.layout("togglesplit"))
+
+-- i3 'layout tabbed' / 'layout stacking' : both toggle a group
+hl.bind("SUPER + w", hl.dsp.group.toggle())
+hl.bind("SUPER + s", hl.dsp.group.toggle())
+
+-- (no equivalent) bindsym $mod+BackSpace open
diff --git a/config/desktop/hyprland/config.lua.d/12-dwindle-resize-mode.lua b/config/desktop/hyprland/config.lua.d/12-dwindle-resize-mode.lua
new file mode 100644
index 0000000..d4edaf1
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/12-dwindle-resize-mode.lua
@@ -0,0 +1,28 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- i3wm-style resize mode
+--
+
+local resize = function(x, y)
+ return function()
+ local monitor = hl.get_active_monitor()
+ local scale = (monitor ~= nil and monitor.scale ~= nil and monitor.scale > 0) and monitor.scale or 1
+ local step_x = monitor ~= nil and math.floor(monitor.width / scale * 0.1) or 10
+ local step_y = monitor ~= nil and math.floor(monitor.height / scale * 0.1) or 10
+ hl.dispatch(hl.dsp.window.resize({ x = x * step_x, y = y * step_y, relative = true }))
+ end
+end
+
+hl.bind("SUPER + r", hl.dsp.submap("resize"))
+
+hl.define_submap("resize", function()
+ hl.bind("h", resize( 1, 0), { repeating = true })
+ hl.bind("j", resize( 0, -1), { repeating = true })
+ hl.bind("k", resize( 0, 1), { repeating = true })
+ hl.bind("l", resize(-1, 0), { repeating = true })
+
+ hl.bind("Return", hl.dsp.submap("reset"))
+ hl.bind("Escape", hl.dsp.submap("reset"))
+ hl.bind("SUPER + r", hl.dsp.submap("reset"))
+end)
diff --git a/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua b/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua
new file mode 100644
index 0000000..f897ff6
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/15-dwindle-i3-scratchpad.lua
@@ -0,0 +1,128 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- i3wm-style floating windows and scratchpad
+--
+
+hl.bind("SUPER + SHIFT + space", hl.dsp.window.float({ action = "toggle" }))
+
+hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true })
+hl.bind("SUPER + mouse:273", hl.dsp.window.resize(), { mouse = true })
+
+local SCRATCHPAD = "special:scratchpad"
+
+local SCRATCHPAD_FILTERS = {
+ terminal = { "^Alacritty$", "^kitty$" },
+ games = { "^steam$", "^dolphin%-emu$", "^Visualboyadvance%-m$", "^steam_app_" },
+}
+
+local matches_filter = function(class, filter)
+ if filter == nil then
+ for name, _ in pairs(SCRATCHPAD_FILTERS) do
+ if matches_filter(class, name) then return false end
+ end
+ return true
+ end
+ for _, pattern in ipairs(SCRATCHPAD_FILTERS[filter]) do
+ if class ~= nil and class:match(pattern) then return true end
+ end
+ return false
+end
+
+local workspace_selector = function(workspace)
+ return workspace.id > 0 and tostring(workspace.id) or ("name:" .. workspace.name)
+end
+
+local scratchpad_shown = {}
+local scratchpad_queue = {}
+
+local scratchpad_enqueue = function(address)
+ for index, queued in ipairs(scratchpad_queue) do
+ if queued == address then table.remove(scratchpad_queue, index) break end
+ end
+ table.insert(scratchpad_queue, address)
+end
+
+local scratchpad_windows = function()
+ local stashed = {}
+ for _, window in ipairs(hl.get_workspace_windows(SCRATCHPAD)) do stashed[window.address] = window end
+
+ local ordered = {}
+ for _, address in ipairs(scratchpad_queue) do
+ if stashed[address] ~= nil then
+ table.insert(ordered, stashed[address])
+ stashed[address] = nil
+ end
+ end
+ for address, window in pairs(stashed) do
+ table.insert(ordered, window)
+ scratchpad_enqueue(address)
+ end
+ return ordered
+end
+
+local scratchpad_fit = function(selector)
+ local monitor = hl.get_active_monitor()
+ if monitor ~= nil then
+ local scale = (monitor.scale ~= nil and monitor.scale > 0) and monitor.scale or 1
+ hl.dispatch(hl.dsp.window.resize({
+ window = selector,
+ x = math.floor(monitor.width / scale * 0.75),
+ y = math.floor(monitor.height / scale * 0.75),
+ }))
+ end
+ hl.dispatch(hl.dsp.window.center({ window = selector }))
+end
+
+hl.bind("SUPER + SHIFT + Tab", function()
+ local window = hl.get_active_window()
+ if window == nil then return end
+
+ local selector = "address:" .. window.address
+ hl.dispatch(hl.dsp.window.float({ window = selector, action = "enable" }))
+ scratchpad_fit(selector)
+ hl.dispatch(hl.dsp.window.move({ window = selector, workspace = SCRATCHPAD, follow = false }))
+ scratchpad_shown[window.address] = nil
+ scratchpad_enqueue(window.address)
+end)
+
+local scratchpad_show = function(filter)
+ return function()
+ local workspace = hl.get_active_workspace()
+ if workspace == nil then return end
+
+ local active = hl.get_active_window()
+ local candidates = {}
+ if filter == nil and active ~= nil and scratchpad_shown[active.address] then
+ table.insert(candidates, active.address)
+ end
+ for address, _ in pairs(scratchpad_shown) do table.insert(candidates, address) end
+
+ for _, address in ipairs(candidates) do
+ local window = hl.get_window("address:" .. address)
+ if window == nil or window.workspace == nil or window.workspace.name == SCRATCHPAD then
+ scratchpad_shown[address] = nil
+ elseif window.workspace.id == workspace.id and (filter == nil or matches_filter(window.class, filter)) then
+ hl.dispatch(hl.dsp.window.move({ window = "address:" .. address, workspace = SCRATCHPAD, follow = false }))
+ scratchpad_shown[address] = nil
+ scratchpad_enqueue(address)
+ return
+ end
+ end
+
+ for _, window in ipairs(scratchpad_windows()) do
+ if matches_filter(window.class, filter) then
+ local selector = "address:" .. window.address
+ hl.dispatch(hl.dsp.window.move({ window = selector, workspace = workspace_selector(workspace), follow = false }))
+ scratchpad_fit(selector)
+ hl.dispatch(hl.dsp.focus({ window = selector }))
+ scratchpad_shown[window.address] = true
+ return
+ end
+ end
+ end
+end
+
+hl.bind("SUPER + Tab", scratchpad_show(nil))
+hl.bind("SUPER + x", scratchpad_show("terminal"))
+hl.bind("SUPER + SHIFT + x", scratchpad_show("games"))
diff --git a/config/desktop/hyprland/config.lua.d/80-workspaces.lua b/config/desktop/hyprland/config.lua.d/80-workspaces.lua
new file mode 100644
index 0000000..b66bb51
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/80-workspaces.lua
@@ -0,0 +1,32 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- desktop workspace settings
+--
+
+dotwryn.workspace_defaults = {
+ gaps = { inner = "0.5%", outer = "1%" },
+}
+
+dotwryn.workspaces = {
+ { key = "1", name = "1" },
+ { key = "2", name = "2", gaps = { enabled = false } },
+ { key = "3", name = "3" },
+ { key = "4", name = "4" },
+ { key = "5", name = "5" },
+ { key = "6", name = "6" },
+ { key = "7", name = "7" },
+ { key = "8", name = "8" },
+ { key = "9", name = "9" },
+
+ { key = "g", name = "100", label = "🎮", vertical = true, },
+ { key = "0", name = "name:🎧", vertical = true, },
+}
+
+for _, workspace in ipairs(dotwryn.workspaces) do
+ if workspace.label ~= nil then
+ hl.workspace_rule({ workspace = workspace.name, default_name = workspace.label })
+ end
+
+ hl.bind("SUPER + SHIFT + " .. workspace.key, hl.dsp.window.move({ workspace = workspace.name, follow = false }))
+end
diff --git a/config/desktop/hyprland/config.lua.d/81-workspaces-animations.lua b/config/desktop/hyprland/config.lua.d/81-workspaces-animations.lua
new file mode 100644
index 0000000..159fd37
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/81-workspaces-animations.lua
@@ -0,0 +1,34 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- workspace navigation with per-switch slide direction
+--
+
+local VERTICAL_WORKSPACES = {} -- by target ("100", "name:🎧") and display name ("🎮", "🎧")
+for _, workspace in ipairs(dotwryn.workspaces) do
+ if workspace.vertical then
+ VERTICAL_WORKSPACES[workspace.name] = true
+ VERTICAL_WORKSPACES[workspace.label or (workspace.name:gsub("^name:", ""))] = true
+ end
+end
+
+function animate_workspace_focus(name)
+ local active = hl.get_active_workspace()
+ local vertical = VERTICAL_WORKSPACES[name]
+ or (active ~= nil and VERTICAL_WORKSPACES[active.name])
+
+ hl.animation({ leaf = "workspaces", enabled = true, speed = 3, bezier = "easeOutQuint", style = vertical and "slidevert" or "slide" })
+ hl.dispatch(hl.dsp.focus({ workspace = name }))
+end
+
+for _, workspace in ipairs(dotwryn.workspaces) do
+ if workspace.name ~= "2" then -- (special case below)
+ hl.bind("SUPER + " .. workspace.key, function() animate_workspace_focus(workspace.name) end)
+ end
+end
+
+-- workspace 2 is home to the 'omni' tmux session; open a terminal if nothing is attached
+hl.bind("SUPER + 2", function()
+ animate_workspace_focus("2")
+ hl.dispatch(hl.dsp.exec_cmd("[[ $(tmux -L omni.socket list-clients 2>/dev/null | wc -l) -eq 0 ]] && $TERMINAL"))
+end)
diff --git a/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua b/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua
new file mode 100644
index 0000000..f76c740
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/82-workspaces-gaps.lua
@@ -0,0 +1,73 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+--
+-- provides per-workspace gaps settings
+--
+
+local get_gaps_config = function(workspace)
+ local gaps = {}
+ for key, value in pairs(dotwryn.workspace_defaults.gaps) do gaps[key] = value end
+ for key, value in pairs(workspace.gaps or {}) do gaps[key] = value end
+ return gaps
+end
+
+local to_pixels = function(value)
+ local percent = type(value) == "string" and tonumber((value:gsub("%%$", ""))) or nil
+ if percent == nil then return value end
+
+ local monitor = hl.get_active_monitor()
+ if monitor == nil then return 0 end -- (recomputed once a monitor shows up)
+
+ local scale = (monitor.scale ~= nil and monitor.scale > 0) and monitor.scale or 1
+ return math.floor(math.min(monitor.width, monitor.height) / scale * percent / 100)
+end
+
+local gaps_rules = {}
+local set_gaps = function(workspace, inner, outer)
+ if gaps_rules[workspace] ~= nil then gaps_rules[workspace].rule:set_enabled(false) end
+ gaps_rules[workspace] = {
+ inner = inner,
+ outer = outer,
+ rule = hl.workspace_rule({ workspace = workspace, gaps_in = inner, gaps_out = outer }),
+ }
+end
+
+local apply_gaps = function()
+ for _, workspace in ipairs(dotwryn.workspaces) do
+ local gaps = get_gaps_config(workspace)
+ if gaps.enabled == false then
+ set_gaps(workspace.name, 0, 0)
+ else
+ set_gaps(workspace.name, to_pixels(gaps.inner), to_pixels(gaps.outer))
+ end
+ end
+end
+
+apply_gaps()
+
+for _, event in ipairs({ "monitor.added", "monitor.removed", "monitor.layout_changed" }) do
+ hl.on(event, apply_gaps)
+end
+
+hl.window_rule({
+ name = "smart-no-borders",
+ match = { float = false, group = false, workspace = "s[false] w[tv1]" },
+ border_size = 0,
+})
+
+local GAPS_STEP = "0.5%"
+
+local adjust_gaps = function(direction)
+ return function()
+ local workspace = hl.get_active_workspace()
+ if workspace == nil then return end
+
+ local name = workspace.id > 0 and tostring(workspace.id) or ("name:" .. workspace.name)
+ local current = gaps_rules[name] or { inner = 0, outer = 0 }
+ local delta = direction * math.max(1, to_pixels(GAPS_STEP))
+ set_gaps(name, math.max(0, current.inner + delta), math.max(0, current.outer + delta))
+ end
+end
+
+hl.bind("SUPER + z", adjust_gaps( 1), { repeating = true })
+hl.bind("SUPER + SHIFT + z", adjust_gaps(-1), { repeating = true })
diff --git a/config/desktop/hyprland/config.lua.d/90-key-bindings.lua b/config/desktop/hyprland/config.lua.d/90-key-bindings.lua
new file mode 100644
index 0000000..de7732e
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/90-key-bindings.lua
@@ -0,0 +1,56 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+
+local bind = function(bindings, ...)
+ for keys, dispatcher in pairs(bindings) do
+ hl.bind(keys, dispatcher, ...)
+ end
+end
+
+local set_background =
+
+---------------------------------------------------------------------
+
+bind({
+ ["XF86AudioRewind"] = hl.dsp.exec_cmd("playerctl previous"),
+ ["XF86AudioPrev"] = hl.dsp.exec_cmd("playerctl previous"),
+ ["XF86AudioForward"] = hl.dsp.exec_cmd("playerctl next"),
+ ["XF86AudioNext"] = hl.dsp.exec_cmd("playerctl next"),
+ ["XF86AudioPlay"] = hl.dsp.exec_cmd("playerctl play-pause"),
+
+ ["XF86AudioMute"] = hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"),
+ ["XF86AudioMicMute"] = hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"),
+}, { locked = true })
+
+------------------------------------------
+
+bind({
+ ["XF86AudioRaiseVolume"] = hl.dsp.exec_cmd("qs ipc call volume up"), -- ../../../user/quickshell/VolumeOsd.qml "going past 100%"
+ ["XF86AudioLowerVolume"] = hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 10%-"),
+}, { locked = true, repeating = true })
+
+------------------------------------------
+
+bind({
+ ["SUPER + SHIFT + m"] = dotwryn.scwrypts.hyprland.launch_or_show("slack"),
+ ["SUPER + SHIFT + d"] = dotwryn.scwrypts.hyprland.launch_or_show("fluxer", "-c", "Fluxer"),
+ ["SUPER + c"] = dotwryn.scwrypts.hyprland.launch_or_show("google-voice-desktop"),
+ ["SUPER + m"] = dotwryn.scwrypts.hyprland.launch_or_show("youtube-music-desktop-app", "-c", "YouTube Music Desktop App"),
+ ["SUPER + SHIFT + s"] = dotwryn.scwrypts.hyprland.launch_or_show("signal"),
+ ["SUPER + a"] = dotwryn.scwrypts.hyprland.launch_or_show("pavucontrol", "-s", "0.5", "-c", "pavucontrol"),
+ ["SUPER + SHIFT + o"] = dotwryn.scwrypts.hyprland.launch_or_show("obs", "-c", "^obs", "-n", "-l"),
+ ["SUPER + Return"] = hl.dsp.exec_cmd("$TERMINAL"),
+
+ ["SUPER + p"] = dotwryn.scwrypts.hyprland.set_background("random"),
+ ["SUPER + SHIFT + p"] = dotwryn.scwrypts.hyprland.set_background("theme"),
+
+ ["SUPER + SHIFT + minus"] = hl.dsp.exec_cmd("pidof hyprlock || hyprlock"),
+ ["SUPER + space"] = hl.dsp.exec_cmd("rofi -theme $DOTWRYN/config/rofi.theme.rasi -modes drun,run -show run"),
+ ["SUPER + backslash"] = dotwryn.scwrypts.hyprland.screenshot(),
+
+ --
+ -- global application shortcuts (wayland apps can't grab global keys)
+ --
+
+ ["ALT + SHIFT + space"] = hl.dsp.exec_cmd("1password --quick-access"),
+})
diff --git a/config/desktop/hyprland/config.lua.d/90-window-rules.lua b/config/desktop/hyprland/config.lua.d/90-window-rules.lua
new file mode 100644
index 0000000..9fb409e
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/90-window-rules.lua
@@ -0,0 +1,25 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+
+hl.window_rule({ name = "float-modal", match = { modal = true }, float = true })
+
+hl.window_rule({
+ name = "float-dialog-titles",
+ match = { title = "^(About|Organizer|Preferences|Task Dialog|Toolbox|Web Console).*$" },
+ float = true,
+})
+
+hl.window_rule({
+ name = "screenshot-flameshot", -- scwrypts hyprland screenshot
+ match = { class = "^flameshot$" },
+ float = true,
+ move = "0 0",
+ pin = true,
+ no_anim = true,
+})
+
+hl.window_rule({
+ name = "games-workspace",
+ match = { class = "^steam_app_.*$" },
+ workspace = "100", -- 🎮
+})
diff --git a/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua b/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua
new file mode 100644
index 0000000..d7283b9
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/98-host-local-overrides.lua
@@ -0,0 +1,18 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+
+local hostnamectl = io.popen("hostnamectl --static")
+local HOSTNAME = hostnamectl:read("l")
+hostnamectl:close()
+
+local dofile_if_exists = function(path)
+ local file = io.open(path, "r")
+ if file == nil then return end
+ file:close()
+ dofile(path)
+end
+
+------------------------------------------
+
+dofile_if_exists(dotwryn.DOTWRYN .. "/config/local/" .. HOSTNAME .. "/user/hypr/host.lua")
+dofile_if_exists((os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME") .. "/.config")) .. "/hypr/local.lua")
diff --git a/config/desktop/hyprland/config.lua.d/99-autostart.lua b/config/desktop/hyprland/config.lua.d/99-autostart.lua
new file mode 100644
index 0000000..2bc3be7
--- /dev/null
+++ b/config/desktop/hyprland/config.lua.d/99-autostart.lua
@@ -0,0 +1,12 @@
+local dotwryn = ... -- ../config.lua
+---------------------------------------------------------------------
+
+hl.on("hyprland.start", function()
+ hl.dispatch(dotwryn.scwrypts.hyprland.set_background("theme")) -- (starts hyprpaper if it isn't running)
+ hl.dispatch(dotwryn.scwrypts.wayland.sync_gtk_settings()) -- gtk theme from settings.ini
+ hl.exec_cmd("quickshell")
+ hl.exec_cmd("1password --silent")
+ hl.dispatch(dotwryn.scwrypts.tmux.omni())
+ hl.exec_cmd("/usr/lib/xfce-polkit/xfce-polkit")
+ hl.exec_cmd("command -v apps && apps") -- host-local background apps ($DOTWRYN/config/local//bin/apps)
+end)
diff --git a/config/desktop/hyprland/scwrypts.lua b/config/desktop/hyprland/scwrypts.lua
new file mode 100644
index 0000000..8a2e500
--- /dev/null
+++ b/config/desktop/hyprland/scwrypts.lua
@@ -0,0 +1,78 @@
+--
+-- custom (temporary?) interface to scwrypts for hyprland lua
+--
+
+local scwrypts = function(args_meta, args_scwrypt, environment)
+ local get_quoted = function(args)
+ local quoted_args = {}
+ for _, arg in ipairs(args) do
+ table.insert(quoted_args, "'" .. arg:gsub("'", "'\\''") .. "'")
+ end
+ return table.concat(quoted_args, " ")
+ end
+
+ local environment_names = {}
+ for name, _ in pairs(environment or {}) do table.insert(environment_names, name) end
+ table.sort(environment_names)
+
+ local environment_assignments = {}
+ for _, name in ipairs(environment_names) do
+ table.insert(environment_assignments, name .. "=" .. get_quoted({ environment[name] }))
+ end
+
+ return table.concat({
+ table.concat(environment_assignments, " "),
+ get_quoted({ os.getenv("DOTWRYN") .. "/zsh/plugins/scwrypts/scwrypts" }),
+ get_quoted(args_meta or {}),
+ "--",
+ get_quoted(args_scwrypt or {}),
+ }, " ")
+end
+
+------------------------------------------
+
+local scwrypts_desktop = function(args_meta, args_scwrypt)
+ return scwrypts(args_meta, args_scwrypt, {
+ SCWRYPTS_LOG_LEVEL = "0",
+ DESKTOP__SFX_PATH = os.getenv("HOME") .. "/Media/sfx",
+ })
+end
+
+---------------------------------------------------------------------
+
+local hyprland = {
+ launch_or_show = function(program, ...)
+ return hl.dsp.exec_cmd(scwrypts_desktop({ 'hyprland', 'launch-or-show' }, { program, ... }))
+ end,
+
+ screenshot = function(...)
+ return hl.dsp.exec_cmd(scwrypts_desktop({ 'hyprland', 'screenshot' }, { ... }))
+ end,
+
+ set_background = function(...)
+ return hl.dsp.exec_cmd(scwrypts_desktop({ 'hyprland', 'set-background' }, { ... }))
+ end,
+}
+
+local wayland = {
+ sync_gtk_settings = function(...)
+ return hl.dsp.exec_cmd(scwrypts({ 'sync-gtk-settings' }, { ... }))
+ end,
+}
+
+local tmux = {
+ omni = function()
+ return hl.dsp.exec_cmd(scwrypts({ 'omni', 'tmux' }, { '--background' }))
+ end,
+}
+
+---------------------------------------------------------------------
+
+return {
+ scwrypts = scwrypts,
+ scwrypts_desktop = scwrypts_desktop,
+
+ hyprland = hyprland,
+ wayland = wayland,
+ tmux = tmux,
+}
diff --git a/config/desktop/hyprland/session b/config/desktop/hyprland/session
new file mode 100755
index 0000000..df67da1
--- /dev/null
+++ b/config/desktop/hyprland/session
@@ -0,0 +1,15 @@
+#!/usr/bin/env zsh
+# hyprland session; TTY alias is 'starth'
+export XDG_SESSION_TYPE=wayland
+source "${DOTWRYN}/config/desktop/server/common.zsh"
+#####################################################################
+
+export XDG_CURRENT_DESKTOP=Hyprland
+export XDG_SESSION_DESKTOP=Hyprland
+export DESKTOP_SESSION=hyprland
+
+#####################################################################
+
+systemctl --user start hyprland-session.target
+cd; start-hyprland
+systemctl --user stop hyprland-session.target
diff --git a/config/xinitrc.i3wm b/config/desktop/i3wm/session
old mode 100644
new mode 100755
similarity index 58%
rename from config/xinitrc.i3wm
rename to config/desktop/i3wm/session
index 43ec976..39bbb72
--- a/config/xinitrc.i3wm
+++ b/config/desktop/i3wm/session
@@ -1,5 +1,8 @@
#!/bin/zsh
-source "${DOTWRYN}/config/xinitrc.common"
+# KDE plasma session; TTY alias is 'starti'; also default 'startx' target
+export XDG_SESSION_TYPE=x11
+source "${DOTWRYN}/config/desktop/server/common.zsh"
+#####################################################################
export DESKTOP_SESSION=i3
export XDG_CURRENT_DESKTOP=i3
diff --git a/config/desktop/kde/session b/config/desktop/kde/session
new file mode 100755
index 0000000..90d09a6
--- /dev/null
+++ b/config/desktop/kde/session
@@ -0,0 +1,8 @@
+#!/bin/zsh
+# KDE plasma session; TTY alias is 'startk'
+export XDG_SESSION_TYPE=x11
+source "$DOTWRYN/config/desktop/server/common.zsh"
+#####################################################################
+
+export DESKTOP_SESSION=plasma
+exec startplasma-x11
diff --git a/config/desktop/server/common.zsh b/config/desktop/server/common.zsh
new file mode 100644
index 0000000..5eaf59b
--- /dev/null
+++ b/config/desktop/server/common.zsh
@@ -0,0 +1,47 @@
+#
+# shared by every desktop session (config/desktop/*/session)
+#
+
+case "${XDG_SESSION_TYPE}" in
+ ( x11 | wayland ) ;;
+ ( '' )
+ echo "ERROR : XDG_SESSION_TYPE must be set to one of 'x11' or 'wayland' (currently unset)" >&2
+ return 1
+ ;;
+ ( * )
+ echo "ERROR : XDG_SESSION_TYPE must be 'x11' or 'wayland' (got '${XDG_SESSION_TYPE}')" >&2
+ return 1
+ ;;
+esac
+
+#####################################################################
+
+for PREFERRED_TERMINAL in \
+ alacritty \
+ kitty \
+ ;
+do
+ command -v ${PREFERRED_TERMINAL} &>/dev/null \
+ && export TERMINAL="$(which ${PREFERRED_TERMINAL})" \
+ && break
+done
+unset PREFERRED_TERMINAL
+
+#####################################################################
+
+for PREFERRED_BROWSER in \
+ brave \
+ firefox \
+ google-chrome-stable \
+ google-chrome \
+ ;
+do
+ command -v ${PREFERRED_BROWSER} &>/dev/null \
+ && export BROWSER="$(which ${PREFERRED_BROWSER})" \
+ && break
+done
+unset PREFERRED_BROWSER
+
+#####################################################################
+
+source "${DOTWRYN}/config/desktop/server/${XDG_SESSION_TYPE}.zsh"
diff --git a/config/desktop/server/wayland.zsh b/config/desktop/server/wayland.zsh
new file mode 100644
index 0000000..4250189
--- /dev/null
+++ b/config/desktop/server/wayland.zsh
@@ -0,0 +1,5 @@
+#
+# currently empty; the previous server-level configurations are
+# owned by the "Wayland Compositor" instead rather than the server
+#
+true
diff --git a/config/xinitrc.common b/config/desktop/server/x11.zsh
similarity index 52%
rename from config/xinitrc.common
rename to config/desktop/server/x11.zsh
index 186c712..c0d2f59 100644
--- a/config/xinitrc.common
+++ b/config/desktop/server/x11.zsh
@@ -1,32 +1,3 @@
-#!/bin/zsh
-#####################################################################
-
-for PREFERRED_TERMINAL in \
- alacritty \
- kitty \
- ;
-do
- command -v ${PREFERRED_TERMINAL} &>/dev/null \
- && export TERMINAL="$(which ${PREFERRED_TERMINAL})" \
- && break
-done
-unset PREFERRED_TERMINAL
-
-#####################################################################
-
-for PREFERRED_BROWSER in \
- brave \
- firefox \
- google-chrome-stable \
- google-chrome \
- ;
-do
- command -v ${PREFERRED_BROWSER} &>/dev/null \
- && export BROWSER="$(which ${PREFERRED_BROWSER})" \
- && break
-done
-unset PREFERRED_BROWSER
-
#####################################################################
for XINITRC_FILE in $(find /etc/X11/xinit/xinitrc.d | sort); do source "${XINITRC_FILE}"; done
@@ -47,7 +18,7 @@ for BACKGROUND_APPLICATION in \
"${DOTWRYN}/config/local/$(hostnamectl --static)/bin/default" \
;
do
- command -v ${BACKGROUND_APPLCATION} && exec ${BACKGROUND_APPLICATION} &
+ command -v ${BACKGROUND_APPLICATION} && exec ${BACKGROUND_APPLICATION} &
done
#####################################################################
diff --git a/config/sfx.map b/config/sfx.map
new file mode 100644
index 0000000..58b73e1
--- /dev/null
+++ b/config/sfx.map
@@ -0,0 +1,17 @@
+# sound effect names -> files in DESKTOP__SFX_PATH
+#
+# shared by:
+# - scwrypts 'media audio/play-sfx' (i3, login/dock scripts, automation)
+# - quickshell VolumeOsd.qml (hyprland volume / mute feedback)
+#
+#
+backlight yaru-audio-volume-change.oga
+gamedock gamedock.oga
+homedock homedock.oga
+login yaru-desktop-login.oga
+logout smooth-desktop-login.oga
+mute smooth-dialog-warning.oga
+notify yaru-complete.oga
+undock yaru-desktop-login.oga
+volume yaru-message.oga
+volume-max smooth-dialog-warning.oga
diff --git a/config/user/X11/xinitrc b/config/user/X11/xinitrc
index 3497d14..8621206 100644
--- a/config/user/X11/xinitrc
+++ b/config/user/X11/xinitrc
@@ -1,3 +1,3 @@
#!/bin/zsh
source "${HOME}/.zshrc" &>/dev/null
-source "${DOTWRYN}/config/xinitrc.i3wm"
+source "${DOTWRYN}/config/desktop/i3wm/session"
diff --git a/config/user/tmux/tmux.conf b/config/user/tmux/tmux.conf
index 5fa789e..2a06be4 100644
--- a/config/user/tmux/tmux.conf
+++ b/config/user/tmux/tmux.conf
@@ -7,8 +7,10 @@ set -sg repeat-time 0
set-option -g default-shell /bin/zsh
+set-option -ga update-environment HYPRLAND_INSTANCE_SIGNATURE
+
# quick reload
-bind-key r source-file ~/.tmux.conf\; display "TMUX CONFIG RELOADED"
+bind-key r source-file ~/.config/tmux/tmux.conf\; display "TMUX CONFIG RELOADED"
# force color
set -g default-terminal "tmux-256color"
diff --git a/config/xinitrc.kde b/config/xinitrc.kde
deleted file mode 100644
index 9eac5a1..0000000
--- a/config/xinitrc.kde
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/zsh
-source "$DOTWRYN/config/xinitrc.common"
-
-export DESKTOP_SESSION=plasma
-exec startplasma-x11
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/eval.module.zsh b/scwrypts/dotwryn/desktop/wayland/hyprland/eval.module.zsh
new file mode 100644
index 0000000..4a8231e
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/eval.module.zsh
@@ -0,0 +1,40 @@
+#####################################################################
+
+DEPENDENCIES+=(hyprctl)
+
+#####################################################################
+
+${scwryptsmodule}() {
+ eval "$(utils.parse.autosetup)"
+ ##########################################
+
+ local result="$(hyprctl eval "$(printf '%s\n' "${ARGS[@]}")")"
+
+ [[ "${result}" == ok ]] \
+ || echo.error "hyprctl eval failed : ${result}" \
+ || return 1
+}
+
+#####################################################################
+
+${scwryptsmodule}.parse() { return 0; }
+
+${scwryptsmodule}.parse.locals() {
+ local ARGS=()
+}
+
+${scwryptsmodule}.parse.usage() {
+ USAGE__description='
+ run hyprland lua in the current hyprland instance (one statement per
+ argument, evaluated together); fails unless hyprland replies "ok"
+ '
+
+ USAGE__args='
+ \$@ lua statements (e.g. "hl.dispatch(hl.dsp.window.center({}))")
+ '
+}
+
+${scwryptsmodule}.parse.validate() {
+ [[ "${#ARGS[@]}" -gt 0 ]] \
+ || echo.error 'at least one lua statement is required'
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/get-window-addresses.module.zsh b/scwrypts/dotwryn/desktop/wayland/hyprland/get-window-addresses.module.zsh
new file mode 100644
index 0000000..d995610
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/get-window-addresses.module.zsh
@@ -0,0 +1,41 @@
+#####################################################################
+
+DEPENDENCIES+=(hyprctl jq)
+
+#####################################################################
+
+${scwryptsmodule}() {
+ eval "$(utils.parse.autosetup)"
+ ##########################################
+
+ hyprctl -j clients \
+ | jq -r --arg class "${CLIENT_CLASS}" '.[] | select(.class | test($class; "i")) | .address' \
+ ;
+}
+
+#####################################################################
+
+${scwryptsmodule}.parse() { return 0; }
+
+${scwryptsmodule}.parse.locals() {
+ local ARGS=()
+ local CLIENT_CLASS
+}
+
+${scwryptsmodule}.parse.usage() {
+ USAGE__description='
+ print the address of every hyprland window whose class matches
+ (one per line; case-insensitive regex)
+ '
+
+ USAGE__args='
+ \$1 window class regex
+ '
+}
+
+${scwryptsmodule}.parse.validate() {
+ CLIENT_CLASS="${ARGS[1]}"
+
+ [[ "${CLIENT_CLASS}" ]] \
+ || echo.error 'window class is required'
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/hyprland.module.zsh b/scwrypts/dotwryn/desktop/wayland/hyprland/hyprland.module.zsh
new file mode 100644
index 0000000..be98f7b
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/hyprland.module.zsh
@@ -0,0 +1,10 @@
+#
+# interact with the running hyprland instance
+#
+
+
+# run hyprland lua (hyprctl eval) with error checking
+use desktop/wayland/hyprland/eval --group dotwryn
+
+# list window addresses matching a class regex
+use desktop/wayland/hyprland/get-window-addresses --group dotwryn
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/launch-or-show b/scwrypts/dotwryn/desktop/wayland/hyprland/launch-or-show
new file mode 100755
index 0000000..e232c44
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/launch-or-show
@@ -0,0 +1,163 @@
+#!/usr/bin/env zsh
+use notify
+use desktop/wayland/hyprland --group dotwryn
+
+DEPENDENCIES+=(jq)
+#####################################################################
+
+USAGE__usage+=' [...options...]'
+
+USAGE__options="
+ -c, --client if different from the executable name, the window class (regex)
+
+ -s, --scale (default: 0.8 or 0.5 if screen width >3000px)
+ -x, --x-offset (default: 0.0)
+ -y, --y-offset (default: 0.0)
+
+ -a, --always-launch invoke executable even if a window of client-class exists
+ -n, --no-resize don't resize the window (ignores -sxy flags)
+ -l, --no-center leave the window wherever it was last positioned
+
+ --has-statusbar-icon (accepted for i3 compatibility; no effect)
+ "
+
+USAGE__description="
+ Hyprland port of 'desktop/xorg/i3/launch-or-show'.
+
+ Makes it easy to bind appications to key shortcuts without having to
+ spin up redundant instances. Each application gets its own special
+ workspace (hyprland's scratchpad), named after the executable:
+ 1) starts the application if it is not running
+ 2) moves all instances of the application to its special workspace
+ 3) (toggle) hides the special workspace if it is showing
+ 4) (toggle) shows the special workspace (resized and centered) if it is hidden
+ "
+
+#####################################################################
+
+MAIN() {
+ local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop)
+
+ local application client_class
+ local x_offset=0.0 y_offset=0.0 scale
+ local always_launch=false resize=true center=true
+
+ local _s _p=0
+ while [[ "${#}" -gt 0 ]]
+ do
+ _s=1
+ case "${1}" in
+ ( -c | --client ) _s=2; client_class="${2}" ;;
+ ( -x | --x-offset ) _s=2; x_offset="${2}" ;;
+ ( -y | --y-offset ) _s=2; y_offset="${2}" ;;
+ ( -s | --scale ) _s=2; scale="${2}" ;;
+
+ ( -a | --always-launch ) always_launch=true ;;
+ ( -n | --no-resize ) resize=false ;;
+ ( -l | --no-center ) center=false ;;
+
+ ( --has-statusbar-icon ) ;;
+
+ ( -h | --help ) USAGE; return 0 ;;
+
+ ( * )
+ case "$((_p+=1))" in
+ ( 1 ) application="${1}" ;;
+ ( * ) echo.error "extra positional argument '${1}'" ;;
+ esac
+ ;;
+ esac
+ shift "${_s}" || echo.error "missing value for argument '${1}'" || shift "${#}"
+ done
+ unset _s _p
+
+ [[ "${application}" ]] || echo.error 'path-executable required'
+ [[ "${client_class}" ]] || client_class="${application}"
+
+ [[ "${application}" ]] && {
+ utils.dependencies.check "${application}" \
+ || notify.error "${application} not found"
+ }
+
+ utils.check-errors --fail
+
+ ##########################################
+
+ local special_workspace="special:${application:t}"
+
+ local monitor="$(hyprctl -j monitors | jq -c '.[] | select(.focused)')"
+ [[ "${scale}" ]] || {
+ [[ "$(jq -r '.width' <<< "${monitor}")" -gt 3000 ]] \
+ && scale=0.5 \
+ || scale=0.8 \
+ ;
+ }
+
+ ##########################################
+
+ local addresses=($(dotwryn.desktop.wayland.hyprland.get-window-addresses "${client_class}"))
+
+ [[ "${always_launch}" == true ]] || [[ "${#addresses[@]}" -eq 0 ]] && {
+ echo.status 'launching application'
+ hyprctl dispatch "hl.dsp.exec_cmd(\"${application}\")" >/dev/null
+
+ echo.status 'waiting for application window'
+ local wait_count
+ for wait_count in {1..50}
+ do
+ [[ "$(dotwryn.desktop.wayland.hyprland.get-window-addresses "${client_class}" | wc -l)" -gt "${#addresses[@]}" ]] && break
+ sleep 0.1
+ done
+ addresses=($(dotwryn.desktop.wayland.hyprland.get-window-addresses "${client_class}"))
+ }
+
+ [[ "${#addresses[@]}" -gt 0 ]] \
+ || notify.error "no '${client_class}' window found for ${application}" \
+ || return 1
+
+ ##########################################
+
+ echo.status 'moving windows to special workspace'
+ local address lua=()
+ for address in "${addresses[@]}"
+ do
+ lua+=(
+ "hl.dispatch(hl.dsp.window.float({ window = \"address:${address}\", action = \"enable\" }))"
+ "hl.dispatch(hl.dsp.window.move({ window = \"address:${address}\", workspace = \"${special_workspace}\", follow = false }))"
+ )
+ done
+
+ local shown_special="$(jq -r '.specialWorkspace.name' <<< "${monitor}")"
+ [[ "${shown_special}" == "${special_workspace}" ]] && {
+ echo.status 'hiding special workspace'
+ lua+=("hl.dispatch(hl.dsp.workspace.toggle_special(\"${application:t}\"))")
+ dotwryn.desktop.wayland.hyprland.eval "${lua[@]}"
+ return $?
+ }
+
+ echo.status 'showing special workspace'
+ lua+=("hl.dispatch(hl.dsp.workspace.toggle_special(\"${application:t}\"))")
+
+ [[ "${resize}" == true ]] && {
+ local window_size=($(jq -r \
+ --argjson f "${scale}" --argjson x "${x_offset}" --argjson y "${y_offset}" \
+ '"\((.width / .scale * $f + $x) | floor) \((.height / .scale * $f + $y) | floor)"' \
+ <<< "${monitor}"
+ ))
+ echo.debug "window size: ${window_size[*]}"
+
+ for address in "${addresses[@]}"
+ do
+ lua+=("hl.dispatch(hl.dsp.window.resize({ window = \"address:${address}\", x = ${window_size[1]}, y = ${window_size[2]} }))")
+ done
+ }
+
+ [[ "${center}" == true ]] && {
+ for address in "${addresses[@]}"
+ do
+ lua+=("hl.dispatch(hl.dsp.window.center({ window = \"address:${address}\" }))")
+ done
+ }
+
+ dotwryn.desktop.wayland.hyprland.eval "${lua[@]}"
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/lock b/scwrypts/dotwryn/desktop/wayland/hyprland/lock
new file mode 100755
index 0000000..a8ced13
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/lock
@@ -0,0 +1,22 @@
+#!/usr/bin/env zsh
+use notify
+
+DEPENDENCIES+=(hyprlock)
+#####################################################################
+
+USAGE__description='
+ Hyprland port of "desktop/xorg/i3/lock"; locks the session with hyprlock
+ (themed by ~/.config/hypr/hyprlock.conf and the active colorscheme)
+ '
+
+#####################################################################
+
+MAIN() {
+ local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop)
+
+ pgrep -x hyprlock &>/dev/null && return 0
+
+ hyprlock \
+ || notify.error 'something went wrong; unable to lock' \
+ ;
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/logout b/scwrypts/dotwryn/desktop/wayland/hyprland/logout
new file mode 100755
index 0000000..83fa2ef
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/logout
@@ -0,0 +1,20 @@
+#!/usr/bin/env zsh
+use notify
+
+DEPENDENCIES+=(qs)
+#####################################################################
+
+USAGE__description='
+ Hyprland port of "desktop/xorg/i3/logout"; asks for confirmation
+ (quickshell LogoutPrompt) before exiting hyprland
+ '
+
+#####################################################################
+
+MAIN() {
+ local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop)
+
+ qs ipc call logout show \
+ || notify.error 'unable to show logout prompt (is quickshell running?)' \
+ ;
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/screenshot b/scwrypts/dotwryn/desktop/wayland/hyprland/screenshot
new file mode 100755
index 0000000..50e1855
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/screenshot
@@ -0,0 +1,26 @@
+#!/usr/bin/env zsh
+use notify
+
+DEPENDENCIES+=(flameshot hyprctl jq)
+#####################################################################
+
+USAGE__description='
+ Hyprland equivalent of the i3 flameshot screenshot; freezes the
+ focused monitor in flameshot to select / annotate / copy / save
+ '
+
+#####################################################################
+
+MAIN() {
+ local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop)
+
+ # flameshot screens are numbered from 0 in output order (hyprland ids are not)
+ local screen_number
+ screen_number="$(hyprctl monitors -j | jq -r 'sort_by(.id) | to_entries[] | select(.value.focused) | .key')"
+ [ "${screen_number}" ] \
+ || { notify.error 'unable to determine focused monitor'; return 1; }
+
+ flameshot screen --edit --number "${screen_number}" \
+ || notify.error 'unable to take screenshot' \
+ ;
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/hyprland/set-background b/scwrypts/dotwryn/desktop/wayland/hyprland/set-background
new file mode 100755
index 0000000..7424a8a
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/hyprland/set-background
@@ -0,0 +1,72 @@
+#!/usr/bin/env zsh
+use notify
+use scwrypts/get-realpath
+
+DEPENDENCIES+=(hyprctl)
+REQUIRED_ENV+=(DESKTOP__WALLPAPER_PATH)
+#####################################################################
+
+USAGE__usage+=' [wallpaper]'
+
+USAGE__args+='
+ $1 one of:
+ random : any wallpaper (default)
+ theme : the wallpaper named after the active colorscheme (e.g. main.jpg)
+ (optional \$2 : use this theme name instead)
+ : a wallpaper file within DESKTOP__WALLPAPER_PATH
+ '
+
+USAGE__description='
+ Hyprland port of "desktop/xorg/i3/set-background"; sets the wallpaper
+ on every monitor through hyprpaper
+ '
+
+#####################################################################
+
+MAIN() {
+ local SCWRYPTS_NOTIFICATION_ENGINES=(echo notify.desktop)
+
+ # env values are literal (e.g. '$HOME/Pictures/bg'); resolve before use
+ local wallpaper_path="$(scwrypts.get-realpath "${DESKTOP__WALLPAPER_PATH}")"
+
+ [[ -d "${wallpaper_path}" ]] \
+ || echo.error "no such directory for DESKTOP__WALLPAPER_PATH='${DESKTOP__WALLPAPER_PATH}'" \
+ || return 1
+
+ local selection="${1:-random}"
+ local wallpaper
+
+ case "${selection}" in
+ ( random )
+ wallpaper="$(find "${wallpaper_path}" -type f | shuf -n1)"
+ ;;
+ ( theme )
+ # optional $2 : theme name (default: the active theme)
+ local theme_name="${2:-$(cat -- "${DOTWRYN}/config/colorschemes/active/name.txt" 2>/dev/null)}"
+ wallpaper="$(find "${wallpaper_path}" -type f -name "${theme_name}.*" | head -n1)"
+ [[ "${wallpaper}" ]] \
+ || echo.error "no wallpaper for theme '${theme_name}' in wallpaper path" \
+ || return 2
+ ;;
+ ( * )
+ wallpaper="${wallpaper_path}/${selection}"
+ [[ -f "${wallpaper}" ]] \
+ || echo.error "no such wallpaper '${selection}' in wallpaper path" \
+ || return 2
+ ;;
+ esac
+
+ [[ "${wallpaper}" ]] \
+ || echo.error "no wallpapers found in '${wallpaper_path}'" \
+ || return 3
+
+ pgrep -x hyprpaper &>/dev/null || {
+ hyprctl dispatch 'hl.dsp.exec_cmd("hyprpaper")' >/dev/null
+ sleep 1
+ }
+
+ # empty monitor : fallback for any monitor without a specific wallpaper
+ hyprctl hyprpaper wallpaper ", ${wallpaper}, cover" >/dev/null \
+ || notify.error "unable to set wallpaper '${wallpaper}'" \
+ ;
+}
diff --git a/scwrypts/dotwryn/desktop/wayland/sync-gtk-settings b/scwrypts/dotwryn/desktop/wayland/sync-gtk-settings
new file mode 100755
index 0000000..a63c0ab
--- /dev/null
+++ b/scwrypts/dotwryn/desktop/wayland/sync-gtk-settings
@@ -0,0 +1,45 @@
+#!/usr/bin/env zsh
+DEPENDENCIES+=(gsettings)
+#####################################################################
+
+USAGE__description='
+ On X11, GTK reads its theme from ~/.config/gtk-3.0/settings.ini (written by
+ lxappearance). Wayland GTK apps read gsettings instead, so copy the values
+ across; settings.ini stays the single source of truth for both sessions.
+ '
+
+#####################################################################
+
+MAIN() {
+ local settings_file="${XDG_CONFIG_HOME:-${HOME}/.config}/gtk-3.0/settings.ini"
+
+ [[ -f "${settings_file}" ]] \
+ || echo.error "no gtk settings found at '${settings_file}'" \
+ || return 1
+
+ local ini_key gsettings_key value
+ for ini_key gsettings_key in \
+ gtk-theme-name gtk-theme \
+ gtk-icon-theme-name icon-theme \
+ gtk-font-name font-name \
+ gtk-cursor-theme-name cursor-theme \
+ ;
+ do
+ value="$(sed -n "s/^${ini_key}=//p" "${settings_file}" | tail -n1)"
+ [[ "${value}" ]] || continue
+
+ gsettings set org.gnome.desktop.interface "${gsettings_key}" "${value}" \
+ && echo.status "${gsettings_key} : ${value}" \
+ || echo.error "unable to set ${gsettings_key}" \
+ ;
+ done
+
+ # libadwaita / portal-aware apps (electron, gtk4) follow color-scheme rather than the theme name
+ local theme="$(gsettings get org.gnome.desktop.interface gtk-theme)"
+ case "${theme:l}" in
+ ( *dark* ) gsettings set org.gnome.desktop.interface color-scheme prefer-dark ;;
+ ( * ) gsettings set org.gnome.desktop.interface color-scheme default ;;
+ esac
+
+ [[ "${ERRORS}" -eq 0 ]]
+}
diff --git a/scwrypts/media/audio/play-sfx.module.zsh b/scwrypts/media/audio/play-sfx.module.zsh
index 2b382b5..e02bee3 100644
--- a/scwrypts/media/audio/play-sfx.module.zsh
+++ b/scwrypts/media/audio/play-sfx.module.zsh
@@ -28,21 +28,14 @@ ${scwryptsmodule}.parse() {
[[ ${POSITIONAL_ARGS} -eq 0 ]] || return 0
((POSITIONAL_ARGS+=1))
- case $1 in
- ( backlight ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-audio-volume-change.oga" ;;
- ( gamedock ) SFX_FILE="${DESKTOP__SFX_PATH}/gamedock.oga" ;;
- ( homedock ) SFX_FILE="${DESKTOP__SFX_PATH}/homedock.oga" ;;
- ( login ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-desktop-login.oga" ;;
- ( logout ) SFX_FILE="${DESKTOP__SFX_PATH}/smooth-desktop-login.oga" ;;
- ( mute ) SFX_FILE="${DESKTOP__SFX_PATH}/smooth-dialog-warning.oga" ;;
- ( notify ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-complete.oga" ;;
- ( undock ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-desktop-login.oga" ;;
- ( volume ) SFX_FILE="${DESKTOP__SFX_PATH}/yaru-message.oga" ;;
- ( * )
- SFX_FILE="$1"
- ;;
- esac
+ # mapped names live in $DOTWRYN/config/sfx.map (shared with the hyprland quickshell)
+ local mapped_file="$(awk -v name="${1}" '!/^#/ && $1 == name { print $2; exit }' "${DOTWRYN}/config/sfx.map" 2>/dev/null)"
+
+ [[ "${mapped_file}" ]] \
+ && SFX_FILE="${DESKTOP__SFX_PATH}/${mapped_file}" \
+ || SFX_FILE="${1}" \
+ ;
return 1
}
diff --git a/scwrypts/remote/tmux/tmux.conf b/scwrypts/remote/tmux/tmux.conf
index 289d944..8602bd1 100644
--- a/scwrypts/remote/tmux/tmux.conf
+++ b/scwrypts/remote/tmux/tmux.conf
@@ -3,6 +3,7 @@ set -g default-terminal "tmux-256color"
set -g mouse off
set -sg escape-time 0
set-option -g default-shell /bin/zsh
+set-option -ga update-environment HYPRLAND_INSTANCE_SIGNATURE
set-option -gw xterm-keys on
set-window-option -g mode-keys vi
set-window-option -g status-keys vi