Skip to content

feat(cycle): add Toggle Window mode for last-active window - #10

Open
xuziyang wants to merge 1 commit into
ipatalas:mainfrom
xuziyang:feat/toggle-window
Open

feat(cycle): add Toggle Window mode for last-active window#10
xuziyang wants to merge 1 commit into
ipatalas:mainfrom
xuziyang:feat/toggle-window

Conversation

@xuziyang

@xuziyang xuziyang commented Aug 5, 2026

Copy link
Copy Markdown

Summary

  • Add a new Toggle Window cycle mode that always shows or hides the app's last active window
  • Keeps the existing Hide behavior (Z-order first) unchanged
  • Multi-window apps stick to one window instead of flipping through Z-order

Details

In multi-window apps, Hide mode can surface different windows based on Z-order. Toggle Window remembers and always targets the app's last active window, so repeated hotkey presses reliably show/hide the same window.

Changes include:

  • ToggleWindowTargetResolver for resolving the last-active window target
  • Wiring in Switcher and Configuration / cycle mode UI
  • Docs update (cycle-modes.mdx, README)
  • Unit tests for the new resolver

Based on upstream v1.10.3.

Test plan

  • Select Toggle Window mode in settings and verify the label appears correctly
  • With a multi-window app, hotkey show/hide consistently targets the last active window
  • Existing Hide mode behavior is unchanged
  • Unit tests pass (dotnet test)

Keep existing Hide (Z-order first). Toggle Window always shows or hides the app's last active window so multi-window apps stick to one.

Based on upstream v1.10.3 without local passthrough changes.
Comment thread AppSwitcher/Switcher.cs
Comment on lines +170 to +174
var matchingHandles = new List<HWND>(matchingWindows.Count);
foreach (var window in matchingWindows)
{
matchingHandles.Add(window.Handle);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just this:

var matchingHandles = matchingWindows.Select(w => w.Handle).ToList();

?

}

// Prefer BCL IndexOf when the caller passed a List (common case).
if (handles is List<HWND> list)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is always true, because there's only one call to ToggleWindowTargetResolver.Resolve and it's always passing List<HWND> there.
I think this could be greatly simplified then. Let's just pass List<HWND> directly. We don't need this generalization today so I would not complicate it for the sake of potential future uses.


## Toggle Window

**Best for:** Multi-window apps where you always want the *same* window (the last one you used).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is really good idea. I think I was thinking about the same but not as separate cycle mode, but just make this a default behavior for Hide mode. Z-order is a bit confusing on windows and this was pissing me off as well :)
I think the only reason I did not do that is that I only use Hide mode for apps where I only have a single window so it didn't really matter.
I'll pull these changes and spend some time using that to get a better opinion myself.


private static int IndexOf(IReadOnlyList<HWND> handles, HWND? handle)
{
if (handle is not { } h)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly this is so non-obvious with that negative pattern that I had to look it up what it does to be sure.
Given the other comment simplifies that function we can go back to something more straightforward like:

if (handle is null) 
{
    return -1;
}

/// <param name="currentHandle">Foreground window handle, or null if unknown.</param>
/// <param name="rememberedHandle">Last toggled handle for this app, or null.</param>
/// <returns>Null when <paramref name="matchingHandles"/> is empty.</returns>
public static Result? Resolve(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why you went with static API in this case?

Comment thread AppSwitcher/Switcher.cs
return null;
}

var target = matchingWindows[resolution.Value.TargetIndex];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't make more sense to actually pass collection of ApplicationWindow to the resolver and then we can avoid all that playing around with indexes which seems like extra complexity that is not necessary here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a fourth cycle mode, Toggle Window, which always shows/hides an app's last-active window instead of following Z-order like Hide. It introduces a pure ToggleWindowTargetResolver (focused match → remembered handle → first match) and wires it into Switcher, which now remembers per-app target handles in a _lastToggleWindows dictionary. The Switcher.Execute control flow is refactored into ActivateFirst/ExecuteToggleWindow helpers, and the off-app gate is broadened to match any window of the target process. The mode is surfaced through the enum, label converter, cycle-mode selector UI, docs, and tests.

Changes:

  • New ToggleWindow cycle mode with a dedicated, unit-tested target resolver and remembered-window logic in Switcher.
  • Refactored Switcher.Execute dispatch and broadened the "already on this app" detection to consider all matching windows.
  • Updated UI (selector button, label converter), configuration enum, docs (README, cycle-modes.mdx), and round-trip/resolver tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
AppSwitcher/ToggleWindowTargetResolver.cs New pure resolver deciding target window index and Activate/Hide action.
AppSwitcher/Switcher.cs Adds ExecuteToggleWindow/ActivateFirst, remembered-handle map, and refactors mode dispatch/off-app gate.
AppSwitcher/Configuration/Configuration.cs Appends ToggleWindow to the CycleMode enum.
AppSwitcher/UI/Converters/CycleModeToLabelConverter.cs Maps ToggleWindow to the "Toggle Window" label.
AppSwitcher/UI/Controls/CycleModeSelector.xaml Adds the Toggle Window selector button and tooltip.
AppSwitcher/UI/Controls/CycleModeSelector.xaml.cs Updates button appearance state for the new button.
AppSwitcher.Tests/ToggleWindowTargetResolverTests.cs Unit tests covering resolver priority/action cases.
AppSwitcher.Tests/Configuration/Storage/ApplicationConfigurationDocumentTests.cs Adds ToggleWindow to the config round-trip theory.
docs/src/content/docs/configuration/cycle-modes.mdx Documents the new mode and clarifies Hide behavior.
README.md Updates feature list to four cycle modes including Toggle Window.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants