Skip to content
Closed
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
8 changes: 5 additions & 3 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
Expand Down
13 changes: 9 additions & 4 deletions app/qemu_nonwindows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 22 additions & 2 deletions app/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand All @@ -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),
}
Expand Down Expand Up @@ -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
}
Expand Down
26 changes: 19 additions & 7 deletions app/settings_dialog_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const (
settingsCancelID = 2002
settingsBrowseID = 2003
settingsFullID = 2010
settingsWindowID = 2017
settingsBorderlessID = 2018
settingsMemID = 2011
settingsShareID = 2012
settingsFwdID = 2013
Expand Down Expand Up @@ -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 {
Expand All @@ -143,15 +145,16 @@ 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 {
render = renderGPU
} 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 29 additions & 15 deletions app/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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{}
Expand All @@ -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) {
Expand All @@ -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)
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions app/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const (
swpShowWindow = 0x0040
smCxscreen = 0
smCyscreen = 1
smCycaption = 4
iccProgress = 0x20
transparentBkMode = 1
cancelControlID = 1001
Expand Down
Loading