Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor

## [Unreleased]
### What's new
- Session menu: a pinned panel at the top of the side column with the **File explorer (ControlFS)**, the team's open-source, controller-first file manager. One press opens ControlFS on top, always full screen on the session screen (or brings it to the front if already open); if it isn't installed, its download page opens. Console Mode only calls it through its own `--start` flag, never closes it, and never sends F11 to another window if Windows denies focus. (#122)
- Session menu (the controller shortcut, Select + Y in the suggestion) redesigned like Steam's overlay (Shift + Tab): it covers the whole screen, the game dims and blurs behind it, a side panel gathers the options (volume with a gauge, resolution, audio, FPS, HDR) and the **open windows** are always visible in the middle, an in-menu Alt + Tab: the D-pad picks, A brings the window to the front, X closes it and B goes back to the game (switching windows does not end the session). The frosted glass only shows when Windows' transparency effects are on (otherwise the panel is dark and solid, and it changes live if you flip the setting), and in high contrast it uses the system colours. It has a focus that grows, sounds and animations (the panel slides in, the rows follow one after another and closing fades it out). In this alpha it also opens **outside a session**, as a preview with a badge on top, so it can be tried without turning on console mode (in it the FPS limit is hidden and "Back to the PC" becomes "Close menu"). (#120)
- Redesigned Console interface, made to feel like a console: a top bar with three tabs (Home, Session and System) you switch with LB/RB (L1/R1 on PlayStation) with a slide animation, screen tiles that show what will happen to each monitor, quick settings with icons and a focus that grows as you move over it. Home brings the Play now banner and a summary of the current setup (game screen, audio, launcher and HDR/VRR), each opening its choices. (#117)
- Console background: by default, a collage of the covers of your installed Steam games under a dark gradient (the app only reads images Steam already downloaded; nothing is bundled). You can switch to just the gradient or to a picture of your own, in System → Background. (#117)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH

## [Unreleased]
### Novidades
- Menu da sessão: painel fixo no topo da lateral com o **Explorador de arquivos (ControlFS)**, o gerenciador de arquivos de código aberto da equipe, feito para o controle. Um toque abre o ControlFS por cima, sempre em tela cheia na tela da sessão (ou o traz para a frente se já estiver aberto); se não estiver instalado, abre a página de download. O Console Mode só o chama pelo `--start` dele, não o encerra nem envia F11 a outra janela se o Windows negar o foco. (#122)
- Menu da sessão (o atalho do controle, Select + Y na sugestão) redesenhado como o overlay do Steam (Shift+Tab): cobre a tela inteira, o jogo escurece e desfoca por trás, um painel lateral reúne as opções (volume com medidor, resolução, áudio, FPS, HDR) e no meio ficam sempre visíveis as **janelas abertas**, um ALT+TAB interno: o direcional escolhe, A traz a janela para a frente, X a fecha e B volta ao jogo (trocar de janela não encerra a sessão). O vidro fosco só aparece se os efeitos de transparência do Windows estiverem ligados (senão o painel fica escuro e sólido, e muda ao vivo se você trocar a configuração), e em alto contraste usa as cores do sistema. Tem foco que cresce, sons e animações (o painel entra deslizando, as linhas vêm em cascata e ao fechar ele some suavemente). Nesta alpha ele abre também **fora da sessão**, como prévia com um selo no topo, para testar sem ligar o modo console (nela o limite de FPS não aparece e "Voltar ao PC" vira "Fechar menu"). (#120)
- Interface Console redesenhada, com cara de console: barra no topo com três abas (Início, Sessão e Sistema) que você troca com LB/RB (L1/R1 no PlayStation) com uma animação de deslize, blocos das telas que mostram o que vai acontecer com cada monitor, ajustes rápidos com ícones e foco que cresce ao passar. Início traz o banner com Jogar agora e um resumo do setup atual (tela de jogo, áudio, iniciador e HDR/VRR), que abre cada escolha. (#117)
- Plano de fundo do modo Console: por padrão, uma colagem com as capas dos seus jogos instalados na Steam sob um gradiente escuro (o app só lê as imagens que a Steam já baixou; nada é embutido). Dá para trocar por só o gradiente ou por uma imagem sua, em Sistema → Plano de fundo. (#117)
Expand Down
16 changes: 16 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ Pré-condições: sessão ativa com Big Picture (ou jogo borderless) na tela; co
- [ ] Jogo em **tela cheia exclusiva**: o menu não aparece por cima (limitação documentada); Select + Y não quebra nada. Resultado: ______
- [ ] `start consolemode://menu` no cmd abre o menu; fora da sessão, não faz nada. Resultado: ______

## 1d. Explorador de arquivos (ControlFS) no menu da sessão

Pré-condição: ControlFS instalado (https://github.com/nextestudios/ControlFS).

- [ ] Select + Y: no topo da lateral esquerda, fora da lista que rola, há o painel "Explorador de arquivos (ControlFS)"; o restante das opções rola por baixo dele. Resultado: ______
- [ ] Direcional para cima a partir de "Voltar ao jogo" chega nele; A abre o ControlFS em tela cheia na TV, por cima do jogo/Big Picture, e o menu fecha. Resultado: ______
- [ ] O ControlFS abre **sempre em tela cheia** na tela da sessão (a TV), cobrindo-a por inteiro, mesmo que ele estivesse em janela ou maximizado; se já estava em tela cheia, nada muda. Resultado: ______
- [ ] Se outra aplicação permanecer em primeiro plano porque o Windows negou o foco ao ControlFS, o F11 não é enviado a ela; a janela de outra aplicação não muda de estado. Resultado: ______
- [ ] Abrir de novo com o ControlFS já aberto só o traz para a frente (continua um processo só no Gerenciador de Tarefas). Resultado: ______
- [ ] O ControlFS responde ao controle logo de cara (o foco do controle fica com ele, sem o Console Mode reagir aos botões). Resultado: ______
- [ ] Com o ControlFS já aberto: A só o traz para a frente (não abre uma segunda instância). Resultado: ______
- [ ] A sessão **não** termina nem restaura a mesa enquanto o ControlFS está na frente; ao sair dele (Menu do ControlFS → Sair) volta-se ao jogo. Resultado: ______
- [ ] Select + Y com o ControlFS na frente abre o menu por cima dele (saída de emergência). Resultado: ______
- [ ] Sem o ControlFS instalado: a linha diz "Não instalado · aperte para baixar" e A abre a página de releases no navegador. Resultado: ______
- [ ] Fora da sessão (prévia do menu) o painel funciona igual. Resultado: ______

## 2. Links `consolemode://`

- [ ] `start consolemode://start` no `cmd` com o app **fechado**: abre e entra no modo console. Resultado: ______
Expand Down
15 changes: 15 additions & 0 deletions src/ConsoleMode/Native/NativeWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,21 @@ public static WindowMatch[] GetAllVisibleWindows()
return [.. list];
}

/// <summary>A plain F11 to the window in front (ControlFS uses it for full screen).</summary>
public static void SendF11()
{
keybd_event(VK_F11, 0, 0, UIntPtr.Zero);
keybd_event(VK_F11, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}

public static bool TryGetWindowRect(nint hWnd, out int left, out int top, out int width, out int height)
{
left = top = width = height = 0;
if (hWnd == 0 || !GetWindowRect(hWnd, out var r)) return false;
left = r.Left; top = r.Top; width = r.Right - r.Left; height = r.Bottom - r.Top;
return true;
}

public static void SendWinF11()
{
keybd_event(VK_LWIN, 0, 0, UIntPtr.Zero);
Expand Down
4 changes: 4 additions & 0 deletions src/ConsoleMode/Resources/Strings.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
"ConsoleSettingsHint": "Press A on a row to change its value. Everything is saved right away.",
"BackToPc": "Back to the PC",
"CloseMenu": "Close menu",
"ControlFsCard": "File explorer (ControlFS)",
"ControlFsOpen": "Browse your files with the controller",
"ControlFsMissing": "Not installed · press to get it",
"ControlFsMissingNote": "ControlFS is not installed. Opening its download page in the browser.",
"SessionWindowsCard": "Windows",
"SessionWindowsCount": "{0} open",
"SwitcherEmpty": "No open windows",
Expand Down
4 changes: 4 additions & 0 deletions src/ConsoleMode/Resources/Strings.es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
"ConsoleSettingsHint": "Pulsa A en una fila para cambiar su valor. Todo se guarda al momento.",
"BackToPc": "Volver al PC",
"CloseMenu": "Cerrar menú",
"ControlFsCard": "Explorador de archivos (ControlFS)",
"ControlFsOpen": "Explora tus archivos con el mando",
"ControlFsMissing": "No instalado · pulsa para descargarlo",
"ControlFsMissingNote": "ControlFS no está instalado. Abriendo su página de descarga en el navegador.",
"SessionWindowsCard": "Ventanas",
"SessionWindowsCount": "{0} abiertas",
"SwitcherEmpty": "No hay ventanas abiertas",
Expand Down
4 changes: 4 additions & 0 deletions src/ConsoleMode/Resources/Strings.pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
"ConsoleSettingsHint": "Aperte A numa linha para trocar o valor. Tudo é salvo na hora.",
"BackToPc": "Voltar ao PC",
"CloseMenu": "Fechar menu",
"ControlFsCard": "Explorador de arquivos (ControlFS)",
"ControlFsOpen": "Navegue pelos seus arquivos com o controle",
"ControlFsMissing": "Não instalado · aperte para baixar",
"ControlFsMissingNote": "O ControlFS não está instalado. Abrindo a página de download no navegador.",
"SessionWindowsCard": "Janelas",
"SessionWindowsCount": "{0} abertas",
"SwitcherEmpty": "Nenhuma janela aberta",
Expand Down
47 changes: 47 additions & 0 deletions src/ConsoleMode/Services/ControlFsLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace ConsoleMode.Services;

/// <summary>
/// Where ControlFS (the controller-first file explorer from the same team) is installed. Pure: the registry and
/// the disk are passed in, so the order of preference is testable.
/// </summary>
public static class ControlFsLocator
{
public const string ExeName = "ControlFS.exe";
public const string ReleasesUrl = "https://github.com/nextestudios/ControlFS/releases/latest";

/// <summary>Folders to look in, most reliable first, without blanks or repeats.</summary>
public static IReadOnlyList<string> Folders(string? registryInstallLocation, string? localAppData, string? programFiles)
{
var folders = new List<string>();
void Add(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return;
var clean = path.Trim().Trim('"').TrimEnd('\\', '/');
if (clean.Length > 0 && !folders.Contains(clean, StringComparer.OrdinalIgnoreCase)) folders.Add(clean);
}
Add(registryInstallLocation);
if (!string.IsNullOrWhiteSpace(localAppData)) Add(Path.Combine(localAppData, "Programs", "ControlFS"));
if (!string.IsNullOrWhiteSpace(programFiles)) Add(Path.Combine(programFiles, "ControlFS"));
return folders;
}

/// <summary>
/// Full screen means the window is exactly the screen. A maximized window is about 16 px bigger (its frame hangs
/// over the edges), so it is not counted; a pixel of tolerance covers rounding.
/// </summary>
public static bool IsFullScreen(int left, int top, int width, int height, int screenX, int screenY, int screenWidth, int screenHeight) =>
screenWidth > 0 && screenHeight > 0
&& Math.Abs(left - screenX) <= 1 && Math.Abs(top - screenY) <= 1
&& Math.Abs(width - screenWidth) <= 1 && Math.Abs(height - screenHeight) <= 1;

/// <summary>The first folder that really has ControlFS.exe, or null (not installed).</summary>
public static string? Resolve(IEnumerable<string> folders, Func<string, bool> fileExists)
{
foreach (var folder in folders)
{
var exe = Path.Combine(folder, ExeName);
if (fileExists(exe)) return exe;
}
return null;
}
}
161 changes: 161 additions & 0 deletions src/ConsoleMode/Services/ControlFsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Diagnostics;
using ConsoleMode.Models;
using ConsoleMode.Native;
using Microsoft.Win32;

namespace ConsoleMode.Services;

/// <summary>
/// Opens ControlFS from the session menu. ControlFS is a separate app (installed on its own): it is found the way
/// Windows lists it, started with its own <c>--start</c> flag (opens it, or brings the open one to the front; it
/// runs one instance per user) and never killed from here.
/// </summary>
public static class ControlFsService
{
public static string? FindExe()
{
try
{
var folders = ControlFsLocator.Folders(
ReadInstallLocation(),
Environment.GetEnvironmentVariable("LOCALAPPDATA"),
Environment.GetEnvironmentVariable("ProgramFiles"));
return ControlFsLocator.Resolve(folders, File.Exists);
}
catch (Exception ex)
{
AppLog.Write($"ControlFS: procurar: {ex.Message}");
return null;
}
}

/// <summary>Starts (or brings to the front) ControlFS. False when it isn't installed or didn't start.</summary>
public static bool Open()
{
var exe = FindExe();
if (exe is null) return false;
try
{
// Already open: bring that window forward. Launching it again can leave a second copy running
// (seen with the installed build), so the flag is only for when there is no window yet.
var open = FindOpenWindow();
if (open != 0)
{
NativeWindows.ShowWindow(open, 9 /* SW_RESTORE */);
NativeWindows.ForceForeground(open);
AppLog.Write("ControlFS: já estava aberto, trazido para a frente");
return true;
}

var psi = new ProcessStartInfo { FileName = exe, UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(exe)! };
psi.ArgumentList.Add("--start");
Process.Start(psi)?.Dispose();
AppLog.Write("ControlFS: aberto");
return true;
}
catch (Exception ex)
{
AppLog.Write($"ControlFS: abrir: {ex.Message}");
return false;
}
}

/// <summary>
/// ControlFS has no start-up flag for full screen (it keeps the choice in its own settings), so once its window is
/// up it is moved to the session screen and, if it isn't exactly covering it, F11 (its full-screen key) is sent.
/// Maximized is the fallback if F11 didn't take. Runs off the UI thread; never throws.
/// </summary>
public static void EnsureFullScreen(ScreenRect? screen)
{
if (screen is null || screen.Width <= 0 || screen.Height <= 0) return;
try
{
var hwnd = WaitForWindow(TimeSpan.FromSeconds(12));
if (hwnd == 0) { AppLog.Write("ControlFS: janela não apareceu para colocar em tela cheia"); return; }
if (IsCovering(hwnd, screen)) return;

// A restored or maximized window first goes to the session screen, so F11 makes it cover that one.
NativeWindows.ShowWindow(hwnd, 9 /* SW_RESTORE */);
NativeWindows.MoveWindowToRect(hwnd, screen.X + 80, screen.Y + 80, Math.Max(400, screen.Width - 160), Math.Max(300, screen.Height - 160));
Thread.Sleep(300);
if (NativeWindows.GetForegroundWindow() != hwnd) NativeWindows.ForceForeground(hwnd);
Thread.Sleep(200);
if (NativeWindows.GetForegroundWindow() == hwnd)
{
NativeWindows.SendF11();
Thread.Sleep(700);
}
else
{
AppLog.Write("ControlFS: F11 n\u00e3o enviado porque outra janela est\u00e1 em primeiro plano");
}
if (IsCovering(hwnd, screen)) { AppLog.Write("ControlFS: em tela cheia"); return; }

NativeWindows.ShowWindow(hwnd, 3 /* SW_MAXIMIZE */);
AppLog.Write("ControlFS: F11 não surtiu efeito, janela maximizada");
}
catch (Exception ex) { AppLog.Write($"ControlFS: tela cheia: {ex.Message}"); }
}

private static bool IsCovering(nint hwnd, ScreenRect screen) =>
NativeWindows.TryGetWindowRect(hwnd, out var l, out var t, out var w, out var h)
&& ControlFsLocator.IsFullScreen(l, t, w, h, screen.X, screen.Y, screen.Width, screen.Height);

private static nint FindOpenWindow()
{
foreach (var process in Process.GetProcessesByName("ControlFS"))
{
using (process)
{
process.Refresh();
var hwnd = process.MainWindowHandle;
if (hwnd != 0 && NativeWindows.IsWindowStillVisible(hwnd)) return hwnd;
}
}
return 0;
}

private static nint WaitForWindow(TimeSpan timeout)
{
var deadline = Environment.TickCount64 + (long)timeout.TotalMilliseconds;
while (Environment.TickCount64 < deadline)
{
var hwnd = FindOpenWindow();
if (hwnd != 0) { Thread.Sleep(500); return hwnd; }
Thread.Sleep(250);
}
return 0;
}

/// <summary>Not installed: the release page opens in the browser.</summary>
public static void OpenDownloadPage()
{
try { Process.Start(new ProcessStartInfo(ControlFsLocator.ReleasesUrl) { UseShellExecute = true })?.Dispose(); }
catch (Exception ex) { AppLog.Write($"ControlFS: página de download: {ex.Message}"); }
}

private static string? ReadInstallLocation()
{
const string uninstall = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";
foreach (var (hive, view) in new[]
{ (RegistryHive.CurrentUser, RegistryView.Default), (RegistryHive.LocalMachine, RegistryView.Registry64), (RegistryHive.LocalMachine, RegistryView.Registry32) })
{
try
{
using var root = RegistryKey.OpenBaseKey(hive, view).OpenSubKey(uninstall);
if (root is null) continue;
foreach (var name in root.GetSubKeyNames())
{
using var key = root.OpenSubKey(name);
if (key?.GetValue("DisplayName") is string display
&& display.Equals("ControlFS", StringComparison.OrdinalIgnoreCase)
&& key.GetValue("InstallLocation") is string location
&& !string.IsNullOrWhiteSpace(location))
return location;
}
}
catch { /* this hive/view isn't readable: try the next */ }
}
return null;
}
}
3 changes: 3 additions & 0 deletions src/ConsoleMode/Services/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ internal void NotifyAll()
public string SoundsCard => LocalizationService.Get(nameof(SoundsCard));
public string SessionPreviewBadge => LocalizationService.Get(nameof(SessionPreviewBadge));
public string CloseMenu => LocalizationService.Get(nameof(CloseMenu));
public string ControlFsCard => LocalizationService.Get(nameof(ControlFsCard));
public string ControlFsOpen => LocalizationService.Get(nameof(ControlFsOpen));
public string ControlFsMissing => LocalizationService.Get(nameof(ControlFsMissing));
public string SessionWindowsCard => LocalizationService.Get(nameof(SessionWindowsCard));
public string SwitcherEmpty => LocalizationService.Get(nameof(SwitcherEmpty));
public string SwitcherOpen => LocalizationService.Get(nameof(SwitcherOpen));
Expand Down
Loading
Loading