diff options
| author | Sam Nystrom <sam@samnystrom.dev> | 2025-11-27 17:35:48 -0500 |
|---|---|---|
| committer | Sam Nystrom <sam@samnystrom.dev> | 2025-11-27 17:35:48 -0500 |
| commit | e521805e7f3f52214056720e3a280c9e9f2ec6e9 (patch) | |
| tree | 22e01c53c43b3fda9b7c7f214fc35f29536cc135 /.config/waywall/waywork | |
| parent | 77b295b8b0f9b73ecbda809e7843812c5f4c3737 (diff) | |
| parent | 747b8a465a501211ebe41d5892eb9262f26743dc (diff) | |
Merge commit '747b8a465a501211ebe41d5892eb9262f26743dc' as '.config/waywall/waywork'
Diffstat (limited to '.config/waywall/waywork')
| -rw-r--r-- | .config/waywall/waywork/LICENSE | 21 | ||||
| -rw-r--r-- | .config/waywall/waywork/README.md | 283 | ||||
| -rw-r--r-- | .config/waywall/waywork/core.lua | 61 | ||||
| -rw-r--r-- | .config/waywall/waywork/keys.lua | 12 | ||||
| -rw-r--r-- | .config/waywall/waywork/modes.lua | 103 | ||||
| -rw-r--r-- | .config/waywall/waywork/processes.lua | 110 | ||||
| -rw-r--r-- | .config/waywall/waywork/scene.lua | 115 |
7 files changed, 705 insertions, 0 deletions
diff --git a/.config/waywall/waywork/LICENSE b/.config/waywall/waywork/LICENSE new file mode 100644 index 0000000..c388b70 --- /dev/null +++ b/.config/waywall/waywork/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Esensats + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/waywall/waywork/README.md b/.config/waywall/waywork/README.md new file mode 100644 index 0000000..5850eb0 --- /dev/null +++ b/.config/waywall/waywork/README.md @@ -0,0 +1,283 @@ +# Waywork + +A framework for building [waywall](https://github.com/tesselslate/waywall) configurations for Minecraft speedrunning setups on Linux/Wayland. + +## Overview + +Waywork provides a structured, modular approach to managing waywall configurations. It abstracts common patterns like resolution switching, scene management, and process orchestration into reusable components, making waywall configs more maintainable and easier to understand. + +## Example config + +For reference you can look at my config which uses waywork here: [Esensats/waywall-config](https://github.com/Esensats/waywall-config) + +## Table of Contents + +<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false} --> + +<!-- code_chunk_output --> + +- [Overview](#overview) +- [Example config](#example-config) +- [Table of Contents](#table-of-contents) +- [Components](#components) + - [Scene Manager (`scene.lua`)](#scene-manager-scenelua) + - [Mode Manager (`modes.lua`)](#mode-manager-modeslua) + - [Key Bindings (`keys.lua`)](#key-bindings-keyslua) + - [Core Utilities (`core.lua`)](#core-utilities-corelua) + - [Process Management (`processes.lua`)](#process-management-processeslua) +- [Migration to Waywork](#migration-to-waywork) + - [Before](#before) + - [After (Waywork)](#after-waywork) +- [Benefits](#benefits) + +<!-- /code_chunk_output --> + +## Components + +### Scene Manager (`scene.lua`) + +Manages visual elements (mirrors, images, text) uniformly with grouping and lifecycle management. + +**Features:** + +- **Unified Management**: Handle mirrors, images, and text objects through a single interface +- **Grouping**: Organize scene objects into logical groups for batch operations +- **Lazy Loading**: Objects are only created when enabled +- **Dynamic Updates**: Modify object properties at runtime + +**Example:** + +```lua +local Scene = require("waywork.scene") +local scene = Scene.SceneManager.new(waywall) + +-- Register scene objects +scene:register("e_counter", { + kind = "mirror", + options = { + src = { x = 1, y = 37, w = 49, h = 9 }, + dst = { x = 1150, y = 300, w = 196, h = 36 } + }, + groups = { "thin" }, +}) + +scene:register("eye_overlay", { + kind = "image", + path = "/path/to/overlay.png", + options = { dst = { x = 30, y = 340, w = 700, h = 400 } }, + groups = { "tall" }, +}) + +-- Enable/disable by group +scene:enable_group("thin", true) -- Enable all "thin" objects +scene:enable_group("tall", false) -- Disable all "tall" objects + +-- Enable/disable individual objects +scene:enable("e_counter", true) +``` + +### Mode Manager (`modes.lua`) + +Orchestrates resolution switching with enter/exit hooks and guard conditions. + +**Features:** + +- **Resolution Management**: Automatic resolution switching with cleanup +- **Lifecycle Hooks**: `on_enter` and `on_exit` callbacks for mode transitions +- **Toggle Guards**: Conditional guards to prevent accidental mode switches (e.g. pressing F3 + F4 to switch gamemode, but accidentally triggering mode transition instead) +- **State Tracking**: Knows which mode is currently active + +**Example:** + +```lua +local Modes = require("waywork.modes") +local ModeManager = Modes.ModeManager.new(waywall) + +ModeManager:define("thin", { + width = 340, + height = 1080, + on_enter = function() + scene:enable_group("thin", true) + end, + on_exit = function() + scene:enable_group("thin", false) + end, +}) + +ModeManager:define("tall", { + width = 384, + height = 16384, + toggle_guard = function() + return not waywall.get_key("F3") -- Prevent toggle during F3 debug + end, + on_enter = function() + scene:enable_group("tall", true) + waywall.set_sensitivity(tall_sens) + end, + on_exit = function() + scene:enable_group("tall", false) + waywall.set_sensitivity(0) + end, +}) + +-- Toggle modes +ModeManager:toggle("thin") -- Switch to thin mode +ModeManager:toggle("thin") -- Switch back to default (0x0) +``` + +### Key Bindings (`keys.lua`) + +Simple utility for building action tables from key mappings. + +**Example:** + +```lua +local Keys = require("waywork.keys") + +local actions = Keys.actions({ + ["*-Alt_L"] = function() + return ModeManager:toggle("thin") + end, + ["*-F4"] = function() + return ModeManager:toggle("tall") + end, + ["Ctrl-E"] = function() + waywall.press_key("ESC") + end, +}) + +config.actions = actions +``` + +### Core Utilities (`core.lua`) + +Low-level utilities used throughout the framework. + +**Features:** + +- **Toggle**: Boolean state management with callbacks +- **Resettable Timeout**: Timeout that cancels previous invocations +- **Table Operations**: Copy and merge utilities + +### Process Management (`processes.lua`) + +Utilities for managing external processes with shell command handling. + +**Features:** + +- **Process Detection**: Check if processes are running using `pgrep` +- **Java JAR Support**: Specialized utilities for launching Java applications +- **Argument Handling**: Proper handling of command arguments as arrays + +**Example:** + +```lua +local P = require("waywork.processes") + +-- Check if a process is running +if P.is_running("firefox") then + print("Firefox is running") +end + +-- Create Java JAR launchers with proper argument handling +local ensure_paceman = P.ensure_java_jar( + waywall, + "/usr/lib/jvm/java-24-openjdk/bin/java", + "/home/user/apps/paceman-tracker.jar", + {"--nogui"} -- arguments as array +)("paceman-tracker\\.jar*") -- process pattern to check + +local ensure_ninjabrain = P.ensure_java_jar( + waywall, + "/usr/lib/jvm/java-24-openjdk/bin/java", + "/home/user/apps/ninjabrain-bot.jar", + {"-Dawt.useSystemAAFontSettings=on"} -- JVM arguments +)("ninjabrain-bot\\.jar") -- process pattern to check + +-- Use in key bindings +["Ctrl-Shift-P"] = function() + ensure_ninjabrain() -- ensure Ninjabrain is running + ensure_paceman() -- ensure Paceman is running +end, +``` + +## Migration to Waywork + +### Before + +```lua +-- Scattered mirror management +local make_mirror = function(options) + local this = nil + return function(enable) + if enable and not this then + this = waywall.mirror(options) + elseif this and not enable then + this:close() + this = nil + end + end +end + +local mirrors = { + e_counter = make_mirror({ src = {...}, dst = {...} }), + thin_pie_all = make_mirror({ src = {...}, dst = {...} }), + -- ... dozens more +} + +-- Manual resolution management +local make_res = function(width, height, enable, disable) + return function() + local active_width, active_height = waywall.active_res() + if active_width == width and active_height == height then + waywall.set_resolution(0, 0) + disable() + else + waywall.set_resolution(width, height) + enable() + end + end +end +``` + +### After (Waywork) + +```lua +local waywall = require("waywall") + +local Scene = require("waywork.scene") +local Modes = require("waywork.modes") +local Keys = require("waywork.keys") + +local scene = Scene.SceneManager.new(waywall) +local ModeManager = Modes.ModeManager.new(waywall) + +-- Clean object registration +scene:register("e_counter", { + kind = "mirror", + options = { src = {...}, dst = {...} }, + groups = { "thin" }, +}) + +-- Declarative mode definitions +ModeManager:define("thin", { + width = 340, + height = 1080, + on_enter = function() scene:enable_group("thin", true) end, + on_exit = function() scene:enable_group("thin", false) end, +}) + +-- Simple key mappings +local actions = Keys.actions({ + ["*-Alt_L"] = function() return ModeManager:toggle("thin") end, +}) +``` + +## Benefits + +1. **Reduced Boilerplate**: Framework handles object lifecycle, resolution management, and state tracking +2. **Better Organization**: Logical grouping of related functionality +3. **Maintainability**: Clear separation of concerns and declarative configuration +4. **Reusability**: Common patterns abstracted into reusable components +5. **Error Prevention**: Toggle guards and proper state management prevent common issues +6. **Cleaner Code**: Focus on what you want to achieve, not how to implement it diff --git a/.config/waywall/waywork/core.lua b/.config/waywall/waywork/core.lua new file mode 100644 index 0000000..f8cd76b --- /dev/null +++ b/.config/waywall/waywork/core.lua @@ -0,0 +1,61 @@ +local core = {} + +--- Create a boolean toggle with on/off callbacks. +function core.toggle(on, off) + local state = false + return { + set = function(v) + if v == state then + return state + end + state = not not v + if state then + on() + else + off() + end + return state + end, + get = function() + return state + end, + toggle = function(self) + return self.set(not state) + end, + } +end + +--- Create a resettable timeout using blocking sleep (matches waywall execution model). +--- Calls `f()` only if this invocation is the last one. +function core.resettable_timeout(sleep, f) + local gen = 0 + return function(delay_ms) + gen = gen + 1 + local my = gen + sleep(delay_ms) + if my == gen then + f() + end + end +end + +--- Shallow table copy +function core.copy(t) + local r = {} + for k, v in pairs(t) do + r[k] = v + end + return r +end + +--- Merge (dst gets missing fields from src) +function core.merge(dst, src) + for k, v in pairs(src or {}) do + if dst[k] == nil then + dst[k] = v + end + end + return dst +end + +return core diff --git a/.config/waywall/waywork/keys.lua b/.config/waywall/waywork/keys.lua new file mode 100644 index 0000000..1bb18e1 --- /dev/null +++ b/.config/waywall/waywork/keys.lua @@ -0,0 +1,12 @@ +local K = {} + +--- Build actions table from simple mapping { [key] = fn, ... } +function K.actions(map) + local t = {} + for k, fn in pairs(map) do + t[k] = fn + end + return t +end + +return K diff --git a/.config/waywall/waywork/modes.lua b/.config/waywall/waywork/modes.lua new file mode 100644 index 0000000..3bf0047 --- /dev/null +++ b/.config/waywall/waywork/modes.lua @@ -0,0 +1,103 @@ +local modes = {} + +--- ModeManager orchestrates resolution toggles and on_enter/on_exit hooks. +local ModeManager = {} +ModeManager.__index = ModeManager + +function ModeManager.new(waywall) + return setmetatable({ ww = waywall, active = nil, defs = {} }, ModeManager) +end + +--- def: { width, height, on_enter=function(), on_exit=function(), toggle_guard=function()->bool } +function ModeManager:define(name, def) + self.defs[name] = def +end + +local function active_res(ww) + local w, h = ww.active_res() + return tonumber(w), tonumber(h) +end + +--- Get the mode definition by name, erroring if not found. +--- @param name string +function ModeManager:_get_def(name) + assert(self.defs[name], "No such mode: " .. tostring(name)) + return self.defs[name] +end + +--- Transition to a mode by name, turning off any previously active mode. +--- If name is nil, turn off any active mode. +--- This does not check toggle_guard. +--- @param name string? +function ModeManager:_transition_to(name) + local function exit_active() + if self.active then + local prev = self:_get_def(self.active) + self.ww.set_resolution(0, 0) + if prev.on_exit then + prev.on_exit() + end + self.active = nil + end + end + + local function enter_mode(_name) + if _name then + local new = self:_get_def(_name) + self.ww.set_resolution(new.width, new.height) + if new.on_enter then + new.on_enter() + end + self.active = _name + end + end + + if name == nil then + exit_active() + return + end + if name == self.active then + -- already active, do nothing + return + end + exit_active() + enter_mode(name) +end + +--- Toggle a mode by name. If it's active, turn it off. If it's inactive, turn it on. +--- If the mode has a toggle_guard and it returns false, do nothing and return false. +--- @param name string +--- @return boolean|nil +function ModeManager:toggle(name) + local ww, def = self.ww, self.defs[name] + if not def then + return + end + if def.toggle_guard and def.toggle_guard() == false then + return false + end + -- local w, h = active_res(ww) + -- if w == def.width and h == def.height then + -- ww.set_resolution(0, 0) + -- if def.on_exit then + -- def.on_exit() + -- end + -- self.active = nil + -- else + -- ww.set_resolution(def.width, def.height) + -- if def.on_enter then + -- def.on_enter() + -- end + -- -- exit previous if different + -- self.active = name + -- end + + if name == self.active then + self:_transition_to(nil) + else + self:_transition_to(name) + end +end + +modes.ModeManager = ModeManager +return modes diff --git a/.config/waywall/waywork/processes.lua b/.config/waywall/waywork/processes.lua new file mode 100644 index 0000000..4ba61e8 --- /dev/null +++ b/.config/waywall/waywork/processes.lua @@ -0,0 +1,110 @@ +local P = {} + +-- Shell-safe quoting: wraps in single quotes, escapes inner quotes +local function shell_quote(str) + return "'" .. tostring(str):gsub("'", "'\"'\"'") .. "'" +end + +-- Build a safe shell command from argv +local function build_cmd(argv) + local quoted = {} + for i, arg in ipairs(argv) do + quoted[i] = shell_quote(arg) + end + return table.concat(quoted, " ") +end + +--- Check if a process matching the given pattern is running. +--- Uses `pgrep -f -- <pattern>` +--- @param pattern string +function P.is_running(pattern) + local cmd = build_cmd({ "pgrep", "-f", "--", pattern }) + local h = io.popen(cmd) + if not h then + return false + end + local r = h:read("*l") + h:close() + return r ~= nil +end + +--- @class waywall +--- @field exec fun(cmd: string): nil + +--- wrapper around waywall.exec +--- @param ww waywall +--- @param argv table array-like table of args +--- @return nil +function P.ww_exec_argv(ww, argv) + -- No quoting needed; execvp-style expects raw argv + assert(type(argv) == "table", "argv must be an array-like table of strings") + for i, v in ipairs(argv) do + assert(type(v) == "string", "argv element must be a string") + end + local cmd = table.concat(argv, " ") + return ww.exec(cmd) +end + +--- Return a function that starts an application when called. +--- @param ww waywall waywall dependency +--- @param app_path string path to the application executable +--- @param args? string[] optional additional arguments to pass to the application +--- @return fun(): nil +function P.start_application(ww, app_path, args) + return function() + local argv = { app_path } + if args then + for _, a in ipairs(args) do + argv[#argv + 1] = a + end + end + P.ww_exec_argv(ww, argv)(ww, argv) + end +end + +--- Ensure an application is running, start it if not. +--- @param ww waywall waywall dependency +--- @param app_path string path to the application executable +--- @param args? string[] optional additional arguments to pass to the application +--- @return fun(pattern: string): fun(): nil +function P.ensure_application(ww, app_path, args) + return function(pattern) + return function() + if not P.is_running(pattern) then + local argv = { app_path } + if args then + for _, a in ipairs(args) do + argv[#argv + 1] = a + end + end + P.ww_exec_argv(ww, argv) + end + end + end +end + +--- Ensure a Java JAR is running, start it if not. +--- @param ww waywall +--- @param java_path string path to java executable +--- @param jar_path string path to the JAR file +--- @param args? string[] optional additional arguments to pass to java +--- @return fun(pattern: string): fun(): nil +function P.ensure_java_jar(ww, java_path, jar_path, args) + return function(pattern) + return function() + if not P.is_running(pattern) then + local argv = { java_path } + if args then + for _, a in ipairs(args) do + argv[#argv + 1] = a + end + end + argv[#argv + 1] = "-jar" + argv[#argv + 1] = jar_path + P.ww_exec_argv(ww, argv) + end + end + end +end + +return P diff --git a/.config/waywall/waywork/scene.lua b/.config/waywall/waywork/scene.lua new file mode 100644 index 0000000..bbb670a --- /dev/null +++ b/.config/waywall/waywork/scene.lua @@ -0,0 +1,115 @@ +local core = require("waywork.core") + +--- SceneManager manages images, mirrors, and text objects uniformly. +local SceneManager = {} +SceneManager.__index = SceneManager + +function SceneManager.new(waywall) + return setmetatable({ ww = waywall, defs = {}, instances = {}, groups = {} }, SceneManager) +end + +--- A scene object definition. +--- @class SceneDef +--- @field kind "mirror" | "image" | "text" +--- @field options table +--- @field path? string (for images) +--- @field text? string (for text) +--- @field groups string[] +--- @field enabled_by_default? boolean + +--- Register a scene object by name. +--- @param name string +--- @param def SceneDef +function SceneManager:register(name, def) + self.defs[name] = core.copy(def) + if def.groups then + for _, g in ipairs(def.groups) do + self.groups[g] = self.groups[g] or {} + table.insert(self.groups[g], name) + end + end + if def.enabled_by_default then + self:enable(name, true) + end +end + +function SceneManager:_create(def) + if def.kind == "mirror" then + return self.ww.mirror(def.options) + elseif def.kind == "image" then + return self.ww.image(def.path, def.options) + elseif def.kind == "text" then + return self.ww.text(def.text, def.options) + end +end + +function SceneManager:_ensure(name, enable) + local inst = self.instances[name] + if enable and not inst then + inst = self:_create(self.defs[name]) + self.instances[name] = inst + elseif not enable and inst then + inst:close() + self.instances[name] = nil + end +end + +--- Enable or disable a registered scene object by name. +--- @param name string +--- @param on boolean +function SceneManager:enable(name, on) + if not self.defs[name] then + return + end + self:_ensure(name, on) +end + +--- Disable a registered scene object by name. +--- @param name string +function SceneManager:disable(name) + self:enable(name, false) +end + +--- Enable or disable all scene objects in a group. +--- @param group string +--- @param on boolean +function SceneManager:enable_group(group, on) + for _, name in ipairs(self.groups[group] or {}) do + self:enable(name, on) + end +end + +--- Apply a predicate function to all registered scene objects. +--- +--- Example: +--- ```lua +--- scene:apply(function(name, def) +--- return def.kind == "mirror" -- enable all mirrors +--- end) +--- ``` +--- @param predicate fun(name: string, def: table): boolean +function SceneManager:apply(predicate) + for name, def in pairs(self.defs) do + self:enable(name, predicate(name, def)) + end +end + +--- Update the destination rectangle of a registered scene object. +--- @param name string +--- @param new_dst table { x=number, y=number, w=number, h=number } +SceneManager.update_dst = function(self, name, new_dst) + local def = self.defs[name] + if not def then + return + end + if def.options then + def.options.dst = core.copy(new_dst) + end + local inst = self.instances[name] + if inst then + inst:close() + self.instances[name] = self:_create(def) + end +end + +return { SceneManager = SceneManager } |
