diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index eb11b522..fcdc7cee 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -143,6 +143,8 @@ pub enum SocketMessage { ToggleFocusFollowsMouse(FocusFollowsMouseImplementation), MouseFollowsFocus(bool), ToggleMouseFollowsFocus, + RemoveTitleBar(ApplicationIdentifier, String), + ToggleTitleBars, AddSubscriber(String), RemoveSubscriber(String), NotificationSchema, diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index bf732041..ac6a45e4 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -152,6 +152,10 @@ lazy_static! { static ref BORDER_OFFSET: Arc>> = Arc::new(Mutex::new(None)); + + // Use app-specific titlebar removal options where possible + // eg. Windows Terminal, IntelliJ IDEA, Firefox + static ref NO_TITLEBAR: Arc>> = Arc::new(Mutex::new(vec![])); } pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false); @@ -168,6 +172,7 @@ pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0); pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(20); // 0 0 0 aka pure black, I doubt anyone will want this as a border colour pub const TRANSPARENCY_COLOUR: u32 = 0; +pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false); pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); @@ -511,7 +516,7 @@ fn main() -> Result<()> { tracing::error!("received ctrl-c, restoring all hidden windows and terminating process"); - wm.lock().restore_all_windows(); + wm.lock().restore_all_windows()?; if WindowsApi::focus_follows_mouse()? { WindowsApi::disable_focus_follows_mouse()?; diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 608d2d19..d08f1865 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -61,7 +61,9 @@ use crate::INITIAL_CONFIGURATION_LOADED; use crate::LAYERED_WHITELIST; use crate::MANAGE_IDENTIFIERS; use crate::MONITOR_INDEX_PREFERENCES; +use crate::NO_TITLEBAR; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; +use crate::REMOVE_TITLEBARS; use crate::SUBSCRIPTION_PIPES; use crate::TCP_CONNECTIONS; use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS; @@ -575,7 +577,7 @@ impl WindowManager { tracing::info!( "received stop command, restoring all hidden windows and terminating process" ); - self.restore_all_windows(); + self.restore_all_windows()?; if WindowsApi::focus_follows_mouse()? { WindowsApi::disable_focus_follows_mouse()?; @@ -1076,6 +1078,17 @@ impl WindowManager { let mut stream = UnixStream::connect(socket)?; stream.write_all(schema.as_bytes())?; } + SocketMessage::RemoveTitleBar(_, ref id) => { + let mut identifiers = NO_TITLEBAR.lock(); + if !identifiers.contains(id) { + identifiers.push(id.clone()); + } + } + SocketMessage::ToggleTitleBars => { + let current = REMOVE_TITLEBARS.load(Ordering::SeqCst); + REMOVE_TITLEBARS.store(!current, Ordering::SeqCst); + self.update_focused_workspace(false)?; + } }; match message { diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index 13b001ce..8a6878bb 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -32,6 +32,7 @@ use crate::HIDDEN_HWNDS; use crate::HIDING_BEHAVIOUR; use crate::LAYERED_WHITELIST; use crate::MANAGE_IDENTIFIERS; +use crate::NO_TITLEBAR; use crate::WSL2_UI_PROCESSES; #[derive(Debug, Clone, Copy, JsonSchema)] @@ -346,7 +347,7 @@ impl Window { pub fn transparent(self) -> Result<()> { let mut ex_style = self.ex_style()?; ex_style.insert(ExtendedWindowStyle::LAYERED); - self.update_ex_style(ex_style)?; + self.update_ex_style(&ex_style)?; WindowsApi::set_transparent(self.hwnd()); Ok(()) } @@ -354,15 +355,15 @@ impl Window { pub fn opaque(self) -> Result<()> { let mut ex_style = self.ex_style()?; ex_style.remove(ExtendedWindowStyle::LAYERED); - self.update_ex_style(ex_style) + self.update_ex_style(&ex_style) } #[allow(dead_code)] - pub fn update_style(self, style: WindowStyle) -> Result<()> { + pub fn update_style(self, style: &WindowStyle) -> Result<()> { WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?) } - pub fn update_ex_style(self, style: ExtendedWindowStyle) -> Result<()> { + pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> { WindowsApi::update_ex_style(self.hwnd(), isize::try_from(style.bits())?) } @@ -397,6 +398,20 @@ impl Window { WindowsApi::is_window(self.hwnd()) } + pub fn remove_title_bar(self) -> Result<()> { + let mut style = self.style()?; + style.remove(WindowStyle::CAPTION); + style.remove(WindowStyle::THICKFRAME); + self.update_style(&style) + } + + pub fn add_title_bar(self) -> Result<()> { + let mut style = self.style()?; + style.insert(WindowStyle::CAPTION); + style.insert(WindowStyle::THICKFRAME); + self.update_style(&style) + } + #[tracing::instrument(fields(exe, title))] pub fn should_manage(self, event: Option) -> Result { if let Some(WindowManagerEvent::DisplayChange(_)) = event { @@ -427,7 +442,7 @@ impl Window { // If not allowing cloaked windows, we need to ensure the window is not cloaked (false, false) => { if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) { - return Ok(window_is_eligible(&title, &exe_name, &class, self.style()?, self.ex_style()?, event)); + return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event)); } } _ => {} @@ -441,8 +456,8 @@ fn window_is_eligible( title: &String, exe_name: &String, class: &String, - style: WindowStyle, - ex_style: ExtendedWindowStyle, + style: &WindowStyle, + ex_style: &ExtendedWindowStyle, event: Option, ) -> bool { let mut should_float = false; @@ -503,7 +518,12 @@ fn window_is_eligible( wsl2_ui_processes.contains(exe_name) }; - if (allow_wsl2_gui || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE)) + let allow_titlebar_removed = { + let titlebars_removed = NO_TITLEBAR.lock(); + titlebars_removed.contains(exe_name) + }; + + if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE)) && !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME) // Get a lot of dupe events coming through that make the redrawing go crazy // on FocusChange events if I don't filter out this one. But, if we are diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index e011bd7b..d69f96dc 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -49,7 +49,9 @@ use crate::FLOAT_IDENTIFIERS; use crate::HOME_DIR; use crate::LAYERED_WHITELIST; use crate::MANAGE_IDENTIFIERS; +use crate::NO_TITLEBAR; use crate::OBJECT_NAME_CHANGE_ON_LAUNCH; +use crate::REMOVE_TITLEBARS; use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS; use crate::WORKSPACE_RULES; @@ -75,6 +77,7 @@ pub struct WindowManager { pub already_moved_window_handles: Arc>>, } +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Serialize, JsonSchema)] pub struct State { pub monitors: Ring, @@ -87,6 +90,7 @@ pub struct State { pub focus_follows_mouse: Option, pub mouse_follows_focus: bool, pub has_pending_raise_op: bool, + pub remove_titlebars: bool, pub float_identifiers: Vec, pub manage_identifiers: Vec, pub layered_whitelist: Vec, @@ -114,6 +118,7 @@ impl From<&WindowManager> for State { focus_follows_mouse: wm.focus_follows_mouse, mouse_follows_focus: wm.mouse_follows_focus, has_pending_raise_op: wm.has_pending_raise_op, + remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst), float_identifiers: FLOAT_IDENTIFIERS.lock().clone(), manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(), layered_whitelist: LAYERED_WHITELIST.lock().clone(), @@ -480,6 +485,7 @@ impl WindowManager { Ok(()) } + #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip(self))] fn add_window_handle_to_move_based_on_workspace_rule( &self, @@ -954,18 +960,26 @@ impl WindowManager { } #[tracing::instrument(skip(self))] - pub fn restore_all_windows(&mut self) { + pub fn restore_all_windows(&mut self) -> Result<()> { tracing::info!("restoring all hidden windows"); + let no_titlebar = NO_TITLEBAR.lock(); + for monitor in self.monitors_mut() { for workspace in monitor.workspaces_mut() { for containers in workspace.containers_mut() { for window in containers.windows_mut() { + if no_titlebar.contains(&window.exe()?) { + window.add_title_bar()?; + } + window.restore(); } } } } + + Ok(()) } #[tracing::instrument(skip(self))] diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index 429f2ef5..be3f66fd 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -524,6 +524,8 @@ impl WindowsApi { } fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result { + // Can return 0, which does not always mean that an error has occurred + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw Result::from(WindowsResult::from(unsafe { GetWindowLongPtrW(hwnd, index) })) diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 21ac4c02..2ae9f913 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -23,6 +23,8 @@ use crate::ring::Ring; use crate::window::Window; use crate::windows_api::WindowsApi; use crate::INITIAL_CONFIGURATION_LOADED; +use crate::NO_TITLEBAR; +use crate::REMOVE_TITLEBARS; #[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)] pub struct Workspace { @@ -212,9 +214,18 @@ impl Workspace { self.resize_dimensions(), ); + let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst); + let no_titlebar = NO_TITLEBAR.lock().clone(); + let windows = self.visible_windows_mut(); for (i, window) in windows.into_iter().enumerate() { if let (Some(window), Some(layout)) = (window, layouts.get(i)) { + if should_remove_titlebars && no_titlebar.contains(&window.exe()?) { + window.remove_title_bar()?; + } else if no_titlebar.contains(&window.exe()?) { + window.add_title_bar()?; + } + window.set_position(layout, invisible_borders, false)?; } } diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 9a1ae5a2..66e9fb63 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -513,6 +513,7 @@ gen_application_target_subcommand_args! { IdentifyLayeredApplication, IdentifyObjectNameChangeApplication, IdentifyBorderOverflowApplication, + RemoveTitleBar, } #[derive(Parser, AhkFunction)] @@ -964,6 +965,11 @@ enum SubCommand { /// Identify an application that has WS_EX_LAYERED, but should still be managed #[clap(arg_required_else_help = true)] IdentifyLayeredApplication(IdentifyLayeredApplication), + /// Whitelist an application for title bar removal + #[clap(arg_required_else_help = true)] + RemoveTitleBar(RemoveTitleBar), + /// Toggle title bars for whitelisted applications + ToggleTitleBars, /// Identify an application that has overflowing borders #[clap(arg_required_else_help = true)] #[clap(alias = "identify-border-overflow")] @@ -1710,6 +1716,21 @@ fn main() -> Result<()> { .as_bytes()?, )?; } + SubCommand::RemoveTitleBar(target) => { + match target.identifier { + ApplicationIdentifier::Exe => {} + _ => { + return Err(anyhow!( + "this command requires applications to be identified by their exe" + )) + } + } + + send_message(&SocketMessage::RemoveTitleBar(target.identifier, target.id).as_bytes()?)?; + } + SubCommand::ToggleTitleBars => { + send_message(&SocketMessage::ToggleTitleBars.as_bytes()?)?; + } SubCommand::Manage => { send_message(&SocketMessage::ManageFocusedWindow.as_bytes()?)?; }