Skip to content

Commit

Permalink
feat: add support for multiple desktops (#151)
Browse files Browse the repository at this point in the history
* Add support for including multiple desktops

Include windows cloaked by the shell, but exclude "invisible" windows.
This seems to result in pretty much the expected list of windows across
all desktops instead of just the current one.

* Use registry key instead of config setting

Windows already has a setting for the Alt-tab switcher to include
virtual desktops, so we can just use that instead of a config.

* Add configuration for only_current_desktop

Did a bit of cleanup / refactoring as well.

* Combine invisible/visible check and more cleanup

* Move registry check out of loop + minor refactor
  • Loading branch information
ian-h-chamberlain authored Nov 21, 2024
1 parent eb6341a commit 55f0046
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 28 deletions.
10 changes: 8 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,10 @@ impl App {
}

fn switch_windows(&mut self, hwnd: HWND, reverse: bool) -> Result<bool> {
let windows = list_windows(self.config.switch_windows_ignore_minimal)?;
let windows = list_windows(
self.config.switch_windows_ignore_minimal,
self.config.switch_windows_only_current_desktop(),
)?;
debug!(
"switch windows: hwnd:{hwnd:?} reverse:{reverse} state:{:?}",
self.switch_windows_state
Expand Down Expand Up @@ -394,7 +397,10 @@ impl App {
debug!("switch apps: new index:{}", state.index);
return Ok(());
}
let windows = list_windows(self.config.switch_apps_ignore_minimal)?;
let windows = list_windows(
self.config.switch_apps_ignore_minimal,
self.config.switch_apps_only_current_desktop(),
)?;
let mut apps = vec![];
for (module_path, hwnds) in windows.iter() {
let module_hwnd = if is_iconic_window(hwnds[0].0) {
Expand Down
47 changes: 46 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use anyhow::{anyhow, Result};
use indexmap::IndexMap;
use ini::{Ini, ParseOption};
use log::LevelFilter;
use windows::core::w;
use windows::Win32::UI::Input::KeyboardAndMouse::{
VIRTUAL_KEY, VK_LCONTROL, VK_LMENU, VK_LWIN, VK_RCONTROL, VK_RMENU, VK_RWIN,
};

use crate::utils::get_exe_folder;
use crate::utils::{get_exe_folder, RegKey};

pub const SWITCH_WINDOWS_HOTKEY_ID: u32 = 1;
pub const SWITCH_APPS_HOTKEY_ID: u32 = 2;
Expand All @@ -23,10 +24,12 @@ pub struct Config {
pub switch_windows_hotkey: Hotkey,
pub switch_windows_blacklist: HashSet<String>,
pub switch_windows_ignore_minimal: bool,
switch_windows_only_current_desktop: Option<bool>,
pub switch_apps_enable: bool,
pub switch_apps_hotkey: Hotkey,
pub switch_apps_ignore_minimal: bool,
pub switch_apps_override_icons: IndexMap<String, String>,
switch_apps_only_current_desktop: Option<bool>,
}

impl Default for Config {
Expand All @@ -43,11 +46,13 @@ impl Default for Config {
.unwrap(),
switch_windows_blacklist: Default::default(),
switch_windows_ignore_minimal: false,
switch_windows_only_current_desktop: None,
switch_apps_enable: false,
switch_apps_hotkey: Hotkey::create(SWITCH_APPS_HOTKEY_ID, "switch apps", "alt + tab")
.unwrap(),
switch_apps_ignore_minimal: false,
switch_apps_override_icons: Default::default(),
switch_apps_only_current_desktop: None,
}
}
}
Expand Down Expand Up @@ -95,6 +100,12 @@ impl Config {
if let Some(v) = section.get("ignore_minimal").and_then(Config::to_bool) {
conf.switch_windows_ignore_minimal = v;
}
if let Some(v) = section
.get("only_current_desktop")
.and_then(Config::to_bool)
{
conf.switch_windows_only_current_desktop = Some(v);
}
}
if let Some(section) = ini_conf.section(Some("switch-apps")) {
if let Some(v) = section.get("enable").and_then(Config::to_bool) {
Expand All @@ -119,6 +130,13 @@ impl Config {
})
.collect();
}

if let Some(v) = section
.get("only_current_desktop")
.and_then(Config::to_bool)
{
conf.switch_apps_only_current_desktop = Some(v);
}
}
Ok(conf)
}
Expand All @@ -138,6 +156,33 @@ impl Config {
_ => None,
}
}

/// Whether the user has configured app switching to include other desktops.
/// If the configured value is not a valid bool, the Windows registry will be
/// used as a fallback.
pub fn switch_apps_only_current_desktop(&self) -> bool {
self.switch_apps_only_current_desktop
.unwrap_or_else(Self::system_switcher_only_current_desktop)
}

/// Whether the user has configured window switching to include other desktops.
/// If the configured value is not a valid bool, the Windows registry will be
/// used as a fallback.
pub fn switch_windows_only_current_desktop(&self) -> bool {
self.switch_windows_only_current_desktop
.unwrap_or_else(Self::system_switcher_only_current_desktop)
}

fn system_switcher_only_current_desktop() -> bool {
let alt_tab_filter = RegKey::new_hkcu(
w!(r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"),
w!("VirtualDesktopAltTabFilter"),
)
.and_then(|k| k.get_int())
.unwrap_or(1);

alt_tab_filter != 0
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
60 changes: 45 additions & 15 deletions src/utils/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use windows::core::PWSTR;
use windows::Win32::{
Foundation::{BOOL, HWND, LPARAM, MAX_PATH, POINT, RECT},
Graphics::{
Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED},
Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED, DWM_CLOAKED_SHELL},
Gdi::{GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST},
},
System::{
Expand All @@ -16,11 +16,15 @@ use windows::Win32::{
PROCESS_VM_READ,
},
},
UI::WindowsAndMessaging::{
EnumWindows, GetCursorPos, GetForegroundWindow, GetWindow, GetWindowLongPtrW,
GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindowVisible,
SetForegroundWindow, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_USERDATA, GW_OWNER,
SWP_NOZORDER, SW_RESTORE, WINDOWPLACEMENT, WS_EX_TOPMOST,
UI::{
Controls::STATE_SYSTEM_INVISIBLE,
WindowsAndMessaging::{
EnumWindows, GetCursorPos, GetForegroundWindow, GetTitleBarInfo, GetWindow,
GetWindowLongPtrW, GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId,
IsIconic, IsWindowVisible, SetForegroundWindow, SetWindowPos, ShowWindow, GWL_EXSTYLE,
GWL_USERDATA, GW_OWNER, SWP_NOZORDER, SW_RESTORE, TITLEBARINFO, WINDOWPLACEMENT,
WS_EX_TOPMOST,
},
},
};

Expand All @@ -30,25 +34,48 @@ pub fn is_iconic_window(hwnd: HWND) -> bool {

pub fn is_visible_window(hwnd: HWND) -> bool {
let ret = unsafe { IsWindowVisible(hwnd) };
ret.as_bool()
if !ret.as_bool() {
return false;
}

// Some "visible" windows are cloaked with `DWM_CLOAKED_SHELL` but always have
// this invisible flag set, so this filters out those unusual cases:
let mut title_info = TITLEBARINFO::default();
title_info.cbSize = std::mem::size_of_val(&title_info) as u32;
let _ = unsafe { GetTitleBarInfo(hwnd, &mut title_info) };

title_info.rgstate[0] & STATE_SYSTEM_INVISIBLE.0 == 0
}

pub fn is_topmost_window(hwnd: HWND) -> bool {
let ex_style = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) } as u32;
ex_style & WS_EX_TOPMOST.0 != 0
}

pub fn is_cloaked_window(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
pub fn get_window_cloak_type(hwnd: HWND) -> u32 {
let mut cloak_type = 0u32;
let _ = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
&mut cloaked as *mut u32 as *mut c_void,
&mut cloak_type as *mut u32 as *mut c_void,
size_of::<u32>() as u32,
)
};
cloaked != 0
cloak_type
}

