Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions borderless.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ package main

import (
"fmt"
"sync"

"github.com/lxn/win"
)

var (
taskbarMu sync.Mutex
taskbarOriginalState uint32
taskbarStateSaved bool
taskbarHiddenByApp bool
)

func isBorderless(window Window) bool {
style := getWindowStyle(window.hwnd)
return !(style&win.WS_CAPTION > 0 &&
Expand Down Expand Up @@ -33,4 +41,68 @@ func restoreWindow(window Window, appSetting AppSetting) {
// Restore the border and title bar
setWindowStyle(window.hwnd, style|win.WS_OVERLAPPEDWINDOW)
setWindowPos(window.hwnd, appSetting.PreOffsetX, appSetting.PreOffsetY, appSetting.PreWidth, appSetting.PreHeight)
if appSetting.HideTaskbar {
restoreTaskbar()
}
}

// saveTaskbarState records the user's original taskbar auto-hide preference
// so we can restore it later. Only saves once until explicitly restored.
func saveTaskbarState() {
taskbarMu.Lock()
defer taskbarMu.Unlock()
if !taskbarStateSaved {
state, err := getTaskbarAutoHide()
if err != nil {
fmt.Println("saveTaskbarState:", err)
return
}
taskbarOriginalState = state
taskbarStateSaved = true
}
}

func hideTaskbar() {
taskbarMu.Lock()
defer taskbarMu.Unlock()
if !taskbarHiddenByApp {
if !taskbarStateSaved {
state, err := getTaskbarAutoHide()
if err != nil {
fmt.Println("hideTaskbar:", err)
return
}
taskbarOriginalState = state
taskbarStateSaved = true
}
if err := setTaskbarAutoHide(ABS_AUTOHIDE); err != nil {
fmt.Println("hideTaskbar:", err)
return
}
taskbarHiddenByApp = true
}
}

func restoreTaskbar() {
taskbarMu.Lock()
defer taskbarMu.Unlock()
if taskbarHiddenByApp && taskbarStateSaved {
if err := setTaskbarAutoHide(taskbarOriginalState); err != nil {
fmt.Println("restoreTaskbar:", err)
}
taskbarHiddenByApp = false
}
}

// restoreTaskbarOnExit forces taskbar restoration regardless of tracking state.
// Called when the application is shutting down.
func restoreTaskbarOnExit() {
taskbarMu.Lock()
defer taskbarMu.Unlock()
if taskbarStateSaved {
if err := setTaskbarAutoHide(taskbarOriginalState); err != nil {
fmt.Println("restoreTaskbarOnExit:", err)
}
taskbarHiddenByApp = false
}
}
1 change: 1 addition & 0 deletions gui_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ func handleWindowsInit(fyneApp fyne.App, window fyne.Window, settings *Settings)

window.SetCloseIntercept(func() {
if !settings.CloseToTray {
restoreTaskbarOnExit()
fyneApp.Quit()
Comment on lines 312 to 315

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.

return
}
Expand Down
11 changes: 11 additions & 0 deletions gui_appsetting.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
filterApplications *widget.Check
displaySelect *ui.Select[Monitor]
matchType *widget.RadioGroup
hideTaskbarCheck *widget.Check
xOffsetText *widget.Entry
yOffsetText *widget.Entry
widthText *widget.Entry
Expand Down Expand Up @@ -165,6 +166,15 @@ func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool,
matchType.Horizontal = true
matchType.Required = true

hideTaskbarCheck = widget.NewCheck("Hide Taskbar when active", func(checked bool) {
appSetting.HideTaskbar = checked
})
if isNew {
hideTaskbarCheck.SetChecked(settings.Defaults.HideTaskbar)
} else {
hideTaskbarCheck.SetChecked(appSetting.HideTaskbar)
}

// Textboxes with labels
xOffsetLabel := widget.NewLabel("X Offset:")
xOffsetText = widget.NewEntry()
Expand Down Expand Up @@ -283,6 +293,7 @@ func makeAppSettingWindow(settings *Settings, appSetting AppSetting, isNew bool,
displaySelect,
widget.NewLabel("Match Type"),
matchType,
hideTaskbarCheck,
textGrid,
widget.NewLabel(""), // spacer
container.NewHBox(cancelButton, layout.NewSpacer(), confirmButton),
Expand Down
7 changes: 7 additions & 0 deletions gui_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ func buildDefaultsTab(settings *Settings) *fyne.Container {
defaultMatchType.Horizontal = true
defaultMatchType.Required = true

defaultHideTaskbar := widget.NewCheck("Hide Taskbar when active", func(checked bool) {
settings.Defaults.HideTaskbar = checked
settings.Save()
})
defaultHideTaskbar.SetChecked(settings.Defaults.HideTaskbar)

defaultsGrid := container.NewGridWithRows(2,
container.NewGridWithColumns(2,
container.NewVBox(defaultXOffsetLabel, defaultXOffset),
Expand All @@ -95,6 +101,7 @@ func buildDefaultsTab(settings *Settings) *fyne.Container {
defaultDisplay,
defaultMatchTypeLabel,
defaultMatchType,
defaultHideTaskbar,
defaultsGrid,
))
}
35 changes: 33 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (w Window) String() string {
}

var monitors []Monitor
var chWindowList = make(chan []Window) // Channel to send window list updates
var chWindowList = make(chan []Window, 1) // Buffered so scan loop doesn't block when GUI isn't consuming updates

var ALWAYS_HIDDEN_PROCESSESS = []string{
// Skip self
Expand Down Expand Up @@ -161,7 +161,14 @@ func scanWindows(settings *Settings) {
allWindows := EnumWindows()
windowData := getWindowData(allWindows)

chWindowList <- windowData // Update global window list
// Non-blocking send: drop stale data if GUI isn't consuming
select {
case chWindowList <- windowData:
default:
}
Comment on lines +164 to +168

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.


shouldHideTaskbar := false

for appSettingIdx, appSetting := range settings.Apps {
if !appSetting.AutoApply {
continue
Expand All @@ -178,11 +185,33 @@ func scanWindows(settings *Settings) {
settings.Save()
}
makeBorderless(win, appSetting)
// Hide taskbar as long as this app's window exists
if appSetting.HideTaskbar {
shouldHideTaskbar = true
}
break
}
}
}

// Also check non-auto-apply apps that are already borderless and running
for _, appSetting := range settings.Apps {
if !appSetting.HideTaskbar {
continue
}
for _, win := range windowData {
if matchWindow(win, appSetting) && isBorderless(win) {
shouldHideTaskbar = true
}
}
}

if shouldHideTaskbar {
hideTaskbar()
} else {
restoreTaskbar()
}

time.Sleep(time.Second * 1) // Sleep for 1 second before next scan
}
}
Expand Down Expand Up @@ -217,6 +246,8 @@ func main() {
}

settings.Save()
// Save the user's original taskbar state before we potentially modify it
saveTaskbarState()
fmt.Println(settings)
for _, mon := range monitors {
fmt.Printf("Monitor %d\n", mon.number)
Expand Down
40 changes: 21 additions & 19 deletions settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,32 +55,34 @@ func GetMatchTypeFromString(s string) MatchType {
}

type AppSetting struct {
WindowName string `json:"windowName"`
ExePath string `json:"exePath"`
MatchType MatchType `json:"matchType"`
AutoApply bool `json:"autoApply"`
Monitor int `json:"monitor"`
OffsetX int32 `json:"offsetX"`
OffsetY int32 `json:"offsetY"`
Width int32 `json:"width"`
Height int32 `json:"height"`
PreOffsetX int32 `json:"preOffsetX"`
PreOffsetY int32 `json:"preOffsetY"`
PreWidth int32 `json:"preWidth"`
PreHeight int32 `json:"preHeight"`
WindowName string `json:"windowName"`
ExePath string `json:"exePath"`
MatchType MatchType `json:"matchType"`
AutoApply bool `json:"autoApply"`
HideTaskbar bool `json:"hideTaskbar"`
Monitor int `json:"monitor"`
OffsetX int32 `json:"offsetX"`
OffsetY int32 `json:"offsetY"`
Width int32 `json:"width"`
Height int32 `json:"height"`
PreOffsetX int32 `json:"preOffsetX"`
PreOffsetY int32 `json:"preOffsetY"`
PreWidth int32 `json:"preWidth"`
PreHeight int32 `json:"preHeight"`
}

func (as AppSetting) Display() string {
return fmt.Sprintf("%s | %s", as.WindowName, as.ExePath)
}

type AppSettingDefaults struct {
Monitor int `json:"monitor"`
MatchType MatchType `json:"matchType"`
OffsetX int32 `json:"offsetX"`
OffsetY int32 `json:"offsetY"`
Width int32 `json:"width"`
Height int32 `json:"height"`
Monitor int `json:"monitor"`
MatchType MatchType `json:"matchType"`
HideTaskbar bool `json:"hideTaskbar"`
OffsetX int32 `json:"offsetX"`
OffsetY int32 `json:"offsetY"`
Width int32 `json:"width"`
Height int32 `json:"height"`
}

type Settings struct {
Expand Down
68 changes: 68 additions & 0 deletions winapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,34 @@ const (
maxPath = 260 // Maximum path length for Windows file paths
)

const (
ABM_GETSTATE = 0x00000004
ABM_SETSTATE = 0x0000000A
ABM_ACTIVATE = 0x00000006
ABS_AUTOHIDE = 0x01
ABS_ALWAYSONTOP = 0x02
)

type APPBARDATA struct {
cbSize uint32
hWnd uintptr
uCallbackMessage uint32
uEdge uint32
rc win.RECT
lParam uintptr
}

var (
user32 = windows.NewLazySystemDLL("user32.dll")
shell32 = windows.NewLazySystemDLL("shell32.dll")

procGetWindowTextW = user32.NewProc("GetWindowTextW")
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
procFindWindowW = user32.NewProc("FindWindowW")
procGetKnownFolderPath = shell32.NewProc("SHGetKnownFolderPath")
procSHAppBarMessage = shell32.NewProc("SHAppBarMessage")
)

func enumWindows(callback func(hwnd uintptr, lparam uintptr) uintptr, extra unsafe.Pointer) {
Expand Down Expand Up @@ -153,3 +173,51 @@ func closeMutex(mutex windows.Handle) error {
// func waitForSingleObject(mutex *windows.Mutex) error {
// return windows.WaitForSingleObject(windows.Handle(mutex), windows.INFINITE)
// }

func getForegroundWindow() win.HWND {
ret, _, _ := procGetForegroundWindow.Call()
return win.HWND(ret)
}

func findWindowByClass(className string) (uintptr, error) {
classPtr, err := windows.UTF16PtrFromString(className)
if err != nil {
return 0, fmt.Errorf("findWindowByClass: %w", err)
}
ret, _, _ := procFindWindowW.Call(uintptr(unsafe.Pointer(classPtr)), 0)
if ret == 0 {
return 0, fmt.Errorf("findWindowByClass: window %q not found", className)
}
return ret, nil
}

func getTaskbarAutoHide() (uint32, error) {
taskbarHwnd, err := findWindowByClass("Shell_TrayWnd")
if err != nil {
return 0, fmt.Errorf("getTaskbarAutoHide: %w", err)
}
abd := APPBARDATA{
cbSize: uint32(unsafe.Sizeof(APPBARDATA{})),
hWnd: taskbarHwnd,
}
ret, _, _ := procSHAppBarMessage.Call(ABM_GETSTATE, uintptr(unsafe.Pointer(&abd)))
return uint32(ret), nil
}

func setTaskbarAutoHide(state uint32) error {
taskbarHwnd, err := findWindowByClass("Shell_TrayWnd")
if err != nil {
return fmt.Errorf("setTaskbarAutoHide: %w", err)
}
abd := APPBARDATA{
cbSize: uint32(unsafe.Sizeof(APPBARDATA{})),
hWnd: taskbarHwnd,
lParam: uintptr(state),
}
procSHAppBarMessage.Call(ABM_SETSTATE, uintptr(unsafe.Pointer(&abd)))
// ABM_ACTIVATE forces the taskbar to re-evaluate its auto-hide state,
// causing it to actually retract immediately instead of waiting for
// a mouse/focus event.
procSHAppBarMessage.Call(ABM_ACTIVATE, uintptr(unsafe.Pointer(&abd)))
return nil
}