Peek a file using command line and named pipe. (#26422) (#38754)

This is a PoC. It adds the ability to peek a file using a named pipe and
a command line.

Usage/testing before this gets merged and released:

1. Build release configuration of Peek.UI and Peek.CLI.
2. Terminate PowerToys.Peek.UI.exe if running.
3. Back up and replace PowerToys.Peek.UI[.dll;.exe;.pdb;.pri]. Use
[Everything](https://www.voidtools.com/downloads/) to find the source
and destination folders.
4. Call `PowerToys.Peek.CLI.exe <path>` or send the path to peek to the
`PeekPipe` named pipe.

If this solution is OK, documentation and installer need to be updated
and a follow-up issue needs to be filed to support navigation.

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Antonín Procházka
2025-11-05 09:38:07 +01:00
committed by GitHub
parent 20b5ca79a3
commit 31a0deee35
6 changed files with 213 additions and 42 deletions

View File

@@ -62,6 +62,12 @@ namespace Peek.UI
[ObservableProperty]
private IFileSystemItem? _currentItem;
/// <summary>
/// Work around missing navigation when peeking from CLI.
/// TODO: Implement navigation when peeking from CLI.
/// </summary>
private bool _isFromCli;
partial void OnCurrentItemChanged(IFileSystemItem? value)
{
WindowTitle = value != null
@@ -129,7 +135,24 @@ namespace Peek.UI
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
}
public void Initialize(HWND foregroundWindowHandle)
public void Initialize(SelectedItem selectedItem)
{
switch (selectedItem)
{
case SelectedItemByPath selectedItemByPath:
InitializeFromCli(selectedItemByPath.Path);
break;
case SelectedItemByWindowHandle selectedItemByWindowHandle:
InitializeFromExplorer(selectedItemByWindowHandle.WindowHandle);
break;
default:
throw new NotImplementedException($"Invalid type of selected item: '{selectedItem.GetType().FullName}'");
}
}
private void InitializeFromExplorer(HWND foregroundWindowHandle)
{
try
{
@@ -141,10 +164,20 @@ namespace Peek.UI
}
_currentIndex = DisplayIndex = 0;
_isFromCli = false;
CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null;
}
private void InitializeFromCli(string path)
{
// TODO: implement navigation
_isFromCli = true;
Items = null;
_currentIndex = DisplayIndex = 0;
CurrentItem = new FileItem(path, Path.GetFileName(path));
}
public void Uninitialize()
{
_currentIndex = DisplayIndex = 0;
@@ -153,6 +186,7 @@ namespace Peek.UI
Items = null;
_navigationDirection = NavigationDirection.Forwards;
IsErrorVisible = false;
_isFromCli = false;
}
public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards);
@@ -166,6 +200,12 @@ namespace Peek.UI
return;
}
// TODO: implement navigation.
if (_isFromCli)
{
return;
}
if (Items == null || Items.Count == _deletedItemIndexes.Count)
{
_currentIndex = DisplayIndex = 0;

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Peek.UI.Models
{
public abstract class SelectedItem
{
public abstract bool Matches(string? path);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Peek.UI.Models
{
public class SelectedItemByPath : SelectedItem
{
public string Path { get; }
public SelectedItemByPath(string path)
{
Path = path;
}
public override bool Matches(string? path)
{
return string.Equals(Path, path, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Windows.Win32.Foundation;
namespace Peek.UI.Models
{
public class SelectedItemByWindowHandle : SelectedItem
{
public HWND WindowHandle { get; }
public SelectedItemByWindowHandle(HWND windowHandle)
{
WindowHandle = windowHandle;
}
public override bool Matches(string? path)
{
var selectedItems = FileExplorerHelper.GetSelectedItems(WindowHandle);
var selectedItemsCount = selectedItems?.GetCount() ?? 0;
if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1)
{
return false;
}
var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path;
var currentItemPath = path;
return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath;
}
}
}

View File

@@ -3,16 +3,19 @@
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Threading;
using ManagedCommon;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Peek.Common;
using Peek.FilePreviewer;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers;
using Peek.UI.Models;
using Peek.UI.Native;
using Peek.UI.Telemetry.Events;
using Peek.UI.Views;
@@ -23,7 +26,7 @@ namespace Peek.UI
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application, IApp
public partial class App : Application, IApp, IDisposable
{
public static int PowerToysPID { get; set; }
@@ -36,6 +39,10 @@ namespace Peek.UI
private MainWindow? Window { get; set; }
private bool _disposed;
private SelectedItem? _selectedItem;
private bool _launchedFromCli;
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code
@@ -52,10 +59,10 @@ namespace Peek.UI
InitializeComponent();
Logger.InitializeLogger("\\Peek\\Logs");
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureServices((context, services) =>
{
// Core Services
services.AddTransient<NeighboringItemsQuery>();
@@ -66,8 +73,8 @@ namespace Peek.UI
services.AddTransient<TitleBar>();
services.AddTransient<FilePreview>();
services.AddTransient<MainWindowViewModel>();
}).
Build();
})
.Build();
UnhandledException += App_UnhandledException;
}
@@ -99,6 +106,7 @@ namespace Peek.UI
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
// Check if the last argument is a PowerToys Runner PID
if (int.TryParse(cmdArgs[^1], out int powerToysRunnerPid))
{
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
@@ -107,9 +115,25 @@ namespace Peek.UI
Environment.Exit(0);
});
}
else
{
// Command line argument is a file path - activate Peek with that file
string filePath = cmdArgs[^1];
if (File.Exists(filePath) || Directory.Exists(filePath))
{
_selectedItem = new SelectedItemByPath(filePath);
_launchedFromCli = true;
OnShowPeek();
return;
}
else
{
Logger.LogError($"Command line argument is not a valid file or directory: {filePath}");
}
}
}
NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey);
NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnShowPeek);
NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () =>
{
ShellPreviewHandlerPreviewer.ReleaseHandlerFactories();
@@ -126,11 +150,16 @@ namespace Peek.UI
/// <summary>
/// Handle Peek hotkey
/// </summary>
private void OnPeekHotkey()
private void OnShowPeek()
{
// null means explorer, not null means CLI
if (_selectedItem == null)
{
// Need to read the foreground HWND before activating Peek to avoid focus stealing
// Foreground HWND must always be Explorer or Desktop
var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow();
_selectedItem = new SelectedItemByWindowHandle(foregroundWindowHandle);
}
bool firstActivation = false;
@@ -140,7 +169,38 @@ namespace Peek.UI
Window = new MainWindow();
}
Window.Toggle(firstActivation, foregroundWindowHandle);
Window.Toggle(firstActivation, _selectedItem, _launchedFromCli);
_launchedFromCli = false;
_selectedItem = null;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
_disposed = true;
}
}
/* // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~App()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// } */
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -18,6 +18,7 @@ using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Peek.UI.Models;
using Peek.UI.Telemetry.Events;
using Windows.Foundation;
using WinUIEx;
@@ -38,6 +39,7 @@ namespace Peek.UI
/// dialog is open at a time.
/// </summary>
private bool _isDeleteInProgress;
private bool _exitAfterClose;
public MainWindow()
{
@@ -116,12 +118,17 @@ namespace Peek.UI
/// <summary>
/// Toggling the window visibility and querying files when necessary.
/// </summary>
public void Toggle(bool firstActivation, Windows.Win32.Foundation.HWND foregroundWindowHandle)
public void Toggle(bool firstActivation, SelectedItem selectedItem, bool exitAfterClose)
{
if (exitAfterClose)
{
_exitAfterClose = true;
}
if (firstActivation)
{
Activate();
Initialize(foregroundWindowHandle);
Initialize(selectedItem);
return;
}
@@ -132,9 +139,9 @@ namespace Peek.UI
if (AppWindow.IsVisible)
{
if (IsNewSingleSelectedItem(foregroundWindowHandle))
if (IsNewSingleSelectedItem(selectedItem))
{
Initialize(foregroundWindowHandle);
Initialize(selectedItem);
Activate(); // Brings existing window into focus in case it was previously minimized
}
else
@@ -144,7 +151,7 @@ namespace Peek.UI
}
else
{
Initialize(foregroundWindowHandle);
Initialize(selectedItem);
}
}
@@ -182,12 +189,12 @@ namespace Peek.UI
Uninitialize();
}
private void Initialize(Windows.Win32.Foundation.HWND foregroundWindowHandle)
private void Initialize(SelectedItem selectedItem)
{
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
ViewModel.Initialize(foregroundWindowHandle);
ViewModel.Initialize(selectedItem);
ViewModel.ScalingFactor = this.GetMonitorScale();
this.Content.KeyUp += Content_KeyUp;
@@ -207,6 +214,11 @@ namespace Peek.UI
this.Content.KeyUp -= Content_KeyUp;
ShellPreviewHandlerPreviewer.ReleaseHandlerFactories();
if (_exitAfterClose)
{
Environment.Exit(0);
}
}
/// <summary>
@@ -272,20 +284,11 @@ namespace Peek.UI
Uninitialize();
}
private bool IsNewSingleSelectedItem(Windows.Win32.Foundation.HWND foregroundWindowHandle)
private bool IsNewSingleSelectedItem(SelectedItem selectedItem)
{
try
{
var selectedItems = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle);
var selectedItemsCount = selectedItems?.GetCount() ?? 0;
if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1)
{
return false;
}
var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path;
var currentItemPath = ViewModel.CurrentItem?.Path;
return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath;
return selectedItem.Matches(ViewModel.CurrentItem?.Path);
}
catch (Exception ex)
{