From 3f2b6358da407f66b6dac44b581371e9c8629638 Mon Sep 17 00:00:00 2001 From: nextestudios Date: Thu, 24 Sep 2026 12:18:26 -0300 Subject: [PATCH] fix: consolemode://stop and Start + Select close Big Picture / Playnite before restoring Leaving console mode from outside the session restored the monitors but left the front-end running, so Big Picture ended up on the desk monitor and had to be closed by hand. LaunchService.CloseFrontEnd asks Steam to leave Big Picture (steam://close/bigpicture) or Playnite to close, falling back to WM_CLOSE on its window; MainViewModel.StopConsoleAsync then lets the loop restore the desk on its own (the same path as exiting Big Picture yourself) and restores explicitly only if console mode is still active. Xbox mode has no window and just restores. The tray's Restore is unchanged. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +-- src/ConsoleMode/App.xaml.cs | 4 +-- src/ConsoleMode/Native/NativeWindows.cs | 5 +++ src/ConsoleMode/Services/LaunchService.cs | 40 +++++++++++++++++++++ src/ConsoleMode/ViewModels/MainViewModel.cs | 16 +++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cf98f81..c39e81d 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Typical setup: two desk monitors and a distant HDMI TV that was off. Console Mod 2. On first launch a short tour shows the screen map: pick the screen you play on (the others turn off) 3. From then on it is one click: **Play now**, or the **Console Mode 1 Click** desktop shortcut (Settings → Create shortcut). The first time with a new game screen, the TV asks "Can you see this screen?" and everything reverts on its own if nobody answers (mouse, keyboard or Xbox/PlayStation controller) 4. Or skip the PC altogether: with the app in the tray (turn on **Start with Windows**), **hold the Xbox button on the controller for 1 second** and console mode starts from the couch. **Hold Start + Select for 1 second** during a session to go back to the PC. Xbox (XInput) controllers only. In Settings you can switch to a **short press**: the app then turns off the controller shortcut for Game Bar in Windows (Win+G keeps working); if Steam is open, also turn off "Guide button focuses Steam" in Steam -5. Automation: `consolemode://start`, `consolemode://stop` and `consolemode://show` links work from Stream Deck, launchers, scripts or any shortcut +5. Automation: `consolemode://start`, `consolemode://stop` and `consolemode://show` links work from Stream Deck, launchers, scripts or any shortcut (`stop`, like Start + Select, also closes Big Picture / Playnite before restoring the desk) 6. Two interfaces: the **Desktop** one (mouse, screen map) and the **Console** one (full screen, big cards, D-pad/stick + A/B, works with Xbox and PlayStation pads). By default the app picks Console whenever a controller is connected; change it in Settings → Interface or with the switch button on either screen 4. When you are done, exit Big Picture / Playnite (or restore manually in Xbox mode) 5. New versions are announced in the app from GitHub Releases (installed: updates silently; portable: swaps the exe) @@ -191,7 +191,7 @@ Transforme seu PC Windows em um **console de jogos** com um clique: foque na TV, 2. Na primeira vez, um tour curto mostra o mapa das telas: escolha a tela onde você joga (as outras desligam) 3. Depois é 1 clique: **Jogar agora**, ou o atalho **Console Mode 1 Click** na Área de Trabalho (Ajustes → Criar atalho). Na primeira vez com uma tela de jogo nova, a TV pergunta "Está vendo esta tela?" e tudo volta sozinho se ninguém responder (mouse, teclado ou controle de Xbox/PlayStation) 4. Ou nem encoste no PC: com o app na bandeja (ligue **Iniciar com o Windows**), **segure o botão Xbox do controle por 1 segundo** e o modo console entra direto do sofá. **Segure Start + Select por 1 segundo** durante a sessão para voltar ao PC. Só controles Xbox (XInput). Em Ajustes dá para trocar por um **toque curto**: aí o app desliga o atalho do controle para a Game Bar no Windows (Win+G continua funcionando); se a Steam estiver aberta, desligue também "Botão Guide foca a Steam" na Steam -5. Automação: os links `consolemode://start`, `consolemode://stop` e `consolemode://show` funcionam em Stream Deck, launchers, scripts ou qualquer atalho +5. Automação: os links `consolemode://start`, `consolemode://stop` e `consolemode://show` funcionam em Stream Deck, launchers, scripts ou qualquer atalho (`stop`, assim como Start + Select, também fecha o Big Picture / Playnite antes de restaurar a mesa) 6. Duas interfaces: a **Desktop** (mouse, mapa de telas) e a **Console** (tela cheia, cartões grandes, D-pad/analógico + A/B, funciona com controles Xbox e PlayStation). Por padrão o app escolhe Console sempre que há um controle conectado; mude em Ajustes → Interface ou pelo botão de troca em qualquer uma das telas 4. Ao terminar, saia do Big Picture / Playnite (ou restaure manualmente no Modo Xbox) 5. O app avisa das versões novas pelas releases do GitHub (instalado: atualiza sozinho; portátil: troca o exe) diff --git a/src/ConsoleMode/App.xaml.cs b/src/ConsoleMode/App.xaml.cs index e5cb7f8..27e1b73 100644 --- a/src/ConsoleMode/App.xaml.cs +++ b/src/ConsoleMode/App.xaml.cs @@ -125,7 +125,7 @@ private void ListenForSignals() _window?.DispatcherQueue.TryEnqueue(() => { if (ViewModel is null) return; - if (ViewModel.IsConsoleActive) _ = ViewModel.RestoreNowAsync(); + if (ViewModel.IsConsoleActive) _ = ViewModel.StopConsoleAsync(); else _tray?.ShowWindow(); }), null, Timeout.Infinite, executeOnlyOnce: false)); } @@ -153,7 +153,7 @@ private void WatchGuideButton() _ = HandleStartRequestAsync(); }; _exitChord = new ControllerHoldWatcher(_window.DispatcherQueue, (ushort)(ControllerHoldWatcher.StartButton | ControllerHoldWatcher.BackButton), "Start + Back"); - _exitChord.Held += () => { if (ViewModel?.IsConsoleActive == true) _ = ViewModel.RestoreNowAsync(); }; + _exitChord.Held += () => { if (ViewModel?.IsConsoleActive == true) _ = ViewModel.StopConsoleAsync(); }; ViewModel.PropertyChanged += OnViewModelChangedForGuide; RefreshGuideWatch(); diff --git a/src/ConsoleMode/Native/NativeWindows.cs b/src/ConsoleMode/Native/NativeWindows.cs index eea21a1..fc8f5f4 100644 --- a/src/ConsoleMode/Native/NativeWindows.cs +++ b/src/ConsoleMode/Native/NativeWindows.cs @@ -38,6 +38,11 @@ public static class NativeWindows [DllImport("user32.dll")] public static extern bool IsWindow(nint hWnd); + [DllImport("user32.dll")] + public static extern bool PostMessage(nint hWnd, uint Msg, nint wParam, nint lParam); + + public const uint WM_CLOSE = 0x0010; + [DllImport("user32.dll")] public static extern nint SetWinEventHook(uint eventMin, uint eventMax, nint hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); diff --git a/src/ConsoleMode/Services/LaunchService.cs b/src/ConsoleMode/Services/LaunchService.cs index 561c49c..e63a0e6 100644 --- a/src/ConsoleMode/Services/LaunchService.cs +++ b/src/ConsoleMode/Services/LaunchService.cs @@ -196,6 +196,46 @@ public bool IsXboxActive(ConsoleRuntimeState state) return false; } + /// + /// Asks the fullscreen front-end to quit, so leaving console mode from outside doesn't leave + /// Big Picture / Playnite sitting on the desk monitor. Xbox mode has no window of its own. + /// Returns once it is gone, or after . + /// + public bool CloseFrontEnd(string mode, ConsoleRuntimeState state, int timeoutMs = 8000) + { + if (mode == "xboxMode" || !IsFullscreenActive(mode, state)) return true; + var handles = GetFullscreenHandles(mode); + if (mode == "playnite") + { + foreach (var p in Process.GetProcessesByName("Playnite.FullscreenApp")) + { + try { p.CloseMainWindow(); } catch { /* best effort */ } + } + } + else + { + // Steam's own URL leaves Big Picture the clean way (back to the desktop client) + try { ProcessRunner.StartDetached("steam://close/bigpicture"); } + catch (Exception ex) { AppLog.Write($"Fechar Big Picture: {ex.Message}"); } + } + + var deadline = Environment.TickCount64 + timeoutMs; + var nudged = false; + while (Environment.TickCount64 < deadline) + { + Thread.Sleep(500); + if (!IsFullscreenActive(mode, state)) return true; + // halfway through and still there: close the window itself + if (!nudged && Environment.TickCount64 > deadline - timeoutMs / 2) + { + nudged = true; + foreach (var h in handles.Concat(GetFullscreenHandles(mode)).Distinct()) + NativeWindows.PostMessage(h, NativeWindows.WM_CLOSE, 0, 0); + } + } + return !IsFullscreenActive(mode, state); + } + public bool IsFullscreenActive(string mode, ConsoleRuntimeState state) => mode switch { "bigPicture" => IsBigPictureActive(state), diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs index 99a0afb..7b63f1a 100644 --- a/src/ConsoleMode/ViewModels/MainViewModel.cs +++ b/src/ConsoleMode/ViewModels/MainViewModel.cs @@ -775,6 +775,22 @@ private void EndTour() TrySave(config); } + /// + /// Leave console mode on request from outside the session (consolemode://stop, the + /// controller exit chord): quit Big Picture / Playnite first — the loop then restores the + /// desk on its own, as when the user exits it — and restore explicitly if still active. + /// The tray's Restore keeps restoring only. + /// + public async Task StopConsoleAsync() + { + if (!IsConsoleActive) return; + var mode = Engine.State.FullscreenMode; + var closed = await Task.Run(() => Engine.Launch.CloseFrontEnd(mode, Engine.State)); + AppLog.Write($"Stop-ConsoleMode: {mode} {(closed ? "fechado" : "ainda aberto")}"); + for (var i = 0; i < 12 && IsConsoleActive; i++) await Task.Delay(500); + if (IsConsoleActive) await RestoreNowAsync(); + } + [RelayCommand] public async Task RestoreNowAsync() {