Diff
diff --git a/Cargo.lock b/Cargo.lock
index 35ac832..964d511 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,6 +38,16 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -100,6 +110,18 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "env_home"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
+
[[package]]
name = "errno"
version = "0.3.14"
@@ -193,12 +215,74 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+[[package]]
+name = "lua-src"
+version = "547.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "luajit-src"
+version = "210.5.12+a4f56a4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
+dependencies = [
+ "cc",
+ "which",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "mlua"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0"
+dependencies = [
+ "bstr",
+ "either",
+ "mlua-sys",
+ "num-traits",
+ "parking_lot",
+ "rustc-hash",
+ "rustversion",
+]
+
+[[package]]
+name = "mlua-sys"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "lua-src",
+ "luajit-src",
+ "pkg-config",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -227,12 +311,36 @@ dependencies = [
"anyhow",
"chrono",
"dirs",
+ "mlua",
"ron",
"serde",
"x11",
"x11rb",
]
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -257,6 +365,15 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "redox_users"
version = "0.4.6"
@@ -280,6 +397,12 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustix"
version = "1.1.2"
@@ -299,6 +422,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -335,6 +464,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
[[package]]
name = "syn"
version = "2.0.107"
@@ -437,6 +572,18 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "which"
+version = "7.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
+dependencies = [
+ "either",
+ "env_home",
+ "rustix",
+ "winsafe",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -571,6 +718,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+[[package]]
+name = "winsafe"
+version = "0.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
[[package]]
name = "x11"
version = "2.21.0"
diff --git a/Cargo.toml b/Cargo.toml
index 813d17e..0917b3a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,3 +19,4 @@ chrono = "0.4"
dirs = "5.0"
serde = { version = "1.0", features = ["derive"] }
ron = "0.8"
+mlua = { version = "0.10", features = ["lua54", "vendored"] }
diff --git a/UPDATES.md b/UPDATES.md
new file mode 100644
index 0000000..1309c84
--- /dev/null
+++ b/UPDATES.md
@@ -0,0 +1,407 @@
+# OXWM Recent Updates
+
+## Lua Configuration Support (Latest)
+
+OXWM now supports **Lua-based configuration** as an alternative to the compiled Rust configuration. This is a major update that makes the window manager much more user-friendly and easier to customize.
+
+### Key Features
+
+#### 1. **Dynamic Configuration - No Compilation Required**
+- Edit your config file and reload with `Mod+Shift+R` - changes apply instantly
+- No need to recompile the entire window manager for config changes
+- Configuration file located at `~/.config/oxwm/config.lua`
+
+#### 2. **Full Feature Parity**
+The Lua configuration supports all features previously available in Rust config:
+- Window appearance (borders, gaps, fonts)
+- Keybindings (single keys and keychords)
+- Layout management and custom symbols
+- Status bar configuration
+- Workspace tags
+- Autostart commands
+- Color schemes
+
+#### 3. **LSP Support & Autocomplete**
+Config files include comprehensive type annotations for Lua language servers:
+- Full autocomplete for all configuration options
+- Type checking to catch errors before runtime
+- Hover documentation for all fields
+- IntelliSense support in modern editors (VS Code, Neovim, etc.)
+
+### Configuration Structure
+
+```lua
+---@type Config
+config = {
+ -- Appearance
+ border_width = 2,
+ border_focused = 0x6dade3,
+ border_unfocused = 0xbbbbbb,
+ font = "monospace:style=Bold:size=10",
+
+ -- Window gaps
+ gaps_enabled = true,
+ gap_inner_horizontal = 5,
+ gap_inner_vertical = 5,
+ gap_outer_horizontal = 5,
+ gap_outer_vertical = 5,
+
+ -- Basics
+ modkey = "Mod4",
+ terminal = "st",
+
+ -- Workspace tags
+ tags = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
+
+ -- Keybindings (see below for details)
+ keybindings = { ... },
+
+ -- Status bar blocks (see below for details)
+ status_blocks = { ... },
+
+ -- Color schemes
+ scheme_normal = { ... },
+ scheme_occupied = { ... },
+ scheme_selected = { ... },
+
+ -- Autostart commands
+ autostart = {
+ "picom -b",
+ "nitrogen --restore &",
+ },
+}
+```
+
+### Keybindings
+
+#### Single Key Bindings
+```lua
+{
+ modifiers = {"Mod", "Shift"},
+ key = "Return",
+ action = "Spawn",
+ arg = "alacritty"
+}
+```
+
+#### Keychords (Multi-key Sequences)
+```lua
+{
+ keys = {
+ {modifiers = {"Mod4"}, key = "Space"},
+ {modifiers = {}, key = "T"}
+ },
+ action = "Spawn",
+ arg = "st"
+}
+```
+
+Press Escape to cancel any in-progress keychord.
+
+#### Available Modifiers
+- `"Mod"` - Replaced with your configured modkey
+- `"Mod1"` - Alt key
+- `"Mod4"` - Super/Windows key
+- `"Shift"` - Shift key
+- `"Control"` - Control key
+- `"Mod2"`, `"Mod3"`, `"Mod5"` - Additional modifiers
+
+#### Available Actions
+- `"Spawn"` - Launch a program (arg: string or string[])
+- `"KillClient"` - Close focused window
+- `"FocusStack"` - Focus next/prev in stack (arg: 1 or -1)
+- `"FocusDirection"` - Focus by direction (arg: 0=up, 1=down, 2=left, 3=right)
+- `"SwapDirection"` - Swap window by direction (arg: 0=up, 1=down, 2=left, 3=right)
+- `"Quit"` - Exit window manager
+- `"Restart"` - Restart and reload config
+- `"Recompile"` - Recompile and restart (for Rust config)
+- `"ViewTag"` - Switch to tag (arg: tag index)
+- `"MoveToTag"` - Move window to tag (arg: tag index)
+- `"ToggleGaps"` - Toggle gaps on/off
+- `"ToggleFullScreen"` - Toggle fullscreen mode
+- `"ToggleFloating"` - Toggle floating mode
+- `"ChangeLayout"` - Switch to specific layout (arg: layout name)
+- `"CycleLayout"` - Cycle through layouts
+- `"FocusMonitor"` - Focus monitor (arg: 1 or -1)
+- `"SmartMoveWin"` - Smart window movement
+- `"ExchangeClient"` - Exchange window positions
+
+### Status Bar Configuration
+
+Status blocks support various command types for displaying system information:
+
+```lua
+status_blocks = {
+ -- RAM usage
+ {
+ format = "Ram: {used}/{total} GB",
+ command = "Ram",
+ interval_secs = 5,
+ color = 0x7aa2f7,
+ underline = true
+ },
+
+ -- Date/Time with custom format
+ {
+ format = "{}",
+ command = "DateTime",
+ command_arg = "%a, %b %d - %-I:%M %P",
+ interval_secs = 1,
+ color = 0x0db9d7,
+ underline = true
+ },
+
+ -- Custom shell command
+ {
+ format = "Kernel: {}",
+ command = "Shell",
+ command_arg = "uname -r",
+ interval_secs = 999999999,
+ color = 0xf7768e,
+ underline = true
+ },
+
+ -- Static text (separator)
+ {
+ format = " │ ",
+ command = "Static",
+ interval_secs = 999999999,
+ color = 0xa9b1d6,
+ underline = false
+ },
+
+ -- Battery status
+ {
+ format = "{}",
+ command = "Battery",
+ interval_secs = 30,
+ color = 0x9ece6a,
+ underline = true,
+ battery_formats = {
+ charging = "⚡ {}%",
+ discharging = "🔋 {}%",
+ full = "✓ {}%"
+ }
+ },
+}
+```
+
+#### Available Status Block Commands
+- `"Ram"` - Shows RAM usage (no command_arg needed)
+- `"DateTime"` - Shows date/time with strftime format in command_arg
+- `"Shell"` - Runs shell command from command_arg
+- `"Static"` - Static text (format field determines what's shown)
+- `"Battery"` - Shows battery status (requires battery_formats table)
+
+### Layout Symbols
+
+Customize how layouts appear in the status bar:
+
+```lua
+layout_symbols = {
+ {name = "tiling", symbol = "[T]"},
+ {name = "normie", symbol = "[F]"},
+}
+```
+
+### Color Schemes
+
+Define color schemes for tag indicators in the status bar:
+
+```lua
+scheme_normal = {
+ foreground = 0xbbbbbb,
+ background = 0x1a1b26,
+ underline = 0x444444
+},
+scheme_occupied = {
+ foreground = 0x0db9d7,
+ background = 0x1a1b26,
+ underline = 0x0db9d7
+},
+scheme_selected = {
+ foreground = 0x0db9d7,
+ background = 0x1a1b26,
+ underline = 0xad8ee6
+}
+```
+
+### Autostart Commands
+
+Commands to run when the window manager starts:
+
+```lua
+autostart = {
+ "picom -b",
+ "nitrogen --restore &",
+ "dunst &",
+}
+```
+
+### Advanced Usage
+
+You can use Lua's full programming capabilities for dynamic configuration:
+
+```lua
+-- Generate keybindings programmatically
+for i = 1, 9 do
+ table.insert(config.keybindings, {
+ modifiers = {"Mod"},
+ key = tostring(i),
+ action = "ViewTag",
+ arg = i - 1
+ })
+end
+
+-- Create helper functions
+local function create_spawn_binding(mods, key, cmd)
+ return {modifiers = mods, key = key, action = "Spawn", arg = cmd}
+end
+
+table.insert(config.keybindings,
+ create_spawn_binding({"Mod"}, "Return", terminal))
+```
+
+### Migration from Rust Config
+
+For existing users with Rust-based configurations:
+1. Your old Rust config still works - no breaking changes
+2. To migrate to Lua, run OXWM and it will generate `~/.config/oxwm/config.lua`
+3. Customize the generated file to match your preferences
+4. Reload with `Mod+Shift+R` to apply changes
+
+### Technical Implementation
+
+- Configuration parsing via `mlua` (Lua 5.4)
+- Full deserialization into Rust types with `serde`
+- Hot-reloading on `Restart` action
+- Comprehensive error reporting for config issues
+- Type-safe validation of all configuration values
+
+### Files
+
+- Template: `templates/config.lua` - Default configuration template
+- User config: `~/.config/oxwm/config.lua` - User's configuration file
+- Test config: `resources/test-config.lua` - Configuration for Xephyr testing
+
+---
+
+## Documentation Suggestions
+
+### New Pages to Create
+
+1. **"Lua Configuration Guide"** - Complete guide to Lua config
+ - Getting started
+ - Configuration structure
+ - Examples for common use cases
+ - Tips and best practices
+
+2. **"Keybindings Reference"** - Detailed keybinding documentation
+ - How to define single key bindings
+ - How to create keychords
+ - List of all available actions with examples
+ - Modifier key reference
+
+3. **"Status Bar Configuration"** - Status bar setup guide
+ - Available command types
+ - Format string syntax
+ - Custom shell commands
+ - Battery configuration
+ - Creating custom blocks
+
+### Pages to Update
+
+1. **Installation/Quickstart**
+ - Mention Lua configuration as the recommended approach
+ - Add note that no recompilation needed for config changes
+ - Update first-run instructions
+
+2. **Configuration Page** (if exists)
+ - Add prominent notice about Lua support
+ - Deprecation notice for Rust config (if planned)
+ - Link to new Lua configuration guide
+
+3. **Keybindings Page** (if exists)
+ - Update with Lua syntax
+ - Add keychord examples
+ - Update modifier key documentation
+
+### Highlights for Documentation
+
+**Key Selling Points:**
+- "Edit and reload instantly - no compilation required"
+- "LSP-powered autocomplete for config editing"
+- "Full Lua programming support for dynamic configs"
+- "Keychord support for complex key combinations"
+
+**Common Questions to Address:**
+- How do I reload my config? (`Mod+Shift+R`)
+- Where is my config file? (`~/.config/oxwm/config.lua`)
+- Can I still use the Rust config? (Yes, for now)
+- How do I see config errors? (Check terminal output when starting OXWM)
+
+### Example Snippets for Docs
+
+#### Quick Config Example
+```lua
+-- Minimal working config
+config = {
+ modkey = "Mod4",
+ terminal = "alacritty",
+ tags = {"web", "code", "term"},
+ border_width = 2,
+ border_focused = 0x89b4fa,
+ border_unfocused = 0x45475a,
+ keybindings = {
+ {modifiers = {"Mod"}, key = "Return", action = "Spawn", arg = "alacritty"},
+ {modifiers = {"Mod"}, key = "Q", action = "KillClient"},
+ {modifiers = {"Mod", "Shift"}, key = "Q", action = "Quit"},
+ }
+}
+```
+
+#### Advanced Keychord Example
+```lua
+-- Open application menu with Mod+Space, then choose app
+keybindings = {
+ -- Mod+Space, then B -> browser
+ {
+ keys = {
+ {modifiers = {"Mod4"}, key = "Space"},
+ {modifiers = {}, key = "B"}
+ },
+ action = "Spawn",
+ arg = "firefox"
+ },
+ -- Mod+Space, then T -> terminal
+ {
+ keys = {
+ {modifiers = {"Mod4"}, key = "Space"},
+ {modifiers = {}, key = "T"}
+ },
+ action = "Spawn",
+ arg = "alacritty"
+ },
+}
+```
+
+---
+
+## Other Recent Updates
+
+### Autostart Support
+- Added `autostart` field to configuration
+- Commands run when window manager starts
+- Useful for launching compositors, wallpaper setters, notification daemons, etc.
+
+### Fullscreen Fixes
+- Fixed issue where fullscreen windows weren't properly applying geometries
+- Border handling corrected for fullscreen mode
+
+### Layout Updates
+- Improved layout switching behavior
+- Better handling of layout symbols in status bar
+
+---
+
+**Last Updated:** 2025-11-07
diff --git a/justfile b/justfile
index 4c8528d..0cbb0a3 100644
--- a/justfile
+++ b/justfile
@@ -10,7 +10,7 @@ install: build
uninstall:
rm -f /usr/bin/oxwm
@echo "✓ oxwm uninstalled"
- @echo " Your config at ~/.config/oxwm/config.ron is preserved"
+ @echo " Your config at ~/.config/oxwm/ is preserved"
clean:
cargo clean
@@ -19,23 +19,23 @@ test-clean:
pkill Xephyr || true
rm -rf ~/.config/oxwm
Xephyr -screen 1280x800 :1 & sleep 1
- DISPLAY=:1 cargo run --release -- --config resources/test-config.ron
+ DISPLAY=:1 cargo run --release -- --config resources/test-config.lua
test:
pkill Xephyr || true
Xephyr -screen 1280x800 :1 & sleep 1
- DISPLAY=:1 cargo run --release -- --config resources/test-config.ron
+ DISPLAY=:1 cargo run --release -- --config resources/test-config.lua
test-multimon:
pkill Xephyr || true
Xephyr +xinerama -screen 640x480 -screen 640x480 :1 & sleep 1
- DISPLAY=:1 cargo run --release -- --config resources/test-config.ron
+ DISPLAY=:1 cargo run --release -- --config resources/test-config.lua
init:
cargo run --release -- --init
edit:
- $EDITOR ~/.config/oxwm/config.ron
+ $EDITOR ~/.config/oxwm/config.lua
check:
cargo clippy -- -W clippy::all
diff --git a/resources/test-config.lua b/resources/test-config.lua
new file mode 100644
index 0000000..28524c2
--- /dev/null
+++ b/resources/test-config.lua
@@ -0,0 +1,260 @@
+-- OXWM Test Configuration File (Lua)
+-- This config uses Mod1 (Alt) as the modkey for testing in Xephyr
+
+---@class LayoutSymbol
+---@field name string The internal layout name (e.g., "tiling", "normie")
+---@field symbol string The display symbol for the layout (e.g., "[T]", "[F]")
+
+---@class KeyBinding
+---@field modifiers string[] List of modifiers: "Mod", "Mod1"-"Mod5", "Shift", "Control"
+---@field key string The key name (e.g., "Return", "Q", "1", "Space")
+---@field action string Action to perform (see below for list)
+---@field arg? string|number|string[] Optional argument for the action
+
+---@class KeyChord
+---@field keys {modifiers: string[], key: string}[] Sequence of keys to press
+---@field action string Action to perform when chord completes
+---@field arg? string|number|string[] Optional argument for the action
+
+---@class StatusBlock
+---@field format string Display format with {} placeholders
+---@field command string Command type: "Ram", "DateTime", "Shell", "Static", "Battery"
+---@field command_arg? string Argument for command (shell command, date format, etc.)
+---@field interval_secs number Update interval in seconds
+---@field color number Color as hex number (e.g., 0xff0000 for red)
+---@field underline boolean Whether to show underline
+---@field battery_formats? {charging: string, discharging: string, full: string} Battery format strings
+
+---@class ColorScheme
+---@field foreground number Foreground color (hex)
+---@field background number Background color (hex)
+---@field underline number Underline color (hex)
+
+---@class Config
+---@field border_width number Width of window borders in pixels
+---@field border_focused number Color for focused window border (hex)
+---@field border_unfocused number Color for unfocused window border (hex)
+---@field font string Font specification (e.g., "monospace:style=Bold:size=10")
+---@field gaps_enabled boolean Whether gaps are enabled
+---@field gap_inner_horizontal number Inner horizontal gap size
+---@field gap_inner_vertical number Inner vertical gap size
+---@field gap_outer_horizontal number Outer horizontal gap size
+---@field gap_outer_vertical number Outer vertical gap size
+---@field modkey string Main modifier key (e.g., "Mod4" for Super)
+---@field terminal string Terminal emulator command
+---@field tags string[] List of workspace tag names
+---@field layout_symbols LayoutSymbol[] Custom layout symbols
+---@field keybindings (KeyBinding|KeyChord)[] List of keybindings
+---@field status_blocks StatusBlock[] Status bar configuration blocks
+---@field scheme_normal ColorScheme Color scheme for normal tags
+---@field scheme_occupied ColorScheme Color scheme for occupied tags
+---@field scheme_selected ColorScheme Color scheme for selected tag
+---@field autostart string[] Commands to run on startup
+
+-- Available Actions:
+-- "Spawn" - Launch a program (arg: string or string[])
+-- "KillClient" - Close focused window
+-- "FocusStack" - Focus next/prev in stack (arg: 1 or -1)
+-- "FocusDirection" - Focus by direction (arg: 0=up, 1=down, 2=left, 3=right)
+-- "SwapDirection" - Swap window by direction (arg: 0=up, 1=down, 2=left, 3=right)
+-- "Quit" - Exit window manager
+-- "Restart" - Restart and reload config
+-- "Recompile" - Recompile and restart
+-- "ViewTag" - Switch to tag (arg: tag index)
+-- "MoveToTag" - Move window to tag (arg: tag index)
+-- "ToggleGaps" - Toggle gaps on/off
+-- "ToggleFullScreen" - Toggle fullscreen mode
+-- "ToggleFloating" - Toggle floating mode
+-- "ChangeLayout" - Switch to specific layout (arg: layout name)
+-- "CycleLayout" - Cycle through layouts
+-- "FocusMonitor" - Focus monitor (arg: 1 or -1)
+-- "SmartMoveWin" - Smart window movement
+-- "ExchangeClient" - Exchange window positions
+
+-- Available Modifiers:
+-- "Mod" - Replaced with configured modkey
+-- "Mod1" - Alt key
+-- "Mod4" - Super/Windows key
+-- "Shift" - Shift key
+-- "Control" - Control key
+-- "Mod2", "Mod3", "Mod5" - Additional modifiers
+
+-- Define variables for easy customization
+local terminal = "st"
+local modkey = "Mod1" -- Alt key for Xephyr testing
+local secondary_modkey = "Control"
+
+-- Color palette (Tokyo Night theme)
+local colors = {
+ blue = 0x6dade3,
+ grey = 0xbbbbbb,
+ green = 0x9ece6a,
+ red = 0xf7768e,
+ cyan = 0x0db9d7,
+ purple = 0xad8ee6,
+ lavender = 0xa9b1d6,
+ bg = 0x1a1b26,
+ fg = 0xbbbbbb,
+ light_blue = 0x7aa2f7,
+}
+
+-- Main configuration table
+---@type Config
+config = {
+ -- Appearance
+ border_width = 2,
+ border_focused = colors.blue,
+ border_unfocused = colors.grey,
+ font = "monospace:style=Bold:size=10",
+
+ -- Window gaps
+ gaps_enabled = true,
+ gap_inner_horizontal = 5,
+ gap_inner_vertical = 5,
+ gap_outer_horizontal = 5,
+ gap_outer_vertical = 5,
+
+ -- Basics
+ modkey = modkey,
+ terminal = terminal,
+
+ -- Workspace tags
+ tags = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
+
+ -- Layout symbol overrides
+ layout_symbols = {
+ {name = "tiling", symbol = "[T]"},
+ {name = "normie", symbol = "[F]"},
+ },
+
+ -- Keybindings
+ -- Using Mod1 (Alt) for Xephyr testing so it doesn't conflict with host WM
+ keybindings = {
+ -- Basic window management
+ {modifiers = {"Mod"}, key = "Return", action = "Spawn", arg = terminal},
+ {modifiers = {"Mod"}, key = "D", action = "Spawn", arg = {"sh", "-c", "dmenu_run -l 10"}},
+ {modifiers = {"Mod"}, key = "S", action = "Spawn", arg = {"sh", "-c", "maim -s | xclip -selection clipboard -t image/png"}},
+ {modifiers = {"Mod"}, key = "Q", action = "KillClient"},
+ {modifiers = {"Mod", "Shift"}, key = "F", action = "ToggleFullScreen"},
+ {modifiers = {"Mod", "Shift"}, key = "Space", action = "ToggleFloating"},
+
+ -- Layout management
+ {modifiers = {"Mod"}, key = "F", action = "ChangeLayout", arg = "normie"},
+ {modifiers = {"Mod"}, key = "C", action = "ChangeLayout", arg = "tiling"},
+ {modifiers = {secondary_modkey}, key = "N", action = "CycleLayout"},
+ {modifiers = {"Mod"}, key = "A", action = "ToggleGaps"},
+
+ -- WM control
+ {modifiers = {"Mod", "Shift"}, key = "Q", action = "Quit"},
+ {modifiers = {"Mod", "Shift"}, key = "R", action = "Restart"},
+
+ -- Focus movement (vim-style hjkl)
+ {modifiers = {"Mod"}, key = "H", action = "FocusDirection", arg = 2}, -- left
+ {modifiers = {"Mod"}, key = "J", action = "FocusDirection", arg = 1}, -- down
+ {modifiers = {"Mod"}, key = "K", action = "FocusDirection", arg = 0}, -- up
+ {modifiers = {"Mod"}, key = "L", action = "FocusDirection", arg = 3}, -- right
+
+ -- Window swapping (vim-style hjkl)
+ {modifiers = {"Mod", "Shift"}, key = "H", action = "SwapDirection", arg = 2}, -- left
+ {modifiers = {"Mod", "Shift"}, key = "J", action = "SwapDirection", arg = 1}, -- down
+ {modifiers = {"Mod", "Shift"}, key = "K", action = "SwapDirection", arg = 0}, -- up
+ {modifiers = {"Mod", "Shift"}, key = "L", action = "SwapDirection", arg = 3}, -- right
+
+ -- Monitor focus
+ {modifiers = {"Mod"}, key = "Comma", action = "FocusMonitor", arg = -1},
+ {modifiers = {"Mod"}, key = "Period", action = "FocusMonitor", arg = 1},
+
+ -- View tags
+ {modifiers = {"Mod"}, key = "1", action = "ViewTag", arg = 0},
+ {modifiers = {"Mod"}, key = "2", action = "ViewTag", arg = 1},
+ {modifiers = {"Mod"}, key = "3", action = "ViewTag", arg = 2},
+ {modifiers = {"Mod"}, key = "4", action = "ViewTag", arg = 3},
+ {modifiers = {"Mod"}, key = "5", action = "ViewTag", arg = 4},
+ {modifiers = {"Mod"}, key = "6", action = "ViewTag", arg = 5},
+ {modifiers = {"Mod"}, key = "7", action = "ViewTag", arg = 6},
+ {modifiers = {"Mod"}, key = "8", action = "ViewTag", arg = 7},
+ {modifiers = {"Mod"}, key = "9", action = "ViewTag", arg = 8},
+
+ -- Move window to tag
+ {modifiers = {"Mod", "Shift"}, key = "1", action = "MoveToTag", arg = 0},
+ {modifiers = {"Mod", "Shift"}, key = "2", action = "MoveToTag", arg = 1},
+ {modifiers = {"Mod", "Shift"}, key = "3", action = "MoveToTag", arg = 2},
+ {modifiers = {"Mod", "Shift"}, key = "4", action = "MoveToTag", arg = 3},
+ {modifiers = {"Mod", "Shift"}, key = "5", action = "MoveToTag", arg = 4},
+ {modifiers = {"Mod", "Shift"}, key = "6", action = "MoveToTag", arg = 5},
+ {modifiers = {"Mod", "Shift"}, key = "7", action = "MoveToTag", arg = 6},
+ {modifiers = {"Mod", "Shift"}, key = "8", action = "MoveToTag", arg = 7},
+ {modifiers = {"Mod", "Shift"}, key = "9", action = "MoveToTag", arg = 8},
+
+ -- Example keychord: Alt+Space, then T to spawn terminal
+ {
+ keys = {
+ {modifiers = {"Mod1"}, key = "Space"},
+ {modifiers = {}, key = "T"}
+ },
+ action = "Spawn",
+ arg = terminal
+ },
+ },
+
+ -- Status bar blocks
+ status_blocks = {
+ {
+ format = "Ram: {used}/{total} GB",
+ command = "Ram",
+ interval_secs = 5,
+ color = colors.light_blue,
+ underline = true
+ },
+ {
+ format = " │ ",
+ command = "Static",
+ interval_secs = 999999999, -- Very large number for static blocks
+ color = colors.lavender,
+ underline = false
+ },
+ {
+ format = "Kernel: {}",
+ command = "Shell",
+ command_arg = "uname -r",
+ interval_secs = 999999999,
+ color = colors.red,
+ underline = true
+ },
+ {
+ format = " │ ",
+ command = "Static",
+ interval_secs = 999999999,
+ color = colors.lavender,
+ underline = false
+ },
+ {
+ format = "{}",
+ command = "DateTime",
+ command_arg = "%a, %b %d - %-I:%M %P",
+ interval_secs = 1,
+ color = colors.cyan,
+ underline = true
+ },
+ },
+
+ -- Color schemes for bar
+ scheme_normal = {
+ foreground = colors.fg,
+ background = colors.bg,
+ underline = 0x444444
+ },
+ scheme_occupied = {
+ foreground = colors.cyan,
+ background = colors.bg,
+ underline = colors.cyan
+ },
+ scheme_selected = {
+ foreground = colors.cyan,
+ background = colors.bg,
+ underline = colors.purple
+ },
+
+ -- Autostart commands
+ autostart = {},
+}
diff --git a/src/bin/main.rs b/src/bin/main.rs
index cff5555..af27e0b 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -50,27 +50,46 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<oxwm::Config> {
let config_path = if let Some(path) = custom_path {
path
} else {
- let default_path = get_config_path().join("config.ron");
- if !default_path.exists() {
- println!("No config found at {:?}", default_path);
- println!("Creating default config...");
+ // Try to find config.lua first, then config.ron
+ let config_dir = get_config_path();
+ let lua_path = config_dir.join("config.lua");
+ let ron_path = config_dir.join("config.ron");
+
+ if lua_path.exists() {
+ lua_path
+ } else if ron_path.exists() {
+ ron_path
+ } else {
+ println!("No config found at {:?}", config_dir);
+ println!("Creating default Lua config...");
init_config()?;
+ config_dir.join("config.lua")
}
- default_path
};
let config_str =
std::fs::read_to_string(&config_path).with_context(|| "Failed to read config file")?;
- oxwm::config::parse_config(&config_str).with_context(|| "Failed to parse config")
+ // Determine config format based on file extension
+ let is_lua = config_path
+ .extension()
+ .and_then(|s| s.to_str())
+ .map(|s| s == "lua")
+ .unwrap_or(false);
+
+ if is_lua {
+ oxwm::config::parse_lua_config(&config_str).with_context(|| "Failed to parse Lua config")
+ } else {
+ oxwm::config::parse_config(&config_str).with_context(|| "Failed to parse RON config")
+ }
}
fn init_config() -> Result<()> {
let config_dir = get_config_path();
std::fs::create_dir_all(&config_dir)?;
- let config_template = include_str!("../../templates/config.ron");
- let config_path = config_dir.join("config.ron");
+ let config_template = include_str!("../../templates/config.lua");
+ let config_path = config_dir.join("config.lua");
std::fs::write(&config_path, config_template)?;
@@ -92,12 +111,12 @@ fn print_help() {
println!("USAGE:");
println!(" oxwm [OPTIONS]\n");
println!("OPTIONS:");
- println!(" --init Create default config in ~/.config/oxwm/config.ron");
- println!(" --config <PATH> Use custom config file");
+ println!(" --init Create default config in ~/.config/oxwm/config.lua");
+ println!(" --config <PATH> Use custom config file (.lua or .ron)");
println!(" --version Print version information");
println!(" --help Print this help message\n");
println!("CONFIG:");
- println!(" Location: ~/.config/oxwm/config.ron");
+ println!(" Location: ~/.config/oxwm/config.lua (or config.ron for legacy)");
println!(" Edit the config file and use Mod+Shift+R to reload");
println!(" No compilation needed - instant hot-reload!\n");
println!("FIRST RUN:");
diff --git a/src/config/lua.rs b/src/config/lua.rs
new file mode 100644
index 0000000..e1a9930
--- /dev/null
+++ b/src/config/lua.rs
@@ -0,0 +1,574 @@
+use crate::bar::{BlockCommand, BlockConfig};
+use crate::errors::ConfigError;
+use crate::keyboard::handlers::{KeyBinding, KeyPress};
+use crate::keyboard::keysyms::{self, Keysym};
+use crate::keyboard::{Arg, KeyAction};
+use crate::{ColorScheme, LayoutSymbolOverride};
+use mlua::{Lua, Table, Value};
+use x11rb::protocol::xproto::KeyButMask;
+
+pub fn parse_lua_config(input: &str) -> Result<crate::Config, ConfigError> {
+ let lua = Lua::new();
+
+ // Execute the config file
+ lua.load(input)
+ .exec()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to execute Lua config: {}", e)))?;
+
+ // Get the config table from global scope
+ let globals = lua.globals();
+ let config: Table = globals
+ .get("config")
+ .map_err(|e| ConfigError::LuaError(format!("Config table not found: {}", e)))?;
+
+ // Parse each config section
+ let border_width: u32 = get_table_field(&config, "border_width")?;
+ let border_focused: u32 = parse_color(&config, "border_focused")?;
+ let border_unfocused: u32 = parse_color(&config, "border_unfocused")?;
+ let font: String = get_table_field(&config, "font")?;
+
+ let gaps_enabled: bool = get_table_field(&config, "gaps_enabled")?;
+ let gap_inner_horizontal: u32 = get_table_field(&config, "gap_inner_horizontal")?;
+ let gap_inner_vertical: u32 = get_table_field(&config, "gap_inner_vertical")?;
+ let gap_outer_horizontal: u32 = get_table_field(&config, "gap_outer_horizontal")?;
+ let gap_outer_vertical: u32 = get_table_field(&config, "gap_outer_vertical")?;
+
+ let terminal: String = get_table_field(&config, "terminal")?;
+ let modkey = parse_modkey(&config)?;
+
+ let tags = parse_tags(&config)?;
+ let layout_symbols = parse_layout_symbols(&config)?;
+ let keybindings = parse_keybindings(&config, modkey)?;
+ let status_blocks = parse_status_blocks(&config)?;
+
+ let scheme_normal = parse_color_scheme(&config, "scheme_normal")?;
+ let scheme_occupied = parse_color_scheme(&config, "scheme_occupied")?;
+ let scheme_selected = parse_color_scheme(&config, "scheme_selected")?;
+
+ let autostart = parse_autostart(&config)?;
+
+ Ok(crate::Config {
+ border_width,
+ border_focused,
+ border_unfocused,
+ font,
+ gaps_enabled,
+ gap_inner_horizontal,
+ gap_inner_vertical,
+ gap_outer_horizontal,
+ gap_outer_vertical,
+ terminal,
+ modkey,
+ tags,
+ layout_symbols,
+ keybindings,
+ status_blocks,
+ scheme_normal,
+ scheme_occupied,
+ scheme_selected,
+ autostart,
+ })
+}
+
+fn get_table_field<T>(table: &Table, field: &str) -> Result<T, ConfigError>
+where
+ T: mlua::FromLua,
+{
+ table
+ .get::<T>(field)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to get field '{}': {}", field, e)))
+}
+
+fn parse_color(table: &Table, field: &str) -> Result<u32, ConfigError> {
+ let value: Value = table
+ .get(field)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to get color field '{}': {}", field, e)))?;
+
+ match value {
+ Value::String(s) => {
+ let s = s.to_str().map_err(|e| {
+ ConfigError::LuaError(format!("Invalid UTF-8 in color string: {}", e))
+ })?;
+ parse_color_string(&s)
+ }
+ Value::Integer(i) => Ok(i as u32),
+ Value::Number(n) => Ok(n as u32),
+ _ => Err(ConfigError::LuaError(format!(
+ "Color field '{}' must be a string or number",
+ field
+ ))),
+ }
+}
+
+fn parse_color_string(s: &str) -> Result<u32, ConfigError> {
+ let s = s.trim();
+ if s.starts_with('#') {
+ u32::from_str_radix(&s[1..], 16)
+ .map_err(|e| ConfigError::LuaError(format!("Invalid hex color '{}': {}", s, e)))
+ } else if s.starts_with("0x") {
+ u32::from_str_radix(&s[2..], 16)
+ .map_err(|e| ConfigError::LuaError(format!("Invalid hex color '{}': {}", s, e)))
+ } else {
+ s.parse::<u32>()
+ .map_err(|e| ConfigError::LuaError(format!("Invalid color '{}': {}", s, e)))
+ }
+}
+
+fn parse_modkey(config: &Table) -> Result<KeyButMask, ConfigError> {
+ let modkey_str: String = get_table_field(config, "modkey")?;
+ parse_modkey_string(&modkey_str)
+}
+
+fn parse_modkey_string(s: &str) -> Result<KeyButMask, ConfigError> {
+ match s {
+ "Mod1" => Ok(KeyButMask::MOD1),
+ "Mod2" => Ok(KeyButMask::MOD2),
+ "Mod3" => Ok(KeyButMask::MOD3),
+ "Mod4" => Ok(KeyButMask::MOD4),
+ "Mod5" => Ok(KeyButMask::MOD5),
+ "Shift" => Ok(KeyButMask::SHIFT),
+ "Control" => Ok(KeyButMask::CONTROL),
+ _ => Err(ConfigError::InvalidModkey(s.to_string())),
+ }
+}
+
+fn parse_tags(config: &Table) -> Result<Vec<String>, ConfigError> {
+ let tags_table: Table = get_table_field(config, "tags")?;
+ let mut tags = Vec::new();
+
+ for i in 1..=tags_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get tags length: {}", e))
+ })? {
+ let tag: String = tags_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get tag at index {}: {}", i, e))
+ })?;
+ tags.push(tag);
+ }
+
+ Ok(tags)
+}
+
+fn parse_layout_symbols(config: &Table) -> Result<Vec<LayoutSymbolOverride>, ConfigError> {
+ let layout_symbols_result: Result<Table, _> = config.get("layout_symbols");
+
+ match layout_symbols_result {
+ Ok(layout_symbols_table) => {
+ let mut layout_symbols = Vec::new();
+
+ for i in 1..=layout_symbols_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get layout_symbols length: {}", e))
+ })? {
+ let entry: Table = layout_symbols_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get layout_symbol at index {}: {}", i, e))
+ })?;
+
+ let name: String = get_table_field(&entry, "name")?;
+ let symbol: String = get_table_field(&entry, "symbol")?;
+
+ layout_symbols.push(LayoutSymbolOverride { name, symbol });
+ }
+
+ Ok(layout_symbols)
+ }
+ Err(_) => Ok(Vec::new()), // layout_symbols is optional
+ }
+}
+
+fn parse_keybindings(
+ config: &Table,
+ modkey: KeyButMask,
+) -> Result<Vec<KeyBinding>, ConfigError> {
+ let keybindings_table: Table = get_table_field(config, "keybindings")?;
+ let mut keybindings = Vec::new();
+
+ for i in 1..=keybindings_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get keybindings length: {}", e))
+ })? {
+ let kb_table: Table = keybindings_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get keybinding at index {}: {}", i, e))
+ })?;
+
+ let keys = parse_keypress_list(&kb_table, modkey)?;
+ let action = parse_key_action(&kb_table)?;
+ let arg = parse_arg(&kb_table)?;
+
+ keybindings.push(KeyBinding::new(keys, action, arg));
+ }
+
+ Ok(keybindings)
+}
+
+fn parse_keypress_list(
+ kb_table: &Table,
+ modkey: KeyButMask,
+) -> Result<Vec<KeyPress>, ConfigError> {
+ // Check if 'keys' field exists (for keychords)
+ let keys_result: Result<Table, _> = kb_table.get("keys");
+
+ if let Ok(keys_table) = keys_result {
+ // Parse keychord
+ let mut keys = Vec::new();
+ for i in 1..=keys_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get keys length: {}", e))
+ })? {
+ let key_entry: Table = keys_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get key at index {}: {}", i, e))
+ })?;
+
+ let modifiers = parse_modifiers(&key_entry, "modifiers", modkey)?;
+ let keysym = parse_keysym(&key_entry, "key")?;
+
+ keys.push(KeyPress { modifiers, keysym });
+ }
+ Ok(keys)
+ } else {
+ // Parse single key (old format)
+ let modifiers = parse_modifiers(kb_table, "modifiers", modkey)?;
+ let keysym = parse_keysym(kb_table, "key")?;
+
+ Ok(vec![KeyPress { modifiers, keysym }])
+ }
+}
+
+fn parse_modifiers(
+ table: &Table,
+ field: &str,
+ modkey: KeyButMask,
+) -> Result<Vec<KeyButMask>, ConfigError> {
+ let mods_table: Table = get_table_field(table, field)?;
+ let mut modifiers = Vec::new();
+
+ for i in 1..=mods_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get modifiers length: {}", e))
+ })? {
+ let mod_str: String = mods_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get modifier at index {}: {}", i, e))
+ })?;
+
+ let modifier = if mod_str == "Mod" {
+ modkey
+ } else {
+ parse_modkey_string(&mod_str)?
+ };
+
+ modifiers.push(modifier);
+ }
+
+ Ok(modifiers)
+}
+
+fn parse_keysym(table: &Table, field: &str) -> Result<Keysym, ConfigError> {
+ let key_str: String = get_table_field(table, field)?;
+ string_to_keysym(&key_str)
+}
+
+fn string_to_keysym(s: &str) -> Result<Keysym, ConfigError> {
+ let keysym = match s {
+ "Return" => keysyms::XK_RETURN,
+ "Q" => keysyms::XK_Q,
+ "Escape" => keysyms::XK_ESCAPE,
+ "Space" => keysyms::XK_SPACE,
+ "Tab" => keysyms::XK_TAB,
+ "Backspace" => keysyms::XK_BACKSPACE,
+ "Delete" => keysyms::XK_DELETE,
+ "F1" => keysyms::XK_F1,
+ "F2" => keysyms::XK_F2,
+ "F3" => keysyms::XK_F3,
+ "F4" => keysyms::XK_F4,
+ "F5" => keysyms::XK_F5,
+ "F6" => keysyms::XK_F6,
+ "F7" => keysyms::XK_F7,
+ "F8" => keysyms::XK_F8,
+ "F9" => keysyms::XK_F9,
+ "F10" => keysyms::XK_F10,
+ "F11" => keysyms::XK_F11,
+ "F12" => keysyms::XK_F12,
+ "A" => keysyms::XK_A,
+ "B" => keysyms::XK_B,
+ "C" => keysyms::XK_C,
+ "D" => keysyms::XK_D,
+ "E" => keysyms::XK_E,
+ "F" => keysyms::XK_F,
+ "G" => keysyms::XK_G,
+ "H" => keysyms::XK_H,
+ "I" => keysyms::XK_I,
+ "J" => keysyms::XK_J,
+ "K" => keysyms::XK_K,
+ "L" => keysyms::XK_L,
+ "M" => keysyms::XK_M,
+ "N" => keysyms::XK_N,
+ "O" => keysyms::XK_O,
+ "P" => keysyms::XK_P,
+ "R" => keysyms::XK_R,
+ "S" => keysyms::XK_S,
+ "T" => keysyms::XK_T,
+ "U" => keysyms::XK_U,
+ "V" => keysyms::XK_V,
+ "W" => keysyms::XK_W,
+ "X" => keysyms::XK_X,
+ "Y" => keysyms::XK_Y,
+ "Z" => keysyms::XK_Z,
+ "0" => keysyms::XK_0,
+ "1" => keysyms::XK_1,
+ "2" => keysyms::XK_2,
+ "3" => keysyms::XK_3,
+ "4" => keysyms::XK_4,
+ "5" => keysyms::XK_5,
+ "6" => keysyms::XK_6,
+ "7" => keysyms::XK_7,
+ "8" => keysyms::XK_8,
+ "9" => keysyms::XK_9,
+ "Left" => keysyms::XK_LEFT,
+ "Right" => keysyms::XK_RIGHT,
+ "Up" => keysyms::XK_UP,
+ "Down" => keysyms::XK_DOWN,
+ "Home" => keysyms::XK_HOME,
+ "End" => keysyms::XK_END,
+ "PageUp" => keysyms::XK_PAGE_UP,
+ "PageDown" => keysyms::XK_PAGE_DOWN,
+ "Insert" => keysyms::XK_INSERT,
+ "Minus" => keysyms::XK_MINUS,
+ "Equal" => keysyms::XK_EQUAL,
+ "BracketLeft" => keysyms::XK_LEFT_BRACKET,
+ "BracketRight" => keysyms::XK_RIGHT_BRACKET,
+ "Semicolon" => keysyms::XK_SEMICOLON,
+ "Apostrophe" => keysyms::XK_APOSTROPHE,
+ "Grave" => keysyms::XK_GRAVE,
+ "Backslash" => keysyms::XK_BACKSLASH,
+ "Comma" => keysyms::XK_COMMA,
+ "Period" => keysyms::XK_PERIOD,
+ "Slash" => keysyms::XK_SLASH,
+ "AudioRaiseVolume" => keysyms::XF86_AUDIO_RAISE_VOLUME,
+ "AudioLowerVolume" => keysyms::XF86_AUDIO_LOWER_VOLUME,
+ "AudioMute" => keysyms::XF86_AUDIO_MUTE,
+ "MonBrightnessUp" => keysyms::XF86_MON_BRIGHTNESS_UP,
+ "MonBrightnessDown" => keysyms::XF86_MON_BRIGHTNESS_DOWN,
+ _ => return Err(ConfigError::UnknownKey(s.to_string())),
+ };
+
+ Ok(keysym)
+}
+
+fn parse_key_action(kb_table: &Table) -> Result<KeyAction, ConfigError> {
+ let action_str: String = get_table_field(kb_table, "action")?;
+ string_to_key_action(&action_str)
+}
+
+fn string_to_key_action(s: &str) -> Result<KeyAction, ConfigError> {
+ let action = match s {
+ "Spawn" => KeyAction::Spawn,
+ "KillClient" => KeyAction::KillClient,
+ "FocusStack" => KeyAction::FocusStack,
+ "FocusDirection" => KeyAction::FocusDirection,
+ "SwapDirection" => KeyAction::SwapDirection,
+ "Quit" => KeyAction::Quit,
+ "Restart" => KeyAction::Restart,
+ "Recompile" => KeyAction::Recompile,
+ "ViewTag" => KeyAction::ViewTag,
+ "ToggleGaps" => KeyAction::ToggleGaps,
+ "ToggleFullScreen" => KeyAction::ToggleFullScreen,
+ "ToggleFloating" => KeyAction::ToggleFloating,
+ "ChangeLayout" => KeyAction::ChangeLayout,
+ "CycleLayout" => KeyAction::CycleLayout,
+ "MoveToTag" => KeyAction::MoveToTag,
+ "FocusMonitor" => KeyAction::FocusMonitor,
+ "SmartMoveWin" => KeyAction::SmartMoveWin,
+ "ExchangeClient" => KeyAction::ExchangeClient,
+ "None" => KeyAction::None,
+ _ => return Err(ConfigError::UnknownAction(s.to_string())),
+ };
+
+ Ok(action)
+}
+
+fn parse_arg(kb_table: &Table) -> Result<Arg, ConfigError> {
+ let arg_result: Result<Value, _> = kb_table.get("arg");
+
+ match arg_result {
+ Ok(Value::Nil) | Err(_) => Ok(Arg::None),
+ Ok(Value::String(s)) => {
+ let s = s
+ .to_str()
+ .map_err(|e| ConfigError::LuaError(format!("Invalid UTF-8 in arg: {}", e)))?;
+ Ok(Arg::Str(s.to_string()))
+ }
+ Ok(Value::Integer(i)) => Ok(Arg::Int(i as i32)),
+ Ok(Value::Number(n)) => Ok(Arg::Int(n as i32)),
+ Ok(Value::Table(t)) => {
+ let mut arr = Vec::new();
+ for i in 1..=t
+ .len()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to get arg array length: {}", e)))?
+ {
+ let item: String = t.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get arg array item at index {}: {}", i, e))
+ })?;
+ arr.push(item);
+ }
+ Ok(Arg::Array(arr))
+ }
+ Ok(_) => Err(ConfigError::LuaError(
+ "Arg must be nil, string, number, or array".to_string(),
+ )),
+ }
+}
+
+fn parse_status_blocks(config: &Table) -> Result<Vec<BlockConfig>, ConfigError> {
+ let blocks_table: Table = get_table_field(config, "status_blocks")?;
+ let mut blocks = Vec::new();
+
+ for i in 1..=blocks_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get status_blocks length: {}", e))
+ })? {
+ let block_table: Table = blocks_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get status_block at index {}: {}", i, e))
+ })?;
+
+ let format: String = get_table_field(&block_table, "format")?;
+ let command_str: String = get_table_field(&block_table, "command")?;
+
+ // Parse interval_secs - handle both integer and number types
+ let interval_secs: u64 = {
+ let value: Value = block_table.get("interval_secs").map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get interval_secs: {}", e))
+ })?;
+ match value {
+ Value::Integer(i) => i as u64,
+ Value::Number(n) => n as u64,
+ _ => return Err(ConfigError::LuaError("interval_secs must be a number".to_string())),
+ }
+ };
+
+ let color: u32 = parse_color(&block_table, "color")?;
+ let underline: bool = get_table_field(&block_table, "underline")?;
+
+ let command = match command_str.as_str() {
+ "DateTime" => {
+ let fmt: String = get_table_field(&block_table, "command_arg")?;
+ BlockCommand::DateTime(fmt)
+ }
+ "Shell" => {
+ let cmd: String = get_table_field(&block_table, "command_arg")?;
+ BlockCommand::Shell(cmd)
+ }
+ "Ram" => BlockCommand::Ram,
+ "Static" => {
+ let text_result: Result<String, _> = block_table.get("command_arg");
+ let text = text_result.unwrap_or_default();
+ BlockCommand::Static(text)
+ }
+ "Battery" => {
+ let formats_table: Table = get_table_field(&block_table, "battery_formats")?;
+ let format_charging: String = get_table_field(&formats_table, "charging")?;
+ let format_discharging: String = get_table_field(&formats_table, "discharging")?;
+ let format_full: String = get_table_field(&formats_table, "full")?;
+
+ BlockCommand::Battery {
+ format_charging,
+ format_discharging,
+ format_full,
+ }
+ }
+ _ => return Err(ConfigError::UnknownBlockCommand(command_str)),
+ };
+
+ blocks.push(BlockConfig {
+ format,
+ command,
+ interval_secs,
+ color,
+ underline,
+ });
+ }
+
+ Ok(blocks)
+}
+
+fn parse_color_scheme(config: &Table, field: &str) -> Result<ColorScheme, ConfigError> {
+ let scheme_table: Table = get_table_field(config, field)?;
+
+ let foreground = parse_color(&scheme_table, "foreground")?;
+ let background = parse_color(&scheme_table, "background")?;
+ let underline = parse_color(&scheme_table, "underline")?;
+
+ Ok(ColorScheme {
+ foreground,
+ background,
+ underline,
+ })
+}
+
+fn parse_autostart(config: &Table) -> Result<Vec<String>, ConfigError> {
+ let autostart_result: Result<Table, _> = config.get("autostart");
+
+ match autostart_result {
+ Ok(autostart_table) => {
+ let mut autostart = Vec::new();
+
+ for i in 1..=autostart_table.len().map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get autostart length: {}", e))
+ })? {
+ let cmd: String = autostart_table.get(i).map_err(|e| {
+ ConfigError::LuaError(format!("Failed to get autostart command at index {}: {}", i, e))
+ })?;
+ autostart.push(cmd);
+ }
+
+ Ok(autostart)
+ }
+ Err(_) => Ok(Vec::new()), // autostart is optional
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_minimal_lua_config() {
+ let config_str = r#"
+config = {
+ border_width = 2,
+ border_focused = 0x6dade3,
+ border_unfocused = 0xbbbbbb,
+ font = "monospace:style=Bold:size=10",
+
+ gaps_enabled = true,
+ gap_inner_horizontal = 5,
+ gap_inner_vertical = 5,
+ gap_outer_horizontal = 5,
+ gap_outer_vertical = 5,
+
+ modkey = "Mod4",
+ terminal = "st",
+
+ tags = {"1", "2", "3"},
+
+ keybindings = {
+ {modifiers = {"Mod4"}, key = "Return", action = "Spawn", arg = "st"},
+ {modifiers = {"Mod4"}, key = "Q", action = "KillClient"},
+ },
+
+ status_blocks = {
+ {format = "{}", command = "DateTime", command_arg = "%H:%M", interval_secs = 1, color = 0xffffff, underline = true},
+ },
+
+ scheme_normal = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
+ scheme_occupied = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
+ scheme_selected = {foreground = 0xffffff, background = 0x000000, underline = 0x444444},
+
+ autostart = {},
+}
+"#;
+
+ let config = parse_lua_config(config_str).expect("Failed to parse config");
+
+ assert_eq!(config.border_width, 2);
+ assert_eq!(config.border_focused, 0x6dade3);
+ assert_eq!(config.terminal, "st");
+ assert_eq!(config.tags.len(), 3);
+ assert_eq!(config.keybindings.len(), 2);
+ assert_eq!(config.status_blocks.len(), 1);
+ assert!(config.gaps_enabled);
+ }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index d7c4f2c..02fbd8f 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,3 +1,5 @@
+mod lua;
+
use crate::bar::{BlockCommand, BlockConfig};
use crate::errors::ConfigError;
use crate::keyboard::handlers::{KeyBinding, KeyPress};
@@ -8,6 +10,8 @@ use serde::Deserialize;
use std::collections::HashMap;
use x11rb::protocol::xproto::KeyButMask;
+pub use lua::parse_lua_config;
+
#[derive(Debug, Deserialize)]
pub enum ModKey {
Mod,
diff --git a/src/errors.rs b/src/errors.rs
index a48010e..0ee19d4 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -23,6 +23,7 @@ pub enum X11Error {
#[derive(Debug)]
pub enum ConfigError {
ParseError(ron::error::SpannedError),
+ LuaError(String),
InvalidModkey(String),
UnknownKey(String),
UnknownAction(String),
@@ -68,6 +69,7 @@ impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseError(err) => write!(f, "Failed to parse RON config: {}", err),
+ Self::LuaError(msg) => write!(f, "Lua config error: {}", msg),
Self::InvalidModkey(key) => write!(f, "Invalid modkey: {}", key),
Self::UnknownKey(key) => write!(f, "Unknown key: {}", key),
Self::UnknownAction(action) => write!(f, "Unknown action: {}", action),
diff --git a/templates/config.lua b/templates/config.lua
new file mode 100644
index 0000000..fd3eabb
--- /dev/null
+++ b/templates/config.lua
@@ -0,0 +1,309 @@
+-- OXWM Configuration File (Lua)
+-- Edit this file and reload with Mod+Shift+R (no compilation needed!)
+
+---@class LayoutSymbol
+---@field name string The internal layout name (e.g., "tiling", "normie")
+---@field symbol string The display symbol for the layout (e.g., "[T]", "[F]")
+
+---@class KeyBinding
+---@field modifiers string[] List of modifiers: "Mod", "Mod1"-"Mod5", "Shift", "Control"
+---@field key string The key name (e.g., "Return", "Q", "1", "Space")
+---@field action string Action to perform (see below for list)
+---@field arg? string|number|string[] Optional argument for the action
+
+---@class KeyChord
+---@field keys {modifiers: string[], key: string}[] Sequence of keys to press
+---@field action string Action to perform when chord completes
+---@field arg? string|number|string[] Optional argument for the action
+
+---@class StatusBlock
+---@field format string Display format with {} placeholders
+---@field command string Command type: "Ram", "DateTime", "Shell", "Static", "Battery"
+---@field command_arg? string Argument for command (shell command, date format, etc.)
+---@field interval_secs number Update interval in seconds
+---@field color number Color as hex number (e.g., 0xff0000 for red)
+---@field underline boolean Whether to show underline
+---@field battery_formats? {charging: string, discharging: string, full: string} Battery format strings
+
+---@class ColorScheme
+---@field foreground number Foreground color (hex)
+---@field background number Background color (hex)
+---@field underline number Underline color (hex)
+
+---@class Config
+---@field border_width number Width of window borders in pixels
+---@field border_focused number Color for focused window border (hex)
+---@field border_unfocused number Color for unfocused window border (hex)
+---@field font string Font specification (e.g., "monospace:style=Bold:size=10")
+---@field gaps_enabled boolean Whether gaps are enabled
+---@field gap_inner_horizontal number Inner horizontal gap size
+---@field gap_inner_vertical number Inner vertical gap size
+---@field gap_outer_horizontal number Outer horizontal gap size
+---@field gap_outer_vertical number Outer vertical gap size
+---@field modkey string Main modifier key (e.g., "Mod4" for Super)
+---@field terminal string Terminal emulator command
+---@field tags string[] List of workspace tag names
+---@field layout_symbols LayoutSymbol[] Custom layout symbols
+---@field keybindings (KeyBinding|KeyChord)[] List of keybindings
+---@field status_blocks StatusBlock[] Status bar configuration blocks
+---@field scheme_normal ColorScheme Color scheme for normal tags
+---@field scheme_occupied ColorScheme Color scheme for occupied tags
+---@field scheme_selected ColorScheme Color scheme for selected tag
+---@field autostart string[] Commands to run on startup
+
+-- Available Actions:
+-- "Spawn" - Launch a program (arg: string or string[])
+-- "KillClient" - Close focused window
+-- "FocusStack" - Focus next/prev in stack (arg: 1 or -1)
+-- "FocusDirection" - Focus by direction (arg: 0=up, 1=down, 2=left, 3=right)
+-- "SwapDirection" - Swap window by direction (arg: 0=up, 1=down, 2=left, 3=right)
+-- "Quit" - Exit window manager
+-- "Restart" - Restart and reload config
+-- "Recompile" - Recompile and restart
+-- "ViewTag" - Switch to tag (arg: tag index)
+-- "MoveToTag" - Move window to tag (arg: tag index)
+-- "ToggleGaps" - Toggle gaps on/off
+-- "ToggleFullScreen" - Toggle fullscreen mode
+-- "ToggleFloating" - Toggle floating mode
+-- "ChangeLayout" - Switch to specific layout (arg: layout name)
+-- "CycleLayout" - Cycle through layouts
+-- "FocusMonitor" - Focus monitor (arg: 1 or -1)
+-- "SmartMoveWin" - Smart window movement
+-- "ExchangeClient" - Exchange window positions
+
+-- Available Modifiers:
+-- "Mod" - Replaced with configured modkey
+-- "Mod1" - Alt key
+-- "Mod4" - Super/Windows key
+-- "Shift" - Shift key
+-- "Control" - Control key
+-- "Mod2", "Mod3", "Mod5" - Additional modifiers
+
+-- Define variables for easy customization
+local terminal = "st"
+local modkey = "Mod4"
+local secondary_modkey = "Mod1"
+
+-- Color palette (Tokyo Night theme)
+local colors = {
+ blue = 0x6dade3,
+ grey = 0xbbbbbb,
+ green = 0x9ece6a,
+ red = 0xf7768e,
+ cyan = 0x0db9d7,
+ purple = 0xad8ee6,
+ lavender = 0xa9b1d6,
+ bg = 0x1a1b26,
+ fg = 0xbbbbbb,
+ light_blue = 0x7aa2f7,
+}
+
+-- Main configuration table
+---@type Config
+config = {
+ -- Appearance
+ border_width = 2,
+ border_focused = colors.blue,
+ border_unfocused = colors.grey,
+ font = "monospace:style=Bold:size=10",
+
+ -- Window gaps
+ gaps_enabled = true,
+ gap_inner_horizontal = 5,
+ gap_inner_vertical = 5,
+ gap_outer_horizontal = 5,
+ gap_outer_vertical = 5,
+
+ -- Basics
+ modkey = modkey,
+ terminal = terminal,
+
+ -- Workspace tags
+ tags = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
+
+ -- Layout symbol overrides
+ layout_symbols = {
+ {name = "tiling", symbol = "[T]"},
+ {name = "normie", symbol = "[F]"},
+ },
+
+ -- Keybindings
+ --
+ -- Format:
+ -- Single key: {modifiers = {...}, key = "X", action = "...", arg = ...}
+ -- Keychord: {keys = {{modifiers = {...}, key = "X"}, {modifiers = {...}, key = "Y"}}, action = "...", arg = ...}
+ --
+ -- Available modifiers: "Mod", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5", "Shift", "Control"
+ -- "Mod" will be replaced with the configured modkey
+ --
+ -- Available actions:
+ -- "Spawn", "KillClient", "FocusStack", "FocusDirection", "SwapDirection",
+ -- "Quit", "Restart", "Recompile", "ViewTag", "ToggleGaps", "ToggleFullScreen",
+ -- "ToggleFloating", "ChangeLayout", "CycleLayout", "MoveToTag", "FocusMonitor",
+ -- "SmartMoveWin", "ExchangeClient"
+ --
+ -- arg can be: string, number, array of strings, or omitted (nil)
+ --
+ -- You can cancel any in-progress keychord by pressing Escape.
+ keybindings = {
+ -- Basic window management
+ {modifiers = {"Mod"}, key = "Return", action = "Spawn", arg = terminal},
+ {modifiers = {"Mod"}, key = "D", action = "Spawn", arg = {"sh", "-c", "dmenu_run -l 10"}},
+ {modifiers = {"Mod"}, key = "S", action = "Spawn", arg = {"sh", "-c", "maim -s | xclip -selection clipboard -t image/png"}},
+ {modifiers = {"Mod"}, key = "Q", action = "KillClient"},
+ {modifiers = {"Mod", "Shift"}, key = "F", action = "ToggleFullScreen"},
+ {modifiers = {"Mod", "Shift"}, key = "Space", action = "ToggleFloating"},
+
+ -- Layout management
+ {modifiers = {"Mod"}, key = "F", action = "ChangeLayout", arg = "normie"},
+ {modifiers = {"Mod"}, key = "C", action = "ChangeLayout", arg = "tiling"},
+ {modifiers = {secondary_modkey}, key = "N", action = "CycleLayout"},
+ {modifiers = {"Mod"}, key = "A", action = "ToggleGaps"},
+
+ -- WM control
+ {modifiers = {"Mod", "Shift"}, key = "Q", action = "Quit"},
+ {modifiers = {"Mod", "Shift"}, key = "R", action = "Restart"},
+
+ -- Focus movement (vim-style hjkl)
+ {modifiers = {"Mod"}, key = "H", action = "FocusDirection", arg = 2}, -- left
+ {modifiers = {"Mod"}, key = "J", action = "FocusDirection", arg = 1}, -- down
+ {modifiers = {"Mod"}, key = "K", action = "FocusDirection", arg = 0}, -- up
+ {modifiers = {"Mod"}, key = "L", action = "FocusDirection", arg = 3}, -- right
+
+ -- Window swapping (vim-style hjkl)
+ {modifiers = {"Mod", "Shift"}, key = "H", action = "SwapDirection", arg = 2}, -- left
+ {modifiers = {"Mod", "Shift"}, key = "J", action = "SwapDirection", arg = 1}, -- down
+ {modifiers = {"Mod", "Shift"}, key = "K", action = "SwapDirection", arg = 0}, -- up
+ {modifiers = {"Mod", "Shift"}, key = "L", action = "SwapDirection", arg = 3}, -- right
+
+ -- Monitor focus
+ {modifiers = {"Mod"}, key = "Comma", action = "FocusMonitor", arg = -1},
+ {modifiers = {"Mod"}, key = "Period", action = "FocusMonitor", arg = 1},
+
+ -- View tags
+ {modifiers = {"Mod"}, key = "1", action = "ViewTag", arg = 0},
+ {modifiers = {"Mod"}, key = "2", action = "ViewTag", arg = 1},
+ {modifiers = {"Mod"}, key = "3", action = "ViewTag", arg = 2},
+ {modifiers = {"Mod"}, key = "4", action = "ViewTag", arg = 3},
+ {modifiers = {"Mod"}, key = "5", action = "ViewTag", arg = 4},
+ {modifiers = {"Mod"}, key = "6", action = "ViewTag", arg = 5},
+ {modifiers = {"Mod"}, key = "7", action = "ViewTag", arg = 6},
+ {modifiers = {"Mod"}, key = "8", action = "ViewTag", arg = 7},
+ {modifiers = {"Mod"}, key = "9", action = "ViewTag", arg = 8},
+
+ -- Move window to tag
+ {modifiers = {"Mod", "Shift"}, key = "1", action = "MoveToTag", arg = 0},
+ {modifiers = {"Mod", "Shift"}, key = "2", action = "MoveToTag", arg = 1},
+ {modifiers = {"Mod", "Shift"}, key = "3", action = "MoveToTag", arg = 2},
+ {modifiers = {"Mod", "Shift"}, key = "4", action = "MoveToTag", arg = 3},
+ {modifiers = {"Mod", "Shift"}, key = "5", action = "MoveToTag", arg = 4},
+ {modifiers = {"Mod", "Shift"}, key = "6", action = "MoveToTag", arg = 5},
+ {modifiers = {"Mod", "Shift"}, key = "7", action = "MoveToTag", arg = 6},
+ {modifiers = {"Mod", "Shift"}, key = "8", action = "MoveToTag", arg = 7},
+ {modifiers = {"Mod", "Shift"}, key = "9", action = "MoveToTag", arg = 8},
+
+ -- Example keychord: Mod+Space, then T to spawn terminal
+ {
+ keys = {
+ {modifiers = {"Mod4"}, key = "Space"},
+ {modifiers = {}, key = "T"}
+ },
+ action = "Spawn",
+ arg = terminal
+ },
+ },
+
+ -- Status bar blocks
+ --
+ -- Available commands:
+ -- "Ram" - Shows RAM usage (no command_arg needed)
+ -- "DateTime" - Shows date/time with strftime format in command_arg
+ -- "Shell" - Runs shell command from command_arg
+ -- "Static" - Static text from command_arg
+ -- "Battery" - Shows battery status (requires battery_formats table)
+ --
+ -- Battery formats example:
+ -- battery_formats = {
+ -- charging = "⚡ {}%",
+ -- discharging = "🔋 {}%",
+ -- full = "✓ {}%"
+ -- }
+ status_blocks = {
+ {
+ format = "Ram: {used}/{total} GB",
+ command = "Ram",
+ interval_secs = 5,
+ color = colors.light_blue,
+ underline = true
+ },
+ {
+ format = " │ ",
+ command = "Static",
+ interval_secs = 999999999, -- Very large number for static blocks (never update)
+ color = colors.lavender,
+ underline = false
+ },
+ {
+ format = "Kernel: {}",
+ command = "Shell",
+ command_arg = "uname -r",
+ interval_secs = 999999999,
+ color = colors.red,
+ underline = true
+ },
+ {
+ format = " │ ",
+ command = "Static",
+ interval_secs = 999999999,
+ color = colors.lavender,
+ underline = false
+ },
+ {
+ format = "{}",
+ command = "DateTime",
+ command_arg = "%a, %b %d - %-I:%M %P",
+ interval_secs = 1,
+ color = colors.cyan,
+ underline = true
+ },
+ },
+
+ -- Color schemes for bar
+ scheme_normal = {
+ foreground = colors.fg,
+ background = colors.bg,
+ underline = 0x444444
+ },
+ scheme_occupied = {
+ foreground = colors.cyan,
+ background = colors.bg,
+ underline = colors.cyan
+ },
+ scheme_selected = {
+ foreground = colors.cyan,
+ background = colors.bg,
+ underline = colors.purple
+ },
+
+ -- Autostart commands
+ -- These are executed when the window manager starts
+ -- Add your startup applications here
+ autostart = {
+ -- Uncomment and add your autostart commands:
+ -- "picom -b",
+ -- "nitrogen --restore &",
+ -- "dunst &",
+ },
+}
+
+-- You can also add helper functions for more complex configs:
+--
+-- Example: Generate keybindings programmatically
+-- for i = 1, 9 do
+-- table.insert(config.keybindings, {
+-- modifiers = {"Mod"},
+-- key = tostring(i),
+-- action = "ViewTag",
+-- arg = i - 1
+-- })
+-- end