Skip to content

feat: add per-app auto-hide taskbar when active#12

Open
tkaiman wants to merge 5 commits into
adamk33n3r:mainfrom
tkaiman:feature/auto-hide-taskbar
Open

feat: add per-app auto-hide taskbar when active#12
tkaiman wants to merge 5 commits into
adamk33n3r:mainfrom
tkaiman:feature/auto-hide-taskbar

Conversation

@tkaiman
Copy link
Copy Markdown

@tkaiman tkaiman commented Mar 11, 2026

Summary

  • Adds a "Hide Taskbar when active" per-app setting that automatically sets the Windows taskbar to auto-hide when a configured borderless app is the foreground window
  • Restores the user's original taskbar state when focus moves away from the app
  • Safely restores the taskbar on application exit regardless of shutdown method
  • Includes the setting in both the app config dialog and the defaults tab

Implementation Details

  • Uses SHAppBarMessage Windows API to programmatically control taskbar auto-hide state
  • Detects foreground window via GetForegroundWindow in the existing scan loop
  • Preserves the user's full original taskbar state bitmask (ABM_GETSTATE return value) and restores it exactly via ABM_SETSTATE, so flags like ABS_ALWAYSONTOP are not lost
  • findWindowByClass returns (uintptr, error) — treats a zero hwnd as an error instead of silently continuing
  • getTaskbarAutoHide returns (uint32, error) — propagates lookup errors and returns the raw bitmask rather than flattening to a bool
  • setTaskbarAutoHide accepts uint32 and writes the bitmask directly to APPBARDATA.lParam, avoiding the previous synthesized ABS_AUTOHIDE/ABS_ALWAYSONTOP toggle
  • Thread-safe taskbar state management via mutex

Test plan

  • Create an app config with "Hide Taskbar when active" enabled
  • Enable Auto Apply, switch focus to the configured app — taskbar auto-hides
  • Switch focus away — taskbar restores to original state (including ABS_ALWAYSONTOP if it was set)
  • Close GoBorderless — taskbar restores to original state
  • Verify setting persists across app restarts via settings.json
  • Confirm taskbar functions log errors gracefully if Shell_TrayWnd is not found

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Option to auto-hide the Windows taskbar while the app is active; setting is available in Defaults and App Settings and saved at startup.
  • Bug Fixes

    • Taskbar state is saved and reliably restored when the window is restored or the app exits, preventing the taskbar from remaining hidden.

Adds a "Hide Taskbar when active" option to each app config that
automatically sets the Windows taskbar to auto-hide when the configured
borderless app is in the foreground, and restores the original taskbar
state when focus moves away or the app exits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04f6fbbb-6f21-4841-afc2-d11f98235200

📥 Commits

Reviewing files that changed from the base of the PR and between c47cabd and dfdc7dd.

📒 Files selected for processing (1)
  • winapi.go

📝 Walkthrough

Walkthrough

Adds Windows taskbar auto-hide control: records original state at startup, hides the taskbar while configured borderless windows are active, restores on focus loss or app exit, adds WinAPI helpers, and exposes a "Hide Taskbar when active" option in defaults and per-app settings.

Changes

Cohort / File(s) Summary
Settings
settings.go
Adds HideTaskbar bool to AppSetting and AppSettingDefaults.
WinAPI & taskbar primitives
winapi.go
Adds ABM/ABS constants and APPBARDATA; introduces wrappers for SHAppBarMessage, GetForegroundWindow, FindWindowW, and helpers to get/set taskbar auto-hide.
Taskbar state management
borderless.go
Introduces mutex-protected taskbar state, records original auto-hide state, and adds saveTaskbarState, hideTaskbar, restoreTaskbar, restoreTaskbarOnExit; restores taskbar when a window is restored if configured.
Main loop & startup changes
main.go
Makes chWindowList buffered and non-blocking; computes shouldHideTaskbar per scan (auto-apply and non-auto scans); calls saveTaskbarState() at startup; applies hideTaskbar()/restoreTaskbar() each scan.
App lifecycle
gui_app.go
Calls restoreTaskbarOnExit() before quitting in the non–CloseToTray exit path.
UI — Defaults & App settings
gui_defaults.go, gui_appsetting.go
Adds "Hide Taskbar when active" checkbox to defaults and per-app settings; wires checkbox to settings.Defaults.HideTaskbar and AppSetting.HideTaskbar, and persists changes.

Sequence Diagram(s)

