mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
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:
committed by
GitHub
parent
20b5ca79a3
commit
31a0deee35
@@ -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;
|
||||
|
||||
11
src/modules/peek/Peek.UI/Models/SelectedItem.cs
Normal file
11
src/modules/peek/Peek.UI/Models/SelectedItem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs
Normal file
23
src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,22 +59,22 @@ namespace Peek.UI
|
||||
InitializeComponent();
|
||||
Logger.InitializeLogger("\\Peek\\Logs");
|
||||
|
||||
Host = Microsoft.Extensions.Hosting.Host.
|
||||
CreateDefaultBuilder().
|
||||
UseContentRoot(AppContext.BaseDirectory).
|
||||
ConfigureServices((context, services) =>
|
||||
{
|
||||
// Core Services
|
||||
services.AddTransient<NeighboringItemsQuery>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IPreviewSettings, PreviewSettings>();
|
||||
Host = Microsoft.Extensions.Hosting.Host
|
||||
.CreateDefaultBuilder()
|
||||
.UseContentRoot(AppContext.BaseDirectory)
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Core Services
|
||||
services.AddTransient<NeighboringItemsQuery>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IPreviewSettings, PreviewSettings>();
|
||||
|
||||
// Views and ViewModels
|
||||
services.AddTransient<TitleBar>();
|
||||
services.AddTransient<FilePreview>();
|
||||
services.AddTransient<MainWindowViewModel>();
|
||||
}).
|
||||
Build();
|
||||
// Views and ViewModels
|
||||
services.AddTransient<TitleBar>();
|
||||
services.AddTransient<FilePreview>();
|
||||
services.AddTransient<MainWindowViewModel>();
|
||||
})
|
||||
.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()
|
||||
{
|
||||
// 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();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user