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
3 changes: 3 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the release workflow uses the section matching the tag and fails if either is missing.

## [Unreleased]
### What's new
- **Automation and `controlfs://` protocol**: support for `controlfs://start`, `controlfs://stop`, and `controlfs://show` links, as well as command-line arguments (`--start`, `--stop`, `--show`, `--close`), enabling seamless external integration with game launchers and frontends (such as Console Mode), Stream Deck buttons, and automation scripts. Invoking `controlfs://start` when the app is already open restores and brings the window to the foreground without launching duplicate instances; `controlfs://stop` cleanly exits the app. The protocol is registered automatically by the installer and on portable runs.

### Fixes
- **Recognizable icon for .rar files** (#274): with no associated program, Windows showed a blank page. ControlFS now uses the icon Windows registered for .rar (WinRAR's, for example) and, when there is none, its own archive icon (a box with a zipper, drawn in the app, no third-party artwork). It applies to List and Grid at any scale, and the icon refreshes by itself if you install or change the .rar program.

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` **nos dois arquivos**. O workflow de release usa a seção da tag e falha se faltar alguma.

## [Unreleased]
### Novidades
- **Automação e protocolo `controlfs://`**: suporte aos links `controlfs://start`, `controlfs://stop` e `controlfs://show`, bem como argumentos de linha de comando (`--start`, `--stop`, `--show`, `--close`), facilitando a integração externa com launchers e frontends (como o Console Mode), botões de Stream Deck e scripts. Acionar `controlfs://start` quando o app já está aberto restaura e traz a janela para o primeiro plano sem abrir instâncias duplicadas; `controlfs://stop` encerra o app de forma limpa. O protocolo é registrado automaticamente pelo instalador e também na execução portátil.

### Correções
- **Arquivos .rar com ícone reconhecível** (#274): sem programa associado, o Windows mostrava uma página em branco. Agora o ControlFS usa o ícone que o Windows registrou para .rar (o do WinRAR, por exemplo) e, quando não há nenhum, um ícone de arquivo compactado próprio (uma caixa com zíper, desenhado no app, sem arte de terceiros). Vale na lista e na grade, em qualquer escala, e o ícone se atualiza sozinho se você instalar ou trocar o programa de .rar.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Buttons follow **physical position**, so a Nintendo layout doesn't flip confirm
- **Welcome and guided tutorial** on first launch: the real buttons of your controller and an interactive, skippable walkthrough that never touches files (Menu → Ajuda e tutorial)
- **More from the team:** a one-time screen with the team's other apps, NextBoost PRO and Console Mode; it can be opened again from Menu → Ajuda e tutorial
- **Light in the background:** minimized or behind a game, ControlFS drops to low priority and Windows efficiency mode, and gives its memory back
- **Automation:** `controlfs://start`, `controlfs://stop`, `controlfs://show` links and `--start` / `--stop` flags for game launchers (such as Console Mode), Stream Deck buttons, and scripts; single-instance management prevents duplicates and switches cleanly

## Roadmap

Expand Down
1 change: 1 addition & 0 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Os botões seguem a **posição física**, então um controle Nintendo não inve
- **Boas-vindas e tutorial guiado** na primeira vez: os botões de verdade do seu controle e um passo a passo interativo, que dá para pular e nunca mexe em arquivos (Menu → Ajuda e tutorial)
- **Mais da equipe:** uma tela, uma única vez, com os outros apps da equipe, NextBoost PRO e Console Mode; dá para abrir de novo em Menu → Ajuda e tutorial
- **Leve em segundo plano:** minimizado ou atrás de um jogo, o ControlFS baixa a prioridade, entra no modo de eficiência do Windows e devolve memória
- **Automação:** links `controlfs://start`, `controlfs://stop`, `controlfs://show` e opções de linha de comando `--start` / `--stop` para launchers (como o Console Mode), botões do Stream Deck e scripts; instância única evita duplicatas e traz o app para frente

## Roadmap

Expand Down
7 changes: 7 additions & 0 deletions build/ControlFS.iss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ Source: "ControlFS.installed"; DestDir: "{app}"; Flags: ignoreversion
Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExe}"; IconFilename: "{app}\controlfs.ico"
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExe}"; IconFilename: "{app}\controlfs.ico"; Tasks: desktopicon

