oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

Lua Api.

Commit
5d148dfb3297969e5e6d4de848c1c7bd1c80575b
Parent
ad06fa5
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-11-12 09:29:30

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