diff --git a/borderless.go b/borderless.go index c736d80..430dce2 100644 --- a/borderless.go +++ b/borderless.go @@ -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 && @@ -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 + } } diff --git a/gui_app.go b/gui_app.go index 4384884..a7cd0e1 100644 --- a/gui_app.go +++ b/gui_app.go @@ -311,6 +311,7 @@ func handleWindowsInit(fyneApp fyne.App, window fyne.Window, settings *Settings) window.SetCloseIntercept(func() { if !settings.CloseToTray { + restoreTaskbarOnExit() fyneApp.Quit() return } diff --git a/gui_appsetting.go b/gui_appsetting.go index 7a697a6..6433f94 100644 --- a/gui_appsetting.go +++ b/gui_appsetting.go @@ -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 @@ -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() @@ -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), diff --git a/gui_defaults.go b/gui_defaults.go index ab0b627..0066b2c 100644 --- a/gui_defaults.go +++ b/gui_defaults.go @@ -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), @@ -95,6 +101,7 @@ func buildDefaultsTab(settings *Settings) *fyne.Container { defaultDisplay, defaultMatchTypeLabel, defaultMatchType, + defaultHideTaskbar, defaultsGrid, )) } diff --git a/main.go b/main.go index 1d79503..a74f13f 100644 --- a/main.go +++ b/main.go @@ -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 @@ -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: + } + + shouldHideTaskbar := false + for appSettingIdx, appSetting := range settings.Apps { if !appSetting.AutoApply { continue @@ -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 } } @@ -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) diff --git a/settings.go b/settings.go index 5e917fd..fc31e50 100644 --- a/settings.go +++ b/settings.go @@ -55,19 +55,20 @@ 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 { @@ -75,12 +76,13 @@ func (as AppSetting) Display() string { } 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 { diff --git a/winapi.go b/winapi.go index 855ce0e..42d937e 100644 --- a/winapi.go +++ b/winapi.go @@ -14,6 +14,23 @@ 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") @@ -21,7 +38,10 @@ var ( 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) { @@ -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 +}