[Registry]
; Links controlfs://start|stop|show para automação (ProtocolRegistration registra as mesmas chaves ao iniciar).
Root: HKCU; Subkey: "Software\Classes\controlfs"; ValueType: string; ValueData: "URL:ControlFS"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\controlfs"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
Root: HKCU; Subkey: "Software\Classes\controlfs\DefaultIcon"; ValueType: string; ValueData: """{app}\{#AppExe}"",0"
Root: HKCU; Subkey: "Software\Classes\controlfs\shell\open\command"; ValueType: string; ValueData: """{app}\{#AppExe}"" ""%1"""

[Run]
Filename: "{app}\{#AppExe}"; Description: "{cm:LaunchApp}"; Flags: nowait postinstall skipifsilent
; Atualização "instalar e reiniciar" (o app passa /RELAUNCH=1): reabre o app depois da instalação silenciosa.
Expand Down
10 changes: 10 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ The **installed** version updates itself:
Menu → Configurações → **Updates**: check now, automatic check on/off, install on quit on/off, pre-releases (automatic / yes / no).
The **portable** version only tells you a new version exists; download it from the release page.

## Automation

ControlFS can be opened, brought to the front, or closed from external tools, scripts, game launchers/frontends (such as **Console Mode**), and Stream Deck buttons using the `controlfs://` protocol scheme or command-line arguments:

- `controlfs://start` (or `ControlFS.exe --start`): opens the app, or brings the existing window to the foreground if already running.
- `controlfs://show` (or `ControlFS.exe --show`): restores and brings the window to the front.
- `controlfs://stop` (or `ControlFS.exe --stop`, `--close`): cleanly closes the running instance, canceling any active operations and saving settings. If no instance is open, it exits immediately.

The `controlfs://` scheme is registered in Windows automatically by the installer and on startup in portable mode. Only one instance of ControlFS runs per user session; launching a second instance signals the active window and exits immediately.

## Screen readers

With Narrator (or another UI Automation screen reader) on, the app announces where the focus is and the focused item as you move with the controller or keyboard: the home screen, folder, menu, dialog or on-screen keyboard when you enter it, then just the item as you move (name, type, size and position such as "3 of 20"). States are spoken in words: marked, cut, blocked (with the reason), password-protected, unavailable (with the reason). Notices (the pop-up in the bottom-right corner) and the line above the list are read without moving the focus. Nothing depends on sound, vibration or color alone.
Expand Down
10 changes: 10 additions & 0 deletions docs/GUIDE.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ A versão **instalada** se atualiza sozinha:
Menu → Configurações → **Atualizações**: verificar agora, verificação automática sim/não, instalar ao sair sim/não, pré-lançamentos (automático / sim / não).
A versão **portátil** só avisa que existe versão nova; baixe-a na página da release.

## Automação

O ControlFS pode ser aberto, trazido para a frente ou encerrado por ferramentas externas, scripts, frontends de jogos (como o **Console Mode**) e botões do Stream Deck através do protocolo `controlfs://` ou por argumentos de linha de comando:

- `controlfs://start` (ou `ControlFS.exe --start`): abre o aplicativo ou restaura e traz a janela existente para o primeiro plano se já estiver em execução.
- `controlfs://show` (ou `ControlFS.exe --show`): restaura e traz a janela para a frente.
- `controlfs://stop` (ou `ControlFS.exe --stop`, `--close`): encerra a instância aberta de forma limpa, cancelando operações em andamento e salvando preferências. Se nada estiver aberto, encerra imediatamente.

O esquema `controlfs://` é registrado no Windows automaticamente pelo instalador e ao iniciar na versão portátil. Apenas uma instância do ControlFS roda por sessão de usuário; abrir uma segunda chamada sinaliza a janela ativa e encerra o novo processo imediatamente.

## Leitores de tela

Com o Narrador (ou outro leitor de tela com UI Automation) ligado, o app anuncia onde está o foco e o item focado enquanto você anda com o controle ou o teclado: a tela inicial, a pasta, o menu, o diálogo ou o teclado virtual ao entrar, depois só o item a cada movimento (nome, tipo, tamanho e posição, como "3 de 20"). Estados são ditos por extenso: marcado, recortado, bloqueado (com o motivo), com senha, indisponível (com o motivo). Os avisos (no canto inferior direito) e a linha acima da lista são lidos sem mover o foco. Nada depende só de som, vibração ou cor.
Expand Down
8 changes: 8 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ versão, Windows, controle e conexão. Resultados de controles vão para `contro
- [ ] Cursor do teclado virtual visível a 3 m e em 4K; renomear "ControlFS" para "Control-FS" com LT, RB e Sul; LT/RT num nome longo; o Narrador lê a posição do cursor.
- [ ] Seleção no teclado virtual (#44): ao renomear `example-file.zip`, `example-file` aparece destacado e sublinhado, visível a 3 m e em 4K; digitar `novo` resulta em `novo.zip`; `Sel. tudo` na página `…` e Ctrl+A selecionam tudo; o Narrador lê "N de M caracteres selecionados".

## Links `controlfs://` e automação

- [ ] `start controlfs://start` no `cmd` com o app fechado: abre e exibe a janela. Resultado: ______
- [ ] `controlfs://start` com o app aberto: restaura e traz a janela para frente sem abrir uma segunda instância (apenas um `ControlFS.exe` no Gerenciador de Tarefas). Resultado: ______
- [ ] `controlfs://stop` ou `ControlFS.exe --stop` com o app aberto: encerra o app de forma limpa. Resultado: ______
- [ ] `controlfs://show`: traz a janela para frente. Resultado: ______
- [ ] Versão **portátil** movida de pasta: ao abrir, o registro passa a apontar para o novo caminho (log `Protocolo: controlfs:// registrado`). Resultado: ______

## Narrador (#40)

Com o Narrador ligado (Ctrl+Win+Enter), usando só o controle:
Expand Down
4 changes: 4 additions & 0 deletions src/ControlFS.App/App.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ControlFS.App.Diagnostics;
using ControlFS.App.Views;
using ControlFS.Infrastructure.Windows.Shell;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
Expand Down Expand Up @@ -35,6 +36,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
_ = ScreenRenderer.RunAsync(renderTo, Environment.GetCommandLineArgs());
return;
}

