Skip to content

Add taskbar monitor picker to the context menu - #65

Open
svenbledt wants to merge 6 commits into
CodeZeno:mainfrom
svenbledt:monitor-picker
Open

Add taskbar monitor picker to the context menu#65
svenbledt wants to merge 6 commits into
CodeZeno:mainfrom
svenbledt:monitor-picker

Conversation

@svenbledt

Copy link
Copy Markdown

Summary

On multi-monitor setups, the widget could previously only be moved to another monitor's taskbar by dragging it (and dragging is restricted to the divider handle), which is hard to discover. This PR adds an explicit "Display on" submenu to the right-click context menu so users can pick the monitor directly.

What changed

  • A new "Display on" submenu in the context menu (shared by both the widget and the tray icon, so it works even when the widget is hidden).
  • Each detected taskbar is listed as "Monitor 1", "Monitor 2", …, with the primary display tagged (e.g. "Monitor 1 (Primary)") and a checkmark on the current selection.
  • Selecting an entry re-embeds the widget on that monitor's taskbar and remembers the choice across restarts. If the widget was hidden, picking a monitor shows it there.
  • The submenu is omitted on single-monitor systems, so nothing changes for those users.

Implementation notes

  • Persistence reuses the existing positional taskbar_index and the existing attach_to_taskbar re-embed path — no settings-schema change.
  • Primary-monitor detection uses MonitorFromRect + GetMonitorInfoW (MONITORINFOF_PRIMARY) against each taskbar rect; taskbar enumeration and sort order are unchanged, so the positional index stays consistent with the existing drag-to-move behavior.
  • New menu command IDs use the 90..=105 range to avoid colliding with the existing tray-toggle command ID.
  • The visibility flip and persistence only happen when the re-attach succeeds, so a transient failure can't leave the saved state marked visible while the window stays hidden.
  • New strings (display_on, monitor, primary) are localized across all 11 supported languages.

Testing

  • cargo build and cargo test pass; a unit test covers the monitor label formatting (primary tagging and 1-based numbering). cargo clippy introduces no new warnings.
  • Manually verified on a 3-monitor setup: the submenu lists all taskbars with the primary tagged and the current one checked, selecting an entry moves the widget to that monitor's taskbar, and the choice survives a restart. Also verified selecting from the tray menu while the widget is hidden shows it on the chosen monitor, and that single-monitor systems see no submenu.

Scope

Deliberately out of scope: device-ID persistence that survives monitor re-arrangement, resolution labels, and auto-follow-cursor placement.

On multi-monitor setups the widget could only be moved between taskbars
by dragging it, which is hard to discover. This adds a "Display on"
submenu to the right-click context menu (shared by the widget and the
tray icon) that lists each detected taskbar as "Monitor N", tags the
primary display, and checkmarks the current one. Selecting an entry
re-embeds the widget on that monitor's taskbar and persists the choice.

The submenu is omitted on single-monitor systems. Selection reuses the
existing positional taskbar_index and attach_to_taskbar path, so there
is no settings-schema change; if the widget was hidden, picking a
monitor shows it there. New menu command IDs use the 90..=105 range to
avoid colliding with the existing tray toggle ID. Strings are localized
across all 11 supported languages.
The monitor picker persisted only a positional taskbar_index. Taskbars
are ordered by screen position, but the *set* is not stable: when a
fullscreen app hides or recreates a monitor's taskbar, that taskbar drops
out of (and later returns to) the enumeration, so a given index maps to a
different physical monitor. Any re-attach that resolved the saved index
against the changed set therefore moved the widget onto the wrong
taskbar. A diagnostic capture showed exactly this: index=1 resolving to
the right monitor with count=2, then to a middle monitor once count grew
to 3.

Identify the target monitor by its stable device name (szDevice, e.g.
"\.\DISPLAY2") instead of list position:

- find_taskbars() now records each taskbar's monitor device name.
- attach_to_taskbar() prefers the saved device, falling back to the
  clamped index only when that monitor's taskbar is absent, and persists
  the resolved device.
- The new taskbar_monitor setting carries the choice (index kept for
  back-compat / migration; existing settings upgrade on first attach).
- The picker resolves the clicked entry to the device captured when the
  menu was built, and checkmarks by device rather than index.
