Diff
diff --git a/Cargo.lock b/Cargo.lock
index 69fcf92..d90c691 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,20 +23,11 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-[[package]]
-name = "base64"
-version = "0.21.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
-
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
-dependencies = [
- "serde_core",
-]
[[package]]
name = "bstr"
@@ -312,7 +303,6 @@ dependencies = [
"chrono",
"dirs",
"mlua",
- "ron",
"serde",
"x11",
"x11rb",
@@ -385,18 +375,6 @@ dependencies = [
"thiserror",
]
-[[package]]
-name = "ron"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
-dependencies = [
- "base64",
- "bitflags",
- "serde",
- "serde_derive",
-]
-
[[package]]
name = "rustc-hash"
version = "2.1.1"
diff --git a/Cargo.toml b/Cargo.toml
index 8383b76..bce4eb0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,5 +18,4 @@ anyhow = "1"
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/readme.org b/readme.org
index 9423936..bc0e692 100644
--- a/readme.org
+++ b/readme.org
@@ -30,7 +30,7 @@
- [[#license][License]]
* OXWM — DWM but Better (and oxidized)
-A dynamic window manager written in Rust, inspired by dwm but designed to evolve on its own. Configuration is done in Rust source code, ditching the suckless philosophy of *"edit + recompile."*, and instead focusing on lowering friction for users, with sane defaults and no arbitrary elitism enforcing bad variable names and bad file structure topology.
+A dynamic window manager written in Rust, inspired by dwm but designed to evolve beyond it. OXWM features a clean, functional Lua API for configuration with hot-reloading support, ditching the suckless philosophy of *"edit + recompile"*. Instead, we focus on lowering friction for users with sane defaults, LSP-powered autocomplete, and instant configuration changes without restarting your X session.
* Installation
** NixOS (with Flakes)
@@ -154,27 +154,62 @@ startx
If using a display manager (LightDM, GDM, SDDM), OXWM should appear in the session list after installation.
* Configuration
-OXWM was inspired by dwm, but ditched the suckless philosophy. This philosophy quite literally discourages users from using the software for the sake of 'elitism'. I find that quite nonsensical, so I went ahead and created this project to be user friendly. The configuration is done by editing =~/.config/oxwm/config.lua= and the binary can be reloaded with a hotkey (Super+Shift+R by default).
+OXWM uses a clean, functional Lua API for configuration. On first run, a default config is automatically created at =~/.config/oxwm/config.lua=.
+
+** Quick Example
+Here's what the new functional API looks like:
+
+#+begin_src lua
+-- Set basic options
+oxwm.set_terminal("st")
+oxwm.set_modkey("Mod4")
+oxwm.set_tags({ "1", "2", "3", "4", "5", "6", "7", "8", "9" })
+
+-- Configure borders
+oxwm.border.set_width(2)
+oxwm.border.set_focused_color("#6dade3")
+oxwm.border.set_unfocused_color("#bbbbbb")
+
+-- Configure gaps
+oxwm.gaps.set_enabled(true)
+oxwm.gaps.set_inner(5, 5) -- horizontal, vertical
+oxwm.gaps.set_outer(5, 5)
+
+-- Set up keybindings
+oxwm.key.bind({ "Mod4" }, "Return", oxwm.spawn("st"))
+oxwm.key.bind({ "Mod4" }, "Q", oxwm.client.kill())
+oxwm.key.bind({ "Mod4", "Shift" }, "Q", oxwm.quit())
+
+-- Add status bar blocks
+oxwm.bar.add_block("{}", "DateTime", "%H:%M", 60, "#0db9d7", true)
+oxwm.bar.add_block("RAM: {used}/{total} GB", "Ram", nil, 5, "#7aa2f7", true)
+#+end_src
+
+** Features
+- *Hot-reload*: Changes take effect immediately with =Mod+Shift+R= (no X restart needed)
+- *LSP Support*: Full autocomplete and type hints for the API (=oxwm.lua= definitions included)
+- *Functional API*: Clean, discoverable functions instead of nested tables
+- *No compilation*: Edit and reload instantly
+** Key Configuration Areas
Edit =~/.config/oxwm/config.lua= to customize:
-- Keybindings
-- Colors and appearance
-- Status bar blocks
-- Gaps and borders
-- Terminal and applications
+- Basic settings (terminal, modkey, tags)
+- Borders and colors
+- Window gaps
+- Status bar (font, blocks, color schemes)
+- Keybindings and keychords
+- Layout symbols
+- Autostart commands
After making changes, reload OXWM with =Mod+Shift+R=
-** Migrating from RON
-If you have an old RON config (=config.ron=), use the migration tool to automatically convert it:
-
+** Creating Your Config
+Generate the default config:
#+begin_src sh
-oxwm --migrate
-# Or specify a path:
-oxwm --migrate ~/.config/oxwm/config.ron
+oxwm --init
#+end_src
-This will convert your =config.ron= to =config.lua=, preserving all your settings. Your old config will remain intact for backup.
+Or just start OXWM - it will create one automatically on first run.
* Contributing
When contributing to OXWM:
@@ -236,9 +271,13 @@ DISPLAY=:1 cargo run
* Project Structure
#+begin_src sh
src/
-├── main.rs
-│ └── main()
-│ └── Creates WindowManager and calls .run()
+├── bin/
+│ └── main.rs [Entry point - handles CLI args, config loading, WM init]
+│ ├── main() [Parse args, load config, start WM]
+│ ├── load_config() [Lua config loading with auto-init]
+│ └── init_config() [Create default config.lua + oxwm.lua]
+│
+├── lib.rs [Library exports]
│
├── window_manager.rs [CORE - X11 event handling]
│ ├── struct WindowManager
@@ -248,42 +287,28 @@ src/
│ │ ├── layout: Box<dyn Layout>
│ │ ├── window_tags: HashMap<Window, TagMask>
│ │ ├── selected_tags: TagMask
-│ │ └── bar: Bar [Status bar]
+│ │ └── bars: Vec<Bar> [Status bars (multi-monitor)]
│ │
│ ├── new() [Initialize WM, grab root, restore tags, scan windows]
│ ├── run() [Main event loop with block updates]
│ ├── handle_event() [Route X11 events]
-│ │ ├── MapRequest → add window, apply layout, update bar, save tag
-│ │ ├── UnmapNotify → remove window, update bar
-│ │ ├── DestroyNotify → remove window, update bar
-│ │ ├── KeyPress → get action, handle it (includes Restart)
-│ │ ├── ButtonPress → handle bar clicks
-│ │ └── Expose → redraw bar
-│ ├── handle_key_action() [Execute keyboard actions]
-│ ├── get_saved_selected_tags() [Restore selected tags from _NET_CURRENT_DESKTOP]
-│ ├── save_selected_tags() [Persist selected tags to root window]
-│ ├── get_saved_tag() [Restore window tag from _NET_CLIENT_INFO]
-│ ├── save_client_tag() [Persist window tag to window property]
-│ ├── scan_existing_windows() [Manage windows on startup]
-│ ├── remove_window() [Remove from Vec, reapply layout]
-│ ├── set_focus() [Focus window, update visuals]
-│ ├── cycle_focus() [Move focus to next/prev window]
-│ ├── view_tag() [Switch to tag/workspace, update visibility]
-│ ├── move_to_tag() [Move window to tag]
-│ ├── update_bar() [Calculate occupied tags, redraw bar]
-│ ├── update_focus_visuals() [Set border colors]
-│ ├── update_window_visibility() [Map/unmap windows based on tags]
-│ └── apply_layout() [Position all windows below bar]
+│ ├── try_reload_config() [Hot-reload Lua config]
+│ └── ... [Window/tag/focus management]
│
-├── config.rs [CONFIGURATION - all settings here]
-│ ├── BORDER_WIDTH, BORDER_FOCUSED, BORDER_UNFOCUSED
-│ ├── FONT [XFT font string]
-│ ├── TAG_COUNT, TAGS [Workspace configuration]
-│ ├── TERMINAL, MODKEY
-│ ├── ColorScheme [Foreground, background, border colors]
-│ ├── SCHEME_NORMAL, SCHEME_OCCUPIED, SCHEME_SELECTED
-│ ├── KEYBINDINGS [All keybinds as const array]
-│ └── STATUS_BLOCKS [Block configurations with format, command, interval]
+├── config/
+│ ├── mod.rs [Config module exports]
+│ ├── lua.rs [Lua config parser - loads and executes config.lua]
+│ └── lua_api.rs [Functional Lua API implementation]
+│ ├── register_api() [Set up oxwm.* functions in Lua]
+│ ├── register_spawn() [oxwm.spawn()]
+│ ├── register_key_module() [oxwm.key.bind(), oxwm.key.chord()]
+│ ├── register_gaps_module() [oxwm.gaps.*]
+│ ├── register_border_module() [oxwm.border.*]
+│ ├── register_client_module() [oxwm.client.*]
+│ ├── register_layout_module() [oxwm.layout.*]
+│ ├── register_tag_module() [oxwm.tag.*]
+│ ├── register_bar_module() [oxwm.bar.*]
+│ └── register_misc() [oxwm.set_terminal(), etc.]
│
├── bar/
│ ├── mod.rs [Re-exports: Bar, BlockCommand, BlockConfig]
@@ -292,35 +317,46 @@ src/
│ │ ├── new() [Create bar X11 window, load font, init blocks]
│ │ ├── draw() [Render tags + blocks with underlines]
│ │ ├── update_blocks() [Update block content based on intervals]
-│ │ ├── handle_click() [Detect which tag was clicked]
-│ │ └── invalidate() [Mark bar as needing redraw]
+│ │ └── handle_click() [Detect which tag was clicked]
│ ├── font.rs
│ │ ├── struct Font [XFT font wrapper]
-│ │ ├── struct FontDraw [XFT drawing context]
│ │ └── draw_text() [Render text with color]
│ └── blocks/
│ ├── mod.rs [Block trait, BlockConfig, BlockCommand enum]
│ ├── battery.rs [Battery status block]
│ ├── datetime.rs [Date/time formatting block]
-│ └── shell.rs [Shell command execution block]
+│ ├── ram.rs [RAM usage block]
+│ ├── shell.rs [Shell command execution block]
+│ └── static.rs [Static text block]
│
├── keyboard/
│ ├── mod.rs [Re-exports]
-│ ├── keycodes.rs [Key constants: Q, J, RETURN, etc]
+│ ├── keysyms.rs [X11 keysym constants]
│ └── handlers.rs
-│ ├── enum KeyAction [Spawn, KillClient, FocusStack, ViewTag, Restart, etc]
-│ ├── enum Arg [None, Int, Str, Array]
-│ ├── struct Key [Keybinding definition]
-│ ├── setup_keybinds() [Register keys with X11]
-│ └── handle_key_press() [Parse KeyPressEvent → KeyAction]
+│ ├── enum KeyAction [All keyboard actions]
+│ ├── enum Arg [Action arguments]
+│ ├── struct KeyBinding [Keybinding + keychord support]
+│ └── struct KeyPress [Individual key press in chord]
+│
+├── layout/
+│ ├── mod.rs [Layout trait definition]
+│ └── tiling.rs [Tiling layout implementation]
│
-└── layout/
- ├── mod.rs [Layout trait definition]
- └── tiling.rs
- └── TilingLayout::arrange() [Calculate window positions]
+└── errors.rs [Error types: WmError, ConfigError, etc.]
+
+templates/
+├── config.lua [Default config with functional API]
+└── oxwm.lua [LSP type definitions for autocomplete]
#+end_src
* Architecture Notes
+** Lua Configuration System
+OXWM uses mlua to embed a Lua interpreter. The functional API is implemented in =src/config/lua_api.rs=:
+- Each API function (e.g., =oxwm.border.set_width()=) is registered as a Lua function
+- Functions modify a shared ConfigBuilder that accumulates settings
+- When config execution completes, the builder produces the final Config struct
+- Type definitions in =templates/oxwm.lua= provide LSP autocomplete and documentation
+
** Tag System
Tags are implemented as bitmasks (TagMask = u32), allowing windows to belong to multiple tags simultaneously. Each window has an associated TagMask stored in a HashMap. Tags persist across WM restarts using X11 properties (_NET_CURRENT_DESKTOP for selected tags, _NET_CLIENT_INFO for per-window tags).
@@ -330,6 +366,7 @@ The bar uses a performance-optimized approach with a modular block system:
- Pre-calculates tag widths on creation
- Blocks update independently based on their configured intervals
- Supports custom colors and underline indicators
+- Color schemes (normal/occupied/selected) control tag appearance
- Easily extensible - add new block types in src/bar/blocks/
** Layout System
@@ -345,24 +382,17 @@ The tiling layout divides the screen into a master area (left half) and stack ar
- Fullscreen state should be maintained when switching tags
- Window should remain fullscreen when returning to its tag
-** PRIORITY Medium [1/4]
+** PRIORITY Medium [2/4]
- [ ] Add keybindings to increase/decrease window size
- Master area resize (Mod+H / Mod+L)
- Individual window resize for floating windows
-- [ ] Keychords:
- - Desired Behaviour:
-#+begin_src rust
-// Keychord: Mod+f, then f (same key twice)
-(keys: [
- (modifiers: [Mod], key: F),
- (modifiers: [], key: F),
-], action: Spawn, arg: "repos-dmenu.sh"),
-
-#+end_src
+- [X] Keychords
+ - Implemented with =oxwm.key.chord()= in the functional API
+ - Example: =oxwm.key.chord({ { {"Mod4"}, "Space" }, { {}, "T" } }, oxwm.spawn("st"))=
- [X] Fix cursor on hover for bar
- Bar should show pointer cursor on hover
- Indicate clickable tag areas
-- [ ] Add guess_terminal() function to default config.rs
+- [ ] Add auto-detect terminal to default config
- Auto-detect available terminal emulator
- Priority order: st → alacritty → kitty → wezterm → xterm
- Fallback to xterm if none found
diff --git a/resources/test-config.lua b/resources/test-config.lua
index a4bd390..4ea370a 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -1,180 +1,133 @@
--- OXWM Configuration File (Lua)
--- Migrated from config.ron
--- Edit this file and reload with Mod+Shift+R (no compilation needed!)
+---@meta
+---OXWM Test Configuration File (Lua)
+---Using the new functional API
+---Edit this file and reload with Mod+Alt+R
-local terminal = "st"
-local modkey = "Mod4"
+---Load type definitions for LSP
+---@module 'oxwm'
-- Color palette
local colors = {
- lavender = "#a9b1d6",
- light_blue = "#7aa2f7",
- grey = "#bbbbbb",
- purple = "#ad8ee6",
- cyan = "#0db9d7",
- bg = "#1a1b26",
- green = "#9ece6a",
- red = "#f7768e",
- fg = "#bbbbbb",
- blue = "#6dade3",
+ lavender = 0xa9b1d6,
+ light_blue = 0x7aa2f7,
+ grey = 0xbbbbbb,
+ purple = 0xad8ee6,
+ cyan = 0x0db9d7,
+ bg = 0x1a1b26,
+ green = 0x9ece6a,
+ red = 0xf7768e,
+ fg = 0xbbbbbb,
+ blue = 0x6dade3,
}
--- Main configuration table
-return {
- -- Appearance
- border_width = 2,
- border_focused = colors.blue,
- border_unfocused = colors.grey,
- font = "JetBrainsMono Nerd Font:style=Bold:size=12",
-
- -- Window gaps
- gaps_enabled = true,
- gap_inner_horizontal = 5,
- gap_inner_vertical = 5,
- gap_outer_horizontal = 5,
- gap_outer_vertical = 5,
-
- -- Basics
- modkey = "Mod1",
- terminal = "st",
-
- -- 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
- keybindings = {
- {
- keys = {
- { modifiers = { "Mod1" }, key = "Space" },
- { modifiers = { }, key = "T" },
- },
- action = "Spawn",
- arg = "st"
- },
- { modifiers = { "Mod1" }, key = "Return", action = "Spawn", arg = "st" },
- { modifiers = { "Mod1" }, key = "D", action = "Spawn", arg = { "sh", "-c", "dmenu_run -l 10" } },
- { modifiers = { "Mod1" }, key = "S", action = "Spawn", arg = { "sh", "-c", "maim -s | xclip -selection clipboard -t image/png" } },
- { modifiers = { "Mod1" }, key = "Q", action = "KillClient" },
- { modifiers = { "Mod1", "Shift" }, key = "Slash", action = "ShowKeybindOverlay" },
- { modifiers = { "Mod1", "Shift" }, key = "F", action = "ToggleFullScreen" },
- { modifiers = { "Mod1", "Shift" }, key = "Space", action = "ToggleFloating" },
- { modifiers = { "Mod1" }, key = "F", action = "ChangeLayout", arg = "normie" },
- { modifiers = { "Mod1" }, key = "C", action = "ChangeLayout", arg = "tiling" },
- { modifiers = { "Mod1" }, key = "N", action = "CycleLayout" },
- { modifiers = { "Mod1" }, key = "A", action = "ToggleGaps" },
- { modifiers = { "Mod1", "Shift" }, key = "Q", action = "Quit" },
- { modifiers = { "Mod1", "Shift" }, key = "R", action = "Restart" },
- { modifiers = { "Mod1" }, key = "H", action = "FocusDirection", arg = 2 },
- { modifiers = { "Mod1" }, key = "J", action = "FocusDirection", arg = 1 },
- { modifiers = { "Mod1" }, key = "K", action = "FocusDirection", arg = 0 },
- { modifiers = { "Mod1" }, key = "L", action = "FocusDirection", arg = 3 },
- { modifiers = { "Mod1", "Shift" }, key = "H", action = "SwapDirection", arg = 2 },
- { modifiers = { "Mod1", "Shift" }, key = "J", action = "SwapDirection", arg = 1 },
- { modifiers = { "Mod1", "Shift" }, key = "K", action = "SwapDirection", arg = 0 },
- { modifiers = { "Mod1", "Shift" }, key = "L", action = "SwapDirection", arg = 3 },
- { modifiers = { "Mod1" }, key = "1", action = "ViewTag", arg = 0 },
- { modifiers = { "Mod1" }, key = "2", action = "ViewTag", arg = 1 },
- { modifiers = { "Mod1" }, key = "3", action = "ViewTag", arg = 2 },
- { modifiers = { "Mod1" }, key = "4", action = "ViewTag", arg = 3 },
- { modifiers = { "Mod1" }, key = "5", action = "ViewTag", arg = 4 },
- { modifiers = { "Mod1" }, key = "6", action = "ViewTag", arg = 5 },
- { modifiers = { "Mod1" }, key = "7", action = "ViewTag", arg = 6 },
- { modifiers = { "Mod1" }, key = "8", action = "ViewTag", arg = 7 },
- { modifiers = { "Mod1" }, key = "9", action = "ViewTag", arg = 8 },
- { modifiers = { "Mod1", "Shift" }, key = "1", action = "MoveToTag", arg = 0 },
- { modifiers = { "Mod1", "Shift" }, key = "2", action = "MoveToTag", arg = 1 },
- { modifiers = { "Mod1", "Shift" }, key = "3", action = "MoveToTag", arg = 2 },
- { modifiers = { "Mod1", "Shift" }, key = "4", action = "MoveToTag", arg = 3 },
- { modifiers = { "Mod1", "Shift" }, key = "5", action = "MoveToTag", arg = 4 },
- { modifiers = { "Mod1", "Shift" }, key = "6", action = "MoveToTag", arg = 5 },
- { modifiers = { "Mod1", "Shift" }, key = "7", action = "MoveToTag", arg = 6 },
- { modifiers = { "Mod1", "Shift" }, key = "8", action = "MoveToTag", arg = 7 },
- { modifiers = { "Mod1", "Shift" }, key = "9", action = "MoveToTag", arg = 8 },
- },
-
- -- Status bar blocks
- status_blocks = {
- {
- format = "",
- command = "Battery",
- battery_formats = {
- charging = " Bat: {}%",
- discharging = " Bat:{}%",
- full = " Bat: {}%"
- },
- interval_secs = 30,
- color = colors.green,
- underline = true
- },
- {
- format = " │ ",
- command = "Static",
- interval_secs = 999999999,
- color = colors.lavender,
- underline = false
- },
- {
- format = " {used}/{total} GB",
- command = "Ram",
- interval_secs = 5,
- color = colors.light_blue,
- underline = true
- },
- {
- format = " │ ",
- command = "Static",
- interval_secs = 999999999,
- color = colors.lavender,
- underline = false
- },
- {
- format = " {}",
- 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 = "#444444"
- },
- 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 = {},
-}
+-- Basic settings
+oxwm.set_terminal("st")
+oxwm.set_modkey("Mod1")
+oxwm.set_tags({ "1", "2", "3", "4", "5", "6", "7", "8", "9" })
+
+-- Layout symbol overrides
+oxwm.set_layout_symbol("tiling", "[T]")
+oxwm.set_layout_symbol("normie", "[F]")
+
+-- Border configuration
+oxwm.border.set_width(2)
+oxwm.border.set_focused_color(colors.blue)
+oxwm.border.set_unfocused_color(colors.grey)
+
+-- Gap configuration
+oxwm.gaps.set_enabled(true)
+oxwm.gaps.set_inner(5, 5)
+oxwm.gaps.set_outer(5, 5)
+
+-- Bar configuration
+oxwm.bar.set_font("JetBrainsMono Nerd Font:style=Bold:size=12")
+
+-- Bar color schemes (for tag display)
+oxwm.bar.set_scheme_normal(colors.fg, colors.bg, 0x444444)
+oxwm.bar.set_scheme_occupied(colors.cyan, colors.bg, colors.cyan)
+oxwm.bar.set_scheme_selected(colors.cyan, colors.bg, colors.purple)
+
+-- Keybindings
+
+-- Keychord: Mod1+Space then T to spawn terminal
+oxwm.key.chord({
+ { { "Mod1" }, "Space" },
+ { {}, "T" }
+}, oxwm.spawn("st"))
+
+-- Basic window management
+oxwm.key.bind({ "Mod1" }, "Return", oxwm.spawn("st"))
+oxwm.key.bind({ "Mod1" }, "D", oxwm.spawn({ "sh", "-c", "dmenu_run -l 10" }))
+oxwm.key.bind({ "Mod1" }, "S", oxwm.spawn({ "sh", "-c", "maim -s | xclip -selection clipboard -t image/png" }))
+oxwm.key.bind({ "Mod1" }, "Q", oxwm.client.kill())
+
+-- Keybind overlay
+oxwm.key.bind({ "Mod1", "Shift" }, "Slash", oxwm.show_keybinds())
+
+-- Client actions
+oxwm.key.bind({ "Mod1", "Shift" }, "F", oxwm.client.toggle_fullscreen())
+oxwm.key.bind({ "Mod1", "Shift" }, "Space", oxwm.client.toggle_floating())
+
+-- Layout management
+oxwm.key.bind({ "Mod1" }, "F", oxwm.layout.set("normie"))
+oxwm.key.bind({ "Mod1" }, "C", oxwm.layout.set("tiling"))
+oxwm.key.bind({ "Mod1" }, "N", oxwm.layout.cycle())
+
+-- Gaps toggle
+oxwm.key.bind({ "Mod1" }, "A", oxwm.toggle_gaps())
+
+-- WM controls
+oxwm.key.bind({ "Mod1", "Shift" }, "Q", oxwm.quit())
+oxwm.key.bind({ "Mod1", "Shift" }, "R", oxwm.restart())
+
+-- Focus direction (vim keys)
+oxwm.key.bind({ "Mod1" }, "H", oxwm.client.focus_direction("left"))
+oxwm.key.bind({ "Mod1" }, "J", oxwm.client.focus_direction("down"))
+oxwm.key.bind({ "Mod1" }, "K", oxwm.client.focus_direction("up"))
+oxwm.key.bind({ "Mod1" }, "L", oxwm.client.focus_direction("right"))
+
+-- Swap windows in direction
+oxwm.key.bind({ "Mod1", "Shift" }, "H", oxwm.client.swap_direction("left"))
+oxwm.key.bind({ "Mod1", "Shift" }, "J", oxwm.client.swap_direction("down"))
+oxwm.key.bind({ "Mod1", "Shift" }, "K", oxwm.client.swap_direction("up"))
+oxwm.key.bind({ "Mod1", "Shift" }, "L", oxwm.client.swap_direction("right"))
+
+-- Tag viewing
+oxwm.key.bind({ "Mod1" }, "1", oxwm.tag.view(0))
+oxwm.key.bind({ "Mod1" }, "2", oxwm.tag.view(1))
+oxwm.key.bind({ "Mod1" }, "3", oxwm.tag.view(2))
+oxwm.key.bind({ "Mod1" }, "4", oxwm.tag.view(3))
+oxwm.key.bind({ "Mod1" }, "5", oxwm.tag.view(4))
+oxwm.key.bind({ "Mod1" }, "6", oxwm.tag.view(5))
+oxwm.key.bind({ "Mod1" }, "7", oxwm.tag.view(6))
+oxwm.key.bind({ "Mod1" }, "8", oxwm.tag.view(7))
+oxwm.key.bind({ "Mod1" }, "9", oxwm.tag.view(8))
+
+-- Move window to tag
+oxwm.key.bind({ "Mod1", "Shift" }, "1", oxwm.tag.move_to(0))
+oxwm.key.bind({ "Mod1", "Shift" }, "2", oxwm.tag.move_to(1))
+oxwm.key.bind({ "Mod1", "Shift" }, "3", oxwm.tag.move_to(2))
+oxwm.key.bind({ "Mod1", "Shift" }, "4", oxwm.tag.move_to(3))
+oxwm.key.bind({ "Mod1", "Shift" }, "5", oxwm.tag.move_to(4))
+oxwm.key.bind({ "Mod1", "Shift" }, "6", oxwm.tag.move_to(5))
+oxwm.key.bind({ "Mod1", "Shift" }, "7", oxwm.tag.move_to(6))
+oxwm.key.bind({ "Mod1", "Shift" }, "8", oxwm.tag.move_to(7))
+oxwm.key.bind({ "Mod1", "Shift" }, "9", oxwm.tag.move_to(8))
+
+-- Status bar blocks
+oxwm.bar.add_block("", "Battery", {
+ charging = " Bat: {}%",
+ discharging = " Bat:{}%",
+ full = " Bat: {}%"
+}, 30, colors.green, true)
+
+oxwm.bar.add_block(" │ ", "Static", "", 999999999, colors.lavender, false)
+oxwm.bar.add_block(" {used}/{total} GB", "Ram", nil, 5, colors.light_blue, true)
+oxwm.bar.add_block(" │ ", "Static", "", 999999999, colors.lavender, false)
+oxwm.bar.add_block(" {}", "Shell", "uname -r", 999999999, colors.red, true)
+oxwm.bar.add_block(" │ ", "Static", "", 999999999, colors.lavender, false)
+oxwm.bar.add_block(" {}", "DateTime", "%a, %b %d - %-I:%M %P", 1, colors.cyan, true)
+
+-- Autostart commands (runs once at startup)
+-- oxwm.autostart("picom")
+-- oxwm.autostart("feh --bg-scale ~/wallpaper.jpg")
diff --git a/src/bin/main.rs b/src/bin/main.rs
index 9b1d7cd..519965b 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -19,11 +19,6 @@ fn main() -> Result<()> {
init_config()?;
return Ok(());
}
- Some("--migrate") => {
- let path = args.get(2).map(PathBuf::from);
- migrate_config(path)?;
- return Ok(());
- }
Some("--config") => {
if let Some(path) = args.get(2) {
custom_config_path = Some(PathBuf::from(path));
@@ -57,48 +52,52 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<oxwm::Config> {
} else {
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 {
+ if !lua_path.exists() {
+ // Check if user had an old RON config
+ let ron_path = config_dir.join("config.ron");
+ let had_ron_config = ron_path.exists();
+
println!("No config found at {:?}", config_dir);
println!("Creating default Lua config...");
init_config()?;
- config_dir.join("config.lua")
+
+ if had_ron_config {
+ println!("\n⚠️ NOTICE: OXWM has migrated to Lua configuration.");
+ println!(" Your old config.ron has been preserved, but is no longer used.");
+ println!(" Your settings have been reset to defaults.");
+ println!(" Please manually port your configuration to the new Lua format.");
+ println!(" See the new config.lua template for examples.\n");
+ }
}
+
+ lua_path
};
let config_str =
std::fs::read_to_string(&config_path).with_context(|| "Failed to read config file")?;
- let is_lua = config_path
- .extension()
- .and_then(|s| s.to_str())
- .map(|s| s == "lua")
- .unwrap_or(false);
-
- if is_lua {
- let config_dir = config_path.parent();
- oxwm::config::parse_lua_config(&config_str, config_dir)
- .with_context(|| "Failed to parse Lua config")
- } else {
- oxwm::config::parse_config(&config_str).with_context(|| "Failed to parse RON config")
- }
+ let config_dir = config_path.parent();
+ oxwm::config::parse_lua_config(&config_str, config_dir)
+ .with_context(|| "Failed to parse Lua config")
}
fn init_config() -> Result<()> {
let config_dir = get_config_path();
std::fs::create_dir_all(&config_dir)?;
+ // Copy config.lua template
let config_template = include_str!("../../templates/config.lua");
let config_path = config_dir.join("config.lua");
-
std::fs::write(&config_path, config_template)?;
+ // Copy oxwm.lua API definitions for LSP support
+ let oxwm_lua_template = include_str!("../../templates/oxwm.lua");
+ let oxwm_lua_path = config_dir.join("oxwm.lua");
+ std::fs::write(&oxwm_lua_path, oxwm_lua_template)?;
+
println!("✓ Config created at {:?}", config_path);
+ println!("✓ LSP definitions installed at {:?}", oxwm_lua_path);
println!(" Edit the file and reload with Mod+Shift+R");
println!(" No compilation needed - changes take effect immediately!");
@@ -111,62 +110,20 @@ fn get_config_path() -> PathBuf {
.join("oxwm")
}
-fn migrate_config(custom_path: Option<PathBuf>) -> Result<()> {
- let ron_path = if let Some(path) = custom_path {
- path
- } else {
- get_config_path().join("config.ron")
- };
-
- if !ron_path.exists() {
- eprintln!("Error: RON config file not found at {:?}", ron_path);
- eprintln!("Please specify a path: oxwm --migrate <path/to/config.ron>");
- std::process::exit(1);
- }
-
- println!("Migrating RON config to Lua...");
- println!(" Reading: {:?}", ron_path);
-
- let ron_content = std::fs::read_to_string(&ron_path)
- .with_context(|| format!("Failed to read RON config from {:?}", ron_path))?;
-
- let lua_content = oxwm::config::migrate::ron_to_lua(&ron_content)
- .with_context(|| "Failed to migrate RON config to Lua")?;
-
- let lua_path = ron_path.with_extension("lua");
-
- std::fs::write(&lua_path, lua_content)
- .with_context(|| format!("Failed to write Lua config to {:?}", lua_path))?;
-
- println!("✓ Migration complete!");
- println!(" Output: {:?}", lua_path);
- println!("\nYour old config.ron is still intact.");
- println!(
- "Review the new config.lua and then you can delete config.ron if everything looks good."
- );
-
- Ok(())
-}
-
fn print_help() {
println!("OXWM - A dynamic window manager written in Rust\n");
println!("USAGE:");
println!(" oxwm [OPTIONS]\n");
println!("OPTIONS:");
println!(" --init Create default config in ~/.config/oxwm/config.lua");
- println!(
- " --migrate [PATH] Convert RON config to Lua (default: ~/.config/oxwm/config.ron)"
- );
- println!(" --config <PATH> Use custom config file (.lua or .ron)");
+ println!(" --config <PATH> Use custom config file");
println!(" --version Print version information");
println!(" --help Print this help message\n");
println!("CONFIG:");
- println!(" Location: ~/.config/oxwm/config.lua (or config.ron for legacy)");
+ println!(" Location: ~/.config/oxwm/config.lua");
println!(" Edit the config file and use Mod+Shift+R to reload");
- println!(" No compilation needed - instant hot-reload!\n");
- println!("MIGRATION:");
- println!(" To migrate from RON to Lua: oxwm --migrate");
- println!(" Or specify a custom path: oxwm --migrate /path/to/config.ron\n");
+ println!(" No compilation needed - instant hot-reload!");
+ println!(" LSP support included with oxwm.lua type definitions\n");
println!("FIRST RUN:");
println!(" Run 'oxwm --init' to create a config file");
println!(" Or just start oxwm and it will create one automatically\n");
diff --git a/src/config/lua.rs b/src/config/lua.rs
index 6a42c0e..74a0973 100644
--- a/src/config/lua.rs
+++ b/src/config/lua.rs
@@ -7,6 +7,8 @@ use crate::{ColorScheme, LayoutSymbolOverride};
use mlua::{Lua, Table, Value};
use x11rb::protocol::xproto::KeyButMask;
+use super::lua_api;
+
pub fn parse_lua_config(
input: &str,
config_dir: Option<&std::path::Path>,
@@ -22,6 +24,38 @@ pub fn parse_lua_config(
}
}
+ let builder = lua_api::register_api(&lua)?;
+
+ lua.load(input)
+ .exec()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to execute Lua config: {}", e)))?;
+
+ let builder_data = builder.borrow().clone();
+
+ return Ok(crate::Config {
+ border_width: builder_data.border_width,
+ border_focused: builder_data.border_focused,
+ border_unfocused: builder_data.border_unfocused,
+ font: builder_data.font,
+ gaps_enabled: builder_data.gaps_enabled,
+ gap_inner_horizontal: builder_data.gap_inner_horizontal,
+ gap_inner_vertical: builder_data.gap_inner_vertical,
+ gap_outer_horizontal: builder_data.gap_outer_horizontal,
+ gap_outer_vertical: builder_data.gap_outer_vertical,
+ terminal: builder_data.terminal,
+ modkey: builder_data.modkey,
+ tags: builder_data.tags,
+ layout_symbols: builder_data.layout_symbols,
+ keybindings: builder_data.keybindings,
+ status_blocks: builder_data.status_blocks,
+ scheme_normal: builder_data.scheme_normal,
+ scheme_occupied: builder_data.scheme_occupied,
+ scheme_selected: builder_data.scheme_selected,
+ autostart: builder_data.autostart,
+ });
+
+ #[allow(unreachable_code)]
+ {
let config: Table = lua
.load(input)
.eval()
@@ -72,6 +106,7 @@ pub fn parse_lua_config(
scheme_selected,
autostart,
})
+ }
}
fn get_table_field<T>(table: &Table, field: &str) -> Result<T, ConfigError>
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
new file mode 100644
index 0000000..d1c1840
--- /dev/null
+++ b/src/config/lua_api.rs
@@ -0,0 +1,717 @@
+use mlua::{Lua, Table, Value};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::bar::BlockConfig;
+use crate::errors::ConfigError;
+use crate::keyboard::handlers::{Arg, KeyAction, KeyBinding, KeyPress};
+use crate::keyboard::keysyms::{self, Keysym};
+use crate::ColorScheme;
+use x11rb::protocol::xproto::KeyButMask;
+
+#[derive(Clone)]
+pub struct ConfigBuilder {
+ pub border_width: u32,
+ pub border_focused: u32,
+ pub border_unfocused: u32,
+ pub font: String,
+ pub gaps_enabled: bool,
+ pub gap_inner_horizontal: u32,
+ pub gap_inner_vertical: u32,
+ pub gap_outer_horizontal: u32,
+ pub gap_outer_vertical: u32,
+ pub terminal: String,
+ pub modkey: KeyButMask,
+ pub tags: Vec<String>,
+ pub layout_symbols: Vec<crate::LayoutSymbolOverride>,
+ pub keybindings: Vec<KeyBinding>,
+ pub status_blocks: Vec<BlockConfig>,
+ pub scheme_normal: ColorScheme,
+ pub scheme_occupied: ColorScheme,
+ pub scheme_selected: ColorScheme,
+ pub autostart: Vec<String>,
+}
+
+impl Default for ConfigBuilder {
+ fn default() -> Self {
+ Self {
+ border_width: 2,
+ border_focused: 0x6dade3,
+ border_unfocused: 0xbbbbbb,
+ font: "monospace:style=Bold:size=10".to_string(),
+ gaps_enabled: true,
+ gap_inner_horizontal: 5,
+ gap_inner_vertical: 5,
+ gap_outer_horizontal: 5,
+ gap_outer_vertical: 5,
+ terminal: "st".to_string(),
+ modkey: KeyButMask::MOD4,
+ tags: vec!["1".into(), "2".into(), "3".into()],
+ layout_symbols: Vec::new(),
+ keybindings: Vec::new(),
+ status_blocks: Vec::new(),
+ scheme_normal: ColorScheme {
+ foreground: 0xffffff,
+ background: 0x000000,
+ underline: 0x444444,
+ },
+ scheme_occupied: ColorScheme {
+ foreground: 0xffffff,
+ background: 0x000000,
+ underline: 0x444444,
+ },
+ scheme_selected: ColorScheme {
+ foreground: 0xffffff,
+ background: 0x000000,
+ underline: 0x444444,
+ },
+ autostart: Vec::new(),
+ }
+ }
+}
+
+type SharedBuilder = Rc<RefCell<ConfigBuilder>>;
+
+pub fn register_api(lua: &Lua) -> Result<SharedBuilder, ConfigError> {
+ let builder = Rc::new(RefCell::new(ConfigBuilder::default()));
+
+ let oxwm_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create oxwm table: {}", e)))?;
+
+ register_spawn(&lua, &oxwm_table, builder.clone())?;
+ register_key_module(&lua, &oxwm_table, builder.clone())?;
+ register_gaps_module(&lua, &oxwm_table, builder.clone())?;
+ register_border_module(&lua, &oxwm_table, builder.clone())?;
+ register_client_module(&lua, &oxwm_table)?;
+ register_layout_module(&lua, &oxwm_table)?;
+ register_tag_module(&lua, &oxwm_table)?;
+ register_bar_module(&lua, &oxwm_table, builder.clone())?;
+ register_misc(&lua, &oxwm_table, builder.clone())?;
+
+ lua.globals().set("oxwm", oxwm_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set oxwm global: {}", e)))?;
+
+ Ok(builder)
+}
+
+fn register_spawn(lua: &Lua, parent: &Table, _builder: SharedBuilder) -> Result<(), ConfigError> {
+ let spawn = lua.create_function(|lua, cmd: Value| {
+ create_action_table(lua, "Spawn", cmd)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create spawn: {}", e)))?;
+ parent.set("spawn", spawn)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set spawn: {}", e)))?;
+ Ok(())
+}
+
+fn register_key_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+ let key_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create key table: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let bind = lua.create_function(move |lua, (mods, key, action): (Value, String, Value)| {
+ let modifiers = parse_modifiers_value(lua, mods)?;
+ let keysym = parse_keysym(&key)?;
+ let (key_action, arg) = parse_action_value(lua, action)?;
+
+ let binding = KeyBinding::single_key(modifiers, keysym, key_action, arg);
+ builder_clone.borrow_mut().keybindings.push(binding);
+
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create bind: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let chord = lua.create_function(move |lua, (keys, action): (Table, Value)| {
+ let mut key_presses = Vec::new();
+
+ for i in 1..=keys.len()? {
+ let key_spec: Table = keys.get(i)?;
+ let mods: Value = key_spec.get(1)?;
+ let key: String = key_spec.get(2)?;
+
+ let modifiers = parse_modifiers_value(lua, mods)?;
+ let keysym = parse_keysym(&key)?;
+
+ key_presses.push(KeyPress { modifiers, keysym });
+ }
+
+ let (key_action, arg) = parse_action_value(lua, action)?;
+ let binding = KeyBinding::new(key_presses, key_action, arg);
+ builder_clone.borrow_mut().keybindings.push(binding);
+
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create chord: {}", e)))?;
+
+ key_table.set("bind", bind)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set bind: {}", e)))?;
+ key_table.set("chord", chord)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set chord: {}", e)))?;
+ parent.set("key", key_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set key: {}", e)))?;
+ Ok(())
+}
+
+fn register_gaps_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+ let gaps_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create gaps table: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_enabled = lua.create_function(move |_, enabled: bool| {
+ builder_clone.borrow_mut().gaps_enabled = enabled;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_enabled: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let enable = lua.create_function(move |_, ()| {
+ builder_clone.borrow_mut().gaps_enabled = true;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create enable: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let disable = lua.create_function(move |_, ()| {
+ builder_clone.borrow_mut().gaps_enabled = false;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create disable: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_inner = lua.create_function(move |_, (h, v): (u32, u32)| {
+ let mut b = builder_clone.borrow_mut();
+ b.gap_inner_horizontal = h;
+ b.gap_inner_vertical = v;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_inner: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_outer = lua.create_function(move |_, (h, v): (u32, u32)| {
+ let mut b = builder_clone.borrow_mut();
+ b.gap_outer_horizontal = h;
+ b.gap_outer_vertical = v;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_outer: {}", e)))?;
+
+ gaps_table.set("set_enabled", set_enabled)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_enabled: {}", e)))?;
+ gaps_table.set("enable", enable)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set enable: {}", e)))?;
+ gaps_table.set("disable", disable)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set disable: {}", e)))?;
+ gaps_table.set("set_inner", set_inner)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_inner: {}", e)))?;
+ gaps_table.set("set_outer", set_outer)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_outer: {}", e)))?;
+ parent.set("gaps", gaps_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set gaps: {}", e)))?;
+ Ok(())
+}
+
+fn register_border_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+ let border_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create border table: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_width = lua.create_function(move |_, width: u32| {
+ builder_clone.borrow_mut().border_width = width;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_width: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_focused_color = lua.create_function(move |_, color: Value| {
+ let color_u32 = parse_color_value(color)?;
+ builder_clone.borrow_mut().border_focused = color_u32;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_focused_color: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_unfocused_color = lua.create_function(move |_, color: Value| {
+ let color_u32 = parse_color_value(color)?;
+ builder_clone.borrow_mut().border_unfocused = color_u32;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_unfocused_color: {}", e)))?;
+
+ border_table.set("set_width", set_width)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_width: {}", e)))?;
+ border_table.set("set_focused_color", set_focused_color)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_focused_color: {}", e)))?;
+ border_table.set("set_unfocused_color", set_unfocused_color)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_unfocused_color: {}", e)))?;
+ parent.set("border", border_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set border: {}", e)))?;
+ Ok(())
+}
+
+fn register_client_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
+ let client_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create client table: {}", e)))?;
+
+ let kill = lua.create_function(|lua, ()| {
+ create_action_table(lua, "KillClient", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create kill: {}", e)))?;
+
+ let toggle_fullscreen = lua.create_function(|lua, ()| {
+ create_action_table(lua, "ToggleFullScreen", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create toggle_fullscreen: {}", e)))?;
+
+ let toggle_floating = lua.create_function(|lua, ()| {
+ create_action_table(lua, "ToggleFloating", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create toggle_floating: {}", e)))?;
+
+ let focus_stack = lua.create_function(|lua, dir: i32| {
+ create_action_table(lua, "FocusStack", Value::Integer(dir as i64))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create focus_stack: {}", e)))?;
+
+ let focus_direction = lua.create_function(|lua, dir: String| {
+ let dir_int = direction_string_to_int(&dir)?;
+ create_action_table(lua, "FocusDirection", Value::Integer(dir_int))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create focus_direction: {}", e)))?;
+
+ let swap_direction = lua.create_function(|lua, dir: String| {
+ let dir_int = direction_string_to_int(&dir)?;
+ create_action_table(lua, "SwapDirection", Value::Integer(dir_int))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create swap_direction: {}", e)))?;
+
+ let smart_move = lua.create_function(|lua, dir: String| {
+ let dir_int = direction_string_to_int(&dir)?;
+ create_action_table(lua, "SmartMoveWin", Value::Integer(dir_int))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create smart_move: {}", e)))?;
+
+ let exchange = lua.create_function(|lua, ()| {
+ create_action_table(lua, "ExchangeClient", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create exchange: {}", e)))?;
+
+ client_table.set("kill", kill)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set kill: {}", e)))?;
+ client_table.set("toggle_fullscreen", toggle_fullscreen)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set toggle_fullscreen: {}", e)))?;
+ client_table.set("toggle_floating", toggle_floating)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set toggle_floating: {}", e)))?;
+ client_table.set("focus_stack", focus_stack)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set focus_stack: {}", e)))?;
+ client_table.set("focus_direction", focus_direction)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set focus_direction: {}", e)))?;
+ client_table.set("swap_direction", swap_direction)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set swap_direction: {}", e)))?;
+ client_table.set("smart_move", smart_move)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set smart_move: {}", e)))?;
+ client_table.set("exchange", exchange)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set exchange: {}", e)))?;
+
+ parent.set("client", client_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set client: {}", e)))?;
+ Ok(())
+}
+
+fn register_layout_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
+ let layout_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create layout table: {}", e)))?;
+
+ let cycle = lua.create_function(|lua, ()| {
+ create_action_table(lua, "CycleLayout", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create cycle: {}", e)))?;
+
+ let set = lua.create_function(|lua, name: String| {
+ create_action_table(lua, "ChangeLayout", Value::String(lua.create_string(&name)?))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set: {}", e)))?;
+
+ layout_table.set("cycle", cycle)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set cycle: {}", e)))?;
+ layout_table.set("set", set)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set: {}", e)))?;
+ parent.set("layout", layout_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set layout: {}", e)))?;
+ Ok(())
+}
+
+fn register_tag_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
+ let tag_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create tag table: {}", e)))?;
+
+ let view = lua.create_function(|lua, idx: i32| {
+ create_action_table(lua, "ViewTag", Value::Integer(idx as i64))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create view: {}", e)))?;
+
+ let move_to = lua.create_function(|lua, idx: i32| {
+ create_action_table(lua, "MoveToTag", Value::Integer(idx as i64))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create move_to: {}", e)))?;
+
+ tag_table.set("view", view)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set view: {}", e)))?;
+ tag_table.set("move_to", move_to)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set move_to: {}", e)))?;
+ parent.set("tag", tag_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set tag: {}", e)))?;
+ Ok(())
+}
+
+fn register_bar_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+ let bar_table = lua.create_table()
+ .map_err(|e| ConfigError::LuaError(format!("Failed to create bar table: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_font = lua.create_function(move |_, font: String| {
+ builder_clone.borrow_mut().font = font;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_font: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let add_block = lua.create_function(move |_, (format, command, arg, interval, color, underline): (String, String, Option<Value>, u64, Value, bool)| {
+ use crate::bar::BlockCommand;
+
+ let cmd = match command.as_str() {
+ "DateTime" => {
+ let fmt = arg.and_then(|v| {
+ if let Value::String(s) = v {
+ s.to_str().ok().map(|s| s.to_string())
+ } else {
+ None
+ }
+ }).ok_or_else(|| mlua::Error::RuntimeError("DateTime requires format string".into()))?;
+ BlockCommand::DateTime(fmt)
+ }
+ "Shell" => {
+ let cmd_str = arg.and_then(|v| {
+ if let Value::String(s) = v {
+ s.to_str().ok().map(|s| s.to_string())
+ } else {
+ None
+ }
+ }).ok_or_else(|| mlua::Error::RuntimeError("Shell requires command string".into()))?;
+ BlockCommand::Shell(cmd_str)
+ }
+ "Ram" => BlockCommand::Ram,
+ "Static" => {
+ let text = arg.and_then(|v| {
+ if let Value::String(s) = v {
+ s.to_str().ok().map(|s| s.to_string())
+ } else {
+ None
+ }
+ }).unwrap_or_default();
+ BlockCommand::Static(text)
+ }
+ "Battery" => {
+ let formats = arg.and_then(|v| {
+ if let Value::Table(t) = v {
+ Some(t)
+ } else {
+ None
+ }
+ }).ok_or_else(|| mlua::Error::RuntimeError("Battery requires formats table".into()))?;
+
+ let charging: String = formats.get("charging")?;
+ let discharging: String = formats.get("discharging")?;
+ let full: String = formats.get("full")?;
+
+ BlockCommand::Battery {
+ format_charging: charging,
+ format_discharging: discharging,
+ format_full: full,
+ }
+ }
+ _ => return Err(mlua::Error::RuntimeError(format!("Unknown block command: {}", command))),
+ };
+
+ let color_u32 = parse_color_value(color)?;
+
+ let block = crate::bar::BlockConfig {
+ format,
+ command: cmd,
+ interval_secs: interval,
+ color: color_u32,
+ underline,
+ };
+
+ builder_clone.borrow_mut().status_blocks.push(block);
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create add_block: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_scheme_normal = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
+ let foreground = parse_color_value(fg)?;
+ let background = parse_color_value(bg)?;
+ let underline = parse_color_value(ul)?;
+
+ builder_clone.borrow_mut().scheme_normal = ColorScheme {
+ foreground,
+ background,
+ underline,
+ };
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_scheme_normal: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_scheme_occupied = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
+ let foreground = parse_color_value(fg)?;
+ let background = parse_color_value(bg)?;
+ let underline = parse_color_value(ul)?;
+
+ builder_clone.borrow_mut().scheme_occupied = ColorScheme {
+ foreground,
+ background,
+ underline,
+ };
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_scheme_occupied: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_scheme_selected = lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
+ let foreground = parse_color_value(fg)?;
+ let background = parse_color_value(bg)?;
+ let underline = parse_color_value(ul)?;
+
+ builder_clone.borrow_mut().scheme_selected = ColorScheme {
+ foreground,
+ background,
+ underline,
+ };
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_scheme_selected: {}", e)))?;
+
+ bar_table.set("set_font", set_font)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_font: {}", e)))?;
+ bar_table.set("add_block", add_block)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set add_block: {}", e)))?;
+ bar_table.set("set_scheme_normal", set_scheme_normal)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_scheme_normal: {}", e)))?;
+ bar_table.set("set_scheme_occupied", set_scheme_occupied)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_scheme_occupied: {}", e)))?;
+ bar_table.set("set_scheme_selected", set_scheme_selected)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_scheme_selected: {}", e)))?;
+ parent.set("bar", bar_table)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set bar: {}", e)))?;
+ Ok(())
+}
+
+fn register_misc(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
+ let builder_clone = builder.clone();
+ let set_terminal = lua.create_function(move |_, term: String| {
+ builder_clone.borrow_mut().terminal = term;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_terminal: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_modkey = lua.create_function(move |_, modkey_str: String| {
+ let modkey = parse_modkey_string(&modkey_str)
+ .map_err(|e| mlua::Error::RuntimeError(format!("{}", e)))?;
+ builder_clone.borrow_mut().modkey = modkey;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_modkey: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_tags = lua.create_function(move |_, tags: Vec<String>| {
+ builder_clone.borrow_mut().tags = tags;
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_tags: {}", e)))?;
+
+ let quit = lua.create_function(|lua, ()| {
+ create_action_table(lua, "Quit", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create quit: {}", e)))?;
+
+ let restart = lua.create_function(|lua, ()| {
+ create_action_table(lua, "Restart", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create restart: {}", e)))?;
+
+ let recompile = lua.create_function(|lua, ()| {
+ create_action_table(lua, "Recompile", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create recompile: {}", e)))?;
+
+ let toggle_gaps = lua.create_function(|lua, ()| {
+ create_action_table(lua, "ToggleGaps", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create toggle_gaps: {}", e)))?;
+
+ let show_keybinds = lua.create_function(|lua, ()| {
+ create_action_table(lua, "ShowKeybindOverlay", Value::Nil)
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create show_keybinds: {}", e)))?;
+
+ let focus_monitor = lua.create_function(|lua, idx: i32| {
+ create_action_table(lua, "FocusMonitor", Value::Integer(idx as i64))
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create focus_monitor: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let set_layout_symbol = lua.create_function(move |_, (name, symbol): (String, String)| {
+ builder_clone.borrow_mut().layout_symbols.push(crate::LayoutSymbolOverride {
+ name,
+ symbol,
+ });
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create set_layout_symbol: {}", e)))?;
+
+ let builder_clone = builder.clone();
+ let autostart = lua.create_function(move |_, cmd: String| {
+ builder_clone.borrow_mut().autostart.push(cmd);
+ Ok(())
+ }).map_err(|e| ConfigError::LuaError(format!("Failed to create autostart: {}", e)))?;
+
+ parent.set("set_terminal", set_terminal)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_terminal: {}", e)))?;
+ parent.set("set_modkey", set_modkey)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_modkey: {}", e)))?;
+ parent.set("set_tags", set_tags)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_tags: {}", e)))?;
+ parent.set("set_layout_symbol", set_layout_symbol)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set set_layout_symbol: {}", e)))?;
+ parent.set("autostart", autostart)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set autostart: {}", e)))?;
+ parent.set("quit", quit)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set quit: {}", e)))?;
+ parent.set("restart", restart)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set restart: {}", e)))?;
+ parent.set("recompile", recompile)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set recompile: {}", e)))?;
+ parent.set("toggle_gaps", toggle_gaps)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set toggle_gaps: {}", e)))?;
+ parent.set("show_keybinds", show_keybinds)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set show_keybinds: {}", e)))?;
+ parent.set("focus_monitor", focus_monitor)
+ .map_err(|e| ConfigError::LuaError(format!("Failed to set focus_monitor: {}", e)))?;
+ Ok(())
+}
+
+fn parse_modifiers_value(_lua: &Lua, value: Value) -> mlua::Result<Vec<KeyButMask>> {
+ match value {
+ Value::Table(t) => {
+ let mut mods = Vec::new();
+ for i in 1..=t.len()? {
+ let mod_str: String = t.get(i)?;
+ let mask = parse_modkey_string(&mod_str)
+ .map_err(|e| mlua::Error::RuntimeError(format!("{}", e)))?;
+ mods.push(mask);
+ }
+ Ok(mods)
+ }
+ Value::String(s) => {
+ let s_str = s.to_str()?;
+ let mask = parse_modkey_string(&s_str)
+ .map_err(|e| mlua::Error::RuntimeError(format!("{}", e)))?;
+ Ok(vec![mask])
+ }
+ _ => Err(mlua::Error::RuntimeError(
+ "modifiers must be string or table".into(),
+ )),
+ }
+}
+
+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_keysym(key: &str) -> mlua::Result<Keysym> {
+ keysyms::keysym_from_str(key)
+ .ok_or_else(|| mlua::Error::RuntimeError(format!("Unknown key: {}", key)))
+}
+
+fn parse_action_value(_lua: &Lua, value: Value) -> mlua::Result<(KeyAction, Arg)> {
+ match value {
+ Value::Table(t) => {
+ // Check if this table has our action metadata
+ if let Ok(action_name) = t.get::<String>("__action") {
+ let action = string_to_action(&action_name)?;
+ let arg = if let Ok(arg_val) = t.get::<Value>("__arg") {
+ value_to_arg(arg_val)?
+ } else {
+ Arg::None
+ };
+ return Ok((action, arg));
+ }
+
+ Err(mlua::Error::RuntimeError(
+ "action table missing __action field".into(),
+ ))
+ }
+ _ => Err(mlua::Error::RuntimeError(
+ "action must be a table returned by oxwm actions".into(),
+ )),
+ }
+}
+
+fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
+ match s {
+ "Spawn" => Ok(KeyAction::Spawn),
+ "KillClient" => Ok(KeyAction::KillClient),
+ "FocusStack" => Ok(KeyAction::FocusStack),
+ "FocusDirection" => Ok(KeyAction::FocusDirection),
+ "SwapDirection" => Ok(KeyAction::SwapDirection),
+ "Quit" => Ok(KeyAction::Quit),
+ "Restart" => Ok(KeyAction::Restart),
+ "Recompile" => Ok(KeyAction::Recompile),
+ "ViewTag" => Ok(KeyAction::ViewTag),
+ "ToggleGaps" => Ok(KeyAction::ToggleGaps),
+ "ToggleFullScreen" => Ok(KeyAction::ToggleFullScreen),
+ "ToggleFloating" => Ok(KeyAction::ToggleFloating),
+ "ChangeLayout" => Ok(KeyAction::ChangeLayout),
+ "CycleLayout" => Ok(KeyAction::CycleLayout),
+ "MoveToTag" => Ok(KeyAction::MoveToTag),
+ "FocusMonitor" => Ok(KeyAction::FocusMonitor),
+ "SmartMoveWin" => Ok(KeyAction::SmartMoveWin),
+ "ExchangeClient" => Ok(KeyAction::ExchangeClient),
+ "ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
+ _ => Err(mlua::Error::RuntimeError(format!("Unknown action: {}", s))),
+ }
+}
+
+fn value_to_arg(value: Value) -> mlua::Result<Arg> {
+ match value {
+ Value::Nil => Ok(Arg::None),
+ Value::String(s) => Ok(Arg::Str(s.to_str()?.to_string())),
+ Value::Integer(i) => Ok(Arg::Int(i as i32)),
+ Value::Number(n) => Ok(Arg::Int(n as i32)),
+ Value::Table(t) => {
+ let mut arr = Vec::new();
+ for i in 1..=t.len()? {
+ let item: String = t.get(i)?;
+ arr.push(item);
+ }
+ Ok(Arg::Array(arr))
+ }
+ _ => Ok(Arg::None),
+ }
+}
+
+fn create_action_table(lua: &Lua, action_name: &str, arg: Value) -> mlua::Result<Table> {
+ let table = lua.create_table()?;
+ table.set("__action", action_name)?;
+ table.set("__arg", arg)?;
+ Ok(table)
+}
+
+fn direction_string_to_int(dir: &str) -> mlua::Result<i64> {
+ match dir {
+ "up" => Ok(0),
+ "down" => Ok(1),
+ "left" => Ok(2),
+ "right" => Ok(3),
+ _ => Err(mlua::Error::RuntimeError(
+ format!("Invalid direction '{}', must be one of: up, down, left, right", dir)
+ )),
+ }
+}
+
+fn parse_color_value(value: Value) -> mlua::Result<u32> {
+ match value {
+ Value::Integer(i) => Ok(i as u32),
+ Value::Number(n) => Ok(n as u32),
+ Value::String(s) => {
+ let s = s.to_str()?;
+ if s.starts_with('#') {
+ u32::from_str_radix(&s[1..], 16)
+ .map_err(|e| mlua::Error::RuntimeError(format!("Invalid hex color: {}", e)))
+ } else if s.starts_with("0x") {
+ u32::from_str_radix(&s[2..], 16)
+ .map_err(|e| mlua::Error::RuntimeError(format!("Invalid hex color: {}", e)))
+ } else {
+ s.parse::<u32>()
+ .map_err(|e| mlua::Error::RuntimeError(format!("Invalid color: {}", e)))
+ }
+ }
+ _ => Err(mlua::Error::RuntimeError(
+ "color must be number or string".into(),
+ )),
+ }
+}
diff --git a/src/config/migrate.rs b/src/config/migrate.rs
deleted file mode 100644
index 2a48eaa..0000000
--- a/src/config/migrate.rs
+++ /dev/null
@@ -1,553 +0,0 @@
-use crate::errors::ConfigError;
-use std::collections::HashMap;
-
-pub fn ron_to_lua(ron_content: &str) -> Result<String, ConfigError> {
- let mut lua_output = String::new();
- let defines = extract_defines(ron_content);
-
- lua_output.push_str("-- OXWM Configuration File (Lua)\n");
- lua_output.push_str("-- Migrated from config.ron\n");
- lua_output.push_str("-- Edit this file and reload with Mod+Shift+R (no compilation needed!)\n\n");
-
- let terminal = resolve_value(&defines.get("$terminal").cloned().unwrap_or_else(|| "\"st\"".to_string()), &defines);
- let modkey = resolve_value(&defines.get("$modkey").cloned().unwrap_or_else(|| "Mod4".to_string()), &defines);
- let secondary_modkey = defines.get("$secondary_modkey").map(|v| resolve_value(v, &defines));
-
- lua_output.push_str(&format!("local terminal = {}\n", terminal));
- lua_output.push_str(&format!("local modkey = \"{}\"\n", modkey.trim_matches('"')));
- if let Some(sec_mod) = secondary_modkey {
- lua_output.push_str(&format!("local secondary_modkey = \"{}\"\n", sec_mod.trim_matches('"')));
- }
- lua_output.push_str("\n");
-
- lua_output.push_str("-- Color palette\n");
- lua_output.push_str("local colors = {\n");
- for (key, value) in &defines {
- if key.starts_with("$color_") {
- let color_name = &key[7..];
- let color_value = if value.starts_with("0x") {
- format!("\"#{}\"", &value[2..])
- } else {
- value.clone()
- };
- lua_output.push_str(&format!(" {} = {},\n", color_name, color_value));
- }
- }
- lua_output.push_str("}\n\n");
-
- lua_output.push_str("-- Main configuration table\n");
- lua_output.push_str("return {\n");
-
- if let Some(config_start) = ron_content.find('(') {
- let config_content = &ron_content[config_start + 1..];
-
- lua_output.push_str(" -- Appearance\n");
- if let Some(val) = extract_field(config_content, "border_width") {
- lua_output.push_str(&format!(" border_width = {},\n", val));
- }
- if let Some(val) = extract_field(config_content, "border_focused") {
- lua_output.push_str(&format!(" border_focused = {},\n", resolve_color_value(&val, &defines)));
- }
- if let Some(val) = extract_field(config_content, "border_unfocused") {
- lua_output.push_str(&format!(" border_unfocused = {},\n", resolve_color_value(&val, &defines)));
- }
- if let Some(val) = extract_field(config_content, "font") {
- lua_output.push_str(&format!(" font = {},\n", val));
- }
-
- lua_output.push_str("\n -- Window gaps\n");
- if let Some(val) = extract_field(config_content, "gaps_enabled") {
- lua_output.push_str(&format!(" gaps_enabled = {},\n", val));
- }
- if let Some(val) = extract_field(config_content, "gap_inner_horizontal") {
- lua_output.push_str(&format!(" gap_inner_horizontal = {},\n", val));
- }
- if let Some(val) = extract_field(config_content, "gap_inner_vertical") {
- lua_output.push_str(&format!(" gap_inner_vertical = {},\n", val));
- }
- if let Some(val) = extract_field(config_content, "gap_outer_horizontal") {
- lua_output.push_str(&format!(" gap_outer_horizontal = {},\n", val));
- }
- if let Some(val) = extract_field(config_content, "gap_outer_vertical") {
- lua_output.push_str(&format!(" gap_outer_vertical = {},\n", val));
- }
-
- lua_output.push_str("\n -- Basics\n");
- if let Some(val) = extract_field(config_content, "modkey") {
- let resolved = resolve_value(&val, &defines).trim_matches('"').to_string();
- if resolved == "modkey" {
- lua_output.push_str(" modkey = modkey,\n");
- } else {
- lua_output.push_str(&format!(" modkey = \"{}\",\n", resolved));
- }
- }
- if let Some(val) = extract_field(config_content, "terminal") {
- let resolved = resolve_value(&val, &defines);
- if resolved == "terminal" {
- lua_output.push_str(" terminal = terminal,\n");
- } else {
- lua_output.push_str(&format!(" terminal = {},\n", resolved));
- }
- }
-
- lua_output.push_str("\n -- Workspace tags\n");
- if let Some(val) = extract_field(config_content, "tags") {
- lua_output.push_str(&format!(" tags = {},\n", convert_array_to_lua(&val)));
- }
-
- lua_output.push_str("\n -- Layout symbol overrides\n");
- if let Some(val) = extract_field(config_content, "layout_symbols") {
- lua_output.push_str(" layout_symbols = ");
- lua_output.push_str(&convert_layout_symbols(&val));
- lua_output.push_str(",\n");
- }
-
- lua_output.push_str("\n -- Keybindings\n");
- if let Some(val) = extract_field(config_content, "keybindings") {
- lua_output.push_str(" keybindings = ");
- lua_output.push_str(&convert_keybindings(&val, &defines));
- lua_output.push_str(",\n");
- }
-
- lua_output.push_str("\n -- Status bar blocks\n");
- if let Some(val) = extract_field(config_content, "status_blocks") {
- lua_output.push_str(" status_blocks = ");
- lua_output.push_str(&convert_status_blocks(&val, &defines));
- lua_output.push_str(",\n");
- }
-
- lua_output.push_str("\n -- Color schemes for bar\n");
- if let Some(val) = extract_field(config_content, "scheme_normal") {
- lua_output.push_str(" scheme_normal = ");
- lua_output.push_str(&convert_color_scheme(&val, &defines));
- lua_output.push_str(",\n");
- }
- if let Some(val) = extract_field(config_content, "scheme_occupied") {
- lua_output.push_str(" scheme_occupied = ");
- lua_output.push_str(&convert_color_scheme(&val, &defines));
- lua_output.push_str(",\n");
- }
- if let Some(val) = extract_field(config_content, "scheme_selected") {
- lua_output.push_str(" scheme_selected = ");
- lua_output.push_str(&convert_color_scheme(&val, &defines));
- lua_output.push_str(",\n");
- }
-
- lua_output.push_str("\n -- Autostart commands\n");
- if let Some(val) = extract_field(config_content, "autostart") {
- let converted = convert_array_to_lua(&val);
- lua_output.push_str(" autostart = ");
- lua_output.push_str(&converted);
- lua_output.push_str(",\n");
- } else {
- lua_output.push_str(" autostart = {},\n");
- }
- }
-
- lua_output.push_str("}\n");
-
- Ok(lua_output)
-}
-
-fn extract_defines(content: &str) -> HashMap<String, String> {
- let mut defines = HashMap::new();
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("#DEFINE") {
- if let Some(rest) = trimmed.strip_prefix("#DEFINE") {
- if let Some(eq_pos) = rest.find('=') {
- let var_name = rest[..eq_pos].trim().to_string();
- let value = rest[eq_pos + 1..].trim().trim_end_matches(',').to_string();
- defines.insert(var_name, value);
- }
- }
- }
- }
- defines
-}
-
-fn resolve_value(value: &str, defines: &HashMap<String, String>) -> String {
- if let Some(resolved) = defines.get(value) {
- resolved.clone()
- } else {
- value.to_string()
- }
-}
-
-fn resolve_color_value(value: &str, defines: &HashMap<String, String>) -> String {
- let resolved = resolve_value(value, defines);
- if resolved.starts_with("$color_") {
- format!("colors.{}", &resolved[7..])
- } else if value.starts_with("$color_") {
- format!("colors.{}", &value[7..])
- } else if resolved.starts_with("0x") {
- format!("\"#{}\"", &resolved[2..])
- } else {
- resolved
- }
-}
-
-fn extract_field(content: &str, field_name: &str) -> Option<String> {
- let pattern = format!("{}:", field_name);
- let cleaned_content = remove_comments(content);
-
- if let Some(start) = cleaned_content.find(&pattern) {
- let after_colon = &cleaned_content[start + pattern.len()..];
- let value_start = after_colon.trim_start();
-
- if value_start.starts_with('[') {
- extract_bracketed(value_start, '[', ']')
- } else if value_start.starts_with('(') {
- extract_bracketed(value_start, '(', ')')
- } else if value_start.starts_with('"') {
- if let Some(end) = value_start[1..].find('"') {
- Some(value_start[..end + 2].to_string())
- } else {
- None
- }
- } else {
- let end = value_start
- .find(|c: char| c == ',' || c == '\n' || c == ')')
- .unwrap_or(value_start.len());
- Some(value_start[..end].trim().to_string())
- }
- } else {
- None
- }
-}
-
-fn extract_bracketed(s: &str, open: char, close: char) -> Option<String> {
- if !s.starts_with(open) {
- return None;
- }
- let mut depth = 0;
- let mut end = 0;
- for (i, c) in s.char_indices() {
- if c == open {
- depth += 1;
- } else if c == close {
- depth -= 1;
- if depth == 0 {
- end = i + 1;
- break;
- }
- }
- }
- if end > 0 {
- Some(s[..end].to_string())
- } else {
- None
- }
-}
-
-fn convert_array_to_lua(ron_array: &str) -> String {
- let inner = ron_array.trim_start_matches('[').trim_end_matches(']');
- let items: Vec<&str> = inner.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
- format!("{{ {} }}", items.join(", "))
-}
-
-fn convert_layout_symbols(ron_array: &str) -> String {
- let mut result = String::from("{\n");
- let inner = ron_array.trim_start_matches('[').trim_end_matches(']');
-
- let items = extract_all_bracketed(inner, '(', ')');
- for item in items {
- let item_inner = item.trim_start_matches('(').trim_end_matches(')');
- if let (Some(name), Some(symbol)) = (extract_quoted_value(item_inner, "name"), extract_quoted_value(item_inner, "symbol")) {
- result.push_str(&format!(" {{ name = \"{}\", symbol = \"{}\" }},\n", name, symbol));
- }
- }
-
- result.push_str(" }");
- result
-}
-
-fn convert_keybindings(ron_array: &str, defines: &HashMap<String, String>) -> String {
- let mut result = String::from("{\n");
- let inner = ron_array.trim_start_matches('[').trim_end_matches(']');
-
- let items = extract_all_bracketed(inner, '(', ')');
- for item in items {
- let binding = convert_keybinding(&item, defines);
- result.push_str(&binding);
- result.push_str(",\n");
- }
-
- result.push_str(" }");
- result
-}
-
-fn convert_keybinding(ron_binding: &str, defines: &HashMap<String, String>) -> String {
- let inner = ron_binding.trim_start_matches('(').trim_end_matches(')');
-
- if inner.contains("keys:") {
- convert_keychord(inner, defines)
- } else {
- convert_single_key(inner, defines)
- }
-}
-
-fn convert_keychord(inner: &str, defines: &HashMap<String, String>) -> String {
- let mut result = String::from(" {\n keys = {\n");
-
- if let Some(keys_str) = extract_field(inner, "keys") {
- let keys = extract_all_bracketed(&keys_str, '(', ')');
- for key in keys {
- let key_inner = key.trim_start_matches('(').trim_end_matches(')');
- let mods = extract_modifiers(key_inner, defines);
- let key_name = extract_key(key_inner);
- result.push_str(&format!(" {{ modifiers = {{ {} }}, key = \"{}\" }},\n", mods, key_name));
- }
- }
-
- result.push_str(" },\n");
-
- if let Some(action) = extract_identifier(inner, "action") {
- result.push_str(&format!(" action = \"{}\",\n", action));
- }
-
- if let Some(arg) = extract_arg(inner, defines) {
- result.push_str(&format!(" arg = {}\n", arg));
- }
-
- result.push_str(" }");
- result
-}
-
-fn convert_single_key(inner: &str, defines: &HashMap<String, String>) -> String {
- let mods = extract_modifiers(inner, defines);
- let key = extract_key(inner);
- let action = extract_identifier(inner, "action").unwrap_or_default();
-
- let mut result = format!(" {{ modifiers = {{ {} }}, key = \"{}\", action = \"{}\"", mods, key, action);
-
- if let Some(arg) = extract_arg(inner, defines) {
- result.push_str(&format!(", arg = {}", arg));
- }
-
- result.push_str(" }");
- result
-}
-
-fn extract_modifiers(content: &str, defines: &HashMap<String, String>) -> String {
- if let Some(mods_str) = extract_field(content, "modifiers") {
- let inner = mods_str.trim_start_matches('[').trim_end_matches(']').trim();
- if inner.is_empty() {
- return String::new();
- }
- let mods: Vec<String> = inner
- .split(',')
- .map(|s| {
- let trimmed = s.trim();
- if !trimmed.is_empty() {
- let resolved = resolve_value(trimmed, defines);
- format!("\"{}\"", resolved)
- } else {
- String::new()
- }
- })
- .filter(|s| !s.is_empty())
- .collect();
- mods.join(", ")
- } else {
- String::new()
- }
-}
-
-fn extract_key(content: &str) -> String {
- if let Some(key_str) = extract_identifier(content, "key") {
- if key_str.starts_with("Key") && key_str.len() == 4 {
- if let Some(digit) = key_str.chars().nth(3) {
- if digit.is_ascii_digit() {
- return digit.to_string();
- }
- }
- }
- key_str
- } else {
- String::from("Return")
- }
-}
-
-fn extract_identifier(content: &str, field_name: &str) -> Option<String> {
- let pattern = format!("{}:", field_name);
- if let Some(start) = content.find(&pattern) {
- let after_colon = &content[start + pattern.len()..];
- let value_start = after_colon.trim_start();
- let end = value_start
- .find(|c: char| c == ',' || c == ')' || c == '\n')
- .unwrap_or(value_start.len());
- Some(value_start[..end].trim().to_string())
- } else {
- None
- }
-}
-
-fn extract_arg(content: &str, defines: &HashMap<String, String>) -> Option<String> {
- if let Some(arg_str) = extract_field(content, "arg") {
- let resolved = resolve_value(&arg_str, defines);
- if resolved.starts_with('[') {
- Some(convert_array_to_lua(&resolved))
- } else if resolved.starts_with('"') || resolved.parse::<i32>().is_ok() || resolved.starts_with("0x") {
- Some(resolved)
- } else {
- Some(format!("\"{}\"", resolved))
- }
- } else {
- None
- }
-}
-
-fn convert_status_blocks(ron_array: &str, defines: &HashMap<String, String>) -> String {
- let mut result = String::from("{\n");
- let inner = ron_array.trim_start_matches('[').trim_end_matches(']');
-
- let items = extract_all_bracketed(inner, '(', ')');
- for item in items {
- let block = convert_status_block(&item, defines);
- if !block.trim().ends_with("{\n }") {
- result.push_str(&block);
- result.push_str(",\n");
- }
- }
-
- result.push_str(" }");
- result
-}
-
-fn convert_status_block(ron_block: &str, defines: &HashMap<String, String>) -> String {
- let mut result = String::from(" {\n");
- let inner = ron_block.trim_start_matches('(').trim_end_matches(')');
-
- if let Some(format) = extract_field(inner, "format") {
- result.push_str(&format!(" format = {},\n", format));
- }
- if let Some(command) = extract_field(inner, "command") {
- result.push_str(&format!(" command = {},\n", command));
- }
- if let Some(command_arg) = extract_field(inner, "command_arg") {
- result.push_str(&format!(" command_arg = {},\n", command_arg));
- }
- if inner.contains("battery_formats:") {
- if let Some(battery_str) = extract_field(inner, "battery_formats") {
- result.push_str(" battery_formats = {\n");
- let battery_inner = battery_str.trim_start_matches('(').trim_end_matches(')');
- if let Some(charging) = extract_quoted_value(battery_inner, "charging") {
- result.push_str(&format!(" charging = \"{}\",\n", charging));
- }
- if let Some(discharging) = extract_quoted_value(battery_inner, "discharging") {
- result.push_str(&format!(" discharging = \"{}\",\n", discharging));
- }
- if let Some(full) = extract_quoted_value(battery_inner, "full") {
- result.push_str(&format!(" full = \"{}\"\n", full));
- }
- result.push_str(" },\n");
- }
- }
- if let Some(interval) = extract_field(inner, "interval_secs") {
- let interval_val = if interval.len() > 10 {
- "999999999".to_string()
- } else {
- interval
- };
- result.push_str(&format!(" interval_secs = {},\n", interval_val));
- }
- if let Some(color) = extract_field(inner, "color") {
- let resolved = resolve_color_value(&color, defines);
- result.push_str(&format!(" color = {},\n", resolved));
- }
- if let Some(underline) = extract_field(inner, "underline") {
- result.push_str(&format!(" underline = {}\n", underline));
- }
-
- result.push_str(" }");
- result
-}
-
-fn convert_color_scheme(ron_scheme: &str, defines: &HashMap<String, String>) -> String {
- let mut result = String::from("{\n");
- let inner = ron_scheme.trim_start_matches('(').trim_end_matches(')');
-
- if let Some(fg) = extract_field(inner, "foreground") {
- let resolved = resolve_color_value(&fg, defines);
- result.push_str(&format!(" foreground = {},\n", resolved));
- }
- if let Some(bg) = extract_field(inner, "background") {
- let resolved = resolve_color_value(&bg, defines);
- result.push_str(&format!(" background = {},\n", resolved));
- }
- if let Some(ul) = extract_field(inner, "underline") {
- let resolved = resolve_color_value(&ul, defines);
- result.push_str(&format!(" underline = {}\n", resolved));
- }
-
- result.push_str(" }");
- result
-}
-
-fn extract_all_bracketed(s: &str, open: char, close: char) -> Vec<String> {
- let mut results = Vec::new();
- let mut depth = 0;
- let mut start = None;
-
- let cleaned = remove_comments(s);
-
- for (i, c) in cleaned.char_indices() {
- if c == open {
- if depth == 0 {
- start = Some(i);
- }
- depth += 1;
- } else if c == close {
- depth -= 1;
- if depth == 0 {
- if let Some(start_idx) = start {
- results.push(cleaned[start_idx..=i].to_string());
- start = None;
- }
- }
- }
- }
-
- results
-}
-
-fn remove_comments(s: &str) -> String {
- let mut result = String::new();
- for line in s.lines() {
- let mut in_string = false;
- let mut comment_start = None;
-
- for (i, c) in line.char_indices() {
- if c == '"' && (i == 0 || line.chars().nth(i - 1) != Some('\\')) {
- in_string = !in_string;
- }
- if !in_string && i + 1 < line.len() && &line[i..i + 2] == "//" {
- comment_start = Some(i);
- break;
- }
- }
-
- if let Some(pos) = comment_start {
- result.push_str(&line[..pos]);
- } else {
- result.push_str(line);
- }
- result.push('\n');
- }
- result
-}
-
-fn extract_quoted_value(content: &str, field_name: &str) -> Option<String> {
- let pattern = format!("{}:", field_name);
- if let Some(start) = content.find(&pattern) {
- let after_colon = &content[start + pattern.len()..];
- let trimmed = after_colon.trim_start();
- if trimmed.starts_with('"') {
- if let Some(end) = trimmed[1..].find('"') {
- return Some(trimmed[1..end + 1].to_string());
- }
- }
- }
- None
-}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index abdc0bf..ec402b7 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,480 +1,5 @@
mod lua;
-pub mod migrate;
-
-use crate::bar::{BlockCommand, BlockConfig};
-use crate::errors::ConfigError;
-use crate::keyboard::handlers::{KeyBinding, KeyPress};
-use crate::keyboard::keysyms;
-use crate::keyboard::{Arg, KeyAction};
-use crate::keyboard::keysyms::Keysym;
-use serde::Deserialize;
-use std::collections::HashMap;
-use x11rb::protocol::xproto::KeyButMask;
+mod lua_api;
pub use lua::parse_lua_config;
-#[derive(Debug, Deserialize)]
-pub enum ModKey {
- Mod,
- Mod1,
- Mod2,
- Mod3,
- Mod4,
- Mod5,
- Shift,
- Control,
-}
-
-impl ModKey {
- fn to_keybut_mask(&self) -> KeyButMask {
- match self {
- ModKey::Mod => panic!("ModKey::Mod should be replaced during config parsing"),
- ModKey::Mod1 => KeyButMask::MOD1,
- ModKey::Mod2 => KeyButMask::MOD2,
- ModKey::Mod3 => KeyButMask::MOD3,
- ModKey::Mod4 => KeyButMask::MOD4,
- ModKey::Mod5 => KeyButMask::MOD5,
- ModKey::Shift => KeyButMask::SHIFT,
- ModKey::Control => KeyButMask::CONTROL,
- }
- }
-}
-
-#[rustfmt::skip]
-#[derive(Debug, Deserialize)]
-pub enum KeyData {
- Return,
- Q,
- Escape,
- Space,
- Tab,
- Backspace,
- Delete,
- F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
- A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, R, S, T, U, V, W, X, Y, Z,
- Key0,
- Key1,
- Key2,
- Key3,
- Key4,
- Key5,
- Key6,
- Key7,
- Key8,
- Key9,
- Left,
- Right,
- Up,
- Down,
- Home,
- End,
- PageUp,
- PageDown,
- Insert,
- Minus,
- Equal,
- BracketLeft,
- BracketRight,
- Semicolon,
- Apostrophe,
- Grave,
- Backslash,
- Comma,
- Period,
- Slash,
- AudioRaiseVolume,
- AudioLowerVolume,
- AudioMute,
- MonBrightnessUp,
- MonBrightnessDown,
-}
-
-impl KeyData {
- fn to_keysym(&self) -> Keysym {
- match self {
- KeyData::Return => keysyms::XK_RETURN,
- KeyData::Q => keysyms::XK_Q,
- KeyData::Escape => keysyms::XK_ESCAPE,
- KeyData::Space => keysyms::XK_SPACE,
- KeyData::Tab => keysyms::XK_TAB,
- KeyData::Backspace => keysyms::XK_BACKSPACE,
- KeyData::Delete => keysyms::XK_DELETE,
- KeyData::F1 => keysyms::XK_F1,
- KeyData::F2 => keysyms::XK_F2,
- KeyData::F3 => keysyms::XK_F3,
- KeyData::F4 => keysyms::XK_F4,
- KeyData::F5 => keysyms::XK_F5,
- KeyData::F6 => keysyms::XK_F6,
- KeyData::F7 => keysyms::XK_F7,
- KeyData::F8 => keysyms::XK_F8,
- KeyData::F9 => keysyms::XK_F9,
- KeyData::F10 => keysyms::XK_F10,
- KeyData::F11 => keysyms::XK_F11,
- KeyData::F12 => keysyms::XK_F12,
- KeyData::A => keysyms::XK_A,
- KeyData::B => keysyms::XK_B,
- KeyData::C => keysyms::XK_C,
- KeyData::D => keysyms::XK_D,
- KeyData::E => keysyms::XK_E,
- KeyData::F => keysyms::XK_F,
- KeyData::G => keysyms::XK_G,
- KeyData::H => keysyms::XK_H,
- KeyData::I => keysyms::XK_I,
- KeyData::J => keysyms::XK_J,
- KeyData::K => keysyms::XK_K,
- KeyData::L => keysyms::XK_L,
- KeyData::M => keysyms::XK_M,
- KeyData::N => keysyms::XK_N,
- KeyData::O => keysyms::XK_O,
- KeyData::P => keysyms::XK_P,
- KeyData::R => keysyms::XK_R,
- KeyData::S => keysyms::XK_S,
- KeyData::T => keysyms::XK_T,
- KeyData::U => keysyms::XK_U,
- KeyData::V => keysyms::XK_V,
- KeyData::W => keysyms::XK_W,
- KeyData::X => keysyms::XK_X,
- KeyData::Y => keysyms::XK_Y,
- KeyData::Z => keysyms::XK_Z,
- KeyData::Key0 => keysyms::XK_0,
- KeyData::Key1 => keysyms::XK_1,
- KeyData::Key2 => keysyms::XK_2,
- KeyData::Key3 => keysyms::XK_3,
- KeyData::Key4 => keysyms::XK_4,
- KeyData::Key5 => keysyms::XK_5,
- KeyData::Key6 => keysyms::XK_6,
- KeyData::Key7 => keysyms::XK_7,
- KeyData::Key8 => keysyms::XK_8,
- KeyData::Key9 => keysyms::XK_9,
- KeyData::Left => keysyms::XK_LEFT,
- KeyData::Right => keysyms::XK_RIGHT,
- KeyData::Up => keysyms::XK_UP,
- KeyData::Down => keysyms::XK_DOWN,
- KeyData::Home => keysyms::XK_HOME,
- KeyData::End => keysyms::XK_END,
- KeyData::PageUp => keysyms::XK_PAGE_UP,
- KeyData::PageDown => keysyms::XK_PAGE_DOWN,
- KeyData::Insert => keysyms::XK_INSERT,
- KeyData::Minus => keysyms::XK_MINUS,
- KeyData::Equal => keysyms::XK_EQUAL,
- KeyData::BracketLeft => keysyms::XK_LEFT_BRACKET,
- KeyData::BracketRight => keysyms::XK_RIGHT_BRACKET,
- KeyData::Semicolon => keysyms::XK_SEMICOLON,
- KeyData::Apostrophe => keysyms::XK_APOSTROPHE,
- KeyData::Grave => keysyms::XK_GRAVE,
- KeyData::Backslash => keysyms::XK_BACKSLASH,
- KeyData::Comma => keysyms::XK_COMMA,
- KeyData::Period => keysyms::XK_PERIOD,
- KeyData::Slash => keysyms::XK_SLASH,
- KeyData::AudioRaiseVolume => keysyms::XF86_AUDIO_RAISE_VOLUME,
- KeyData::AudioLowerVolume => keysyms::XF86_AUDIO_LOWER_VOLUME,
- KeyData::AudioMute => keysyms::XF86_AUDIO_MUTE,
- KeyData::MonBrightnessUp => keysyms::XF86_MON_BRIGHTNESS_UP,
- KeyData::MonBrightnessDown => keysyms::XF86_MON_BRIGHTNESS_DOWN,
- }
- }
-
-}
-
-fn preprocess_variables(input: &str) -> Result<String, ConfigError> {
- let mut variables: HashMap<String, String> = HashMap::new();
- let mut result = String::new();
-
- for line in input.lines() {
- let trimmed = line.trim();
-
- if trimmed.starts_with("#DEFINE") {
- let rest = trimmed.strip_prefix("#DEFINE").unwrap().trim();
-
- if let Some(eq_pos) = rest.find('=') {
- let var_name = rest[..eq_pos].trim();
- let value = rest[eq_pos + 1..].trim().trim_end_matches(',');
-
- if !var_name.starts_with('$') {
- return Err(ConfigError::InvalidVariableName(var_name.to_string()));
- }
-
- variables.insert(var_name.to_string(), value.to_string());
- } else {
- return Err(ConfigError::InvalidDefine(trimmed.to_string()));
- }
-
- result.push('\n');
- } else {
- let mut processed_line = line.to_string();
- for (var_name, value) in &variables {
- processed_line = processed_line.replace(var_name, value);
- }
- result.push_str(&processed_line);
- result.push('\n');
- }
- }
-
- for line in result.lines() {
- if let Some(var_start) = line.find('$') {
- let rest = &line[var_start..];
- let var_end = rest[1..]
- .find(|c: char| !c.is_alphanumeric() && c != '_')
- .unwrap_or(rest.len() - 1)
- + 1;
- let undefined_var = &rest[..var_end];
- return Err(ConfigError::UndefinedVariable(undefined_var.to_string()));
- }
- }
- Ok(result)
-}
-
-pub fn parse_config(input: &str) -> Result<crate::Config, ConfigError> {
- let preprocessed = preprocess_variables(input)?;
- let config_data: ConfigData = ron::from_str(&preprocessed)?;
- config_data_to_config(config_data)
-}
-
-#[derive(Debug, Deserialize)]
-struct LayoutSymbolOverrideData {
- name: String,
- symbol: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct ConfigData {
- border_width: u32,
- border_focused: u32,
- border_unfocused: u32,
- font: String,
-
- gaps_enabled: bool,
- gap_inner_horizontal: u32,
- gap_inner_vertical: u32,
- gap_outer_horizontal: u32,
- gap_outer_vertical: u32,
-
- terminal: String,
- modkey: ModKey,
-
- tags: Vec<String>,
- #[serde(default)]
- layout_symbols: Vec<LayoutSymbolOverrideData>,
- keybindings: Vec<KeybindingData>,
- status_blocks: Vec<StatusBlockData>,
-
- scheme_normal: ColorSchemeData,
- scheme_occupied: ColorSchemeData,
- scheme_selected: ColorSchemeData,
-
- #[serde(default)]
- autostart: Vec<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct KeybindingData {
- #[serde(default)]
- keys: Option<Vec<KeyPressData>>,
- #[serde(default)]
- modifiers: Option<Vec<ModKey>>,
- #[serde(default)]
- key: Option<KeyData>,
- action: KeyAction,
- #[serde(default)]
- arg: ArgData,
-}
-
-#[derive(Debug, Deserialize)]
-struct KeyPressData {
- modifiers: Vec<ModKey>,
- key: KeyData,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(untagged)]
-enum ArgData {
- None,
- String(String),
- Int(i32),
- Array(Vec<String>),
-}
-
-impl Default for ArgData {
- fn default() -> Self {
- ArgData::None
- }
-}
-
-#[derive(Debug, Deserialize)]
-struct StatusBlockData {
- format: String,
- command: String,
- #[serde(default)]
- command_arg: Option<String>,
- #[serde(default)]
- battery_formats: Option<BatteryFormats>,
- interval_secs: u64,
- color: u32,
- underline: bool,
-}
-
-#[derive(Debug, Deserialize)]
-struct BatteryFormats {
- charging: String,
- discharging: String,
- full: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct ColorSchemeData {
- foreground: u32,
- background: u32,
- underline: u32,
-}
-
-fn config_data_to_config(data: ConfigData) -> Result<crate::Config, ConfigError> {
- let modkey = data.modkey.to_keybut_mask();
-
- let mut keybindings = Vec::new();
- for kb_data in data.keybindings {
- let keys = if let Some(keys_data) = kb_data.keys {
- keys_data
- .into_iter()
- .map(|kp| {
- let modifiers = kp
- .modifiers
- .iter()
- .map(|m| match m {
- ModKey::Mod => modkey,
- _ => m.to_keybut_mask(),
- })
- .collect();
-
- KeyPress {
- modifiers,
- keysym: kp.key.to_keysym(),
- }
- })
- .collect()
- } else if let (Some(modifiers), Some(key)) = (kb_data.modifiers, kb_data.key) {
- vec![KeyPress {
- modifiers: modifiers
- .iter()
- .map(|m| match m {
- ModKey::Mod => modkey,
- _ => m.to_keybut_mask(),
- })
- .collect(),
- keysym: key.to_keysym(),
- }]
- } else {
- return Err(ConfigError::ValidationError(
- "Keybinding must have either 'keys' or 'modifiers'+'key'".to_string(),
- ));
- };
-
- let action = kb_data.action;
- let arg = arg_data_to_arg(kb_data.arg)?;
-
- keybindings.push(KeyBinding::new(keys, action, arg));
- }
-
- let mut status_blocks = Vec::new();
- for block_data in data.status_blocks {
- let command = match block_data.command.as_str() {
- "DateTime" => {
- let fmt = block_data
- .command_arg
- .ok_or_else(|| ConfigError::MissingCommandArg {
- command: "DateTime".to_string(),
- field: "command_arg".to_string(),
- })?;
- BlockCommand::DateTime(fmt)
- }
- "Shell" => {
- let cmd = block_data
- .command_arg
- .ok_or_else(|| ConfigError::MissingCommandArg {
- command: "Shell".to_string(),
- field: "command_arg".to_string(),
- })?;
- BlockCommand::Shell(cmd)
- }
- "Ram" => BlockCommand::Ram,
- "Static" => {
- let text = block_data.command_arg.unwrap_or_default();
- BlockCommand::Static(text)
- }
- "Battery" => {
- let formats =
- block_data
- .battery_formats
- .ok_or_else(|| ConfigError::MissingCommandArg {
- command: "Battery".to_string(),
- field: "battery_formats".to_string(),
- })?;
- BlockCommand::Battery {
- format_charging: formats.charging,
- format_discharging: formats.discharging,
- format_full: formats.full,
- }
- }
- _ => return Err(ConfigError::UnknownBlockCommand(block_data.command)),
- };
-
- status_blocks.push(BlockConfig {
- format: block_data.format,
- command,
- interval_secs: block_data.interval_secs,
- color: block_data.color,
- underline: block_data.underline,
- });
- }
-
- let layout_symbols = data
- .layout_symbols
- .into_iter()
- .map(|l| crate::LayoutSymbolOverride {
- name: l.name,
- symbol: l.symbol,
- })
- .collect();
-
- Ok(crate::Config {
- border_width: data.border_width,
- border_focused: data.border_focused,
- border_unfocused: data.border_unfocused,
- font: data.font,
- gaps_enabled: data.gaps_enabled,
- gap_inner_horizontal: data.gap_inner_horizontal,
- gap_inner_vertical: data.gap_inner_vertical,
- gap_outer_horizontal: data.gap_outer_horizontal,
- gap_outer_vertical: data.gap_outer_vertical,
- terminal: data.terminal,
- modkey,
- tags: data.tags,
- layout_symbols,
- keybindings,
- status_blocks,
- scheme_normal: crate::ColorScheme {
- foreground: data.scheme_normal.foreground,
- background: data.scheme_normal.background,
- underline: data.scheme_normal.underline,
- },
- scheme_occupied: crate::ColorScheme {
- foreground: data.scheme_occupied.foreground,
- background: data.scheme_occupied.background,
- underline: data.scheme_occupied.underline,
- },
- scheme_selected: crate::ColorScheme {
- foreground: data.scheme_selected.foreground,
- background: data.scheme_selected.background,
- underline: data.scheme_selected.underline,
- },
- autostart: data.autostart,
- })
-}
-
-fn arg_data_to_arg(data: ArgData) -> Result<Arg, ConfigError> {
- match data {
- ArgData::None => Ok(Arg::None),
- ArgData::String(s) => Ok(Arg::Str(s)),
- ArgData::Int(n) => Ok(Arg::Int(n)),
- ArgData::Array(arr) => Ok(Arg::Array(arr)),
- }
-}
diff --git a/src/errors.rs b/src/errors.rs
index ae9a47a..256ae38 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -22,7 +22,6 @@ pub enum X11Error {
#[derive(Debug)]
pub enum ConfigError {
- ParseError(ron::error::SpannedError),
LuaError(String),
InvalidModkey(String),
UnknownKey(String),
@@ -30,10 +29,6 @@ pub enum ConfigError {
UnknownBlockCommand(String),
MissingCommandArg { command: String, field: String },
ValidationError(String),
- InvalidVariableName(String),
- InvalidDefine(String),
- UndefinedVariable(String),
- MigrationError(String),
}
#[derive(Debug)]
@@ -78,7 +73,6 @@ impl std::error::Error for X11Error {}
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),
@@ -88,24 +82,6 @@ impl std::fmt::Display for ConfigError {
write!(f, "{} command requires {}", command, field)
}
Self::ValidationError(msg) => write!(f, "Config validation error: {}", msg),
- Self::InvalidVariableName(name) => {
- write!(f, "Invalid variable name '{}': must start with $", name)
- }
- Self::InvalidDefine(line) => {
- write!(
- f,
- "Invalid #DEFINE syntax: '{}'. Expected: #DEFINE $var_name = value",
- line
- )
- }
- Self::UndefinedVariable(var) => {
- write!(
- f,
- "Undefined variable '{}': define it with #DEFINE before use",
- var
- )
- }
- Self::MigrationError(msg) => write!(f, "Migration error: {}", msg),
}
}
}
@@ -162,12 +138,6 @@ impl From<std::num::ParseIntError> for BlockError {
}
}
-impl From<ron::error::SpannedError> for ConfigError {
- fn from(value: ron::error::SpannedError) -> Self {
- ConfigError::ParseError(value)
- }
-}
-
impl From<x11rb::errors::ConnectError> for X11Error {
fn from(value: x11rb::errors::ConnectError) -> Self {
X11Error::ConnectError(value)
diff --git a/src/keyboard/keysyms.rs b/src/keyboard/keysyms.rs
index a3a1bf6..9a524a5 100644
--- a/src/keyboard/keysyms.rs
+++ b/src/keyboard/keysyms.rs
@@ -83,6 +83,91 @@ pub const XF86_AUDIO_MUTE: Keysym = 0x1008ff12;
pub const XF86_MON_BRIGHTNESS_UP: Keysym = 0x1008ff02;
pub const XF86_MON_BRIGHTNESS_DOWN: Keysym = 0x1008ff03;
+pub fn keysym_from_str(s: &str) -> Option<Keysym> {
+ match s {
+ "Return" => Some(XK_RETURN),
+ "Escape" => Some(XK_ESCAPE),
+ "Space" => Some(XK_SPACE),
+ "Tab" => Some(XK_TAB),
+ "Backspace" => Some(XK_BACKSPACE),
+ "Delete" => Some(XK_DELETE),
+ "F1" => Some(XK_F1),
+ "F2" => Some(XK_F2),
+ "F3" => Some(XK_F3),
+ "F4" => Some(XK_F4),
+ "F5" => Some(XK_F5),
+ "F6" => Some(XK_F6),
+ "F7" => Some(XK_F7),
+ "F8" => Some(XK_F8),
+ "F9" => Some(XK_F9),
+ "F10" => Some(XK_F10),
+ "F11" => Some(XK_F11),
+ "F12" => Some(XK_F12),
+ "A" => Some(XK_A),
+ "B" => Some(XK_B),
+ "C" => Some(XK_C),
+ "D" => Some(XK_D),
+ "E" => Some(XK_E),
+ "F" => Some(XK_F),
+ "G" => Some(XK_G),
+ "H" => Some(XK_H),
+ "I" => Some(XK_I),
+ "J" => Some(XK_J),
+ "K" => Some(XK_K),
+ "L" => Some(XK_L),
+ "M" => Some(XK_M),
+ "N" => Some(XK_N),
+ "O" => Some(XK_O),
+ "P" => Some(XK_P),
+ "Q" => Some(XK_Q),
+ "R" => Some(XK_R),
+ "S" => Some(XK_S),
+ "T" => Some(XK_T),
+ "U" => Some(XK_U),
+ "V" => Some(XK_V),
+ "W" => Some(XK_W),
+ "X" => Some(XK_X),
+ "Y" => Some(XK_Y),
+ "Z" => Some(XK_Z),
+ "0" => Some(XK_0),
+ "1" => Some(XK_1),
+ "2" => Some(XK_2),
+ "3" => Some(XK_3),
+ "4" => Some(XK_4),
+ "5" => Some(XK_5),
+ "6" => Some(XK_6),
+ "7" => Some(XK_7),
+ "8" => Some(XK_8),
+ "9" => Some(XK_9),
+ "Left" => Some(XK_LEFT),
+ "Right" => Some(XK_RIGHT),
+ "Up" => Some(XK_UP),
+ "Down" => Some(XK_DOWN),
+ "Home" => Some(XK_HOME),
+ "End" => Some(XK_END),
+ "PageUp" => Some(XK_PAGE_UP),
+ "PageDown" => Some(XK_PAGE_DOWN),
+ "Insert" => Some(XK_INSERT),
+ "Minus" => Some(XK_MINUS),
+ "Equal" => Some(XK_EQUAL),
+ "BracketLeft" => Some(XK_LEFT_BRACKET),
+ "BracketRight" => Some(XK_RIGHT_BRACKET),
+ "Semicolon" => Some(XK_SEMICOLON),
+ "Apostrophe" => Some(XK_APOSTROPHE),
+ "Grave" => Some(XK_GRAVE),
+ "Backslash" => Some(XK_BACKSLASH),
+ "Comma" => Some(XK_COMMA),
+ "Period" => Some(XK_PERIOD),
+ "Slash" => Some(XK_SLASH),
+ "AudioRaiseVolume" => Some(XF86_AUDIO_RAISE_VOLUME),
+ "AudioLowerVolume" => Some(XF86_AUDIO_LOWER_VOLUME),
+ "AudioMute" => Some(XF86_AUDIO_MUTE),
+ "MonBrightnessUp" => Some(XF86_MON_BRIGHTNESS_UP),
+ "MonBrightnessDown" => Some(XF86_MON_BRIGHTNESS_DOWN),
+ _ => None,
+ }
+}
+
pub fn format_keysym(keysym: Keysym) -> String {
match keysym {
XK_RETURN => "Return".to_string(),
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 5fbfb25..8e68e30 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -254,32 +254,16 @@ impl WindowManager {
};
let lua_path = config_dir.join("config.lua");
- let ron_path = config_dir.join("config.ron");
- let config_path = if lua_path.exists() {
- lua_path
- } else if ron_path.exists() {
- ron_path
- } else {
- return Err("No config file found".to_string());
- };
+ if !lua_path.exists() {
+ return Err("No config.lua file found".to_string());
+ }
- let config_str = std::fs::read_to_string(&config_path)
+ let config_str = std::fs::read_to_string(&lua_path)
.map_err(|e| format!("Failed to read config: {}", e))?;
- let is_lua = config_path
- .extension()
- .and_then(|s| s.to_str())
- .map(|s| s == "lua")
- .unwrap_or(false);
-
- let new_config = if is_lua {
- let config_dir = config_path.parent();
- crate::config::parse_lua_config(&config_str, config_dir)
- .map_err(|e| format!("Config error: {}", e))?
- } else {
- crate::config::parse_config(&config_str).map_err(|e| format!("Config error: {}", e))?
- };
+ let new_config = crate::config::parse_lua_config(&config_str, Some(&config_dir))
+ .map_err(|e| format!("Config error: {}", e))?;
self.config = new_config;
self.error_message = None;
diff --git a/templates/config.lua b/templates/config.lua
index a6dc367..08e1523 100644
--- a/templates/config.lua
+++ b/templates/config.lua
@@ -1,10 +1,10 @@
--- OXWM Configuration File (Lua)
--- Migrated from config.ron
--- Edit this file and reload with Mod+Shift+R (no compilation needed!)
+---@meta
+---OXWM Configuration File (Lua)
+---Using the new functional API
+---Edit this file and reload with Mod+Shift+R (no compilation needed!)
-local terminal = "st"
-local modkey = "Mod4"
-local secondary_modkey = "Mod1"
+---Load type definitions for LSP
+---@module 'oxwm'
-- Color palette
local colors = {
@@ -20,145 +20,111 @@ local colors = {
purple = "#ad8ee6",
}
--- Main configuration table
-return {
- -- 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 = "Mod4",
- terminal = "st",
-
- -- 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
- keybindings = {
- { modifiers = { "Mod4" }, key = "Return", action = "Spawn", arg = "st" },
- { modifiers = { "Mod4" }, key = "D", action = "Spawn", arg = { "sh", "-c", "dmenu_run -l 10" } },
- { modifiers = { "Mod4" }, key = "S", action = "Spawn", arg = { "sh", "-c", "maim -s | xclip -selection clipboard -t image/png" } },
- { modifiers = { "Mod4" }, key = "Q", action = "KillClient" },
- { modifiers = { "Mod4", "Shift" }, key = "Slash", action = "ShowKeybindOverlay" },
- { modifiers = { "Mod4", "Shift" }, key = "F", action = "ToggleFullScreen" },
- { modifiers = { "Mod4", "Shift" }, key = "Space", action = "ToggleFloating" },
- { modifiers = { "Mod4" }, key = "F", action = "ChangeLayout", arg = "normie" },
- { modifiers = { "Mod4" }, key = "C", action = "ChangeLayout", arg = "tiling" },
- { modifiers = { "Mod1" }, key = "N", action = "CycleLayout" },
- { modifiers = { "Mod4" }, key = "A", action = "ToggleGaps" },
- { modifiers = { "Mod4", "Shift" }, key = "Q", action = "Quit" },
- { modifiers = { "Mod4", "Shift" }, key = "R", action = "Restart" },
- { modifiers = { "Mod4" }, key = "H", action = "FocusDirection", arg = 2 },
- { modifiers = { "Mod4" }, key = "J", action = "FocusDirection", arg = 1 },
- { modifiers = { "Mod4" }, key = "K", action = "FocusDirection", arg = 0 },
- { modifiers = { "Mod4" }, key = "L", action = "FocusDirection", arg = 3 },
- { modifiers = { "Mod4" }, key = "Comma", action = "FocusMonitor", arg = -1 },
- { modifiers = { "Mod4" }, key = "Period", action = "FocusMonitor", arg = 1 },
- { modifiers = { "Mod4" }, key = "1", action = "ViewTag", arg = 0 },
- { modifiers = { "Mod4" }, key = "2", action = "ViewTag", arg = 1 },
- { modifiers = { "Mod4" }, key = "3", action = "ViewTag", arg = 2 },
- { modifiers = { "Mod4" }, key = "4", action = "ViewTag", arg = 3 },
- { modifiers = { "Mod4" }, key = "5", action = "ViewTag", arg = 4 },
- { modifiers = { "Mod4" }, key = "6", action = "ViewTag", arg = 5 },
- { modifiers = { "Mod4" }, key = "7", action = "ViewTag", arg = 6 },
- { modifiers = { "Mod4" }, key = "8", action = "ViewTag", arg = 7 },
- { modifiers = { "Mod4" }, key = "9", action = "ViewTag", arg = 8 },
- { modifiers = { "Mod4", "Shift" }, key = "1", action = "MoveToTag", arg = 0 },
- { modifiers = { "Mod4", "Shift" }, key = "2", action = "MoveToTag", arg = 1 },
- { modifiers = { "Mod4", "Shift" }, key = "3", action = "MoveToTag", arg = 2 },
- { modifiers = { "Mod4", "Shift" }, key = "4", action = "MoveToTag", arg = 3 },
- { modifiers = { "Mod4", "Shift" }, key = "5", action = "MoveToTag", arg = 4 },
- { modifiers = { "Mod4", "Shift" }, key = "6", action = "MoveToTag", arg = 5 },
- { modifiers = { "Mod4", "Shift" }, key = "7", action = "MoveToTag", arg = 6 },
- { modifiers = { "Mod4", "Shift" }, key = "8", action = "MoveToTag", arg = 7 },
- { modifiers = { "Mod4", "Shift" }, key = "9", action = "MoveToTag", arg = 8 },
- { modifiers = { "Mod4", "Shift" }, key = "H", action = "SwapDirection", arg = 2 },
- { modifiers = { "Mod4", "Shift" }, key = "J", action = "SwapDirection", arg = 1 },
- { modifiers = { "Mod4", "Shift" }, key = "K", action = "SwapDirection", arg = 0 },
- { modifiers = { "Mod4", "Shift" }, key = "L", action = "SwapDirection", arg = 3 },
- {
- keys = {
- { modifiers = { "Mod4" }, key = "Space" },
- { modifiers = { }, key = "T" },
- },
- action = "Spawn",
- arg = "st"
- },
- },
-
- -- 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,
- 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 = "#444444"
- },
- 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 = { },
-}
+-- Basic settings
+oxwm.set_terminal("st")
+oxwm.set_modkey("Mod4")
+oxwm.set_tags({ "1", "2", "3", "4", "5", "6", "7", "8", "9" })
+
+-- Layout symbol overrides
+oxwm.set_layout_symbol("tiling", "[T]")
+oxwm.set_layout_symbol("normie", "[F]")
+
+-- Border configuration
+oxwm.border.set_width(2)
+oxwm.border.set_focused_color(colors.blue)
+oxwm.border.set_unfocused_color(colors.grey)
+
+-- Gap configuration
+oxwm.gaps.set_enabled(true)
+oxwm.gaps.set_inner(5, 5) -- horizontal, vertical
+oxwm.gaps.set_outer(5, 5) -- horizontal, vertical
+
+-- Bar configuration
+oxwm.bar.set_font("monospace:style=Bold:size=10")
+
+-- Bar color schemes (for tag display)
+oxwm.bar.set_scheme_normal(colors.fg, colors.bg, "#444444")
+oxwm.bar.set_scheme_occupied(colors.cyan, colors.bg, colors.cyan)
+oxwm.bar.set_scheme_selected(colors.cyan, colors.bg, colors.purple)
+
+-- Keybindings
+
+-- Basic window management
+oxwm.key.bind({ "Mod4" }, "Return", oxwm.spawn("st"))
+oxwm.key.bind({ "Mod4" }, "D", oxwm.spawn({ "sh", "-c", "dmenu_run -l 10" }))
+oxwm.key.bind({ "Mod4" }, "S", oxwm.spawn({ "sh", "-c", "maim -s | xclip -selection clipboard -t image/png" }))
+oxwm.key.bind({ "Mod4" }, "Q", oxwm.client.kill())
+
+-- Keybind overlay
+oxwm.key.bind({ "Mod4", "Shift" }, "Slash", oxwm.show_keybinds())
+
+-- Client actions
+oxwm.key.bind({ "Mod4", "Shift" }, "F", oxwm.client.toggle_fullscreen())
+oxwm.key.bind({ "Mod4", "Shift" }, "Space", oxwm.client.toggle_floating())
+
+-- Layout management
+oxwm.key.bind({ "Mod4" }, "F", oxwm.layout.set("normie"))
+oxwm.key.bind({ "Mod4" }, "C", oxwm.layout.set("tiling"))
+oxwm.key.bind({ "Mod1" }, "N", oxwm.layout.cycle())
+
+-- Gaps toggle
+oxwm.key.bind({ "Mod4" }, "A", oxwm.toggle_gaps())
+
+-- WM controls
+oxwm.key.bind({ "Mod4", "Shift" }, "Q", oxwm.quit())
+oxwm.key.bind({ "Mod4", "Shift" }, "R", oxwm.restart())
+
+-- Focus direction (vim keys: h=left=2, j=down=1, k=up=0, l=right=3)
+oxwm.key.bind({ "Mod4" }, "H", oxwm.client.focus_direction("left"))
+oxwm.key.bind({ "Mod4" }, "J", oxwm.client.focus_direction("down"))
+oxwm.key.bind({ "Mod4" }, "K", oxwm.client.focus_direction("up"))
+oxwm.key.bind({ "Mod4" }, "L", oxwm.client.focus_direction("right"))
+
+-- Monitor focus
+oxwm.key.bind({ "Mod4" }, "Comma", oxwm.focus_monitor(-1))
+oxwm.key.bind({ "Mod4" }, "Period", oxwm.focus_monitor(1))
+
+-- Tag viewing
+oxwm.key.bind({ "Mod4" }, "1", oxwm.tag.view(0))
+oxwm.key.bind({ "Mod4" }, "2", oxwm.tag.view(1))
+oxwm.key.bind({ "Mod4" }, "3", oxwm.tag.view(2))
+oxwm.key.bind({ "Mod4" }, "4", oxwm.tag.view(3))
+oxwm.key.bind({ "Mod4" }, "5", oxwm.tag.view(4))
+oxwm.key.bind({ "Mod4" }, "6", oxwm.tag.view(5))
+oxwm.key.bind({ "Mod4" }, "7", oxwm.tag.view(6))
+oxwm.key.bind({ "Mod4" }, "8", oxwm.tag.view(7))
+oxwm.key.bind({ "Mod4" }, "9", oxwm.tag.view(8))
+
+-- Move window to tag
+oxwm.key.bind({ "Mod4", "Shift" }, "1", oxwm.tag.move_to(0))
+oxwm.key.bind({ "Mod4", "Shift" }, "2", oxwm.tag.move_to(1))
+oxwm.key.bind({ "Mod4", "Shift" }, "3", oxwm.tag.move_to(2))
+oxwm.key.bind({ "Mod4", "Shift" }, "4", oxwm.tag.move_to(3))
+oxwm.key.bind({ "Mod4", "Shift" }, "5", oxwm.tag.move_to(4))
+oxwm.key.bind({ "Mod4", "Shift" }, "6", oxwm.tag.move_to(5))
+oxwm.key.bind({ "Mod4", "Shift" }, "7", oxwm.tag.move_to(6))
+oxwm.key.bind({ "Mod4", "Shift" }, "8", oxwm.tag.move_to(7))
+oxwm.key.bind({ "Mod4", "Shift" }, "9", oxwm.tag.move_to(8))
+
+-- Swap windows in direction
+oxwm.key.bind({ "Mod4", "Shift" }, "H", oxwm.client.swap_direction("left"))
+oxwm.key.bind({ "Mod4", "Shift" }, "J", oxwm.client.swap_direction("down"))
+oxwm.key.bind({ "Mod4", "Shift" }, "K", oxwm.client.swap_direction("up"))
+oxwm.key.bind({ "Mod4", "Shift" }, "L", oxwm.client.swap_direction("right"))
+
+-- Keychord example: Mod4+Space then T to spawn terminal
+oxwm.key.chord({
+ { { "Mod4" }, "Space" },
+ { {}, "T" }
+}, oxwm.spawn("st"))
+
+-- Status bar blocks
+oxwm.bar.add_block("Ram: {used}/{total} GB", "Ram", nil, 5, colors.light_blue, true)
+oxwm.bar.add_block(" │ ", "Static", " │ ", 999999999, colors.lavender, false)
+oxwm.bar.add_block("Kernel: {}", "Shell", "uname -r", 999999999, colors.red, true)
+oxwm.bar.add_block(" │ ", "Static", " │ ", 999999999, colors.lavender, false)
+oxwm.bar.add_block("{}", "DateTime", "%a, %b %d - %-I:%M %P", 1, colors.cyan, true)
+
+-- Autostart commands (runs once at startup)
+-- oxwm.autostart("picom")
+-- oxwm.autostart("feh --bg-scale ~/wallpaper.jpg")
diff --git a/templates/config.ron b/templates/config.ron
deleted file mode 100644
index 3af89fc..0000000
--- a/templates/config.ron
+++ /dev/null
@@ -1,127 +0,0 @@
-#![enable(implicit_some)]
-// OXWM Configuration File
-// Edit this file and reload with Mod+Shift+R (no compilation needed!)
-
-#DEFINE $terminal = "st"
-#DEFINE $color_blue = 0x6dade3
-#DEFINE $color_grey = 0xbbbbbb
-#DEFINE $color_green = 0x9ece6a
-#DEFINE $color_red = 0xf7768e
-#DEFINE $color_cyan = 0x0db9d7
-#DEFINE $color_purple = 0xad8ee6
-#DEFINE $color_lavender = 0xa9b1d6
-#DEFINE $color_bg = 0x1a1b26
-#DEFINE $color_fg = 0xbbbbbb
-#DEFINE $color_light_blue = 0x7aa2f7
-#DEFINE $modkey = Mod4
-#DEFINE $secondary_modkey = Mod1
-
-(
- border_width: 2,
- border_focused: $color_blue,
- border_unfocused: $color_grey,
- 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: $modkey,
-
- terminal: $terminal,
-
- tags: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
- layout_symbols: [
- (name: "tiling", symbol: "[T]"),
- (name: "normie", symbol: "[F]"),
- ],
-
- // Keybinding Format:
- //
- // New format (supports keychords - multi-key sequences):
- // (keys: [(modifiers: [...], key: X), (modifiers: [...], key: Y)], action: ..., arg: ...)
- //
- // Old format (single key, backwards compatible):
- // (modifiers: [...], key: X, action: ..., arg: ...)
- //
- // Examples:
- // Single key: (keys: [(modifiers: [Mod4], key: Return)], action: Spawn, arg: "st")
- // Keychord: (keys: [(modifiers: [Mod4], key: Space), (modifiers: [], key: F)], action: Spawn, arg: "firefox")
- //
- // You can cancel any in-progress keychord by pressing Escape.
-
- keybindings: [
- // Basic keybindings (old format for backwards compatibility)
- (modifiers: [$modkey], key: Return, action: Spawn, arg: $terminal),
- (modifiers: [$modkey], key: D, action: Spawn, arg: ["sh", "-c", "dmenu_run -l 10"]),
- (modifiers: [$modkey], key: S, action: Spawn, arg: ["sh", "-c", "maim -s | xclip -selection clipboard -t image/png"]),
- (modifiers: [$modkey], key: Q, action: KillClient),
- (modifiers: [$modkey, Shift], key: F, action: ToggleFullScreen),
- (modifiers: [$modkey, Shift], key: Space, action: ToggleFloating),
- (modifiers: [$modkey], key: F, action: ChangeLayout, arg: "normie"),
- (modifiers: [$modkey], key: C, action: ChangeLayout, arg: "tiling"),
- (modifiers: [$secondary_modkey], key: N, action: CycleLayout),
- (modifiers: [$modkey], key: A, action: ToggleGaps),
- (modifiers: [$modkey, Shift], key: Q, action: Quit),
- (modifiers: [$modkey, Shift], key: R, action: Restart),
- (modifiers: [$modkey], key: H, action: FocusDirection, arg: 2),
- (modifiers: [$modkey], key: J, action: FocusDirection, arg: 1),
- (modifiers: [$modkey], key: K, action: FocusDirection, arg: 0),
- (modifiers: [$modkey], key: L, action: FocusDirection, arg: 3),
- (modifiers: [$modkey], key: Comma, action: FocusMonitor, arg: -1),
- (modifiers: [$modkey], key: Period, action: FocusMonitor, arg: 1),
- (modifiers: [$modkey], key: Key1, action: ViewTag, arg: 0),
- (modifiers: [$modkey], key: Key2, action: ViewTag, arg: 1),
- (modifiers: [$modkey], key: Key3, action: ViewTag, arg: 2),
- (modifiers: [$modkey], key: Key4, action: ViewTag, arg: 3),
- (modifiers: [$modkey], key: Key5, action: ViewTag, arg: 4),
- (modifiers: [$modkey], key: Key6, action: ViewTag, arg: 5),
- (modifiers: [$modkey], key: Key7, action: ViewTag, arg: 6),
- (modifiers: [$modkey], key: Key8, action: ViewTag, arg: 7),
- (modifiers: [$modkey], key: Key9, action: ViewTag, arg: 8),
- (modifiers: [$modkey, Shift], key: Key1, action: MoveToTag, arg: 0),
- (modifiers: [$modkey, Shift], key: Key2, action: MoveToTag, arg: 1),
- (modifiers: [$modkey, Shift], key: Key3, action: MoveToTag, arg: 2),
- (modifiers: [$modkey, Shift], key: Key4, action: MoveToTag, arg: 3),
- (modifiers: [$modkey, Shift], key: Key5, action: MoveToTag, arg: 4),
- (modifiers: [$modkey, Shift], key: Key6, action: MoveToTag, arg: 5),
- (modifiers: [$modkey, Shift], key: Key7, action: MoveToTag, arg: 6),
- (modifiers: [$modkey, Shift], key: Key8, action: MoveToTag, arg: 7),
- (modifiers: [$modkey, Shift], key: Key9, action: MoveToTag, arg: 8),
-
- (modifiers: [$modkey, Shift], key: H, action: SwapDirection, arg: 2),
- (modifiers: [$modkey, Shift], key: J, action: SwapDirection, arg: 1),
- (modifiers: [$modkey, Shift], key: K, action: SwapDirection, arg: 0),
- (modifiers: [$modkey, Shift], key: L, action: SwapDirection, arg: 3),
-
- // Example keychord bindings (uncomment to use):
- // KEYCHORDS
- // Press Mod4+Space, then t to spawn terminal
- (keys: [(modifiers: [Mod4], key: Space), (modifiers: [], key: T)], action: Spawn, arg: $terminal),
- ],
-
- status_blocks: [
- (format: "Ram: {used}/{total} GB", command: "Ram", interval_secs: 5, color: $color_light_blue, underline: true),
- (format: " │ ", command: "Static", interval_secs: 18446744073709551615, color: $color_lavender, underline: false),
- (format: "Kernel: {}", command: "Shell", command_arg: "uname -r", interval_secs: 18446744073709551615, color: $color_red, underline: true),
- (format: " │ ", command: "Static", interval_secs: 18446744073709551615, color: $color_lavender, underline: false),
- (format: "{}", command: "DateTime", command_arg: "%a, %b %d - %-I:%M %P", interval_secs: 1, color: $color_cyan, underline: true),
- ],
-
- scheme_normal: (foreground: $color_fg, background: $color_bg, underline: 0x444444),
- scheme_occupied: (foreground: $color_cyan, background: $color_bg, underline: $color_cyan),
- scheme_selected: (foreground: $color_cyan, background: $color_bg, underline: $color_purple),
-
- // Autostart commands - these are executed when the window manager starts
- // Commands are run in a shell, so you can use shell syntax (pipes, &&, etc.)
- // Example: ["picom", "nitrogen --restore", "~/.config/polybar/launch.sh"]
- autostart: [
- // Uncomment and add your autostart commands here:
- // "picom -b",
- // "nitrogen --restore &",
- // "dunst &",
- ],
-)
-
diff --git a/templates/oxwm.lua b/templates/oxwm.lua
new file mode 100644
index 0000000..f79e5cb
--- /dev/null
+++ b/templates/oxwm.lua
@@ -0,0 +1,213 @@
+---@meta
+
+---OXWM Configuration API
+---@class oxwm
+oxwm = {}
+
+---Spawn a command
+---@param cmd string|string[] Command to spawn (string or array of strings)
+---@return table Action table for keybinding
+function oxwm.spawn(cmd) end
+
+---Set the terminal emulator
+---@param terminal string Terminal command (e.g., "st", "alacritty")
+function oxwm.set_terminal(terminal) end
+
+---Set the modifier key
+---@param modkey string Modifier key ("Mod1", "Mod4", "Shift", "Control")
+function oxwm.set_modkey(modkey) end
+
+---Set workspace tags
+---@param tags string[] Array of tag names
+function oxwm.set_tags(tags) end
+
+---Set layout symbol override
+---@param name string Layout name (e.g., "tiling", "normie")
+---@param symbol string Symbol to display (e.g., "[T]", "[F]")
+function oxwm.set_layout_symbol(name, symbol) end
+
+---Quit the window manager
+---@return table Action table for keybinding
+function oxwm.quit() end
+
+---Restart the window manager
+---@return table Action table for keybinding
+function oxwm.restart() end
+
+---Recompile the window manager
+---@return table Action table for keybinding
+function oxwm.recompile() end
+
+---Toggle gaps on/off
+---@return table Action table for keybinding
+function oxwm.toggle_gaps() end
+
+---Show keybind overlay
+---@return table Action table for keybinding
+function oxwm.show_keybinds() end
+
+---Focus monitor by index
+---@param index integer Monitor index (-1 for previous, 1 for next)
+---@return table Action table for keybinding
+function oxwm.focus_monitor(index) end
+
+---Keybinding module
+---@class oxwm.key
+oxwm.key = {}
+
+---Bind a key combination to an action
+---@param modifiers string|string[] Modifier keys (e.g., {"Mod4"}, {"Mod4", "Shift"})
+---@param key string Key name (e.g., "Return", "Q", "1")
+---@param action table Action returned by oxwm functions
+function oxwm.key.bind(modifiers, key, action) end
+
+---Bind a keychord (multi-key sequence) to an action
+---@param keys table[] Array of key presses, each: {{modifiers}, key}
+---@param action table Action returned by oxwm functions
+function oxwm.key.chord(keys, action) end
+
+---Gap configuration module
+---@class oxwm.gaps
+oxwm.gaps = {}
+
+---Set gaps enabled/disabled
+---@param enabled boolean Enable or disable gaps
+function oxwm.gaps.set_enabled(enabled) end
+
+---Enable gaps
+function oxwm.gaps.enable() end
+
+---Disable gaps
+function oxwm.gaps.disable() end
+
+---Set inner gaps
+---@param horizontal integer Horizontal inner gap in pixels
+---@param vertical integer Vertical inner gap in pixels
+function oxwm.gaps.set_inner(horizontal, vertical) end
+
+---Set outer gaps
+---@param horizontal integer Horizontal outer gap in pixels
+---@param vertical integer Vertical outer gap in pixels
+function oxwm.gaps.set_outer(horizontal, vertical) end
+
+---Border configuration module
+---@class oxwm.border
+oxwm.border = {}
+
+---Set border width
+---@param width integer Border width in pixels
+function oxwm.border.set_width(width) end
+
+---Set focused window border color
+---@param color string|integer Color as hex string ("#ff0000", "0xff0000") or integer
+function oxwm.border.set_focused_color(color) end
+
+---Set unfocused window border color
+---@param color string|integer Color as hex string ("#666666", "0x666666") or integer
+function oxwm.border.set_unfocused_color(color) end
+
+---Client/window management module
+---@class oxwm.client
+oxwm.client = {}
+
+---Kill the focused window
+---@return table Action table for keybinding
+function oxwm.client.kill() end
+
+---Toggle fullscreen mode
+---@return table Action table for keybinding
+function oxwm.client.toggle_fullscreen() end
+
+---Toggle floating mode
+---@return table Action table for keybinding
+function oxwm.client.toggle_floating() end
+
+---Focus window in direction
+---@param direction "up"|"down"|"left"|"right" Direction to focus
+---@return table Action table for keybinding
+function oxwm.client.focus_direction(direction) end
+
+---Swap window in direction
+---@param direction "up"|"down"|"left"|"right" Direction to swap
+---@return table Action table for keybinding
+function oxwm.client.swap_direction(direction) end
+
+---Smart move window (move or swap)
+---@param direction "up"|"down"|"left"|"right" Direction to move
+---@return table Action table for keybinding
+function oxwm.client.smart_move(direction) end
+
+---Focus stack (next/previous window)
+---@param dir integer Direction (1 for next, -1 for previous)
+---@return table Action table for keybinding
+function oxwm.client.focus_stack(dir) end
+
+---Exchange client positions
+---@return table Action table for keybinding
+function oxwm.client.exchange() end
+
+---Layout management module
+---@class oxwm.layout
+oxwm.layout = {}
+
+---Cycle through layouts
+---@return table Action table for keybinding
+function oxwm.layout.cycle() end
+
+---Set specific layout
+---@param name string Layout name (e.g., "tiling", "normie")
+---@return table Action table for keybinding
+function oxwm.layout.set(name) end
+
+---Tag/workspace management module
+---@class oxwm.tag
+oxwm.tag = {}
+
+---View/switch to tag
+---@param index integer Tag index (0-based)
+---@return table Action table for keybinding
+function oxwm.tag.view(index) end
+
+---Move focused window to tag
+---@param index integer Tag index (0-based)
+---@return table Action table for keybinding
+function oxwm.tag.move_to(index) end
+
+---Status bar configuration module
+---@class oxwm.bar
+oxwm.bar = {}
+
+---Set status bar font
+---@param font string Font string (e.g., "monospace:style=Bold:size=10")
+function oxwm.bar.set_font(font) end
+
+---Add a status bar block
+---@param format string Format string with {} placeholders
+---@param command "DateTime"|"Shell"|"Ram"|"Static"|"Battery" Block command type
+---@param arg string|table|nil Command argument (format for DateTime, command for Shell, text for Static, formats table for Battery, nil for Ram)
+---@param interval integer Update interval in seconds
+---@param color string|integer Color as hex string or integer
+---@param underline boolean Whether to underline the block
+function oxwm.bar.add_block(format, command, arg, interval, color, underline) end
+
+---Set normal tag color scheme (unselected, no windows)
+---@param foreground string|integer Foreground color
+---@param background string|integer Background color
+---@param underline string|integer Underline color
+function oxwm.bar.set_scheme_normal(foreground, background, underline) end
+
+---Set occupied tag color scheme (unselected, has windows)
+---@param foreground string|integer Foreground color
+---@param background string|integer Background color
+---@param underline string|integer Underline color
+function oxwm.bar.set_scheme_occupied(foreground, background, underline) end
+
+---Set selected tag color scheme (currently selected tag)
+---@param foreground string|integer Foreground color
+---@param background string|integer Background color
+---@param underline string|integer Underline color
+function oxwm.bar.set_scheme_selected(foreground, background, underline) end
+
+---Add an autostart command
+---@param cmd string Command to run at startup
+function oxwm.autostart(cmd) end