ProtocolRegistration.EnsureRegistered(AppLog.Info);

AppLog.Info("OnLaunched: criando janela");
_window = new MainWindow();
AppLog.Info("OnLaunched: ativando janela");
Expand Down
26 changes: 19 additions & 7 deletions src/ControlFS.App/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using ControlFS.App.Diagnostics;
using ControlFS.Infrastructure.Windows.Automation;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;

Expand All @@ -15,16 +17,26 @@ public static void Main()
{
if (e.ExceptionObject is Exception ex) AppLog.Crash(ex, "AppDomain.UnhandledException");
};

var args = Environment.GetCommandLineArgs();
if (SingleInstanceCoordinator.HandleLaunch(args, out var instanceMutex, bypass: ScreenRenderer.OutputDirectory(args) is not null, log: AppLog.Info))
{
return;
}

try
{
WinRT.ComWrappersSupport.InitializeComWrappers();
Microsoft.UI.Xaml.Application.Start(_callbackParams =>
using (instanceMutex)
{
// Continuações assíncronas e o AppController rodam na thread de UI.
SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()));
_ = new App();
});
AppLog.Info("Encerrado normalmente");
WinRT.ComWrappersSupport.InitializeComWrappers();
Microsoft.UI.Xaml.Application.Start(_callbackParams =>
{
// Continuações assíncronas e o AppController rodam na thread de UI.
SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()));
_ = new App();
});
AppLog.Info("Encerrado normalmente");
}
}
catch (Exception ex)
{
Expand Down
45 changes: 43 additions & 2 deletions src/ControlFS.App/Views/MainWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using ControlFS.Infrastructure.Archives;
using ControlFS.Infrastructure.Updates;
using ControlFS.Infrastructure.Windows.FileSystem;
using ControlFS.Infrastructure.Windows.Automation;
using ControlFS.Infrastructure.Windows.Shell;
using ControlFS.Infrastructure.Windows.Settings;
using ControlFS.Core.Text;
Expand All @@ -31,12 +32,14 @@ namespace ControlFS.App.Views;
/// Janela única: cabeçalho (local + estado), lista virtualizada, rodapé de comandos contextuais
/// e camada modal. Toda interação vira InputAction no AppController.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA1001", Justification = "InputHost, o serviço de atualização, o canal do celular, os ícones e o monitor de unidades são descartados no evento Closed da janela.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA1001", Justification = "InputHost, o serviço de atualização, o canal do celular, os ícones, o monitor de unidades e o ouvinte de instância única são descartados no evento Closed da janela.")]
public sealed class MainWindow : Window
{
private readonly AppController _app;
private readonly InputHost _input;
private readonly GitHubReleaseUpdateService? _updates;
/// <summary>Ouvinte de instância única e automação (sinais Show e Close). Null nas capturas.</summary>
private readonly SingleInstanceListener? _singleInstanceListener;

/// <summary>Celular como controle (#223). Só escuta na rede durante uma sessão que o usuário abriu; null nas capturas.</summary>
private readonly Infrastructure.Remote.PhoneLinkServer? _phone;
Expand Down Expand Up @@ -158,7 +161,13 @@ internal MainWindow(string? dataDirectory)
}
var inputStarted = startup.ElapsedMilliseconds;
_input = new InputHost(_app, DispatcherQueue);
if (dataDirectory is null) _background = new BackgroundMode(_app, DispatcherQueue, _drives);
if (dataDirectory is null)
{
_background = new BackgroundMode(_app, DispatcherQueue, _drives);
_singleInstanceListener = new SingleInstanceListener(
() => DispatcherQueue.TryEnqueue(BringToForeground),
() => DispatcherQueue.TryEnqueue(RequestAutomationExit));
}
AppLog.Info($"MainWindow: serviços em {inputStarted} ms; entrada (SDL) em {startup.ElapsedMilliseconds - inputStarted} ms");
_icons = new IconLoader(_iconProvider);
// Caches por tamanho: ícones grandes custam mais por entrada (144 px no dobro da escala ≈ 330 KB), então guardam menos.
Expand Down Expand Up @@ -252,6 +261,7 @@ internal MainWindow(string? dataDirectory)
AppWindow.Changed += (_, _) => UpdateForeground();
Closed += (_, _) =>
{
_singleInstanceListener?.Dispose();
_app.ReleaseMediaForShutdown();
_app.PrepareShutdown(); // instala em silêncio uma atualização verificada, se o usuário deixou ligado
_input.Dispose();
Expand Down Expand Up @@ -1208,10 +1218,41 @@ private void ApplyLayout()
internal void OnTitleBarInsetChanged() =>
_header.Padding = new Thickness(Theme.SpaceL + Theme.SpaceXs, Theme.SpaceM, Theme.SpaceL + _titleBar.ReservedWidth, Theme.SpaceM);

/// <summary>
/// Restaura a janela se estiver minimizada e traz para o primeiro plano (ativação por sinal ou automação externa).
/// </summary>
public void BringToForeground()
{
try
{
if (AppWindow.Presenter is OverlappedPresenter overlapped && overlapped.State == OverlappedPresenterState.Minimized)
{
overlapped.Restore();
}
Activate();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowActivation.BringToForeground(hwnd);
_root.Focus(FocusState.Programmatic);
}
catch (Exception ex)
{
AppLog.Info($"MainWindow.BringToForeground: {ex.Message}");
}
}

/// <summary>
/// Encerramento limpo solicitado por automação (controlfs://stop, --stop, etc.).
/// </summary>
public void RequestAutomationExit()
{
_app.RequestAutomationExit();
}

private static int IndexOf(IReadOnlyList<FileEntry> items, FileEntry entry)
{
for (var i = 0; i < items.Count; i++)
if (ReferenceEquals(items[i], entry) || items[i].Id == entry.Id) return i;
return -1;
}
}

10 changes: 10 additions & 0 deletions src/ControlFS.Application/AppController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,16 @@ private void ShowExitDialog()
PushModal(dialog);
}

/// <summary>
/// Encerramento solicitado por automação externa (protocolo controlfs://stop ou argumento --stop).
/// Cancela operações ativas e fecha a aplicação de maneira limpa.
/// </summary>
public void RequestAutomationExit()
{
foreach (var op in Operations.Items.Where(o => o.IsActive).ToList()) Operations.Cancel(op);
RequestExit();
}

// ---------- Infra interna ----------

internal void Track(Task task)
Expand Down
Loading
Loading