summaryrefslogtreecommitdiff
path: root/.config/waywall
diff options
context:
space:
mode:
Diffstat (limited to '.config/waywall')
-rw-r--r--.config/waywall/flake.lock61
-rw-r--r--.config/waywall/flake.nix57
-rw-r--r--.config/waywall/init.lua224
-rw-r--r--.config/waywall/resources/bubble_pie.pngbin0 -> 2436 bytes
-rw-r--r--.config/waywall/resources/measuring_overlay.pngbin0 -> 11882 bytes
-rw-r--r--.config/waywall/resources/tall_bg.pngbin0 -> 41553 bytes
-rw-r--r--.config/waywall/resources/thin_bg.pngbin0 -> 75567 bytes
-rw-r--r--.config/waywall/resources/wide_bg.pngbin0 -> 68803 bytes
-rw-r--r--.config/waywall/waywork/LICENSE21
-rw-r--r--.config/waywall/waywork/README.md283
-rw-r--r--.config/waywall/waywork/core.lua61
-rw-r--r--.config/waywall/waywork/keys.lua12
-rw-r--r--.config/waywall/waywork/modes.lua103
-rw-r--r--.config/waywall/waywork/processes.lua110
-rw-r--r--.config/waywall/waywork/scene.lua115
15 files changed, 1047 insertions, 0 deletions
diff --git a/.config/waywall/flake.lock b/.config/waywall/flake.lock
new file mode 100644
index 0000000..128df5b
--- /dev/null
+++ b/.config/waywall/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1763966396,
+ "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/.config/waywall/flake.nix b/.config/waywall/flake.nix
new file mode 100644
index 0000000..a463acc
--- /dev/null
+++ b/.config/waywall/flake.nix
@@ -0,0 +1,57 @@
+{
+ description = "Ninjabrain Bot - Minecraft speedrunning utility";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ ninjabrain-bot = pkgs.stdenv.mkDerivation {
+ pname = "ninjabrain-bot";
+ version = "1.5.1";
+
+ src = pkgs.fetchurl {
+ url = "https://github.com/Ninjabrain1/Ninjabrain-Bot/releases/download/1.5.1/Ninjabrain-Bot-1.5.1.jar";
+ sha256 = "sha256-Rxu9A2EiTr69fLBUImRv+RLC2LmosawIDyDPIaRcrdw=";
+ };
+
+ dontUnpack = true;
+
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+
+ installPhase = ''
+ mkdir -p $out/bin $out/share/ninjabrain-bot
+ cp $src $out/share/ninjabrain-bot/ninjabrain-bot.jar
+
+ makeWrapper ${pkgs.jre8}/bin/java $out/bin/ninjabrain-bot \
+ --add-flags "-Dawt.useSystemAAFontSettings=on" \
+ --add-flags "-jar $out/share/ninjabrain-bot/ninjabrain-bot.jar" \
+ --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [
+ pkgs.libxkbcommon
+ pkgs.xorg.libX11
+ pkgs.xorg.libXt
+ ]}
+ '';
+
+ meta = with pkgs.lib; {
+ description = "Stronghold calculator for Minecraft speedrunning";
+ homepage = "https://github.com/Ninjabrain1/Ninjabrain-Bot";
+ license = licenses.mit;
+ platforms = platforms.linux;
+ };
+ };
+ in
+ {
+ packages.default = ninjabrain-bot;
+ apps.default = {
+ type = "app";
+ program = "${ninjabrain-bot}/bin/ninjabrain-bot";
+ };
+ }
+ );
+}
diff --git a/.config/waywall/init.lua b/.config/waywall/init.lua
new file mode 100644
index 0000000..853ac64
--- /dev/null
+++ b/.config/waywall/init.lua
@@ -0,0 +1,224 @@
+local waywall = require("waywall")
+local helpers = require("waywall.helpers")
+
+local Scene = require("waywork.scene")
+local Modes = require("waywork.modes")
+local Keys = require("waywork.keys")
+local Processes = require("waywork.processes")
+
+local scene = Scene.SceneManager.new(waywall)
+local ModeManager = Modes.ModeManager.new(waywall)
+
+local waywall_config_path = os.getenv("HOME") .. "/.config/waywall"
+
+--[[
+celeste menu colors:
+
+light pink: ff92b1
+lavender: 9768e4
+med green: 419462
+pale yellow: ffff99
+gray blue: 759cb2
+dark gray blue: 36527c
+dark gray: 3b566b
+cyan: 53cfde
+brick red: ba5358
+med yellow: fff672
+royal purple: 8d24ed
+pale pink: eb82ff
+sky blue: 54b0ff
+cornflower blue: 606de7
+gold: ffea42
+med blue: 448ede
+deep red: 8a2939
+brightish red: f53c4c
+
+--]]
+
+local bg_color = "#ffffff"
+
+local pie_colors = {
+ entities = { pie = "#e446c4", text = "#e145c2", out = "#f225fc" },
+ unspecified = { pie = "#46ce66", text = "#45cc65", out = "#56f440" },
+ blockEntities = { pie = "#ec6e4e", text = "#e96d4d", out = "#f48769" },
+ destroyProgress = { pie = "#cc6c46", text = "#ca6b45", out = "#c78b56" },
+ mob_spawner = { pie = "#4ee4cc", text = "#4de1ca", out = "#63f9fb" },
+ chest = { pie = "#c66ee4", text = "#c46de1", out = "#e75dfc" },
+}
+
+local normal_sens = 12.800000599064097
+local tall_sens = 0.8634803836976988
+
+local pie_dst = { x = 1200, y = 400, w = 340, h = 340 }
+local percent_dst = { x = 1280, y = 800, w = 34 * 6, h = 25 * 6 }
+local eye_dst = { x = 30, y = 340, w = 700, h = 400 }
+
+local f3_root = { x = 1200, y = 150 }
+local f3_scale = 5
+local f3_text_color = "#48106e"
+
+function add_f3_scene(name, row, col, len, groups)
+ scene:register(name, {
+ kind = "mirror",
+ options = {
+ src = { x = col * 6 + 1, y = row * 9 + 1, w = len * 6, h = 9 },
+ dst = { x = f3_root.x, y = f3_root.y, w = len * 6 * f3_scale, h = 9 * f3_scale },
+ color_key = {
+ input = "#dddddd",
+ output = f3_text_color,
+ },
+ depth = 1,
+ },
+ groups = groups,
+ })
+ f3_root.y = f3_root.y + 9 * f3_scale
+end
+
+add_f3_scene("c_counter", 3, 0, 11, { "thin", "tall" })
+add_f3_scene("e_counter", 4, 0, 8, { "thin", "tall" })
+
+for _, name in ipairs({ "wide", "thin", "tall" }) do
+ scene:register(name .. "_bg", {
+ kind = "image",
+ path = waywall_config_path .. "/resources/" .. name .. "_bg.png",
+ options = {
+ dst = { x = 0, y = 0, w = 1920, h = 1080 },
+ },
+ groups = { name },
+ })
+end
+
+scene:register("bubble", {
+ kind = "image",
+ path = waywall_config_path .. "/resources/bubble_pie.png",
+ options = {
+ dst = pie_dst,
+ depth = -1
+ },
+ groups = { "thin", "tall" },
+})
+
+for name, colors in pairs(pie_colors) do
+ scene:register("thin_pie_" .. name, {
+ kind = "mirror",
+ options = {
+ src = { x = 9, y = 680, w = 321, h = 160 },
+ dst = pie_dst,
+ color_key = { input = colors.pie, output = colors.out },
+ },
+ groups = { "thin" },
+ })
+
+ scene:register("tall_pie_" .. name, {
+ kind = "mirror",
+ options = {
+ src = { x = 9, y = 15984, w = 321, h = 160 },
+ dst = pie_dst,
+ color_key = { input = colors.pie, output = colors.out },
+ },
+ groups = { "tall" },
+ })
+ scene:register("thin_percent_" .. name, {
+ kind = "mirror",
+ options = {
+ src = { x = 247, y = 859, w = 34, h = 25 },
+ dst = percent_dst,
+ color_key = { input = colors.text, output = colors.out },
+ depth = 1,
+ },
+ groups = { "thin" },
+ })
+ scene:register("tall_percent_" .. name, {
+ kind = "mirror",
+ options = {
+ src = { x = 247, y = 16163, w = 34, h = 25 },
+ dst = percent_dst,
+ color_key = { input = colors.text, output = colors.out },
+ depth = 1,
+ },
+ groups = { "tall" },
+ })
+end
+
+scene:register("eye_measure", {
+ kind = "mirror",
+ options = {
+ src = { x = 140, y = 7902, w = 60, h = 580 },
+ dst = eye_dst,
+ },
+ groups = { "tall" },
+})
+
+scene:register("eye_overlay", {
+ kind = "image",
+ path = waywall_config_path .. "/resources/measuring_overlay.png",
+ options = { dst = eye_dst },
+ groups = { "tall" },
+})
+
+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 = 340,
+ height = 16384,
+ 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(normal_sens)
+ end,
+ toggle_guard = function() return not waywall.get_key("F3") end,
+})
+
+ModeManager:define("wide", {
+ width = 1920,
+ height = 300,
+ on_enter = function() scene:enable_group("wide", true) end,
+ on_exit = function() scene:enable_group("wide", false) end,
+})
+
+local ninbot_path = waywall_config_path .. "/result/bin/ninjabrain-bot"
+local ensure_ninbot = Processes.ensure_application(waywall, ninbot_path)("[Nn]injabrain.*\\.jar")
+
+return {
+ input = {
+ layout = "us",
+ variant = "colemak_dh",
+ repeat_rate = 40,
+ repeat_delay = 300,
+ sensitivity = normal_sens,
+ confine_pointer = false,
+ remaps = {
+ ["MB5"] = "F3",
+ ["Enter"] = "Esc",
+ },
+ },
+ theme = {
+ background = bg_color,
+ ninb_anchor = "bottomright",
+ ninb_opacity = 1.0,
+ },
+ experimental = {
+ debug = false,
+ jit = false,
+ tearing = false,
+ scene_add_text = true,
+ },
+ actions = Keys.actions({
+ ["*-F2"] = function() return ModeManager:toggle("thin") end,
+ ["*-F4"] = function() return ModeManager:toggle("tall") end,
+ ["*-apostrophe"] = function() return ModeManager:toggle("wide") end,
+ ["*-Alt_L"] = function()
+ ensure_ninbot()
+ helpers.toggle_floating()
+ end,
+ ["*-Shift-f"] = waywall.toggle_fullscreen,
+ }),
+}
diff --git a/.config/waywall/resources/bubble_pie.png b/.config/waywall/resources/bubble_pie.png
new file mode 100644
index 0000000..b721bbf
--- /dev/null
+++ b/.config/waywall/resources/bubble_pie.png
Binary files differ
diff --git a/.config/waywall/resources/measuring_overlay.png b/.config/waywall/resources/measuring_overlay.png
new file mode 100644
index 0000000..eb3a666
--- /dev/null
+++ b/.config/waywall/resources/measuring_overlay.png
Binary files differ
diff --git a/.config/waywall/resources/tall_bg.png b/.config/waywall/resources/tall_bg.png
new file mode 100644
index 0000000..873ad11
--- /dev/null
+++ b/.config/waywall/resources/tall_bg.png
Binary files differ
diff --git a/.config/waywall/resources/thin_bg.png b/.config/waywall/resources/thin_bg.png
new file mode 100644
index 0000000..8e8e617
--- /dev/null
+++ b/.config/waywall/resources/thin_bg.png
Binary files differ
diff --git a/.config/waywall/resources/wide_bg.png b/.config/waywall/resources/wide_bg.png
new file mode 100644
index 0000000..e353758
--- /dev/null
+++ b/.config/waywall/resources/wide_bg.png
Binary files differ
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 }