- On WM_DISPLAYCHANGE the widget re-homes to its saved monitor once that
  monitor's taskbar returns (e.g. after leaving fullscreen), only ever
  pulling toward the user's choice.

Adds unit tests for the resolver covering the changed-set case.
The previous fix identified the target monitor by stable device name so a
changing taskbar set could not move the widget. But attach_to_taskbar still
persisted whatever monitor it *resolved to* into taskbar_monitor on every
call — including automatic attaches (startup, WM_DISPLAYCHANGE re-home) that
fell back to a different monitor because the chosen one's taskbar was
temporarily absent (fullscreen app, monitor churn).

That rewrote the user's saved choice with the fallback monitor. Two visible
symptoms followed: the widget appeared to switch taskbars, and the picker's
own checkmark / persisted setting silently moved to the wrong monitor, from
which reattach_to_saved_monitor_if_needed could never bring it home because
the saved intent itself was now gone.

Treat the saved preference as the user's intent and only adopt a resolved
monitor when:
  - the user explicitly picked it (picker click / drag), or
  - the resolve actually matched the saved preference, or
  - no preference was saved yet (legacy index-only migration).

- resolve_taskbar_index() now also reports whether it matched the preferred
  device or fell back to the positional index.
- New pure monitor_pref_after_attach() encodes the persist decision.
- attach_to_taskbar() gains a persist_choice flag: picker and drag pass true,
  startup and display-change re-home pass false so a transient fallback can
  never clobber the saved monitor.

Adds unit tests covering the fallback (must not overwrite), matched-keep,
legacy migration, and user-choice-on-fallback cases.
reattach_to_saved_monitor_if_needed only ran on WM_DISPLAYCHANGE, but that
message is not raised when a taskbar is merely hidden and shown again — which
is exactly what a fullscreen app does. So the widget could sit on a fallback
taskbar indefinitely: the saved monitor's taskbar was back, the saved
preference was intact, and nothing ever asked the window to return to it.

The startup path has the same gap. If the app launches (or relaunches itself
after an explorer restart) while the chosen monitor's taskbar is temporarily
absent, attach_to_taskbar falls back to another taskbar and — correctly, since
the previous commit — leaves the saved preference alone. Without a later
trigger the widget stays on the fallback.

Extend the existing taskbar watchdog, which already polls every few seconds,
to detect the condition:

- It now reads taskbar_monitor and the main HWND alongside taskbar_hwnd.
- The pre-existing "our taskbar vanished entirely -> relaunch" case is
  unchanged and now short-circuits with `continue`.
- Otherwise, when the saved monitor's taskbar is present in the enumeration
  and our embedded taskbar is not that one, it posts WM_APP_REHOME.

The watchdog only detects and posts. The re-embed itself (SetParent, hook
rewiring) has to happen on the thread that owns the window, so wnd_proc
handles WM_APP_REHOME by calling reattach_to_saved_monitor_if_needed. Doing
the work on the watchdog thread would race the message loop.

Also expands the "taskbar selected" diagnostic to record the requested device,
whether it matched, and whether the choice was persisted. The previous line
logged only the resolved device, which made it impossible to tell a matched
attach from a fallback when reading a capture after the fact.
The upstream project is no longer maintained, so releases will be published
from this fork instead. updater.rs derives the GitHub owner/repo it queries
from CARGO_PKG_REPOSITORY at compile time (github_repo()), so repointing the
repository field is the whole switch — every binary built from here on checks
svenbledt/Claude-Code-Usage-Monitor/releases/latest rather than CodeZeno's.

Nothing else is rebranded: homepage, copyright, the winget package id, the
README links and FUNDING.yml still credit the original author, which is
accurate for a fork that is only taking over distribution.

Note that a binary already built before this commit keeps the old URL baked
in and will still check upstream; the first hop onto a fork release has to be
installed by hand.
First release published from this fork. Covers the taskbar monitor picker
and the multi-monitor fixes that followed it:

- "Display on" submenu on the widget and tray context menus.
- Monitors identified by stable device name, so a changing taskbar set
  (fullscreen apps hiding and restoring taskbars) can no longer move the
  widget to a different screen.
- A fallback attach no longer overwrites the saved monitor.
- The watchdog re-homes the widget once the chosen monitor's taskbar
  returns, covering the cases WM_DISPLAYCHANGE never reports.

Minor rather than patch: the picker is a new user-facing feature.
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.

1 participant