sequenceDiagram
    participant Main as Main Loop
    participant Scanner as Window Scanner
    participant Settings as Settings Store
    participant WinAPI as Windows API
    participant Taskbar as Taskbar Manager

    Main->>Scanner: periodic scan
    Scanner->>WinAPI: GetForegroundWindow()
    WinAPI-->>Scanner: HWND

    Scanner->>Settings: lookup app setting for HWND
    Settings-->>Scanner: {HideTaskbar?, AutoApply?}

    alt app requests hide
        Scanner->>Taskbar: shouldHideTaskbar = true
        Taskbar->>WinAPI: setTaskbarAutoHide(ABS_AUTOHIDE)
    else otherwise
        Scanner->>Taskbar: shouldHideTaskbar = false
        Taskbar->>WinAPI: setTaskbarAutoHide(0)
    end

    Note right of Taskbar: saveTaskbarState() on startup\nrestoreTaskbarOnExit() before quit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I tuck the bar when windows gleam,
I remember how it used to seem,
When borders vanish, I slip away,
I hop it back at close of day,
A quiet helper on the stream.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add per-app auto-hide taskbar when active' directly and accurately describes the primary change: introducing a per-app setting to automatically hide the Windows taskbar when a configured borderless app's window is active.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gui_app.go`:
- Around line 312-315: The restoreTaskbarOnExit() call is only invoked in the
window close intercept, leaving other shutdown paths (notably restartApp() which
currently calls os.Exit(0)) without taskbar restore; refactor by creating a
single shutdown helper (e.g., shutdownAndExit or performShutdown) that invokes
restoreTaskbarOnExit(), performs any necessary cleanup and then quits/exits,
update window.SetCloseIntercept to call this helper instead of calling
fyneApp.Quit() directly, and change restartApp() to route through the same
helper rather than calling os.Exit(0) so all exits restore the taskbar.

In `@winapi.go`:
- Around line 181-208: findWindowByClass currently swallows lookup failures and
getTaskbarAutoHide flattens the ABM_GETSTATE bitmask to a bool, causing loss of
state when restoring; change findWindowByClass to return (uintptr, error) and
treat a zero hwnd as an error, change getTaskbarAutoHide to return (uint32,
error) and propagate findWindowByClass errors, read the full return value from
procSHAppBarMessage for ABM_GETSTATE (not just evaluating ABS_AUTOHIDE) and
return that bitmask, and change setTaskbarAutoHide to accept the original
bitmask (uint32) and write it back using APPBARDATA.lParam and
procSHAppBarMessage with ABM_SETSTATE so you restore the exact flags you read
(preserving ABS_AUTOHIDE) instead of synthesizing ABS_AUTOHIDE/ABS_ALWAYSONTOP.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bdbeb18-5457-4266-aff5-370e30014520

📥 Commits

Reviewing files that changed from the base of the PR and between a581e16 and 3aa9dc1.

📒 Files selected for processing (7)
  • borderless.go
  • gui_app.go
  • gui_appsetting.go
  • gui_defaults.go
  • main.go
  • settings.go
  • winapi.go

Comment thread gui_app.go
Comment on lines 312 to 315
window.SetCloseIntercept(func() {
if !settings.CloseToTray {
restoreTaskbarOnExit()
fyneApp.Quit()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore the taskbar from every shutdown path, not just window-close.

Line 314 only covers the close intercept. restartApp() in this file still exits via os.Exit(0) without calling restoreTaskbarOnExit(), so update/restart can leave the taskbar stuck in the app-forced state. Please route quit/restart through one shared shutdown helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui_app.go` around lines 312 - 315, The restoreTaskbarOnExit() call is only
invoked in the window close intercept, leaving other shutdown paths (notably
restartApp() which currently calls os.Exit(0)) without taskbar restore; refactor
by creating a single shutdown helper (e.g., shutdownAndExit or performShutdown)
that invokes restoreTaskbarOnExit(), performs any necessary cleanup and then
quits/exits, update window.SetCloseIntercept to call this helper instead of
calling fyneApp.Quit() directly, and change restartApp() to route through the
same helper rather than calling os.Exit(0) so all exits restore the taskbar.

Comment thread winapi.go Outdated
findWindowByClass now returns an error on lookup failure instead of
silently returning zero.  getTaskbarAutoHide returns the full
ABM_GETSTATE bitmask (uint32) so we no longer lose ABS_ALWAYSONTOP
when saving/restoring.  setTaskbarAutoHide accepts that bitmask and
writes it back via ABM_SETSTATE, restoring the exact original flags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tkaiman
Copy link
Copy Markdown
Author

tkaiman commented Mar 12, 2026

Changes in this update

Problem

  • findWindowByClass silently swallowed lookup failures (returned zero hwnd with no error)
  • getTaskbarAutoHide flattened the ABM_GETSTATE bitmask to a bool (ret & ABS_AUTOHIDE != 0), losing the ABS_ALWAYSONTOP flag — so restoring the taskbar could never write back the exact original state
  • setTaskbarAutoHide synthesized ABS_AUTOHIDE or ABS_ALWAYSONTOP from a bool, rather than restoring whatever flags were originally set

