oxwm

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

Updated hot reload, xmonad style

Commit
0fc3d435e6bbf7ff432b203536c5d9a8a0d63ccc
Parent
7c10ebe
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-10-10 04:40:54

Diff

diff --git a/Cargo.lock b/Cargo.lock
index 29e52c0..fd7f154 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -70,6 +70,27 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "errno"
 version = "0.3.14"
@@ -77,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.61.0",
 ]
 
 [[package]]
@@ -93,7 +114,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
 dependencies = [
  "rustix",
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
 ]
 
 [[package]]
@@ -136,6 +168,16 @@ version = "0.2.175"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
 
+[[package]]
+name = "libredox"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.11.0"
@@ -163,12 +205,19 @@ version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "oxwm"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
+ "dirs",
  "x11",
  "x11rb",
 ]
@@ -197,6 +246,17 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
 [[package]]
 name = "rustix"
 version = "1.1.2"
@@ -207,7 +267,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.61.0",
 ]
 
 [[package]]
@@ -233,12 +293,38 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
 
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.104"
@@ -357,6 +443,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.61.0"
@@ -366,34 +461,67 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -406,24 +534,48 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
diff --git a/Cargo.toml b/Cargo.toml
index 9bcfaaf..bb57ae3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,8 +3,17 @@ name = "oxwm"
 version = "0.1.0"
 edition = "2024"
 
+[lib]
+name = "oxwm"
+path = "src/lib.rs"
+
+[[bin]]
+name = "oxwm"
+path = "src/bin/main.rs"
+
 [dependencies]
 x11 = { version = "2.21", features = ["xlib", "xft"] }
 x11rb = "0.13"
 anyhow = "1"
 chrono = "0.4"
+dirs = "5.0"
diff --git a/justfile b/justfile
index 77f67f8..890e089 100644
--- a/justfile
+++ b/justfile
@@ -1,28 +1,41 @@
-ensure-config:
-    @if [ ! -f src/config.rs ]; then \
-        echo "Creating config.rs from default_config.rs..."; \
-        cp src/default_config.rs src/config.rs; \
-        echo "✓ Edit src/config.rs to customize your setup"; \
-    fi
-
-build: ensure-config
+build:
     cargo build --release
 
 install: build
     sudo cp target/release/oxwm /usr/local/bin/oxwm
-    @echo "✓ oxwm installed. Restart X or hit Mod+Shift+R (when implemented)"
+    @echo "✓ oxwm installed to /usr/local/bin/oxwm"
+    @echo "  Run 'oxwm --init' to set up your config"
 
 uninstall:
     sudo rm -f /usr/local/bin/oxwm
+    @echo "✓ oxwm uninstalled"
+    @echo "  Your config at ~/.config/oxwm is preserved"
 
 clean:
     cargo clean
 
-test: ensure-config
+test:
     pkill Xephyr || true
     Xephyr -screen 1280x800 :1 & sleep 1
-    DISPLAY=:1 cargo run
+    DISPLAY=:1 cargo run -- --init
+    DISPLAY=:1 ~/.cache/oxwm/oxwm-binary
+
+init:
+    cargo run -- --init
+
+recompile:
+    cargo run -- --recompile
+
+edit:
+    $EDITOR ~/.config/oxwm/config.rs
+
+check:
+    cargo clippy -- -W clippy::all
+    cargo fmt -- --check
+
+fmt:
+    cargo fmt
+
+pre-commit: fmt check build
+    @echo "✓ All checks passed!"
 
-reset-config:
-    cp src/default_config.rs src/config.rs
-    @echo "✓ Config reset to default"
diff --git a/src/bar/bar.rs b/src/bar/bar.rs
index 1d2b7cc..fd8ac7b 100644
--- a/src/bar/bar.rs
+++ b/src/bar/bar.rs
@@ -1,6 +1,6 @@
 use super::blocks::Block;
 use super::font::{Font, FontDraw};
-use crate::config::{FONT, SCHEME_NORMAL, SCHEME_OCCUPIED, SCHEME_SELECTED, STATUS_BLOCKS, TAGS};
+use crate::Config;
 use anyhow::Result;
 use std::time::Instant;
 use x11rb::COPY_DEPTH_FROM_PARENT;