fn is_cloaked_window(hwnd: HWND, only_current_desktop: bool) -> bool {
let cloak_type = get_window_cloak_type(hwnd);

if only_current_desktop {
// Any kind of cloaking counts against a window
cloak_type != 0
} else {
// Windows from other desktops will be cloaked as SHELL, so we treat them
// as if they are uncloaked. All other cloak types count against the window
cloak_type | DWM_CLOAKED_SHELL != DWM_CLOAKED_SHELL
}
}

pub fn is_small_window(hwnd: HWND) -> bool {
Expand Down Expand Up @@ -192,7 +219,10 @@ pub fn set_window_user_data(hwnd: HWND, ptr: isize) -> isize {
///
/// Duo to the limitation of `OpenProcess`, this function will not list `Task Manager`
/// and others which are running as administrator if `Switcher` is not `running as administrator`.
pub fn list_windows(ignore_minimal: bool) -> Result<IndexMap<String, Vec<(HWND, String)>>> {
pub fn list_windows(
ignore_minimal: bool,
only_current_desktop: bool,
) -> Result<IndexMap<String, Vec<(HWND, String)>>> {
let mut result: IndexMap<String, Vec<(HWND, String)>> = IndexMap::new();
let mut hwnds: Vec<HWND> = Default::default();
unsafe { EnumWindows(Some(enum_window), LPARAM(&mut hwnds as *mut _ as isize)) }
Expand All @@ -201,16 +231,16 @@ pub fn list_windows(ignore_minimal: bool) -> Result<IndexMap<String, Vec<(HWND,
let mut owner_hwnds = vec![];
for hwnd in hwnds.iter().cloned() {
let mut valid = is_visible_window(hwnd)
&& !is_cloaked_window(hwnd)
&& !is_cloaked_window(hwnd, only_current_desktop)
&& !is_topmost_window(hwnd)
&& !is_small_window(hwnd);
if valid && ignore_minimal && is_iconic_window(hwnd) {
valid = false;
}
if valid {
let title = get_window_title(hwnd);
if !title.is_empty() && title != "Program Manager" {
valid_hwnds.push((hwnd, title))
if !title.is_empty() {
valid_hwnds.push((hwnd, title));
}
}
owner_hwnds.push(get_owner_window(hwnd))
Expand Down
21 changes: 16 additions & 5 deletions tools/inspect-windows/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::{Context, Result};
use window_switcher::utils::*;

use windows::Win32::Foundation::{BOOL, HWND, LPARAM};
use windows::Win32::Graphics::Dwm::{DWM_CLOAKED_APP, DWM_CLOAKED_INHERITED, DWM_CLOAKED_SHELL};
use windows::Win32::UI::WindowsAndMessaging::{EnumWindows, GetWindow, GW_OWNER};

fn main() -> Result<()> {
Expand All @@ -22,7 +23,7 @@ struct WindowInfo {
owner_title: String,
size: (usize, usize),
is_visible: bool,
is_cloaked: bool,
cloak_type: u32,
is_iconic: bool,
is_topmost: bool,
}
Expand All @@ -31,9 +32,9 @@ impl WindowInfo {
pub fn stringify(&self) -> String {
let size = format!("{}x{}", self.size.0, self.size.1);
format!(
"visible:{}cloacked{}iconic{}topmost:{} {:>10} {:>10}:{} {}:{}",
"visible:{}cloak:{}iconic:{}topmost:{} {:>10} {:>10}:{} {}:{}",
pretty_bool(self.is_visible),
pretty_bool(self.is_cloaked),
pretty_cloak(self.cloak_type),
pretty_bool(self.is_iconic),
pretty_bool(self.is_topmost),
size,
Expand All @@ -52,7 +53,7 @@ fn collect_windows_info() -> anyhow::Result<Vec<WindowInfo>> {
let mut output = vec![];
for hwnd in hwnds {
let title = get_window_title(hwnd);
let is_cloaked = is_cloaked_window(hwnd);
let cloak_type = get_window_cloak_type(hwnd);
let is_iconic = is_iconic_window(hwnd);
let is_topmost = is_topmost_window(hwnd);
let is_visible = is_visible_window(hwnd);
Expand All @@ -70,7 +71,7 @@ fn collect_windows_info() -> anyhow::Result<Vec<WindowInfo>> {
owner_title,
size: (width as usize, height as usize),
is_visible,
is_cloaked,
cloak_type,
is_iconic,
is_topmost,
};
Expand All @@ -87,6 +88,16 @@ fn pretty_bool(value: bool) -> String {
}
}

fn pretty_cloak(value: u32) -> &'static str {
match value {
0 => " ",
DWM_CLOAKED_SHELL => "S",
DWM_CLOAKED_APP => "A",
DWM_CLOAKED_INHERITED => "I",
_ => "?",
}
}

extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let windows: &mut Vec<HWND> = unsafe { &mut *(lparam.0 as *mut Vec<HWND>) };
windows.push(hwnd);
Expand Down
20 changes: 15 additions & 5 deletions window-switcher.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Whether to show trayicon, yes/no
trayicon = yes
trayicon = yes

[switch-windows]

Expand All @@ -13,6 +13,11 @@ blacklist =
# Ignore minimal windows
ignore_minimal = no

# Switch to windows from only the current virtual desktops instead of all desktops.
# Defaults to match the Alt-Tab behavior of Windows:
# Settings > System > Multitasking > Virtual Desktops
only_current_desktop = auto

[switch-apps]

# Whether to enable switching apps
Expand All @@ -27,15 +32,20 @@ ignore_minimal = no
# List of override icons, syntax: app1.exe=icon1.ico,app2.exe=icon2.png.
# The icon path can be a full path or a relative path to the app's directory.
# The icon format can be ico or png.
override_icons =
override_icons =

# Switch to apps from only the current virtual desktops instead of all desktops.
# Defaults to match the Alt-Tab behavior of Windows:
# Settings > System > Multitasking > Virtual Desktops
only_current_desktop = auto

[log]

# Log level can be one of off,error,warn,info,debug,trace.
level = info
level = info

# Log file path.
# Log file path.
# e.g.
# window-switcher.log (located in the same directory as window-switcher.exe)
# C:\Users\sigod\AppData\Local\Temp\window-switcher.log (or used the full path)
path =
path =

0 comments on commit 55f0046

Please sign in to comment.