What changed

winapi.go

Function Before After
findWindowByClass uintptr — ignored errors from UTF16PtrFromString, returned 0 silently on failure (uintptr, error) — returns wrapped error if string conversion fails or hwnd is zero
getTaskbarAutoHide bool — collapsed bitmask to ABS_AUTOHIDE check (uint32, error) — returns full ABM_GETSTATE bitmask, propagates findWindowByClass errors
setTaskbarAutoHide (bool) — toggled between ABS_AUTOHIDE and ABS_ALWAYSONTOP (uint32) error — writes the provided bitmask directly to APPBARDATA.lParam via ABM_SETSTATE

borderless.go

  • taskbarOriginalState changed from bool to uint32 to hold the full bitmask
  • All call sites (saveTaskbarState, hideTaskbar, restoreTaskbar, restoreTaskbarOnExit) now handle errors — log and bail on failure
  • hideTaskbar passes ABS_AUTOHIDE explicitly
  • restoreTaskbar / restoreTaskbarOnExit pass taskbarOriginalState directly, preserving whatever combination of flags (ABS_AUTOHIDE, ABS_ALWAYSONTOP) was originally active

🤖 Generated with Claude Code

Remove foreground window check so the taskbar stays hidden as long as
the configured app's window exists, rather than restoring immediately
when focus moves to another window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tkaiman
Copy link
Copy Markdown
Author

tkaiman commented Mar 13, 2026

Update: Keep taskbar hidden while app is running

Changed the taskbar hide behavior so it persists as long as the configured app's window exists, rather than only while the app is the foreground window.

What changed

  • Removed the getForegroundWindow() check from the scan loop in main.go
  • Taskbar now stays hidden as long as a matching window with HideTaskbar enabled is found in the window list
  • Taskbar restores when the app is closed (window no longer exists), not when it simply loses focus

This applies to both auto-apply apps and non-auto-apply apps that are already borderless.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
main.go (1)

193-203: Consider early exit once shouldHideTaskbar is true.

Once a single app requires the taskbar hidden, continuing to iterate serves no purpose. This is a minor optimization.

♻️ Suggested optimization
 		// Also check non-auto-apply apps that are already borderless and running