@@ -25,10 +25,20 @@ pub struct Bar {
     block_last_updates: Vec<Instant>,
     block_underlines: Vec<bool>,
     status_text: String,
+
+    tags: Vec<String>,
+    scheme_normal: crate::ColorScheme,
+    scheme_occupied: crate::ColorScheme,
+    scheme_selected: crate::ColorScheme,
 }
 
 impl Bar {
-    pub fn new(connection: &RustConnection, screen: &Screen, screen_num: usize) -> Result<Self> {
+    pub fn new(
+        connection: &RustConnection,
+        screen: &Screen,
+        screen_num: usize,
+        config: &Config,
+    ) -> Result<Self> {
         let window = connection.generate_id()?;
         let graphics_context = connection.generate_id()?;
 
@@ -38,7 +48,7 @@ impl Bar {
         if display.is_null() {
             anyhow::bail!("Failed to open X11 display for XFT");
         }
-        let font = Font::new(display, screen_num as i32, FONT)?;
+        let font = Font::new(display, screen_num as i32, &config.font)?;
 
         let height = (font.height() as f32 * 1.4) as u16;
 
@@ -54,7 +64,7 @@ impl Bar {
             WindowClass::INPUT_OUTPUT,
             screen.root_visual,
             &CreateWindowAux::new()
-                .background_pixel(SCHEME_NORMAL.background)
+                .background_pixel(config.scheme_normal.background)
                 .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS)
                 .override_redirect(1),
         )?;
@@ -63,8 +73,8 @@ impl Bar {
             graphics_context,
             window,
             &CreateGCAux::new()
-                .foreground(SCHEME_NORMAL.foreground)
-                .background(SCHEME_NORMAL.background),
+                .foreground(config.scheme_normal.foreground)
+                .background(config.scheme_normal.background),
         )?;
 
         connection.map_window(window)?;
@@ -77,7 +87,8 @@ impl Bar {
 
         let horizontal_padding = (font.height() as f32 * 0.4) as u16;
 
-        let tag_widths = TAGS
+        let tag_widths = config
+            .tags
             .iter()
             .map(|tag| {
                 let text_width = font.text_width(tag);
@@ -85,14 +96,16 @@ impl Bar {
             })
             .collect();
 
-        let blocks: Vec<Box<dyn Block>> = STATUS_BLOCKS
+        let blocks: Vec<Box<dyn Block>> = config
+            .status_blocks
             .iter()
-            .map(|config| config.to_block())
+            .map(|block_config| block_config.to_block())
             .collect();
 
-        let block_underlines: Vec<bool> = STATUS_BLOCKS
+        let block_underlines: Vec<bool> = config
+            .status_blocks
             .iter()
-            .map(|config| config.underline)
+            .map(|block_config| block_config.underline)
             .collect();
 
         let block_last_updates = vec![Instant::now(); blocks.len()];
@@ -111,6 +124,10 @@ impl Bar {
             block_last_updates,
             block_underlines,
             status_text: String::new(),
+            tags: config.tags.clone(),
+            scheme_normal: config.scheme_normal,
+            scheme_occupied: config.scheme_occupied,
+            scheme_selected: config.scheme_selected,
         })
     }
 
@@ -134,7 +151,7 @@ impl Bar {
             let elapsed = now.duration_since(self.block_last_updates[i]);
 
             if elapsed >= block.interval() {
-                if let Ok(_) = block.content() {
+                if block.content().is_ok() {
                     self.block_last_updates[i] = now;
                     changed = true;
                 }
@@ -167,7 +184,7 @@ impl Bar {
 
         connection.change_gc(
             self.graphics_context,
-            &ChangeGCAux::new().foreground(SCHEME_NORMAL.background),
+            &ChangeGCAux::new().foreground(self.scheme_normal.background),
         )?;
         connection.poly_fill_rectangle(
             self.window,
@@ -182,7 +199,7 @@ impl Bar {
 
         let mut x_position: i16 = 0;
 
-        for (tag_index, tag) in TAGS.iter().enumerate() {
+        for (tag_index, tag) in self.tags.iter().enumerate() {
             let tag_mask = 1 << tag_index;
             let is_selected = (current_tags & tag_mask) != 0;
             let is_occupied = (occupied_tags & tag_mask) != 0;
@@ -190,11 +207,11 @@ impl Bar {
             let tag_width = self.tag_widths[tag_index];
 
             let scheme = if is_selected {
-                &SCHEME_SELECTED
+                &self.scheme_selected
             } else if is_occupied {
-                &SCHEME_OCCUPIED
+                &self.scheme_occupied
             } else {
-                &SCHEME_NORMAL
+                &self.scheme_normal
             };
 
             let text_width = self.font.text_width(tag);
@@ -297,7 +314,7 @@ impl Bar {
             }
             current_x_position += tag_width as i16;
         }
-        return None;
+        None
     }
 
     pub fn needs_redraw(&self) -> bool {
diff --git a/src/bar/blocks/mod.rs b/src/bar/blocks/mod.rs
index e1babfb..ac73099 100644
--- a/src/bar/blocks/mod.rs
+++ b/src/bar/blocks/mod.rs
@@ -17,6 +17,7 @@ pub trait Block {
     fn color(&self) -> u32;
 }
 
+#[derive(Clone)]
 pub struct BlockConfig {
     pub format: &'static str,
     pub command: BlockCommand,
@@ -25,6 +26,7 @@ pub struct BlockConfig {
     pub underline: bool,
 }
 
+#[derive(Clone)]
 pub enum BlockCommand {
     Shell(&'static str),
     DateTime(&'static str),
diff --git a/src/bar/blocks/ram.rs b/src/bar/blocks/ram.rs
index 025525b..eeb8f66 100644
--- a/src/bar/blocks/ram.rs
+++ b/src/bar/blocks/ram.rs
@@ -40,8 +40,6 @@ impl Ram {
         }
 
         let used = total.saturating_sub(available);
-        let used_gb = used as f32 / 1024.0 / 1024.0;
-        let total_gb = total as f32 / 1024.0 / 1024.0;
         let percentage = if total > 0 {
             (used as f32 / total as f32) * 100.0
         } else {
diff --git a/src/bin/main.rs b/src/bin/main.rs
new file mode 100644
index 0000000..6ba9687
--- /dev/null
+++ b/src/bin/main.rs
@@ -0,0 +1,195 @@
+use anyhow::Result;
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() -> Result<()> {
+    let args: Vec<String> = std::env::args().collect();
+
+    match args.get(1).map(|s| s.as_str()) {
+        Some("--init") => return init_config(),
+        Some("--recompile") => return recompile_config(),
+        Some("--version") => {
+            println!("oxwm {}", env!("CARGO_PKG_VERSION"));
+            return Ok(());
+        }
+        Some("--help") => {
+            print_help();
+            return Ok(());
+        }
+        _ => {}
+    }
+
+    let cache_binary = get_cache_binary_path();
+
+    if cache_binary.exists() {
+        let config_path = get_config_path().join("config.rs");
+        if should_recompile(&config_path, &cache_binary)? {
+            println!("Config changed, recompiling...");
+            recompile_config()?;
+        }
+
+        use std::os::unix::process::CommandExt;
+        let err = Command::new(&cache_binary).args(&args[1..]).exec();
+        anyhow::bail!("Failed to exec user binary: {}", err);
+    } else {
+        eprintln!("No configuration found.");
+        eprintln!("Run: oxwm --init");
+        eprintln!("Then add 'exec oxwm' to your ~/.xinitrc");
+        std::process::exit(1);
+    }
+}
+
+fn init_config() -> Result<()> {
+    let config_dir = get_config_path();
+    std::fs::create_dir_all(&config_dir)?;
+
+    if config_dir.join("config.rs").exists() {
+        eprintln!("Config already exists at ~/.config/oxwm/config.rs");
+        eprintln!("Remove it first if you want to reinitialize.");
+        return Ok(());
+    }
+
+    let config_template = include_str!("../../templates/config.rs");
+    std::fs::write(config_dir.join("config.rs"), config_template)?;
+
+    let main_template = include_str!("../../templates/main.rs");
+    std::fs::write(config_dir.join("main.rs"), main_template)?;
+
+    let manifest_dir = env!("CARGO_MANIFEST_DIR");
+    let cargo_toml = format!(
+        r#"[package]
+name = "oxwm-user"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+oxwm = {{ path = "{}" }}
+anyhow = "1"
+
+[[bin]]
+name = "oxwm-user"
+path = "main.rs"
+"#,
+        manifest_dir
+    );
+    std::fs::write(config_dir.join("Cargo.toml"), cargo_toml)?;
+
+    std::fs::write(config_dir.join(".gitignore"), "target/\nCargo.lock\n")?;
+
+    println!("✓ Created ~/.config/oxwm/config.rs");
+    println!("✓ Created ~/.config/oxwm/main.rs");
+    println!("✓ Edit ~/.config/oxwm/config.rs to customize your setup");
+
+    println!("\nCompiling initial configuration...");
+    recompile_config()?;
+
+    println!("\n✓ Setup complete!");
+    println!("  Add 'exec oxwm' to your ~/.xinitrc");
+    println!("  Reload config anytime with Mod+Shift+R");
+
+    Ok(())
+}
+
+fn recompile_config() -> Result<()> {
+    let config_dir = get_config_path();
+
+    if !config_dir.join("config.rs").exists() {
+        anyhow::bail!("No config found. Run: oxwm --init");
+    }
+
+    println!("Compiling oxwm configuration...");
+
+    let output = Command::new("cargo")
+        .args(&["build", "--release"])
+        .current_dir(&config_dir)
+        .output()?;
+
+    if !output.status.success() {
+        let stderr = String::from_utf8_lossy(&output.stderr);
+        eprintln!("\n❌ Compilation failed:\n{}", stderr);
+
+        let _ = Command::new("notify-send")
+            .args(&[
+                "-u",
+                "critical",
+                "OXWM Compile Error",
+                "Check terminal for details",
+            ])
+            .spawn();
+
+        anyhow::bail!("Failed to compile configuration");
+    }
+
+    let source = config_dir.join("target/release/oxwm-user");
+    let dest = get_cache_binary_path();
+
+    std::fs::create_dir_all(dest.parent().unwrap())?;
+    std::fs::copy(&source, &dest)?;
+
+    println!("✓ Compiled successfully");
+
+    let _ = Command::new("notify-send")
+        .args(&[
+            "OXWM",
+            "Configuration recompiled! Hit Mod+Shift+R to restart.",
+        ])
+        .spawn();
+
+    Ok(())
+}
+
+fn should_recompile(config: &PathBuf, binary: &PathBuf) -> Result<bool> {
+    if !config.exists() {
+        return Ok(false);
+    }
+
+    let config_dir = get_config_path();
+    let binary_time = std::fs::metadata(binary)?.modified()?;
+
+    let watch_files = ["config.rs", "main.rs", "Cargo.toml"];
+
+    for filename in &watch_files {
+        let path = config_dir.join(filename);
+        if !path.exists() {
+            continue;
+        }
+
+        let file_time = std::fs::metadata(&path)?.modified()?;
+        if file_time > binary_time {
+            return Ok(true);
+        }
+    }
+
+    Ok(false)
+}
+
+fn get_config_path() -> PathBuf {
+    dirs::config_dir()
+        .expect("Could not find config directory")
+        .join("oxwm")
+}
+
+fn get_cache_binary_path() -> PathBuf {
+    dirs::cache_dir()
+        .expect("Could not find cache directory")
+        .join("oxwm/oxwm-binary")
+}
+
+fn print_help() {
+    println!("OXWM - A dynamic window manager written in Rust\n");
+    println!("USAGE:");
+    println!("    oxwm [OPTIONS]\n");
+    println!("OPTIONS:");
+    println!("    --init         Initialize user configuration in ~/.config/oxwm");
+    println!("    --recompile    Recompile user configuration");
+    println!("    --version      Print version information");
+    println!("    --help         Print this help message\n");
+    println!("SETUP:");
+    println!("    1. Run 'oxwm --init' to create your config");
+    println!("    2. Edit ~/.config/oxwm/config.rs");
+    println!("    3. Add 'exec oxwm' to your ~/.xinitrc");
+    println!("    4. Start X with 'startx'\n");
+    println!("CONFIGURATION:");
+    println!("    Config location: ~/.config/oxwm/config.rs");
+    println!("    Reload hotkey:   Mod+Shift+R (auto-recompiles if needed)");
+}
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 8313e86..3933192 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -1,4 +1,3 @@
-use crate::config::KEYBINDINGS;
 use anyhow::Result;
 use x11rb::connection::Connection;
 use x11rb::protocol::xproto::*;
@@ -10,6 +9,7 @@ pub enum KeyAction {
     FocusStack,
     Quit,
     Restart,
+    Recompile,
     ViewTag,
     ToggleGaps,
     ToggleFullScreen,
@@ -17,7 +17,7 @@ pub enum KeyAction {
     None,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum Arg {
     None,
     Int(i32),
@@ -25,6 +25,13 @@ pub enum Arg {
     Array(&'static [&'static str]),
 }
 
+impl Arg {
+    pub const fn none() -> Self {
+        Arg::None
+    }
+}
+
+#[derive(Clone)]
 pub struct Key {
     pub(crate) modifiers: &'static [KeyButMask],
     pub(crate) key: Keycode,
@@ -54,8 +61,12 @@ fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
         .fold(0u16, |acc, &modifier| acc | u16::from(modifier))
 }
 
-pub fn setup_keybinds(connection: &impl Connection, root: Window) -> Result<()> {
-    for keybinding in KEYBINDINGS {
+pub fn setup_keybinds(
+    connection: &impl Connection,
+    root: Window,
+    keybindings: &[Key],
+) -> Result<()> {
+    for keybinding in keybindings {
         let modifier_mask = modifiers_to_mask(keybinding.modifiers);
 
         connection.grab_key(
@@ -70,14 +81,14 @@ pub fn setup_keybinds(connection: &impl Connection, root: Window) -> Result<()>
     Ok(())
 }
 
-pub fn handle_key_press(event: KeyPressEvent) -> Result<(KeyAction, &'static Arg)> {
-    for keybinding in KEYBINDINGS {
+pub fn handle_key_press(event: KeyPressEvent, keybindings: &[Key]) -> Result<(KeyAction, Arg)> {
+    for keybinding in keybindings {
         let modifier_mask = modifiers_to_mask(keybinding.modifiers);
 
         if event.detail == keybinding.key && event.state == modifier_mask.into() {
-            return Ok((keybinding.func, &keybinding.arg));
+            return Ok((keybinding.func, keybinding.arg.clone()));
         }
     }
 
-    Ok((KeyAction::None, &Arg::None))
+    Ok((KeyAction::None, Arg::None))
 }
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..dcd06a7
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,91 @@
+pub mod bar;
+pub mod keyboard;
+pub mod layout;
+pub mod window_manager;
+
+pub mod prelude {
+    pub use crate::ColorScheme;
+    pub use crate::bar::{BlockCommand, BlockConfig};
+    pub use crate::keyboard::{Arg, KeyAction, handlers::Key, keycodes};
+    pub use x11rb::protocol::xproto::KeyButMask;
+}
+
+#[derive(Clone)]
+pub struct Config {
+    // Appearance
+    pub border_width: u32,
+    pub border_focused: u32,
+    pub border_unfocused: u32,
+    pub font: String,
+
+    // Gaps
+    pub gaps_enabled: bool,
+    pub gap_inner_horizontal: u32,
+    pub gap_inner_vertical: u32,
+    pub gap_outer_horizontal: u32,
+    pub gap_outer_vertical: u32,
+
+    // Basics
+    pub terminal: String,
+    pub modkey: x11rb::protocol::xproto::KeyButMask,
+
+    // Tags
+    pub tags: Vec<String>,
+
+    // Keybindings
+    pub keybindings: Vec<crate::keyboard::handlers::Key>,
+
+    // Status bar
+    pub status_blocks: Vec<crate::bar::BlockConfig>,
+
+    // Bar color schemes
+    pub scheme_normal: ColorScheme,
+    pub scheme_occupied: ColorScheme,
+    pub scheme_selected: ColorScheme,
+}
+
+#[derive(Clone, Copy)]
+pub struct ColorScheme {
+    pub foreground: u32,
+    pub background: u32,
+    pub underline: u32,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            border_width: 2,
+            border_focused: 0x6dade3,
+            border_unfocused: 0xbbbbbb,
+            font: "monospace:size=12".to_string(),
+            gaps_enabled: false,
+            gap_inner_horizontal: 0,
+            gap_inner_vertical: 0,
+            gap_outer_horizontal: 0,
+            gap_outer_vertical: 0,
+            terminal: "xterm".to_string(),
+            modkey: x11rb::protocol::xproto::KeyButMask::MOD4,
+            tags: vec!["1", "2", "3", "4", "5", "6", "7", "8", "9"]
+                .into_iter()
+                .map(String::from)
+                .collect(),
+            keybindings: Vec::new(),
+            status_blocks: Vec::new(),
+            scheme_normal: ColorScheme {
+                foreground: 0xbbbbbb,
+                background: 0x1a1b26,
+                underline: 0x444444,
+            },
+            scheme_occupied: ColorScheme {
+                foreground: 0x0db9d7,
+                background: 0x1a1b26,
+                underline: 0x0db9d7,
+            },
+            scheme_selected: ColorScheme {
+                foreground: 0x0db9d7,
+                background: 0x1a1b26,
+                underline: 0xad8ee6,
+            },
+        }
+    }
+}
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 5ebeaf1..56323a6 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -1,14 +1,13 @@
+use crate::Config;
 use crate::bar::Bar;
-use crate::config::{
-    BORDER_FOCUSED, BORDER_UNFOCUSED, BORDER_WIDTH, GAP_INNER_HORIZONTAL, GAP_INNER_VERTICAL,
-    GAP_OUTER_HORIZONTAL, GAP_OUTER_VERTICAL, GAPS_ENABLED, MODKEY, TAG_COUNT,
-};
 use crate::keyboard::{self, Arg, KeyAction};
 use crate::layout::GapConfig;
 use crate::layout::Layout;
 use crate::layout::tiling::TilingLayout;
+
 use anyhow::Result;
 use std::collections::HashSet;
+use std::path::PathBuf;
 
 use x11rb::connection::Connection;
 use x11rb::protocol::Event;
@@ -21,6 +20,7 @@ pub fn tag_mask(tag: usize) -> TagMask {
 }
 
 pub struct WindowManager {
+    config: Config,
     connection: RustConnection,
     screen_number: usize,
     root: Window,
@@ -37,7 +37,7 @@ pub struct WindowManager {
 }
 
 impl WindowManager {
-    pub fn new() -> Result<Self> {
+    pub fn new(config: Config) -> Result<Self> {
         let (connection, screen_number) = x11rb::connect(None)?;
         let root = connection.setup().roots[screen_number].root;
         let screen = connection.setup().roots[screen_number].clone();
@@ -64,7 +64,7 @@ impl WindowManager {
             x11rb::NONE,
             x11rb::NONE,
             ButtonIndex::M1,
-            u16::from(MODKEY).into(),
+            u16::from(config.modkey).into(),
         )?;
 
         connection.grab_button(
@@ -76,15 +76,16 @@ impl WindowManager {
             x11rb::NONE,
             x11rb::NONE,
             ButtonIndex::M3,
-            u16::from(MODKEY).into(),
+            u16::from(config.modkey).into(),
         )?;
 
-        let bar = Bar::new(&connection, &screen, screen_number)?;
+        let bar = Bar::new(&connection, &screen, screen_number, &config)?;
 
-        let selected_tags = Self::get_saved_selected_tags(&connection, root)?;
-        let gaps_enabled = GAPS_ENABLED;
+        let selected_tags = Self::get_saved_selected_tags(&connection, root, config.tags.len())?;
+        let gaps_enabled = config.gaps_enabled;
 
-        let mut window_manger = Self {
+        let mut window_manager = Self {
+            config,
             connection,
             screen_number,
             root,
@@ -100,13 +101,17 @@ impl WindowManager {
             bar,
         };
 
-        window_manger.scan_existing_windows()?;
-        window_manger.update_bar()?;
+        window_manager.scan_existing_windows()?;
+        window_manager.update_bar()?;
 
-        Ok(window_manger)
+        Ok(window_manager)
     }
 
-    fn get_saved_selected_tags(connection: &RustConnection, root: Window) -> Result<TagMask> {
+    fn get_saved_selected_tags(
+        connection: &RustConnection,
+        root: Window,
+        tag_count: usize,
+    ) -> Result<TagMask> {
         let net_current_desktop = connection
             .intern_atom(false, b"_NET_CURRENT_DESKTOP")?
             .reply()?
@@ -123,7 +128,7 @@ impl WindowManager {
                     prop.value[2],
                     prop.value[3],
                 ]);
-                if desktop < TAG_COUNT as u32 {
+                if desktop < tag_count as u32 {
                     let mask = tag_mask(desktop as usize);
                     return Ok(mask);
                 }
@@ -224,7 +229,7 @@ impl WindowManager {
                     prop.value[3],
                 ]);
 
-                if tags != 0 && tags < (1 << crate::config::TAG_COUNT) {
+                if tags != 0 && tags < (1 << self.config.tags.len()) {
                     return Ok(tags);
                 }
             }
@@ -284,10 +289,81 @@ impl WindowManager {
         Ok(())
     }
 
+    fn handle_restart(&self) -> Result<bool> {
+        if !can_recompile() {
+            eprintln!("Error: cargo not found. Install rust toolchain.");
+            notify_error("OXWM", "Cannot recompile: cargo not installed");
+            return Ok(false);
+        }
+
+        if self.needs_recompile()? {
+            println!("Config changed, recompiling...");
+            self.recompile()?;
+        }
+
+        Ok(true)
+    }
+
+    fn needs_recompile(&self) -> Result<bool> {
+        let config_dir = get_config_path();
+        let binary_path = get_cache_binary_path();
+
+        if !binary_path.exists() {
+            return Ok(true);
+        }
+
+        let binary_time = std::fs::metadata(&binary_path)?.modified()?;
+
+        let watch_files = ["config.rs", "main.rs", "Cargo.toml"];
+
+        for filename in &watch_files {
+            let path = config_dir.join(filename);
+            if !path.exists() {
+                continue;
+            }
+
+            let file_time = std::fs::metadata(&path)?.modified()?;
+            if file_time > binary_time {
+                println!("✓ Change detected: {}", filename);
+                return Ok(true);
+            }
+        }
+
+        Ok(false)
+    }
+
+    fn recompile(&self) -> Result<()> {
+        let config_dir = get_config_path();
+
+        notify("OXWM", "Recompiling configuration...");
+
+        let output = std::process::Command::new("cargo")
+            .args(&["build", "--release"])
+            .current_dir(&config_dir)
+            .output()?;
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            notify_error("OXWM Compile Error", &stderr);
+            eprintln!("Compilation failed:\n{}", stderr);
+            anyhow::bail!("Failed to compile configuration");
+        }
+
+        let source = config_dir.join("target/release/oxwm-user");
+        let dest = get_cache_binary_path();
+
+        std::fs::create_dir_all(dest.parent().unwrap())?;
+        std::fs::copy(&source, &dest)?;
+
+        notify("OXWM", "Recompiled successfully! Restarting...");
+
+        Ok(())
+    }
+
     pub fn run(&mut self) -> Result<bool> {
         println!("oxwm started on display {}", self.screen_number);
 
-        keyboard::setup_keybinds(&self.connection, self.root)?;
+        keyboard::setup_keybinds(&self.connection, self.root, &self.config.keybindings)?;
         self.update_bar()?;
 
         loop {
@@ -387,7 +463,12 @@ impl WindowManager {
                 }
             }
             KeyAction::Quit | KeyAction::Restart => {
-                //no-op
+                // Handled in handle_event
+            }
+            KeyAction::Recompile => {
+                if let Err(e) = self.recompile() {
+                    eprintln!("Recompile failed: {}", e);
+                }
             }
             KeyAction::ViewTag => {
                 if let Arg::Int(tag_index) = arg {
@@ -403,9 +484,7 @@ impl WindowManager {
                 self.gaps_enabled = !self.gaps_enabled;
                 self.apply_layout()?;
             }
-            KeyAction::None => {
-                //no-op
-            }
+            KeyAction::None => {}
         }
         Ok(())
     }
@@ -439,7 +518,7 @@ impl WindowManager {
     }
 
     pub fn view_tag(&mut self, tag_index: usize) -> Result<()> {
-        if tag_index >= TAG_COUNT {
+        if tag_index >= self.config.tags.len() {
             return Ok(());
         }
 
@@ -487,7 +566,7 @@ impl WindowManager {
     }
 
     pub fn move_to_tag(&mut self, tag_index: usize) -> Result<()> {
-        if tag_index >= TAG_COUNT {
+        if tag_index >= self.config.tags.len() {
             return Ok(());
         }
 
@@ -547,9 +626,9 @@ impl WindowManager {
     fn update_focus_visuals(&self) -> Result<()> {
         for &window in &self.windows {
             let (border_color, border_width) = if self.focused_window == Some(window) {
-                (BORDER_FOCUSED, BORDER_WIDTH)
+                (self.config.border_focused, self.config.border_width)
             } else {
-                (BORDER_UNFOCUSED, BORDER_WIDTH)
+                (self.config.border_unfocused, self.config.border_width)
             };
 
             self.connection.configure_window(
@@ -585,7 +664,7 @@ impl WindowManager {
             .reply()?;
 
         let pointer = self.connection.query_pointer(self.root)?.reply()?;
-        let (original_x_value, original_y_value) = (pointer.root_x, pointer.root_y);
+        let (start_x, start_y) = (pointer.root_x, pointer.root_y);
 
         self.connection.configure_window(
             window,
@@ -596,13 +675,11 @@ impl WindowManager {
             let event = self.connection.wait_for_event()?;
             match event {
                 Event::MotionNotify(e) => {
-                    let new_x_value = geometry.x + (e.root_x - original_x_value);
-                    let new_y_value = geometry.y + (e.root_y - original_y_value);
+                    let new_x = geometry.x + (e.root_x - start_x);
+                    let new_y = geometry.y + (e.root_y - start_y);
                     self.connection.configure_window(
                         window,
-                        &ConfigureWindowAux::new()
-                            .x(new_x_value as i32)
-                            .y(new_y_value as i32),
+                        &ConfigureWindowAux::new().x(new_x as i32).y(new_y as i32),
                     )?;
                     self.connection.flush()?;
                 }
@@ -735,11 +812,11 @@ impl WindowManager {
                 }
             }
             Event::KeyPress(event) => {
-                let (action, arg) = keyboard::handle_key_press(event)?;
+                let (action, arg) = keyboard::handle_key_press(event, &self.config.keybindings)?;
                 match action {
                     KeyAction::Quit => return Ok(Some(false)),
-                    KeyAction::Restart => return Ok(Some(true)),
-                    _ => self.handle_key_action(action, arg)?,
+                    KeyAction::Restart => return Ok(Some(self.handle_restart()?)),
+                    _ => self.handle_key_action(action, &arg)?,
                 }
             }
             Event::ButtonPress(event) => {
@@ -774,17 +851,17 @@ impl WindowManager {
         }
         let screen_width = self.screen.width_in_pixels as u32;
         let screen_height = self.screen.height_in_pixels as u32;
-        let border_width = BORDER_WIDTH;
+        let border_width = self.config.border_width;
 
         let bar_height = self.bar.height() as u32;
         let usable_height = screen_height.saturating_sub(bar_height);
 
         let gaps = if self.gaps_enabled {
             GapConfig {
-                inner_horizontal: GAP_INNER_HORIZONTAL,
-                inner_vertical: GAP_INNER_VERTICAL,
-                outer_horizontal: GAP_OUTER_HORIZONTAL,
-                outer_vertical: GAP_OUTER_VERTICAL,
+                inner_horizontal: self.config.gap_inner_horizontal,
+                inner_vertical: self.config.gap_inner_vertical,
+                outer_horizontal: self.config.gap_outer_horizontal,
+                outer_vertical: self.config.gap_outer_vertical,
             }
         } else {
             GapConfig {
@@ -851,3 +928,35 @@ impl WindowManager {
         Ok(())
     }
 }
+
+fn get_config_path() -> PathBuf {
+    dirs::config_dir()
+        .expect("Could not find config directory")
+        .join("oxwm")
+}
+
+fn get_cache_binary_path() -> PathBuf {
+    dirs::cache_dir()
+        .expect("Could not find cache directory")
+        .join("oxwm/oxwm-binary")
+}
+
+fn can_recompile() -> bool {
+    std::process::Command::new("cargo")
+        .arg("--version")
+        .output()
+        .map(|o| o.status.success())
+        .unwrap_or(false)
+}
+
+fn notify(title: &str, body: &str) {
+    let _ = std::process::Command::new("notify-send")
+        .args(&[title, body])
+        .spawn();
+}
+
+fn notify_error(title: &str, body: &str) {
+    let _ = std::process::Command::new("notify-send")
+        .args(&["-u", "critical", title, body])
+        .spawn();
+}
diff --git a/templates/config.rs b/templates/config.rs
new file mode 100644
index 0000000..cc10649
--- /dev/null
+++ b/templates/config.rs
@@ -0,0 +1,212 @@
+use oxwm::ColorScheme;
+use oxwm::prelude::*;
+
+// ========================================
+// APPEARANCE
+// ========================================
+pub const BORDER_WIDTH: u32 = 2;
+pub const BORDER_FOCUSED: u32 = 0x6dade3;
+pub const BORDER_UNFOCUSED: u32 = 0xbbbbbb;
+pub const FONT: &str = "JetBrainsMono Nerd Font:style=Bold:size=12";
+
+// ========================================
+// GAPS (Vanity Gaps)
+// ========================================
+pub const GAPS_ENABLED: bool = false;
+pub const GAP_INNER_HORIZONTAL: u32 = 3;
+pub const GAP_INNER_VERTICAL: u32 = 3;
+pub const GAP_OUTER_HORIZONTAL: u32 = 3;
+pub const GAP_OUTER_VERTICAL: u32 = 3;
+
+// ========================================
+// DEFAULTS
+// ========================================
+pub const TERMINAL: &str = "st";
+pub const MODKEY: KeyButMask = KeyButMask::MOD4;
+
+// ========================================
+// TAGS (Workspaces)
+// ========================================
+pub const TAG_COUNT: usize = 9;
+pub const TAGS: [&str; TAG_COUNT] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
+
+// Alternative tag styles you can use:
+// pub const TAGS: [&str; TAG_COUNT] = ["", "󰊯", "", "", "󰙯", "󱇤", "", "󱘶", "󰧮"];
+// pub const TAGS: [&str; TAG_COUNT] = ["DEV", "WWW", "SYS", "DOC", "VBOX", "CHAT", "MUS", "VID", "MISC"];
+
+// ========================================
+// BAR COLORS
+// ========================================
+const GRAY_DARK: u32 = 0x1a1b26;
+const GRAY_SEP: u32 = 0xa9b1d6;
+const GRAY_MID: u32 = 0x444444;
+const GRAY_LIGHT: u32 = 0xbbbbbb;
+const CYAN: u32 = 0x0db9d7;
+const MAGENTA: u32 = 0xad8ee6;
+const RED: u32 = 0xf7768e;
+const GREEN: u32 = 0x9ece6a;
+const BLUE: u32 = 0x7aa2f7;
+const YELLOW: u32 = 0xe0af68;
+
+pub const SCHEME_NORMAL: ColorScheme = ColorScheme {
+    foreground: GRAY_LIGHT,
+    background: GRAY_DARK,
+    underline: GRAY_MID,
+};
+
+pub const SCHEME_OCCUPIED: ColorScheme = ColorScheme {
+    foreground: CYAN,
+    background: GRAY_DARK,
+    underline: CYAN,
+};
+
+pub const SCHEME_SELECTED: ColorScheme = ColorScheme {
+    foreground: CYAN,
+    background: GRAY_DARK,
+    underline: MAGENTA,
+};
+
+// ========================================
+// COMMANDS
+// ========================================
+const SCREENSHOT_CMD: &[&str] = &[
+    "sh",
+    "-c",
+    "maim -s | xclip -selection clipboard -t image/png",
+];
+
+const DMENU_CMD: &[&str] = &["sh", "-c", "dmenu_run -l 10"];
+
+// ========================================
+// KEYBINDINGS
+// ========================================
+const SHIFT: KeyButMask = KeyButMask::SHIFT;
+
+#[rustfmt::skip]
+pub const KEYBINDINGS: &[Key] = &[
+    // Launch applications
+    Key::new(&[MODKEY],        keycodes::RETURN, KeyAction::Spawn,      Arg::Str(TERMINAL)),
+    Key::new(&[MODKEY],        keycodes::F,      KeyAction::Spawn,      Arg::Str("xclock")),
+    Key::new(&[MODKEY],        keycodes::S,      KeyAction::Spawn,      Arg::Array(SCREENSHOT_CMD)),
+    Key::new(&[MODKEY],        keycodes::D,      KeyAction::Spawn,      Arg::Array(DMENU_CMD)),
+    
+    // Window management
+    Key::new(&[MODKEY],        keycodes::Q,      KeyAction::KillClient, Arg::None),
+    Key::new(&[MODKEY, SHIFT], keycodes::F,      KeyAction::ToggleFullScreen, Arg::None),
+    Key::new(&[MODKEY],        keycodes::A,      KeyAction::ToggleGaps, Arg::None),
+    
+    // WM controls
+    Key::new(&[MODKEY, SHIFT], keycodes::Q,      KeyAction::Quit,       Arg::None),
+    Key::new(&[MODKEY, SHIFT], keycodes::R,      KeyAction::Restart,    Arg::None),
+    Key::new(&[MODKEY, SHIFT], keycodes::C,      KeyAction::Recompile,  Arg::None),
+    
+    // Focus
+    Key::new(&[MODKEY],        keycodes::J,      KeyAction::FocusStack, Arg::Int(-1)),
+    Key::new(&[MODKEY],        keycodes::K,      KeyAction::FocusStack, Arg::Int(1)),
+    
+    // View tags (workspaces)
+    Key::new(&[MODKEY], keycodes::KEY_1, KeyAction::ViewTag, Arg::Int(0)),
+    Key::new(&[MODKEY], keycodes::KEY_2, KeyAction::ViewTag, Arg::Int(1)),
+    Key::new(&[MODKEY], keycodes::KEY_3, KeyAction::ViewTag, Arg::Int(2)),
+    Key::new(&[MODKEY], keycodes::KEY_4, KeyAction::ViewTag, Arg::Int(3)),
+    Key::new(&[MODKEY], keycodes::KEY_5, KeyAction::ViewTag, Arg::Int(4)),
+    Key::new(&[MODKEY], keycodes::KEY_6, KeyAction::ViewTag, Arg::Int(5)),
+    Key::new(&[MODKEY], keycodes::KEY_7, KeyAction::ViewTag, Arg::Int(6)),
+    Key::new(&[MODKEY], keycodes::KEY_8, KeyAction::ViewTag, Arg::Int(7)),
+    Key::new(&[MODKEY], keycodes::KEY_9, KeyAction::ViewTag, Arg::Int(8)),
+    
+    // Move windows to tags
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_1, KeyAction::MoveToTag, Arg::Int(0)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_2, KeyAction::MoveToTag, Arg::Int(1)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_3, KeyAction::MoveToTag, Arg::Int(2)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_4, KeyAction::MoveToTag, Arg::Int(3)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_5, KeyAction::MoveToTag, Arg::Int(4)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_6, KeyAction::MoveToTag, Arg::Int(5)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_7, KeyAction::MoveToTag, Arg::Int(6)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_8, KeyAction::MoveToTag, Arg::Int(7)),
+    Key::new(&[MODKEY, SHIFT], keycodes::KEY_9, KeyAction::MoveToTag, Arg::Int(8)),
+];
+
+// ========================================
+// STATUS BAR BLOCKS
+// ========================================
+pub const STATUS_BLOCKS: &[BlockConfig] = &[
+    BlockConfig {
+        format: "",
+        command: BlockCommand::Battery {
+            format_charging: "󰂄 Bat: {}%",
+            format_discharging: "󰁹 Bat:{}%",
+            format_full: "󰁹 Bat: {}%",
+        },
+        interval_secs: 30,
+        color: GREEN,
+        underline: true,
+    },
+    BlockConfig {
+        format: " │  ",
+        command: BlockCommand::Static(""),
+        interval_secs: u64::MAX,
+        color: GRAY_SEP,
+        underline: false,
+    },
+    BlockConfig {
+        format: "󰍛 {used}/{total} GB",
+        command: BlockCommand::Ram,
+        interval_secs: 5,
+        color: BLUE,
+        underline: true,
+    },
+    BlockConfig {
+        format: " │  ",
+        command: BlockCommand::Static(""),
+        interval_secs: u64::MAX,
+        color: GRAY_SEP,
+        underline: false,
+    },
+    BlockConfig {
+        format: " {}",
+        command: BlockCommand::Shell("uname -r"),
+        interval_secs: u64::MAX,
+        color: RED,
+        underline: true,
+    },
+    BlockConfig {
+        format: " │  ",
+        command: BlockCommand::Static(""),
+        interval_secs: u64::MAX,
+        color: GRAY_SEP,
+        underline: false,
+    },
+    BlockConfig {
+        format: "󰸘 {}",
+        command: BlockCommand::DateTime("%a, %b %d - %-I:%M %P"),
+        interval_secs: 1,
+        color: CYAN,
+        underline: true,
+    },
+];
+
+// ========================================
+// BUILD CONFIG FROM CONSTANTS
+// ========================================
+pub fn build_config() -> oxwm::Config {
+    oxwm::Config {
+        border_width: BORDER_WIDTH,
+        border_focused: BORDER_FOCUSED,
+        border_unfocused: BORDER_UNFOCUSED,
+        font: FONT.to_string(),
+        gaps_enabled: GAPS_ENABLED,
+        gap_inner_horizontal: GAP_INNER_HORIZONTAL,
+        gap_inner_vertical: GAP_INNER_VERTICAL,
+        gap_outer_horizontal: GAP_OUTER_HORIZONTAL,
+        gap_outer_vertical: GAP_OUTER_VERTICAL,
+        terminal: TERMINAL.to_string(),
+        modkey: MODKEY,
+        tags: TAGS.iter().map(|s| s.to_string()).collect(),
+        keybindings: KEYBINDINGS.to_vec(),
+        status_blocks: STATUS_BLOCKS.to_vec(),
+        scheme_normal: SCHEME_NORMAL,
+        scheme_occupied: SCHEME_OCCUPIED,
+        scheme_selected: SCHEME_SELECTED,
+    }
+}
diff --git a/templates/main.rs b/templates/main.rs
new file mode 100644
index 0000000..97c27c0
--- /dev/null
+++ b/templates/main.rs
@@ -0,0 +1,21 @@
+mod config;
+
+use anyhow::Result;
+
+fn main() -> Result<()> {
+    let cfg = config::build_config();
+
+    let args: Vec<String> = std::env::args().collect();
+    let mut wm = oxwm::window_manager::WindowManager::new(cfg)?;
+    let should_restart = wm.run()?;
+
+    drop(wm);
+
+    if should_restart {
+        use std::os::unix::process::CommandExt;
+        let err = std::process::Command::new(&args[0]).args(&args[1..]).exec();
+        eprintln!("Failed to restart: {}", err);
+    }
+
+    Ok(())
+}