diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 13700cf..7e7d1ed 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -35,8 +35,7 @@ jobs: if: steps.src_changes.outputs.has_src_changes == 'true' uses: actions/upload-artifact@v4 with: - name: be-there-dev-${{ github.sha }} + name: harken-dev-${{ github.sha }} path: | - dist\be-there-source-dev.zip - dist\be-there-dev-win64.zip - dist\be-there.exe + dist\harken-source-dev.zip + dist\harken.exe diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3792685..8b01bda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,5 @@ jobs: tag_name: ${{ env.VERSION }} prerelease: ${{ env.PRERELEASE }} files: | - dist/be-there-source-${{ env.VERSION }}.zip - dist/be-there-${{ env.VERSION }}-win64.zip - dist/be-there.exe + dist/harken-source-${{ env.VERSION }}.zip + dist/harken.exe diff --git a/.gitignore b/.gitignore index 65551be..ea18efd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ PLAN.md -config/config.json +config/harken.toml dist/ ahk/ +*_PLAN.md diff --git a/AGENTS.md b/AGENTS.md index 6f4344f..8e50ef7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,10 +7,10 @@ No Cursor rules found in `.cursor/rules/` or `.cursorrules`. No Copilot instructions found in `.github/copilot-instructions.md`. ## Quick Context -- Entry point: `be-there.ahk`. +- Entry point: `harken.ahk`. - Source modules: `src/` (hotkeys, window management, UI helpers, config loader). - Tools: `tools/` (build script, inspector utilities). -- Config example: `config/config.example.json` (user config lives outside the repo). +- Config example: `config/config.example.toml` (user config lives outside the repo). ## Build / Lint / Test This repo does not currently use a formal test or lint framework. @@ -25,13 +25,18 @@ Dev build (CI): - GitHub Actions runs `tools/build_release.ps1 -Version dev` on `main`/`dev` when `src/` changes. Single test / focused run: -- No unit test runner exists. Use manual verification steps (launch `be-there.ahk`, validate +- No unit test runner exists. Use manual verification steps (launch `harken.ahk`, validate hotkeys, reload flow, and Command Overlay) or run targeted helper tools in `tools/`. +- Focused validation (pick relevant items): + - Launch `harken.ahk` with a clean config and verify: super hotkeys, window cycling, overlay. + - If virtual desktops touched: test `super+alt+h/l`, mapped desktop hotkeys, and tray indicator. ## Code Style Guidelines ### AutoHotkey version and file headers - Use AutoHotkey v2 syntax and conventions. -- Keep `#Requires AutoHotkey v2.0` in entry points and new top-level scripts. +- Virtual desktop integration requires AutoHotkey v2.1 alpha (VD.ahk dependency). +- Keep `#Requires AutoHotkey v2.0` in entry points and new top-level scripts unless a file + specifically requires v2.1 alpha features. - Keep `#Include` statements at the top and ordered by dependency. ### Formatting @@ -55,32 +60,40 @@ Single test / focused run: ### Imports and module structure - Keep modules focused by domain (hotkeys, window management, utilities). - Avoid circular dependencies; shared helpers should live in `src/lib/`. -- `be-there.ahk` should remain the only top-level orchestrator. +- `harken.ahk` should remain the only top-level orchestrator. ### Error handling - Use `try`/`catch` for file IO and JSON parsing; return structured errors where possible. - Use clear, user-facing error messages for missing dependencies or invalid config. - For config validation, return error arrays and handle them in the entry point. - Avoid throwing for normal control flow. +- Guard `WinGetID("A")` calls when no active window is possible (use `try`/`catch`). ### Config handling - Keep user configuration separate from core behavior. - Do not edit or delete user config without backup. - Validate config with schema before registering hotkeys. - When adding config keys, update: - - `DefaultConfig()` in `be-there.ahk` + - `DefaultConfig()` in `harken.ahk` - Schema in `src/lib/config_loader.ahk` - - `config/config.example.json` + - `config/config.example.toml` - `README.md` +- If adding virtual desktop hotkeys: + - Normalize new config formats in `NormalizeVirtualDesktopConfig`. + - Update debug logs and config validation for duplicate hotkeys. ### Hotkeys and window behavior - Keep hotkey registration centralized under `src/hotkeys/`. - Avoid direct global state unless required; prefer explicit `global` declarations when needed. - For window manipulation, consider edge cases with elevated windows and multiple monitors. +- Be careful with modifier ordering: use `HotIf` guards and wildcard hotkeys when needed. ### UI helpers - GUI helpers (overlays, inspectors) should remain non-blocking and lightweight. - Prefer explicit refresh actions instead of continuous loops when possible. +- Command overlay behavior: + - Normal mode: `super + /` shows temporary overlay; any key hides it. + - Command/move modes: overlay stays visible and is centered on screen. ### Third-party code - Keep third-party code in `src/lib/` and document licensing in `LICENSES/`. @@ -94,24 +107,37 @@ Single test / focused run: - Keep `README.md` and `AGENTS.md` aligned with current behavior. ## Suggested Manual Checks -- Launch `be-there.ahk` with a clean `config.json` and verify hotkeys. +- Launch `harken.ahk` with a clean `harken.toml` and verify hotkeys. - Validate reload flow (normal hotkey and command mode). - Confirm Command Overlay and helper tools still open and update. -- If touching config schema, ensure errors log correctly in `~/.config/be-there/config.errors.log`. +- If touching config schema, ensure errors log correctly in `%APPDATA%\harken\config.errors.log`. +- If touching tray indicator: confirm tray icon updates on desktop change. +- If touching cycling: verify `super+c` (all desktops) and `super+shift+c` (current desktop). ## Paths and Layout Notes -- Main script: `be-there.ahk`. +- Main script: `harken.ahk`. - Config loader: `src/lib/config_loader.ahk` (schema + validation). - JSON parsing: `src/lib/JXON.ahk`. - Window manager: `src/lib/window_manager.ahk`. - Hotkeys: `src/hotkeys/*.ahk`. +- Virtual desktop helpers: `src/lib/virtual_desktop.ahk` + `src/lib/VD.ahk`. ## Build Artifacts -- `dist/be-there.exe` -- `dist/be-there-source-.zip` -- `dist/be-there--win64.zip` +- `dist/harken.exe` +- `dist/harken-source-.zip` +- `dist/harken--win64.zip` ## When In Doubt - Keep behavior consistent with existing hotkeys and overlays. - Document any new public functions or configuration keys. - Favor explicitness over cleverness. + +## Recent Project-Specific Notes +- Debug logs: + - `virtual_desktop.debug_cycle` writes `%APPDATA%\harken\cycle.debug.log`. + - `virtual_desktop.debug_hotkeys` writes `%APPDATA%\harken\vd.hotkeys.log` and `vd.actions.log`. + - Logs reset on startup when debug flags are enabled. +- Tray indicator: + - `virtual_desktop.tray_indicator` draws `{current}/{total}` on the existing tray icon. + - Tooltip uses `virtual_desktop.tray_format`. +- Window cycling across desktops uses a cache to include off-desktop windows. diff --git a/LICENSES/VD.ahk-LICENSE.md b/LICENSES/VD.ahk-LICENSE.md new file mode 100644 index 0000000..35e86cf --- /dev/null +++ b/LICENSES/VD.ahk-LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 [Fu Pei Jiang](https://github.com/FuPeiJiang) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index fb23275..896ddf4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# be-there +# Harken A window manager written in AutoHotkey v2. -The aim is a low-friction workflow: a single super modifier, mnemonic app keys, and fast window actions. Alt+Tab and Win+Tab still work, but you will hardly use them. +Harken allows a keyboard-centered workflow on Windows: a single super modifier, mnemonic app keys, and fast window actions. Alt+Tab and Win+Tab still work, but you will hardly use them. ## Contents - [Overview](#overview) @@ -24,6 +24,20 @@ The aim is a low-friction workflow: a single super modifier, mnemonic app keys, ### Features +- Launch-or-focus your most common programs with the hotkeys you assign in your config file. +- Focused tile includes border highlight that can be customized, including per app, window class, or window title. +- Snap tiles to grid and cycle through various positions with subsequent key presses. +- Freely move tiles with your keyboard. +- Resize tiles and their edges with your keyboard. +- Directional focus changes with vim-like motions. +- Cycle stacked tiles with `super + [` and `super + ]`. +- Cycle focus between tiles of the same program with `super + c`. +- Show a hotkey menu with `super + /`. +- Virtual desktops (native) + - Navigate between desktops with better hotkeys + - Assign custom hotkeys for the index-based virtual desktops. + + Launch-or-focus a program with `super + [letter]`, or directionally change window focus with `alt + h/l/j/k` (left, right, down, up) and `alt + [` / `alt + ]` for back/forward in a stack. ![Alt text](docs/assets/focus.gif) @@ -52,10 +66,15 @@ Use the "window switcher" (like powertoys window walker) with `super + w`. Other - `super + alt` sends `ctrl + tab` (configurable via `global_hotkeys`) - `super + c` cycle through windows of the same app +- `super + shift + c` cycle through windows of the same app on the current desktop +- `super + alt + h/l` switch to previous/next virtual desktop +- `super + alt + shift + h/l` move the active window to previous/next desktop (follow) - `super + w` open Window Selector (fuzzy find open windows) - `alt + h/l` move window focus left/right - `alt + j/k` move window focus down/up (non-stacked) - `alt + [` / `alt + ]` move window focus forward/back through stacked windows +- `super + alt + h/l` to move between desktops +- `super + shift + alt + h/l` send current tile to adjacent desktop Enter Command Mode with `super + ;`. - `r` to reload program/config @@ -64,38 +83,89 @@ Enter Command Mode with `super + ;`. - `n` toggles the command overlay on or off - `i` opens the [Helper Utility](#helper-utility) +## TODO + +- Clarify and work on areas of state + - saving layouts + - per app configs determining where things go (virtual desktop destination is supported now) +- Possibly rework the config schema. It's hectic. + ## Configuration ### Quick start -- Start the program and enter command mode with `super + ;`. The binary is not currently signed and you will be warned by Windows. Clone and use `main.ahk` directly as an alternative. -- Press `e` to open the config file. You can also find it manually in `~/.config/be-there/config.json`. +- If not using the binary, make sure to install AutoHotKey 2.1-alpha18 or newer. +- Start the program and enter command mode with `super + ;`. The binary is not currently signed and you will be warned by Windows. Clone and use `harken.ahk` directly as an alternative. +- The program might fail on first run? Probably something to do with the config. For now you can create the config first to _maybe_ avoid the initial-crash scenario. +- Press `e` to open the config file. You can also find it manually in `~/.config/harken/harken.toml` as it will be created on first run. - After making changes to your config you can reload the config (the entire program, actually) with `r` while in command mode. -### Default Config Keys -- `super_key`: key or modifier chord used as the super modifier (e.g., `CapsLock`, `Ctrl+Shift+Alt`). -- `apps`: list of app bindings with `hotkey`, `win_title`, and `run` command. -- `apps[].run_paths`: optional list of directories to search for the executable. -- `global_hotkeys`: array of scoped hotkey bindings (set `target_exes` empty for global use). -- `window`: resize/move steps and hotkeys (including move mode). -- `window_selector`: Window Selector settings (hotkey, match fields, display limits). -- `window_manager`: grid size, margins, gaps, and ignored window classes. -- `directional_focus`: directional focus settings (stacked threshold, stack tolerance, topmost preference, last-stacked preference, frontmost guard, perpendicular overlap min, cross-monitor, debug). -- `focus_border`: overlay appearance and update interval. -- `helper`: command overlay settings. -- `reload`: hotkey and file watch settings for config reload. - -### Window Matching -- `apps[].win_title` accepts standard AutoHotkey window selectors. -- Common forms: plain title text, `ahk_exe `, `ahk_class `, `ahk_pid `. -- Use `ahk_exe` for stable matching when window titles change (e.g., tabs/documents). -- Plain title text supports AutoHotkey's standard title matching and wildcards (e.g., `* - Notepad`). -- `ahk_exe`, `ahk_class`, and `ahk_pid` are exact matches; wildcards/regex are not supported today but could be added later. - -### Path Expansion -- `apps[].run_paths` supports environment variables like `%APPDATA%` and `%LOCALAPPDATA%`. -- `~\` expands to your user profile (e.g., `~\AppData\Roaming`). +### All default keybindings + +#### Window management (Super) + +| Shortcut | Action | +| --- | --- | +| `super + /` | Show command overlay (temporary) | +| `super + w` | Window selector (window walker) | +| `super + c` | Cycle app windows on current desktop | +| `super + shift + c` | Cycle app windows across desktops (not working) | +| `super + space` | Center width cycle | +| `super + m` | Maximize/un-maximize | +| `super + q` | Close window | +| `super + Left/Right/Up/Down` | Resize window and snap to grids | +| `super + shift + h/j/k/l` | Resize centered | +| `super + ctrl + h/j/k/l` | Move window | +| `super` (double tap) | Toggle move mode | + +#### Window management (Move mode) + +| Shortcut | Action | +| --- | --- | +| `h/j/k/l` | Move window | +| `Esc` or `super` | Exit move mode | + +#### Focus navigation (Alt) + +| Shortcut | Action | +| --- | --- | +| `alt + h/l` | Focus left/right | +| `alt + j/k` | Focus down/up | +| `alt + [` / `alt + ]` | Cycle stacked (prev/next) | + +#### Virtual desktops + +| Shortcut | Action | +| --- | --- | +| `super + alt + h/l` | Previous/next desktop | +| `super + alt + shift + h/l` | Move window to previous/next desktop (follow) | +| `super + alt + ` | Go to mapped desktop (`[[virtual_desktop.]]`) | +| `super + alt + shift + ` | Move window to mapped desktop (follow) | + +#### Apps (defaults) + +These are examples for the launch-or-focus keybindings. + +| Shortcut | Action | +| --- | --- | +| `super + e` | Files (`explorer.exe`) | +| `super + v` | Editor (`Code.exe`) | +| `super + s` | Terminal (`WindowsTerminal.exe`) | +| `super + n` | Notes (`notepad++.exe`) | + +#### Command mode + +| Shortcut | Action | +| --- | --- | +| `super + ;` | Enter command mode | +| `r` | Reload program/config | +| `e` | Open config file | +| `w` | Open a new window for the active app | +| `n` | Toggle command overlay | +| `i` | Open window inspector | +| `Esc` | Exit command mode | + ### Helper Utility - `tools/window_inspector.ahk` lists active window titles, exe names, classes, and PIDs. @@ -103,23 +173,26 @@ Enter Command Mode with `super + ;`. - In Command Mode, press `i` to launch the window inspector. - Use Refresh to update the list; Copy Selected/All or Export to save results. -## Known Limitations +## Limitations - This has not been tested with multi-monitor setups or much outside of ultra-wide monitors. +- Virtual desktop integration requires AutoHotkey v2.1-alpha-18 or later. - Some apps (e.g., Discord) launch via `Update.exe` and keep versioned subfolders, which makes auto-resolution unreliable for launching or focusing more challenging. - For some apps that minimize or close to the system tray, it's recommended you disable that in the program. Otherwise you can try to set `apps[].run` to a stable full path (or use `run_paths`) in your config. -- Windows with elevated permissions may ignore be-there hotkeys unless be-there is run as Administrator. +- Windows with elevated permissions may ignore Harken hotkeys unless Harken is run as Administrator. ## Third-Party - JXON (AHK v2 JSON serializer) from https://github.com/TheArkive/JXON_ahk2 - License: `LICENSES/JXON_ahk2-LICENSE.md` +- VD.ahk from https://github.com/FuPeiJiang/VD.ahk + - License: `LICENSES/VD.ahk-LICENSE.md` ## Similar tools and inspirations -For this project I was primarily inspired by what I was able to accomplish with [Raycast](https://www.raycast.com/) on macOS. Between [Karabiner](https://karabiner-elements.pqrs.org/), Raycast, and [HammerSpoon](https://www.hammerspoon.org/) one could achieve all of `be-there` and more on macOS. I needed to move back to Windows for work, and I wanted a way to use the same flow on Windows that I had become accustomed to on macOS. +For this project I was primarily inspired by what I was able to accomplish with [Raycast](https://www.raycast.com/) on macOS. Between [Karabiner](https://karabiner-elements.pqrs.org/), Raycast, and [HammerSpoon](https://www.hammerspoon.org/) one could achieve all of Harken and more on macOS. I needed to move back to windows for work, and I wanted a way to use the same flow on Windows that I had become accustomed to on macOS. Other macOS tools that I tried for more than five minutes were [AeroSpace](https://github.com/nikitabobko/AeroSpace) and [Loop](https://github.com/MrKai77/Loop). -The foundation of be-there was built upon [this reddit post](https://old.reddit.com/r/AutoHotkey/comments/17qv594/window_management_tool/), shared by u/CrashKZ -- Thanks to [/u/plankoe](https://old.reddit.com/user/plankoe) for the pieces they shared, too. +The foundation of Harken was built upon [this reddit post](https://old.reddit.com/r/AutoHotkey/comments/17qv594/window_management_tool/), shared by u/CrashKZ -- Thanks to [/u/plankoe](https://old.reddit.com/user/plankoe) for their initial contributions, too. ### [FancyZones](https://learn.microsoft.com/en-us/windows/powertoys/fancyzones) diff --git a/be-there.ahk b/be-there.ahk deleted file mode 100644 index 37e8220..0000000 --- a/be-there.ahk +++ /dev/null @@ -1,299 +0,0 @@ -#Requires AutoHotkey v2.0 -#SingleInstance - -#Include src/lib/JXON.ahk -#Include src/lib/config_loader.ahk -#Include src/lib/state_store.ahk -#Include src/lib/focus_or_run.ahk -#Include src/lib/command_toast.ahk -#Include src/lib/window_inspector.ahk - -config_dir := GetConfigDir() -DirCreate(config_dir) -global config_path := config_dir "\config.json" -EnsureConfigExists(config_path, DefaultConfig()) -config_result := LoadConfig(config_path, DefaultConfig()) -global Config := config_result["config"] -config_errors := config_result["errors"] -if (config_errors.Length) { - LogConfigErrors(config_errors, config_dir "\config.errors.log", config_path) - ExitApp -} -global AppState := LoadState() -InitCommandToast() - -super_key := Config["super_key"] - -Hotkey("~" super_key, (*) => OnSuperKeyDown()) -if (super_key = "CapsLock") - SetCapsLockState "AlwaysOff" - -reload_config := Config["reload"] -if reload_config["enabled"] { - reload_hotkey := reload_config["hotkey"] - reload_mode_hotkey := reload_config["mode_hotkey"] - reload_mode_enabled := reload_config["mode_enabled"] - reload_mode_timeout := reload_config["mode_timeout_ms"] - global reload_mode_active := false - - if reload_config["super_key_required"] { - HotIf (*) => GetKeyState(super_key, "P") - Hotkey(reload_hotkey, (*) => Reload()) - HotIf - } else { - Hotkey(reload_hotkey, (*) => Reload()) - } - - if reload_mode_enabled { - Hotkey(super_key " & " reload_mode_hotkey, (*) => ActivateReloadMode(reload_mode_timeout)) - HotIf ReloadModeActive - Hotkey(reload_hotkey, (*) => ExecuteCommand(() => Reload())) - Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) - Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) - Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) - Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) - Hotkey("Esc", ClearReloadMode) - HotIf - } -} - -if reload_config["enabled"] && reload_config["watch_enabled"] - StartConfigWatcher(config_path, reload_config["watch_interval_ms"]) - -SetWinDelay(-1) - -; This variable is the modifier key for window navigation hotkeys. -window_nav_modifier := super_key - -#Include src/lib/window_manager.ahk -#Include src/lib/directional_focus.ahk -#Include src/lib/focus_border.ahk -#Include src/lib/window_walker.ahk -#Include src/hotkeys/global_hotkey.ahk -#Include src/hotkeys/apps.ahk -#Include src/hotkeys/window.ahk -#Include src/hotkeys/directional_focus.ahk -#Include src/hotkeys/window_walker.ahk -#Include src/hotkeys/unbound.ahk - -DefaultConfig() { - return Map( - "config_version", 1, - "super_key", "CapsLock", - "apps", [ - Map("id", "files", "hotkey", "e", "win_title", "ahk_exe explorer.exe", "run", "explorer"), - Map("id", "editor", "hotkey", "v", "win_title", "ahk_exe Code.exe", "run", "code"), - Map("id", "terminal", "hotkey", "s", "win_title", "ahk_exe WindowsTerminal.exe", "run", "wt"), - Map("id", "notes", "hotkey", "n", "win_title", "ahk_exe notepad++.exe", "run", "notepad++") - ], - "global_hotkeys", [ - Map( - "enabled", true, - "hotkey", "Alt", - "target_exes", [], - "send_keys", "^{Tab}" - ) - ], - "window", Map( - "resize_step", 20, - "move_step", 20, - "super_double_tap_ms", 300, - "move_mode", Map( - "enable", true, - "cancel_key", "Esc" - ), - "cycle_app_windows_hotkey", "c", - "center_width_cycle_hotkey", "Space" - ), - "window_selector", Map( - "enabled", true, - "hotkey", "w", - "max_results", 12, - "title_preview_len", 60, - "match_title", true, - "match_exe", true, - "include_minimized", true, - "close_on_focus_loss", true - ), - "window_manager", Map( - "grid_size", 3, - "margins", Map( - "top", 6, - "left", 4, - "right", 4 - ), - "gap_px", 0, - "exceptions_regex", "(Shell_TrayWnd|Shell_SecondaryTrayWnd|WorkerW|XamlExplorerHostIslandWindow)" - ), - "directional_focus", Map( - "enabled", true, - "stacked_overlap_threshold", 0.5, - "stack_tolerance_px", 25, - "prefer_topmost", true, - "prefer_last_stacked", true, - "frontmost_guard_px", 200, - "perpendicular_overlap_min", 0.2, - "cross_monitor", false, - "debug_enabled", false - ), - "focus_border", Map( - "enabled", true, - "border_color", "#357EC7", - "move_mode_color", "#2ECC71", - "border_thickness", 4, - "corner_radius", 12, - "update_interval_ms", 20 - ), - "helper", Map( - "enabled", true, - "overlay_opacity", 200 - ), - "reload", Map( - "enabled", true, - "hotkey", "r", - "super_key_required", true, - "watch_enabled", false, - "watch_interval_ms", 1000, - "mode_enabled", true, - "mode_hotkey", ";", - "mode_timeout_ms", 20000 - ) - ) -} - -LogConfigErrors(errors, log_path, config_path := "") { - DirCreate(GetConfigDir()) - header := "[" A_Now "] Config errors:" "`n" - FileAppend(header, log_path) - for _, err in errors { - FileAppend("- " err "`n", log_path) - } - FileAppend("`n", log_path) - - summary := "Config errors detected.`n" - summary .= "Log: " log_path "`n" - if config_path - summary .= "Config: " config_path "`n" - - details := summary "`n" - for _, err in errors { - details .= "- " err "`n" - } - - ShowConfigErrorsGui(details, log_path) -} - -ShowConfigErrorsGui(details, log_path) { - gui := Gui("+AlwaysOnTop", "be-there Config Errors") - gui.SetFont("s10", "Segoe UI") - edit := gui.AddEdit("w760 r18 ReadOnly", details) - open_btn := gui.AddButton("xm y+10 w120", "Open Log") - open_btn.OnEvent("Click", (*) => Run(log_path)) - close_btn := gui.AddButton("x+10 yp w120", "Close") - close_btn.OnEvent("Click", (*) => gui.Destroy()) - gui.Show() -} - -EnsureConfigExists(config_path, default_config) { - if FileExist(config_path) - return - - config_text := Jxon_Dump(default_config, 2) - FileAppend(config_text, config_path) -} - -GetConfigDir() { - user_profile := EnvGet("USERPROFILE") - if !user_profile - user_profile := A_ScriptDir - return user_profile "\.config\be-there" -} - -StartConfigWatcher(path, interval_ms := 1000) { - global config_watch_mtime := "" - if FileExist(path) - config_watch_mtime := FileGetTime(path, "M") - - SetTimer((*) => CheckConfigWatcher(path), interval_ms) -} - -CheckConfigWatcher(path) { - global config_watch_mtime - if !FileExist(path) - return - - current := FileGetTime(path, "M") - if (config_watch_mtime = "") { - config_watch_mtime := current - return - } - if (current != config_watch_mtime) - Reload() -} - -ActivateReloadMode(timeout_ms := 1500) { - global reload_mode_active := true - global reload_mode_activated_at := A_TickCount - SetTimer(ClearReloadMode, 0) - SetTimer(ClearReloadMode, -timeout_ms) - UpdateCommandToastVisibility() -} - -ClearReloadMode(*) { - global reload_mode_active := false - UpdateCommandToastVisibility() -} - -ReloadModeActive(*) { - global reload_mode_active - return reload_mode_active -} - -ExecuteCommand(callback) { - callback.Call() - ClearReloadMode() -} - -OnSuperKeyDown() { - UpdateCommandToastVisibility() -} - -OpenWindowInspector() { - ShowWindowInspector() -} - -OpenConfigFile() { - global config_path - if !config_path - return - if FileExist(config_path) - Run(config_path) -} - -OpenNewWindowForActiveApp() { - hwnd := WinExist("A") - if !hwnd - return - - exe := WinGetProcessName("ahk_id " hwnd) - if !exe - return - - app_config := FindAppConfigByExe(exe) - if (app_config is Map) { - RunResolved(app_config["run"], app_config) - return - } - - RunResolved(exe) -} - -FindAppConfigByExe(exe_name) { - exe_lower := StrLower(exe_name) - for _, app in Config["apps"] { - win_title := StrLower(app["win_title"]) - if InStr(win_title, "ahk_exe " exe_lower) - return app - } - return "" -} diff --git a/config/config.example.json b/config/config.example.json deleted file mode 100644 index 1e77621..0000000 --- a/config/config.example.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "config_version": 1, - "super_key": "CapsLock", - "apps": [ - { - "id": "browser", - "hotkey": "f", - "win_title": "ahk_exe floorp.exe", - "run": "floorp", - "run_paths": [] - }, - { - "id": "files", - "hotkey": "e", - "win_title": "ahk_exe explorer.exe", - "run": "explorer", - "run_paths": [] - }, - { - "id": "editor", - "hotkey": "v", - "win_title": "ahk_exe Code.exe", - "run": "code", - "run_paths": [] - }, - { - "id": "terminal", - "hotkey": "s", - "win_title": "ahk_exe WindowsTerminal.exe", - "run": "wt", - "run_paths": [] - }, - { - "id": "notepad", - "hotkey": "n", - "win_title": "ahk_exe notepad++.exe", - "run": "notepad++", - "run_paths": [] - }, - { - "id": "chat", - "hotkey": "t", - "win_title": "ahk_exe telegram.exe", - "run": "telegram", - "run_paths": [] - }, - { - "id": "discord", - "hotkey": "d", - "win_title": "ahk_exe discord.exe", - "run": "discord", - "run_paths": [] - } - ], - "global_hotkeys": [ - { - "enabled": true, - "hotkey": "Alt", - "target_exes": [], - "send_keys": "^{Tab}" - } - ], - "window": { - "resize_step": 20, - "move_step": 20, - "super_double_tap_ms": 300, - "move_mode": { - "enable": true, - "cancel_key": "Esc" - }, - "cycle_app_windows_hotkey": "c", - "center_width_cycle_hotkey": "Space" - }, - "window_selector": { - "enabled": true, - "hotkey": "w", - "max_results": 12, - "title_preview_len": 60, - "match_title": true, - "match_exe": true, - "include_minimized": true, - "close_on_focus_loss": true - }, - "window_manager": { - "grid_size": 3, - "margins": { - "top": 6, - "left": 4, - "right": 4 - }, - "gap_px": 0, - "exceptions_regex": "(Shell_TrayWnd|Shell_SecondaryTrayWnd|WorkerW|XamlExplorerHostIslandWindow)" - }, - "directional_focus": { - "enabled": true, - "stacked_overlap_threshold": 0.5, - "stack_tolerance_px": 25, - "prefer_topmost": true, - "prefer_last_stacked": true, - "frontmost_guard_px": 200, - "perpendicular_overlap_min": 0.2, - "cross_monitor": false, - "debug_enabled": false - }, - "focus_border": { - "enabled": true, - "border_color": "#357EC7", - "move_mode_color": "#2ECC71", - "border_thickness": 4, - "corner_radius": 12, - "update_interval_ms": 20 - }, - "helper": { - "enabled": true, - "overlay_opacity": 200 - }, - "reload": { - "enabled": true, - "hotkey": "r", - "super_key_required": true, - "watch_enabled": false, - "watch_interval_ms": 1000, - "mode_enabled": true, - "mode_hotkey": ";", - "mode_timeout_ms": 20000 - } -} diff --git a/config/config.example.toml b/config/config.example.toml new file mode 100644 index 0000000..295caeb --- /dev/null +++ b/config/config.example.toml @@ -0,0 +1,164 @@ +config_version = 1 +# Use one or more keys as super (CapsLock is the default). +super_key = ["F24", "CapsLock"] + +[[apps]] +id = "browser" +hotkey = "f" +win_title = "ahk_exe floorp.exe" +run = "floorp" +# desktop = 2 +# follow_on_spawn = true +run_paths = [] + +[[apps]] +id = "files" +hotkey = "e" +win_title = "ahk_exe explorer.exe" +run = "explorer" +run_paths = [] + +[[apps]] +id = "editor" +hotkey = "v" +win_title = "ahk_exe Code.exe" +run = "code" +# run_start_in = "C:\\projects" +run_paths = [] + +[[apps]] +id = "terminal" +hotkey = "s" +win_title = "ahk_exe WindowsTerminal.exe" +run = "wt" +run_paths = [] + +[[apps]] +id = "notepad" +hotkey = "n" +win_title = "ahk_exe notepad++.exe" +run = "notepad++" +run_paths = [] + +[[apps]] +id = "chat" +hotkey = "t" +win_title = "ahk_exe telegram.exe" +run = "telegram" +run_paths = [] +# desktop = 2 + +[[apps]] +id = "discord" +hotkey = "d" +win_title = "ahk_exe discord.exe" +run = "discord" +run_paths = [] +# desktop = 2 + +[[apps]] +id = "vscode-border" +match = { exe = "Code.exe", title = " - Visual Studio Code$", title_regex = true } +# Optional per-app focus border tweaks. +focus_border = { border_color = "#4D96FF", border_thickness = 3, corner_radius = 12lll } + +[[global_hotkeys]] +enabled = true +hotkey = "Alt" +target_exes = [] +# Send Ctrl+Tab when holding super+alt. +# Will require virtual desktop hotkeys to be used without pressing super first in the combo. +send_keys = "^{Tab}" + +[window] +resize_step = 20 +move_step = 20 +super_double_tap_ms = 300 +# Cycle app windows across all desktops. +cycle_app_windows_hotkey = "c" +# Cycle app windows on the current desktop only. +cycle_app_windows_current_hotkey = "+c" +center_width_cycle_hotkey = "Space" + +[window.move_mode] +enable = true +cancel_key = "Esc" + +[window_selector] +enabled = true +hotkey = "w" +max_results = 12 +title_preview_len = 60 +match_title = true +match_exe = true +include_minimized = true +close_on_focus_loss = true + +[window_manager] +grid_size = 3 +gap_px = 0 +exceptions_regex = "(Shell_TrayWnd|Shell_SecondaryTrayWnd|WorkerW|XamlExplorerHostIslandWindow)" + +[window_manager.margins] +top = 6 +left = 4 +right = 4 +bottom = 5 + +[virtual_desktop] +enabled = true +switch_on_focus = true +ensure_count = 0 +cycle_prefer_current = true +# Hotkey fields here use AHK syntax without super (super is implied). +# Modifiers are applied by Harken (alt or alt+shift) for desktop actions. +prev_hotkey = "h" +next_hotkey = "l" +move_prev_hotkey = "h" +move_next_hotkey = "l" +# Debug logs write to %APPDATA%\harken when enabled. +debug_cycle = false +debug_hotkeys = false +tray_indicator = true +tray_format = "{current}/{total}" + +[[virtual_desktop.1]] +hotkey = "1" + +[[virtual_desktop.2]] +hotkey = "2" + +[directional_focus] +# Directional focus was tricky to make good enough. +enabled = true +stacked_overlap_threshold = 0.5 +stack_tolerance_px = 25 +prefer_topmost = true +prefer_last_stacked = true +frontmost_guard_px = 200 +perpendicular_overlap_min = 0.2 +cross_monitor = false +debug_enabled = false + +[focus_border] +enabled = true +border_color = "#222feb" +move_mode_color = "#2ECC71" +command_mode_color = "#FFD400" +border_thickness = 4 +corner_radius = 16 +update_interval_ms = 20 + +[helper] +enabled = true +overlay_opacity = 200 + +[reload] +enabled = true +hotkey = "r" +super_key_required = true +watch_enabled = false +watch_interval_ms = 2500 +mode_enabled = true +mode_hotkey = ";" +mode_timeout_ms = 20000 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index a391146..4174a74 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,14 +1,14 @@ # Installation -be-there supports two distribution formats: +harken supports two distribution formats: ## Option A: Source (recommended for customization) 1. Install [AutoHotkey v2](https://github.com/AutoHotkey/AutoHotkey). 2. Download the source release (zip) or clone the repo. -3. Copy `config/config.example.json` to `~/.config/be-there/config.json`. -4. Run `be-there.ahk`. +3. Copy `config/config.example.toml` to `~/.config/harken/harken.toml`. +4. Run `harken.ahk`. ## Option B: Compiled EXE (recommended for non-technical users) 1. Download the compiled exe. -2. Run `be-there.exe`. -3. Enter command mode (`super + ;`) then `e` to edit your config (auto created in `~/.config/be-there` if it doesn't already exist) +2. Run `harken.exe`. +3. Enter command mode (`super + ;`) then `e` to edit your config (auto created in `~/.config/harken` if it doesn't already exist) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 74a4939..0e5595a 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -4,23 +4,23 @@ This project ships two artifacts per release: ## 1) Source zip Include: -- `be-there.ahk` +- `harken.ahk` - `src/` - `tools/` -- `config/config.example.json` +- `config/config.example.toml` - `README.md`, `LICENSE`, `LICENSES/` ## 2) Compiled zip Include: -- `be-there.exe` +- `harken.exe` - `tools/` (optional; requires AutoHotkey for `.ahk` tools) -- `config/config.example.json` +- `config/config.example.toml` - `README.md`, `LICENSE`, `LICENSES/` ## Checklist - Update `README.md` if config/schema changed. - Update `docs/INSTALL.md` if paths or requirements changed. -- Verify `config/config.example.json` matches defaults. +- Verify `config/config.example.toml` matches defaults. - (Optional) test start/reload and Command Overlay in both variants. ## GitHub Release Flow @@ -28,9 +28,9 @@ Include: 2. Create a tag: `git tag vX.Y.Z`. 3. Push tags: `git push origin vX.Y.Z`. 4. GitHub Actions builds and publishes release assets: - - `be-there-source-vX.Y.Z.zip` - - `be-there-vX.Y.Z-win64.zip` - - `be-there-vX.Y.Z.exe` + - `harken-source-vX.Y.Z.zip` + - `harken-vX.Y.Z-win64.zip` + - `harken-vX.Y.Z.exe` ## Release Candidates - Use a prerelease tag like `vX.Y.Z-rc.1`. @@ -52,4 +52,4 @@ powershell -ExecutionPolicy Bypass -File tools/build_release.ps1 -Version dev Artifacts are written to `dist/`. ## Tooling -- The build script also exports `config.example.json` into `dist/` for quick inspection. +- The build script also exports `config.example.toml` into `dist/` for quick inspection. diff --git a/harken.ahk b/harken.ahk new file mode 100644 index 0000000..44f5642 --- /dev/null +++ b/harken.ahk @@ -0,0 +1,504 @@ +#Requires AutoHotkey v2.0 +#SingleInstance + +#Include src/lib/JXON.ahk +#Include src/lib/toml.ahk +#Include src/lib/config_loader.ahk +#Include src/lib/state_store.ahk +#Include src/lib/virtual_desktop.ahk +#Include src/lib/focus_or_run.ahk +#Include src/lib/command_toast.ahk +#Include src/lib/window_inspector.ahk + +config_dir := GetConfigDir() +DirCreate(config_dir) +global config_path := config_dir "\harken.toml" +EnsureConfigExists(config_path, DefaultConfig()) +config_result := LoadConfig(config_path, DefaultConfig()) +global Config := config_result["config"] +config_errors := config_result["errors"] +if (config_errors.Length) { + appdata_dir := GetAppDataDir() + LogConfigErrors(config_errors, appdata_dir "\config.errors.log", config_path) + return +} +InitVirtualDesktop() +global AppState := LoadState() +InitCommandToast() + +super_keys := Config["super_key"] +if !(super_keys is Array) + super_keys := [super_keys] + +RegisterSuperKeyHotkey("~", "", (*) => OnSuperKeyDown()) +if HasSuperKey("CapsLock") + SetCapsLockState "AlwaysOff" + +reload_config := Config["reload"] +if reload_config["enabled"] { + reload_hotkey := reload_config["hotkey"] + reload_mode_hotkey := reload_config["mode_hotkey"] + reload_mode_enabled := reload_config["mode_enabled"] + reload_mode_timeout := reload_config["mode_timeout_ms"] + global reload_mode_active := false + + if reload_mode_enabled { + RegisterSuperComboHotkey(reload_mode_hotkey, (*) => ActivateReloadMode(reload_mode_timeout)) + HotIf ReloadModeActive + Hotkey(reload_hotkey, (*) => ExecuteCommand(() => Reload())) + Hotkey("i", (*) => ExecuteCommand(OpenWindowInspector)) + Hotkey("n", (*) => ExecuteCommand(ToggleCommandHelper)) + Hotkey("w", (*) => ExecuteCommand(OpenNewWindowForActiveApp)) + Hotkey("e", (*) => ExecuteCommand(OpenConfigFile)) + Hotkey("Esc", ClearReloadMode) + HotIf + } +} + +if reload_config["enabled"] && reload_config["watch_enabled"] + StartConfigWatcher(config_path, reload_config["watch_interval_ms"]) + +SetWinDelay(-1) + +#Include src/lib/window_manager.ahk +#Include src/lib/directional_focus.ahk +#Include src/lib/focus_border.ahk +#Include src/lib/window_walker.ahk +#Include src/hotkeys/global_hotkey.ahk +#Include src/hotkeys/apps.ahk +#Include src/hotkeys/window.ahk +#Include src/hotkeys/directional_focus.ahk +#Include src/hotkeys/window_walker.ahk +#Include src/hotkeys/unbound.ahk + +DefaultConfig() { + return Map( + "config_version", 1, + "super_key", ["CapsLock"], + "apps", [ + Map("id", "files", "hotkey", "e", "win_title", "ahk_exe explorer.exe", "run", "explorer"), + Map("id", "editor", "hotkey", "v", "win_title", "ahk_exe Code.exe", "run", "code"), + Map("id", "terminal", "hotkey", "s", "win_title", "ahk_exe WindowsTerminal.exe", "run", "wt"), + Map("id", "notes", "hotkey", "n", "win_title", "ahk_exe notepad++.exe", "run", "notepad++") + ], + "global_hotkeys", [ + Map( + "enabled", true, + "hotkey", "Alt", + "target_exes", [], + "send_keys", "^{Tab}" + ) + ], + "window", Map( + "resize_step", 20, + "move_step", 20, + "super_double_tap_ms", 300, + "move_mode", Map( + "enable", true, + "cancel_key", "Esc" + ), + "cycle_app_windows_hotkey", "c", + "cycle_app_windows_current_hotkey", "+c", + "center_width_cycle_hotkey", "Space", + "minimize_others_hotkey", "" + ), + "window_selector", Map( + "enabled", true, + "hotkey", "w", + "max_results", 12, + "title_preview_len", 60, + "match_title", true, + "match_exe", true, + "include_minimized", true, + "close_on_focus_loss", true + ), + "window_manager", Map( + "grid_size", 3, + "margins", Map( + "top", 6, + "left", 4, + "right", 4, + "bottom", 5 + ), + "gap_px", 0, + "exceptions_regex", "(Shell_TrayWnd|Shell_SecondaryTrayWnd|WorkerW|XamlExplorerHostIslandWindow)" + ), + "virtual_desktop", Map( + "enabled", true, + "switch_on_focus", true, + "ensure_count", 0, + "cycle_prefer_current", true, + "prev_hotkey", "h", + "next_hotkey", "l", + "move_prev_hotkey", "h", + "move_next_hotkey", "l", + "desktop_hotkeys", [], + "goto_hotkeys", [], + "move_hotkeys", [], + "debug_cycle", false, + "debug_hotkeys", false, + "tray_indicator", false, + "tray_format", "{current}/{total}" + ), + "directional_focus", Map( + "enabled", true, + "stacked_overlap_threshold", 0.5, + "stack_tolerance_px", 25, + "prefer_topmost", true, + "prefer_last_stacked", true, + "frontmost_guard_px", 200, + "perpendicular_overlap_min", 0.2, + "cross_monitor", false, + "debug_enabled", false + ), + "focus_border", Map( + "enabled", true, + "border_color", "#357EC7", + "move_mode_color", "#2ECC71", + "command_mode_color", "#FFD400", + "border_thickness", 4, + "corner_radius", 16, + "update_interval_ms", 20 + ), + "helper", Map( + "enabled", true, + "overlay_opacity", 200 + ), + "reload", Map( + "enabled", true, + "hotkey", "r", + "super_key_required", true, + "watch_enabled", false, + "watch_interval_ms", 1000, + "mode_enabled", true, + "mode_hotkey", ";", + "mode_timeout_ms", 20000 + ) + ) +} + +LogConfigErrors(errors, log_path, config_path := "") { + DirCreate(GetAppDataDir()) + header := "[" A_Now "] Config errors:" "`n" + FileAppend(header, log_path) + for _, err in errors { + FileAppend("- " err "`n", log_path) + } + FileAppend("`n", log_path) + + summary := "Config errors detected.`n" + summary .= "Log: " log_path "`n" + if config_path + summary .= "Config: " config_path "`n" + + details := summary "`n" + for _, err in errors { + details .= "- " err "`n" + } + + ShowConfigErrorsGui(details, log_path) +} + +ShowConfigErrorsGui(details, log_path) { + error_gui := Gui("+AlwaysOnTop", "harken Config Errors") + error_gui.SetFont("s10", "Segoe UI") + edit := error_gui.AddEdit("w760 r18 ReadOnly", details) + open_btn := error_gui.AddButton("xm y+10 w120", "Open Log") + open_btn.OnEvent("Click", (*) => Run(log_path)) + close_btn := error_gui.AddButton("x+10 yp w120", "Close") + close_btn.OnEvent("Click", (*) => error_gui.Destroy()) + error_gui.Show() +} + +EnsureConfigExists(config_path, default_config) { + if FileExist(config_path) + return + + config_text := TomlDump(default_config, ["apps", "global_hotkeys"]) + FileAppend(config_text, config_path, "UTF-8") +} + +GetConfigDir() { + user_profile := EnvGet("USERPROFILE") + if !user_profile + user_profile := A_ScriptDir + return user_profile "\.config\harken" +} + +GetAppDataDir() { + appdata := EnvGet("APPDATA") + if appdata + return appdata "\harken" + return GetConfigDir() +} + +ResetDebugLogs() { + if !Config.Has("virtual_desktop") || !(Config["virtual_desktop"] is Map) + return + vd_config := Config["virtual_desktop"] + debug_cycle := vd_config.Has("debug_cycle") && vd_config["debug_cycle"] + debug_hotkeys := vd_config.Has("debug_hotkeys") && vd_config["debug_hotkeys"] + if !(debug_cycle || debug_hotkeys) + return + + log_dir := GetAppDataDir() + DirCreate(log_dir) + if debug_cycle { + TryDeleteFile(log_dir "\cycle.debug.log") + } + if debug_hotkeys { + TryDeleteFile(log_dir "\vd.hotkeys.log") + TryDeleteFile(log_dir "\vd.actions.log") + } +} + +TryDeleteFile(path) { + try { + if FileExist(path) + FileDelete(path) + } +} + +StartConfigWatcher(path, interval_ms := 1000) { + global config_watch_mtime := "" + global config_watch_handle := 0 + global config_watch_dir := "" + global config_watch_path := path + global config_watch_use_fallback := false + global config_watch_fallback_notified := false + + if FileExist(path) + config_watch_mtime := FileGetTime(path, "M") + + try { + SplitPath(path, &config_watch_filename, &config_watch_dir) + flags := 0x00000010 | 0x00000001 + config_watch_handle := DllCall("FindFirstChangeNotificationW", "str", config_watch_dir, "int", false, "uint", flags, "ptr") + if (!config_watch_handle || config_watch_handle = -1) + throw Error("FindFirstChangeNotificationW failed") + } catch { + config_watch_use_fallback := true + NotifyConfigWatcherFallback() + } + + SetTimer((*) => CheckConfigWatcher(), interval_ms) +} + +CheckConfigWatcher() { + global config_watch_mtime, config_watch_handle, config_watch_path, config_watch_use_fallback + if config_watch_use_fallback { + CheckConfigWatcherPoll() + return + } + + if (!config_watch_handle || config_watch_handle = -1) { + config_watch_use_fallback := true + NotifyConfigWatcherFallback() + CheckConfigWatcherPoll() + return + } + + result := DllCall("WaitForSingleObject", "ptr", config_watch_handle, "uint", 0) + if (result != 0) + return + + if !FileExist(config_watch_path) { + DllCall("FindNextChangeNotification", "ptr", config_watch_handle) + return + } + + current := FileGetTime(config_watch_path, "M") + if (config_watch_mtime = "") { + config_watch_mtime := current + } else if (current != config_watch_mtime) { + config_watch_mtime := current + Reload() + return + } + + if !DllCall("FindNextChangeNotification", "ptr", config_watch_handle) { + config_watch_use_fallback := true + NotifyConfigWatcherFallback() + } +} + +CheckConfigWatcherPoll() { + global config_watch_mtime, config_watch_path + if !FileExist(config_watch_path) + return + + current := FileGetTime(config_watch_path, "M") + if (config_watch_mtime = "") { + config_watch_mtime := current + return + } + if (current != config_watch_mtime) { + config_watch_mtime := current + Reload() + } +} + +NotifyConfigWatcherFallback() { + global config_watch_fallback_notified + if config_watch_fallback_notified + return + config_watch_fallback_notified := true + TrayTip("harken", "Config watcher fell back to polling.", 5) +} + +ActivateReloadMode(timeout_ms := 1500) { + global reload_mode_active := true + global reload_mode_activated_at := A_TickCount + SetTimer(ClearReloadMode, 0) + SetTimer(ClearReloadMode, -timeout_ms) + UpdateCommandToastVisibility() +} + +ClearReloadMode(*) { + global reload_mode_active := false + UpdateCommandToastVisibility() +} + +ReloadModeActive(*) { + global reload_mode_active + return reload_mode_active +} + +ExecuteCommand(callback) { + callback.Call() + ClearReloadMode() +} + +OnSuperKeyDown() { + if WindowWalker.IsActive() { + WindowWalker.Hide() + return + } + if command_toast_temp_visible { + HideCommandToast() + return + } + UpdateCommandToastVisibility() +} + +IsSuperKeyPressed(*) { + global super_keys + for _, key in super_keys { + if GetKeyState(key, "P") + return true + } + return false +} + +HasSuperKey(target_key) { + global super_keys + target_lower := StrLower(target_key) + for _, key in super_keys { + if (StrLower(key) = target_lower) + return true + } + return false +} + +RegisterSuperKeyHotkey(prefix, suffix, callback) { + global super_keys + for _, key in super_keys + Hotkey(prefix key suffix, callback) +} + +RegisterSuperComboHotkey(hotkey_name, callback) { + global super_keys + for _, key in super_keys + Hotkey(key " & " hotkey_name, callback) +} + +MatchAppWindow(app, hwnd := 0) { + if !(app is Map) + return false + if (hwnd = 0) + try hwnd := WinGetID("A") + if !hwnd + return false + + if app.Has("match") && (app["match"] is Map) + return MatchWindowFields(app["match"], hwnd) + + if app.Has("win_title") && app["win_title"] != "" { + active_hwnd := 0 + try active_hwnd := WinGetID("A") + if (active_hwnd && hwnd = active_hwnd) + return WinActive(app["win_title"]) + } + + return false +} + +MatchWindowFields(match, hwnd) { + exe_name := WinGetProcessName("ahk_id " hwnd) + class_name := WinGetClass("ahk_id " hwnd) + title := WinGetTitle("ahk_id " hwnd) + + if !MatchField(match, "exe", exe_name) + return false + if !MatchField(match, "class", class_name) + return false + if !MatchField(match, "title", title) + return false + + return true +} + +MatchField(match, field_key, value) { + if !match.Has(field_key) || match[field_key] = "" + return true + + pattern := match[field_key] + regex_key := field_key "_regex" + use_regex := match.Has(regex_key) && match[regex_key] + + if use_regex { + if !RegExMatch(pattern, "^\(\?i\)") + pattern := "(?i)" pattern + return RegExMatch(value, pattern) != 0 + } + + return StrLower(value) = StrLower(pattern) +} + +OpenWindowInspector() { + ShowWindowInspector() +} + +OpenConfigFile() { + global config_path + if !config_path + return + if FileExist(config_path) + Run(config_path) +} + +OpenNewWindowForActiveApp() { + hwnd := WinExist("A") + if !hwnd + return + + exe := WinGetProcessName("ahk_id " hwnd) + if !exe + return + + app_config := FindAppConfigByWindow(hwnd) + if (app_config is Map) { + RunResolved(app_config["run"], app_config) + return + } + + RunResolved(exe) +} + +FindAppConfigByWindow(hwnd) { + for _, app in Config["apps"] { + if MatchAppWindow(app, hwnd) + return app + } + return "" +} diff --git a/justfile b/justfile index 66ed7c7..994c0bf 100644 --- a/justfile +++ b/justfile @@ -16,10 +16,10 @@ set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] ./tools/build_release.ps1 @start: - ./be-there.ahk + ./harken.ahk @start_bin: - ./dist/be-there.exe + ./dist/harken.exe compress_gifs path: #!{{shebang}} @@ -29,4 +29,4 @@ compress_gifs path: ffmpeg -y -i "$in" ` -vf "fps=11,scale=720:-2:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" ` "$tmp" - } \ No newline at end of file + } diff --git a/src/hotkeys/apps.ahk b/src/hotkeys/apps.ahk index b2e8a31..2beea09 100644 --- a/src/hotkeys/apps.ahk +++ b/src/hotkeys/apps.ahk @@ -1,10 +1,12 @@ -global Config, super_key +global Config -HotIf (*) => GetKeyState(super_key, "P") +HotIf IsSuperKeyPressed for _, app in Config["apps"] { + if !app.Has("hotkey") || app["hotkey"] = "" + continue hotkey_name := app["hotkey"] - win_title := app["win_title"] - run_cmd := app["run"] + win_title := app.Has("win_title") ? app["win_title"] : "" + run_cmd := app.Has("run") ? app["run"] : "" hotkey_id := app["id"] Hotkey(hotkey_name, FocusOrRun.Bind(win_title, run_cmd, hotkey_id, app)) } diff --git a/src/hotkeys/directional_focus.ahk b/src/hotkeys/directional_focus.ahk index b04d12f..45d90ba 100644 --- a/src/hotkeys/directional_focus.ahk +++ b/src/hotkeys/directional_focus.ahk @@ -1,6 +1,7 @@ global Config if Config.Has("directional_focus") && Config["directional_focus"]["enabled"] { + HotIf (*) => !IsSuperKeyPressed() Hotkey("!h", (*) => DirectionalFocus("left")) Hotkey("!l", (*) => DirectionalFocus("right")) Hotkey("!j", (*) => DirectionalFocus("down")) @@ -9,4 +10,5 @@ if Config.Has("directional_focus") && Config["directional_focus"]["enabled"] { Hotkey("!]", (*) => DirectionalFocusStacked("next")) Hotkey("!+d", (*) => ToggleDirectionalFocusDebug()) Hotkey("!+s", (*) => SetLastStackedFromActive()) + HotIf } diff --git a/src/hotkeys/global_hotkey.ahk b/src/hotkeys/global_hotkey.ahk index 0155de5..02f02d7 100644 --- a/src/hotkeys/global_hotkey.ahk +++ b/src/hotkeys/global_hotkey.ahk @@ -1,4 +1,4 @@ -global Config, super_key +global Config for _, hotkey_config in Config["global_hotkeys"] { if !hotkey_config["enabled"] @@ -10,10 +10,10 @@ for _, hotkey_config in Config["global_hotkeys"] { if (target_exes.Length > 0) { HotIf (*) => ScopedHotIf(target_exes) - Hotkey(super_key " & " hotkey_name, (*) => Send(send_keys)) + RegisterSuperComboHotkey(hotkey_name, (*) => Send(send_keys)) HotIf } else { - Hotkey(super_key " & " hotkey_name, (*) => Send(send_keys)) + RegisterSuperComboHotkey(hotkey_name, (*) => Send(send_keys)) } } diff --git a/src/hotkeys/unbound.ahk b/src/hotkeys/unbound.ahk index 39a212b..12743cb 100644 --- a/src/hotkeys/unbound.ahk +++ b/src/hotkeys/unbound.ahk @@ -1,10 +1,11 @@ -global Config, super_key +global Config RegisterUnboundHotkeys() { used_keys := Map() AddUsedKey(used_keys, Config["window"]["center_width_cycle_hotkey"]) AddUsedKey(used_keys, Config["window"]["cycle_app_windows_hotkey"]) + AddUsedKey(used_keys, Config["window"]["cycle_app_windows_current_hotkey"]) AddUsedKey(used_keys, "m") AddUsedKey(used_keys, "q") AddUsedKey(used_keys, "h") @@ -20,7 +21,8 @@ RegisterUnboundHotkeys() { AddUsedKey(used_keys, reload_config["hotkey"]) for _, app in Config["apps"] { - AddUsedKey(used_keys, app["hotkey"]) + if app.Has("hotkey") && app["hotkey"] != "" + AddUsedKey(used_keys, app["hotkey"]) } for _, hotkey_config in Config["global_hotkeys"] { @@ -29,7 +31,7 @@ RegisterUnboundHotkeys() { } candidates := BuildUnboundCandidateKeys() - HotIf (*) => GetKeyState(super_key, "P") + HotIf IsSuperKeyPressed for _, key in candidates { if !used_keys.Has(StrLower(key)) Hotkey(key, (*) => FlashUnboundHotkey()) diff --git a/src/hotkeys/window.ahk b/src/hotkeys/window.ahk index 477b065..28f6269 100644 --- a/src/hotkeys/window.ahk +++ b/src/hotkeys/window.ahk @@ -1,4 +1,4 @@ -global Config, super_key +global Config resize_step := Config["window"]["resize_step"] move_step := Config["window"]["move_step"] @@ -7,15 +7,38 @@ move_mode_enabled := Config["window"]["move_mode"]["enable"] move_mode_cancel_key := Config["window"]["move_mode"]["cancel_key"] center_cycle_hotkey := Config["window"]["center_width_cycle_hotkey"] cycle_app_windows_hotkey := Config["window"]["cycle_app_windows_hotkey"] +cycle_app_windows_current_hotkey := Config["window"]["cycle_app_windows_current_hotkey"] +vd_config := Config.Has("virtual_desktop") ? Config["virtual_desktop"] : Map() +vd_prev_hotkey := vd_config.Has("prev_hotkey") ? vd_config["prev_hotkey"] : "" +vd_next_hotkey := vd_config.Has("next_hotkey") ? vd_config["next_hotkey"] : "" +vd_move_prev_hotkey := vd_config.Has("move_prev_hotkey") ? vd_config["move_prev_hotkey"] : "" +vd_move_next_hotkey := vd_config.Has("move_next_hotkey") ? vd_config["move_next_hotkey"] : "" +vd_desktop_hotkeys := vd_config.Has("desktop_hotkeys") ? vd_config["desktop_hotkeys"] : [] +vd_goto_hotkeys := vd_config.Has("goto_hotkeys") ? vd_config["goto_hotkeys"] : [] +vd_move_hotkeys := vd_config.Has("move_hotkeys") ? vd_config["move_hotkeys"] : [] +minimize_others_hotkey := "" +if Config["window"].Has("minimize_others_hotkey") + minimize_others_hotkey := Config["window"]["minimize_others_hotkey"] + +vd_prev_hotkey := NormalizeAltHotkey(vd_prev_hotkey) +vd_next_hotkey := NormalizeAltHotkey(vd_next_hotkey) +vd_move_prev_hotkey := NormalizeAltHotkey(vd_move_prev_hotkey, true) +vd_move_next_hotkey := NormalizeAltHotkey(vd_move_next_hotkey, true) last_super_tap := 0 max_restore := Map() +app_cycle_cache := Map() +cycle_hwnd_desktop := Map() +debug_logs_initialized := false ResizeActiveWindow(delta_w, delta_h) { hwnd := WinExist("A") if !hwnd || Window.IsException("ahk_id " hwnd) return + if IsAltPressed() || AltPressedSoon() + return + if (WinGetMinMax("ahk_id " hwnd) = 1) WinRestore "ahk_id " hwnd @@ -45,6 +68,9 @@ ResizeActiveWindowCentered(delta_w, delta_h) { if !hwnd || Window.IsException("ahk_id " hwnd) return + if IsAltPressed() || AltPressedSoon() + return + if (WinGetMinMax("ahk_id " hwnd) = 1) WinRestore "ahk_id " hwnd @@ -123,24 +149,330 @@ SortWindowList(list) { return sorted } +IsAltPressed() { + return GetKeyState("LAlt", "P") || GetKeyState("RAlt", "P") +} + +AltPressedSoon() { + if IsAltPressed() + return true + KeyWait "Alt", "D T0.05" + return IsAltPressed() +} + +NormalizeAltHotkey(hotkey_name, require_shift := false) { + if !hotkey_name + return "" + normalized := hotkey_name + if (SubStr(normalized, 1, 1) != "*") + normalized := "*" normalized + if !InStr(normalized, "!") + normalized := "!" normalized + if (require_shift && !InStr(normalized, "+")) + normalized := "+" normalized + return normalized +} + + FilterWindowList(exe, list) { filtered := [] for _, id in list { - class_name := WinGetClass("ahk_id " id) + if !WindowExistsAcrossDesktops(id) + continue + try class_name := WinGetClass("ahk_id " id) + catch + continue if (exe = "explorer.exe") { if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd") continue } - ex_style := WinGetExStyle("ahk_id " id) + try ex_style := WinGetExStyle("ahk_id " id) + catch + continue if (ex_style & 0x80) || (ex_style & 0x8000000) continue - if (!(WinGetStyle("ahk_id " id) & 0x10000000)) + try style := WinGetStyle("ahk_id " id) + catch + continue + allow_invisible := false + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(id) + if (desktop_num <= 0) + allow_invisible := true + else if (desktop_num != VD.getCurrentDesktopNum()) + allow_invisible := true + } + if (!allow_invisible && !(style & 0x10000000)) + continue + filtered.Push(id) + } + return filtered +} + +ApplyDesktopCyclePreference(list, current_only := false) { + if !VirtualDesktopEnabled() + return list + + current := [] + other := [] + for _, id in list { + desktop_num := GetWindowDesktopForCycle(id) + is_current := (desktop_num > 0 && desktop_num = VD.getCurrentDesktopNum()) + if is_current + current.Push(id) + else if !current_only + other.Push(id) + } + + if current_only + return current + + if !Config["virtual_desktop"]["cycle_prefer_current"] + return list + + ordered := [] + for _, id in current + ordered.Push(id) + for _, id in other + ordered.Push(id) + return ordered +} + +ApplyCurrentDesktopOrder(exe, win_list, active_hwnd, current_only := false) { + if !VirtualDesktopEnabled() + return win_list + current_order := GetCurrentDesktopOrderedList(exe) + current_set := Map() + for _, id in current_order + current_set[id] := true + + if (current_order.Length = 0) { + if (current_only && active_hwnd && WindowExistsAcrossDesktops(active_hwnd)) + return [active_hwnd] + return win_list + } + + if current_only + return current_order + + if !Config["virtual_desktop"]["cycle_prefer_current"] + return win_list + + ordered := [] + for _, id in current_order + ordered.Push(id) + + seen := Map() + for _, id in current_order + seen[id] := true + + for _, id in win_list { + if seen.Has(id) + continue + ordered.Push(id) + seen[id] := true + } + return ordered +} + +GetCurrentDesktopWindowSet(exe) { + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := false + list := WinGetList("ahk_exe " exe) + A_DetectHiddenWindows := bak_detect_hidden_windows + + list := FilterCurrentDesktopWindowList(exe, list) + set := Map() + for _, id in list + set[id] := true + return set +} + +GetCurrentDesktopOrderedList(exe) { + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := false + list := WinGetList("ahk_exe " exe) + A_DetectHiddenWindows := bak_detect_hidden_windows + + return FilterCurrentDesktopWindowList(exe, list) +} + +FilterCurrentDesktopWindowList(exe, list) { + filtered := [] + for _, id in list { + if !WinExist("ahk_id " id) + continue + try class_name := WinGetClass("ahk_id " id) + catch + continue + if (exe = "explorer.exe") { + if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd") + continue + } + try ex_style := WinGetExStyle("ahk_id " id) + catch + continue + if (ex_style & 0x80) || (ex_style & 0x8000000) continue filtered.Push(id) } return filtered } +GetWindowDesktopForCycle(hwnd) { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num > 0) + return desktop_num + if cycle_hwnd_desktop.Has(hwnd) + return cycle_hwnd_desktop[hwnd] + return 0 +} + +CycleDebugEnabled() { + if !Config.Has("virtual_desktop") + return false + return Config["virtual_desktop"].Has("debug_cycle") && Config["virtual_desktop"]["debug_cycle"] +} + +HotkeyDebugEnabled() { + if !Config.Has("virtual_desktop") + return false + return Config["virtual_desktop"].Has("debug_hotkeys") && Config["virtual_desktop"]["debug_hotkeys"] +} + +EnsureDebugLogInit() { + global debug_logs_initialized + if debug_logs_initialized + return + if !(CycleDebugEnabled() || HotkeyDebugEnabled()) + return + log_dir := GetCycleDebugDir() + DirCreate(log_dir) + if CycleDebugEnabled() { + TryResetLogFile(log_dir "\\cycle.debug.log") + } + if HotkeyDebugEnabled() { + TryResetLogFile(log_dir "\\vd.hotkeys.log") + TryResetLogFile(log_dir "\\vd.actions.log") + } + debug_logs_initialized := true +} + +TryResetLogFile(path) { + try { + if FileExist(path) + FileDelete(path) + FileAppend("", path) + } +} + +LogCycleDebug(lines) { + if !CycleDebugEnabled() + return + EnsureDebugLogInit() + log_dir := GetCycleDebugDir() + DirCreate(log_dir) + log_path := log_dir "\\cycle.debug.log" + header := "[" A_Now "] " + if !(lines is Array) + lines := [lines] + for _, line in lines { + FileAppend(header line "`n", log_path) + } +} + +LogVirtualDesktopHotkeys(lines) { + if !HotkeyDebugEnabled() + return + EnsureDebugLogInit() + log_dir := GetCycleDebugDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.hotkeys.log" + header := "[" A_Now "] " + if !(lines is Array) + lines := [lines] + for _, line in lines { + FileAppend(header line "`n", log_path) + } +} + +LogVirtualDesktopAction(lines) { + if !HotkeyDebugEnabled() + return + EnsureDebugLogInit() + log_dir := GetCycleDebugDir() + DirCreate(log_dir) + log_path := log_dir "\\vd.actions.log" + header := "[" A_Now "] " + if !(lines is Array) + lines := [lines] + for _, line in lines { + FileAppend(header line "`n", log_path) + } +} + +RegisterDesktopHotkey(kind, hotkey_name, desktop_num, callback) { + if (hotkey_name = "") + return + Hotkey(hotkey_name, (*) => ( + LogVirtualDesktopAction(kind " hotkey=" hotkey_name " desktop=" desktop_num " current=" GetCurrentDesktopNumFresh()), + callback() + )) +} + +GetCycleDebugDir() { + appdata := EnvGet("APPDATA") + if appdata + return appdata "\\harken" + return GetConfigDir() +} + +UpdateAppCycleCache(exe, win_list) { + if !app_cycle_cache.Has(exe) + app_cycle_cache[exe] := Map() + + cache := app_cycle_cache[exe] + for _, hwnd in win_list { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num > 0) + cache[hwnd] := desktop_num + else if !cache.Has(hwnd) + cache[hwnd] := 0 + if (desktop_num > 0) + cycle_hwnd_desktop[hwnd] := desktop_num + } + + for hwnd, _ in cache { + if !WindowExistsAcrossDesktops(hwnd) + cache.Delete(hwnd) + } + + return cache +} + +BuildCycleWindowList(exe, win_list) { + if !VirtualDesktopEnabled() + return win_list + + cache := UpdateAppCycleCache(exe, win_list) + if (cache.Count = 0) + return win_list + + dedup := Map() + merged := [] + for _, hwnd in win_list { + dedup[hwnd] := true + merged.Push(hwnd) + } + for hwnd, _ in cache { + if !dedup.Has(hwnd) { + merged.Push(hwnd) + dedup[hwnd] := true + } + } + return merged +} + HandleSuperTap() { global last_super_tap, super_double_tap_ms @@ -176,9 +508,11 @@ OnSuperKeyUp() { } if (move_mode_enabled || Config["reload"]["mode_enabled"] || Config["helper"]["enabled"]) { - Hotkey(super_key " up", (*) => OnSuperKeyUp()) + RegisterSuperKeyHotkey("", " up", (*) => OnSuperKeyUp()) } +Hotkey("~LButton", (*) => BeginSuperDrag()) + CenterWidthCycle(*) { static state := 0 state := Mod(state + 1, 3) @@ -199,11 +533,12 @@ CenterWidthCycle(*) { left_margin := Screen.left_margin right_margin := Screen.right_margin top_margin := Screen.top_margin + bottom_margin := Screen.bottom_margin gap_px := Config["window_manager"]["gap_px"] mx1 += left_margin mw := mw - left_margin - right_margin - mh := mh - top_margin + mh := mh - top_margin - bottom_margin if (gap_px > 0) { mx1 += gap_px @@ -277,12 +612,38 @@ CycleAppWindows(*) { if !exe return - win_list := WinGetList("ahk_exe " exe) + win_list := GetWindowsAcrossDesktops("ahk_exe " exe) win_list := FilterWindowList(exe, win_list) + win_list := BuildCycleWindowList(exe, win_list) if (win_list.Length < 2) return + if CycleDebugEnabled() { + lines := [] + lines.Push("cycle_all exe=" exe " active=" Format("0x{:X}", hwnd) " current_desktop=" GetCurrentDesktopNumFresh() " cache_count=" (app_cycle_cache.Has(exe) ? app_cycle_cache[exe].Count : 0)) + for _, id in win_list { + desktop_num := GetWindowDesktopNum(id) + exists := WindowExistsAcrossDesktops(id) ? "1" : "0" + lines.Push(" hwnd=" Format("0x{:X}", id) " desktop=" desktop_num " exists=" exists) + } + LogCycleDebug(lines) + } + + found_current := false + for _, id in win_list { + if (id = hwnd) { + found_current := true + break + } + } + if (!found_current && WindowExistsAcrossDesktops(hwnd)) + win_list.Push(hwnd) + win_list := SortWindowList(win_list) + win_list := ApplyDesktopCyclePreference(win_list, false) + win_list := ApplyCurrentDesktopOrder(exe, win_list, hwnd, false) + if (win_list.Length < 2) + return current_index := 0 for i, id in win_list { @@ -293,10 +654,145 @@ CycleAppWindows(*) { } next_index := (current_index >= win_list.Length || current_index = 0) ? 1 : current_index + 1 - WinActivate "ahk_id " win_list[next_index] + ActivateNextAvailableWindow(win_list, next_index) } -HotIf (*) => GetKeyState(super_key, "P") +CycleAppWindowsCurrent(*) { + hwnd := WinExist("A") + if !hwnd + return + + exe := WinGetProcessName("ahk_id " hwnd) + if !exe + return + + win_list := GetWindowsAcrossDesktops("ahk_exe " exe) + win_list := FilterWindowList(exe, win_list) + win_list := BuildCycleWindowList(exe, win_list) + if (win_list.Length < 2) + return + + if CycleDebugEnabled() { + lines := [] + lines.Push("cycle_current exe=" exe " active=" Format("0x{:X}", hwnd) " current_desktop=" GetCurrentDesktopNumFresh() " cache_count=" (app_cycle_cache.Has(exe) ? app_cycle_cache[exe].Count : 0)) + for _, id in win_list { + desktop_num := GetWindowDesktopNum(id) + exists := WindowExistsAcrossDesktops(id) ? "1" : "0" + lines.Push(" hwnd=" Format("0x{:X}", id) " desktop=" desktop_num " exists=" exists) + } + LogCycleDebug(lines) + } + + found_current := false + for _, id in win_list { + if (id = hwnd) { + found_current := true + break + } + } + if (!found_current && WindowExistsAcrossDesktops(hwnd)) + win_list.Push(hwnd) + + win_list := SortWindowList(win_list) + win_list := ApplyDesktopCyclePreference(win_list, true) + win_list := ApplyCurrentDesktopOrder(exe, win_list, hwnd, true) + if (win_list.Length < 2) + return + + current_index := 0 + for i, id in win_list { + if (id = hwnd) { + current_index := i + break + } + } + + next_index := (current_index >= win_list.Length || current_index = 0) ? 1 : current_index + 1 + ActivateNextAvailableWindow(win_list, next_index) +} + +ActivateNextAvailableWindow(win_list, start_index) { + count := win_list.Length + if (count = 0) + return + + index := start_index + loop count { + if (index > count) + index := 1 + hwnd := win_list[index] + if WindowExistsAcrossDesktops(hwnd) { + activated := ActivateWindowAcrossDesktops(hwnd) + if CycleDebugEnabled() + LogCycleDebug(" try hwnd=" Format("0x{:X}", hwnd) " activated=" activated) + if activated + return + } + index += 1 + } +} + +GoToRelativeDesktop(delta) { + if !VirtualDesktopEnabled() + return + RefreshVirtualDesktopState() + current := GetCurrentDesktopNumFresh() + if (current <= 0) + current := VD.getCurrentDesktopNum() + if (current <= 0) + return + target := VD.modulusResolveDesktopNum(current + delta) + LogVirtualDesktopAction("goto_relative current=" current " delta=" delta " target=" target) + RefreshVirtualDesktopState() + VD.goToDesktopNum(target) + VD.WaitDesktopSwitched(target) + RefreshVirtualDesktopState() +} + +GoToDesktopNumber(desktop_num) { + if !VirtualDesktopEnabled() + return + if (desktop_num <= 0) + return + LogVirtualDesktopAction("goto_absolute target=" desktop_num " current=" GetCurrentDesktopNumFresh()) + RefreshVirtualDesktopState() + GetCurrentDesktopNumFresh() + VD.goToDesktopNum(desktop_num) + VD.WaitDesktopSwitched(desktop_num) + RefreshVirtualDesktopState() +} + +MoveWindowToRelativeDesktop(delta) { + if !VirtualDesktopEnabled() + return + RefreshVirtualDesktopState() + current := GetCurrentDesktopNumFresh() + if (current <= 0) + current := VD.getCurrentDesktopNum() + if (current <= 0) + return + target := VD.modulusResolveDesktopNum(current + delta) + LogVirtualDesktopAction("move_relative current=" current " delta=" delta " target=" target) + RefreshVirtualDesktopState() + VD.MoveWindowToDesktopNum("A", target, true) + VD.WaitDesktopSwitched(target) + RefreshVirtualDesktopState() +} + +MoveWindowToDesktopNumber(desktop_num) { + if !VirtualDesktopEnabled() + return + if (desktop_num <= 0) + return + LogVirtualDesktopAction("move_absolute target=" desktop_num " current=" GetCurrentDesktopNumFresh()) + RefreshVirtualDesktopState() + GetCurrentDesktopNumFresh() + VD.MoveWindowToDesktopNum("A", desktop_num, true) + VD.WaitDesktopSwitched(desktop_num) + RefreshVirtualDesktopState() +} + +HotIf (*) => IsSuperKeyPressed() && !IsAltPressed() Hotkey(center_cycle_hotkey, CenterWidthCycle) Hotkey("Left", (*) => ResizeActiveWindow(-resize_step, 0)) Hotkey("Right", (*) => ResizeActiveWindow(resize_step, 0)) @@ -310,9 +806,109 @@ Hotkey("^h", (*) => MoveActiveWindow(-move_step, 0)) Hotkey("^l", (*) => MoveActiveWindow(move_step, 0)) Hotkey("^j", (*) => MoveActiveWindow(0, move_step)) Hotkey("^k", (*) => MoveActiveWindow(0, -move_step)) +HotIf IsSuperKeyPressed Hotkey("m", ToggleMaximize) Hotkey("q", CloseWindow) Hotkey(cycle_app_windows_hotkey, CycleAppWindows) +if (cycle_app_windows_current_hotkey != "") + Hotkey(cycle_app_windows_current_hotkey, CycleAppWindowsCurrent) +RegisterSuperComboHotkey("/", (*) => ShowCommandToastTemporary()) +if (minimize_others_hotkey != "") + Hotkey(minimize_others_hotkey, MinimizeOtherWindows) +HotIf + +HotIf (*) => IsSuperKeyPressed() && IsAltPressed() && !GetKeyState("Shift", "P") +if (vd_prev_hotkey != "") + Hotkey(vd_prev_hotkey, (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=" vd_prev_hotkey " delta=-1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(-1) + )) +if (vd_next_hotkey != "") + Hotkey(vd_next_hotkey, (*) => ( + LogVirtualDesktopAction("goto_relative hotkey=" vd_next_hotkey " delta=1 current=" GetCurrentDesktopNumFresh()), + GoToRelativeDesktop(1) + )) +LogVirtualDesktopHotkeys("prev_hotkey=" vd_prev_hotkey " next_hotkey=" vd_next_hotkey) +for _, entry in vd_goto_hotkeys { + if !(entry is Map) + continue + if !entry.Has("hotkey") || !entry.Has("desktop") + continue + hotkey_name := NormalizeAltHotkey(entry["hotkey"]) + desktop_num := entry["desktop"] + key_copy := hotkey_name + num_copy := desktop_num + if (key_copy != "") { + callback := GoToDesktopNumber.Bind(num_copy) + RegisterDesktopHotkey("goto_absolute", key_copy, num_copy, callback) + } + LogVirtualDesktopHotkeys("map goto hotkey=" key_copy " desktop=" num_copy) +} +if (vd_desktop_hotkeys is Array && vd_desktop_hotkeys.Length > 0) { + LogVirtualDesktopHotkeys("desktop_hotkeys_count=" vd_desktop_hotkeys.Length) + for _, entry in vd_desktop_hotkeys { + if !(entry is Map) + continue + if !entry.Has("hotkey") || !entry.Has("desktop") + continue + hotkey_name := NormalizeAltHotkey(entry["hotkey"]) + desktop_num := entry["desktop"] + key_copy := hotkey_name + num_copy := desktop_num + if (key_copy != "") { + callback := GoToDesktopNumber.Bind(num_copy) + RegisterDesktopHotkey("goto_absolute", key_copy, num_copy, callback) + } + LogVirtualDesktopHotkeys("map goto hotkey=" key_copy " desktop=" num_copy) + } +} +HotIf + +HotIf (*) => IsSuperKeyPressed() && IsAltPressed() && GetKeyState("Shift", "P") +if (vd_move_prev_hotkey != "") + Hotkey(vd_move_prev_hotkey, (*) => ( + LogVirtualDesktopAction("move_relative hotkey=" vd_move_prev_hotkey " delta=-1 current=" GetCurrentDesktopNumFresh()), + MoveWindowToRelativeDesktop(-1) + )) +if (vd_move_next_hotkey != "") + Hotkey(vd_move_next_hotkey, (*) => ( + LogVirtualDesktopAction("move_relative hotkey=" vd_move_next_hotkey " delta=1 current=" GetCurrentDesktopNumFresh()), + MoveWindowToRelativeDesktop(1) + )) +LogVirtualDesktopHotkeys("move_prev_hotkey=" vd_move_prev_hotkey " move_next_hotkey=" vd_move_next_hotkey) +for _, entry in vd_move_hotkeys { + if !(entry is Map) + continue + if !entry.Has("hotkey") || !entry.Has("desktop") + continue + hotkey_name := NormalizeAltHotkey(entry["hotkey"], true) + desktop_num := entry["desktop"] + key_copy := hotkey_name + num_copy := desktop_num + if (key_copy != "") { + callback := MoveWindowToDesktopNumber.Bind(num_copy) + RegisterDesktopHotkey("move_absolute", key_copy, num_copy, callback) + } + LogVirtualDesktopHotkeys("map move hotkey=" key_copy " desktop=" num_copy) +} +if (vd_desktop_hotkeys is Array && vd_desktop_hotkeys.Length > 0) { + LogVirtualDesktopHotkeys("desktop_hotkeys_count=" vd_desktop_hotkeys.Length) + for _, entry in vd_desktop_hotkeys { + if !(entry is Map) + continue + if !entry.Has("hotkey") || !entry.Has("desktop") + continue + hotkey_name := NormalizeAltHotkey(entry["hotkey"], true) + desktop_num := entry["desktop"] + key_copy := hotkey_name + num_copy := desktop_num + if (key_copy != "") { + callback := MoveWindowToDesktopNumber.Bind(num_copy) + RegisterDesktopHotkey("move_absolute", key_copy, num_copy, callback) + } + LogVirtualDesktopHotkeys("map move hotkey=" key_copy " desktop=" num_copy) + } +} HotIf Hotkey("!-", MinimizeWindow) @@ -332,6 +928,46 @@ ExitMoveMode() { UpdateCommandToastVisibility() } +BeginSuperDrag(*) { + if !IsSuperKeyPressed() + return + if Window.IsMoveMode() + return + MouseGetPos(, , &hwnd) + if !hwnd + return + if Window.IsException("ahk_id " hwnd) + return + if (WinGetMinMax("ahk_id " hwnd) = -1) + return + WinActivate "ahk_id " hwnd + DllCall("ReleaseCapture") + SendMessage(0xA1, 2, 0, , "ahk_id " hwnd) +} + +MinimizeOtherWindows(*) { + active_hwnd := WinGetID("A") + if !active_hwnd + return + active_monitor := Screen.FromWindow("ahk_id " active_hwnd) + for _, hwnd in WinGetList() { + if (hwnd = active_hwnd) + continue + if Window.IsException("ahk_id " hwnd) + continue + if (WinGetMinMax("ahk_id " hwnd) = -1) + continue + ex_style := WinGetExStyle("ahk_id " hwnd) + if (ex_style & 0x80) || (ex_style & 0x8000000) + continue + if !(WinGetStyle("ahk_id " hwnd) & 0x10000000) + continue + if (Screen.FromWindow("ahk_id " hwnd) != active_monitor) + continue + WinMinimize "ahk_id " hwnd + } +} + ActivateMostRecentWindow(exclude_hwnd := 0) { z_list := WinGetList() for _, hwnd in z_list { diff --git a/src/hotkeys/window_walker.ahk b/src/hotkeys/window_walker.ahk index af6ed0e..e406cc7 100644 --- a/src/hotkeys/window_walker.ahk +++ b/src/hotkeys/window_walker.ahk @@ -1,6 +1,6 @@ -global Config, super_key +global Config if Config.Has("window_selector") && Config["window_selector"]["enabled"] { hotkey_name := Config["window_selector"]["hotkey"] - Hotkey(super_key " & " hotkey_name, (*) => WindowWalker.Show()) + RegisterSuperComboHotkey(hotkey_name, (*) => WindowWalker.Toggle()) } diff --git a/src/lib/VD.ahk b/src/lib/VD.ahk new file mode 100644 index 0000000..01c5ac4 --- /dev/null +++ b/src/lib/VD.ahk @@ -0,0 +1,1231 @@ +#requires AutoHotkey v2.1-alpha.5 + +class VD { + class Array { + static find(arr, callback) { + for v in arr { + if (callback(v)) { + return v + } + } + return VD.Null + } + static flatMap(iterator, callback) { + arr := [] + if (callback.IsVariadic || callback.MaxParams > 1) { + idx := 1 + for v in iterator { + arr.Push(callback(v, idx)*) + ++idx + } + } else if (callback.MaxParams == 1) { + for v in iterator { + arr.Push(callback(v)*) + } + } else { + throw "Invalid Callback With MaxParams == 0" + } + return arr + } + } + + static Null := {} + + static versions := [ + { + buildNumber: 20348, + revisionNumber: 0, + IID_IVirtualDesktopManagerInternal_str: "{f31574d6-b682-4cdc-bd56-1827860abec6}", + IID_IVirtualDesktop_str: "{ff72ffdd-be7e-43fc-9c03-ad81681e88e4}", + IID_IVirtualDesktopNotification_str: "{c179334c-4295-40d3-bea1-c654d965605a}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_Normal, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:10, + idx_RemoveDesktop:11, + idx_FindDesktop:12, + ; get/set + idx_GetName:-1, + idx_SetDesktopName:-1, + idx_GetWallpaper:-1, + idx_SetDesktopWallpaper:-1, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_Normal, + IVirtualDesktopNotification_methods_count: 9, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:8, + }, + { + buildNumber: 22000, + revisionNumber: 0, + IID_IVirtualDesktopManagerInternal_str: "{094afe11-44f2-4ba0-976f-29a97e263ee0}", + IID_IVirtualDesktop_str: "{62fdf88b-11ca-4afb-8bd8-2296dfae49e2}", + IID_IVirtualDesktopNotification_str: "{f3163e11-6b04-433c-a64b-6f82c9094257}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_HMONITOR, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:10, + idx_RemoveDesktop:11, + idx_FindDesktop:12, + ; get/set + idx_GetName:6, + idx_SetDesktopName:14, + idx_GetWallpaper:-1, + idx_SetDesktopWallpaper:-1, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_Normal, + IVirtualDesktopNotification_methods_count: 11, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:10, + }, + { + buildNumber: 22483, + revisionNumber: 0, + IID_IVirtualDesktopManagerInternal_str: "{b2f925b9-5a0f-4d2e-9f4d-2b1507593c10}", + IID_IVirtualDesktop_str: "{536d3495-b208-4cc9-ae26-de8111275bf8}", + IID_IVirtualDesktopNotification_str: "{cd403e52-deed-4c13-b437-b98380f2b1e8}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_HMONITOR, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:10, + idx_RemoveDesktop:12, + idx_FindDesktop:13, + ; get/set + idx_GetName:6, + idx_SetDesktopName:15, + idx_GetWallpaper:7, + idx_SetDesktopWallpaper:16, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_IObjectArray, + IVirtualDesktopNotification_methods_count: 13, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:11, + }, + { + buildNumber: 22621, + revisionNumber: 2215, + IID_IVirtualDesktopManagerInternal_str: "{b2f925b9-5a0f-4d2e-9f4d-2b1507593c10}", + IID_IVirtualDesktop_str: "{536d3495-b208-4cc9-ae26-de8111275bf8}", + IID_IVirtualDesktopNotification_str: "{cd403e52-deed-4c13-b437-b98380f2b1e8}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_HMONITOR, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:8, + idx_SwitchDesktop:10, + idx_CreateDesktop:11, + idx_RemoveDesktop:13, + idx_FindDesktop:14, + ; get/set + idx_GetName:6, + idx_SetDesktopName:16, + idx_GetWallpaper:7, + idx_SetDesktopWallpaper:17, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_IObjectArray, + IVirtualDesktopNotification_methods_count: 13, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:11, + }, + { + buildNumber: 22631, + revisionNumber: 3085, + IID_IVirtualDesktopManagerInternal_str: "{a3175f2d-239c-4bd2-8aa0-eeba8b0b138e}", + IID_IVirtualDesktop_str: "{3f07f4be-b107-441a-af0f-39d82529072c}", + IID_IVirtualDesktopNotification_str: "{b287fa1c-7771-471a-a2df-9b6b21f0d675}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_Normal, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:10, + idx_RemoveDesktop:12, + idx_FindDesktop:13, + ; get/set + idx_GetName:5, + idx_SetDesktopName:15, + idx_GetWallpaper:6, + idx_SetDesktopWallpaper:16, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_Normal, + IVirtualDesktopNotification_methods_count: 14, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:10, + }, + { + buildNumber: 26100, + revisionNumber: 0, + IID_IVirtualDesktopManagerInternal_str: "{53f5ca0b-158f-4124-900c-057158060b27}", + IID_IVirtualDesktop_str: "{3f07f4be-b107-441a-af0f-39d82529072c}", + IID_IVirtualDesktopNotification_str: "{b9e5e94d-233e-49ab-af5c-2b4541c3aade}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_Normal, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:10, + idx_RemoveDesktop:12, + idx_FindDesktop:13, + ; get/set + idx_GetName:5, + idx_SetDesktopName:15, + idx_GetWallpaper:6, + idx_SetDesktopWallpaper:16, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_Normal, + IVirtualDesktopNotification_methods_count: 14, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:10, + }, + { + buildNumber: 99999, + revisionNumber: 0, + IID_IVirtualDesktopManagerInternal_str: "{53f5ca0b-158f-4124-900c-057158060b27}", + IID_IVirtualDesktop_str: "{3f07f4be-b107-441a-af0f-39d82529072c}", + IID_IVirtualDesktopNotification_str: "{b9e5e94d-233e-49ab-af5c-2b4541c3aade}", + ; vtbl + IVirtualDesktopManagerInternal: VD.IVirtualDesktopManagerInternal_Normal, + idx_MoveViewToDesktop:4, + idx_GetCurrentDesktop:6, + idx_GetDesktops:7, + idx_SwitchDesktop:9, + idx_CreateDesktop:11, + idx_RemoveDesktop:13, + idx_FindDesktop:14, + ; get/set + idx_GetName:5, + idx_SetDesktopName:16, + idx_GetWallpaper:6, + idx_SetDesktopWallpaper:17, + ;vtbl Notification + IVirtualDesktopNotification: VD.IVirtualDesktopNotification_Normal, + IVirtualDesktopNotification_methods_count: 14, + idx_VirtualDesktopCreated:3, + idx_VirtualDesktopDestroyed:6, + idx_CurrentVirtualDesktopChanged:10, + }, + ] ; default: version: 99999 ; (just a big number like Infinity) + + static __New() { + OS_Version := FileGetVersion(A_WinDir "\System32\twinui.pcshell.dll") + splitByDot:=StrSplit(OS_Version, ".") + buildNumber:=Integer(splitByDot[3]) + revisionNumber:=Integer(splitByDot[4]) + VD.version := VD.Array.find(VD.versions, + version => (buildNumber < version.buildNumber || (buildNumber == version.buildNumber && revisionNumber < version.revisionNumber)) + ) + DllCall("ole32\CLSIDFromString", "WStr", VD.version.IID_IVirtualDesktop_str, "Ptr", VD.version.IID_IVirtualDesktop_ptr := Buffer(0x10)) + + VD.ret := Buffer(1, 0xc3) + DllCall("VirtualProtect", "Ptr", VD.ret, "Ptr", 1, "Uint", 0x40, "Uint*", &lpflOldProtect:=0) ;0x40=PAGE_EXECUTE_READWRITE ;WRITE is needed because it changes more than 1 byte + + VD.ListenersCurrentVirtualDesktopChanged := Map() + VD.WinActivate_callback := 0 + + VD.DesktopIdMap := Map() + + VD.IApplicationViewCollection := VD.IApplicationViewCollection_Class() + VD.IVirtualPinnedAppsHandler := VD.IVirtualPinnedAppsHandler_Class() + VD.IVirtualDesktopNotificationService := VD.IVirtualDesktopNotificationService_Class() + VD.IVirtualDesktopNotification := VD.version.IVirtualDesktopNotification() + VD.IVirtualDesktopManagerInternal := VD.version.IVirtualDesktopManagerInternal() + + (VD.LocalizedWord_TaskView) ; "Task View" + + OnMessage(DllCall("RegisterWindowMessageW","WStr","TaskbarCreated","Uint"), (*) => (VD.reinit(), "")) + VD.reinit() + if (VD.waiting) { ; block / poll because it may be immediately used, ex: VD.createUntil(3) + loop 50 { ; 10 seconds + Sleep 200 + if (!VD.waiting) { + break + } + } + } + } + + static reinit() { + try { + CImmersiveShell_IServiceProvider := ComObject("{c2f03a33-21f5-47fa-b4bb-156362a2f239}", "{6d5140c1-7436-11ce-8034-00aa006009fa}") + VD.IApplicationViewCollection.reinit(CImmersiveShell_IServiceProvider) + VD.IVirtualPinnedAppsHandler.reinit(CImmersiveShell_IServiceProvider) + VD.IVirtualDesktopNotificationService.reinit(CImmersiveShell_IServiceProvider) + VD.IVirtualDesktopNotificationService.Register(VD.IVirtualDesktopNotification) + VD.IVirtualDesktopManagerInternal.reinit(CImmersiveShell_IServiceProvider) + + VD.IVirtualDesktopListChanged() + VD.currentDesktopNum := VD.IVirtualDesktopMap[VD.IVirtualDesktopManagerInternal.GetCurrentDesktop()] + VD.waiting := false + return + } + VD.waiting := true + } + + static IVirtualDesktopListChanged() { + VD.IVirtualDesktopList := [VD.IObjectArray(VD.IVirtualDesktopManagerInternal.GetDesktops(), VD.version.IID_IVirtualDesktop_ptr)*] + VD.IVirtualDesktopMap := Map(VD.Array.flatMap(VD.IVirtualDesktopList, (IVirtualDesktop, i) => [IVirtualDesktop, i])*) + } + + class IApplicationViewCollection_Class { + reinit(CImmersiveShell_IServiceProvider) { + this.IApplicationViewCollection := ComObjQuery(CImmersiveShell_IServiceProvider, "{1841c6d7-4f9d-42c0-af41-8747538f10e5}", "{1841c6d7-4f9d-42c0-af41-8747538f10e5}") + } + GetViewForHwnd(HWND) { + hr := ComCall(6, this.IApplicationViewCollection, "Ptr", HWND, "Ptr*", &IApplicationView := 0, "Uint") + return IApplicationView + } + } + + static WinActivatePriority := { + NewWindow: "new window", + OldWindowExcludeDesktop: "old window - Exclude Desktop", + OldWindowIncludeDesktop: "old window - Include Desktop", + } + + static ModulusResolveDesktopNum(desktopNum) { + desktopNum := Mod(desktopNum, VD.IVirtualDesktopList.Length) + if (desktopNum <= 0) { + desktopNum := desktopNum + VD.IVirtualDesktopList.Length + } + return desktopNum + } + + static goToRelativeDesktopNum(relative_count) { + absolute_desktopNum := VD.modulusResolveDesktopNum(VD.currentDesktopNum + relative_count) + return VD.goToDesktopNum(absolute_desktopNum) + } + + static MoveWindowToRelativeDesktopNum(wintitle, relative_count, follow := false, WinActivatePriority := VD.WinActivatePriority.NewWindow) { + absolute_desktopNum := VD.modulusResolveDesktopNum(VD.currentDesktopNum + relative_count) + return VD.MoveWindowToDesktopNum(wintitle, absolute_desktopNum, follow, WinActivatePriority) + } + + static MoveWindowToCurrentDesktop(wintitle, activateYourWindow := true) { + return VD.MoveWindowToDesktopNum(wintitle, VD.currentDesktopNum, activateYourWindow) + } + + static TryWinGetID(wintitle) { + loop 3 { + hwnd := VD.FindFirstWindowInAllDesktops(wintitle) + if (hwnd) { + break + } + Sleep 10 + } + return hwnd + } + + static MoveWindowToDesktopNum(wintitle, desktopNum, follow := false, WinActivatePriority := VD.WinActivatePriority.NewWindow) { + loop 1 { + hwnd := VD.TryWinGetID(wintitle) + if (!hwnd) { + desktopNum := -1 + break + } + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + if (!IApplicationView) { + desktopNum := -1 + break + } + window_desktopNum := VD.getDesktopNumOfHWND(hwnd) + activeWindow := WinGetID("A") + if (window_desktopNum !== desktopNum) { + VD.IVirtualDesktopManagerInternal.MoveViewToDesktop(IApplicationView, VD.IVirtualDesktopList[desktopNum]) + if (!follow && activeWindow == hwnd) { + VD.WinActivateFirstWindowInCurrentDesktop(50) + Sleep 100 ; don't want to switch the changing foreground window + } + } + if (follow) { + if (desktopNum == VD.currentDesktopNum) { + VD.SetForegroundWindow(hwnd) + } else { + VD.RegisterWinActivateUponSwitch(hwnd) + if (activeWindow !== hwnd) { + VD.AllowSetForegroundWindowAny() + } + VD.IVirtualDesktopManagerInternal.SwitchDesktop(VD.IVirtualDesktopList[desktopNum]) + } + } + } + return desktopNum + } + + static getDesktopNumOfHWND(hwnd) { + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + if (!IApplicationView) { + return 0 + } + DesktopId := VD.IApplicationView_Class(IApplicationView).GetVirtualDesktopId() + DesktopId_GUID_str := VD._StringFromGUID(DesktopId) + switch DesktopId_GUID_str { + ; https://github.com/MScholtes/VirtualDesktop/blob/a725cbd3cdb9e977678eeaf034a7cc96d2e74bc6/VirtualDesktop11.cs#L329 + ; https://github.com/MScholtes/VirtualDesktop/commit/614c7176a384116afcacac4650d445d7a31f645d + ; CVirtualDesktopVisibilityPolicy::PinViewToAllDesktops(struct IApplicationView * __ptr64,bool) __ptr64 + ; bool:true -> "{BB64D5B7-4DE3-4AB2-A87C-DB7601AEA7DC}" ; AppOnAllDesktops + ; bool:false -> "{C2DDEA68-66F2-4CF9-8264-1BFD00FBBBAC}" ; WindowOnAllDesktops + case "{BB64D5B7-4DE3-4AB2-A87C-DB7601AEA7DC}": return -1 ; PinAppID + case "{C2DDEA68-66F2-4CF9-8264-1BFD00FBBBAC}": return -2 ; PinView (unless IsAppIdPinned is set) + default: + if (VD.DesktopIdMap.Has(DesktopId_GUID_str)) { ;cache + IVirtualDesktop_found := VD.DesktopIdMap[DesktopId_GUID_str] + } else { + IVirtualDesktop_found := VD.IVirtualDesktopManagerInternal.FindDesktop(DesktopId) + if (!IVirtualDesktop_found) { + throw Error("Desktop not found: " DesktopId_GUID_str) + } + VD.DesktopIdMap[DesktopId_GUID_str] := IVirtualDesktop_found + } + return VD.IVirtualDesktopMap[IVirtualDesktop_found] + } + } + + static getDesktopNumOfWindow(wintitle) { + hwnd := WinGetID(wintitle) + return VD.getDesktopNumOfHWND(hwnd) + } + + static FindFirstWindowInAllDesktops(wintitle) { + bak_A_DetectHiddenWindows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + hwnd_list := WinGetList(wintitle) + A_DetectHiddenWindows := bak_A_DetectHiddenWindows + found := VD._FindValidWindow(hwnd_list) + return found + } + + static goToDesktopOfWindow(wintitle, activateYourWindow := true) { + hwnd := VD.FindFirstWindowInAllDesktops(wintitle) + if (!hwnd) { + VD._UnblockVDFunctionRunning() + throw Error("Window not found: " wintitle) + } + desktopNum := VD.getDesktopNumOfHWND(hwnd) + if (desktopNum == VD.currentDesktopNum) { + if (activateYourWindow) { + VD.SetForegroundWindow(hwnd) + } + } else { + if (activateYourWindow) { + VD.RegisterWinActivateUponSwitch(hwnd) + } + VD.AllowSetForegroundWindowAny() + VD.IVirtualDesktopManagerInternal.SwitchDesktop(VD.IVirtualDesktopList[desktopNum]) + } + return desktopNum + } + + static SetForegroundWindow(hWnd, waitCompletionDelay := 0) { + if (DllCall("AllowSetForegroundWindow", "Uint", DllCall("GetCurrentProcessId"))) { + DllCall("SetForegroundWindow", "Ptr", hwnd) + } else { + LCtrlDown := GetKeyState("LCtrl") + RCtrlDown := GetKeyState("RCtrl") + LShiftDown := GetKeyState("LShift") + RShiftDown := GetKeyState("RShift") + LWinDown := GetKeyState("LWin") + RWinDown := GetKeyState("RWin") + LAltDown := GetKeyState("LAlt") + RAltDown := GetKeyState("RAlt") + if ((LCtrlDown || RCtrlDown) && (LWinDown || RWinDown)) { + toRelease := "" + if (LShiftDown) { + toRelease .= "{LShift Up}" + } + if (RShiftDown) { + toRelease .= "{RShift Up}" + } + if (toRelease) { + Send "{Blind}" toRelease + } + } + BlockInput "On" + Send "{LAlt Down}{LAlt Down}" + DllCall("SetForegroundWindow", "Ptr", hwnd) + toAppend := "" + if (!LAltDown) { + toAppend .= "{LAlt Up}" + } + if (RAltDown) { + toAppend .= "{RAlt Down}" + } + if (LCtrlDown) { + toAppend .= "{LCtrl Down}" + } + if (RCtrlDown) { + toAppend .= "{RCtrl Down}" + } + if (LShiftDown) { + toAppend .= "{LShift Down}" + } + if (RShiftDown) { + toAppend .= "{RShift Down}" + } + if (LWinDown) { + toAppend .= "{LWin Down}" + } + if (RWinDown) { + toAppend .= "{RWin Down}" + } + if (toAppend) { + Send "{Blind}" toAppend + } + BlockInput "Off" + } + if (waitCompletionDelay) { + end := A_TickCount + waitCompletionDelay + while (A_TickCount < end) { + if (DllCall("GetForegroundWindow", "Ptr") == hWnd) { + break + } + } + } + } + + static AllowSetForegroundWindowAny() { + VD_animation_gui := Gui("-Border -SysMenu +Owner -Caption") + this.SetForegroundWindow(VD_animation_gui.Hwnd) + DllCall("AllowSetForegroundWindow", "Uint", 0xFFFFFFFF) ;ASFW_ANY + } + + static FindFirstWindowInCurrentDesktop() { + bak_A_DetectHiddenWindows := A_DetectHiddenWindows + A_DetectHiddenWindows := false + hwnd_list := WinGetList() + A_DetectHiddenWindows := bak_A_DetectHiddenWindows + found := VD._FindValidWindow(hwnd_list, true) + return found + } + + static WinActivateFirstWindowInCurrentDesktop(waitCompletionDelay := 0) { + hwnd := VD.FindFirstWindowInCurrentDesktop() + if (!hwnd) { + VD.SetForegroundWindow(WinGetID("ahk_class Progman ahk_exe explorer.exe")) ; Desktop + } else { + VD.SetForegroundWindow(hwnd, waitCompletionDelay) + } + } + + static ShouldActivateUponArrival() { + if (WinActive(VD.LocalizedWord_TaskView " ahk_exe explorer.exe")) { + return false + } + return true + } + + static RegisterWinActivateUponSwitch(hwnd) { + VD.WinActivate_callback := callback := () { + VD.WinActivate_callback := 0 + if (hwnd == 0) { + VD.WinActivateFirstWindowInCurrentDesktop() + } else { + if (VD._isMinimizedWindow(hwnd)) { + DllCall("ShowWindow", "Ptr", hwnd, "Uint", 9) ;SW_RESTORE + } + VD.SetForegroundWindow(hwnd) + } + } + SetTimer () { + if (VD.WinActivate_callback == callback) { + VD.WinActivate_callback := 0 + } + }, -1000 + } + + static _FindValidWindow(hwnd_list, isNotMinized := false) { + already_hwnd := Map() + _innerFindValidWindow(hwnd) { + loop 1 { + if (already_hwnd.Has(hwnd)) { + found := already_hwnd[hwnd] + break + } + owner := DllCall("GetWindow", "Ptr", hwnd, "Uint", 4) + if (owner) { + found := _innerFindValidWindow(owner) + break + } + dwStyle := DllCall("GetWindowLongPtrW", "Ptr", hWnd, "Int", -16, "Ptr") + if (!(dwStyle & 0x10000000)) { ;WS_VISIBLE + found := false + break + } + if (isNotMinized && (dwStyle & 0x20000000)) { ; WS_MINIMIZE + found := false + break + } + dwExStyle := DllCall("GetWindowLongPtrW", "Ptr", hWnd, "Int", -20, "Ptr") + if (dwExStyle & 0x00040000) { ;WS_EX_APPWINDOW + found := hwnd + break + } + if (dwExStyle & 0x08000080) { ; WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW + found := false + break + } + found := hwnd + } + already_hwnd[hwnd] := found + return found + } + found := false + for (hwnd in hwnd_list) { + if ((found := _innerFindValidWindow(hwnd))) { + break + } + } + return found + } + + static _isMinimizedWindow(hWnd) { + dwStyle := DllCall("GetWindowLongPtrW", "Ptr", hWnd, "Int", -16, "Ptr") + if (dwStyle & 0x20000000) { ; WS_MINIMIZE + return true + } + return false + } + + static WaitDesktopSwitched(desktopNum, waitMiliseconds := 1000, additionalWaitMiliseconds := 100) { + if (desktopNum <= 0) { + return + } + loop 1 { + end := A_TickCount + waitMiliseconds + while (A_TickCount < end) { + Critical + if (VD.currentDesktopNum == desktopNum) { + Critical "Off" + Sleep additionalWaitMiliseconds ; additional sleep + return true + } + Critical "Off" + Sleep -1 + } + Sleep additionalWaitMiliseconds ; additional sleep, just in case + return false + } + } + + static goToDesktopNum(desktopNum) { + if (desktopNum == VD.currentDesktopNum) { + if (VD.ShouldActivateUponArrival()) { + VD.WinActivateFirstWindowInCurrentDesktop() + } + } else { + if (VD.ShouldActivateUponArrival()) { + VD.RegisterWinActivateUponSwitch(0) + VD.AllowSetForegroundWindowAny() + } + VD.IVirtualDesktopManagerInternal.SwitchDesktop(VD.IVirtualDesktopList[desktopNum]) + } + return desktopNum + } + + static getCurrentDesktopNum() { + return VD.currentDesktopNum + } + static getCount() { + return VD.IVirtualDesktopList.Length + } + + static createDesktop(goThere := false) { + IVirtualDesktop_ofNewDesktop := VD.IVirtualDesktopManagerInternal.CreateDesktop() + if (goThere) { + VD.IVirtualDesktopManagerInternal.SwitchDesktop(IVirtualDesktop_ofNewDesktop) + } + } + static createUntil(howMany, goToLastlyCreated := false) { + howManyThereAlreadyAre := VD.IVirtualDesktopList.Length + if (howManyThereAlreadyAre >= howMany) { ; ensure at least 1 was created for goToLastlyCreated:true + return + } + loop (howMany - howManyThereAlreadyAre) { + IVirtualDesktop_ofNewDesktop := VD.IVirtualDesktopManagerInternal.CreateDesktop() + } + if (goToLastlyCreated) { + VD.IVirtualDesktopManagerInternal.SwitchDesktop(IVirtualDesktop_ofNewDesktop) + } + } + static removeDesktop(desktopNum := VD.currentDesktopNum, fallback_desktopNum := -1) { + if (VD.IVirtualDesktopList.Length == 1) { + return ;can't delete last + } + if (desktopNum == VD.currentDesktopNum) { + if (fallback_desktopNum == -1) { ; provide default, do not verify fallback_desktopNum if provided + fallback_desktopNum := VD.modulusResolveDesktopNum(VD.currentDesktopNum - 1) + } + } else { ; there's really no need for a fallback if it's not the current + fallback_desktopNum := VD.currentDesktopNum + } + VD.IVirtualDesktopManagerInternal.RemoveDesktop(VD.IVirtualDesktopList[desktopNum], VD.IVirtualDesktopList[fallback_desktopNum]) + } + + static IsWindowPinned(wintitle) { + hwnd := WinGetID(wintitle) + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + viewIsPinned := VD.IVirtualPinnedAppsHandler.IsViewPinned(IApplicationView) + return viewIsPinned + } + static PinWindow(wintitle) { + hwnd := WinGetID(wintitle) + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + VD.IVirtualPinnedAppsHandler.PinView(IApplicationView) + } + static UnPinWindow(wintitle) { + hwnd := WinGetID(wintitle) + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + VD.IVirtualPinnedAppsHandler.UnpinView(IApplicationView) + } + static TogglePinWindow(wintitle) { + hwnd := WinGetID(wintitle) + IApplicationView := VD.IApplicationViewCollection.GetViewForHwnd(hwnd) + viewIsPinned := VD.IVirtualPinnedAppsHandler.IsViewPinned(IApplicationView) + if (viewIsPinned) { + VD.IVirtualPinnedAppsHandler.UnpinView(IApplicationView) + } else { + VD.IVirtualPinnedAppsHandler.PinView(IApplicationView) + } + } + + static IsExePinned(exe_path) { + appIsPinned := VD.IVirtualPinnedAppsHandler.IsAppIdPinned(exe_path) + return appIsPinned + } + static PinExe(exe_path) { + VD.IVirtualPinnedAppsHandler.PinAppID(exe_path) + } + static UnPinExe(exe_path) { + VD.IVirtualPinnedAppsHandler.UnpinAppID(exe_path) + } + static TogglePinExe(exe_path) { + appIsPinned := VD.IVirtualPinnedAppsHandler.IsAppIdPinned(exe_path) + if (appIsPinned) { + VD.IVirtualPinnedAppsHandler.UnpinAppID(exe_path) + } else { + VD.IVirtualPinnedAppsHandler.PinAppID(exe_path) + } + } + + static IsAppPinned(wintitle) { + return VD.IsExePinned(WinGetProcessPath(wintitle)) + } + static PinApp(wintitle) { + VD.PinExe(WinGetProcessPath(wintitle)) + } + static UnPinApp(wintitle) { + VD.UnPinExe(WinGetProcessPath(wintitle)) + } + static TogglePinApp(wintitle) { + VD.TogglePinExe(WinGetProcessPath(wintitle)) + } + + class IVirtualPinnedAppsHandler_Class { + reinit(CImmersiveShell_IServiceProvider) { + this.IVirtualDesktopPinnedApps := ComObjQuery(CImmersiveShell_IServiceProvider, "{b5a399e7-1c87-46b8-88e9-fc5747b171bd}", "{4ce81583-1e4c-4632-a621-07a53543148f}") + } + IsAppIdPinned(exe_path) { + ComCall(3, this.IVirtualDesktopPinnedApps, "WStr", exe_path, "Int*", &appIsPinned := 0) + return appIsPinned + } + PinAppID(exe_path) { + ComCall(4, this.IVirtualDesktopPinnedApps, "WStr", exe_path) + } + UnpinAppID(exe_path) { + ComCall(5, this.IVirtualDesktopPinnedApps, "WStr", exe_path) + } + IsViewPinned(IApplicationView) { + ComCall(6, this.IVirtualDesktopPinnedApps, "Ptr", IApplicationView, "Int*", &viewIsPinned := 0) + return viewIsPinned + } + PinView(IApplicationView) { + ComCall(7, this.IVirtualDesktopPinnedApps, "Ptr", IApplicationView) + } + UnpinView(IApplicationView) { + ComCall(8, this.IVirtualDesktopPinnedApps, "Ptr", IApplicationView) + } + } + + static LocalizedWord_TaskView => VD._LocalizedWord_TaskView ??= VD._get_LocalizedWord_TaskView() + static _LocalizedWord_TaskView := unset + static _get_LocalizedWord_TaskView() { + hModule := DllCall("LoadLibraryExW", "WStr", "twinui.pcshell.dll", "Ptr", 0, "Uint", 0x00000022, "Ptr") ;LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE + chars := 128 + lpBuffer := Buffer(chars << 1) + length := DllCall("LoadStringW", "Uint", hModule, "Uint", 1512, "Ptr", lpBuffer, "Int", chars) + _LocalizedWord_TaskView := StrGet(lpBuffer, length, "UTF-16") + DllCall("FreeLibrary", "Ptr", hModule) + return _LocalizedWord_TaskView + } + + static LocalizedWord_Desktop => VD._LocalizedWord_Desktop ??= VD._get_LocalizedWord_Desktop() + static _LocalizedWord_Desktop := unset + static _get_LocalizedWord_Desktop() { + hModule := DllCall("LoadLibraryExW", "WStr", "shell32.dll", "Ptr", 0, "Uint", 0x00000022, "Ptr") ;LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE + chars := 128 + lpBuffer := Buffer(chars << 1) + length := DllCall("LoadStringW", "Uint", hModule, "Uint", 21769, "Ptr", lpBuffer, "Int", chars) + _LocalizedWord_Desktop := StrGet(lpBuffer, length, "UTF-16") + DllCall("FreeLibrary", "Ptr", hModule) + return _LocalizedWord_Desktop + } + + static getNameFromDesktopNum(desktopNum) { + desktopName := "" + if (VD.version.idx_GetName > -1) { + IVirtualDesktop := VD.IVirtualDesktopList[desktopNum] + desktopName := VD.IVirtualDesktop_Class(IVirtualDesktop, VD.version).GetName() + } + if (!desktopName) { + desktopName := VD.LocalizedWord_Desktop " " desktopNum + } + return desktopName + } + + static getWallpaperFromDesktopNum(desktopNum) { + wallpaperPath := "" + if (VD.version.idx_GetWallpaper > -1) { + IVirtualDesktop := VD.IVirtualDesktopList[desktopNum] + wallpaperPath := VD.IVirtualDesktop_Class(IVirtualDesktop, VD.version).GetWallpaper() + } + return wallpaperPath + } + + static setNameToDesktopNum(desktopNum, desktopName) { + if (VD.version.idx_SetDesktopName > -1) { + IVirtualDesktop := VD.IVirtualDesktopList[desktopNum] + VD.IVirtualDesktopManagerInternal.SetDesktopName(IVirtualDesktop, desktopName) + } + } + + static setWallpaperToDesktopNum(desktopNum, wallpaperPath) { + if (VD.version.idx_SetDesktopWallpaper > -1) { + IVirtualDesktop := VD.IVirtualDesktopList[desktopNum] + VD.IVirtualDesktopManagerInternal.SetDesktopWallpaper(IVirtualDesktop, wallpaperPath) + } + } + + class IVirtualDesktop_Class { + __New(IVirtualDesktop, version) { + this.version := version + this.IVirtualDesktop := IVirtualDesktop + } + GetName() { + ComCall(this.version.idx_GetName, this.IVirtualDesktop, "Ptr*", &HSTRING := 0) + desktopName := StrGet(DllCall("combase\WindowsGetStringRawBuffer", "Ptr", HSTRING, "Uint*", &length := 0, "Ptr"), "UTF-16") + DllCall("combase\WindowsDeleteString", "Ptr", HSTRING) + return desktopName + } + GetWallpaper() { + ComCall(this.version.idx_GetWallpaper, this.IVirtualDesktop, "Ptr*", &HSTRING := 0) + wallpaperPath := StrGet(DllCall("combase\WindowsGetStringRawBuffer", "Ptr", HSTRING, "Uint*", &length := 0, "Ptr"), "UTF-16") + DllCall("combase\WindowsDeleteString", "Ptr", HSTRING) + return wallpaperPath + } + } + + class IApplicationView_Class { + __New(IApplicationView) { + this.IApplicationView := IApplicationView + } + GetVirtualDesktopId() { + ComCall(25, this.IApplicationView, "Ptr", DesktopId := Buffer(16)) ; GUID + return DesktopId + } + } + + class IVirtualDesktopManagerInternal_Normal { + __New(version) { + this.version := version + } + reinit(CImmersiveShell_IServiceProvider) { + this.IVirtualDesktopManagerInternal := ComObjQuery(CImmersiveShell_IServiceProvider, "{c5e0cdca-7b6e-41b2-9fc4-d93975cc467b}", this.version.IID_IVirtualDesktopManagerInternal_str) + } + ; interface + MoveViewToDesktop(IApplicationView, IVirtualDesktop) { + ComCall(this.version.idx_MoveViewToDesktop, this.IVirtualDesktopManagerInternal, "Ptr", IApplicationView, "Ptr", IVirtualDesktop) + } + GetCurrentDesktop() { + ComCall(this.version.idx_GetCurrentDesktop, this.IVirtualDesktopManagerInternal, "Ptr*", &IVirtualDesktop_current := 0) + return IVirtualDesktop_current + } + GetDesktops() { + ComCall(this.version.idx_GetDesktops, this.IVirtualDesktopManagerInternal, "Ptr*", &IObjectArray := 0) + return IObjectArray + } + SwitchDesktop(IVirtualDesktop) { + ComCall(this.version.idx_SwitchDesktop, this.IVirtualDesktopManagerInternal, "Ptr", IVirtualDesktop) + } + CreateDesktop() { + ComCall(this.version.idx_CreateDesktop, this.IVirtualDesktopManagerInternal, "Ptr*", &IVirtualDesktop_created := 0) + return IVirtualDesktop_created + } + RemoveDesktop(IVirtualDesktop, IVirtualDesktop_fallback) { + ComCall(this.version.idx_RemoveDesktop, this.IVirtualDesktopManagerInternal, "Ptr", IVirtualDesktop, "Ptr", IVirtualDesktop_fallback) + } + FindDesktop(DesktopId) { + ComCall(this.version.idx_FindDesktop, this.IVirtualDesktopManagerInternal, "Ptr", DesktopId, "Ptr*", &IVirtualDesktop_found := 0) + return IVirtualDesktop_found + } + SetDesktopName(IVirtualDesktop, desktopName) { + DllCall("combase\WindowsCreateString", "WStr", desktopName, "Uint", StrLen(desktopName), "Ptr*", &HSTRING := 0) + ComCall(this.version.idx_SetDesktopName, this.IVirtualDesktopManagerInternal, "Ptr", IVirtualDesktop, "Ptr", HSTRING) + DllCall("combase\WindowsDeleteString", "Ptr", HSTRING) + } + SetDesktopWallpaper(IVirtualDesktop, wallpaperPath) { + DllCall("combase\WindowsCreateString", "WStr", wallpaperPath, "Uint", StrLen(wallpaperPath), "Ptr*", &HSTRING := 0) + ComCall(this.version.idx_SetDesktopWallpaper, this.IVirtualDesktopManagerInternal, "Ptr", IVirtualDesktop, "Ptr", HSTRING) + DllCall("combase\WindowsDeleteString", "Ptr", HSTRING) + } + } + + class IVirtualDesktopManagerInternal_HMONITOR extends VD.IVirtualDesktopManagerInternal_Normal { + ; just set HMONITOR to 0 ? + GetCurrentDesktop() { + ComCall(this.version.idx_GetCurrentDesktop, this.IVirtualDesktopManagerInternal, "Ptr", 0, "Ptr*", &IVirtualDesktop_current := 0) + return IVirtualDesktop_current + } + GetDesktops() { + ComCall(this.version.idx_GetDesktops, this.IVirtualDesktopManagerInternal, "Ptr", 0, "Ptr*", &IObjectArray := 0) + return IObjectArray + } + SwitchDesktop(IVirtualDesktop) { + ComCall(this.version.idx_SwitchDesktop, this.IVirtualDesktopManagerInternal, "Ptr", 0, "Ptr", IVirtualDesktop) + } + CreateDesktop() { + ComCall(this.version.idx_CreateDesktop, this.IVirtualDesktopManagerInternal, "Ptr", 0, "Ptr*", &IVirtualDesktop_created := 0) + return IVirtualDesktop_created + } + } + + class IObjectArray { + __New(IObjectArray, IID_Interface_ptr) { + this.IObjectArray := IObjectArray + this.IID_Interface_ptr := IID_Interface_ptr + } + Length => _length ??= this.GetCount() + _length := unset + GetCount() { + ComCall(3, this.IObjectArray, "Uint*", &Count := 0) + return Count + } + GetAt(oneBasedIndex) { + ComCall(4, this.IObjectArray, "UInt", oneBasedIndex - 1, "Ptr", this.IID_Interface_ptr, "Ptr*", &IInterface:=0) + return IInterface + } + __Item[idx] => this.GetAt(idx) + __Enum(NumberOfVars) { + switch NumberOfVars { + case 1: return VD.Enumerator1(this) + case 2: return VD.Enumerator2(this) + default: throw "Invalid number of variables" + } + } + } + + class Enumerator1 { + __New(arr) { + this.arr := arr + this.idx := 1 + } + Call(&OutputVar1) { + if (this.idx <= this.arr.Length) { + OutputVar1 := this.arr[this.idx] + ++this.idx + return 1 + } + return 0 + } + } + + class Enumerator2 { + __New(arr) { + this.arr := arr + this.idx := 1 + } + Call(&OutputVar1, &OutputVar2) { + if (this.idx <= this.arr.Length) { + OutputVar1 := this.idx + OutputVar2 := this.arr[this.idx] + ++this.idx + return 1 + } + return 0 + } + } + + class IVirtualDesktopNotificationService_Class { + reinit(CImmersiveShell_IServiceProvider) { + this.IVirtualDesktopNotificationService := ComObjQuery(CImmersiveShell_IServiceProvider, "{a501fdec-4a09-464c-ae4e-1b9c21b84918}", "{0cd45e71-d927-4f15-8b0a-8fef525337bf}") + } + Register(IVirtualDesktopNotification) { + ComCall(3,this.IVirtualDesktopNotificationService,"Ptr",IVirtualDesktopNotification,"Uint*",pdwCookie:=0) ;3=Register + } + } + + class IVirtualDesktopNotification_Normal { + __New(version) { + this.version := version + ; obj and vtbl in the same buffer, obj only contain vtbl_pointer + this.obj_and_vtbl := Buffer(A_PtrSize*(version.IVirtualDesktopNotification_methods_count+1)) + this.vtbl := this.obj_and_vtbl.Ptr + A_PtrSize + NumPut("Ptr", this.vtbl, this.obj_and_vtbl) + offset := 0 + loop version.IVirtualDesktopNotification_methods_count { + NumPut("Ptr", VD.ret.Ptr, this.vtbl, offset) + offset += A_PtrSize + } + + ; QueryInterface only called during IVirtualDesktopNotificationService::Register, thread-safe (hopefully) + NumPut("Ptr", VD.BoundCallbackCreate(CallbackCreate, this.QueryInterface, this), this.vtbl) + + ; PostMessage + CallbackCreate_PostMessage := VD.CallbackCreate_PostMessage.Bind(VD) + NumPut("Ptr", VD.BoundCallbackCreate(CallbackCreate_PostMessage, this.VirtualDesktopCreated, this), this.vtbl, version.idx_VirtualDesktopCreated*A_PtrSize) + NumPut("Ptr", VD.BoundCallbackCreate(CallbackCreate_PostMessage, this.VirtualDesktopDestroyed, this), this.vtbl, version.idx_VirtualDesktopDestroyed*A_PtrSize) + NumPut("Ptr", VD.BoundCallbackCreate(CallbackCreate_PostMessage, this.CurrentVirtualDesktopChanged, this), this.vtbl, version.idx_CurrentVirtualDesktopChanged*A_PtrSize) + } + + Ptr => this.obj_and_vtbl.Ptr + + QueryInterface(that, riid, ppvObject) { + if (!ppvObject) { + return 0x80070057 ;E_INVALIDARG + } + + switch VD._StringFromGUID(riid), 0 { ;not case-sensitive + case "{00000000-0000-0000-c000-000000000046}", this.version.IID_IVirtualDesktopNotification_str: + NumPut("Ptr", that, ppvObject) ; *ppvObject = this; + return 0 ;S_OK + default: + NumPut("Ptr", 0, ppvObject) ; *ppvObject = NULL; + return 0x80004002 ;E_NOINTERFACE + } + } + + VirtualDesktopCreated() { + VD.IVirtualDesktopListChanged() + } + VirtualDesktopDestroyed() { + VD.IVirtualDesktopListChanged() ; called after CurrentVirtualDesktopChanged + } + _common_CurrentVirtualDesktopChanged(IVirtualDesktop_old, IVirtualDesktop_new) { + desktopNum_old := VD.IVirtualDesktopMap[IVirtualDesktop_old] + VD.currentDesktopNum := VD.IVirtualDesktopMap[IVirtualDesktop_new] + if (VD.WinActivate_callback) { + VD.WinActivate_callback.Call() + } + for k, _ in VD.ListenersCurrentVirtualDesktopChanged { + k(desktopNum_old, VD.currentDesktopNum) + } + } + CurrentVirtualDesktopChanged(that, IVirtualDesktop_old, IVirtualDesktop_new) { + this._common_CurrentVirtualDesktopChanged(IVirtualDesktop_old, IVirtualDesktop_new) + } + } + class IVirtualDesktopNotification_IObjectArray extends VD.IVirtualDesktopNotification_Normal { + CurrentVirtualDesktopChanged(that, IObjectArray, IVirtualDesktop_old, IVirtualDesktop_new) { + this._common_CurrentVirtualDesktopChanged(IVirtualDesktop_old, IVirtualDesktop_new) + } + } + static BoundCallbackCreate(func_CallbackCreate, callback, that) { + return func_CallbackCreate(callback.Bind(that),, callback.MinParams - 1) + } + + static _StringFromGUID(guid) { + DllCall("ole32\StringFromGUID2", "Ptr", guid, "ptr", buf := Buffer(78), "Int", 39) + return StrGet(buf, "UTF-16") + } + + static CallbackCreate_PostMessage(callback, Options := "", ParamCount := callback.MinParams) { + ; CallbackCreate for Multithreading (by) using the message queue + ;idea from: + ;Lexikos : RegisterSyncCallback (for multi-threaded APIs) : https://www.autohotkey.com/boards/viewtopic.php?t=21223 + ;https://github.com/thqby/ahk2_lib/blob/master/OVERLAPPED.ahk + static PostMessageW := DllCall('GetProcAddress', 'ptr', DllCall('GetModuleHandle', 'str', 'user32', 'ptr'), 'astr', 'PostMessageW', 'ptr') + static GlobalAlloc := DllCall('GetProcAddress', 'ptr', DllCall('GetModuleHandle', 'str', 'kernel32', 'ptr'), 'astr', 'GlobalAlloc', 'ptr') + static AHK_CallbackCreate_PostMessage := DllCall('RegisterWindowMessageW', 'WStr', 'AHK_CallbackCreate_PostMessage', 'uint') + static asm := init() + static init() { + OnMessage(AHK_CallbackCreate_PostMessage, A_PtrSize == 8 ? VD.CallbackCreate_PostMessage_event_x86_64.Bind(VD) : VD.CallbackCreate_PostMessage_event_x86.Bind(VD)) + if (A_PtrSize == 8) { + asm := Buffer(0x81) + ; params := [] + ; params.Push( + NumPut( + ; 55 | push rbp + ; 48 89 e5 | mov rbp, rsp + 'uint', 0xe5894855, + + ; save to shadow space ; https://github.com/simon-whitehead/assembly-fun/blob/master/windows-x64/README.md#shadow-space + ; 48 89 4c 24 10 | mov [rsp + 0x10], rcx + 'uchar', 0x48, 'uint', 0x10244c89, + ; 48 89 54 24 18 | mov [rsp + 0x18], rdx + 'uchar', 0x48, 'uint', 0x18245489, + ; 4c 89 44 24 20 | mov [rsp + 0x20], r8 + 'uchar', 0x4c, 'uint', 0x20244489, + ; 4c 89 4c 24 28 | mov [rsp + 0x28], r9 + 'uchar', 0x4c, 'uint', 0x28244c89, + + ; 48 83 ec 40 | sub rsp, 0x40 + 'uint', 0x40ec8348, + + ; The x64 ABI considers registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, and XMM6-XMM15 nonvolatile. + ; https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170 + ; save RSI, RDI, R12, R13 + ; 48 89 74 24 20 | mov [rsp + 0x20], rsi + 'uchar', 0x48, 'uint', 0x20247489, + ; 48 89 7c 24 28 | mov [rsp + 0x28], rdi + 'uchar', 0x48, 'uint', 0x28247c89, + ; 4c 89 64 24 30 | mov [rsp + 0x30], r12 + 'uchar', 0x4c, 'uint', 0x30246489, + ; 4c 89 6c 24 38 | mov [rsp + 0x38], r13 + 'uchar', 0x4c, 'uint', 0x38246c89, + + ; The x64 ABI considers the registers RAX, RCX, RDX, R8, R9, R10, R11, and XMM0-XMM5 volatile. + ; R10: ParamCount + ; R11: obj + ; 4d 89 d4 | mov r12, r10 + 'uchar', 0x4d, 'ushort', 0xd489, + ; 4d 89 dd | mov r13, r11 + 'uchar', 0x4d, 'ushort', 0xdd89, + + ; 31 c9 | xor ecx, ecx + 'ushort', 0xc931, + ; 4c 89 e2 | mov rdx, r12 + 'uchar', 0x4c, 'ushort', 0xe289, + ; 48 c1 e2 03 | shl rdx, 3 + 'uint', 0x03e2c148, + ; 48 b8 XX XX XX XX XX XX XX XX | mov rax, GlobalAlloc + 'ushort', 0xb848, "ptr", GlobalAlloc, + ; ff d0 | call rax + ; GlobalAlloc(GMEM_FIXED, A_PtrSize * ParamCount) + 'ushort', 0xd0ff, + + ; 48 8d 74 24 50 | lea rsi, [rsp + 0x50] + 'uchar', 0x48, 'uint', 0x5024748d, + ; 48 89 c7 | mov rdi, rax + ; no error handling, assumes GlobalAlloc succeeded + 'uchar', 0x48, 'ushort', 0xc789, + ; 4c 89 e1 | mov rcx, r12 + 'uchar', 0x4c, 'ushort', 0xe189, + ; could be memcpy + ; f3 48 a5 | rep movsq + 'uchar', 0xf3, 'ushort', 0xa548, + + ; 49 89 c1 | mov r9, rax + 'uchar', 0x49, 'ushort', 0xc189, + ; 4d 89 e8 | mov r8, r13 + 'uchar', 0x4d, 'ushort', 0xe889, + ; ba XX XX XX XX | mov edx, AHK_CallbackCreate_PostMessage + 'uchar', 0xba, 'uint', AHK_CallbackCreate_PostMessage, + ; b9 XX XX XX XX | mov ecx, A_ScriptHwnd ; assume handles are 32-bit + 'uchar', 0xb9, 'uint', A_ScriptHwnd, + ; 48 b8 XX XX XX XX XX XX XX XX | mov rax, PostMessageW + 'ushort', 0xb848, "ptr", PostMessageW, + ; ff d0 | call rax + ; PostMessageW(A_ScriptHwnd, AHK_CallbackCreate_PostMessage, obj, saved_params) + 'ushort', 0xd0ff, + + ; 48 83 c4 20 | add rsp, 0x20 + 'uint', 0x20c48348, + ; 5e | pop rsi + 'uchar', 0x5e, + ; 5f | pop rdi + 'uchar', 0x5f, + ; 41 5c | pop r12 + 'ushort', 0x5c41, + ; 41 5d | pop r13 + 'ushort', 0x5d41, + ; 5d | pop rbp + 'uchar', 0x5d, + ; c3 | ret + 'uchar', 0xc3, + asm + ) + + ; i:=1,sum:=0,end:=params.Length & ~1 + ; sizeMap:=Map() + ; sizeMap.CaseSense:=false + ; sizeMap.Set('uchar',1,"ushort",2,"uint",4,"ptr",A_PtrSize) + ; while (i <= end) { + ; sum+=sizeMap[params[i]] + ; i+=2 + ; } + } else { + throw Error("not implemented") + } + DllCall("VirtualProtect", "Ptr", asm, "Ptr", asm.Size, "Uint", 0x40, "Uint*", &lpflOldProtect:=0) ;0x40=PAGE_EXECUTE_READWRITE ;WRITE is needed because it changes more than 1 byte + return asm + } + + params:=[] + obj := {callback: callback, ParamCount: ParamCount} + + if (A_PtrSize == 8) { + stub := Buffer(0x15) + ; R10: ParamCount + ; R11: obj + NumPut( + ; 41 ba XX XX XX XX | mov r10d, ParamCount + "ushort", 0xba41, "uint", ParamCount, + ; 49 bb XX XX XX XX XX XX XX XX | mov r11, obj + "ushort", 0xbb49, "ptr", ObjPtr(obj), + ; e9 XX XX XX XX | jmp asm ; assumes no further than 0x7FFFFFFF + "uchar", 0xe9, "int", asm.Ptr - (stub.Ptr + stub.Size), + stub + ) + } else { + throw Error("not implemented") + } + DllCall("VirtualProtect", "Ptr", stub, "Ptr", stub.Size, "Uint", 0x40, "Uint*", &lpflOldProtect:=0) ;0x40=PAGE_EXECUTE_READWRITE ;WRITE is needed because it changes more than 1 byte + + if (!VD.CallbackFree_PostMessage.HasOwnProp("CallBacks")) { + VD.CallbackFree_PostMessage.CallBacks := Map() + } + VD.CallbackFree_PostMessage.CallBacks[stub.Ptr] := [stub, obj] + + return stub.Ptr + } + + static CallbackCreate_PostMessage_event_x86_64(wParam, lParam, msg, hwnd) { + Critical + + obj := ObjFromPtrAddRef(wParam) + + params:=[],i:=lParam,end:=i + (obj.ParamCount * A_PtrSize) + while (i < end) { + params.Push(NumGet(i,"Ptr")),i+=A_PtrSize + } + DllCall("GlobalFree","Ptr",lParam) + obj.callback.Call(params*) + } + static CallbackCreate_PostMessage_event_x86(wParam, lParam, msg, hwnd) { + Critical + + throw Error("not implemented") + } + + static CallbackFree_PostMessage(Address) { + if (VD.CallbackFree_PostMessage.HasOwnProp("CallBacks")) { + VD.CallbackFree_PostMessage.CallBacks.Delete(Address) + } + } + +} \ No newline at end of file diff --git a/src/lib/command_toast.ahk b/src/lib/command_toast.ahk index 0ea7af2..e8db702 100644 --- a/src/lib/command_toast.ahk +++ b/src/lib/command_toast.ahk @@ -11,6 +11,12 @@ global command_toast_icon_cache := Map() global command_toast_default_icon_index := 0 global command_toast_last_mode := "" global command_toast_normal_refresh_pending := false +global command_toast_visibility_timer := 0 +global command_toast_temp_visible := false +global command_toast_input_hook := 0 +global command_toast_input_timer := 0 +global command_toast_keydown_enabled := false +global command_toast_keydown_handler := 0 InitCommandToast() { global Config, AppState, command_helper_enabled @@ -20,22 +26,42 @@ InitCommandToast() { } UpdateCommandToastVisibility() { - global command_helper_enabled, super_key - if !command_helper_enabled { + global command_helper_enabled, command_toast_temp_visible + if !command_helper_enabled && !command_toast_temp_visible { + StopCommandToastVisibilityTimer() HideCommandToast() return } - if GetKeyState(super_key, "P") || ReloadModeActive() || Window.IsMoveMode() { - ShowCommandToast() + if ReloadModeActive() || Window.IsMoveMode() || command_toast_temp_visible { + ShowCommandToast(command_toast_temp_visible) + StartCommandToastVisibilityTimer() } else { + StopCommandToastVisibilityTimer() HideCommandToast() } } -ShowCommandToast() { +StartCommandToastVisibilityTimer() { + global command_toast_visibility_timer + if !command_toast_visibility_timer + command_toast_visibility_timer := CommandToastVisibilityTick + SetTimer(command_toast_visibility_timer, 200) +} + +StopCommandToastVisibilityTimer() { + global command_toast_visibility_timer + if command_toast_visibility_timer + SetTimer(command_toast_visibility_timer, 0) +} + +CommandToastVisibilityTick(*) { + UpdateCommandToastVisibility() +} + +ShowCommandToast(force_show := false) { global command_helper_enabled, command_toast_gui, command_toast_visible, command_toast_view_key, command_toast_last_mode, command_toast_normal_refresh_pending - if !command_helper_enabled + if !command_helper_enabled && !force_show return model := BuildCommandToastModel() @@ -76,14 +102,8 @@ ShowCommandToast() { margin := 16 if (margin > 64) margin := 64 - pos_x := right - w - margin - pos_y := bottom - h - margin - min_x := left + margin - max_x := right - w - margin - min_y := top + margin - max_y := bottom - h - margin - pos_x := Max(min_x, Min(pos_x, max_x)) - pos_y := Max(min_y, Min(pos_y, max_y)) + pos_x := left + (right - left - w) / 2 + pos_y := top + (bottom - top - h) / 2 command_toast_gui.Show("NoActivate x" pos_x " y" pos_y) if (model["mode"] = "normal" && command_toast_normal_refresh_pending) { RefreshCommandToastIcons(model) @@ -95,7 +115,7 @@ ShowCommandToast() { CreateCommandToastGui(model) { global command_toast_gui, command_toast_text, command_toast_apps_list, command_toast_actions_list, command_toast_image_list - command_toast_gui := Gui("+AlwaysOnTop -Caption +ToolWindow +Border", "be-there Command Overlay") + command_toast_gui := Gui("+AlwaysOnTop -Caption +ToolWindow +Border", "harken Command Overlay") command_toast_gui.MarginX := 12 command_toast_gui.MarginY := 10 opacity := NormalizeOverlayOpacity() @@ -172,11 +192,14 @@ RefreshCommandToastIcons(model) { } HideCommandToast() { - global command_toast_gui, command_toast_visible + global command_toast_gui, command_toast_visible, command_toast_temp_visible if command_toast_gui { command_toast_gui.Hide() command_toast_visible := false } + command_toast_temp_visible := false + StopCommandToastInputHook() + UnregisterCommandToastKeydown() } ToggleCommandHelper() { @@ -188,10 +211,82 @@ ToggleCommandHelper() { SaveState(AppState) status := command_helper_enabled ? "enabled" : "disabled" TrayTip("", "") - TrayTip("be-there", "Command overlay " status, 2) + TrayTip("harken", "Command overlay " status, 2) UpdateCommandToastVisibility() } +ShowCommandToastTemporary() { + global command_helper_enabled, command_toast_temp_visible + command_toast_temp_visible := true + ShowCommandToast(true) + StartCommandToastInputHook() + RegisterCommandToastKeydown() +} + +StartCommandToastInputHook() { + global command_toast_input_timer + if ReloadModeActive() || Window.IsMoveMode() + return + StopCommandToastInputHook() + command_toast_input_timer := CommandToastStartInputHook + SetTimer(command_toast_input_timer, -100) +} + +CommandToastStartInputHook(*) { + global command_toast_input_hook + if ReloadModeActive() || Window.IsMoveMode() + return + command_toast_input_hook := InputHook("V") + command_toast_input_hook.KeyOpt("{All}", "E") + command_toast_input_hook.OnKeyDown := CommandToastOnKeyDown + command_toast_input_hook.Start() +} + +StopCommandToastInputHook() { + global command_toast_input_timer, command_toast_input_hook + if command_toast_input_timer + SetTimer(command_toast_input_timer, 0) + command_toast_input_timer := 0 + if command_toast_input_hook { + try command_toast_input_hook.Stop() + command_toast_input_hook := 0 + } +} + +RegisterCommandToastKeydown() { + global command_toast_keydown_enabled, command_toast_keydown_handler + if command_toast_keydown_enabled + return + if !command_toast_keydown_handler + command_toast_keydown_handler := CommandToastKeydownHandler + OnMessage(0x100, command_toast_keydown_handler) + OnMessage(0x104, command_toast_keydown_handler) + command_toast_keydown_enabled := true +} + +UnregisterCommandToastKeydown() { + global command_toast_keydown_enabled, command_toast_keydown_handler + if !command_toast_keydown_enabled + return + if command_toast_keydown_handler { + OnMessage(0x100, command_toast_keydown_handler, 0) + OnMessage(0x104, command_toast_keydown_handler, 0) + } + command_toast_keydown_enabled := false +} + +CommandToastKeydownHandler(*) { + if ReloadModeActive() || Window.IsMoveMode() + return + HideCommandToast() +} + +CommandToastOnKeyDown(*) { + if ReloadModeActive() || Window.IsMoveMode() + return + HideCommandToast() +} + GetCommandToastWorkArea(&left, &top, &right, &bottom) { mon := "" try hwnd := WinGetID("A") @@ -256,7 +351,7 @@ BuildCommandToastModel() { } model["mode"] := "normal" - model["title"] := "be-there" + model["title"] := "harken" model["apps"] := BuildAppRows() model["rows"] := BuildCommandToastRows(key_width) model["key"] := "normal|" BuildCommandToastRowsKey(model["rows"]) "|" BuildAppsKey(model["apps"]) @@ -274,6 +369,8 @@ BuildCommandToastRows(key_width := 16) { rows.Push(Map("key", "alt+-", "desc", "minimize")) rows.Push(Map("key", "super+q", "desc", "close")) rows.Push(Map("key", "super+" Config["window"]["cycle_app_windows_hotkey"], "desc", "cycle app windows")) + if (Config["window"]["cycle_app_windows_current_hotkey"] != "") + rows.Push(Map("key", "super+" Config["window"]["cycle_app_windows_current_hotkey"], "desc", "cycle app windows (current desktop)")) if Config.Has("window_selector") && Config["window_selector"]["enabled"] { rows.Push(Map("key", "super+" Config["window_selector"]["hotkey"], "desc", "window selector")) } @@ -306,6 +403,8 @@ BuildAppRows() { global Config rows := [] for _, app in Config["apps"] { + if !app.Has("hotkey") || app["hotkey"] = "" + continue icon_path := ResolveAppIconPath(app) rows.Push(Map( "hotkey", app["hotkey"], @@ -327,7 +426,7 @@ BuildAppsKey(apps) { ResolveAppIconPath(app) { if !(app is Map) return "" - if app.Has("win_title") { + if app.Has("win_title") && app["win_title"] != "" { path := FindAppWindowPath(app["win_title"]) if (path != "") return path diff --git a/src/lib/config_loader.ahk b/src/lib/config_loader.ahk index ae536d3..d1e3e0c 100644 --- a/src/lib/config_loader.ahk +++ b/src/lib/config_loader.ahk @@ -3,9 +3,8 @@ LoadConfig(config_path, default_config := Map()) { errors := [] if FileExist(config_path) { - json_text := FileRead(config_path) try { - user_config := Jxon_Load(&json_text) + user_config := TomlLoadFile(config_path) config := DeepMergeMaps(config, user_config) } catch as err { errors.Push("config.parse: " err.Message) @@ -16,7 +15,12 @@ LoadConfig(config_path, default_config := Map()) { } } + NormalizeSuperKeyConfig(config) + NormalizeVirtualDesktopConfig(config) errors := ValidateConfig(config, ConfigSchema()) + ValidateSuperKeys(config, errors) + ValidateApps(config, errors) + ValidateVirtualDesktopHotkeys(config, errors) return Map( "config", config, @@ -27,13 +31,32 @@ LoadConfig(config_path, default_config := Map()) { ConfigSchema() { return Map( "config_version", "number", - "super_key", "string", + "super_key", ["string"], "apps", [Map( "id", "string", - "hotkey", "string", - "win_title", "string", - "run", "string", - "run_paths", OptionalSpec(["string"]) + "hotkey", OptionalSpec("string"), + "win_title", OptionalSpec("string"), + "match", OptionalSpec(Map( + "exe", OptionalSpec("string"), + "exe_regex", OptionalSpec("bool"), + "class", OptionalSpec("string"), + "class_regex", OptionalSpec("bool"), + "title", OptionalSpec("string"), + "title_regex", OptionalSpec("bool") + )), + "run", OptionalSpec("string"), + "run_start_in", OptionalSpec("string"), + "run_paths", OptionalSpec(["string"]), + "desktop", OptionalSpec("number"), + "follow_on_spawn", OptionalSpec("bool"), + "focus_border", OptionalSpec(Map( + "border_color", OptionalSpec("string"), + "move_mode_color", OptionalSpec("string"), + "command_mode_color", OptionalSpec("string"), + "border_thickness", OptionalSpec("number"), + "corner_radius", OptionalSpec("number"), + "update_interval_ms", OptionalSpec("number") + )) )], "global_hotkeys", [Map( "enabled", "bool", @@ -50,7 +73,9 @@ ConfigSchema() { "cancel_key", "string" ), "cycle_app_windows_hotkey", "string", - "center_width_cycle_hotkey", "string" + "cycle_app_windows_current_hotkey", "string", + "center_width_cycle_hotkey", "string", + "minimize_others_hotkey", OptionalSpec("string") ), "window_selector", Map( "enabled", "bool", @@ -67,11 +92,43 @@ ConfigSchema() { "margins", Map( "top", "number", "left", "number", - "right", "number" + "right", "number", + "bottom", "number" ), "gap_px", "number", "exceptions_regex", "string" ), + "virtual_desktop", Map( + "enabled", "bool", + "switch_on_focus", "bool", + "ensure_count", "number", + "cycle_prefer_current", "bool", + "prev_hotkey", "string", + "next_hotkey", "string", + "move_prev_hotkey", "string", + "move_next_hotkey", "string", + "desktop_hotkeys", OptionalSpec([Map( + "desktop", "number", + "hotkey", "string" + )]), + "goto_hotkeys", [Map( + "hotkey", "string", + "desktop", "number" + )], + "move_hotkeys", [Map( + "hotkey", "string", + "desktop", "number" + )], + "debug_cycle", "bool", + "debug_hotkeys", "bool", + "tray_indicator", "bool", + "tray_format", "string", + "desktop_hotkeys_duplicates", OptionalSpec([Map( + "hotkey", "string", + "desktop", "number", + "existing", "number" + )]) + ), "directional_focus", Map( "enabled", "bool", "stacked_overlap_threshold", "number", @@ -87,6 +144,7 @@ ConfigSchema() { "enabled", "bool", "border_color", "string", "move_mode_color", "string", + "command_mode_color", "string", "border_thickness", "number", "corner_radius", "number", "update_interval_ms", "number" @@ -114,6 +172,134 @@ ValidateConfig(config, schema) { return errors } +NormalizeSuperKeyConfig(config) { + if !config.Has("super_key") + return + + super_value := config["super_key"] + if (super_value is String) + config["super_key"] := [super_value] +} + +NormalizeVirtualDesktopConfig(config) { + if !config.Has("virtual_desktop") || !(config["virtual_desktop"] is Map) + return + + vd_config := config["virtual_desktop"] + desktop_hotkeys := [] + duplicates := [] + seen_hotkeys := Map() + keys_to_remove := [] + + for key, val in vd_config { + if !IsInteger(key) + continue + keys_to_remove.Push(key) + desktop_num := Integer(key) + if (val is Array) { + for _, entry in val { + if !(entry is Map) + continue + if entry.Has("hotkey") && entry["hotkey"] != "" { + hotkey := entry["hotkey"] + if seen_hotkeys.Has(hotkey) { + duplicates.Push(Map("hotkey", hotkey, "desktop", desktop_num, "existing", seen_hotkeys[hotkey])) + } else { + seen_hotkeys[hotkey] := desktop_num + desktop_hotkeys.Push(Map( + "desktop", desktop_num, + "hotkey", hotkey + )) + } + } + } + } else if (val is Map) { + if val.Has("hotkey") && val["hotkey"] != "" { + hotkey := val["hotkey"] + if seen_hotkeys.Has(hotkey) { + duplicates.Push(Map("hotkey", hotkey, "desktop", desktop_num, "existing", seen_hotkeys[hotkey])) + } else { + seen_hotkeys[hotkey] := desktop_num + desktop_hotkeys.Push(Map( + "desktop", desktop_num, + "hotkey", hotkey + )) + } + } + } + } + + if (desktop_hotkeys.Length > 0) + vd_config["desktop_hotkeys"] := desktop_hotkeys + + if (duplicates.Length > 0) + vd_config["desktop_hotkeys_duplicates"] := duplicates + + for _, key in keys_to_remove + vd_config.Delete(key) +} + +ValidateSuperKeys(config, errors) { + if !config.Has("super_key") + return + super_keys := config["super_key"] + if (super_keys is Array && super_keys.Length = 0) + errors.Push("config.super_key should contain at least one key") +} + +ValidateApps(config, errors) { + if !config.Has("apps") || !(config["apps"] is Array) + return + + for index, app in config["apps"] { + if !(app is Map) + continue + + has_hotkey := app.Has("hotkey") && (app["hotkey"] != "") + has_win_title := app.Has("win_title") && (app["win_title"] != "") + has_match := app.Has("match") && (app["match"] is Map) + + if !has_win_title && !has_match + errors.Push("config.apps[" index "] must define win_title or match") + + if has_hotkey && (!app.Has("run") || app["run"] = "") + errors.Push("config.apps[" index "].run is required when hotkey is set") + + if has_match { + match := app["match"] + has_exe := match.Has("exe") && (match["exe"] != "") + has_class := match.Has("class") && (match["class"] != "") + has_title := match.Has("title") && (match["title"] != "") + if !has_exe && !has_class && !has_title + errors.Push("config.apps[" index "].match must define exe, class, or title") + + if (match.Has("exe_regex") && !has_exe) + errors.Push("config.apps[" index "].match.exe_regex requires exe") + if (match.Has("class_regex") && !has_class) + errors.Push("config.apps[" index "].match.class_regex requires class") + if (match.Has("title_regex") && !has_title) + errors.Push("config.apps[" index "].match.title_regex requires title") + } + } +} + +ValidateVirtualDesktopHotkeys(config, errors) { + if !config.Has("virtual_desktop") || !(config["virtual_desktop"] is Map) + return + vd_config := config["virtual_desktop"] + if vd_config.Has("desktop_hotkeys_duplicates") && (vd_config["desktop_hotkeys_duplicates"] is Array) { + for _, entry in vd_config["desktop_hotkeys_duplicates"] { + if !(entry is Map) + continue + hotkey := entry.Has("hotkey") ? entry["hotkey"] : "" + desktop := entry.Has("desktop") ? entry["desktop"] : "" + existing := entry.Has("existing") ? entry["existing"] : "" + if (hotkey != "") + errors.Push("config.virtual_desktop.hotkey '" hotkey "' maps to desktop " desktop " but is already mapped to " existing) + } + } +} + ValidateNode(value, spec, path, errors) { if (spec is Map) { if spec.Has("__optional__") { diff --git a/src/lib/directional_focus.ahk b/src/lib/directional_focus.ahk index 9495829..c56182f 100644 --- a/src/lib/directional_focus.ahk +++ b/src/lib/directional_focus.ahk @@ -209,7 +209,7 @@ UpdateDirectionalFocusDebug(mode, direction, active, candidates, stacked, select return if !directional_focus_debug_gui { - directional_focus_debug_gui := Gui("+AlwaysOnTop +ToolWindow", "be-there Directional Focus Debug") + directional_focus_debug_gui := Gui("+AlwaysOnTop +ToolWindow", "harken Directional Focus Debug") directional_focus_debug_gui.SetFont("s9", "Consolas") directional_focus_debug_text := directional_focus_debug_gui.AddEdit("w720 r22 ReadOnly", "") directional_focus_debug_gui.OnEvent("Close", (*) => directional_focus_debug_gui.Hide()) @@ -610,6 +610,9 @@ IsCandidateInCenterBand(win, center_band) { ActivateWindow(hwnd) { if !hwnd return 0 + if VirtualDesktopSwitchOnFocus() + return ActivateWindowAcrossDesktops(hwnd) + WinActivate("ahk_id " hwnd) try WinWaitActive("ahk_id " hwnd,, 0.2) active_hwnd := WinGetID("A") diff --git a/src/lib/focus_border.ahk b/src/lib/focus_border.ahk index 2aea728..36c4038 100644 --- a/src/lib/focus_border.ahk +++ b/src/lib/focus_border.ahk @@ -11,10 +11,11 @@ focus_config := Config["focus_border"] global focus_border_enabled := focus_config["enabled"] if focus_border_enabled { ; ------------- User Settings ------------- - border_color := ParseHexColor(focus_config["border_color"]) ; Hex color (#RRGGBB) - move_mode_color := ParseHexColor(focus_config["move_mode_color"]) ; Hex color (#RRGGBB) - border_thickness := Integer(focus_config["border_thickness"]) ; Border thickness in pixels - corner_radius := Integer(focus_config["corner_radius"]) ; Corner roundness in pixels + base_border_color := ParseHexColor(focus_config["border_color"]) ; Hex color (#RRGGBB) + base_move_mode_color := ParseHexColor(focus_config["move_mode_color"]) ; Hex color (#RRGGBB) + base_command_mode_color := ParseHexColor(focus_config["command_mode_color"]) ; Hex color (#RRGGBB) + base_border_thickness := Integer(focus_config["border_thickness"]) ; Border thickness in pixels + base_corner_radius := Integer(focus_config["corner_radius"]) ; Corner roundness in pixels update_interval := Integer(focus_config["update_interval_ms"]) ; How often (ms) to check/update active window ; ------------- End Settings ------------- @@ -22,7 +23,7 @@ if focus_border_enabled { overlay := Gui("+AlwaysOnTop +ToolWindow -Caption +E0x20", "ActiveWindowBorder") ; Set the background color of the overlay (it will only be visible in its "region") ; Convert the numeric color (0xRRGGBB) to a 6-digit hex string (without "0x") - bg_color := Format("{:06X}", border_color & 0xFFFFFF) + bg_color := Format("{:06X}", base_border_color & 0xFFFFFF) overlay.BackColor := bg_color overlay.Show("NoActivate") h_overlay := overlay.Hwnd @@ -31,7 +32,7 @@ if focus_border_enabled { ; Global variables to track the last active window's position & size. global prev_hwnd := 0, prev_ax := 0, prev_ay := 0, prev_aw := 0, prev_ah := 0 - global current_color := border_color + global current_color := base_border_color global flash_until := 0 global flash_color := 0xB0B0B0 @@ -43,7 +44,8 @@ if focus_border_enabled { ; ------------------------------- UpdateBorder(*) { global overlay, h_overlay - global border_thickness, corner_radius, border_color, move_mode_color, current_color + global base_border_thickness, base_corner_radius + global base_border_color, base_move_mode_color, base_command_mode_color, current_color global prev_hwnd, prev_ax, prev_ay, prev_aw, prev_ah ; Get the currently active window. @@ -57,6 +59,11 @@ if focus_border_enabled { overlay.Hide() return } + class_name := WinGetClass("ahk_id " active_hwnd) + if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd" || class_name = "Shell_SecondaryTrayWnd") { + overlay.Hide() + return + } style := WinGetStyle("ahk_id " active_hwnd) if (style & 0x20000000) { ; WS_MINIMIZE flag overlay.Hide() @@ -77,10 +84,29 @@ if focus_border_enabled { WinGetPos(&ax, &ay, &aw, &ah, "ahk_id " active_hwnd) } + border_color := base_border_color + move_mode_color := base_move_mode_color + command_mode_color := base_command_mode_color + border_thickness := base_border_thickness + corner_radius := base_corner_radius + override := FindFocusBorderOverride(active_hwnd) + if (override is Map) { + if (override.Has("border_color") && override["border_color"] != "") + border_color := ParseHexColor(override["border_color"]) + if (override.Has("move_mode_color") && override["move_mode_color"] != "") + move_mode_color := ParseHexColor(override["move_mode_color"]) + if (override.Has("command_mode_color") && override["command_mode_color"] != "") + command_mode_color := ParseHexColor(override["command_mode_color"]) + if (override.Has("border_thickness")) + border_thickness := Integer(override["border_thickness"]) + if (override.Has("corner_radius")) + corner_radius := Integer(override["corner_radius"]) + } + if (flash_until > A_TickCount) desired_color := flash_color else if ReloadModeActive() - desired_color := 0xFFD400 + desired_color := command_mode_color else desired_color := Window.IsMoveMode() ? move_mode_color : border_color if (desired_color != current_color) { @@ -125,6 +151,23 @@ if focus_border_enabled { } } +FindFocusBorderOverride(active_hwnd) { + global Config + if !Config.Has("apps") + return "" + + for _, app in Config["apps"] { + if !(app is Map) + continue + if !app.Has("focus_border") || !(app["focus_border"] is Map) + continue + if MatchAppWindow(app, active_hwnd) + return app["focus_border"] + } + + return "" +} + FlashFocusBorder(color := 0xB0B0B0, duration_ms := 130) { global focus_border_enabled, flash_until, flash_color, overlay, current_color if !focus_border_enabled diff --git a/src/lib/focus_or_run.ahk b/src/lib/focus_or_run.ahk index ad71dea..932e078 100644 --- a/src/lib/focus_or_run.ahk +++ b/src/lib/focus_or_run.ahk @@ -4,60 +4,177 @@ FocusOrRun(winTitle, exePath, hotkey_id, app_config := "", *) { static last_window := Map() target_hwnd := 0 - hwnds := WinGetList(winTitle) - current_hwnd := WinGetID("A") + hwnds := GetAppWindowList(winTitle, app_config) + current_hwnd := 0 + try current_hwnd := WinGetID("A") + catch + current_hwnd := 0 if (hwnds.Length) { - for hwnd in hwnds { - if (InStr(winTitle, "explorer.exe")) { - class_name := WinGetClass("ahk_id " hwnd) - if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd") - continue - } - ex_style := WinGetExStyle("ahk_id " hwnd) - if (ex_style & 0x80) || (ex_style & 0x8000000) - continue - if (!(WinGetStyle("ahk_id " hwnd) & 0x10000000)) - continue - - state := WinGetMinMax("ahk_id " hwnd) - if (state = -1) { - if (!target_hwnd) - target_hwnd := hwnd - continue - } - target_hwnd := hwnd - break - } + target_hwnd := PickFocusableAppWindow(hwnds, winTitle) if (!target_hwnd) target_hwnd := hwnds[1] } if target_hwnd { if (current_hwnd = target_hwnd) { - if last_window.Has(hotkey_id) && WinExist("ahk_id " last_window[hotkey_id]) { - WinActivate "ahk_id " last_window[hotkey_id] + if last_window.Has(hotkey_id) && WindowExistsAcrossDesktops(last_window[hotkey_id]) { + ActivateWindowAcrossDesktops(last_window[hotkey_id]) } return } if current_hwnd && (current_hwnd != target_hwnd) { last_window[hotkey_id] := current_hwnd } - WinActivate "ahk_id " target_hwnd + ActivateWindowAcrossDesktops(target_hwnd) } else { RunResolved(exePath, app_config) + ScheduleMoveAppWindowToDesktop(winTitle, app_config) + } +} + +GetAppWindowList(win_title, app_config := "") { + if (app_config is Map && app_config.Has("match") && app_config["match"] is Map) { + return GetWindowsByMatch(app_config["match"]) + } + if !win_title + return [] + return GetWindowsAcrossDesktops(win_title) +} + +GetWindowsByMatch(match) { + matches := [] + for _, hwnd in GetWindowsAcrossDesktops() { + if MatchWindowFields(match, hwnd) + matches.Push(hwnd) + } + return matches +} + +PickFocusableAppWindow(hwnds, win_title) { + for hwnd in hwnds { + if !WinExist("ahk_id " hwnd) + continue + if (InStr(win_title, "explorer.exe")) { + class_name := WinGetClass("ahk_id " hwnd) + if (class_name = "Progman" || class_name = "WorkerW" || class_name = "Shell_TrayWnd") + continue + } + try ex_style := WinGetExStyle("ahk_id " hwnd) + catch + continue + if (ex_style & 0x80) || (ex_style & 0x8000000) + continue + try style := WinGetStyle("ahk_id " hwnd) + catch + continue + allow_invisible := false + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + allow_invisible := true + else if (desktop_num != VD.getCurrentDesktopNum()) + allow_invisible := true + } + if (!allow_invisible && !(style & 0x10000000)) + continue + + state := WinGetMinMax("ahk_id " hwnd) + if (state = -1) + continue + return hwnd + } + + for hwnd in hwnds { + if !WinExist("ahk_id " hwnd) + continue + state := WinGetMinMax("ahk_id " hwnd) + if (state = -1) + return hwnd } + return 0 +} + +ScheduleMoveAppWindowToDesktop(win_title, app_config := "") { + if !VirtualDesktopEnabled() + return + target_desktop := GetAppTargetDesktop(app_config) + if (target_desktop <= 0) + return + + follow_on_spawn := GetAppFollowOnSpawn(app_config) + attempts := 0 + callback := 0 + callback := () => TryMoveAppWindowToDesktop(win_title, app_config, target_desktop, follow_on_spawn, &attempts, callback) + SetTimer(callback, 200) +} + +GetAppTargetDesktop(app_config := "") { + if !(app_config is Map) + return 0 + if !app_config.Has("desktop") + return 0 + return app_config["desktop"] +} + +GetAppFollowOnSpawn(app_config := "") { + if (app_config is Map && app_config.Has("follow_on_spawn")) + return app_config["follow_on_spawn"] + return true +} + +TryMoveAppWindowToDesktop(win_title, app_config, target_desktop, follow_on_spawn, &attempts, callback) { + attempts += 1 + hwnds := GetAppWindowList(win_title, app_config) + hwnd := PickFocusableAppWindow(hwnds, win_title) + if (!hwnd && hwnds.Length) + hwnd := hwnds[1] + + if (hwnd) { + VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, follow_on_spawn) + if follow_on_spawn { + VD.goToDesktopNum(target_desktop) + VD.WaitDesktopSwitched(target_desktop) + } + try { + assigned_desktop := GetWindowDesktopNum(hwnd) + if (assigned_desktop > 0 && assigned_desktop != target_desktop) + VD.MoveWindowToDesktopNum("ahk_id " hwnd, target_desktop, follow_on_spawn) + } + SetTimer(callback, 0) + return + } + + if (attempts >= 30) + SetTimer(callback, 0) } RunResolved(command, app_config := "") { resolved := ResolveRunPath(command, app_config) + run_start_in := "" + if (app_config is Map && app_config.Has("run_start_in")) + run_start_in := app_config["run_start_in"] try { - Run resolved + if (run_start_in = "") { + link_info := ResolveShortcutStartIn(resolved) + if (link_info is Map) { + resolved := link_info["target"] + run_start_in := link_info["start_in"] + } + } else if (StrLower(SubStr(resolved, -4)) = ".lnk") { + shortcut := ResolveShortcutTarget(resolved) + if shortcut + resolved := shortcut + } + if (run_start_in != "") + Run resolved, run_start_in + else + Run resolved } catch as err { MsgBox( "Failed to launch: " command "`n" err.Message "`n`n" . "Tip: set apps[].run to a full path or an App Paths-registered exe.", - "be-there", + "harken", "Iconx" ) } @@ -67,6 +184,9 @@ ResolveRunPath(command, app_config := "") { command := Trim(command, " `t") if (SubStr(command, 1, 1) = '"' && SubStr(command, -1) = '"') command := SubStr(command, 2, -1) + if (StrLower(SubStr(command, -4)) = ".lnk") { + return command + } if (InStr(command, "\\") || InStr(command, "/") || InStr(command, ":")) { if FileExist(command) return command @@ -199,28 +319,45 @@ FindStartMenuShortcut(command) { if !target continue if InStr(StrLower(target), name) - return target + return A_LoopFilePath continue } target := ResolveShortcutTarget(A_LoopFilePath) if target - return target + return A_LoopFilePath } } return "" } ResolveShortcutTarget(link_path) { - if !FileExist(link_path) + if !link_path || !FileExist(link_path) return "" try { shell := ComObject("WScript.Shell") shortcut := shell.CreateShortcut(link_path) - target := shortcut.TargetPath - if target && FileExist(target) - return target + target := Trim(shortcut.TargetPath " " shortcut.Arguments) + return target + } catch { + return "" + } +} + +ResolveShortcutStartIn(link_path) { + if !link_path || !FileExist(link_path) + return "" + if (StrLower(SubStr(link_path, -4)) != ".lnk") + return "" + try { + shell := ComObject("WScript.Shell") + shortcut := shell.CreateShortcut(link_path) + return Map( + "target", Trim(shortcut.TargetPath " " shortcut.Arguments), + "start_in", shortcut.WorkingDirectory + ) + } catch { + return "" } - return "" } FindWindowsAppsAlias(exe_name) { diff --git a/src/lib/toml.ahk b/src/lib/toml.ahk new file mode 100644 index 0000000..167ce79 --- /dev/null +++ b/src/lib/toml.ahk @@ -0,0 +1,408 @@ +TomlLoadFile(file_path) { + toml_text := FileRead(file_path, "UTF-8") + return TomlParse(toml_text) +} + +TomlParse(toml_text) { + data := Map() + current_context := data + + lines := StrSplit(toml_text, "`n", "`r") + for _, raw_line in lines { + line := Trim(TomlStripComment(raw_line)) + if (line = "") + continue + + if (SubStr(line, 1, 2) = "[[" && SubStr(line, -2) = "]]" ) { + section_path := Trim(SubStr(line, 3, StrLen(line) - 4)) + current_context := TomlGetOrCreateArrayItem(data, section_path) + continue + } + + if (SubStr(line, 1, 1) = "[" && SubStr(line, -1) = "]") { + section_path := Trim(SubStr(line, 2, StrLen(line) - 2)) + current_context := TomlGetOrCreateSection(data, section_path) + continue + } + + if InStr(line, "=") { + kv := TomlSplitKeyValue(line) + if (kv.Has("key") && kv["key"] != "") + TomlSetNestedKey(current_context, kv["key"], TomlParseValue(kv["value"])) + } + } + + return data +} + +TomlStripComment(line) { + in_double := false + in_single := false + escaped := false + result := "" + + loop parse, line { + ch := A_LoopField + if (escaped) { + result .= ch + escaped := false + continue + } + + if (ch = "\\" && in_double) { + escaped := true + result .= ch + continue + } + + if (ch = '"' && !in_single) + in_double := !in_double + else if (ch = "'" && !in_double) + in_single := !in_single + + if (ch = "#" && !in_double && !in_single) + break + + result .= ch + } + + return result +} + +TomlSplitKeyValue(line) { + in_double := false + in_single := false + escaped := false + + loop parse, line { + ch := A_LoopField + if (escaped) { + escaped := false + continue + } + if (ch = "\\" && in_double) { + escaped := true + continue + } + if (ch = '"' && !in_single) + in_double := !in_double + else if (ch = "'" && !in_double) + in_single := !in_single + + if (ch = "=" && !in_double && !in_single) { + key := Trim(SubStr(line, 1, A_Index - 1)) + value := Trim(SubStr(line, A_Index + 1)) + return Map("key", key, "value", value) + } + } + + return Map() +} + +TomlGetOrCreateSection(data, section_path) { + parts := StrSplit(section_path, ".") + current := data + for _, part in parts { + if (!current.Has(part) || !(current[part] is Map)) + current[part] := Map() + current := current[part] + } + return current +} + +TomlGetOrCreateArrayItem(data, section_path) { + parts := StrSplit(section_path, ".") + if (parts.Length = 1) { + array_name := parts[1] + if (!data.Has(array_name) || !(data[array_name] is Array)) + data[array_name] := [] + data[array_name].Push(Map()) + return data[array_name][data[array_name].Length] + } + + parent := TomlGetOrCreateSection(data, TomlJoinRange(parts, ".", 1, parts.Length - 1)) + array_name := parts[parts.Length] + if (!parent.Has(array_name) || !(parent[array_name] is Array)) + parent[array_name] := [] + parent[array_name].Push(Map()) + return parent[array_name][parent[array_name].Length] +} + +TomlSetNestedKey(root, key_path, value) { + parts := StrSplit(key_path, ".") + current := root + loop parts.Length - 1 { + part := parts[A_Index] + if (!current.Has(part) || !(current[part] is Map)) + current[part] := Map() + current := current[part] + } + current[parts[parts.Length]] := value +} + +TomlParseValue(value) { + if (value = "") + return "" + + if (SubStr(value, 1, 1) = "{" && SubStr(value, -1) = "}") { + table_content := Trim(SubStr(value, 2, StrLen(value) - 2)) + return TomlParseInlineTable(table_content) + } + + if (SubStr(value, 1, 1) = "[" && SubStr(value, -1) = "]") { + array_content := Trim(SubStr(value, 2, StrLen(value) - 2)) + return TomlParseArray(array_content) + } + + if (SubStr(value, 1, 1) = '"' && SubStr(value, -1) = '"') { + inner := SubStr(value, 2, StrLen(value) - 2) + inner := StrReplace(inner, "\\`"", "`"") + inner := StrReplace(inner, "\\\\", "\\") + return inner + } + + if (SubStr(value, 1, 1) = "'" && SubStr(value, -1) = "'") + return SubStr(value, 2, StrLen(value) - 2) + + if (value = "true") + return true + if (value = "false") + return false + + if (value ~= "^-?\d+$") + return Integer(value) + if (value ~= "^-?\d+\.\d+$") + return Float(value) + + return value +} + +TomlParseInlineTable(content) { + table := Map() + if (content = "") + return table + + elements := [] + in_double := false + in_single := false + escaped := false + depth := 0 + current := "" + + loop parse, content { + ch := A_LoopField + if (escaped) { + current .= ch + escaped := false + continue + } + + if (ch = "\\" && in_double) { + escaped := true + current .= ch + continue + } + + if (ch = '"' && !in_single) + in_double := !in_double + else if (ch = "'" && !in_double) + in_single := !in_single + + if (!in_double && !in_single) { + if (ch = "[" || ch = "{") + depth += 1 + else if (ch = "]" || ch = "}") + depth -= 1 + } + + if (ch = "," && !in_double && !in_single && depth = 0) { + elements.Push(Trim(current)) + current := "" + continue + } + + current .= ch + } + + if (Trim(current) != "") + elements.Push(Trim(current)) + + for _, element in elements { + kv := TomlSplitKeyValue(element) + if (kv.Has("key") && kv["key"] != "") + TomlSetNestedKey(table, kv["key"], TomlParseValue(kv["value"])) + } + + return table +} + +TomlParseArray(content) { + result := [] + if (content = "") + return result + + elements := [] + in_double := false + in_single := false + escaped := false + depth := 0 + current := "" + + loop parse, content { + ch := A_LoopField + if (escaped) { + current .= ch + escaped := false + continue + } + + if (ch = "\\" && in_double) { + escaped := true + current .= ch + continue + } + + if (ch = '"' && !in_single) + in_double := !in_double + else if (ch = "'" && !in_double) + in_single := !in_single + + if (ch = "[" && !in_double && !in_single) + depth += 1 + else if (ch = "]" && !in_double && !in_single) + depth -= 1 + + if (ch = "," && !in_double && !in_single && depth = 0) { + elements.Push(Trim(current)) + current := "" + continue + } + + current .= ch + } + + if (Trim(current) != "") + elements.Push(Trim(current)) + + for _, element in elements + result.Push(TomlParseValue(element)) + + return result +} + +TomlDump(config, table_array_keys := []) { + lines := [] + + for key, value in config { + if (value is Map) + continue + if (TomlIsTableArrayKey(key, value, table_array_keys)) + continue + lines.Push(key " = " TomlFormatValue(value)) + } + + if (lines.Length) + lines.Push("") + + for key, value in config { + if (TomlIsTableArrayKey(key, value, table_array_keys)) { + for _, item in value { + lines.Push("[[" key "]]" ) + for item_key, item_value in item + lines.Push(item_key " = " TomlFormatValue(item_value)) + lines.Push("") + } + } + } + + for key, value in config { + if (value is Map) + TomlAppendSection(lines, key, value) + } + + while (lines.Length && lines[lines.Length] = "") + lines.Pop() + + return TomlJoin(lines, "`n") +} + +TomlAppendSection(lines, section_path, section_map) { + lines.Push("[" section_path "]") + nested_sections := [] + + for key, value in section_map { + if (value is Map) { + nested_sections.Push(Map("path", section_path "." key, "map", value)) + continue + } + lines.Push(key " = " TomlFormatValue(value)) + } + + lines.Push("") + + for _, nested in nested_sections + TomlAppendSection(lines, nested["path"], nested["map"]) +} + +TomlIsTableArrayKey(key, value, table_array_keys) { + if (value is Array) { + if (value.Length > 0 && value[1] is Map) + return true + for _, table_key in table_array_keys { + if (key = table_key) + return true + } + } + return false +} + +TomlFormatValue(value) { + if (value is Array) { + if (value.Length = 0) + return "[]" + elements := [] + for _, item in value + elements.Push(TomlFormatValue(item)) + return "[" TomlJoin(elements, ", ") "]" + } + + if (value is Integer || value is Float) + return value + + if (value = true) + return "true" + if (value = false) + return "false" + + return TomlQuoteString(value) +} + +TomlQuoteString(value) { + escaped := StrReplace(value, "\\", "\\\\") + escaped := StrReplace(escaped, "`"", "\\`"") + return "`"" escaped "`"" +} + +TomlJoin(items, sep) { + output := "" + for i, item in items { + if (i > 1) + output .= sep + output .= item + } + return output +} + +TomlJoinRange(parts, sep, start_index, length) { + output := "" + end_index := start_index + length - 1 + loop length { + idx := start_index + A_Index - 1 + if (idx > end_index) + break + if (output != "") + output .= sep + output .= parts[idx] + } + return output +} diff --git a/src/lib/virtual_desktop.ahk b/src/lib/virtual_desktop.ahk new file mode 100644 index 0000000..636e04f --- /dev/null +++ b/src/lib/virtual_desktop.ahk @@ -0,0 +1,254 @@ +#Include %A_LineFile%\..\VD.ahk + +VirtualDesktopEnabled() { + global Config + if !IsSet(Config) + return false + if !Config.Has("virtual_desktop") + return false + return Config["virtual_desktop"]["enabled"] +} + +VirtualDesktopTrayEnabled() { + global Config + if !VirtualDesktopEnabled() + return false + return Config["virtual_desktop"].Has("tray_indicator") && Config["virtual_desktop"]["tray_indicator"] +} + +VirtualDesktopSwitchOnFocus() { + global Config + if !VirtualDesktopEnabled() + return false + return Config["virtual_desktop"]["switch_on_focus"] +} + +InitVirtualDesktop() { + if !VirtualDesktopEnabled() + return + ensure_count := Config["virtual_desktop"]["ensure_count"] + if (ensure_count > 0) + VD.createUntil(ensure_count) + InitVirtualDesktopTrayIndicator() +} + +InitVirtualDesktopTrayIndicator() { + if !VirtualDesktopTrayEnabled() + return + UpdateVirtualDesktopTrayIndicator() + VD.ListenersCurrentVirtualDesktopChanged[UpdateVirtualDesktopTrayIndicator] := true +} + +UpdateVirtualDesktopTrayIndicator(*) { + if !VirtualDesktopTrayEnabled() + return + current := GetCurrentDesktopNumFresh() + if (current <= 0) + current := VD.getCurrentDesktopNum() + total := VD.getCount() + if (total <= 0) + return + text := FormatVirtualDesktopTrayText(current, total) + try A_TrayMenu.SetTip(text) + SetTrayIconText(text) +} + +FormatVirtualDesktopTrayText(current, total) { + global Config + format := "{current}/{total}" + if Config.Has("virtual_desktop") && Config["virtual_desktop"].Has("tray_format") + format := Config["virtual_desktop"]["tray_format"] + format := StrReplace(format, "{current}", current) + format := StrReplace(format, "{total}", total) + return format +} + +SetTrayIconText(text) { + if (text = "") + return + hicon := CreateTextTrayIcon(text) + if !hicon + return + TraySetIcon("HICON:" hicon) + global tray_indicator_hicon + if (IsSet(tray_indicator_hicon) && tray_indicator_hicon) + DllCall("user32\DestroyIcon", "Ptr", tray_indicator_hicon) + tray_indicator_hicon := hicon +} + +CreateTextTrayIcon(text) { + icon_size := 32 + hdc := DllCall("gdi32\CreateCompatibleDC", "Ptr", 0, "Ptr") + if !hdc + return 0 + + bi := Buffer(40, 0) + NumPut("UInt", 40, bi, 0) + NumPut("Int", icon_size, bi, 4) + NumPut("Int", -icon_size, bi, 8) + NumPut("UShort", 1, bi, 12) + NumPut("UShort", 32, bi, 14) + NumPut("UInt", 0, bi, 16) + ppv_bits := 0 + hbm_color := DllCall("gdi32\CreateDIBSection", "Ptr", hdc, "Ptr", bi, "UInt", 0, "Ptr*", &ppv_bits, "Ptr", 0, "UInt", 0, "Ptr") + if !hbm_color { + DllCall("gdi32\\DeleteDC", "Ptr", hdc) + return 0 + } + + hbm_mask := DllCall("gdi32\CreateBitmap", "Int", icon_size, "Int", icon_size, "UInt", 1, "UInt", 1, "Ptr", 0, "Ptr") + old_bmp := DllCall("gdi32\SelectObject", "Ptr", hdc, "Ptr", hbm_color, "Ptr") + + DllCall("gdi32\SetBkMode", "Ptr", hdc, "Int", 1) + + font_height := -Round(icon_size * 0.55) + hfont := DllCall("gdi32\CreateFontW", "Int", font_height, "Int", 0, "Int", 0, "Int", 0, "Int", 600, "UInt", 0, "UInt", 0, "UInt", 0, "UInt", 0, "UInt", 0, "UInt", 0, "UInt", 0, "UInt", 0, "WStr", "Segoe UI", "Ptr") + old_font := 0 + if hfont + old_font := DllCall("gdi32\SelectObject", "Ptr", hdc, "Ptr", hfont, "Ptr") + + rect := Buffer(16, 0) + NumPut("Int", 0, rect, 0) + NumPut("Int", 0, rect, 4) + NumPut("Int", icon_size, rect, 8) + NumPut("Int", icon_size, rect, 12) + format := 0x00000001 | 0x00000004 | 0x00000020 + + DllCall("gdi32\SetTextColor", "Ptr", hdc, "UInt", 0x000000) + rect_shadow := Buffer(16, 0) + NumPut("Int", 1, rect_shadow, 0) + NumPut("Int", 1, rect_shadow, 4) + NumPut("Int", icon_size + 1, rect_shadow, 8) + NumPut("Int", icon_size + 1, rect_shadow, 12) + DllCall("user32\DrawTextW", "Ptr", hdc, "WStr", text, "Int", -1, "Ptr", rect_shadow, "UInt", format) + + DllCall("gdi32\SetTextColor", "Ptr", hdc, "UInt", 0xFFFFFF) + DllCall("user32\DrawTextW", "Ptr", hdc, "WStr", text, "Int", -1, "Ptr", rect, "UInt", format) + + if hfont { + if old_font + DllCall("gdi32\SelectObject", "Ptr", hdc, "Ptr", old_font) + DllCall("gdi32\DeleteObject", "Ptr", hfont) + } + if old_bmp + DllCall("gdi32\SelectObject", "Ptr", hdc, "Ptr", old_bmp) + DllCall("gdi32\DeleteDC", "Ptr", hdc) + + iconinfo := Buffer(A_PtrSize == 8 ? 32 : 20, 0) + NumPut("UInt", 1, iconinfo, 0) + NumPut("UInt", 0, iconinfo, 4) + NumPut("UInt", 0, iconinfo, 8) + if (A_PtrSize == 8) { + NumPut("Ptr", hbm_mask, iconinfo, 16) + NumPut("Ptr", hbm_color, iconinfo, 24) + } else { + NumPut("Ptr", hbm_mask, iconinfo, 12) + NumPut("Ptr", hbm_color, iconinfo, 16) + } + hicon := DllCall("user32\CreateIconIndirect", "Ptr", iconinfo, "Ptr") + + DllCall("gdi32\DeleteObject", "Ptr", hbm_color) + DllCall("gdi32\DeleteObject", "Ptr", hbm_mask) + + return hicon +} + +RefreshVirtualDesktopState() { + if !VirtualDesktopEnabled() + return + try { + VD.IVirtualDesktopListChanged() + VD.currentDesktopNum := VD.IVirtualDesktopMap[VD.IVirtualDesktopManagerInternal.GetCurrentDesktop()] + } +} + +GetCurrentDesktopNumFresh() { + if !VirtualDesktopEnabled() + return 0 + try { + VD.IVirtualDesktopListChanged() + current := VD.IVirtualDesktopMap[VD.IVirtualDesktopManagerInternal.GetCurrentDesktop()] + if (current > 0) + VD.currentDesktopNum := current + return current + } + return 0 +} + +GetWindowDesktopNum(hwnd) { + if !VirtualDesktopEnabled() + return 0 + try return VD.getDesktopNumOfHWND(hwnd) + catch as err + return 0 +} + +IsWindowOnCurrentDesktop(hwnd) { + if !VirtualDesktopEnabled() + return true + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num <= 0) + return true + return desktop_num = VD.getCurrentDesktopNum() +} + +GetWindowsAcrossDesktops(win_title := "") { + if !VirtualDesktopEnabled() + return WinGetList(win_title) + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + windows := WinGetList(win_title) + A_DetectHiddenWindows := bak_detect_hidden_windows + return windows +} + +WindowExistsAcrossDesktops(hwnd) { + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true + exists := WinExist("ahk_id " hwnd) + A_DetectHiddenWindows := bak_detect_hidden_windows + return exists +} + +ActivateWindowAcrossDesktops(hwnd) { + if !hwnd + return 0 + if !WindowExistsAcrossDesktops(hwnd) + return 0 + + if VirtualDesktopSwitchOnFocus() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num > 0 && desktop_num != VD.getCurrentDesktopNum()) { + try { + VD.goToDesktopOfWindow("ahk_id " hwnd, true) + } catch as err { + try { + WinActivate "ahk_id " hwnd + } catch + return 0 + } + try return WinGetID("A") + catch + return 0 + } + + if (desktop_num <= 0) { + try { + VD.goToDesktopOfWindow("ahk_id " hwnd, true) + try return WinGetID("A") + catch + return 0 + } catch as err { + ; fall through to direct activation + } + } + } + + try { + WinActivate "ahk_id " hwnd + } catch + return 0 + try return WinGetID("A") + catch + return 0 +} diff --git a/src/lib/window_inspector.ahk b/src/lib/window_inspector.ahk index 3ce8697..984e2b2 100644 --- a/src/lib/window_inspector.ahk +++ b/src/lib/window_inspector.ahk @@ -3,22 +3,25 @@ ShowWindowInspector() { static inspector_gui := "" static window_list := "" + static refresh_timer := 0 if inspector_gui { inspector_gui.Show() RefreshList(window_list) + StartAutoRefresh(window_list, &refresh_timer) return } - inspector_gui := Gui("+Resize", "be-there Window Inspector") + inspector_gui := Gui("+Resize", "harken Window Inspector") inspector_gui.SetFont("s10", "Segoe UI") - window_list := inspector_gui.AddListView("w980 r26 Grid", ["Title", "Exe", "Class", "PID", "HWND"]) - window_list.ModifyCol(1, 400) - window_list.ModifyCol(2, 140) - window_list.ModifyCol(3, 200) - window_list.ModifyCol(4, 80) - window_list.ModifyCol(5, 120) + window_list := inspector_gui.AddListView("w1020 r26 Grid", ["Active", "Title", "Exe", "Class", "PID", "HWND"]) + window_list.ModifyCol(1, 60) + window_list.ModifyCol(2, 380) + window_list.ModifyCol(3, 140) + window_list.ModifyCol(4, 200) + window_list.ModifyCol(5, 80) + window_list.ModifyCol(6, 120) refresh_btn := inspector_gui.AddButton("xm y+10 w110", "Refresh") refresh_btn.OnEvent("Click", (*) => RefreshList(window_list)) @@ -32,24 +35,41 @@ ShowWindowInspector() { export_btn := inspector_gui.AddButton("x+10 yp w140", "Export File") export_btn.OnEvent("Click", (*) => ExportAll(window_list)) - inspector_gui.OnEvent("Close", (*) => inspector_gui.Hide()) + inspector_gui.OnEvent("Close", (*) => HideInspector(inspector_gui, &refresh_timer)) inspector_gui.Show() RefreshList(window_list) + StartAutoRefresh(window_list, &refresh_timer) } RefreshList(window_list) { window_list.Delete() + active_hwnd := 0 + try active_hwnd := WinGetID("A") for _, hwnd in WinGetList() { title := WinGetTitle("ahk_id " hwnd) exe := WinGetProcessName("ahk_id " hwnd) class_name := WinGetClass("ahk_id " hwnd) pid := WinGetPID("ahk_id " hwnd) - window_list.Add("", title, exe, class_name, pid, Format("0x{:X}", hwnd)) + active_label := (hwnd = active_hwnd) ? "active" : "" + window_list.Add("", active_label, title, exe, class_name, pid, Format("0x{:X}", hwnd)) } } +StartAutoRefresh(window_list, &refresh_timer) { + if refresh_timer + SetTimer(refresh_timer, 0) + refresh_timer := (*) => RefreshList(window_list) + SetTimer(refresh_timer, 2000) +} + +HideInspector(inspector_gui, &refresh_timer) { + if refresh_timer + SetTimer(refresh_timer, 0) + inspector_gui.Hide() +} + CopySelected(window_list) { rows := [] row := 0 @@ -94,14 +114,15 @@ GetRowValues(window_list, row) { window_list.GetText(row, 2), window_list.GetText(row, 3), window_list.GetText(row, 4), - window_list.GetText(row, 5) + window_list.GetText(row, 5), + window_list.GetText(row, 6) ] } BuildOutput(rows) { - output := "Title\tExe\tClass\tPID\tHWND`n" + output := "Active\tTitle\tExe\tClass\tPID\tHWND`n" for _, row in rows { - output .= row[1] "\t" row[2] "\t" row[3] "\t" row[4] "\t" row[5] "`n" + output .= row[1] "\t" row[2] "\t" row[3] "\t" row[4] "\t" row[5] "\t" row[6] "`n" } return output } diff --git a/src/lib/window_manager.ahk b/src/lib/window_manager.ahk index e669557..39ca5f1 100644 --- a/src/lib/window_manager.ahk +++ b/src/lib/window_manager.ahk @@ -45,10 +45,11 @@ class Window static __New() { - global window_nav_modifier - HotIf (*) => GetKeyState(window_nav_modifier, 'P') + HotIf (*) => IsSuperKeyPressed() && !GetKeyState('Shift', 'P') && !GetKeyState('Ctrl', 'P') + && !GetKeyState('LAlt', 'P') + && !GetKeyState('RAlt', 'P') && !this.IsMoveMode() Hotkey('*k', ObjBindMethod(this, 'HotkeyCallback', ObjBindMethod(this, 'MoveUp'))) Hotkey('*h', ObjBindMethod(this, 'HotkeyCallback', ObjBindMethod(this, 'MoveLeft'))) @@ -61,7 +62,7 @@ class Window HotIf ; releasing modifier key destroys gui guides - Hotkey('*' window_nav_modifier ' up', ObjBindMethod(Gui_Guides, 'Destroy_Guis')) + RegisterSuperKeyHotkey("*", " up", ObjBindMethod(Gui_Guides, 'Destroy_Guis')) } static SetMoveMode(state) => Window.move_mode := state @@ -586,6 +587,7 @@ class Screen static top_margin := Config["window_manager"]["margins"]["top"] static left_margin := Config["window_manager"]["margins"]["left"] static right_margin := Config["window_manager"]["margins"]["right"] + static bottom_margin := Config["window_manager"]["margins"]["bottom"] static activeWindowIsOn => Screen.FromWindow() static top => this.GetScreenCoordinates(this.activeWindowIsOn, 'top') static bottom => this.GetScreenCoordinates(this.activeWindowIsOn, 'bottom') @@ -610,6 +612,8 @@ class Screen left += this.left_margin if this.right_margin right -= this.right_margin + if this.bottom_margin + bottom -= this.bottom_margin width := Abs(right - left) ; calculate width of screen height := Abs(bottom - top) ; calculate height of screen diff --git a/src/lib/window_walker.ahk b/src/lib/window_walker.ahk index 8deaa8a..6d65b07 100644 --- a/src/lib/window_walker.ahk +++ b/src/lib/window_walker.ahk @@ -12,6 +12,19 @@ class WindowWalker static visible := false static image_list := "" static icon_cache := Map() + static corner_radius := 8 + static excluded_exes := Map( + "applicationframehost", true, + "lockapp", true, + "searchexperiencehost", true, + "searchhost", true, + "searchapp", true, + "searchui", true, + "shellexperiencehost", true, + "startmenuexperiencehost", true, + "systemsettings", true, + "textinputhost", true + ) static Show(*) { @@ -28,6 +41,15 @@ class WindowWalker WindowWalker.StartFocusWatch() } + static Toggle(*) + { + if WindowWalker.IsActive() { + WindowWalker.Hide() + return + } + WindowWalker.Show() + } + static Hide(*) { if WindowWalker.gui { @@ -47,19 +69,20 @@ class WindowWalker if WindowWalker.gui return - WindowWalker.gui := Gui("+AlwaysOnTop +ToolWindow -Caption +Border", "be-there Window Selector") + WindowWalker.gui := Gui("+AlwaysOnTop +ToolWindow -Caption", "harken Window Selector") WindowWalker.gui.MarginX := 12 WindowWalker.gui.MarginY := 10 WindowWalker.gui.SetFont("s10", "Segoe UI") - WindowWalker.search_edit := WindowWalker.gui.AddEdit("w560", "") + WindowWalker.search_edit := WindowWalker.gui.AddEdit("w600", "") WindowWalker.search_edit.OnEvent("Change", (*) => WindowWalker.ApplyFilter()) - WindowWalker.list_view := WindowWalker.gui.AddListView("w560 r10 -Multi", ["App", "Title"]) + WindowWalker.list_view := WindowWalker.gui.AddListView("w600 r10 -Multi", ["App", "Title", "Desk"]) WindowWalker.image_list := IL_Create(20) WindowWalker.list_view.SetImageList(WindowWalker.image_list, 1) - WindowWalker.list_view.ModifyCol(1, 160) - WindowWalker.list_view.ModifyCol(2, 380) + WindowWalker.list_view.ModifyCol(1, 150) + WindowWalker.list_view.ModifyCol(2, 360) + WindowWalker.list_view.ModifyCol(3, 60) WindowWalker.list_view.OnEvent("DoubleClick", (*) => WindowWalker.ActivateSelected()) WindowWalker.gui.OnEvent("Close", (*) => WindowWalker.Hide()) @@ -74,6 +97,20 @@ class WindowWalker pos_x := left + (right - left - w) / 2 pos_y := top + (bottom - top - h) / 2 WindowWalker.gui.Show("x" pos_x " y" pos_y) + WindowWalker.ApplyRoundedCorners(w, h) + } + + static ApplyRoundedCorners(width := 0, height := 0) + { + if !WindowWalker.gui + return + if (!width || !height) { + WindowWalker.gui.GetPos(,, &width, &height) + } + if (width <= 0 || height <= 0) + return + radius := WindowWalker.corner_radius + try WinSetRegion("0-0 w" width " h" height " r" radius "-" radius, WindowWalker.gui) } static StartFocusWatch() @@ -160,17 +197,21 @@ class WindowWalker return WindowWalker.Hide() - if WinExist("ahk_id " hwnd) { - if (WinGetMinMax("ahk_id " hwnd) = -1) - WinRestore "ahk_id " hwnd - WinActivate "ahk_id " hwnd + if WindowExistsAcrossDesktops(hwnd) { + try { + if (WinGetMinMax("ahk_id " hwnd) = -1) + WinRestore "ahk_id " hwnd + } + ActivateWindowAcrossDesktops(hwnd) } } static RefreshWindows() { WindowWalker.windows := [] - win_list := WinGetList() + win_list := GetWindowsAcrossDesktops() + bak_detect_hidden_windows := A_DetectHiddenWindows + A_DetectHiddenWindows := true for _, hwnd in win_list { if WindowWalker.gui && (hwnd = WindowWalker.gui.Hwnd) @@ -188,15 +229,19 @@ class WindowWalker exe := "unknown" exe_path := "" try exe_path := WinGetProcessPath("ahk_id " hwnd) + desktop_label := WindowWalker.DesktopLabel(hwnd) WindowWalker.windows.Push(Map( "hwnd", hwnd, "title", title, "exe", exe, "exe_path", exe_path, + "desktop", desktop_label, "order", A_Index )) } + + A_DetectHiddenWindows := bak_detect_hidden_windows } static ApplyFilter() @@ -224,7 +269,8 @@ class WindowWalker "exe_display", WindowWalker.DisplayExe(win["exe"]), "exe_path", win["exe_path"], "title", win["title"], - "title_preview", WindowWalker.TruncateText(win["title"], preview_len) + "title_preview", WindowWalker.TruncateText(win["title"], preview_len), + "desktop", win["desktop"] )) } @@ -239,7 +285,7 @@ class WindowWalker for _, item in matches { icon_index := WindowWalker.GetIconIndex(item["exe"], item["exe_path"]) - WindowWalker.list_view.Add("Icon" icon_index, item["exe_display"], item["title_preview"]) + WindowWalker.list_view.Add("Icon" icon_index, item["exe_display"], item["title_preview"], item["desktop"]) } if (WindowWalker.list_view.GetCount() > 0) @@ -261,21 +307,55 @@ class WindowWalker static IsWindowEligible(hwnd) { + if !WindowExistsAcrossDesktops(hwnd) + return false if Window.IsException("ahk_id " hwnd) return false + try exe_name := WinGetProcessName("ahk_id " hwnd) + catch + return false + exe_name := StrLower(exe_name) + if (SubStr(exe_name, -4) = ".exe") + exe_name := SubStr(exe_name, 1, -4) + if WindowWalker.excluded_exes.Has(exe_name) + return false + if (!Config["window_selector"]["include_minimized"] && WinGetMinMax("ahk_id " hwnd) = -1) return false - ex_style := WinGetExStyle("ahk_id " hwnd) + try ex_style := WinGetExStyle("ahk_id " hwnd) + catch + return false if (ex_style & 0x80) || (ex_style & 0x8000000) return false - if !(WinGetStyle("ahk_id " hwnd) & 0x10000000) + try style := WinGetStyle("ahk_id " hwnd) + catch + return false + allow_invisible := false + if VirtualDesktopEnabled() { + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num > 0 && desktop_num != VD.getCurrentDesktopNum()) + allow_invisible := true + } + if (!allow_invisible && !(style & 0x10000000)) return false return true } + static DesktopLabel(hwnd) + { + if !VirtualDesktopEnabled() + return "" + desktop_num := GetWindowDesktopNum(hwnd) + if (desktop_num = -1 || desktop_num = -2) + return "all" + if (desktop_num <= 0) + return "" + return desktop_num + } + static SortMatches(matches) { count := matches.Length diff --git a/tools/build_release.ps1 b/tools/build_release.ps1 index 39d4a6a..03034cc 100644 --- a/tools/build_release.ps1 +++ b/tools/build_release.ps1 @@ -6,95 +6,87 @@ param( $ErrorActionPreference = "Stop" +# Use the mirror to avoid 403 and not need to mess with useragent +# The alpha releases don't have a reliable mirror for automations currently +# so we created a deps release for 2.1-alpha.18 +$ahk_link = "https://github.com/grantith/harken/releases/download/deps/AutoHotkey_2.1-alpha.18.zip" +$ahk_2exe_link = "https://github.com/AutoHotkey/Ahk2Exe/releases/download/Ahk2Exe1.1.37.02a0a/Ahk2Exe1.1.37.02a0.zip" $script_dir = Split-Path -Parent $MyInvocation.MyCommand.Path $repo_root = Join-Path $script_dir ".." | Resolve-Path +$ahk_dir = "./dist" $ahk_path = Join-Path $repo_root $ahk_dir $out_path = Join-Path $repo_root $out_dir New-Item -ItemType Directory -Force -Path $ahk_path | Out-Null -function Get-AssetUrl($repo, $pattern) { - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" - $asset = $release.assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1 - if (!$asset) { throw "Release asset not found for $repo" } - return $asset.browser_download_url -} - -function Download-And-Extract($url, $dest_dir) { - $zip_path = Join-Path $dest_dir "download.zip" - Invoke-WebRequest -Uri $url -OutFile $zip_path - Expand-Archive -Path $zip_path -DestinationPath $dest_dir -Force - Remove-Item $zip_path -Force +## +# Download AHK Prereqs +## + +# First, AHK +if (!(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey64.exe -ErrorAction SilentlyContinue)) { + Write-Host "Downloading Ahkv2 from $ahk_link" + $ahk_zip_path = Join-Path $ahk_path "ahk.zip" + Invoke-WebRequest -Uri $ahk_link -OutFile $ahk_zip_path + Expand-Archive -Path $ahk_zip_path -DestinationPath $ahk_path -Force + Remove-Item $ahk_zip_path -Force +} else { + Write-Host "./dist/Ahk2Exe.exe already exists, skipping download" } +# Second, Ahk2Exe if (!(Get-ChildItem -Path $ahk_path -Recurse -Filter Ahk2Exe.exe -ErrorAction SilentlyContinue)) { - Write-Host "Downloading Ahk2Exe (latest release)..." - $url = Get-AssetUrl "AutoHotkey/Ahk2Exe" "^Ahk2Exe.*\.zip$" - Download-And-Extract $url $ahk_path -} - -if (!(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkeySC.bin -ErrorAction SilentlyContinue) -and !(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey64.exe -ErrorAction SilentlyContinue) -and !(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey32.exe -ErrorAction SilentlyContinue)) { - Write-Host "Searching for AutoHotkey base bin in installed locations..." - $install_paths = @() - if ($env:ProgramFiles) { $install_paths += Join-Path $env:ProgramFiles "AutoHotkey\\Compiler" } - if (${env:ProgramFiles(x86)}) { $install_paths += Join-Path ${env:ProgramFiles(x86)} "AutoHotkey\\Compiler" } - - foreach ($path in $install_paths) { - $bin = Join-Path $path "AutoHotkeySC.bin" - if (Test-Path $bin) { - Copy-Item -Path $bin -Destination (Join-Path $ahk_path "AutoHotkeySC.bin") -Force - break - } - } -} - -if (!(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkeySC.bin -ErrorAction SilentlyContinue) -and !(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey64.exe -ErrorAction SilentlyContinue) -and !(Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey32.exe -ErrorAction SilentlyContinue)) { - Write-Host "Downloading AutoHotkey base bin (latest release)..." - $url = Get-AssetUrl "AutoHotkey/AutoHotkey" "^AutoHotkey_.*\.zip$" - Download-And-Extract $url $ahk_path + Write-Host "Downloading Ahk2Exe from $ahk_2exe_link" + $ahk_2xe_zip_path = Join-Path $ahk_path "ahk2exe.zip" + Invoke-WebRequest -Uri $ahk_2exe_link -OutFile $ahk_2xe_zip_path + Expand-Archive -Path $ahk_2xe_zip_path -DestinationPath $ahk_path -Force + Remove-Item $ahk_2xe_zip_path -Force +} else { + Write-Host "./dist/Ahk2Exe.exe already exists, skipping download" } $compiler_item = Get-ChildItem -Path $ahk_path -Recurse -Filter Ahk2Exe.exe | Select-Object -First 1 $base_item = Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkeySC.bin | Select-Object -First 1 if (!$base_item) { $base_item = Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey64.exe | Select-Object -First 1 } -if (!$base_item) { $base_item = Get-ChildItem -Path $ahk_path -Recurse -Filter AutoHotkey32.exe | Select-Object -First 1 } if (!$compiler_item -or !$base_item) { throw "Ahk2Exe.exe or AutoHotkeySC.bin/AutoHotkey64.exe not found under $ahk_path. Install AutoHotkey or copy the base file into $ahk_path." } +## +# Build +## + $compiler = $compiler_item.FullName $base = $base_item.FullName New-Item -ItemType Directory -Force -Path $out_path | Out-Null -$exe_path = Join-Path $out_path "be-there.exe" +$exe_path = Join-Path $out_path "harken.exe" if (Test-Path $exe_path) { Remove-Item $exe_path -Force } $main_args = @( - "/in", "$repo_root/be-there.ahk", + "/in", "$repo_root/harken.ahk", "/out", "$exe_path", "/base", "$base", "/silent", "verbose" ) - $null = Start-Process -FilePath $compiler -ArgumentList $main_args -NoNewWindow -Wait -PassThru if (!(Test-Path $exe_path)) { - throw "Ahk2Exe did not produce be-there.exe. Check the compiler base file and try again." + throw "Ahk2Exe did not produce harken.exe. Check the logs and try again." } Write-Host "Compiled: $exe_path" -$source_zip = "$out_path/be-there-source-$version.zip" -$compiled_zip = "$out_path/be-there-$version-win64.zip" +$source_zip = "$out_path/harken-source-$version.zip" $source_files = @( - "be-there.ahk", + "harken.ahk", "src", "tools", - "config/config.example.json", + "config/config.example.toml", "README.md", "LICENSE", "LICENSES", @@ -104,20 +96,17 @@ $source_files = @( Compress-Archive -Path $source_files -DestinationPath $source_zip -Force $compiled_files = @( - "$out_path/be-there.exe", + "$out_path/harken.exe", "tools", - "config/config.example.json", + "config/config.example.toml", "README.md", "LICENSE", "LICENSES", "docs" ) -Compress-Archive -Path $compiled_files -DestinationPath $compiled_zip -Force - -Write-Host "Built: $compiled_zip" Write-Host "Built: $source_zip" -$default_config = Join-Path $out_path "config.example.json" -Get-Content "$repo_root/config/config.example.json" -Raw | Out-File -FilePath $default_config -Encoding UTF8 +$default_config = Join-Path $out_path "config.example.toml" +Get-Content "$repo_root/config/config.example.toml" -Raw | Out-File -FilePath $default_config -Encoding UTF8 Write-Host "Exported: $default_config"