+	outer:
 		for _, appSetting := range settings.Apps {
 			if !appSetting.HideTaskbar {
 				continue
 			}
 			for _, win := range windowData {
 				if matchWindow(win, appSetting) && isBorderless(win) {
 					shouldHideTaskbar = true
+					break outer
 				}
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.go` around lines 193 - 203, Loop continues after shouldHideTaskbar
becomes true; change the nested loops that iterate settings.Apps and windowData
(the block referencing settings.Apps, appSetting, windowData, matchWindow and
isBorderless) to break out early once shouldHideTaskbar is set to true—e.g.,
after setting shouldHideTaskbar = true, break the inner loop and then break the
outer loop (or return immediately if inside a function) to avoid unnecessary
iterations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@main.go`:
- Around line 193-203: Loop continues after shouldHideTaskbar becomes true;
change the nested loops that iterate settings.Apps and windowData (the block
referencing settings.Apps, appSetting, windowData, matchWindow and isBorderless)
to break out early once shouldHideTaskbar is set to true—e.g., after setting
shouldHideTaskbar = true, break the inner loop and then break the outer loop (or
return immediately if inside a function) to avoid unnecessary iterations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cc925e8-2004-49d4-876d-05646dc94db4

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0b256 and 13d3d17.

📒 Files selected for processing (1)
  • main.go

The unbuffered chWindowList channel caused the scan loop to stall
when the GUI wasn't actively consuming updates (e.g. minimized to
tray), which prevented taskbar hiding and borderless logic from
running. Use a buffered channel with non-blocking send to ensure the
scan loop always completes regardless of GUI state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tkaiman
Copy link
Copy Markdown
Author

tkaiman commented Mar 14, 2026

Fix: Scan loop blocking when minimized to tray

Problem

When GoBorderless was minimized to the system tray, the scan loop would stall at the chWindowList <- windowData send. This is because chWindowList was an unbuffered channel — the send blocks until the GUI reads from it. When minimized, the GUI stops consuming, so the scan loop never reaches the taskbar hide/borderless logic below.

This caused the taskbar to not hide until the user opened GoBorderless from the tray (which resumed the GUI's channel reads and unblocked the loop).

Fix

  • Made chWindowList a buffered channel (capacity 1)
  • Changed the send to a non-blocking select — if the GUI isn't reading, stale window data is dropped and the scan loop continues

This ensures borderless application and taskbar hiding always run on every scan iteration, regardless of whether the GUI is active.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
main.go (1)

197-207: Narrow this pass to non-auto-apply apps to match intent.

The comment says “non-auto-apply apps,” but auto-apply entries are also scanned here. Restricting scope and short-circuiting once true will reduce redundant work.

Small cleanup to align code with comment and reduce extra scans
-		// Also check non-auto-apply apps that are already borderless and running
+		// Also check non-auto-apply apps that are already borderless and running
 		for _, appSetting := range settings.Apps {
-			if !appSetting.HideTaskbar {
+			if appSetting.AutoApply || !appSetting.HideTaskbar {
 				continue
 			}
 			for _, win := range windowData {
 				if matchWindow(win, appSetting) && isBorderless(win) {
 					shouldHideTaskbar = true
+					break
 				}
 			}
+			if shouldHideTaskbar {
+				break
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.go` around lines 197 - 207, The loop over settings.Apps is scanning
auto-apply entries even though the intent is "non-auto-apply apps" and it also
continues scanning after shouldHideTaskbar becomes true; update the loop to skip
auto-apply apps (check appSetting.AutoApply and continue) and short-circuit out
when shouldHideTaskbar is set (break the inner/outer loops or return as
appropriate). Locate the block using settings.Apps, appSetting.HideTaskbar,
matchWindow and isBorderless and add the AutoApply check and an early exit once
shouldHideTaskbar is true to avoid redundant work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.go`:
- Around line 164-168: The current non-blocking send to chWindowList drops the
new windowData when the channel is full; instead implement a non-blocking
overwrite: try to send windowData to chWindowList, and if that fails (channel
full) perform a receive from chWindowList to discard the stale entry and then
send windowData so the newest snapshot replaces the old one. Locate the send
using chWindowList and windowData and update the select logic to remove one
stale item on failure before delivering the new snapshot.

---

Nitpick comments:
In `@main.go`:
- Around line 197-207: The loop over settings.Apps is scanning auto-apply
entries even though the intent is "non-auto-apply apps" and it also continues
scanning after shouldHideTaskbar becomes true; update the loop to skip
auto-apply apps (check appSetting.AutoApply and continue) and short-circuit out
when shouldHideTaskbar is set (break the inner/outer loops or return as
appropriate). Locate the block using settings.Apps, appSetting.HideTaskbar,
matchWindow and isBorderless and add the AutoApply check and an early exit once
shouldHideTaskbar is true to avoid redundant work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94ccebf8-4ef9-4b91-9d41-c611a4527b29

📥 Commits

Reviewing files that changed from the base of the PR and between 13d3d17 and c47cabd.

📒 Files selected for processing (1)
  • main.go

Comment thread main.go
Comment on lines +164 to +168
// Non-blocking send: drop stale data if GUI isn't consuming
select {
case chWindowList <- windowData:
default:
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Newest window snapshot is dropped when channel is full.

Line 164 says stale data should be dropped, but this select drops the new windowData and keeps stale data in the channel.

Suggested non-blocking overwrite of stale entry
-		// Non-blocking send: drop stale data if GUI isn't consuming
-		select {
-		case chWindowList <- windowData:
-		default:
-		}
+		// Non-blocking send: keep latest snapshot, drop stale buffered one if needed.
+		select {
+		case chWindowList <- windowData:
+		default:
+			select {
+			case <-chWindowList:
+			default:
+			}
+			select {
+			case chWindowList <- windowData:
+			default:
+			}
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.go` around lines 164 - 168, The current non-blocking send to
chWindowList drops the new windowData when the channel is full; instead
implement a non-blocking overwrite: try to send windowData to chWindowList, and
if that fails (channel full) perform a receive from chWindowList to discard the
stale entry and then send windowData so the newest snapshot replaces the old
one. Locate the send using chWindowList and windowData and update the select
logic to remove one stale item on failure before delivering the new snapshot.

ABM_SETSTATE with ABS_AUTOHIDE only tells Windows to enable auto-hide
mode but doesn't force the taskbar to actually retract. Follow up with
ABM_ACTIVATE to make the taskbar re-evaluate its state and hide
immediately, rather than waiting for a mouse or focus event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tkaiman
Copy link
Copy Markdown
Author

tkaiman commented Mar 15, 2026

Fix: Taskbar not hiding until GoBorderless is opened from tray

Problem

When GoBorderless was minimized to the tray and a game launched, the app would correctly go borderless but the taskbar would remain visible. It only hid once the user opened GoBorderless from the system tray.

Root cause

ABM_SETSTATE with ABS_AUTOHIDE only enables auto-hide mode — it doesn't force the taskbar to actually retract. The taskbar stays visible until something triggers it to re-evaluate (like a focus change from opening GoBorderless from the tray).

Fix

Added an ABM_ACTIVATE call immediately after ABM_SETSTATE in setTaskbarAutoHide(). This tells the taskbar to re-evaluate its auto-hide state and retract immediately.

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