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" },