oxwm

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

Added proper error handling in blocks, and font. added error modal so config syntax error doesnt crash when you reload your oxwm, added keybind overlay modal to show the important keybinds (inspired by niri), moved keysms helper function into const file to reduce code duplication.

Commit
ad06fa55c7038da5141887129961bcc84c97dfb5
Parent
daf7a1c
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-11-09 07:24:42

Diff

diff --git a/resources/test-config.lua b/resources/test-config.lua
index d0c5aac..a4bd390 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -61,6 +61,7 @@ return {
         { 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" },
diff --git a/src/bar/blocks/battery.rs b/src/bar/blocks/battery.rs
index 736fc47..0ee67bb 100644
--- a/src/bar/blocks/battery.rs
+++ b/src/bar/blocks/battery.rs
@@ -1,5 +1,5 @@
 use super::Block;
-use anyhow::Result;
+use crate::errors::BlockError;
 use std::fs;
 use std::time::Duration;
 
@@ -30,22 +30,22 @@ impl Battery {
         }
     }
 
-    fn read_file(&self, filename: &str) -> Result<String> {
+    fn read_file(&self, filename: &str) -> Result<String, BlockError> {
         let path = format!("{}/{}", self.battery_path, filename);
         Ok(fs::read_to_string(path)?.trim().to_string())
     }
 
-    fn get_capacity(&self) -> Result<u32> {
+    fn get_capacity(&self) -> Result<u32, BlockError> {
         Ok(self.read_file("capacity")?.parse()?)
     }
 
-    fn get_status(&self) -> Result<String> {
+    fn get_status(&self) -> Result<String, BlockError> {
         self.read_file("status")
     }
 }
 
 impl Block for Battery {
-    fn content(&mut self) -> Result<String> {
+    fn content(&mut self) -> Result<String, BlockError> {
         let capacity = self.get_capacity()?;
         let status = self.get_status()?;
 
diff --git a/src/bar/blocks/datetime.rs b/src/bar/blocks/datetime.rs
index 6ce6b3d..9285176 100644
--- a/src/bar/blocks/datetime.rs
+++ b/src/bar/blocks/datetime.rs
@@ -1,5 +1,5 @@
 use super::Block;
-use anyhow::Result;
+use crate::errors::BlockError;
 use chrono::Local;
 use std::time::Duration;
 
@@ -22,7 +22,7 @@ impl DateTime {
 }
 
 impl Block for DateTime {
-    fn content(&mut self) -> Result<String> {
+    fn content(&mut self) -> Result<String, BlockError> {
         let now = Local::now();
         let time_str = now.format(&self.time_format).to_string();
         Ok(self.format_template.replace("{}", &time_str))
diff --git a/src/bar/blocks/mod.rs b/src/bar/blocks/mod.rs
index 0c97c22..b6d9a9c 100644
--- a/src/bar/blocks/mod.rs
+++ b/src/bar/blocks/mod.rs
@@ -1,4 +1,4 @@
-use anyhow::Result;
+use crate::errors::BlockError;
 use std::time::Duration;
 
 mod battery;
@@ -12,7 +12,7 @@ use ram::Ram;
 use shell::ShellBlock;
 
 pub trait Block {
-    fn content(&mut self) -> Result<String>;
+    fn content(&mut self) -> Result<String, BlockError>;
     fn interval(&self) -> Duration;
     fn color(&self) -> u32;
 }
@@ -89,7 +89,7 @@ impl StaticBlock {
 }
 
 impl Block for StaticBlock {
-    fn content(&mut self) -> Result<String> {
+    fn content(&mut self) -> Result<String, BlockError> {
         Ok(self.text.clone())
     }
 
diff --git a/src/bar/blocks/ram.rs b/src/bar/blocks/ram.rs
index eeb8f66..c26571a 100644
--- a/src/bar/blocks/ram.rs
+++ b/src/bar/blocks/ram.rs
@@ -1,5 +1,5 @@
 use super::Block;
-use anyhow::Result;
+use crate::errors::BlockError;
 use std::fs;
 use std::time::Duration;
 
@@ -18,7 +18,7 @@ impl Ram {
         }
     }
 
-    fn get_memory_info(&self) -> Result<(u64, u64, f32)> {
+    fn get_memory_info(&self) -> Result<(u64, u64, f32), BlockError> {
         let meminfo = fs::read_to_string("/proc/meminfo")?;
         let mut total: u64 = 0;
         let mut available: u64 = 0;
@@ -51,7 +51,7 @@ impl Ram {
 }
 
 impl Block for Ram {
-    fn content(&mut self) -> Result<String> {
+    fn content(&mut self) -> Result<String, BlockError> {
         let (used, total, percentage) = self.get_memory_info()?;
 
         let used_gb = used as f32 / 1024.0 / 1024.0;
diff --git a/src/bar/blocks/shell.rs b/src/bar/blocks/shell.rs
index 51a20df..bdf6b6f 100644
--- a/src/bar/blocks/shell.rs
+++ b/src/bar/blocks/shell.rs
@@ -1,5 +1,5 @@
 use super::Block;
-use anyhow::Result;
+use crate::errors::BlockError;
 use std::process::Command;
 use std::time::Duration;
 
@@ -22,8 +22,19 @@ impl ShellBlock {
 }
 
 impl Block for ShellBlock {
-    fn content(&mut self) -> Result<String> {
-        let output = Command::new("sh").arg("-c").arg(&self.command).output()?;
+    fn content(&mut self) -> Result<String, BlockError> {
+        let output = Command::new("sh")
+            .arg("-c")
+            .arg(&self.command)
+            .output()
+            .map_err(|e| BlockError::CommandFailed(format!("Failed to execute command: {}", e)))?;
+
+        if !output.status.success() {
+            return Err(BlockError::CommandFailed(format!(
+                "Command exited with status: {}",
+                output.status
+            )));
+        }
 
         let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
         Ok(self.format.replace("{}", &result))
diff --git a/src/bar/font.rs b/src/bar/font.rs
index 8cfe7f1..0295b4d 100644
--- a/src/bar/font.rs
+++ b/src/bar/font.rs
@@ -124,6 +124,13 @@ impl FontDraw {
             );
         }
     }
+
+    pub fn flush(&self) {
+        unsafe {
+            let display = x11::xft::XftDrawDisplay(self.xft_draw);
+            x11::xlib::XFlush(display);
+        }
+    }
 }
 
 impl Drop for FontDraw {
diff --git a/src/bin/main.rs b/src/bin/main.rs
index f01c378..9b1d7cd 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -55,7 +55,6 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<oxwm::Config> {
     let config_path = if let Some(path) = custom_path {
         path
     } else {
-        // Try to find config.lua first, then config.ron
         let config_dir = get_config_path();
         let lua_path = config_dir.join("config.lua");
         let ron_path = config_dir.join("config.ron");
@@ -75,7 +74,6 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<oxwm::Config> {
     let config_str =
         std::fs::read_to_string(&config_path).with_context(|| "Failed to read config file")?;
 
-    // Determine config format based on file extension
     let is_lua = config_path
         .extension()
         .and_then(|s| s.to_str())
@@ -83,7 +81,9 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<oxwm::Config> {
         .unwrap_or(false);
 
     if is_lua {
-        oxwm::config::parse_lua_config(&config_str).with_context(|| "Failed to parse Lua config")
+        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")
     }
@@ -115,7 +115,6 @@ fn migrate_config(custom_path: Option<PathBuf>) -> Result<()> {
     let ron_path = if let Some(path) = custom_path {
         path
     } else {
-        // Default to ~/.config/oxwm/config.ron
         get_config_path().join("config.ron")
     };
 
@@ -132,9 +131,8 @@ fn migrate_config(custom_path: Option<PathBuf>) -> Result<()> {
         .with_context(|| format!("Failed to read RON config from {:?}", ron_path))?;
 
     let lua_content = oxwm::config::migrate::ron_to_lua(&ron_content)
-        .map_err(|e| anyhow::anyhow!("Migration failed: {}", e))?;
+        .with_context(|| "Failed to migrate RON config to Lua")?;
 
-    // Determine output path (same directory, .lua extension)
     let lua_path = ron_path.with_extension("lua");
 
     std::fs::write(&lua_path, lua_content)
@@ -143,7 +141,9 @@ fn migrate_config(custom_path: Option<PathBuf>) -> Result<()> {
     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.");
+    println!(
+        "Review the new config.lua and then you can delete config.ron if everything looks good."
+    );
 
     Ok(())
 }
@@ -154,7 +154,9 @@ fn print_help() {
     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!(
+        "    --migrate [PATH]    Convert RON config to Lua (default: ~/.config/oxwm/config.ron)"
+    );
     println!("    --config <PATH>     Use custom config file (.lua or .ron)");
     println!("    --version           Print version information");
     println!("    --help              Print this help message\n");
diff --git a/src/config/lua.rs b/src/config/lua.rs
index c1c9495..6a42c0e 100644
--- a/src/config/lua.rs
+++ b/src/config/lua.rs
@@ -7,9 +7,21 @@ use crate::{ColorScheme, LayoutSymbolOverride};
 use mlua::{Lua, Table, Value};
 use x11rb::protocol::xproto::KeyButMask;
 
-pub fn parse_lua_config(input: &str) -> Result<crate::Config, ConfigError> {
+pub fn parse_lua_config(
+    input: &str,
+    config_dir: Option<&std::path::Path>,
+) -> Result<crate::Config, ConfigError> {
     let lua = Lua::new();
 
+    if let Some(dir) = config_dir {
+        if let Some(dir_str) = dir.to_str() {
+            let setup_code = format!("package.path = '{}/?.lua;' .. package.path", dir_str);
+            lua.load(&setup_code)
+                .exec()
+                .map_err(|e| ConfigError::LuaError(format!("Failed to set package.path: {}", e)))?;
+        }
+    }
+
     let config: Table = lua
         .load(input)
         .eval()
@@ -364,6 +376,7 @@ fn string_to_key_action(s: &str) -> Result<KeyAction, ConfigError> {
         "FocusMonitor" => KeyAction::FocusMonitor,
         "SmartMoveWin" => KeyAction::SmartMoveWin,
         "ExchangeClient" => KeyAction::ExchangeClient,
+        "ShowKeybindOverlay" => KeyAction::ShowKeybindOverlay,
         "None" => KeyAction::None,
         _ => return Err(ConfigError::UnknownAction(s.to_string())),
     };
@@ -560,7 +573,7 @@ return {
 }
 "#;
 
-        let config = parse_lua_config(config_str).expect("Failed to parse config");
+        let config = parse_lua_config(config_str, None).expect("Failed to parse config");
 
         assert_eq!(config.border_width, 2);
         assert_eq!(config.border_focused, 0x6dade3);
diff --git a/src/config/migrate.rs b/src/config/migrate.rs
index 441f9b7..2a48eaa 100644
--- a/src/config/migrate.rs
+++ b/src/config/migrate.rs
@@ -1,6 +1,7 @@
+use crate::errors::ConfigError;
 use std::collections::HashMap;
 
-pub fn ron_to_lua(ron_content: &str) -> Result<String, String> {
+pub fn ron_to_lua(ron_content: &str) -> Result<String, ConfigError> {
     let mut lua_output = String::new();
     let defines = extract_defines(ron_content);
 
diff --git a/src/errors.rs b/src/errors.rs
index 0ee19d4..ae9a47a 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -4,8 +4,8 @@ use std::io;
 pub enum WmError {
     X11(X11Error),
     Io(io::Error),
-    Anyhow(anyhow::Error),
     Config(ConfigError),
+    Block(BlockError),
     Autostart(String, io::Error),
 }
 
@@ -33,6 +33,16 @@ pub enum ConfigError {
     InvalidVariableName(String),
     InvalidDefine(String),
     UndefinedVariable(String),
+    MigrationError(String),
+}
+
+#[derive(Debug)]
+pub enum BlockError {
+    Io(io::Error),
+    ParseInt(std::num::ParseIntError),
+    MissingFile(String),
+    InvalidData(String),
+    CommandFailed(String),
 }
 
 impl std::fmt::Display for WmError {
@@ -40,8 +50,8 @@ impl std::fmt::Display for WmError {
         match self {
             Self::X11(error) => write!(f, "{}", error),
             Self::Io(error) => write!(f, "{}", error),
-            Self::Anyhow(error) => write!(f, "{}", error),
             Self::Config(error) => write!(f, "{}", error),
+            Self::Block(error) => write!(f, "{}", error),
             Self::Autostart(command, error) => write!(f, "Failed to spawn autostart command '{}': {}", command, error),
         }
     }
@@ -95,12 +105,27 @@ impl std::fmt::Display for ConfigError {
                     var
                 )
             }
+            Self::MigrationError(msg) => write!(f, "Migration error: {}", msg),
         }
     }
 }
 
 impl std::error::Error for ConfigError {}
 
+impl std::fmt::Display for BlockError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Io(err) => write!(f, "Block I/O error: {}", err),
+            Self::ParseInt(err) => write!(f, "Block parse error: {}", err),
+            Self::MissingFile(path) => write!(f, "Block missing file: {}", path),
+            Self::InvalidData(msg) => write!(f, "Block invalid data: {}", msg),
+            Self::CommandFailed(msg) => write!(f, "Block command failed: {}", msg),
+        }
+    }
+}
+
+impl std::error::Error for BlockError {}
+
 impl<T: Into<X11Error>> From<T> for WmError {
     fn from(value: T) -> Self {
         Self::X11(value.into())
@@ -113,18 +138,30 @@ impl From<io::Error> for WmError {
     }
 }
 
-impl From<anyhow::Error> for WmError {
-    fn from(value: anyhow::Error) -> Self {
-        Self::Anyhow(value)
-    }
-}
-
 impl From<ConfigError> for WmError {
     fn from(value: ConfigError) -> Self {
         Self::Config(value)
     }
 }
 
+impl From<BlockError> for WmError {
+    fn from(value: BlockError) -> Self {
+        Self::Block(value)
+    }
+}
+
+impl From<io::Error> for BlockError {
+    fn from(value: io::Error) -> Self {
+        BlockError::Io(value)
+    }
+}
+
+impl From<std::num::ParseIntError> for BlockError {
+    fn from(value: std::num::ParseIntError) -> Self {
+        BlockError::ParseInt(value)
+    }
+}
+
 impl From<ron::error::SpannedError> for ConfigError {
     fn from(value: ron::error::SpannedError) -> Self {
         ConfigError::ParseError(value)
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 2d6e0d3..adebdf5 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -9,7 +9,7 @@ use x11rb::protocol::xproto::*;
 use crate::errors::X11Error;
 use crate::keyboard::keysyms::{self, Keysym};
 
-#[derive(Debug, Copy, Clone, Deserialize)]
+#[derive(Debug, Copy, Clone, Deserialize, PartialEq)]
 pub enum KeyAction {
     Spawn,
     KillClient,
@@ -29,6 +29,7 @@ pub enum KeyAction {
     FocusMonitor,
     SmartMoveWin,
     ExchangeClient,
+    ShowKeybindOverlay,
     None,
 }
 
diff --git a/src/keyboard/keysyms.rs b/src/keyboard/keysyms.rs
index 42e1330..a3a1bf6 100644
--- a/src/keyboard/keysyms.rs
+++ b/src/keyboard/keysyms.rs
@@ -82,3 +82,61 @@ pub const XF86_AUDIO_LOWER_VOLUME: Keysym = 0x1008ff11;
 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 format_keysym(keysym: Keysym) -> String {
+    match keysym {
+        XK_RETURN => "Return".to_string(),
+        XK_ESCAPE => "Esc".to_string(),
+        XK_SPACE => "Space".to_string(),
+        XK_TAB => "Tab".to_string(),
+        XK_BACKSPACE => "Backspace".to_string(),
+        XK_DELETE => "Del".to_string(),
+        XK_LEFT => "Left".to_string(),
+        XK_RIGHT => "Right".to_string(),
+        XK_UP => "Up".to_string(),
+        XK_DOWN => "Down".to_string(),
+        XK_HOME => "Home".to_string(),
+        XK_END => "End".to_string(),
+        XK_PAGE_UP => "PgUp".to_string(),
+        XK_PAGE_DOWN => "PgDn".to_string(),
+        XK_INSERT => "Ins".to_string(),
+        XK_F1 => "F1".to_string(),
+        XK_F2 => "F2".to_string(),
+        XK_F3 => "F3".to_string(),
+        XK_F4 => "F4".to_string(),
+        XK_F5 => "F5".to_string(),
+        XK_F6 => "F6".to_string(),
+        XK_F7 => "F7".to_string(),
+        XK_F8 => "F8".to_string(),
+        XK_F9 => "F9".to_string(),
+        XK_F10 => "F10".to_string(),
+        XK_F11 => "F11".to_string(),
+        XK_F12 => "F12".to_string(),
+        XK_SLASH => "/".to_string(),
+        XK_COMMA => ",".to_string(),
+        XK_PERIOD => ".".to_string(),
+        XK_MINUS => "-".to_string(),
+        XK_EQUAL => "=".to_string(),
+        XK_GRAVE => "`".to_string(),
+        XK_LEFT_BRACKET => "[".to_string(),
+        XK_RIGHT_BRACKET => "]".to_string(),
+        XK_SEMICOLON => ";".to_string(),
+        XK_APOSTROPHE => "'".to_string(),
+        XK_BACKSLASH => "\\".to_string(),
+        XK_PRINT => "Print".to_string(),
+        XF86_AUDIO_RAISE_VOLUME => "Vol+".to_string(),
+        XF86_AUDIO_LOWER_VOLUME => "Vol-".to_string(),
+        XF86_AUDIO_MUTE => "Mute".to_string(),
+        XF86_MON_BRIGHTNESS_UP => "Bright+".to_string(),
+        XF86_MON_BRIGHTNESS_DOWN => "Bright-".to_string(),
+        XK_A..=XK_Z => {
+            let ch = (keysym - XK_A + b'A' as u32) as u8 as char;
+            ch.to_string()
+        }
+        XK_0..=XK_9 => {
+            let ch = (keysym - XK_0 + b'0' as u32) as u8 as char;
+            ch.to_string()
+        }
+        _ => format!("0x{:x}", keysym),
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 6b5ef02..ad38775 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,6 +4,7 @@ pub mod errors;
 pub mod keyboard;
 pub mod layout;
 pub mod monitor;
+pub mod overlay;
 pub mod window_manager;
 
 pub mod prelude {
diff --git a/src/overlay/error.rs b/src/overlay/error.rs
new file mode 100644
index 0000000..42ef9cc
--- /dev/null
+++ b/src/overlay/error.rs
@@ -0,0 +1,157 @@
+use super::{Overlay, OverlayBase};
+use crate::bar::font::Font;
+use crate::errors::X11Error;
+use std::time::Instant;
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+use x11rb::rust_connection::RustConnection;
+
+const PADDING: i16 = 20;
+const LINE_SPACING: i16 = 5;
+const BORDER_WIDTH: u16 = 2;
+const BORDER_COLOR: u32 = 0xff5555;
+const AUTO_DISMISS_SECONDS: u64 = 10;
+
+pub struct ErrorOverlay {
+    base: OverlayBase,
+    created_at: Option<Instant>,
+    lines: Vec<String>,
+}
+
+impl ErrorOverlay {
+    pub fn new(
+        connection: &RustConnection,
+        screen: &Screen,
+        screen_num: usize,
+        display: *mut x11::xlib::Display,
+        _font: &Font,
+        _max_width: u16,
+    ) -> Result<Self, X11Error> {
+        let base = OverlayBase::new(
+            connection,
+            screen,
+            screen_num,
+            display,
+            400,
+            200,
+            BORDER_WIDTH,
+            BORDER_COLOR,
+            0x1a1a1a,
+            0xffffff,
+        )?;
+
+        Ok(ErrorOverlay {
+            base,
+            created_at: None,
+            lines: Vec::new(),
+        })
+    }
+
+    pub fn show_error(
+        &mut self,
+        connection: &RustConnection,
+        font: &Font,
+        error_text: &str,
+        screen_width: u16,
+        screen_height: u16,
+    ) -> Result<(), X11Error> {
+        let max_line_width = (screen_width as i16 - PADDING * 4).max(300) as u16;
+        self.lines = self.wrap_text(error_text, font, max_line_width);
+
+        let mut content_width = 0u16;
+        for line in &self.lines {
+            let line_width = font.text_width(line);
+            if line_width > content_width {
+                content_width = line_width;
+            }
+        }
+
+        let width = content_width + (PADDING as u16 * 2);
+        let line_height = font.height() + LINE_SPACING as u16;
+        let height = (self.lines.len() as u16 * line_height) + (PADDING as u16 * 2);
+
+        let x = ((screen_width - width) / 2) as i16;
+        let y = ((screen_height - height) / 2) as i16;
+
+        self.base.configure(connection, x, y, width, height)?;
+        self.base.show(connection)?;
+        self.created_at = Some(Instant::now());
+        self.draw(connection, font)?;
+        Ok(())
+    }
+
+    pub fn should_auto_dismiss(&self) -> bool {
+        if let Some(created_at) = self.created_at {
+            created_at.elapsed().as_secs() >= AUTO_DISMISS_SECONDS
+        } else {
+            false
+        }
+    }
+
+    fn wrap_text(&self, text: &str, font: &Font, max_width: u16) -> Vec<String> {
+        let mut lines = Vec::new();
+        for paragraph in text.lines() {
+            if paragraph.trim().is_empty() {
+                lines.push(String::new());
+                continue;
+            }
+
+            let words: Vec<&str> = paragraph.split_whitespace().collect();
+            let mut current_line = String::new();
+
+            for word in words {
+                let test_line = if current_line.is_empty() {
+                    word.to_string()
+                } else {
+                    format!("{} {}", current_line, word)
+                };
+                if font.text_width(&test_line) <= max_width {
+                    current_line = test_line;
+                } else {
+                    if !current_line.is_empty() {
+                        lines.push(current_line);
+                    }
+                    current_line = word.to_string();
+                }
+            }
+            if !current_line.is_empty() {
+                lines.push(current_line);
+            }
+        }
+        lines
+    }
+}
+
+impl Overlay for ErrorOverlay {
+    fn window(&self) -> Window {
+        self.base.window
+    }
+
+    fn is_visible(&self) -> bool {
+        self.base.is_visible
+    }
+
+    fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
+        self.base.hide(connection)?;
+        self.created_at = None;
+        self.lines.clear();
+        Ok(())
+    }
+
+    fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error> {
+        if !self.base.is_visible {
+            return Ok(());
+        }
+        self.base.draw_background(connection)?;
+        let line_height = font.height() + LINE_SPACING as u16;
+        let mut y = PADDING + font.ascent();
+        for line in &self.lines {
+            self.base
+                .font_draw
+                .draw_text(font, self.base.foreground_color, PADDING, y, line);
+            y += line_height as i16;
+        }
+        connection.flush()?;
+        Ok(())
+    }
+}
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
new file mode 100644
index 0000000..f1cd47c
--- /dev/null
+++ b/src/overlay/keybind.rs
@@ -0,0 +1,296 @@
+use super::{Overlay, OverlayBase};
+use crate::bar::font::Font;
+use crate::errors::X11Error;
+use crate::keyboard::KeyAction;
+use crate::keyboard::handlers::{KeyBinding, KeyPress};
+use std::time::Instant;
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+use x11rb::rust_connection::RustConnection;
+
+const PADDING: i16 = 24;
+const KEY_ACTION_SPACING: i16 = 20;
+const LINE_SPACING: i16 = 8;
+const BORDER_WIDTH: u16 = 4;
+const BORDER_COLOR: u32 = 0x7fccff;
+const TITLE_BOTTOM_MARGIN: i16 = 20;
+const INPUT_SUPPRESS_MS: u128 = 200;
+
+pub struct KeybindOverlay {
+    base: OverlayBase,
+    keybindings: Vec<(String, String)>,
+    key_bg_color: u32,
+    modkey: KeyButMask,
+    last_shown_at: Option<Instant>,
+    max_key_width: u16,
+}
+
+impl KeybindOverlay {
+    pub fn new(
+        connection: &RustConnection,
+        screen: &Screen,
+        screen_num: usize,
+        display: *mut x11::xlib::Display,
+        modkey: KeyButMask,
+    ) -> Result<Self, X11Error> {
+        let base = OverlayBase::new(
+            connection,
+            screen,
+            screen_num,
+            display,
+            800,
+            600,
+            BORDER_WIDTH,
+            BORDER_COLOR,
+            0x1a1a1a,
+            0xffffff,
+        )?;
+
+        Ok(KeybindOverlay {
+            base,
+            keybindings: Vec::new(),
+            key_bg_color: 0x2a2a2a,
+            modkey,
+            last_shown_at: None,
+            max_key_width: 0,
+        })
+    }
+
+    pub fn show(
+        &mut self,
+        connection: &RustConnection,
+        font: &Font,
+        keybindings: &[KeyBinding],
+        screen_width: u16,
+        screen_height: u16,
+    ) -> Result<(), X11Error> {
+        self.keybindings = self.collect_keybindings(keybindings);
+
+        let title = "Important Keybindings";
+        let title_width = font.text_width(title);
+
+        let mut max_key_width = 0u16;
+        let mut max_action_width = 0u16;
+
+        for (key, action) in &self.keybindings {
+            let key_width = font.text_width(key);
+            let action_width = font.text_width(action);
+            if key_width > max_key_width {
+                max_key_width = key_width;
+            }
+            if action_width > max_action_width {
+                max_action_width = action_width;
+            }
+        }
+
+        let content_width = max_key_width + KEY_ACTION_SPACING as u16 + max_action_width;
+        let min_width = title_width.max(content_width);
+
+        let width = min_width + (PADDING as u16 * 2);
+
+        let line_height = font.height() + LINE_SPACING as u16;
+        let title_height = font.height() + TITLE_BOTTOM_MARGIN as u16;
+        let height =
+            title_height + (self.keybindings.len() as u16 * line_height) + (PADDING as u16 * 2);
+
+        let x = ((screen_width - width) / 2) as i16;
+        let y = ((screen_height - height) / 2) as i16;
+
+        self.base.configure(connection, x, y, width, height)?;
+
+        self.last_shown_at = Some(Instant::now());
+        self.max_key_width = max_key_width;
+
+        self.base.show(connection)?;
+
+        self.draw(connection, font)?;
+
+        Ok(())
+    }
+
+    pub fn toggle(
+        &mut self,
+        connection: &RustConnection,
+        font: &Font,
+        keybindings: &[KeyBinding],
+        screen_width: u16,
+        screen_height: u16,
+    ) -> Result<(), X11Error> {
+        if self.base.is_visible {
+            self.hide(connection)?;
+        } else {
+            self.show(connection, font, keybindings, screen_width, screen_height)?;
+        }
+        Ok(())
+    }
+
+    pub fn should_suppress_input(&self) -> bool {
+        if let Some(shown_at) = self.last_shown_at {
+            shown_at.elapsed().as_millis() < INPUT_SUPPRESS_MS
+        } else {
+            false
+        }
+    }
+
+    fn collect_keybindings(&self, keybindings: &[KeyBinding]) -> Vec<(String, String)> {
+        let mut result = Vec::new();
+
+        let priority_actions = [
+            KeyAction::ShowKeybindOverlay,
+            KeyAction::Quit,
+            KeyAction::Restart,
+            KeyAction::KillClient,
+            KeyAction::Spawn,
+            KeyAction::ToggleFullScreen,
+            KeyAction::ToggleFloating,
+            KeyAction::CycleLayout,
+            KeyAction::FocusStack,
+            KeyAction::ViewTag,
+        ];
+
+        for &action in &priority_actions {
+            let binding = keybindings
+                .iter()
+                .filter(|kb| kb.func == action)
+                .min_by_key(|kb| kb.keys.len());
+
+            if let Some(binding) = binding {
+                if !binding.keys.is_empty() {
+                    let key_str = self.format_key_combo(&binding.keys[0]);
+                    let action_str = self.action_description(binding);
+                    result.push((key_str, action_str));
+                }
+            }
+        }
+
+        result
+    }
+
+    fn format_key_combo(&self, key: &KeyPress) -> String {
+        let mut parts = Vec::new();
+
+        for modifier in &key.modifiers {
+            let mod_str = match *modifier {
+                m if m == self.modkey => "Mod",
+                KeyButMask::SHIFT => "Shift",
+                KeyButMask::CONTROL => "Ctrl",
+                KeyButMask::MOD1 => "Alt",
+                KeyButMask::MOD4 => "Super",
+                _ => continue,
+            };
+            parts.push(mod_str.to_string());
+        }
+
+        parts.push(crate::keyboard::keysyms::format_keysym(key.keysym));
+
+        parts.join(" + ")
+    }
+
+    fn action_description(&self, binding: &KeyBinding) -> String {
+        use crate::keyboard::Arg;
+
+        match binding.func {
+            KeyAction::ShowKeybindOverlay => "Show This Keybind Help".to_string(),
+            KeyAction::Quit => "Quit Window Manager".to_string(),
+            KeyAction::Restart => "Restart Window Manager".to_string(),
+            KeyAction::Recompile => "Recompile Window Manager".to_string(),
+            KeyAction::KillClient => "Close Focused Window".to_string(),
+            KeyAction::Spawn => match &binding.arg {
+                Arg::Str(cmd) => format!("Launch: {}", cmd),
+                Arg::Array(arr) if !arr.is_empty() => format!("Launch: {}", arr[0]),
+                _ => "Launch Program".to_string(),
+            },
+            KeyAction::FocusStack => "Focus Next/Previous Window".to_string(),
+            KeyAction::FocusDirection => "Focus Window in Direction".to_string(),
+            KeyAction::SwapDirection => "Swap Window in Direction".to_string(),
+            KeyAction::ViewTag => match &binding.arg {
+                Arg::Int(n) => format!("View Workspace {}", n),
+                _ => "View Workspace".to_string(),
+            },
+            KeyAction::MoveToTag => "Move Window to Workspace".to_string(),
+            KeyAction::ToggleGaps => "Toggle Window Gaps".to_string(),
+            KeyAction::ToggleFullScreen => "Toggle Fullscreen".to_string(),
+            KeyAction::ToggleFloating => "Toggle Floating Mode".to_string(),
+            KeyAction::ChangeLayout => "Change Layout".to_string(),
+            KeyAction::CycleLayout => "Cycle Through Layouts".to_string(),
+            KeyAction::FocusMonitor => "Focus Next Monitor".to_string(),
+            KeyAction::SmartMoveWin => "Smart Move Window".to_string(),
+            KeyAction::ExchangeClient => "Exchange Client Windows".to_string(),
+            KeyAction::None => "No Action".to_string(),
+        }
+    }
+}
+
+impl Overlay for KeybindOverlay {
+    fn window(&self) -> Window {
+        self.base.window
+    }
+
+    fn is_visible(&self) -> bool {
+        self.base.is_visible
+    }
+
+    fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
+        self.base.hide(connection)?;
+        self.last_shown_at = None;
+        self.keybindings.clear();
+        Ok(())
+    }
+
+    fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error> {
+        if !self.base.is_visible {
+            return Ok(());
+        }
+
+        self.base.draw_background(connection)?;
+
+        let title = "Important Keybindings";
+        let title_width = font.text_width(title);
+        let title_x = ((self.base.width - title_width) / 2) as i16;
+        let title_y = PADDING + font.ascent();
+
+        self.base
+            .font_draw
+            .draw_text(font, self.base.foreground_color, title_x, title_y, title);
+
+        let line_height = font.height() + LINE_SPACING as u16;
+        let mut y = PADDING + font.height() as i16 + TITLE_BOTTOM_MARGIN + font.ascent();
+
+        for (key, action) in &self.keybindings {
+            let key_width = font.text_width(key);
+            let key_x = PADDING;
+
+            connection.change_gc(
+                self.base.graphics_context,
+                &ChangeGCAux::new().foreground(self.key_bg_color),
+            )?;
+            connection.poly_fill_rectangle(
+                self.base.window,
+                self.base.graphics_context,
+                &[Rectangle {
+                    x: key_x - 4,
+                    y: y - font.ascent() - 2,
+                    width: key_width + 8,
+                    height: font.height() + 4,
+                }],
+            )?;
+
+            self.base
+                .font_draw
+                .draw_text(font, self.base.foreground_color, key_x, y, key);
+
+            let action_x = PADDING + self.max_key_width as i16 + KEY_ACTION_SPACING;
+            self.base
+                .font_draw
+                .draw_text(font, self.base.foreground_color, action_x, y, action);
+
+            y += line_height as i16;
+        }
+
+        self.base.font_draw.flush();
+
+        connection.flush()?;
+
+        Ok(())
+    }
+}
diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs
new file mode 100644
index 0000000..bd87f63
--- /dev/null
+++ b/src/overlay/mod.rs
@@ -0,0 +1,156 @@
+use crate::bar::font::{Font, FontDraw};
+use crate::errors::X11Error;
+use x11rb::COPY_DEPTH_FROM_PARENT;
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+use x11rb::rust_connection::RustConnection;
+
+pub mod error;
+pub mod keybind;
+
+pub use error::ErrorOverlay;
+pub use keybind::KeybindOverlay;
+
+pub trait Overlay {
+    fn window(&self) -> Window;
+    fn is_visible(&self) -> bool;
+    fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error>;
+    fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error>;
+}
+
+pub struct OverlayBase {
+    pub window: Window,
+    pub width: u16,
+    pub height: u16,
+    pub graphics_context: Gcontext,
+    pub font_draw: FontDraw,
+    pub is_visible: bool,
+    pub background_color: u32,
+    pub foreground_color: u32,
+}
+
+impl OverlayBase {
+    pub fn new(
+        connection: &RustConnection,
+        screen: &Screen,
+        screen_num: usize,
+        display: *mut x11::xlib::Display,
+        width: u16,
+        height: u16,
+        border_width: u16,
+        border_color: u32,
+        background_color: u32,
+        foreground_color: u32,
+    ) -> Result<Self, X11Error> {
+        let window = connection.generate_id()?;
+        let graphics_context = connection.generate_id()?;
+
+        connection.create_window(
+            COPY_DEPTH_FROM_PARENT,
+            window,
+            screen.root,
+            0,
+            0,
+            width,
+            height,
+            border_width,
+            WindowClass::INPUT_OUTPUT,
+            screen.root_visual,
+            &CreateWindowAux::new()
+                .background_pixel(background_color)
+                .border_pixel(border_color)
+                .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS | EventMask::KEY_PRESS)
+                .override_redirect(1),
+        )?;
+
+        connection.create_gc(
+            graphics_context,
+            window,
+            &CreateGCAux::new()
+                .foreground(foreground_color)
+                .background(background_color),
+        )?;
+
+        connection.flush()?;
+
+        let visual = unsafe { x11::xlib::XDefaultVisual(display, screen_num as i32) };
+        let colormap = unsafe { x11::xlib::XDefaultColormap(display, screen_num as i32) };
+
+        let font_draw = FontDraw::new(display, window as x11::xlib::Drawable, visual, colormap)?;
+
+        Ok(OverlayBase {
+            window,
+            width,
+            height,
+            graphics_context,
+            font_draw,
+            is_visible: false,
+            background_color,
+            foreground_color,
+        })
+    }
+
+    pub fn configure(
+        &mut self,
+        connection: &RustConnection,
+        x: i16,
+        y: i16,
+        width: u16,
+        height: u16,
+    ) -> Result<(), X11Error> {
+        self.width = width;
+        self.height = height;
+
+        connection.configure_window(
+            self.window,
+            &ConfigureWindowAux::new()
+                .x(x as i32)
+                .y(y as i32)
+                .width(width as u32)
+                .height(height as u32),
+        )?;
+
+        Ok(())
+    }
+
+    pub fn show(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
+        connection.configure_window(
+            self.window,
+            &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
+        )?;
+
+        connection.map_window(self.window)?;
+        connection.flush()?;
+
+        self.is_visible = true;
+
+        Ok(())
+    }
+
+    pub fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
+        if self.is_visible {
+            connection.unmap_window(self.window)?;
+            connection.flush()?;
+            self.is_visible = false;
+        }
+        Ok(())
+    }
+
+    pub fn draw_background(&self, connection: &RustConnection) -> Result<(), X11Error> {
+        connection.change_gc(
+            self.graphics_context,
+            &ChangeGCAux::new().foreground(self.background_color),
+        )?;
+        connection.poly_fill_rectangle(
+            self.window,
+            self.graphics_context,
+            &[Rectangle {
+                x: 0,
+                y: 0,
+                width: self.width,
+                height: self.height,
+            }],
+        )?;
+        Ok(())
+    }
+}
diff --git a/src/window_manager.rs b/src/window_manager.rs
index c350064..5fbfb25 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -6,6 +6,7 @@ use crate::layout::GapConfig;
 use crate::layout::tiling::TilingLayout;
 use crate::layout::{Layout, LayoutBox, LayoutType, layout_from_str, next_layout};
 use crate::monitor::{Monitor, detect_monitors};
+use crate::overlay::{ErrorOverlay, KeybindOverlay, Overlay};
 use std::collections::HashSet;
 use std::process::Command;
 use x11rb::cursor::Handle as CursorHandle;
@@ -69,6 +70,9 @@ pub struct WindowManager {
     display: *mut x11::xlib::Display,
     font: crate::bar::font::Font,
     keychord_state: keyboard::handlers::KeychordState,
+    error_message: Option<String>,
+    overlay: ErrorOverlay,
+    keybind_overlay: KeybindOverlay,
 }
 
 type WmResult<T> = Result<T, WmError>;
@@ -156,6 +160,18 @@ impl WindowManager {
 
         let atoms = AtomCache::new(&connection)?;
 
+        let overlay = ErrorOverlay::new(
+            &connection,
+            &screen,
+            screen_number,
+            display,
+            &font,
+            screen.width_in_pixels,
+        )?;
+
+        let keybind_overlay =
+            KeybindOverlay::new(&connection, &screen, screen_number, display, config.modkey)?;
+
         let mut window_manager = Self {
             config,
             connection,
@@ -177,6 +193,9 @@ impl WindowManager {
             display,
             font,
             keychord_state: keyboard::handlers::KeychordState::Idle,
+            error_message: None,
+            overlay,
+            keybind_overlay,
         };
 
         window_manager.scan_existing_windows()?;
@@ -225,6 +244,48 @@ impl WindowManager {
         Ok(tag_mask(0))
     }
 
+    fn try_reload_config(&mut self) -> Result<(), String> {
+        let config_dir = if let Some(xdg_config) = std::env::var_os("XDG_CONFIG_HOME") {
+            std::path::PathBuf::from(xdg_config).join("oxwm")
+        } else if let Some(home) = std::env::var_os("HOME") {
+            std::path::PathBuf::from(home).join(".config").join("oxwm")
+        } else {
+            return Err("Could not find config directory".to_string());
+        };
+
+        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());
+        };
+
+        let config_str = std::fs::read_to_string(&config_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))?
+        };
+
+        self.config = new_config;
+        self.error_message = None;
+        Ok(())
+    }
+
     fn scan_existing_windows(&mut self) -> WmResult<()> {
         let tree = self.connection.query_tree(self.root)?.reply()?;
         let net_client_info = self.atoms.net_client_info;
@@ -377,6 +438,10 @@ impl WindowManager {
                 self.update_bar()?;
             }
 
+            if self.overlay.is_visible() && self.overlay.should_auto_dismiss() {
+                let _ = self.overlay.hide(&self.connection);
+            }
+
             std::thread::sleep(std::time::Duration::from_millis(100));
         }
     }
@@ -808,7 +873,7 @@ impl WindowManager {
                         indicator.push('+');
                     }
 
-                    indicator.push_str(&self.format_keysym(key_press.keysym));
+                    indicator.push_str(&keyboard::keysyms::format_keysym(key_press.keysym));
                 }
 
                 indicator.push('-');
@@ -827,60 +892,6 @@ impl WindowManager {
         }
     }
 
-    fn format_keysym(&self, keysym: keyboard::keysyms::Keysym) -> String {
-        use keyboard::keysyms::*;
-
-        match keysym {
-            XK_RETURN => "Return",
-            XK_ESCAPE => "Esc",
-            XK_SPACE => "Space",
-            XK_TAB => "Tab",
-            XK_BACKSPACE => "Backspace",
-            XK_DELETE => "Del",
-            XK_F1 => "F1",
-            XK_F2 => "F2",
-            XK_F3 => "F3",
-            XK_F4 => "F4",
-            XK_F5 => "F5",
-            XK_F6 => "F6",
-            XK_F7 => "F7",
-            XK_F8 => "F8",
-            XK_F9 => "F9",
-            XK_F10 => "F10",
-            XK_F11 => "F11",
-            XK_F12 => "F12",
-            XK_A..=XK_Z | XK_0..=XK_9 => {
-                return char::from_u32(keysym).unwrap_or('?').to_string();
-            }
-            XK_LEFT => "Left",
-            XK_RIGHT => "Right",
-            XK_UP => "Up",
-            XK_DOWN => "Down",
-            XK_HOME => "Home",
-            XK_END => "End",
-            XK_PAGE_UP => "PgUp",
-            XK_PAGE_DOWN => "PgDn",
-            XK_INSERT => "Ins",
-            XK_MINUS => "-",
-            XK_EQUAL => "=",
-            XK_LEFT_BRACKET => "[",
-            XK_RIGHT_BRACKET => "]",
-            XK_SEMICOLON => ";",
-            XK_APOSTROPHE => "'",
-            XK_GRAVE => "`",
-            XK_BACKSLASH => "\\",
-            XK_COMMA => ",",
-            XK_PERIOD => ".",
-            XK_SLASH => "/",
-            XF86_AUDIO_RAISE_VOLUME => "Vol+",
-            XF86_AUDIO_LOWER_VOLUME => "Vol-",
-            XF86_AUDIO_MUTE => "Mute",
-            XF86_MON_BRIGHTNESS_UP => "Bri+",
-            XF86_MON_BRIGHTNESS_DOWN => "Bri-",
-            _ => "?",
-        }
-        .to_string()
-    }
 
     fn update_bar(&mut self) -> WmResult<()> {
         let layout_symbol = self.get_layout_symbol();
@@ -1020,6 +1031,16 @@ impl WindowManager {
                     self.focus_monitor(*direction)?;
                 }
             }
+            KeyAction::ShowKeybindOverlay => {
+                let monitor = &self.monitors[self.selected_monitor];
+                self.keybind_overlay.toggle(
+                    &self.connection,
+                    &self.font,
+                    &self.config.keybindings,
+                    monitor.width as u16,
+                    monitor.height as u16,
+                )?;
+            }
             KeyAction::None => {}
         }
         Ok(())
@@ -1610,6 +1631,60 @@ impl WindowManager {
 
     fn handle_event(&mut self, event: Event) -> WmResult<Option<bool>> {
         match event {
+            Event::KeyPress(ref e) if e.event == self.overlay.window() => {
+                if self.overlay.is_visible() {
+                    let _ = self.overlay.hide(&self.connection);
+                }
+                return Ok(None);
+            }
+            Event::ButtonPress(ref e) if e.event == self.overlay.window() => {
+                if self.overlay.is_visible() {
+                    let _ = self.overlay.hide(&self.connection);
+                }
+                return Ok(None);
+            }
+            Event::Expose(ref e) if e.window == self.overlay.window() => {
+                if self.overlay.is_visible() {
+                    let _ = self.overlay.draw(&self.connection, &self.font);
+                }
+                return Ok(None);
+            }
+            Event::KeyPress(ref e) if e.event == self.keybind_overlay.window() => {
+                if self.keybind_overlay.is_visible()
+                    && !self.keybind_overlay.should_suppress_input()
+                {
+                    use crate::keyboard::keysyms;
+                    let keyboard_mapping = self
+                        .connection
+                        .get_keyboard_mapping(
+                            self.connection.setup().min_keycode,
+                            self.connection.setup().max_keycode
+                                - self.connection.setup().min_keycode
+                                + 1,
+                        )?
+                        .reply()?;
+
+                    let min_keycode = self.connection.setup().min_keycode;
+                    let keysyms_per_keycode = keyboard_mapping.keysyms_per_keycode;
+                    let index = (e.detail - min_keycode) as usize * keysyms_per_keycode as usize;
+
+                    if let Some(&keysym) = keyboard_mapping.keysyms.get(index) {
+                        if keysym == keysyms::XK_ESCAPE || keysym == keysyms::XK_Q {
+                            let _ = self.keybind_overlay.hide(&self.connection);
+                        }
+                    }
+                }
+                return Ok(None);
+            }
+            Event::ButtonPress(ref e) if e.event == self.keybind_overlay.window() => {
+                return Ok(None);
+            }
+            Event::Expose(ref e) if e.window == self.keybind_overlay.window() => {
+                if self.keybind_overlay.is_visible() {
+                    let _ = self.keybind_overlay.draw(&self.connection, &self.font);
+                }
+                return Ok(None);
+            }
             Event::MapRequest(event) => {
                 let attrs = match self.connection.get_window_attributes(event.window)?.reply() {
                     Ok(attrs) => attrs,
@@ -1705,7 +1780,27 @@ impl WindowManager {
 
                         match action {
                             KeyAction::Quit => return Ok(Some(false)),
-                            KeyAction::Restart => return Ok(Some(true)),
+                            KeyAction::Restart => match self.try_reload_config() {
+                                Ok(()) => {
+                                    self.gaps_enabled = self.config.gaps_enabled;
+                                    self.error_message = None;
+                                    let _ = self.overlay.hide(&self.connection);
+                                    self.apply_layout()?;
+                                    self.update_bar()?;
+                                }
+                                Err(err) => {
+                                    self.error_message = Some(err.clone());
+                                    let screen_width = self.screen.width_in_pixels;
+                                    let screen_height = self.screen.height_in_pixels;
+                                    let _ = self.overlay.show_error(
+                                        &self.connection,
+                                        &self.font,
+                                        &err,
+                                        screen_width,
+                                        screen_height,
+                                    );
+                                }
+                            },
                             _ => self.handle_key_action(action, &arg)?,
                         }
                     }
diff --git a/templates/config.lua b/templates/config.lua
index 577c911..a6dc367 100644
--- a/templates/config.lua
+++ b/templates/config.lua
@@ -54,6 +54,7 @@ return {
         { 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" },