From c42bd183fa9d47aed48693ff91d49618861b83b2 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 17:21:56 +0200 Subject: [PATCH 01/21] silence 3 warnings about loss of 'const' --- src/cmdtab.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index d1150c4..611d02b 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -82,7 +82,7 @@ static const string UWPAppHostClass = string(L"ApplicationFrameWindow"); static const string UWPCoreWindowClass = string(L"Windows.UI.Core.CoreWindow"); static const string UWPAppsPath = string(L"C:\\Program Files\\WindowsApps"); -static bool StringsAreEqual(string *s1, string *s2) +static bool StringsAreEqual(const string *s1, const string *s2) { // Reverse compare strings // Compare in reverse since this function is mostly used to compare file From 12f6847819cedd1f2ded10123575cb2c9492b0fc Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 17:24:29 +0200 Subject: [PATCH 02/21] add Config.showAllWindows flag for collapsing the navigation levels Intended meaning (still to be implemented): If true, will show each window as if it were a separate app. --- src/cmdtab.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cmdtab.c b/src/cmdtab.c index 611d02b..a0e6c5a 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -579,6 +579,7 @@ struct config { bool fastSwitchingForWindows; bool showSwitcherForApps; bool showSwitcherForWindows; + bool showAllWindows; // collapse two navigation levels (apps, windows) into one bool wrapbump; bool restoreOnCancel; // Appearance @@ -628,6 +629,7 @@ static struct config Config = { // cmdtab settings .fastSwitchingForWindows = true, .showSwitcherForApps = true, .showSwitcherForWindows = false, + .showAllWindows = true, .wrapbump = true, .restoreOnCancel = false, // Appearance From 99932d711fd66ed63d684de3bb5b8548e2450c19 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 17:26:33 +0200 Subject: [PATCH 03/21] split AddToSwitcher() behavior according to showAllWindows If true, call AddWindowLikeAnApp() else call AddAppOrCollectWindow() --- src/cmdtab.c | 82 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index a0e6c5a..57acf62 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -764,31 +764,7 @@ static void AddToHistory(handle hwnd) { PrintWindowX(hwnd); } -static void AddToSwitcher(handle hwnd) -{ - // 1. Get hosted window in case of UWP host - hwnd = GetCoreWindow(hwnd); - // 2. Basic checks for eligibility in Alt-Tab - if (!IsAltTabWindow(hwnd)) { - Print(L"(ignored) "); - Print(L"add window %s", GetAppHost(hwnd) != hwnd ? L"(uwp) " : L""); - PrintWindowX(hwnd); - return; - } - // 3. Get module filepath, abort if failed (most likely access denied) - string filepath = GetExePath(hwnd); - if (!filepath.ok) { - return; // Don't have to print error, since GetFilepath already does that - } - // 4. Check window against user-defined blacklist - string filename = StringFileName(&filepath, false); - string class = GetWindowClass(hwnd); - if (WindowIsBlacklisted(&filename, &class)) { - Print(L"(blacklisted) "); - Print(L"add window %s", GetAppHost(hwnd) != hwnd ? L"(uwp) " : L""); - PrintWindowX(hwnd); - return; - } +static bool AddAppOrCollectWindow(handle hwnd, string filepath) { // 5. Find existing app with same module filepath struct app *app = null; for (int i = 0; i < AppsCount; i++) { @@ -801,7 +777,7 @@ static void AddToSwitcher(handle hwnd) if (!app) { if (AppsCount >= countof(Apps)) { Error(Switcher, L"ERROR reached max. apps\n"); - return; + return true; } // Since I reset by truncating (setting AppsCount to 0), make sure to overwrite all struct fields when reusing array slots app = &Apps[AppsCount++]; @@ -813,12 +789,64 @@ static void AddToSwitcher(handle hwnd) } if (app->windowsCount >= countof(app->windows)) { Error(Switcher, L"ERROR reached max. windows for app\n"); - return; + return true; } // 7. Add window to app's window list app->windows[app->windowsCount++] = hwnd; Print(L"add window %s", GetAppHost(hwnd) != hwnd ? L"(uwp) " : L""); PrintWindowX(hwnd); + return false; +} + +static void AddWindowLikeAnApp(handle hwnd, string filepath) { + // 5. Create one pseudo-"app" entry per window + if (AppsCount >= countof(Apps)) { + Error(Switcher, L"ERROR reached max. windows\n"); + return; + } + struct app *windowEntry = &Apps[AppsCount++]; + if (windowEntry->icon) DestroyIcon(windowEntry->icon); + + windowEntry->path = filepath; + windowEntry->name = GetWindowTitle(hwnd); // Use window title instead of app name + if (!windowEntry->name.ok || windowEntry->name.length == 0) { + windowEntry->name = GetAppName(&filepath); // Fallback to app name + } + windowEntry->icon = GetAppIcon(&filepath); + windowEntry->windowsCount = 1; + windowEntry->windows[0] = hwnd; +} + +static void AddToSwitcher(handle hwnd) +{ + // 1. Get hosted window in case of UWP host + hwnd = GetCoreWindow(hwnd); + // 2. Basic checks for eligibility in Alt-Tab + if (!IsAltTabWindow(hwnd)) { + Print(L"(ignored) "); + Print(L"add window %s", GetAppHost(hwnd) != hwnd ? L"(uwp) " : L""); + PrintWindowX(hwnd); + return; + } + // 3. Get module filepath, abort if failed (most likely access denied) + string filepath = GetExePath(hwnd); + if (!filepath.ok) { + return; // Don't have to print error, since GetFilepath already does that + } + // 4. Check window against user-defined blacklist + string filename = StringFileName(&filepath, false); + string class = GetWindowClass(hwnd); + if (WindowIsBlacklisted(&filename, &class)) { + Print(L"(blacklisted) "); + Print(L"add window %s", GetAppHost(hwnd) != hwnd ? L"(uwp) " : L""); + PrintWindowX(hwnd); + return; + } + if (Config.showAllWindows) { + AddWindowLikeAnApp(hwnd, filepath); + } else { + AddAppOrCollectWindow(hwnd, filepath); + } } static void ActivateWindow(handle hwnd) From c74812e9c5156f127dd825c4a5c7df136322aecf Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 17:41:46 +0200 Subject: [PATCH 04/21] add GetWindowIcon() --- src/cmdtab.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 57acf62..9b7a1dc 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -424,6 +424,36 @@ static handle GetAppIcon(string *filepath) return icon; // Caller is responsible for calling DestroyIcon } +static handle GetWindowIcon(handle hwnd) +{ + // Get the window's specific icon (largest available) + HICON icon = (HICON)SendMessageW(hwnd, WM_GETICON, ICON_BIG, 0); + if (!icon) { + icon = (HICON)SendMessageW(hwnd, WM_GETICON, ICON_SMALL2, 0); + } + if (!icon) { + icon = (HICON)SendMessageW(hwnd, WM_GETICON, ICON_SMALL, 0); + } + // Else fall back to class icon + if (!icon) { + icon = (HICON)GetClassLongPtrW(hwnd, GCLP_HICON); + } + if (!icon) { + icon = (HICON)GetClassLongPtrW(hwnd, GCLP_HICONSM); + } + // Return icon if we found one + if (icon) { + return CopyIcon(icon); // we don't own the original + } + // Else fall back to app icon from executable + string filepath = GetExePath(hwnd); + if (filepath.ok) { + return GetAppIcon(&filepath); + } + // Else give up + return null; +} + static bool IsDarkModeEnabled(void) { ULONG value; @@ -812,7 +842,7 @@ static void AddWindowLikeAnApp(handle hwnd, string filepath) { if (!windowEntry->name.ok || windowEntry->name.length == 0) { windowEntry->name = GetAppName(&filepath); // Fallback to app name } - windowEntry->icon = GetAppIcon(&filepath); + windowEntry->icon = GetWindowIcon(hwnd); windowEntry->windowsCount = 1; windowEntry->windows[0] = hwnd; } From 3e46dca11bf58996b7819163b43ac06769bcc037 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 18:19:17 +0200 Subject: [PATCH 05/21] default showAllWindows = false --- src/cmdtab.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 9b7a1dc..08e4b2f 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -659,7 +659,7 @@ static struct config Config = { // cmdtab settings .fastSwitchingForWindows = true, .showSwitcherForApps = true, .showSwitcherForWindows = false, - .showAllWindows = true, + .showAllWindows = false, .wrapbump = true, .restoreOnCancel = false, // Appearance From 8ee7be658ad1a814c605604b23d4462c65ba2b7e Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 18:21:20 +0200 Subject: [PATCH 06/21] add LoadConfigFromIni() --- src/cmdtab.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/cmdtab.c b/src/cmdtab.c index 08e4b2f..fba3a30 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -677,8 +677,52 @@ static struct config Config = { // cmdtab settings }, }; +static void LoadConfigFromIni(void) +{ + u16 iniPath[MAX_PATH]; + + // Get the directory of the executable + GetModuleFileNameW(null, iniPath, countof(iniPath)); + PathCchRemoveFileSpec(iniPath, countof(iniPath)); + PathCchAppend(iniPath, countof(iniPath), L"cmdtab.ini"); + + // Check if INI file exists + if (GetFileAttributesW(iniPath) == INVALID_FILE_ATTRIBUTES) { + Print(L"No cmdtab.ini found, using default config\n"); + return; + } + + Print(L"Loading config from: %s\n", iniPath); + + // Read behavior settings + Config.switchApps = GetPrivateProfileIntW(L"Behavior", L"switchApps", Config.switchApps, iniPath); + Config.switchWindows = GetPrivateProfileIntW(L"Behavior", L"switchWindows", Config.switchWindows, iniPath); + Config.fastSwitchingForApps = GetPrivateProfileIntW(L"Behavior", L"fastSwitchingForApps", Config.fastSwitchingForApps, iniPath); + Config.fastSwitchingForWindows = GetPrivateProfileIntW(L"Behavior", L"fastSwitchingForWindows", Config.fastSwitchingForWindows, iniPath); + Config.showSwitcherForApps = GetPrivateProfileIntW(L"Behavior", L"showSwitcherForApps", Config.showSwitcherForApps, iniPath); + Config.showSwitcherForWindows = GetPrivateProfileIntW(L"Behavior", L"showSwitcherForWindows", Config.showSwitcherForWindows, iniPath); + Config.showAllWindows = GetPrivateProfileIntW(L"Behavior", L"showAllWindows", Config.showAllWindows, iniPath); + Config.wrapbump = GetPrivateProfileIntW(L"Behavior", L"wrapbump", Config.wrapbump, iniPath); + Config.restoreOnCancel = GetPrivateProfileIntW(L"Behavior", L"restoreOnCancel", Config.restoreOnCancel, iniPath); + + // Read appearance settings + Config.switcherHeight = GetPrivateProfileIntW(L"Appearance", L"switcherHeight", Config.switcherHeight, iniPath); + Config.switcherHorzMargin = GetPrivateProfileIntW(L"Appearance", L"switcherHorzMargin", Config.switcherHorzMargin, iniPath); + Config.switcherVertMargin = GetPrivateProfileIntW(L"Appearance", L"switcherVertMargin", Config.switcherVertMargin, iniPath); + Config.iconWidth = GetPrivateProfileIntW(L"Appearance", L"iconWidth", Config.iconWidth, iniPath); + Config.iconHorzPadding = GetPrivateProfileIntW(L"Appearance", L"iconHorzPadding", Config.iconHorzPadding, iniPath); + + // Read hotkey settings (scan codes) + Config.hotkeyForApps.mod = GetPrivateProfileIntW(L"Hotkeys", L"appsModifier", 0x38, iniPath); + Config.hotkeyForApps.key = GetPrivateProfileIntW(L"Hotkeys", L"appsKey", 0x0F, iniPath); + Config.hotkeyForWindows.mod = GetPrivateProfileIntW(L"Hotkeys", L"windowsModifier", 0x38, iniPath); + Config.hotkeyForWindows.key = GetPrivateProfileIntW(L"Hotkeys", L"windowsKey", 0x29, iniPath); +} + static void InitConfig(void) { + // Starting point are the hardcoded defaults in 'Config' struct initializer + LoadConfigFromIni(); // Initialize runtime-dependent settings, like those dependent on user's current kbd layout // The default key2 is scan code 0x29 (hex) / 41 (decimal) // This is the key that is physically below Esc, left of 1 and above Tab From 6c1709293cf2bdac0ef6afd00246301a3360118b Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Wed, 24 Sep 2025 18:22:01 +0200 Subject: [PATCH 07/21] add cmdtab.ini --- src/cmdtab.ini | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/cmdtab.ini diff --git a/src/cmdtab.ini b/src/cmdtab.ini new file mode 100644 index 0000000..7463957 --- /dev/null +++ b/src/cmdtab.ini @@ -0,0 +1,24 @@ +[Behavior] +switchApps=1 +switchWindows=1 +fastSwitchingForApps=0 +fastSwitchingForWindows=1 +showSwitcherForApps=1 +showSwitcherForWindows=0 +showAllWindows=1 +wrapbump=1 +restoreOnCancel=0 + +[Appearance] +switcherHeight=128 +switcherHorzMargin=24 +switcherVertMargin=32 +iconWidth=64 +iconHorzPadding=8 + +[Hotkeys] +; Scan codes in hex: 0x38=Alt, 0x0F=Tab, 0x29=Tilde/Backquote +appsModifier=56 +appsKey=15 +windowsModifier=56 +windowsKey=41 \ No newline at end of file From 4a073f87547b53c566b42a0bf0b61ce2fc3d6fbc Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 10:00:33 +0200 Subject: [PATCH 08/21] add config settings for layout in multiple rows These will eventually replace several of the old ones --- src/cmdtab.c | 25 +++++++++++++++++++++++++ src/cmdtab.ini | 8 ++++++++ 2 files changed, 33 insertions(+) diff --git a/src/cmdtab.c b/src/cmdtab.c index fba3a30..2d0e75d 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -619,6 +619,15 @@ struct config { u32 switcherVertMargin; u32 iconWidth; u32 iconHorzPadding; + // New padding options (before rowLength) TODO + u32 paddingTop; + u32 paddingBottom; + u32 paddingLeftRight; + u32 paddingBetweenRows; + u32 paddingBetweenCols; + u32 paddingTitleTop; + u32 titleHeight; + u32 rowLength; // how many icons before next row is started // Blacklist struct identifier { u16 *filename; // Filename without file extension, ex. "explorer". Can be null. @@ -669,6 +678,14 @@ static struct config Config = { // cmdtab settings .switcherVertMargin = 32, .iconWidth = 64, .iconHorzPadding = 8, + .paddingTop = 20, + .paddingBottom = 20, + .paddingLeftRight = 8, + .paddingBetweenRows = 8, + .paddingBetweenCols = 8, + .paddingTitleTop = 4, + .titleHeight = 20, + .rowLength = 6, // Blacklist .blacklist = { { null, L"ApplicationFrameWindow" }, @@ -711,6 +728,14 @@ static void LoadConfigFromIni(void) Config.switcherVertMargin = GetPrivateProfileIntW(L"Appearance", L"switcherVertMargin", Config.switcherVertMargin, iniPath); Config.iconWidth = GetPrivateProfileIntW(L"Appearance", L"iconWidth", Config.iconWidth, iniPath); Config.iconHorzPadding = GetPrivateProfileIntW(L"Appearance", L"iconHorzPadding", Config.iconHorzPadding, iniPath); + Config.paddingTop = GetPrivateProfileIntW(L"Appearance", L"paddingTop", Config.paddingTop, iniPath); + Config.paddingBottom = GetPrivateProfileIntW(L"Appearance", L"paddingBottom", Config.paddingBottom, iniPath); + Config.paddingLeftRight = GetPrivateProfileIntW(L"Appearance", L"paddingLeftRight", Config.paddingLeftRight, iniPath); + Config.paddingBetweenRows = GetPrivateProfileIntW(L"Appearance", L"paddingBetweenRows", Config.paddingBetweenRows, iniPath); + Config.paddingBetweenCols = GetPrivateProfileIntW(L"Appearance", L"paddingBetweenCols", Config.paddingBetweenCols, iniPath); + Config.paddingTitleTop = GetPrivateProfileIntW(L"Appearance", L"paddingTitleTop", Config.paddingTitleTop, iniPath); + Config.titleHeight = GetPrivateProfileIntW(L"Appearance", L"titleHeight", Config.titleHeight, iniPath); + Config.rowLength = GetPrivateProfileIntW(L"Appearance", L"rowLength", Config.rowLength, iniPath); // Read hotkey settings (scan codes) Config.hotkeyForApps.mod = GetPrivateProfileIntW(L"Hotkeys", L"appsModifier", 0x38, iniPath); diff --git a/src/cmdtab.ini b/src/cmdtab.ini index 7463957..b86f0f6 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -15,6 +15,14 @@ switcherHorzMargin=24 switcherVertMargin=32 iconWidth=64 iconHorzPadding=8 +paddingTop=20 +paddingBottom=20 +paddingLeftRight=8 +paddingBetweenRows=8 +paddingBetweenCols=8 +paddingTitleTop=4 +titleHeight=20 +rowLength=6 ; create rows of so many icons each [Hotkeys] ; Scan codes in hex: 0x38=Alt, 0x0F=Tab, 0x29=Tilde/Backquote From 17870a55840a6c463bde01656f1dc944e1958c63 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 11:57:13 +0200 Subject: [PATCH 09/21] introduce new versions of ResizeSwitcher() and RedrawSwitcher with multi-row capability --- src/cmdtab.c | 143 +++++++++++++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 2d0e75d..60fcb5c 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -656,6 +656,7 @@ static handle DrawingBitmap; // Off-screen bitmap used for double-buffered dr static RECT DrawingRect; // Size of the off-screen bitmap. static u16 Keyboard[16]; // 256 bits to track key repeat for low-level keyboard hook. static i32 MouseX, MouseY; // Mouse position, for highlighting and clicking app icons. +static u32 Width, Height; // Size of the switcher window. static struct config Config = { // cmdtab settings // Hotkeys @@ -1071,19 +1072,24 @@ static void ResizeSwitcher(void) MONITORINFO mi = { .cbSize = sizeof(MONITORINFO) }; GetMonitorInfoW(MonitorFromPoint(mousePos, MONITOR_DEFAULTTONEAREST), &mi); - u32 iconsWidth = AppsCount * Config.iconWidth; - u32 paddingWidth = AppsCount * Config.iconHorzPadding * 2; - u32 marginWidth = 2 * Config.switcherHorzMargin; + // Calculate rows and columns + u32 rows = (AppsCount + Config.rowLength - 1) / Config.rowLength; // Ceiling division + u32 cols = AppsCount < Config.rowLength ? AppsCount : Config.rowLength; - u32 w = iconsWidth + paddingWidth + marginWidth; - u32 h = Config.switcherHeight; - i32 x = mi.rcMonitor.left + (mi.rcMonitor.right - mi.rcMonitor.left - w) / 2; - i32 y = mi.rcMonitor.top + (mi.rcMonitor.bottom - mi.rcMonitor.top - h) / 2; + u32 iconsWidth = cols * Config.iconWidth; + u32 paddingWidth = 2 * Config.paddingLeftRight + (cols-1) * Config.paddingBetweenCols; + u32 iconsHeight = rows * (Config.iconWidth + Config.paddingTitleTop + Config.titleHeight); + u32 paddingHeight = Config.paddingTop + (rows-1) * Config.paddingBetweenRows + Config.paddingBottom; - MoveWindow(Switcher, x, y, w, h, false); // Yes, "MoveWindow" means "ResizeWindow" + Width = iconsWidth + paddingWidth; + Height = iconsHeight + paddingHeight; + i32 x = mi.rcMonitor.left + (mi.rcMonitor.right - mi.rcMonitor.left - Width) / 2; + i32 y = mi.rcMonitor.top + (mi.rcMonitor.bottom - mi.rcMonitor.top - Height) / 2; + + MoveWindow(Switcher, x, y, Width, Height, false); // Resize off-screen double-buffering bitmap - RECT resized = {x, y, x+w, y+h}; + RECT resized = {x, y, x+Width, y+Height}; if (!EqualRect(&DrawingRect, &resized)) { handle context = GetDC(Switcher); DeleteObject(DrawingBitmap); @@ -1099,6 +1105,7 @@ static void ResizeSwitcher(void) static void RedrawSwitcher(void) { + // relies on global vars set by ResizeSwitcher() // TODO Use 'Config.style' #define BACKGROUND RGB(32, 32, 32) // dark mode? @@ -1151,71 +1158,73 @@ static void RedrawSwitcher(void) //DeleteObject(oldPen); //DeleteObject(oldBrush); //DeleteObject(oldFont); - + RECT selectedAppTextRect; for (int i = 0; i < AppsCount; i++) { struct app *app = &Apps[i]; - // Special-case math for index 0 - i32 left0 = i == 0 ? ICON_PAD : 0; - i32 right0 = i != 0 ? ICON_PAD : 0; - - i32 icons = (ICON_PAD + ICON_WIDTH + ICON_PAD) * i; - i32 left = HORZ_PAD + left0 + icons + right0; - i32 top = VERT_PAD; - i32 width = ICON_WIDTH; - i32 height = ICON_WIDTH; - i32 right = left + width; - i32 bottom = top + height; - - //RECT icon_rect = (RECT){left, top, right, bottom}; - //bool app_is_mouseover = PtInRect(&icon_rect, (POINT){MouseX, MouseY}); - //print(L"mouse %s\n", app_is_mouseover ? L"over" : L"not over"); + // Calculate row and column for this icon + u32 row = i / Config.rowLength; + u32 col = i % Config.rowLength; + + // Calculate position based on row/column + i32 iconX = Config.paddingLeftRight + + col * (Config.iconWidth + Config.paddingBetweenCols); + i32 iconY = Config.paddingTop + + row * (Config.iconWidth + Config.paddingTitleTop + Config.titleHeight) + + (row-1) * Config.paddingBetweenRows; + + // Perhaps draw selection rectangle + if (app == SelectedApp) { + i32 selX = iconX - SEL_HORZ_OFF; + i32 selY = iconY - SEL_VERT_OFF; + i32 selW = Config.iconWidth + SEL_HORZ_OFF * 2; + i32 selH = Config.iconWidth + SEL_VERT_OFF * 2; + RoundRect(DrawingContext, selX, selY, selX + selW, selY + selH, SEL_RADIUS, SEL_RADIUS); + } + // Draw app icon and title + if (app->icon) { + DrawIconEx(DrawingContext, iconX, iconY, app->icon, Config.iconWidth, Config.iconWidth, 0, null, DI_NORMAL); + } + i32 textTop = iconY + Config.iconWidth + Config.paddingTitleTop; + i32 textBottom = textTop + Config.titleHeight; + RECT textRect = { + iconX, + textTop, + iconX + Config.iconWidth, + textBottom, + }; if (app != SelectedApp) { - // Draw only icon, with window background - DrawIconEx(DrawingContext, left, top, app->icon, width, height, 0, windowBackground, DI_NORMAL); + DrawTextW(DrawingContext, app->name.text, app->name.length, &textRect, + DT_CENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } else { - // Draw selection rectangle and icon, with selection background - RoundRect(DrawingContext, left - SEL_VERT_OFF, top - SEL_HORZ_OFF, right + SEL_VERT_OFF, bottom + SEL_HORZ_OFF, SEL_RADIUS, SEL_RADIUS); - DrawIconEx(DrawingContext, left, top, app->icon, width, height, 0, selectionBackground, DI_NORMAL); - - // Draw app name - u16 *title = app->name.text; - - // Measure text width - RECT testRect = {0}; - DrawTextW(DrawingContext, title, -1, &testRect, DT_CALCRECT | DT_SINGLELINE | DT_CENTER | DT_BOTTOM); - i32 textWidth = (testRect.right - testRect.left) + 2; // Hmm, measured size seems a teensy bit too small, so +2 - - // DrawTextW uses a destination rect for drawing - RECT textRect = {0}; - textRect.left = left; - textRect.right = right; - textRect.bottom = rect.bottom; - - i32 widthDiff = textWidth - (textRect.right - textRect.left); - if (widthDiff > 0) { - // textRect is too small for text, grow the rect - textRect.left -= (i32)ceil(widthDiff / 2.0); - textRect.right += (i32)floor(widthDiff / 2.0); - } - - textRect.bottom -= ICON_PAD; // *** this lifts the app text up a little *** - - // If the app name, when centered, extends past window bounds, adjust label position to be inside window bounds - if (textRect.right > rect.right) { - textRect.left -= (textRect.right - rect.right) + ICON_PAD; - textRect.right = rect.right - ICON_PAD; - } - if (textRect.left < 0) { - textRect.right += ICON_PAD + abs(textRect.left); - textRect.left = ICON_PAD + 0; - } - - ///* dbg */ FillRect(DrawingContext, &textRect, CreateSolidBrush(RGB(255, 0, 0))); // Check the size of the text box - DrawTextW(DrawingContext, title, -1, &textRect, DT_SINGLELINE | DT_CENTER | DT_BOTTOM); + selectedAppTextRect = textRect; } } + // draw full title for selected app + RECT testRect = {0}; + DrawTextW(DrawingContext, SelectedApp->name.text, SelectedApp->name.length, &testRect, + DT_CALCRECT | DT_SINGLELINE | DT_CENTER); // measure text width + i32 textWidth = (testRect.right - testRect.left) + 2 * Config.paddingBetweenCols; + i32 widthDiff = textWidth - (selectedAppTextRect.right - selectedAppTextRect.left); + if (widthDiff > 0) { + // textRect is too small for text, grow the rect + selectedAppTextRect.left -= (i32)ceil(widthDiff / 2.0); + selectedAppTextRect.right += (i32)floor(widthDiff / 2.0); + } + // If the app name, when centered, extends past window bounds, adjust label position to be inside window bounds + if (selectedAppTextRect.right > Width) { + selectedAppTextRect.left -= (selectedAppTextRect.right - Width); + selectedAppTextRect.right = Width; + } + if (selectedAppTextRect.left < 0) { + selectedAppTextRect.left = Config.paddingBetweenCols; + } + FillRect(DrawingContext, &selectedAppTextRect, CreateSolidBrush(BACKGROUND)); // Make room + SetTextColor(DrawingContext, HIGHLIGHT); + // SetBkMode(DrawingContext, OPAQUE); + DrawTextW(DrawingContext, SelectedApp->name.text, SelectedApp->name.length, &selectedAppTextRect, + DT_CENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } static void ShowSwitcher(void) @@ -1770,4 +1779,4 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { return RunCmdTab(hInstance, GetCommandLineW()); -} \ No newline at end of file +} From 0b4f23e9892f110138fc03298acf145e84e30c20 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 11:59:29 +0200 Subject: [PATCH 10/21] set reasonable config defaults for appearance (padding) --- src/cmdtab.c | 16 ++++++++-------- src/cmdtab.ini | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 60fcb5c..4fdd845 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -679,14 +679,14 @@ static struct config Config = { // cmdtab settings .switcherVertMargin = 32, .iconWidth = 64, .iconHorzPadding = 8, - .paddingTop = 20, - .paddingBottom = 20, - .paddingLeftRight = 8, - .paddingBetweenRows = 8, - .paddingBetweenCols = 8, - .paddingTitleTop = 4, - .titleHeight = 20, - .rowLength = 6, + .paddingTop = 48, + .paddingBottom = 48, + .paddingLeftRight = 48, + .paddingBetweenRows = 16, + .paddingBetweenCols = 32, + .paddingTitleTop = 16, + .titleHeight = 16, + .rowLength = 8, // Blacklist .blacklist = { { null, L"ApplicationFrameWindow" }, diff --git a/src/cmdtab.ini b/src/cmdtab.ini index b86f0f6..170fdab 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -15,14 +15,14 @@ switcherHorzMargin=24 switcherVertMargin=32 iconWidth=64 iconHorzPadding=8 -paddingTop=20 -paddingBottom=20 -paddingLeftRight=8 -paddingBetweenRows=8 -paddingBetweenCols=8 -paddingTitleTop=4 -titleHeight=20 -rowLength=6 ; create rows of so many icons each +paddingTop=48 +paddingBottom=48 +paddingLeftRight=48 +paddingBetweenRows=16 +paddingBetweenCols=32 +paddingTitleTop=16 +titleHeight=16 +rowLength=8 ; create rows of so many icons each [Hotkeys] ; Scan codes in hex: 0x38=Alt, 0x0F=Tab, 0x29=Tilde/Backquote From 9131b7a7922261cf6a78efb7a7781ee05b8646eb Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 12:00:50 +0200 Subject: [PATCH 11/21] remove outdated appearance config parameters --- src/cmdtab.c | 4 ---- src/cmdtab.ini | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 4fdd845..7d2fed9 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -674,11 +674,7 @@ static struct config Config = { // cmdtab settings .restoreOnCancel = false, // Appearance .darkmode = false, - .switcherHeight = 128, - .switcherHorzMargin = 24, - .switcherVertMargin = 32, .iconWidth = 64, - .iconHorzPadding = 8, .paddingTop = 48, .paddingBottom = 48, .paddingLeftRight = 48, diff --git a/src/cmdtab.ini b/src/cmdtab.ini index 170fdab..94cab58 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -10,11 +10,7 @@ wrapbump=1 restoreOnCancel=0 [Appearance] -switcherHeight=128 -switcherHorzMargin=24 -switcherVertMargin=32 iconWidth=64 -iconHorzPadding=8 paddingTop=48 paddingBottom=48 paddingLeftRight=48 From d5b8bfd704e2ab51b042320e0ed21db0ad05d2e4 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 12:01:37 +0200 Subject: [PATCH 12/21] CMakeLists.txt: add copying cmdtab.ini to build directory --- CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 17828ee..c0ea4d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,14 @@ set(BUILD_TYPE "Debug") add_executable(cmdtab WIN32 "src/cmdtab.c" "res/cmdtab.rc") +# Copy cmdtab.ini to build directory +add_custom_command(TARGET cmdtab POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/src/cmdtab.ini" + "$/cmdtab.ini" + COMMENT "Copying cmdtab.ini to build directory" +) + #set_property(TARGET ${PROJECT_NAME} PROPERTY #LINK_FLAGS /MANIFESTUAC:"level='highestAvailable' uiAccess='true'" #) From 3801a772bbedd20bc2b5db030944415a5584e784 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 12:03:01 +0200 Subject: [PATCH 13/21] cmdtab.rc: bump version to 1.6.0 --- res/cmdtab.rc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/cmdtab.rc b/res/cmdtab.rc index cdf76bc..e2fa75c 100644 --- a/res/cmdtab.rc +++ b/res/cmdtab.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO - FILEVERSION 1,5,1,0 - PRODUCTVERSION 1,5,1,0 + FILEVERSION 1,6,0,0 + PRODUCTVERSION 1,6,0,0 FILEFLAGSMASK 0x3FL #ifdef _DEBUG FILEFLAGS 0x1L @@ -22,14 +22,14 @@ VALUE "Comments", "N/A" VALUE "CompanyName", "Stian Gudmundsen Høiland" VALUE "FileDescription", "macOS-style Alt-Tab window switcher alternative" - VALUE "FileVersion", "1.5.1" + VALUE "FileVersion", "1.6.0" VALUE "InternalName", "cmdtab" VALUE "LegalCopyright", "Copyright (C) 2023 Stian Gudmundsen Høiland" VALUE "LegalTrademarks", "N/A" VALUE "OriginalFilename", "cmdtab.exe" VALUE "PrivateBuild"; "N/A" VALUE "ProductName", "cmdtab" - VALUE "ProductVersion", "1.5.1" + VALUE "ProductVersion", "1.6.0" VALUE "SpecialBuild", "N/A" } } From d822667923f16e1b3268b1e5ab702bc343b6d262 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Thu, 25 Sep 2025 12:55:17 +0200 Subject: [PATCH 14/21] README.md: update feature list; add release history --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 82210c8..230afa9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ So, you like the way Apple does it, but you're using Windows? **cmdtab** for Win - A hotkey to cycle apps (Chrome → Spotify → File Explorer) - A different hotkey to cycle windows (Chrome1 → Chrome2 → Chrome3) - Big readable app icons -- Super lightweight program (35kb) +- Super lightweight program (150kb) - Simple, clean, clear, commented C source code (easy to change/fix/extend by you/me/everyone!) - Lots of tiny, useful QoL features, like reverse, arrow keys & enter, cancel, restore, mouse support, wrap bump, quit app, close window, delayed show... - So fast! @@ -29,6 +29,7 @@ The basics of window switching are easy to understand, but why is **cmdtab** *th - Configurable hotkey to cycle apps (default Alt-Tab) - Configurable hotkey to cycle windows of the same app (default Alt-Tilde/Backquote) +- Setting `showAllWindows` to treat windows (not apps) as the first-class object, collapsing the two-level navigation into one (as Windows does it natively). - Reverse direction by holding Shift - Arrow keys are supported (selects apps in switcher) - Enter key is supported (switches to selected window) @@ -36,11 +37,11 @@ The basics of window switching are easy to understand, but why is **cmdtab** *th - Big readable app icons (not those tiny bewildering window previews) - Cancel and close the switcher by pressing Escape - Smart key capture so key presses don't unexpectedly bleed through to other apps -- Wrap bump is hard to explain but easy to feel: Try holding Alt-Tab until the end, then press Tab again—works in reverse, too! -- Press Q to quit the selected app -- Press W to close the selected window +- Wrap bump makes autorepeat stop on the last icon. Very handy! +- Press Q to quit the selected app, W to close the selected window - Option to return to the initial window when cancelling switcher by pressing Escape. This doesn't override or block Windows' native Alt-Escape hotkey - Press F4 while the switcher is open to quit **cmdtab** +- Change appearance and behavior by editing `cmdtab.ini` in the same directory as the executable (example to be found in the code tree) That's a lot of useful stuff, and the code is small! Go read it, and learn some C while you're at it. @@ -72,3 +73,47 @@ These instructions require `git`, `cmake`, and *Visual Studio* or *MSBuild*. 5. *(in developer console)* `devenv build\cmdtab.sln /build "MinSizeRel|x64"` 6. or: *(in developer console)* `msbuild build\cmdtab.sln /property:Configuration=MinSizeRel` 7. *(run cmdtab)* `build\MinSizeRel\cmdtab.exe` + +## Release history + +#### 1.6.0 (2025-09-25) + +- introduce `showAllWindows`` setting to show windows (not apps) at the top level +- introduce `rowLength` to show the resulting longer lists of icons in multiple rows +- show abbreviated titles of all apps (or windows) and full title of selected one +- introduce reading config settings from `cmdtab.ini` file in the executable's directory + +#### 1.5.1 (2024-12-12) + +- fix bug causing cmdtab to stop remembering new window activations +- fix crash when invoked with 0 windows +- change icon to vibrant blue to suit both light and dark themes +- ensure msgbox gets kbd focus + +#### 1.5.0 (2024-12-11) + +- first proper version: new build system CMake, instructions in README.md +- add resource info to cmdtab.exe +- cmdtab.exe has a new (placeholder) icon +- fix crash caused by some apps' product name (#2) +- dark mode and accent color awareness +- don't ShowWindow if already foreground +- extensive logging +- massive memory usage reduction +- persistent window activation order +- improve error handling of access failure on getting filepath +- fix text alignment bug caused by not #including math.h +- fix text alignment bias from right to left +- fix stuck modifier keys +- fix alt key / window menu inconsistency compared to Windows Alt-Tab +- impl Alt-Q to quit process +- rely on switcher being top-most window vs. active window + +#### 1.0 (some time in 2024) + +There was no formal 1.0 release (nor a 1.1, 1.2, 1.3, or 1.4). +Which of the versions to consider 1.0 is a matter of opinion. + +#### 0.1 (some time in 2023) + +Initially, cmdtab was only an idea. Then it grew and evolved. \ No newline at end of file From 741e2b0d967495fa8bbf46a2343fdc0e0a370e95 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 26 Sep 2025 09:51:22 +0200 Subject: [PATCH 15/21] cmdtab.ini: explain main settings --- src/cmdtab.ini | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/cmdtab.ini b/src/cmdtab.ini index 94cab58..5f6854d 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -1,3 +1,5 @@ +; config file for cmdtab.exe. Found in the same directory as cmdtab.exe. +; Delete this file use built-in defaults instead. [Behavior] switchApps=1 switchWindows=1 @@ -5,20 +7,20 @@ fastSwitchingForApps=0 fastSwitchingForWindows=1 showSwitcherForApps=1 showSwitcherForWindows=0 -showAllWindows=1 -wrapbump=1 +showAllWindows=1 ; bool: show one icon per window, not just one per app +wrapbump=1 ; bool: stop autorepeat on the last icon restoreOnCancel=0 [Appearance] -iconWidth=64 -paddingTop=48 -paddingBottom=48 -paddingLeftRight=48 -paddingBetweenRows=16 -paddingBetweenCols=32 -paddingTitleTop=16 -titleHeight=16 -rowLength=8 ; create rows of so many icons each +iconWidth=64 ; pixels, width and height of each icon +paddingTop=48 ; pixels, space above the top row of icons +paddingBottom=48 ; pixels, space below the bottom row of icons +paddingLeftRight=48 ; pixels, space left and right of the icon rows +paddingBetweenRows=16 ; pixels, space between rows of icons +paddingBetweenCols=32 ; pixels, space between columns of icons +paddingTitleTop=16 ; pixels, space between icon and label text +titleHeight=16 ; pixels, height of label text area +rowLength=8 ; show icons in rows of so many icons each [Hotkeys] ; Scan codes in hex: 0x38=Alt, 0x0F=Tab, 0x29=Tilde/Backquote From 16480ca0dea789b14743d5625d579c178f6e0720 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 26 Sep 2025 10:15:17 +0200 Subject: [PATCH 16/21] introduce and use Config.showAllTitles cmdtab.ini: add showAllTitles --- src/cmdtab.c | 13 +++++++++---- src/cmdtab.ini | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 7d2fed9..7fd1154 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -598,7 +598,7 @@ static void TerminateWindowProcess(handle hwnd) // cmdtab impl //============================================================================== -struct config { +struct config { // for explanations see cmdtab.ini // Hotkeys struct { u32 mod, key; } hotkeyForApps; struct { u32 mod, key; } hotkeyForWindows; @@ -609,7 +609,8 @@ struct config { bool fastSwitchingForWindows; bool showSwitcherForApps; bool showSwitcherForWindows; - bool showAllWindows; // collapse two navigation levels (apps, windows) into one + bool showAllWindows; + bool showAllTitles; bool wrapbump; bool restoreOnCancel; // Appearance @@ -670,6 +671,7 @@ static struct config Config = { // cmdtab settings .showSwitcherForApps = true, .showSwitcherForWindows = false, .showAllWindows = false, + .showAllTitles = false, .wrapbump = true, .restoreOnCancel = false, // Appearance @@ -716,6 +718,7 @@ static void LoadConfigFromIni(void) Config.showSwitcherForApps = GetPrivateProfileIntW(L"Behavior", L"showSwitcherForApps", Config.showSwitcherForApps, iniPath); Config.showSwitcherForWindows = GetPrivateProfileIntW(L"Behavior", L"showSwitcherForWindows", Config.showSwitcherForWindows, iniPath); Config.showAllWindows = GetPrivateProfileIntW(L"Behavior", L"showAllWindows", Config.showAllWindows, iniPath); + Config.showAllTitles = GetPrivateProfileIntW(L"Behavior", L"showAllTitles", Config.showAllTitles, iniPath); Config.wrapbump = GetPrivateProfileIntW(L"Behavior", L"wrapbump", Config.wrapbump, iniPath); Config.restoreOnCancel = GetPrivateProfileIntW(L"Behavior", L"restoreOnCancel", Config.restoreOnCancel, iniPath); @@ -1191,8 +1194,10 @@ static void RedrawSwitcher(void) textBottom, }; if (app != SelectedApp) { - DrawTextW(DrawingContext, app->name.text, app->name.length, &textRect, - DT_CENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + if (Config.showAllTitles) { + DrawTextW(DrawingContext, app->name.text, app->name.length, &textRect, + DT_CENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + } } else { selectedAppTextRect = textRect; } diff --git a/src/cmdtab.ini b/src/cmdtab.ini index 5f6854d..c8c8376 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -8,6 +8,7 @@ fastSwitchingForWindows=1 showSwitcherForApps=1 showSwitcherForWindows=0 showAllWindows=1 ; bool: show one icon per window, not just one per app +showAllTitles=1 ; bool: show app or window label also for unselected entries wrapbump=1 ; bool: stop autorepeat on the last icon restoreOnCancel=0 From 359c529310c104f10931cb13c365924b0da6228b Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 26 Sep 2025 10:17:03 +0200 Subject: [PATCH 17/21] remove outdated Config settings --- src/cmdtab.c | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 7fd1154..5984893 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -615,12 +615,7 @@ struct config { // for explanations see cmdtab.ini bool restoreOnCancel; // Appearance bool darkmode; - u32 switcherHeight; - u32 switcherHorzMargin; - u32 switcherVertMargin; u32 iconWidth; - u32 iconHorzPadding; - // New padding options (before rowLength) TODO u32 paddingTop; u32 paddingBottom; u32 paddingLeftRight; @@ -684,7 +679,7 @@ static struct config Config = { // cmdtab settings .paddingBetweenCols = 32, .paddingTitleTop = 16, .titleHeight = 16, - .rowLength = 8, + .rowLength = 20, // Blacklist .blacklist = { { null, L"ApplicationFrameWindow" }, @@ -723,11 +718,7 @@ static void LoadConfigFromIni(void) Config.restoreOnCancel = GetPrivateProfileIntW(L"Behavior", L"restoreOnCancel", Config.restoreOnCancel, iniPath); // Read appearance settings - Config.switcherHeight = GetPrivateProfileIntW(L"Appearance", L"switcherHeight", Config.switcherHeight, iniPath); - Config.switcherHorzMargin = GetPrivateProfileIntW(L"Appearance", L"switcherHorzMargin", Config.switcherHorzMargin, iniPath); - Config.switcherVertMargin = GetPrivateProfileIntW(L"Appearance", L"switcherVertMargin", Config.switcherVertMargin, iniPath); Config.iconWidth = GetPrivateProfileIntW(L"Appearance", L"iconWidth", Config.iconWidth, iniPath); - Config.iconHorzPadding = GetPrivateProfileIntW(L"Appearance", L"iconHorzPadding", Config.iconHorzPadding, iniPath); Config.paddingTop = GetPrivateProfileIntW(L"Appearance", L"paddingTop", Config.paddingTop, iniPath); Config.paddingBottom = GetPrivateProfileIntW(L"Appearance", L"paddingBottom", Config.paddingBottom, iniPath); Config.paddingLeftRight = GetPrivateProfileIntW(L"Appearance", L"paddingLeftRight", Config.paddingLeftRight, iniPath); From e5047391e8be7dcec9c2b403463698d53f633a83 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 26 Sep 2025 10:28:27 +0200 Subject: [PATCH 18/21] fix vertical layout logic --- src/cmdtab.c | 5 ++--- src/cmdtab.ini | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index 5984893..ca3c153 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -673,7 +673,7 @@ static struct config Config = { // cmdtab settings .darkmode = false, .iconWidth = 64, .paddingTop = 48, - .paddingBottom = 48, + .paddingBottom = 32, .paddingLeftRight = 48, .paddingBetweenRows = 16, .paddingBetweenCols = 32, @@ -1160,8 +1160,7 @@ static void RedrawSwitcher(void) i32 iconX = Config.paddingLeftRight + col * (Config.iconWidth + Config.paddingBetweenCols); i32 iconY = Config.paddingTop + - row * (Config.iconWidth + Config.paddingTitleTop + Config.titleHeight) + - (row-1) * Config.paddingBetweenRows; + row * (Config.iconWidth + Config.paddingTitleTop + Config.titleHeight + Config.paddingBetweenRows); // Perhaps draw selection rectangle if (app == SelectedApp) { diff --git a/src/cmdtab.ini b/src/cmdtab.ini index c8c8376..76aa114 100644 --- a/src/cmdtab.ini +++ b/src/cmdtab.ini @@ -15,7 +15,7 @@ restoreOnCancel=0 [Appearance] iconWidth=64 ; pixels, width and height of each icon paddingTop=48 ; pixels, space above the top row of icons -paddingBottom=48 ; pixels, space below the bottom row of icons +paddingBottom=32 ; pixels, space below the bottom row of icons paddingLeftRight=48 ; pixels, space left and right of the icon rows paddingBetweenRows=16 ; pixels, space between rows of icons paddingBetweenCols=32 ; pixels, space between columns of icons From 7c5205bd095a21f4b84b71f402b9701cbe070767 Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 26 Sep 2025 11:26:16 +0200 Subject: [PATCH 19/21] update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 230afa9..0c9fa4b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # cmdtab -Fast and lightweight macOS-style Alt-Tab app/window switcher replacement for Windows, written in the Lord's language, C. +Fast and lightweight macOS-style (or Windows XP style, if you want that) Alt-Tab app/window switcher replacement for Windows, written in the Lord's language, C. ![cmdtab-screenshot](https://github.com/stianhoiland/cmdtab/assets/2081712/ec5d0d61-005f-4123-b191-8d5b49d1f7db) @@ -76,12 +76,12 @@ These instructions require `git`, `cmake`, and *Visual Studio* or *MSBuild*. ## Release history -#### 1.6.0 (2025-09-25) +#### 1.6.0 (2025-09-26) -- introduce `showAllWindows`` setting to show windows (not apps) at the top level -- introduce `rowLength` to show the resulting longer lists of icons in multiple rows -- show abbreviated titles of all apps (or windows) and full title of selected one -- introduce reading config settings from `cmdtab.ini` file in the executable's directory +- `showAllWindows` setting: show windows (not apps) at the top level +- `rowLength` setting: show the resulting longer lists of icons in multiple rows +- `showAllTitles` setting: show abbreviated titles of all apps (or windows) and full title of selected one +- `cmdtab.ini` file: read config settings from file in the executable's directory #### 1.5.1 (2024-12-12) From 3e637b6ba4b514fc7bca7d5aa267a39e1e454a7e Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 16 Jan 2026 11:30:50 +0100 Subject: [PATCH 20/21] cmdtab.c: activated windows no longer receives an AltUp key event. --- src/cmdtab.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cmdtab.c b/src/cmdtab.c index ca3c153..cf9a93f 100644 --- a/src/cmdtab.c +++ b/src/cmdtab.c @@ -1372,13 +1372,17 @@ static LRESULT CALLBACK KeyboardHookProcedure(int code, WPARAM wparam, LPARAM lp if (mod1Up || mod2Up) { // Alt keyup - switch to selected window - // Always pass/never consume mod key up, because: - // 1) cmdtab never hooks mod keydowns - // 2) cmdtab hooks mod keyups, but always passes it through on pain of getting a stuck Alt key because of point 1 + // Synthesize Alt-up to unstick the key BEFORE switching windows + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = keyCode; // VK_MENU/VK_LMENU/VK_RMENU + input.ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(1, &input, sizeof(INPUT)); + ReceiveLastInputEvent(); ShowWindowX(*SelectedWindow); CancelSwitcher(false); - goto passMessage; // See note above on "Alt keyup" on why I don't consume this message + goto consumeMessage; } else { bool keyRepeat = SetKey(keyCode, keyDown); // Manually track key repeat From d0a2f131fe1e8e93a4369446683c14db27aeafee Mon Sep 17 00:00:00 2001 From: Lutz Prechelt Date: Fri, 16 Jan 2026 11:33:59 +0100 Subject: [PATCH 21/21] v1.6.1 --- README.md | 4 ++++ res/cmdtab.rc | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c9fa4b..6dea382 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,10 @@ These instructions require `git`, `cmake`, and *Visual Studio* or *MSBuild*. ## Release history +#### 1.6.1 (2025-10-02) + +- `KeyboardHookProcedure`: fix: activated window no longer receives an AltUp key event + #### 1.6.0 (2025-09-26) - `showAllWindows` setting: show windows (not apps) at the top level diff --git a/res/cmdtab.rc b/res/cmdtab.rc index e2fa75c..ffccdde 100644 --- a/res/cmdtab.rc +++ b/res/cmdtab.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO - FILEVERSION 1,6,0,0 - PRODUCTVERSION 1,6,0,0 + FILEVERSION 1,6,1,0 + PRODUCTVERSION 1,6,1,0 FILEFLAGSMASK 0x3FL #ifdef _DEBUG FILEFLAGS 0x1L @@ -22,14 +22,14 @@ VALUE "Comments", "N/A" VALUE "CompanyName", "Stian Gudmundsen Høiland" VALUE "FileDescription", "macOS-style Alt-Tab window switcher alternative" - VALUE "FileVersion", "1.6.0" + VALUE "FileVersion", "1.6.1" VALUE "InternalName", "cmdtab" VALUE "LegalCopyright", "Copyright (C) 2023 Stian Gudmundsen Høiland" VALUE "LegalTrademarks", "N/A" VALUE "OriginalFilename", "cmdtab.exe" VALUE "PrivateBuild"; "N/A" VALUE "ProductName", "cmdtab" - VALUE "ProductVersion", "1.6.0" + VALUE "ProductVersion", "1.6.1" VALUE "SpecialBuild", "N/A" } }