From 0bdf510ecd9839867477cd1a955a64cc93073ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20Gangst=C3=B8?= Date: Sun, 13 Sep 2026 00:40:13 +0200 Subject: [PATCH] add borderless window support --- app/main.go | 8 +++-- app/qemu_nonwindows_test.go | 13 ++++--- app/settings.go | 24 +++++++++++-- app/settings_dialog_windows.go | 26 ++++++++++---- app/settings_test.go | 44 +++++++++++++++-------- app/ui.go | 1 + app/winapi.go | 65 +++++++++++++++++++++++----------- app/winkey.go | 3 +- 8 files changed, 131 insertions(+), 53 deletions(-) diff --git a/app/main.go b/app/main.go index de606b0..fabd14b 100644 --- a/app/main.go +++ b/app/main.go @@ -33,6 +33,7 @@ type config struct { dir, hostDir, payloadDir string winqEmu, share string fresh, fullscreen, noGpu bool + borderless bool hostCursor bool lanPublic bool instant, portable bool @@ -138,6 +139,7 @@ func main() { flag.BoolVar(&cfg.fresh, "fresh", false, "start over and retain the previous writable disk for recovery") flag.IntVar(&cfg.displays, "displays", 1, "number of guest displays (1 to 16)") flag.BoolVar(&cfg.fullscreen, "fullscreen", false, "start fullscreen (Immersive)") + flag.BoolVar(&cfg.borderless, "borderless", false, "start borderless in the desktop work area") flag.IntVar(&cfg.memOverrideMiB, "memory", 0, "guest RAM in MiB (default: sized to this PC)") flag.IntVar(&cfg.cpuOverride, "cpus", 0, "guest CPUs (default: sized to this PC)") flag.IntVar(&cfg.diskGiB, "disk-size", 0, "guest disk capacity in GiB (0: default; grows existing disks, never shrinks)") @@ -704,8 +706,8 @@ func main() { os.Setenv("SDL_GRAB_KEYBOARD", "0") // Launch-UX contract (NOTES.md): guest console sized to the window it will // actually get, so the picture fills it from the first frame. - conW, conH := screenSize(cfg.fullscreen) - if !cfg.fullscreen { + conW, conH := screenSize(cfg.fullscreen, cfg.borderless) + if !cfg.fullscreen && !cfg.borderless { if p := rememberedWindow(cfg.dir); p != nil && !p.Maximized { conW, conH = p.consoleSize() } @@ -718,7 +720,7 @@ func main() { go runGuestAgent() go runWinKeyHook() go runWinKeyQmp() - go runTitleEnforcer(cfg.dir, cfg.fullscreen) + go runTitleEnforcer(cfg.dir, cfg.fullscreen, cfg.borderless) go runCursorReleaseGuard() go runCloseGuard() runClipboardBridge() diff --git a/app/qemu_nonwindows_test.go b/app/qemu_nonwindows_test.go index e356ea7..1629069 100644 --- a/app/qemu_nonwindows_test.go +++ b/app/qemu_nonwindows_test.go @@ -18,6 +18,7 @@ type config struct { dir, hostDir, payloadDir string winqEmu, share string fresh, fullscreen, noGpu bool + borderless bool hostCursor bool lanPublic bool instant, portable bool @@ -213,8 +214,10 @@ func TestPrepareDiskGrowsCompleteOlderDiskWithoutReplacingIt(t *testing.T) { func TestBuildQemuArgsKeepsKernelIrqchipUnlessRefused(t *testing.T) { for _, gpu := range []bool{true, false} { - cfg := &config{vmDir: "/vm", guestDir: "/guest", disk: "/vm/disk.raw", - diskFormat: "raw", memMiB: 4096, audio: "none", useGpu: gpu} + cfg := &config{ + vmDir: "/vm", guestDir: "/guest", disk: "/vm/disk.raw", + diskFormat: "raw", memMiB: 4096, audio: "none", useGpu: gpu, + } args := strings.Join(buildQemuArgs(cfg, "root=/dev/vda"), " ") if !strings.Contains(args, "-machine q35,accel=whpx -cpu") { t.Fatalf("gpu=%v: default machine missing: %s", gpu, args) @@ -335,8 +338,10 @@ func TestBuildQemuArgsEscapesCommasInsidePaths(t *testing.T) { } func TestBuildQemuArgsUsesTheChosenCPUCountAndHostMem(t *testing.T) { - cfg := &config{vmDir: "/vm", guestDir: "/guest", disk: "/vm/disk.raw", diskFormat: "raw", - memMiB: 8192, hostTotalMiB: 32768, cpus: 6, audio: "none", useGpu: true} + cfg := &config{ + vmDir: "/vm", guestDir: "/guest", disk: "/vm/disk.raw", diskFormat: "raw", + memMiB: 8192, hostTotalMiB: 32768, cpus: 6, audio: "none", useGpu: true, + } args := strings.Join(buildQemuArgs(cfg, "root=/dev/vda"), " ") if !strings.Contains(args, " -smp 6 -m 8192M ") || !strings.Contains(args, "hostmem=4294967296") { t.Fatalf("cpu count or hostmem missing: %s", args) diff --git a/app/settings.go b/app/settings.go index 2920e6c..4adc81b 100644 --- a/app/settings.go +++ b/app/settings.go @@ -22,6 +22,8 @@ type settings struct { SchemaVersion int `json:"schemaVersion"` // Immersive: open fullscreen instead of in a window. Fullscreen bool `json:"fullscreen"` + // Borderless: open without window decorations in the desktop work area. + Borderless bool `json:"borderless,omitempty"` // Guest RAM in MiB. 0 sizes it to the machine automatically. MemoryMiB int `json:"memoryMiB"` // Guest CPUs. 0 sizes them to the machine automatically. @@ -131,6 +133,9 @@ func (s settings) validate() error { if s.Displays < 0 || s.Displays > maximumGuestDisplays { return fmt.Errorf("displays must be between 1 and %d", maximumGuestDisplays) } + if s.Fullscreen && s.Borderless { + return errors.New("fullscreen and borderless cannot both be enabled") + } if s.MemoryMiB != 0 && (s.MemoryMiB < minimumGuestMemoryMiB || s.MemoryMiB > maximumGuestMemoryMiB) { return fmt.Errorf("memoryMiB must be 0 (automatic) or between %d and %d", minimumGuestMemoryMiB, maximumGuestMemoryMiB) } @@ -152,9 +157,9 @@ func (s settings) validate() error { // settingsFromForm converts the Win32 controls into the persisted model. It // stays outside the window procedure so all input and file validation is // covered by the platform-independent test suite. -func settingsFromForm(fullscreen, shareEnabled bool, memory, cpus, share, forwards, sshKey, render string) (settings, error) { +func settingsFromForm(fullscreen, borderless, shareEnabled bool, memory, cpus, share, forwards, sshKey, render string) (settings, error) { s := settings{ - Fullscreen: fullscreen, Share: strings.TrimSpace(share), Render: strings.TrimSpace(render), + Fullscreen: fullscreen, Borderless: borderless, Share: strings.TrimSpace(share), Render: strings.TrimSpace(render), ShareDisabled: !shareEnabled, SharedFolderPrompted: true, SSHKey: strings.TrimSpace(sshKey), } @@ -227,6 +232,21 @@ func applySettings(cfg *config, s settings, explicit map[string]bool, forwards * if !explicit["fullscreen"] { cfg.fullscreen = s.Fullscreen } + if !explicit["borderless"] { + cfg.borderless = s.Borderless + } + if cfg.fullscreen && cfg.borderless { + switch { + case explicit["fullscreen"] && explicit["borderless"]: + return errors.New("-fullscreen and -borderless cannot be used together") + case explicit["fullscreen"]: + cfg.borderless = false + case explicit["borderless"]: + cfg.fullscreen = false + default: + return errors.New("fullscreen and borderless cannot both be enabled") + } + } if !explicit["memory"] { cfg.memOverrideMiB = s.MemoryMiB } diff --git a/app/settings_dialog_windows.go b/app/settings_dialog_windows.go index 768726b..35326c6 100644 --- a/app/settings_dialog_windows.go +++ b/app/settings_dialog_windows.go @@ -53,6 +53,8 @@ const ( settingsCancelID = 2002 settingsBrowseID = 2003 settingsFullID = 2010 + settingsWindowID = 2017 + settingsBorderlessID = 2018 settingsMemID = 2011 settingsShareID = 2012 settingsFwdID = 2013 @@ -129,7 +131,7 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { className, _ := syscall.UTF16PtrFromString("TryOmarchySettings") var hwnd uintptr var scroll settingsScroll - var hFull, hMem, hCPUs, hDisk, hShare, hShareOn, hFwd, hKey uintptr + var hWindow, hFull, hBorderless, hMem, hCPUs, hDisk, hShare, hShareOn, hFwd, hKey uintptr var hRenderAuto, hRenderGPU, hRenderCPU, hDisplays, hLANPublic uintptr text := func(handle uintptr) string { @@ -143,7 +145,8 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { procSendMessageW.Call(handle, wmSettext, 0, uintptr(unsafe.Pointer(t))) } collect := func() (settings, error) { - checked, _, _ := procSendMessageW.Call(hFull, bmGetcheck, 0, 0) + fullscreen, _, _ := procSendMessageW.Call(hFull, bmGetcheck, 0, 0) + borderless, _, _ := procSendMessageW.Call(hBorderless, bmGetcheck, 0, 0) shareChecked, _, _ := procSendMessageW.Call(hShareOn, bmGetcheck, 0, 0) render := renderAuto if r, _, _ := procSendMessageW.Call(hRenderGPU, bmGetcheck, 0, 0); r == bstChecked { @@ -151,7 +154,7 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { } else if r, _, _ := procSendMessageW.Call(hRenderCPU, bmGetcheck, 0, 0); r == bstChecked { render = renderCPU } - s, err := settingsFromForm(checked == bstChecked, shareChecked == bstChecked, + s, err := settingsFromForm(fullscreen == bstChecked, borderless == bstChecked, shareChecked == bstChecked, text(hMem), text(hCPUs), text(hShare), text(hFwd), text(hKey), render) if err != nil { return s, err @@ -332,8 +335,10 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { } cursor, _, _ := procLoadCursorW.Call(0, idcArrow) icon, _, _ := procLoadIconW.Call(hInst, 1) - wc := wndclassex{size: uint32(unsafe.Sizeof(wndclassex{})), wndProc: wndProc, inst: hInst, - icon: icon, cursor: cursor, brush: colorBtnface + 1, className: className, iconSm: icon} + wc := wndclassex{ + size: uint32(unsafe.Sizeof(wndclassex{})), wndProc: wndProc, inst: hInst, + icon: icon, cursor: cursor, brush: colorBtnface + 1, className: className, iconSm: icon, + } if atom, _, err := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 { logf("settings: RegisterClassExW failed: %v", err) return false @@ -379,9 +384,16 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { } const left, labelW, fieldX, fieldW = 16, 150, 170, 294 y := int32(16) - hFull = mk("BUTTON", "Open fullscreen (Immersive)", left, y, 300, 22, bsAutocheckbox|wsTabstop, settingsFullID) + mk("STATIC", "Window mode", left, y+3, labelW, 20, ssNoprefix, 0) + hWindow = mk("BUTTON", "Windowed", fieldX, y, 90, 22, bsAutoradiobutton|wsGroup|wsTabstop, settingsWindowID) + hFull = mk("BUTTON", "Fullscreen", fieldX+96, y, 90, 22, bsAutoradiobutton, settingsFullID) + hBorderless = mk("BUTTON", "Borderless", fieldX+192, y, 90, 22, bsAutoradiobutton, settingsBorderlessID) if current.Fullscreen { procSendMessageW.Call(hFull, bmSetcheck, bstChecked, 0) + } else if current.Borderless { + procSendMessageW.Call(hBorderless, bmSetcheck, bstChecked, 0) + } else { + procSendMessageW.Call(hWindow, bmSetcheck, bstChecked, 0) } y += 30 mk("STATIC", "Guest displays", left, y+3, labelW, 20, ssNoprefix, 0) @@ -498,7 +510,7 @@ func runSettingsDialog(path, dataDir string, portable bool) (saved bool) { procSetWindowPos.Call(hwnd, hwndTopmost, 0, 0, 0, 0, swpNoSize|swpNoMove|swpShowWindow) procSetForegroundWindow.Call(hwnd) procSetWindowPos.Call(hwnd, hwndNotTopmost, 0, 0, 0, 0, swpNoSize|swpNoMove|swpShowWindow) - procSetFocus.Call(hFull) + procSetFocus.Call(hWindow) var m msgStruct for { diff --git a/app/settings_test.go b/app/settings_test.go index f3ff643..63426b8 100644 --- a/app/settings_test.go +++ b/app/settings_test.go @@ -16,8 +16,10 @@ func TestLoadSettingsMissingFileIsDefaults(t *testing.T) { func TestSettingsRoundTrip(t *testing.T) { path := settingsPath(filepath.Join(t.TempDir(), "TryOmarchy")) - in := settings{Fullscreen: true, MemoryMiB: 6144, Share: `C:\Users\me\Work`, SharedFolderPrompted: true, - Forwards: []string{"tcp:2222:22", "udp:5000:5000"}, SSHKey: `C:\Users\me\.ssh\work.pub`} + in := settings{ + Borderless: true, MemoryMiB: 6144, Share: `C:\Users\me\Work`, SharedFolderPrompted: true, + Forwards: []string{"tcp:2222:22", "udp:5000:5000"}, SSHKey: `C:\Users\me\.ssh\work.pub`, + } if err := saveSettings(path, in); err != nil { t.Fatal(err) } @@ -29,7 +31,7 @@ func TestSettingsRoundTrip(t *testing.T) { t.Fatal(err) } in.SchemaVersion = settingsSchemaVersion - if out.SchemaVersion != in.SchemaVersion || out.Fullscreen != in.Fullscreen || out.MemoryMiB != in.MemoryMiB || + if out.SchemaVersion != in.SchemaVersion || out.Fullscreen != in.Fullscreen || out.Borderless != in.Borderless || out.MemoryMiB != in.MemoryMiB || out.Share != in.Share || out.ShareDisabled != in.ShareDisabled || out.SharedFolderPrompted != in.SharedFolderPrompted || out.SSHKey != in.SSHKey || strings.Join(out.Forwards, ",") != strings.Join(in.Forwards, ",") { t.Fatalf("round trip changed settings: %+v vs %+v", out, in) @@ -58,8 +60,10 @@ func TestLoadSettingsRejectsDamageInsteadOfIgnoringIt(t *testing.T) { } func TestApplySettingsLetsExplicitFlagsWin(t *testing.T) { - file := settings{Fullscreen: true, MemoryMiB: 4096, Share: `D:\Share`, - Forwards: []string{"tcp:2222:22"}, SSHKey: `D:\key.pub`} + file := settings{ + Borderless: true, MemoryMiB: 4096, Share: `D:\Share`, + Forwards: []string{"tcp:2222:22"}, SSHKey: `D:\key.pub`, + } // Nothing on the command line: the file decides every row. cfg := &config{} @@ -68,21 +72,31 @@ func TestApplySettingsLetsExplicitFlagsWin(t *testing.T) { if err := applySettings(cfg, file, map[string]bool{}, &forwards, &keyPath); err != nil { t.Fatal(err) } - if !cfg.fullscreen || cfg.memOverrideMiB != 4096 || cfg.share != `D:\Share` || keyPath != `D:\key.pub` || forwards.String() != "tcp:2222:22" { + if !cfg.borderless || cfg.memOverrideMiB != 4096 || cfg.share != `D:\Share` || keyPath != `D:\key.pub` || forwards.String() != "tcp:2222:22" { t.Fatalf("file not applied: %+v forwards=%s key=%s", cfg, forwards.String(), keyPath) } // Explicit flags keep their values; an explicit -ssh replaces the list. - cfg = &config{fullscreen: false, memOverrideMiB: 0, share: ""} + cfg = &config{fullscreen: false, borderless: false, memOverrideMiB: 0, share: ""} forwards = forwardList{{"tcp", 2299, 22, ""}} keyPath = "" - explicit := map[string]bool{"fullscreen": true, "memory": true, "share": true, "ssh": true, "ssh-key": true} + explicit := map[string]bool{"borderless": true, "memory": true, "share": true, "ssh": true, "ssh-key": true} if err := applySettings(cfg, file, explicit, &forwards, &keyPath); err != nil { t.Fatal(err) } - if cfg.fullscreen || cfg.memOverrideMiB != 0 || cfg.share != "" || keyPath != "" || forwards.String() != "tcp:2299:22" { + if cfg.borderless || cfg.memOverrideMiB != 0 || cfg.share != "" || keyPath != "" || forwards.String() != "tcp:2299:22" { t.Fatalf("explicit flags overridden: %+v forwards=%s key=%s", cfg, forwards.String(), keyPath) } + + // An explicit display mode replaces the other mode saved in the file. + cfg = &config{fullscreen: true} + if err := applySettings(cfg, file, map[string]bool{"fullscreen": true}, &forwards, &keyPath); err != nil || !cfg.fullscreen || cfg.borderless { + t.Fatalf("explicit fullscreen did not override borderless settings: %+v %v", cfg, err) + } + cfg = &config{borderless: true} + if err := applySettings(cfg, settings{Fullscreen: true}, map[string]bool{"borderless": true}, &forwards, &keyPath); err != nil || cfg.fullscreen || !cfg.borderless { + t.Fatalf("explicit borderless did not override fullscreen settings: %+v %v", cfg, err) + } } func TestApplySettingsKeepsDisabledShareInactive(t *testing.T) { @@ -106,7 +120,7 @@ func TestSettingsFromFormParsesAndValidatesEveryRow(t *testing.T) { t.Fatal(err) } - s, err := settingsFromForm(true, true, " 6144 ", " 4 ", ` C:\Users\me\Work `, " tcp:2222:22\r\n\r\n udp:5000:5000 ", " "+keyPath+" ", " GPU ") + s, err := settingsFromForm(true, false, true, " 6144 ", " 4 ", ` C:\Users\me\Work `, " tcp:2222:22\r\n\r\n udp:5000:5000 ", " "+keyPath+" ", " GPU ") if err != nil { t.Fatal(err) } @@ -121,19 +135,19 @@ func TestSettingsFromFormParsesAndValidatesEveryRow(t *testing.T) { "forward": {"0", "tcp:22", ""}, "key": {"0", "tcp:2222:22", filepath.Join(dir, "missing.pub")}, } { - if _, err := settingsFromForm(false, false, input[0], "", "", input[1], input[2], ""); err == nil { + if _, err := settingsFromForm(false, false, false, input[0], "", "", input[1], input[2], ""); err == nil { t.Fatalf("%s input accepted", name) } } for _, cpus := range []string{"many", "0x4", "65", "-1"} { - if _, err := settingsFromForm(false, false, "0", cpus, "", "", "", ""); err == nil { + if _, err := settingsFromForm(false, false, false, "0", cpus, "", "", "", ""); err == nil { t.Fatalf("cpus %q accepted", cpus) } } - if _, err := settingsFromForm(false, false, "0", "", "", "", "", "software"); err == nil { + if _, err := settingsFromForm(false, false, false, "0", "", "", "", "", "software"); err == nil { t.Fatal("unknown render mode accepted") } - if s, err := settingsFromForm(false, false, "0", "", "", "", "", " auto "); err != nil || s.Render != "" { + if s, err := settingsFromForm(false, false, false, "0", "", "", "", "", " auto "); err != nil || s.Render != "" { t.Fatalf("automatic rendering should be stored as the empty default, got %q %v", s.Render, err) } } @@ -166,7 +180,7 @@ func TestSharedFolderOfferAndEnableState(t *testing.T) { t.Fatalf("disabled share = %q", got) } - s, err := settingsFromForm(false, false, "0", "", `C:\Users\me\Work`, "", "", "") + s, err := settingsFromForm(false, false, false, "0", "", `C:\Users\me\Work`, "", "", "") if err != nil || !s.ShareDisabled || s.activeShare() != "" || !s.SharedFolderPrompted { t.Fatalf("disabled form state = %+v, %v", s, err) } diff --git a/app/ui.go b/app/ui.go index 6e33945..c6fc717 100644 --- a/app/ui.go +++ b/app/ui.go @@ -78,6 +78,7 @@ const ( swpShowWindow = 0x0040 smCxscreen = 0 smCyscreen = 1 + smCycaption = 4 iccProgress = 0x20 transparentBkMode = 1 cancelControlID = 1001 diff --git a/app/winapi.go b/app/winapi.go index 385fbe3..a744100 100644 --- a/app/winapi.go +++ b/app/winapi.go @@ -32,6 +32,8 @@ var ( procIsWindowVisible = user32.NewProc("IsWindowVisible") procGetWindowTextW = user32.NewProc("GetWindowTextW") procSetWindowTextW = user32.NewProc("SetWindowTextW") + procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW") + procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW") procOpenClipboard = user32.NewProc("OpenClipboard") procCloseClipboard = user32.NewProc("CloseClipboard") procEmptyClipboard = user32.NewProc("EmptyClipboard") @@ -50,22 +52,26 @@ var ( ) const ( - mbIconError = 0x10 - whKeyboardLL = 13 - wmKeydown = 0x100 - wmSyskeydown = 0x104 - vkLwin = 0x5B - vkRwin = 0x5C - vkSnapshot = 0x2C - qsAllinput = 0x04FF - pmRemove = 1 - cfUnicodetext = 13 - cfDib = 8 - cfDibV5 = 17 - gmemMoveable = 2 - fsctlSetSparse = 0x900C4 - fsctlSetZeroData = 0x980C8 - maxTitle = 256 + mbIconError = 0x10 + whKeyboardLL = 13 + wmKeydown = 0x100 + wmSyskeydown = 0x104 + vkLwin = 0x5B + vkRwin = 0x5C + vkSnapshot = 0x2C + qsAllinput = 0x04FF + pmRemove = 1 + cfUnicodetext = 13 + cfDib = 8 + cfDibV5 = 17 + gmemMoveable = 2 + fsctlSetSparse = 0x900C4 + fsctlSetZeroData = 0x980C8 + maxTitle = 256 + gwlpStyle uintptr = ^uintptr(15) + swpNoZorder = 0x0004 + swpFramechanged = 0x0020 + qemuWindowFrame = 0x00CF0000 // caption, resize frame, system menu, min/max buttons ) type msgStruct struct { @@ -149,10 +155,9 @@ func sparseCopy(dst *os.File, src *os.File, total int64, ui *progressUI) error { } // screenSize returns the primary screen bounds (fullscreen) or the desktop -// work area minus window chrome (windowed) - the guest console is sized to -// match so the picture fills the window from the first frame (launch-UX -// contract in NOTES.md). -func screenSize(fullscreen bool) (int, int) { +// work area (borderless/windowed). A framed window loses its title bar, so +// the guest console is sized to match from the first frame. +func screenSize(fullscreen, borderless bool) (int, int) { if fullscreen { w, _, _ := procGetSystemMetrics.Call(smCxscreen) h, _, _ := procGetSystemMetrics.Call(smCyscreen) @@ -163,7 +168,12 @@ func screenSize(fullscreen bool) (int, int) { if ret, _, _ := procSystemParametersInfoW.Call(spiGetworkarea, 0, uintptr(unsafe.Pointer(&r)), 0); ret == 0 { return 1280, 800 } - return int(r.right - r.left), int(r.bottom-r.top) - 31 // minus title bar + height := int(r.bottom - r.top) + if !borderless { + captionHeight, _, _ := procGetSystemMetrics.Call(smCycaption) + height -= int(captionHeight) + } + return int(r.right - r.left), height } func foregroundPid() uint32 { @@ -215,6 +225,7 @@ var ( enumTitleIcon uintptr enumTitleDir string enumTitleFullscreen bool + enumTitleBorderless bool enumTitleWindows = map[uintptr]*displayWindowState{} enumTitleSeen = map[uintptr]bool{} enumTitleMonitors []screenRect @@ -242,6 +253,9 @@ func enumTitleProc(hwnd, _ uintptr) uintptr { return 1 } enumTitleSeen[hwnd] = true + if enumTitleBorderless { + makeBorderless(hwnd) + } var buf [maxTitle]uint16 procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), maxTitle) title := syscall.UTF16ToString(buf[:]) @@ -312,6 +326,15 @@ func enumTitleProc(hwnd, _ uintptr) uintptr { return 1 } +func makeBorderless(hwnd uintptr) { + style, _, _ := procGetWindowLongPtrW.Call(hwnd, uintptr(gwlpStyle)) + if style&uintptr(qemuWindowFrame) == 0 { + return + } + procSetWindowLongPtrW.Call(hwnd, uintptr(gwlpStyle), style&^uintptr(qemuWindowFrame)) + procSetWindowPos.Call(hwnd, 0, 0, 0, 0, 0, swpNoSize|swpNoMove|swpNoZorder|swpFramechanged) +} + func enforceDisplayWindows(pid uint32, dir string, fullscreen bool, icon uintptr) { if pid != enumTitlePid { enumTitleWindows = map[uintptr]*displayWindowState{} diff --git a/app/winkey.go b/app/winkey.go index eac8956..5c61209 100644 --- a/app/winkey.go +++ b/app/winkey.go @@ -178,9 +178,10 @@ func runWinKeyQmp() { // It also remembers where the user leaves the window: the placement is saved // whenever it changes and restored, in place of the maximized default, on // the next windowed launch if that spot is still on a connected display. -func runTitleEnforcer(dir string, fullscreen bool) { +func runTitleEnforcer(dir string, fullscreen, borderless bool) { hInst, _, _ := procGetModuleHandleW.Call(0) appIcon, _, _ := procLoadIconW.Call(hInst, 1) // the embedded Omarchy .ico + enumTitleBorderless = borderless for { if pid := qemuPid.Load(); pid != 0 { enforceDisplayWindows(pid, dir, fullscreen, appIcon)