How Vorssaint Implements Per-App Window Grouping in Its Switcher

Vorssaint implements per-app window grouping by collapsing all windows sharing the same process ID into a single representative entry, selecting the most visible window as the thumbnail while preserving the full window list for downstream UI components.

Vorssaint's app switcher in the vorssaint/vorssaint-utils repository supports both window-level and application-level navigation modes. When enabled, per-app window grouping consolidates multiple windows from the same application into one switcher icon, simplifying the interface while maintaining full access to individual window metadata for previews and keyboard shortcuts.

Configuration Settings That Control Grouping

Grouping behavior is governed by a combination of user preferences and per-application rules defined in Sources/Vorssaint/Services/Switcher/SwitcherSupport.swift.

SwitcherAppRule provides per-app overrides that force specific applications to appear with or without windows, or to be hidden entirely. These rules are evaluated during enumeration to determine eligibility for grouping (lines 17‑23).

SwitcherWindowlessApps controls whether apps without visible windows—such as Finder—remain listed in the switcher. This setting interacts with the grouping logic to ensure windowless applications do not create empty groups (lines 62‑71).

The boolean preference mergeWindowsByApp (exposed through the Switcher preferences panel) serves as the master toggle. When true, the WindowEnumerator applies the grouping algorithm; when false, each window receives its own switcher entry.

Enumerating Windows with Grouping Support

The primary entry point for window discovery is WindowEnumerator.enumerateSwitchableWindows. During enumeration, the system collects all windows and filters them based on visibility, minimization status, and fullscreen state.

If mergeWindowsByApp is enabled and the caller requests preservation of grouped data (preservingGroupedWindows == true), the enumerator stores the filtered list for later expansion:

let groupedBackingWindows = groupByApp && preservingGroupedWindows ? filtered : []

Source: Sources/Vorssaint/Services/Switcher/WindowEnumerator.swift (lines 65‑66)

The enumerator then branches based on the grouping flag:

let ordered = groupByApp ? groupWindowsByApp(orderedRaw) : orderedRaw

Source: Sources/Vorssaint/Services/Switcher/WindowEnumerator.swift (lines 77‑78)

The Core Grouping Algorithm

The static helper groupWindowsByApp(_:) performs the actual deduplication by walking the ordered window array and building a compact list where each PID appears exactly once.

private static func groupWindowsByApp(_ windows: [SwitcherItem]) -> [SwitcherItem] {
    var indexByPid: [pid_t: Int] = [:]
    var grouped: [SwitcherItem] = []
    for window in windows {
        if let index = indexByPid[window.pid] {
            // Keep the most "visible" window as the representative
            if (window.isOnScreen && !grouped[index].isOnScreen) ||
               (window.isFullscreen && !grouped[index].isFullscreen) {
                grouped[index] = window
            }
        } else {
            indexByPid[window.pid] = grouped.count
            grouped.append(window)
        }
    }
    return grouped
}

Source: Sources/Vorssaint/Services/Switcher/WindowEnumerator.swift (lines 945‑960)

Key implementation details:

  • PID-based deduplication – A dictionary (indexByPid) tracks the first occurrence of each process ID.
  • Visibility hierarchy – If a later window is on-screen or fullscreen while the current representative is off-screen, the more visible window replaces it as the representative. This ensures the switcher thumbnail always reflects the window the user expects to see.

Re-expanding Groups for Simple Mode

When the switcher operates in "simple" mode, the UI displays one icon per application but still requires the full list of windows to render title chips and handle keyboard shortcuts. To support this, the enumerator expands the grouped list back into a fully populated window list only for the visible applications:

result = SwitcherSupport.expandGroupedWindows(
    orderedWindows: backingOrdered,
    representatives: result)

Source: Sources/Vorssaint/Services/Switcher/WindowEnumerator.swift (lines 99‑103)

The helper SwitcherSupport.expandGroupedWindows iterates through the representatives and re-injects the remaining windows for each application in most-recently-used (MRU) order.

Source: Sources/Vorssaint/Services/Switcher/SwitcherSupport.swift (lines 224‑231)

Building the App Group Model for the View Layer

The view layer consumes grouped data through SwitcherSupport.appGroups(items:), which transforms the flat window list into an array of SwitcherAppGroup objects (one per PID):

static func appGroups(items: [SwitcherItem]) -> [SwitcherAppGroup] {
    var seen: Set<pid_t> = []
    var groups: [SwitcherAppGroup] = []
    for (index, item) in items.enumerated() where !seen.contains(item.pid) {
        seen.insert(item.pid)
        groups.append(SwitcherAppGroup(pid: item.pid,
                                       appName: item.appName,
                                       representativeIndex: index,
                                       itemIDs: items.filter { $0.pid == item.pid }.map(\.id)))
    }
    return groups
}

Source: Sources/Vorssaint/Services/Switcher/SwitcherSupport.swift (lines 49‑59)

SwitcherView (lines 57‑71) reads these groups to compute preview placement, layout the icon row, and render per-app title chips beneath each grouped icon.

Summary

  • Master toggle: The mergeWindowsByApp user default enables or disables per-app window grouping across the entire switcher.
  • Deduplication logic: WindowEnumerator.groupWindowsByApp collapses windows by PID, using an on-screen visibility heuristic to select the best representative thumbnail.
  • Data preservation: When grouping is active, the filtered window list is stored in groupedBackingWindows to support "simple" mode expansion via SwitcherSupport.expandGroupedWindows.
  • UI abstraction: SwitcherSupport.appGroups converts the flat window array into SwitcherAppGroup objects, allowing SwitcherView to render one icon per application while retaining access to all underlying window IDs.

Frequently Asked Questions

How does Vorssaint decide which window represents a grouped app?

Vorssaint selects the representative window based on visibility hierarchy within the groupWindowsByApp function. If multiple windows exist for the same PID, the algorithm prefers windows that are currently on-screen over off-screen ones, and fullscreen windows over non-fullscreen ones. This ensures the switcher thumbnail matches the user's current context.

What happens to hidden windows when grouping is enabled?

Hidden windows are not discarded; they are preserved in groupedBackingWindows when the preservingGroupedWindows flag is set. For simple mode layouts, SwitcherSupport.expandGroupedWindows re-injects these hidden windows back into the result set so the UI can still access them for title chips and keyboard shortcuts, even though only one icon appears per app.

Can users disable per-app grouping entirely?

Yes. Users can disable grouping by setting the mergeWindowsByApp preference to false in the Switcher preferences panel. Additionally, per-application overrides via SwitcherAppRule (defined in SwitcherSupport.swift) can force specific apps to always show individual windows regardless of the global setting.

Where is the grouping logic located in the codebase?

The core grouping algorithm resides in Sources/Vorssaint/Services/Switcher/WindowEnumerator.swift (lines 945‑960). Supporting utilities—including expansion logic and app group construction—are implemented in Sources/Vorssaint/Services/Switcher/SwitcherSupport.swift. The UI consumption of grouped data occurs in Sources/Vorssaint/UI/Switcher/SwitcherView.swift (lines 57‑71).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →