Compare commits

..

1 Commits

Author SHA1 Message Date
Shawn Yuan (from Dev Box)
8f87c040b2 fix empty endpoint issue 2025-12-24 15:49:10 +08:00
66 changed files with 214 additions and 4861 deletions

View File

@@ -330,9 +330,6 @@ HHH
riday
YYY
# Unicode
precomposed
# GitHub issue/PR commands
azp
feedbackhub

View File

@@ -131,8 +131,6 @@
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
"PowerToys.ImageResizerExt.dll",
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",

View File

@@ -444,10 +444,6 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.FancyZones_ZoneWindowKeyUp</td>
<td>Occurs when a key is released while interacting with zones.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.FancyZones_CLICommand</td>
<td>Triggered when a FancyZones CLI command is executed, logging the command name and success status.</td>
</tr>
</table>
### FileExplorerAddOns

View File

@@ -459,10 +459,6 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">

View File

@@ -1,93 +0,0 @@
# CLI Conventions
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
## Library
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
```xml
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
```
Add the reference to your project:
```xml
<PackageReference Include="System.CommandLine" />
```
## Option Naming and Definition
- Use `--kebab-case` for long form (e.g., `--shrink-only`).
- Use single `-x` for short form (e.g., `-s`, `-w`).
- Define aliases as static readonly arrays: `["--silent", "-s"]`.
- Create options using `Option<T>` with descriptive help text.
- Add validators for options that require range or format checking.
## RootCommand Setup
- Create a `RootCommand` with a brief description.
- Add all options and arguments to the command.
## Parsing
- Use `Parser(rootCommand).Parse(args)` to parse CLI arguments.
- Extract option values using `parseResult.GetValueForOption()`.
- Note: Use `Parser` directly; `RootCommand.Parse()` may not be available with the pinned System.CommandLine version.
### Parse/Validation Errors
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
## Examples
Reference implementations:
- Awake: `src/modules/Awake/Awake/Program.cs`
- ImageResizer: `src/modules/imageresizer/ui/Cli/`
## Help Output
- Provide a `PrintUsage()` method for custom help formatting if needed.
## Best Practices
1. **Consistency**: Follow existing module patterns.
2. **Documentation**: Always provide help text for each option.
3. **Validation**: Validate input and provide clear error messages.
4. **Atomicity**: Make one logical change per PR; avoid drive-by refactors.
5. **Build/Test Discipline**: Build and test synchronously, one terminal per operation.
6. **Style**: Follow repo analyzers (`.editorconfig`, StyleCop) and formatting rules.
## Logging Requirements
- Use `ManagedCommon.Logger` for consistent logging.
- Initialize logging early in `Main()`.
- Use dual output (console + log file) for errors and warnings to ensure visibility.
- Reference: `src/modules/imageresizer/ui/Cli/CliLogger.cs`
## Error Handling
### Exit Codes
- `0`: Success
- `1`: General error (parsing, validation, runtime)
- `2`: Invalid arguments (optional)
### Exception Handling
- Always wrap `Main()` in try-catch for unhandled exceptions.
- Log exceptions before exiting with non-zero code.
- Display user-friendly error messages to stderr.
- Preserve detailed stack traces in log files only.
## Testing Requirements
- Include tests for argument parsing, validation, and edge cases.
- Place CLI tests in module-specific test projects (e.g., `src/modules/[module]/tests/*CliTests.cs`).
## Signing and Deployment
- CLI executables are signed automatically in CI/CD.
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
- Use self-contained deployment (import `Common.SelfContained.props`).

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +0,0 @@
//==============================================================================
//
// Zoomit
// Sysinternals - www.sysinternals.com
//
// Panoramic screenshot capture and stitching
//
//==============================================================================
#pragma once
#include <windows.h>
#include <vector>
#include <atomic>
#include <memory>
// WIL for unique handles
#include <wil/resource.h>
// Message to signal panorama capture stop (must match ZoomIt.h)
#define WM_USER_PANORAMA_STOP WM_USER+500
// Forward declarations
struct ID3D11Device;
struct ID3D11DeviceContext;
struct ID3D11Texture2D;
//----------------------------------------------------------------------------
// Structure to hold a captured frame with its position
//----------------------------------------------------------------------------
struct PanoramaFrame
{
std::vector<BYTE> pixels; // BGRA pixel data
int width; // Frame width
int height; // Frame height
int relativeX; // X position relative to first frame
int relativeY; // Y position relative to first frame
LONGLONG timestamp; // Capture timestamp
};
//----------------------------------------------------------------------------
// Structure for scroll offset detection result
//----------------------------------------------------------------------------
struct ScrollOffset
{
int dx; // Horizontal scroll amount
int dy; // Vertical scroll amount
double confidence; // Match confidence (0.0 - 1.0)
bool valid; // Whether a valid match was found
};
//----------------------------------------------------------------------------
// PanoramaCapture class - handles continuous capture and stitching
//----------------------------------------------------------------------------
class PanoramaCapture
{
public:
PanoramaCapture();
~PanoramaCapture();
// Start panorama capture mode with a selected rectangle
// Returns true if capture started successfully
bool Start(HWND ownerWindow, const RECT& captureRect);
// Stop capture and return stitched result as HBITMAP
// Caller is responsible for deleting the returned bitmap
HBITMAP Stop();
// Cancel capture without producing result
void Cancel();
// Check if capture is currently active
bool IsCapturing() const { return m_capturing; }
// Get the overlay window handle (for message routing)
HWND GetOverlayWindow() const { return m_overlayWindow.get(); }
// Get current frame count
size_t GetFrameCount() const { return m_frames.size(); }
// Force a frame capture (called by timer or manually)
void CaptureFrame();
private:
// Detect scroll direction and amount between two frames
// Uses normalized cross-correlation for accurate matching
ScrollOffset DetectScrollOffset(
const std::vector<BYTE>& frame1,
const std::vector<BYTE>& frame2,
int width, int height);
// Compute normalized cross-correlation between two image regions
double ComputeNCC(
const BYTE* img1, const BYTE* img2,
int width, int height, int stride,
int img1OffsetX, int img1OffsetY,
int img2OffsetX, int img2OffsetY,
int compareWidth, int compareHeight);
// Check if two frames are significantly different
bool FramesAreDifferent(
const std::vector<BYTE>& frame1,
const std::vector<BYTE>& frame2,
int width, int height);
// Stitch all captured frames into final panorama
HBITMAP StitchFrames();
// Blend a frame onto the canvas - only copy new (non-overlapping) content
void BlendFrameOntoCanvas(
BYTE* canvas, int canvasWidth, int canvasHeight,
const BYTE* frame, int frameWidth, int frameHeight,
int destX, int destY, int previousFrameBottom);
// Capture screen region to byte array
std::vector<BYTE> CaptureScreenRegion(const RECT& rect, int& outWidth, int& outHeight);
// Create and manage the overlay window
bool CreateOverlayWindow(HWND ownerWindow);
static LRESULT CALLBACK OverlayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT HandleOverlayMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
// Timer callback for periodic frame capture
static void CALLBACK TimerCallback(HWND hwnd, UINT msg, UINT_PTR idTimer, DWORD dwTime);
// Keyboard hook for ESC detection
static LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
bool InstallKeyboardHook();
void RemoveKeyboardHook();
private:
// Capture state
std::atomic<bool> m_capturing;
RECT m_captureRect;
HWND m_ownerWindow;
wil::unique_hwnd m_overlayWindow;
// Captured frames
std::vector<PanoramaFrame> m_frames;
std::vector<BYTE> m_previousFrame;
int m_previousWidth;
int m_previousHeight;
// Accumulated scroll offsets
int m_totalOffsetX;
int m_totalOffsetY;
// Timer for periodic capture
UINT_PTR m_timerID;
static const UINT CAPTURE_INTERVAL_MS = 100; // Capture every 100ms
// Detection parameters - reduced for performance
static const int SEARCH_RANGE_Y = 150; // Max vertical search range
static const int SEARCH_RANGE_X = 30; // Max horizontal search range
static const int SEARCH_STEP = 8; // Step size for search (larger = faster)
static const int STRIP_HEIGHT = 40; // Height of comparison strip
static const int MIN_SCROLL_THRESHOLD = 5; // Minimum pixels to consider as scroll
static const double MATCH_THRESHOLD; // NCC threshold for valid match
static const double DIFFERENCE_THRESHOLD; // Threshold for frame difference
// Window class name
static const wchar_t* OVERLAY_CLASS_NAME;
// Instance pointer for static callbacks
static PanoramaCapture* s_instance;
// Keyboard hook handle
HHOOK m_keyboardHook;
};

View File

@@ -66,11 +66,10 @@ type_pEnableThemeDialogTexture pEnableThemeDialogTexture;
#define WM_USER_MAGNIFY_CURSOR WM_USER+108
#define WM_USER_EXIT_MODE WM_USER+109
#define WM_USER_RELOAD_SETTINGS WM_USER+110
#define WM_USER_PANORAMA_STOP WM_USER+500
typedef struct _TYPED_KEY {
RECT rc;
struct _TYPED_KEY *Next;
struct _TYPED_KEY *Next;
} TYPED_KEY, *P_TYPED_KEY;
typedef struct _DRAW_UNDO {
@@ -109,17 +108,17 @@ typedef struct {
typedef BOOL (__stdcall *type_pGetMonitorInfo)(
HMONITOR hMonitor, // handle to display monitor
LPMONITORINFO lpmi // display monitor information
LPMONITORINFO lpmi // display monitor information
);
typedef HMONITOR (__stdcall *type_MonitorFromPoint)(
POINT pt, // point
POINT pt, // point
DWORD dwFlags // determine return value
);
typedef HRESULT (__stdcall *type_pSHAutoComplete)(
HWND hwndEdit,
DWORD dwFlags
DWORD dwFlags
);
// DPI awareness
@@ -152,7 +151,7 @@ typedef BOOL(__stdcall *type_pMagSetWindowFilterList)(
HWND* pHWND
);
typedef BOOL(__stdcall* type_pMagSetLensUseBitmapSmoothing)(
_In_ HWND,
_In_ HWND,
_In_ BOOL
);
typedef BOOL(__stdcall* type_MagSetFullscreenUseBitmapSmoothing)(
@@ -170,7 +169,7 @@ typedef BOOL(__stdcall *type_pGetPointerPenInfo)(
_Out_ POINTER_PEN_INFO *penInfo
);
typedef HRESULT (__stdcall *type_pDwmIsCompositionEnabled)(
typedef HRESULT (__stdcall *type_pDwmIsCompositionEnabled)(
BOOL *pfEnabled
);
@@ -183,7 +182,7 @@ typedef BOOL (__stdcall *type_pSetLayeredWindowAttributes)(
);
// Presentation mode check
typedef HRESULT (__stdcall *type_pSHQueryUserNotificationState)(
typedef HRESULT (__stdcall *type_pSHQueryUserNotificationState)(
QUERY_USER_NOTIFICATION_STATE *pQueryUserNotificationState
);

View File

@@ -241,14 +241,6 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="GifRecordingSession.cpp" />
<ClCompile Include="PanoramaCapture.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
</ClCompile>
<ClCompile Include="pch.cpp" />
<ClCompile Include="SelectRectangle.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
@@ -304,7 +296,6 @@
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />
<ClInclude Include="PanoramaCapture.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Registry.h" />
<ClInclude Include="resource.h" />

View File

@@ -57,9 +57,6 @@
<ClCompile Include="GifRecordingSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PanoramaCapture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Registry.h">
@@ -101,9 +98,6 @@
<ClInclude Include="ZoomItSettings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PanoramaCapture.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GifRecordingSession.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -17,7 +17,6 @@ DWORD g_BreakToggleKey = ((HOTKEYF_CONTROL) << 8)| '3';
DWORD g_DemoTypeToggleKey = ((HOTKEYF_CONTROL) << 8) | '7';
DWORD g_RecordToggleKey = ((HOTKEYF_CONTROL) << 8) | '5';
DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
DWORD g_PanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '9';
DWORD g_ShowExpiredTime = 1;
DWORD g_SliderZoomLevel = 3;
@@ -59,7 +58,6 @@ REG_SETTING RegSettings[] = {
{ L"DrawToggleKey", SETTING_TYPE_DWORD, 0, &g_DrawToggleKey, static_cast<DOUBLE>(g_DrawToggleKey) },
{ L"RecordToggleKey", SETTING_TYPE_DWORD, 0, &g_RecordToggleKey, static_cast<DOUBLE>(g_RecordToggleKey) },
{ L"SnipToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipToggleKey, static_cast<DOUBLE>(g_SnipToggleKey) },
{ L"PanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_PanoramaToggleKey, static_cast<DOUBLE>(g_PanoramaToggleKey) },
{ L"PenColor", SETTING_TYPE_DWORD, 0, &g_PenColor, static_cast<DOUBLE>(g_PenColor) },
{ L"PenWidth", SETTING_TYPE_DWORD, 0, &g_RootPenWidth, static_cast<DOUBLE>(g_RootPenWidth) },
{ L"OptionsShown", SETTING_TYPE_BOOLEAN, 0, &g_OptionsShown, static_cast<DOUBLE>(g_OptionsShown) },

View File

@@ -85,8 +85,6 @@ COLORREF g_CustomColors[16];
#define DEMOTYPE_RESET_HOTKEY 11
#define RECORD_GIF_HOTKEY 12
#define RECORD_GIF_WINDOW_HOTKEY 13
#define PANORAMA_HOTKEY 14
#define PANORAMA_SAVE_HOTKEY 15
#define ZOOM_PAGE 0
#define LIVE_PAGE 1
@@ -150,7 +148,6 @@ DWORD g_BreakToggleMod;
DWORD g_DemoTypeToggleMod;
DWORD g_RecordToggleMod;
DWORD g_SnipToggleMod;
DWORD g_PanoramaToggleMod;
BOOLEAN g_ZoomOnLiveZoom = FALSE;
DWORD g_PenWidth = PEN_WIDTH;
@@ -189,12 +186,6 @@ std::wstring g_RecordingSaveLocationGIF;
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
// Panorama capture globals
BOOL g_PanoramaCapturing = FALSE;
std::unique_ptr<PanoramaCapture> g_PanoramaCapture = nullptr;
BOOL g_PanoramaSaveToFile = FALSE;
type_pGetMonitorInfo pGetMonitorInfo;
type_MonitorFromPoint pMonitorFromPoint;
type_pSHAutoComplete pSHAutoComplete;
@@ -2004,8 +1995,6 @@ void UnregisterAllHotkeys( HWND hWnd )
UnregisterHotKey( hWnd, DEMOTYPE_RESET_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_WINDOW_HOTKEY );
UnregisterHotKey( hWnd, PANORAMA_HOTKEY );
UnregisterHotKey( hWnd, PANORAMA_SAVE_HOTKEY );
}
//----------------------------------------------------------------------------
@@ -2038,16 +2027,6 @@ void RegisterAllHotkeys(HWND hWnd)
// Register CTRL+8 for GIF recording and CTRL+ALT+8 for GIF window recording
RegisterHotKey(hWnd, RECORD_GIF_HOTKEY, MOD_CONTROL | MOD_NOREPEAT, 568 && 0xFF);
RegisterHotKey(hWnd, RECORD_GIF_WINDOW_HOTKEY, MOD_CONTROL | MOD_ALT | MOD_NOREPEAT, 568 && 0xFF);
// Register panorama capture hotkeys
if (g_PanoramaToggleKey) {
OutputDebug( L"Registering PANORAMA_HOTKEY: key=0x%X, mod=0x%X\n", g_PanoramaToggleKey & 0xFF, g_PanoramaToggleMod );
BOOL reg1 = RegisterHotKey(hWnd, PANORAMA_HOTKEY, g_PanoramaToggleMod, g_PanoramaToggleKey & 0xFF);
BOOL reg2 = RegisterHotKey(hWnd, PANORAMA_SAVE_HOTKEY, (g_PanoramaToggleMod ^ MOD_SHIFT), g_PanoramaToggleKey & 0xFF);
OutputDebug( L"PANORAMA_HOTKEY registration result: %d, %d (LastError=%d)\n", reg1, reg2, GetLastError() );
} else {
OutputDebug( L"g_PanoramaToggleKey is 0, not registering panorama hotkey\n" );
}
}
@@ -3512,54 +3491,6 @@ inline auto CopyBytesFromTexture(winrt::com_ptr<ID3D11Texture2D> const& texture,
return bytes;
}
//----------------------------------------------------------------------------
//
// TextureToHBITMAP
//
// Convert ID3D11Texture2D to HBITMAP
//
//----------------------------------------------------------------------------
HBITMAP TextureToHBITMAP(winrt::com_ptr<ID3D11Texture2D> const& texture)
{
if (!texture)
{
return nullptr;
}
try
{
auto bytes = CopyBytesFromTexture(texture);
if (bytes.empty())
{
return nullptr;
}
D3D11_TEXTURE2D_DESC desc;
texture->GetDesc(&desc);
BITMAPINFO bitmapInfo = {};
bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth = desc.Width;
bitmapInfo.bmiHeader.biHeight = -static_cast<LONG>(desc.Height); // Top-down
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HBITMAP hBitmap = CreateDIBSection(nullptr, &bitmapInfo, DIB_RGB_COLORS, &bits, nullptr, 0);
if (hBitmap && bits)
{
CopyMemory(bits, bytes.data(), bytes.size());
}
return hBitmap;
}
catch (...)
{
return nullptr;
}
}
//----------------------------------------------------------------------------
//
@@ -3748,7 +3679,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
static std::filesystem::path lastSaveFolder;
wil::unique_cotaskmem_string chosenFolderPath;
wil::com_ptr<IShellItem> currentSelectedFolder;
bool bFolderChanged = false;
bool bFolderChanged = false;
if (SUCCEEDED(saveDialog->GetFolder(currentSelectedFolder.put())))
{
if (SUCCEEDED(currentSelectedFolder->GetDisplayName(SIGDN_FILESYSPATH, chosenFolderPath.put())))
@@ -4178,8 +4109,6 @@ LRESULT APIENTRY MainWndProc(
g_DemoTypeToggleMod = GetKeyMod( g_DemoTypeToggleKey );
g_SnipToggleMod = GetKeyMod( g_SnipToggleKey );
g_RecordToggleMod = GetKeyMod( g_RecordToggleKey );
g_PanoramaToggleMod = GetKeyMod( g_PanoramaToggleKey );
OutputDebug( L"Panorama hotkey settings: g_PanoramaToggleKey=0x%X, g_PanoramaToggleMod=0x%X\n", g_PanoramaToggleKey, g_PanoramaToggleMod );
if( !g_OptionsShown && !g_StartedByPowerToys ) {
// First run should show options when running as standalone. If not running as standalone,
@@ -4245,19 +4174,6 @@ LRESULT APIENTRY MainWndProc(
APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
// Register panorama hotkey at startup
if (g_PanoramaToggleKey) {
OutputDebug( L"Startup: Registering PANORAMA_HOTKEY: key=0x%X, mod=0x%X\n", g_PanoramaToggleKey & 0xFF, g_PanoramaToggleMod );
if (!RegisterHotKey(hWnd, PANORAMA_HOTKEY, g_PanoramaToggleMod, g_PanoramaToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, PANORAMA_SAVE_HOTKEY, (g_PanoramaToggleMod ^ MOD_SHIFT), g_PanoramaToggleKey & 0xFF)) {
OutputDebug( L"Startup: PANORAMA_HOTKEY registration FAILED, LastError=%d\n", GetLastError() );
MessageBox(hWnd, L"The specified panorama hotkey is already in use.\nSelect a different panorama hotkey.",
APPNAME, MB_ICONERROR);
showOptions = TRUE;
} else {
OutputDebug( L"Startup: PANORAMA_HOTKEY registration SUCCESS\n" );
}
}
if( showOptions ) {
SendMessage( hWnd, WM_COMMAND, IDC_OPTIONS, 0 );
@@ -4272,7 +4188,6 @@ LRESULT APIENTRY MainWndProc(
return 0;
case WM_HOTKEY:
OutputDebug( L"WM_HOTKEY received: wParam=%d, lParam=0x%X\n", (int)wParam, (int)lParam );
if( g_RecordCropping == TRUE )
{
if( wParam != RECORD_CROP_HOTKEY )
@@ -4463,120 +4378,6 @@ LRESULT APIENTRY MainWndProc(
break;
}
case PANORAMA_SAVE_HOTKEY:
case PANORAMA_HOTKEY:
{
// Handle panorama capture hotkey
OutputDebug( L"PANORAMA_HOTKEY received, capturing=%d\n", g_PanoramaCapturing );
if( g_PanoramaCapturing )
{
// User pressed hotkey again or ESC - stop and process result
if( g_PanoramaCapture )
{
HBITMAP hResult = g_PanoramaCapture->Stop();
if( hResult )
{
if( g_PanoramaSaveToFile )
{
// Save to file - show Save As dialog
OPENFILENAME openFileName;
TCHAR filePath[MAX_PATH] = L"Panorama.png";
memset( &openFileName, 0, sizeof(openFileName) );
openFileName.lStructSize = OPENFILENAME_SIZE_VERSION_400;
openFileName.hwndOwner = hWnd;
openFileName.hInstance = static_cast<HINSTANCE>(g_hInstance);
openFileName.nMaxFile = sizeof(filePath)/sizeof(filePath[0]);
openFileName.Flags = OFN_LONGNAMES | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
openFileName.lpstrTitle = L"Save panorama screenshot...";
openFileName.lpstrDefExt = L"png";
openFileName.nFilterIndex = 1;
openFileName.lpstrFilter = L"PNG Image\0*.png\0\0";
openFileName.lpstrFile = filePath;
if( GetSaveFileName( &openFileName ) )
{
TCHAR targetFilePath[MAX_PATH];
_tcscpy( targetFilePath, filePath );
if( !_tcsrchr( targetFilePath, '.' ) )
{
_tcscat( targetFilePath, L".png" );
}
SavePng( targetFilePath, hResult );
}
}
else
{
// Copy to clipboard
if( OpenClipboard( hWnd ) )
{
EmptyClipboard();
SetClipboardData( CF_BITMAP, hResult );
CloseClipboard();
// Don't delete hResult - clipboard owns it now
hResult = nullptr;
}
}
if( hResult )
{
DeleteObject( hResult );
}
}
g_PanoramaCapture.reset();
}
g_PanoramaCapturing = FALSE;
g_PanoramaSaveToFile = FALSE;
}
else
{
// Block if LiveZoom/LiveDraw is active due to mirroring bug
if( IsWindowVisible( g_hWndLiveZoom )
&& ( GetWindowLongPtr( hWnd, GWL_EXSTYLE ) & WS_EX_LAYERED ) )
{
break;
}
// Start panorama capture
g_PanoramaSaveToFile = ( LOWORD( wParam ) == PANORAMA_SAVE_HOTKEY );
// Let user select the capture rectangle
SelectRectangle selectRectangle;
if( !selectRectangle.Start( hWnd ) )
{
break;
}
RECT captureRect = selectRectangle.SelectedRect();
selectRectangle.Stop();
// Validate rectangle size
if( (captureRect.right - captureRect.left) < 50 ||
(captureRect.bottom - captureRect.top) < 50 )
{
MessageBox( hWnd, L"Selected region is too small for panorama capture.", APPNAME, MB_OK | MB_ICONWARNING );
break;
}
// Create and start panorama capture
g_PanoramaCapture = std::make_unique<PanoramaCapture>();
if( g_PanoramaCapture->Start( hWnd, captureRect ) )
{
g_PanoramaCapturing = TRUE;
OutputDebug( L"Panorama capture started\n" );
}
else
{
g_PanoramaCapture.reset();
MessageBox( hWnd, L"Failed to start panorama capture.", APPNAME, MB_OK | MB_ICONERROR );
}
}
break;
}
case BREAK_HOTKEY:
//
// Go to break timer
@@ -5493,14 +5294,6 @@ LRESULT APIENTRY MainWndProc(
case WM_KEYDOWN:
// Handle ESC during panorama capture
if( g_PanoramaCapturing && wParam == VK_ESCAPE )
{
// Stop panorama capture and process result
PostMessage( hWnd, WM_HOTKEY, PANORAMA_HOTKEY, 0 );
return TRUE;
}
if( (g_TypeMode != TypeModeOff) && g_HaveTyped && static_cast<char>(wParam) != VK_UP && static_cast<char>(wParam) != VK_DOWN &&
(isprint( static_cast<char>(wParam)) ||
wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK )) {
@@ -6591,11 +6384,11 @@ LRESULT APIENTRY MainWndProc(
{
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
reg.ReadRegSettings(RegSettings);
if (g_RecordingFormat == RecordingFormat::GIF)
{
g_RecordScaling = g_RecordScalingGIF;
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
}
else
{
@@ -6621,7 +6414,6 @@ LRESULT APIENTRY MainWndProc(
g_DemoTypeToggleMod = GetKeyMod(g_DemoTypeToggleKey);
g_SnipToggleMod = GetKeyMod(g_SnipToggleKey);
g_RecordToggleMod = GetKeyMod(g_RecordToggleKey);
g_PanoramaToggleMod = GetKeyMod(g_PanoramaToggleKey);
BOOL showOptions = FALSE;
if (g_ToggleKey)
{
@@ -6691,16 +6483,6 @@ LRESULT APIENTRY MainWndProc(
MessageBox(hWnd, L"The specified GIF recording hotkey is already in use.\nSelect a different GIF recording hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
// Register panorama hotkeys
if (g_PanoramaToggleKey)
{
if (!RegisterHotKey(hWnd, PANORAMA_HOTKEY, g_PanoramaToggleMod, g_PanoramaToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, PANORAMA_SAVE_HOTKEY, (g_PanoramaToggleMod ^ MOD_SHIFT), g_PanoramaToggleKey & 0xFF))
{
MessageBox(hWnd, L"The specified panorama hotkey is already in use.\nSelect a different panorama hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
if (showOptions)
{
// To open the PowerToys settings in the ZoomIt page.
@@ -6708,70 +6490,6 @@ LRESULT APIENTRY MainWndProc(
}
break;
}
case WM_USER_PANORAMA_STOP:
{
OutputDebug( L"WM_USER_PANORAMA_STOP received\n" );
if( g_PanoramaCapturing && g_PanoramaCapture )
{
HBITMAP hResult = g_PanoramaCapture->Stop();
if( hResult )
{
if( g_PanoramaSaveToFile )
{
// Save to file - show Save As dialog
OPENFILENAME openFileName;
TCHAR filePath[MAX_PATH] = L"Panorama.png";
memset( &openFileName, 0, sizeof(openFileName) );
openFileName.lStructSize = OPENFILENAME_SIZE_VERSION_400;
openFileName.hwndOwner = hWnd;
openFileName.hInstance = static_cast<HINSTANCE>(g_hInstance);
openFileName.nMaxFile = sizeof(filePath)/sizeof(filePath[0]);
openFileName.Flags = OFN_LONGNAMES | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
openFileName.lpstrTitle = L"Save panorama screenshot...";
openFileName.lpstrDefExt = L"png";
openFileName.nFilterIndex = 1;
openFileName.lpstrFilter = L"PNG Image\0*.png\0\0";
openFileName.lpstrFile = filePath;
if( GetSaveFileName( &openFileName ) )
{
TCHAR targetFilePath[MAX_PATH];
_tcscpy( targetFilePath, filePath );
if( !_tcsrchr( targetFilePath, '.' ) )
{
_tcscat( targetFilePath, L".png" );
}
SavePng( targetFilePath, hResult );
}
DeleteObject( hResult );
}
else
{
// Copy to clipboard
if( OpenClipboard( hWnd ) )
{
EmptyClipboard();
SetClipboardData( CF_BITMAP, hResult );
CloseClipboard();
// Don't delete hResult - clipboard owns it now
}
else
{
DeleteObject( hResult );
}
}
}
g_PanoramaCapture.reset();
g_PanoramaCapturing = FALSE;
g_PanoramaSaveToFile = FALSE;
}
break;
}
case WM_COMMAND:
switch(LOWORD( wParam )) {
@@ -7363,14 +7081,6 @@ LRESULT APIENTRY MainWndProc(
case WM_DESTROY:
// Clean up panorama capture if active
if( g_PanoramaCapturing && g_PanoramaCapture )
{
g_PanoramaCapture->Cancel();
g_PanoramaCapture.reset();
g_PanoramaCapturing = FALSE;
}
PostQuitMessage( 0 );
break;

View File

@@ -60,7 +60,6 @@
#include "SelectRectangle.h"
#include "DemoType.h"
#include "versionhelper.h"
#include "PanoramaCapture.h"
// WIL
#include <wil/com.h>
@@ -84,9 +83,6 @@
#include <regex>
#include <fstream>
#include <sstream>
#include <thread>
#include <mutex>
#include <chrono>
// robmikh.common
#include <robmikh.common/composition.interop.h>

View File

@@ -104,16 +104,13 @@
#define IDC_COPY_CROP 40008
#define IDC_SAVE_CROP 40009
#define IDC_DEMOTYPE_HOTKEY 40011
#define IDC_PANORAMA_HOTKEY 40013
#define IDC_PANORAMA_CROP 40014
#define IDC_PANORAMA_SAVE 40015
// Next default values for new objects
//
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 118
#define _APS_NEXT_COMMAND_VALUE 40016
#define _APS_NEXT_COMMAND_VALUE 40013
#define _APS_NEXT_CONTROL_VALUE 1078
#define _APS_NEXT_SYMED_VALUE 101
#endif

View File

@@ -28,13 +28,6 @@ namespace Awake.Core.Native
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern void FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle);

View File

@@ -49,8 +49,5 @@ namespace Awake.Core.Native
// Menu Item Info Flags
internal const uint MNS_AUTO_DISMISS = 0x10000000;
internal const uint MIM_STYLE = 0x00000010;
// Attach Console
internal const int ATTACH_PARENT_PROCESS = -1;
}
}

View File

@@ -51,24 +51,6 @@ namespace Awake
private static async Task<int> Main(string[] args)
{
var rootCommand = BuildRootCommand();
Bridge.AttachConsole(Core.Native.Constants.ATTACH_PARENT_PROCESS);
var parseResult = rootCommand.Parse(args);
if (parseResult.Tokens.Any(t => t.Value.ToLowerInvariant() is "--help" or "-h" or "-?"))
{
// Print help and exit.
return rootCommand.Invoke(args);
}
if (parseResult.Errors.Count > 0)
{
// Shows errors and returns non-zero.
return rootCommand.Invoke(args);
}
_settingsUtils = SettingsUtils.Default;
LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
@@ -125,97 +107,116 @@ namespace Awake
Bridge.GetPwrCapabilities(out _powerCapabilities);
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
return await rootCommand.InvokeAsync(args);
Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION)
{
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<string> expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
pidOption.AddValidator(result =>
{
if (result.Tokens.Count == 0)
{
return;
}
string tokenValue = result.Tokens[0].Value;
if (!int.TryParse(tokenValue, out int parsed))
{
string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {tokenValue}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
return;
}
if (parsed <= 0)
{
string errorMessage = $"PID value in --pid must be a positive integer. Value used: {parsed}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
return;
}
// Process existence check. (We also re-validate just before binding.)
if (!ProcessExists(parsed))
{
string errorMessage = $"No running process found with an ID of {parsed}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
expireAtOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
parentPidOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
return rootCommand.InvokeAsync(args).Result;
}
}
}
private static RootCommand BuildRootCommand()
{
Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION)
{
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<string> expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
pidOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !int.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
expireAtOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _))
{
string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
parentPidOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
return rootCommand;
}
private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
@@ -263,7 +264,6 @@ namespace Awake
if (pid == 0 && !useParentPid)
{
Logger.LogInfo("No PID specified. Allocating console...");
Bridge.FreeConsole();
AllocateLocalConsole();
}
else

View File

@@ -8,8 +8,6 @@ using System.CommandLine.Invocation;
using FancyZonesCLI;
using FancyZonesCLI.CommandLine;
using FancyZonesCLI.Telemetry;
using Microsoft.PowerToys.Telemetry;
namespace FancyZonesCLI.CommandLine.Commands;
@@ -26,14 +24,12 @@ internal abstract class FancyZonesBaseCommand : Command
private void InvokeInternal(InvocationContext context)
{
Logger.LogInfo($"Executing command '{Name}'");
bool successful = false;
if (!FancyZonesCliGuards.IsFancyZonesRunning())
{
Logger.LogWarning($"Command '{Name}' blocked: FancyZones is not running");
context.Console.Error.Write($"{Properties.Resources.error_fancyzones_not_running}{Environment.NewLine}");
context.Console.Error.Write($"Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.{Environment.NewLine}");
context.ExitCode = 1;
LogTelemetry(successful: false);
return;
}
@@ -41,7 +37,6 @@ internal abstract class FancyZonesBaseCommand : Command
{
string output = Execute(context);
context.ExitCode = 0;
successful = true;
Logger.LogInfo($"Command '{Name}' completed successfully");
Logger.LogDebug($"Command '{Name}' output length: {output?.Length ?? 0}");
@@ -57,28 +52,6 @@ internal abstract class FancyZonesBaseCommand : Command
Logger.LogError($"Command '{Name}' failed", ex);
context.Console.Error.Write($"Error: {ex.Message}{Environment.NewLine}");
context.ExitCode = 1;
successful = false;
}
finally
{
LogTelemetry(successful);
}
}
private void LogTelemetry(bool successful)
{
try
{
PowerToysTelemetry.Log.WriteEvent(new FancyZonesCLICommandEvent
{
CommandName = Name,
Successful = successful,
});
}
catch (Exception ex)
{
// Don't fail the command if telemetry logging fails
Logger.LogError($"Failed to log telemetry for command '{Name}'", ex);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
{
public GetActiveLayoutCommand()
: base("get-active-layout", Properties.Resources.cmd_get_active_layout)
: base("get-active-layout", "Show currently active layout")
{
AddAlias("active");
}
@@ -28,7 +28,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
{
throw new InvalidOperationException(Properties.Resources.get_active_layout_no_monitor_info);
throw new InvalidOperationException("Could not get current monitor information.");
}
// Read applied layouts.
@@ -36,11 +36,11 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
if (appliedLayouts.AppliedLayouts == null)
{
return Properties.Resources.get_active_layout_no_layouts;
return "No layouts configured.";
}
var sb = new System.Text.StringBuilder();
sb.AppendLine($"\n{Properties.Resources.get_active_layout_header}\n");
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
// Show only layouts for currently connected monitors.
for (int i = 0; i < editorParams.Monitors.Count; i++)
@@ -71,7 +71,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
}
else
{
sb.AppendLine(Properties.Resources.get_active_layout_no_layout);
sb.AppendLine(" No layout applied");
}
if (i < editorParams.Monitors.Count - 1)

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
{
public GetHotkeysCommand()
: base("get-hotkeys", Properties.Resources.cmd_get_hotkeys)
: base("get-hotkeys", "List all layout hotkeys")
{
AddAlias("hk");
}
@@ -26,12 +26,12 @@ internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
if (hotkeys.LayoutHotkeys == null || hotkeys.LayoutHotkeys.Count == 0)
{
return Properties.Resources.get_hotkeys_no_hotkeys;
return "No hotkeys configured.";
}
var sb = new System.Text.StringBuilder();
sb.AppendLine($"{Properties.Resources.get_hotkeys_header}\n");
sb.AppendLine($"{Properties.Resources.get_hotkeys_instruction}\n");
sb.AppendLine("=== Layout Hotkeys ===\n");
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
foreach (var hotkey in hotkeys.LayoutHotkeys.OrderBy(h => h.Key))
{

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
{
public GetLayoutsCommand()
: base("get-layouts", Properties.Resources.cmd_get_layouts)
: base("get-layouts", "List available layouts")
{
AddAlias("ls");
}
@@ -61,7 +61,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
if (customLayouts.CustomLayouts != null)
{
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_layouts_custom_header, customLayouts.CustomLayouts.Count));
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.CustomLayouts.Count} total) ===");
for (int i = 0; i < customLayouts.CustomLayouts.Count; i++)
{
@@ -92,8 +92,8 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
// Add note for canvas layouts.
if (isCanvasLayout)
{
sb.AppendLine($"\n {Properties.Resources.get_layouts_canvas_note}");
sb.AppendLine($" {Properties.Resources.get_layouts_canvas_detail}");
sb.AppendLine("\n Note: Canvas layout preview is approximate.");
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
}
if (i < customLayouts.CustomLayouts.Count - 1)
@@ -102,7 +102,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
}
}
sb.AppendLine($"\n{Properties.Resources.get_layouts_usage}");
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
}
return sb.ToString().TrimEnd();

View File

@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
{
public GetMonitorsCommand()
: base("get-monitors", Properties.Resources.cmd_get_monitors)
: base("get-monitors", "List monitors and FancyZones metadata")
{
AddAlias("m");
}
@@ -31,19 +31,19 @@ internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
}
catch (Exception ex)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_error, ex.Message), ex);
throw new InvalidOperationException($"Failed to read monitor information. {ex.Message}{Environment.NewLine}Note: Ensure FancyZones is running to get current monitor information.", ex);
}
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
{
return Properties.Resources.get_monitors_no_monitors;
return "No monitors found.";
}
// Also read applied layouts to show which layout is active on each monitor.
var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts();
var sb = new System.Text.StringBuilder();
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_header, editorParams.Monitors.Count));
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({editorParams.Monitors.Count} total) ===");
sb.AppendLine();
for (int i = 0; i < editorParams.Monitors.Count; i++)

View File

@@ -5,7 +5,6 @@
using System;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -14,7 +13,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
{
public OpenEditorCommand()
: base("open-editor", Properties.Resources.cmd_open_editor)
: base("open-editor", "Launch FancyZones layout editor")
{
AddAlias("e");
}
@@ -39,7 +38,7 @@ internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
}
catch (Exception ex)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_editor_error, ex.Message), ex);
throw new InvalidOperationException($"Failed to request FancyZones Editor launch. {ex.Message}", ex);
}
}
}

View File

@@ -5,7 +5,6 @@
using System;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Globalization;
using System.IO;
namespace FancyZonesCLI.CommandLine.Commands;
@@ -13,7 +12,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
{
public OpenSettingsCommand()
: base("open-settings", Properties.Resources.cmd_open_settings)
: base("open-settings", "Open FancyZones settings page")
{
AddAlias("settings");
}
@@ -38,14 +37,14 @@ internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
if (process == null)
{
throw new InvalidOperationException(Properties.Resources.open_settings_error_not_started);
throw new InvalidOperationException("PowerToys.exe failed to start.");
}
return string.Empty;
}
catch (Exception ex)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_settings_error, ex.Message), ex);
throw new InvalidOperationException($"Failed to open FancyZones Settings. {ex.Message}", ex);
}
}
}

View File

@@ -5,7 +5,6 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using FancyZonesEditorCommon.Data;
using FancyZonesEditorCommon.Utils;
@@ -17,11 +16,11 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
private readonly Argument<int> _key;
public RemoveHotkeyCommand()
: base("remove-hotkey", Properties.Resources.cmd_remove_hotkey)
: base("remove-hotkey", "Remove hotkey assignment")
{
AddAlias("rhk");
_key = new Argument<int>("key", Properties.Resources.remove_hotkey_arg_key);
_key = new Argument<int>("key", "Hotkey index (0-9)");
AddArgument(_key);
}
@@ -34,14 +33,14 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
if (hotkeysWrapper.LayoutHotkeys == null)
{
return Properties.Resources.remove_hotkey_no_hotkeys;
return "No hotkeys configured.";
}
var hotkeysList = hotkeysWrapper.LayoutHotkeys;
var removed = hotkeysList.RemoveAll(h => h.Key == key);
if (removed == 0)
{
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.remove_hotkey_not_found, key);
return $"No hotkey assigned to key {key}";
}
// Save.

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using System.Linq;
using FancyZonesEditorCommon.Data;
@@ -20,12 +19,12 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
private readonly Argument<string> _layout;
public SetHotkeyCommand()
: base("set-hotkey", Properties.Resources.cmd_set_hotkey)
: base("set-hotkey", "Assign hotkey (0-9) to a custom layout")
{
AddAlias("shk");
_key = new Argument<int>("key", Properties.Resources.set_hotkey_arg_key);
_layout = new Argument<string>("layout", Properties.Resources.set_hotkey_arg_layout);
_key = new Argument<int>("key", "Hotkey index (0-9)");
_layout = new Argument<string>("layout", "Custom layout UUID");
AddArgument(_key);
AddArgument(_layout);
@@ -39,7 +38,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
if (key < 0 || key > 9)
{
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
throw new InvalidOperationException("Key must be between 0 and 9.");
}
// Editor only allows assigning hotkeys to existing custom layouts.
@@ -60,7 +59,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
if (!matchedLayout.HasValue)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
throw new InvalidOperationException($"Layout '{layout}' is not a custom layout UUID.");
}
string layoutName = matchedLayout.Value.Name;

View File

@@ -26,14 +26,14 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
private readonly Option<bool> _all;
public SetLayoutCommand()
: base("set-layout", Properties.Resources.cmd_set_layout)
: base("set-layout", "Set layout by UUID or template name")
{
AddAlias("s");
_layoutId = new Argument<string>("layout", Properties.Resources.set_layout_arg_layout);
_layoutId = new Argument<string>("layout", "Layout UUID or template type (e.g. focus, columns)");
AddArgument(_layoutId);
_monitor = new Option<int?>(AliasesMonitor, Properties.Resources.set_layout_opt_monitor);
_monitor = new Option<int?>(AliasesMonitor, "Apply to monitor N (1-based)");
_monitor.AddValidator(result =>
{
if (result.Tokens.Count == 0)
@@ -44,11 +44,11 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
int? monitor = result.GetValueOrDefault<int?>();
if (monitor.HasValue && monitor.Value < 1)
{
result.ErrorMessage = Properties.Resources.set_layout_error_monitor_index;
result.ErrorMessage = "Monitor index must be >= 1.";
}
});
_all = new Option<bool>(AliasesAll, Properties.Resources.set_layout_opt_all);
_all = new Option<bool>(AliasesAll, "Apply to all monitors");
AddOption(_monitor);
AddOption(_all);
@@ -60,7 +60,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (monitor.HasValue && all)
{
commandResult.ErrorMessage = Properties.Resources.set_layout_error_both_options;
commandResult.ErrorMessage = "Cannot specify both --monitor and --all.";
}
});
}
@@ -97,15 +97,15 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
{
if (all)
{
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_all, layout);
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to all monitors.", layout);
}
if (monitor.HasValue)
{
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_monitor, layout, monitor.Value);
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor {1}.", layout, monitor.Value);
}
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_default, layout);
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor 1.", layout);
}
private static (CustomLayouts.CustomLayoutWrapper? TargetCustomLayout, LayoutTemplates.TemplateLayoutWrapper? TargetTemplate) ResolveTargetLayout(string layout)
@@ -127,7 +127,10 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (!targetCustomLayout.HasValue && !targetTemplate.HasValue)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_not_found, layout));
throw new InvalidOperationException(
$"Layout '{layout}' not found{Environment.NewLine}" +
"Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')" +
$"{Environment.NewLine} For custom layouts, use the UUID from 'get-layouts'");
}
return (targetCustomLayout, targetTemplate);
@@ -194,7 +197,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
int monitorIndex = monitor.Value - 1; // Convert to 0-based.
if (monitorIndex < 0 || monitorIndex >= editorParams.Monitors.Count)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_monitor_not_found, monitor.Value, editorParams.Monitors.Count));
throw new InvalidOperationException($"Monitor {monitor.Value} not found. Available monitors: 1-{editorParams.Monitors.Count}");
}
result.Add(monitorIndex);
@@ -247,7 +250,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
if (newLayouts.Count == 0)
{
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_monitors);
throw new InvalidOperationException("Internal error - no monitors to update.");
}
return newLayouts;
@@ -303,7 +306,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
}
else
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_unsupported_type, targetCustomLayout.Value.Type));
throw new InvalidOperationException($"Unsupported custom layout type '{targetCustomLayout.Value.Type}'.");
}
return (
@@ -326,7 +329,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
targetTemplate.Value.SensitivityRadius);
}
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_layout);
throw new InvalidOperationException("Internal error - no layout selected.");
}
private static AppliedLayouts.AppliedLayoutsListWrapper MergeWithHistoricalLayouts(

View File

@@ -13,7 +13,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AssemblyName>FancyZonesCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852;CA1863;CA1305</NoWarn>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -24,22 +24,6 @@
<ItemGroup>
<ProjectReference Include="..\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->

View File

@@ -1,353 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FancyZonesCLI.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FancyZonesCLI.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
internal static string error_fancyzones_not_running {
get {
return ResourceManager.GetString("error_fancyzones_not_running", resourceCulture);
}
}
internal static string cmd_get_active_layout {
get {
return ResourceManager.GetString("cmd_get_active_layout", resourceCulture);
}
}
internal static string get_active_layout_no_monitor_info {
get {
return ResourceManager.GetString("get_active_layout_no_monitor_info", resourceCulture);
}
}
internal static string get_active_layout_no_layouts {
get {
return ResourceManager.GetString("get_active_layout_no_layouts", resourceCulture);
}
}
internal static string get_active_layout_header {
get {
return ResourceManager.GetString("get_active_layout_header", resourceCulture);
}
}
internal static string get_active_layout_no_layout {
get {
return ResourceManager.GetString("get_active_layout_no_layout", resourceCulture);
}
}
internal static string cmd_get_layouts {
get {
return ResourceManager.GetString("cmd_get_layouts", resourceCulture);
}
}
internal static string get_layouts_templates_header {
get {
return ResourceManager.GetString("get_layouts_templates_header", resourceCulture);
}
}
internal static string get_layouts_custom_header {
get {
return ResourceManager.GetString("get_layouts_custom_header", resourceCulture);
}
}
internal static string get_layouts_canvas_note {
get {
return ResourceManager.GetString("get_layouts_canvas_note", resourceCulture);
}
}
internal static string get_layouts_canvas_detail {
get {
return ResourceManager.GetString("get_layouts_canvas_detail", resourceCulture);
}
}
internal static string get_layouts_usage {
get {
return ResourceManager.GetString("get_layouts_usage", resourceCulture);
}
}
internal static string cmd_get_monitors {
get {
return ResourceManager.GetString("cmd_get_monitors", resourceCulture);
}
}
internal static string get_monitors_error {
get {
return ResourceManager.GetString("get_monitors_error", resourceCulture);
}
}
internal static string get_monitors_no_monitors {
get {
return ResourceManager.GetString("get_monitors_no_monitors", resourceCulture);
}
}
internal static string get_monitors_header {
get {
return ResourceManager.GetString("get_monitors_header", resourceCulture);
}
}
internal static string cmd_set_layout {
get {
return ResourceManager.GetString("cmd_set_layout", resourceCulture);
}
}
internal static string set_layout_arg_layout {
get {
return ResourceManager.GetString("set_layout_arg_layout", resourceCulture);
}
}
internal static string set_layout_opt_monitor {
get {
return ResourceManager.GetString("set_layout_opt_monitor", resourceCulture);
}
}
internal static string set_layout_opt_all {
get {
return ResourceManager.GetString("set_layout_opt_all", resourceCulture);
}
}
internal static string set_layout_error_monitor_index {
get {
return ResourceManager.GetString("set_layout_error_monitor_index", resourceCulture);
}
}
internal static string set_layout_error_both_options {
get {
return ResourceManager.GetString("set_layout_error_both_options", resourceCulture);
}
}
internal static string set_layout_error_not_found {
get {
return ResourceManager.GetString("set_layout_error_not_found", resourceCulture);
}
}
internal static string set_layout_error_monitor_not_found {
get {
return ResourceManager.GetString("set_layout_error_monitor_not_found", resourceCulture);
}
}
internal static string set_layout_error_no_monitors {
get {
return ResourceManager.GetString("set_layout_error_no_monitors", resourceCulture);
}
}
internal static string set_layout_error_unsupported_type {
get {
return ResourceManager.GetString("set_layout_error_unsupported_type", resourceCulture);
}
}
internal static string set_layout_error_no_layout {
get {
return ResourceManager.GetString("set_layout_error_no_layout", resourceCulture);
}
}
internal static string set_layout_success_all {
get {
return ResourceManager.GetString("set_layout_success_all", resourceCulture);
}
}
internal static string set_layout_success_monitor {
get {
return ResourceManager.GetString("set_layout_success_monitor", resourceCulture);
}
}
internal static string set_layout_success_default {
get {
return ResourceManager.GetString("set_layout_success_default", resourceCulture);
}
}
internal static string cmd_open_editor {
get {
return ResourceManager.GetString("cmd_open_editor", resourceCulture);
}
}
internal static string open_editor_error {
get {
return ResourceManager.GetString("open_editor_error", resourceCulture);
}
}
internal static string cmd_open_settings {
get {
return ResourceManager.GetString("cmd_open_settings", resourceCulture);
}
}
internal static string open_settings_error_not_started {
get {
return ResourceManager.GetString("open_settings_error_not_started", resourceCulture);
}
}
internal static string open_settings_error {
get {
return ResourceManager.GetString("open_settings_error", resourceCulture);
}
}
internal static string cmd_set_hotkey {
get {
return ResourceManager.GetString("cmd_set_hotkey", resourceCulture);
}
}
internal static string set_hotkey_arg_key {
get {
return ResourceManager.GetString("set_hotkey_arg_key", resourceCulture);
}
}
internal static string set_hotkey_arg_layout {
get {
return ResourceManager.GetString("set_hotkey_arg_layout", resourceCulture);
}
}
internal static string set_hotkey_error_invalid_key {
get {
return ResourceManager.GetString("set_hotkey_error_invalid_key", resourceCulture);
}
}
internal static string set_hotkey_error_not_custom {
get {
return ResourceManager.GetString("set_hotkey_error_not_custom", resourceCulture);
}
}
internal static string cmd_remove_hotkey {
get {
return ResourceManager.GetString("cmd_remove_hotkey", resourceCulture);
}
}
internal static string remove_hotkey_arg_key {
get {
return ResourceManager.GetString("remove_hotkey_arg_key", resourceCulture);
}
}
internal static string remove_hotkey_no_hotkeys {
get {
return ResourceManager.GetString("remove_hotkey_no_hotkeys", resourceCulture);
}
}
internal static string remove_hotkey_not_found {
get {
return ResourceManager.GetString("remove_hotkey_not_found", resourceCulture);
}
}
internal static string cmd_get_hotkeys {
get {
return ResourceManager.GetString("cmd_get_hotkeys", resourceCulture);
}
}
internal static string get_hotkeys_no_hotkeys {
get {
return ResourceManager.GetString("get_hotkeys_no_hotkeys", resourceCulture);
}
}
internal static string get_hotkeys_header {
get {
return ResourceManager.GetString("get_hotkeys_header", resourceCulture);
}
}
internal static string get_hotkeys_instruction {
get {
return ResourceManager.GetString("get_hotkeys_instruction", resourceCulture);
}
}
internal static string editor_params_timeout {
get {
return ResourceManager.GetString("editor_params_timeout", resourceCulture);
}
}
}
}

View File

@@ -1,233 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Base Command -->
<data name="error_fancyzones_not_running" xml:space="preserve">
<value>Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.</value>
</data>
<!-- GetActiveLayoutCommand -->
<data name="cmd_get_active_layout" xml:space="preserve">
<value>Show currently active layout</value>
</data>
<data name="get_active_layout_no_monitor_info" xml:space="preserve">
<value>Could not get current monitor information.</value>
</data>
<data name="get_active_layout_no_layouts" xml:space="preserve">
<value>No layouts configured.</value>
</data>
<data name="get_active_layout_header" xml:space="preserve">
<value>=== Active FancyZones Layout(s) ===</value>
</data>
<data name="get_active_layout_no_layout" xml:space="preserve">
<value> No layout applied</value>
</data>
<!-- GetLayoutsCommand -->
<data name="cmd_get_layouts" xml:space="preserve">
<value>List available layouts</value>
</data>
<data name="get_layouts_templates_header" xml:space="preserve">
<value>=== Built-in Template Layouts ({0} total) ===</value>
</data>
<data name="get_layouts_custom_header" xml:space="preserve">
<value>=== Custom Layouts ({0} total) ===</value>
</data>
<data name="get_layouts_canvas_note" xml:space="preserve">
<value>Note: Canvas layout preview is approximate.</value>
</data>
<data name="get_layouts_canvas_detail" xml:space="preserve">
<value>Open FancyZones Editor for precise zone boundaries.</value>
</data>
<data name="get_layouts_usage" xml:space="preserve">
<value>Use 'FancyZonesCLI.exe set-layout &lt;UUID&gt;' to apply a layout.</value>
</data>
<!-- GetMonitorsCommand -->
<data name="cmd_get_monitors" xml:space="preserve">
<value>List monitors and FancyZones metadata</value>
</data>
<data name="get_monitors_error" xml:space="preserve">
<value>Failed to read monitor information. {0}
Note: Ensure FancyZones is running to get current monitor information.</value>
</data>
<data name="get_monitors_no_monitors" xml:space="preserve">
<value>No monitors found.</value>
</data>
<data name="get_monitors_header" xml:space="preserve">
<value>=== Monitors ({0} total) ===</value>
</data>
<!-- SetLayoutCommand -->
<data name="cmd_set_layout" xml:space="preserve">
<value>Set layout by UUID or template name</value>
</data>
<data name="set_layout_arg_layout" xml:space="preserve">
<value>Layout UUID or template type (e.g. focus, columns)</value>
</data>
<data name="set_layout_opt_monitor" xml:space="preserve">
<value>Apply to monitor N (1-based)</value>
</data>
<data name="set_layout_opt_all" xml:space="preserve">
<value>Apply to all monitors</value>
</data>
<data name="set_layout_error_monitor_index" xml:space="preserve">
<value>Monitor index must be &gt;= 1.</value>
</data>
<data name="set_layout_error_both_options" xml:space="preserve">
<value>Cannot specify both --monitor and --all.</value>
</data>
<data name="set_layout_error_not_found" xml:space="preserve">
<value>Layout '{0}' not found
Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')
For custom layouts, use the UUID from 'get-layouts'</value>
</data>
<data name="set_layout_error_monitor_not_found" xml:space="preserve">
<value>Monitor {0} not found. Available monitors: 1-{1}</value>
</data>
<data name="set_layout_error_no_monitors" xml:space="preserve">
<value>Internal error - no monitors to update.</value>
</data>
<data name="set_layout_error_unsupported_type" xml:space="preserve">
<value>Unsupported custom layout type '{0}'.</value>
</data>
<data name="set_layout_error_no_layout" xml:space="preserve">
<value>Internal error - no layout selected.</value>
</data>
<data name="set_layout_success_all" xml:space="preserve">
<value>Layout '{0}' applied to all monitors.</value>
</data>
<data name="set_layout_success_monitor" xml:space="preserve">
<value>Layout '{0}' applied to monitor {1}.</value>
</data>
<data name="set_layout_success_default" xml:space="preserve">
<value>Layout '{0}' applied to monitor 1.</value>
</data>
<!-- OpenEditorCommand -->
<data name="cmd_open_editor" xml:space="preserve">
<value>Launch FancyZones layout editor</value>
</data>
<data name="open_editor_error" xml:space="preserve">
<value>Failed to request FancyZones Editor launch. {0}</value>
</data>
<!-- OpenSettingsCommand -->
<data name="cmd_open_settings" xml:space="preserve">
<value>Open FancyZones settings page</value>
</data>
<data name="open_settings_error_not_started" xml:space="preserve">
<value>PowerToys.exe failed to start.</value>
</data>
<data name="open_settings_error" xml:space="preserve">
<value>Failed to open FancyZones Settings. {0}</value>
</data>
<!-- SetHotkeyCommand -->
<data name="cmd_set_hotkey" xml:space="preserve">
<value>Assign hotkey (0-9) to a custom layout</value>
</data>
<data name="set_hotkey_arg_key" xml:space="preserve">
<value>Hotkey index (0-9)</value>
</data>
<data name="set_hotkey_arg_layout" xml:space="preserve">
<value>Custom layout UUID</value>
</data>
<data name="set_hotkey_error_invalid_key" xml:space="preserve">
<value>Key must be between 0 and 9.</value>
</data>
<data name="set_hotkey_error_not_custom" xml:space="preserve">
<value>Layout '{0}' is not a custom layout UUID.</value>
</data>
<!-- RemoveHotkeyCommand -->
<data name="cmd_remove_hotkey" xml:space="preserve">
<value>Remove hotkey assignment</value>
</data>
<data name="remove_hotkey_arg_key" xml:space="preserve">
<value>Hotkey index (0-9)</value>
</data>
<data name="remove_hotkey_no_hotkeys" xml:space="preserve">
<value>No hotkeys configured.</value>
</data>
<data name="remove_hotkey_not_found" xml:space="preserve">
<value>No hotkey assigned to key {0}</value>
</data>
<!-- GetHotkeysCommand -->
<data name="cmd_get_hotkeys" xml:space="preserve">
<value>List all layout hotkeys</value>
</data>
<data name="get_hotkeys_no_hotkeys" xml:space="preserve">
<value>No hotkeys configured.</value>
</data>
<data name="get_hotkeys_header" xml:space="preserve">
<value>=== Layout Hotkeys ===</value>
</data>
<data name="get_hotkeys_instruction" xml:space="preserve">
<value>Press Win + Ctrl + Alt + &lt;number&gt; to switch layouts:</value>
</data>
<!-- EditorParametersRefresh -->
<data name="editor_params_timeout" xml:space="preserve">
<value>Could not get current monitor information (timed out after {0}ms waiting for '{1}').</value>
</data>
</root>

View File

@@ -1,36 +0,0 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace FancyZonesCLI.Telemetry
{
/// <summary>
/// Telemetry event for FancyZones CLI command execution.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class FancyZonesCLICommandEvent : EventBase, IEvent
{
public FancyZonesCLICommandEvent()
{
EventName = "FancyZones_CLICommand";
}
/// <summary>
/// Gets or sets the name of the CLI command that was executed.
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the command executed successfully.
/// </summary>
public bool Successful { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
@@ -61,7 +60,7 @@ internal static class EditorParametersRefresh
var finalParams = FancyZonesDataIO.ReadEditorParameters();
if (finalParams.Monitors == null || finalParams.Monitors.Count == 0)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.editor_params_timeout, maxWaitMilliseconds, Path.GetFileName(filePath)));
throw new InvalidOperationException($"Could not get current monitor information (timed out after {maxWaitMilliseconds}ms waiting for '{Path.GetFileName(filePath)}').");
}
return finalParams;

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ImageResizerCLI</AssemblyTitle>
<AssemblyDescription>PowerToys Image Resizer Command Line Interface</AssemblyDescription>
<Description>PowerToys Image Resizer CLI</Description>
<OutputType>Exe</OutputType>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<AssemblyName>PowerToys.ImageResizerCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Project>

View File

@@ -1,50 +0,0 @@
// 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;
using System.Globalization;
using System.Text;
using ImageResizer.Cli;
using ManagedCommon;
namespace ImageResizerCLI;
internal static class Program
{
private static int Main(string[] args)
{
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException)
{
// Ignore invalid culture and fall back to default.
}
Console.InputEncoding = Encoding.Unicode;
// Initialize logger to file (same as other modules)
CliLogger.Initialize("\\ImageResizer\\Logs");
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
try
{
var executor = new ImageResizerCliExecutor();
return executor.Run(args);
}
catch (Exception ex)
{
CliLogger.Error($"Unhandled exception: {ex.Message}");
CliLogger.Error($"Stack trace: {ex.StackTrace}");
Console.Error.WriteLine($"Fatal error: {ex.Message}");
return 1;
}
}
}

View File

@@ -1,320 +0,0 @@
// 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 ImageResizer.Cli;
using ImageResizer.Models;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Cli
{
[TestClass]
public class CliSettingsApplierTests
{
private Settings CreateDefaultSettings()
{
var settings = new Settings();
settings.Sizes.Add(new ResizeSize(0, "Small", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(1, "Medium", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(2, "Large", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel));
return settings;
}
[TestMethod]
public void Apply_WithCustomWidth_SetsCustomSizeWidth()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithCustomHeight_SetsCustomSizeHeight()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithCustomSize_SelectsCustomSizeIndex()
{
var options = new CliOptions { Width = 800, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size index should be settings.Sizes.Count
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithZeroWidth_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 0, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(0.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithZeroHeight_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 800, Height = 0 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(0.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithNullWidthAndHeight_DoesNotModifyCustomSize()
{
var options = new CliOptions { Width = null, Height = null };
var settings = CreateDefaultSettings();
var originalWidth = settings.CustomSize.Width;
var originalHeight = settings.CustomSize.Height;
CliSettingsApplier.Apply(options, settings);
// When both null, should not modify CustomSize (keeps default 1024x640)
Assert.AreEqual(originalWidth, settings.CustomSize.Width);
Assert.AreEqual(originalHeight, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithUnit_SetsCustomSizeUnit()
{
var options = new CliOptions { Width = 100, Unit = ResizeUnit.Percent };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
}
[TestMethod]
public void Apply_WithFit_SetsCustomSizeFit()
{
var options = new CliOptions { Width = 800, Fit = ResizeFit.Fill };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
}
[TestMethod]
public void Apply_WithValidSizeIndex_SetsSelectedSizeIndex()
{
var options = new CliOptions { SizeIndex = 1 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(1, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithInvalidSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = 99 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
// Should remain unchanged when invalid
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithNegativeSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = -1 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithShrinkOnly_SetsShrinkOnly()
{
var options = new CliOptions { ShrinkOnly = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.ShrinkOnly);
}
[TestMethod]
public void Apply_WithReplace_SetsReplace()
{
var options = new CliOptions { Replace = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.Replace);
}
[TestMethod]
public void Apply_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var options = new CliOptions { IgnoreOrientation = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.IgnoreOrientation);
}
[TestMethod]
public void Apply_WithRemoveMetadata_SetsRemoveMetadata()
{
var options = new CliOptions { RemoveMetadata = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.RemoveMetadata);
}
[TestMethod]
public void Apply_WithJpegQualityLevel_SetsJpegQualityLevel()
{
var options = new CliOptions { JpegQualityLevel = 85 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(85, settings.JpegQualityLevel);
}
[TestMethod]
public void Apply_WithKeepDateModified_SetsKeepDateModified()
{
var options = new CliOptions { KeepDateModified = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.KeepDateModified);
}
[TestMethod]
public void Apply_WithFileName_SetsFileName()
{
var options = new CliOptions { FileName = "%1 (%2)" };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual("%1 (%2)", settings.FileName);
}
[TestMethod]
public void Apply_WithEmptyFileName_DoesNotChangeFileName()
{
var options = new CliOptions { FileName = string.Empty };
var settings = CreateDefaultSettings();
var originalFileName = settings.FileName;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalFileName, settings.FileName);
}
[TestMethod]
public void Apply_WithMultipleOptions_AppliesAllOptions()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
Unit = ResizeUnit.Percent,
Fit = ResizeFit.Fill,
ShrinkOnly = true,
Replace = true,
IgnoreOrientation = true,
RemoveMetadata = true,
JpegQualityLevel = 90,
KeepDateModified = true,
FileName = "test_%2",
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
Assert.IsTrue(settings.ShrinkOnly);
Assert.IsTrue(settings.Replace);
Assert.IsTrue(settings.IgnoreOrientation);
Assert.IsTrue(settings.RemoveMetadata);
Assert.AreEqual(90, settings.JpegQualityLevel);
Assert.IsTrue(settings.KeepDateModified);
Assert.AreEqual("test_%2", settings.FileName);
}
[TestMethod]
public void Apply_CustomSizeTakesPrecedence_OverSizeIndex()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
SizeIndex = 1, // Should be ignored when Width/Height specified
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size should be selected, not preset
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyWidth_StillSelectsCustomSize()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyHeight_StillSelectsCustomSize()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
}
}

View File

@@ -1,268 +0,0 @@
// 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;
using System.Linq;
using ImageResizer.Cli.Commands;
using ImageResizer.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Models
{
[TestClass]
public class CliOptionsTests
{
private static readonly string[] _multiFileArgs = new[] { "test1.jpg", "test2.jpg", "test3.jpg" };
private static readonly string[] _mixedOptionsArgs = new[] { "--width", "800", "test1.jpg", "--height", "600", "test2.jpg" };
[TestMethod]
public void Parse_WithValidWidth_SetsWidth()
{
var args = new[] { "--width", "800", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
}
[TestMethod]
public void Parse_WithValidHeight_SetsHeight()
{
var args = new[] { "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithShortWidthAlias_WorksIdentically()
{
var longFormArgs = new[] { "--width", "800", "test.jpg" };
var shortFormArgs = new[] { "-w", "800", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Width, shortForm.Width);
}
[TestMethod]
public void Parse_WithShortHeightAlias_WorksIdentically()
{
var longFormArgs = new[] { "--height", "600", "test.jpg" };
var shortFormArgs = new[] { "-h", "600", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Height, shortForm.Height);
}
[TestMethod]
public void Parse_WithValidUnit_SetsUnit()
{
var args = new[] { "--unit", "Percent", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Percent, options.Unit);
}
[TestMethod]
public void Parse_WithValidFit_SetsFit()
{
var args = new[] { "--fit", "Fill", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeFit.Fill, options.Fit);
}
[TestMethod]
public void Parse_WithSizeIndex_SetsSizeIndex()
{
var args = new[] { "--size", "2", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(2, options.SizeIndex);
}
[TestMethod]
public void Parse_WithShrinkOnly_SetsShrinkOnly()
{
var args = new[] { "--shrink-only", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ShrinkOnly);
}
[TestMethod]
public void Parse_WithReplace_SetsReplace()
{
var args = new[] { "--replace", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.Replace);
}
[TestMethod]
public void Parse_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var args = new[] { "--ignore-orientation", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.IgnoreOrientation);
}
[TestMethod]
public void Parse_WithRemoveMetadata_SetsRemoveMetadata()
{
var args = new[] { "--remove-metadata", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.RemoveMetadata);
}
[TestMethod]
public void Parse_WithValidQuality_SetsQuality()
{
var args = new[] { "--quality", "85", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(85, options.JpegQualityLevel);
}
[TestMethod]
public void Parse_WithKeepDateModified_SetsKeepDateModified()
{
var args = new[] { "--keep-date-modified", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.KeepDateModified);
}
[TestMethod]
public void Parse_WithFileName_SetsFileName()
{
var args = new[] { "--filename", "%1 (%2)", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("%1 (%2)", options.FileName);
}
[TestMethod]
public void Parse_WithDestination_SetsDestinationDirectory()
{
var args = new[] { "--destination", "C:\\Output", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("C:\\Output", options.DestinationDirectory);
}
[TestMethod]
public void Parse_WithShortDestinationAlias_WorksIdentically()
{
var longFormArgs = new[] { "--destination", "C:\\Output", "test.jpg" };
var shortFormArgs = new[] { "-d", "C:\\Output", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.DestinationDirectory, shortForm.DestinationDirectory);
}
[TestMethod]
public void Parse_WithProgressLines_SetsProgressLines()
{
var args = new[] { "--progress-lines", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithAccessibleAlias_SetsProgressLines()
{
var args = new[] { "--accessible", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithMultipleFiles_AddsAllFiles()
{
var args = _multiFileArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(3, options.Files.Count);
CollectionAssert.Contains(options.Files.ToList(), "test1.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test2.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test3.jpg");
}
[TestMethod]
public void Parse_WithMixedOptionsAndFiles_ParsesCorrectly()
{
var args = _mixedOptionsArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(600.0, options.Height);
Assert.AreEqual(2, options.Files.Count);
}
[TestMethod]
public void Parse_WithHelp_SetsShowHelp()
{
var args = new[] { "--help" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowHelp);
}
[TestMethod]
public void Parse_WithShowConfig_SetsShowConfig()
{
var args = new[] { "--show-config" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowConfig);
}
[TestMethod]
public void Parse_WithNoArguments_ReturnsEmptyOptions()
{
var args = Array.Empty<string>();
var options = CliOptions.Parse(args);
Assert.IsNotNull(options);
Assert.AreEqual(0, options.Files.Count);
}
[TestMethod]
public void Parse_WithZeroWidth_AllowsZeroValue()
{
var args = new[] { "--width", "0", "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(0.0, options.Width);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithZeroHeight_AllowsZeroValue()
{
var args = new[] { "--width", "800", "--height", "0", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(0.0, options.Height);
}
[TestMethod]
public void Parse_CaseInsensitiveEnums_ParsesCorrectly()
{
var args = new[] { "--unit", "pixel", "--fit", "fit", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Pixel, options.Unit);
Assert.AreEqual(ResizeFit.Fit, options.Fit);
}
}
}

View File

@@ -25,27 +25,20 @@ namespace ImageResizer.Models
[TestMethod]
public void FromCommandLineWorks()
{
// Use actual test files that exist in the test directory
var testDir = Path.GetDirectoryName(typeof(ResizeBatchTests).Assembly.Location);
var file1 = Path.Combine(testDir, "Test.jpg");
var file2 = Path.Combine(testDir, "Test.png");
var file3 = Path.Combine(testDir, "Test.gif");
var standardInput =
file1 + EOL +
file2;
"Image1.jpg" + EOL +
"Image2.jpg";
var args = new[]
{
"/d", "OutputDir",
file3,
"Image3.jpg",
};
var result = ResizeBatch.FromCommandLine(
new StringReader(standardInput),
args);
var files = result.Files.Select(Path.GetFileName).ToArray();
CollectionAssert.AreEquivalent(new List<string> { "Test.jpg", "Test.png", "Test.gif" }, files);
CollectionAssert.AreEquivalent(new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" }, result.Files.ToArray());
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}

View File

@@ -1,28 +0,0 @@
// 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 ManagedCommon;
namespace ImageResizer.Cli
{
public static class CliLogger
{
private static bool _initialized;
public static void Initialize(string logSubFolder)
{
if (!_initialized)
{
Logger.InitializeLogger(logSubFolder);
_initialized = true;
}
}
public static void Info(string message) => Logger.LogInfo(message);
public static void Warn(string message) => Logger.LogWarning(message);
public static void Error(string message) => Logger.LogError(message);
}
}

View File

@@ -1,122 +0,0 @@
// 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;
using System.Globalization;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Applies CLI options to Settings object.
/// Separated from executor logic for Single Responsibility Principle.
/// </summary>
public static class CliSettingsApplier
{
/// <summary>
/// Applies CLI options to the settings, overriding default values.
/// </summary>
/// <param name="cliOptions">The CLI options to apply.</param>
/// <param name="settings">The settings to modify.</param>
public static void Apply(CliOptions cliOptions, Settings settings)
{
// Handle complex size options first
ApplySizeOptions(cliOptions, settings);
// Apply simple property mappings
ApplySimpleOptions(cliOptions, settings);
}
private static void ApplySizeOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.Width.HasValue || cliOptions.Height.HasValue)
{
ApplyCustomSizeOptions(cliOptions, settings);
}
else if (cliOptions.SizeIndex.HasValue)
{
ApplyPresetSizeOption(cliOptions, settings);
}
}
private static void ApplyCustomSizeOptions(CliOptions cliOptions, Settings settings)
{
// Set dimensions (0 = auto-calculate for aspect ratio preservation)
// Implementation: ResizeSize.ConvertToPixels() returns double.PositiveInfinity for 0 in Fit mode,
// causing Math.Min(scaleX, scaleY) to preserve aspect ratio by selecting the non-zero scale.
// For Fill/Stretch modes, 0 uses the original dimension instead.
settings.CustomSize.Width = cliOptions.Width ?? 0;
settings.CustomSize.Height = cliOptions.Height ?? 0;
// Apply optional properties
if (cliOptions.Unit.HasValue)
{
settings.CustomSize.Unit = cliOptions.Unit.Value;
}
if (cliOptions.Fit.HasValue)
{
settings.CustomSize.Fit = cliOptions.Fit.Value;
}
// Select custom size (index = Sizes.Count)
settings.SelectedSizeIndex = settings.Sizes.Count;
}
private static void ApplyPresetSizeOption(CliOptions cliOptions, Settings settings)
{
var index = cliOptions.SizeIndex.Value;
if (index >= 0 && index < settings.Sizes.Count)
{
settings.SelectedSizeIndex = index;
}
else
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_WarningInvalidSizeIndex, index));
CliLogger.Warn($"Invalid size index: {index}");
}
}
private static void ApplySimpleOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.ShrinkOnly.HasValue)
{
settings.ShrinkOnly = cliOptions.ShrinkOnly.Value;
}
if (cliOptions.Replace.HasValue)
{
settings.Replace = cliOptions.Replace.Value;
}
if (cliOptions.IgnoreOrientation.HasValue)
{
settings.IgnoreOrientation = cliOptions.IgnoreOrientation.Value;
}
if (cliOptions.RemoveMetadata.HasValue)
{
settings.RemoveMetadata = cliOptions.RemoveMetadata.Value;
}
if (cliOptions.JpegQualityLevel.HasValue)
{
settings.JpegQualityLevel = cliOptions.JpegQualityLevel.Value;
}
if (cliOptions.KeepDateModified.HasValue)
{
settings.KeepDateModified = cliOptions.KeepDateModified.Value;
}
if (!string.IsNullOrEmpty(cliOptions.FileName))
{
settings.FileName = cliOptions.FileName;
}
}
}
}

View File

@@ -1,90 +0,0 @@
// 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.CommandLine;
using ImageResizer.Cli.Options;
namespace ImageResizer.Cli.Commands
{
/// <summary>
/// Root command for the ImageResizer CLI.
/// </summary>
public sealed class ImageResizerRootCommand : RootCommand
{
public ImageResizerRootCommand()
: base("PowerToys Image Resizer - Resize images from command line")
{
HelpOption = new HelpOption();
ShowConfigOption = new ShowConfigOption();
DestinationOption = new DestinationOption();
WidthOption = new WidthOption();
HeightOption = new HeightOption();
UnitOption = new UnitOption();
FitOption = new FitOption();
SizeOption = new SizeOption();
ShrinkOnlyOption = new ShrinkOnlyOption();
ReplaceOption = new ReplaceOption();
IgnoreOrientationOption = new IgnoreOrientationOption();
RemoveMetadataOption = new RemoveMetadataOption();
QualityOption = new QualityOption();
KeepDateModifiedOption = new KeepDateModifiedOption();
FileNameOption = new FileNameOption();
ProgressLinesOption = new ProgressLinesOption();
FilesArgument = new FilesArgument();
AddOption(HelpOption);
AddOption(ShowConfigOption);
AddOption(DestinationOption);
AddOption(WidthOption);
AddOption(HeightOption);
AddOption(UnitOption);
AddOption(FitOption);
AddOption(SizeOption);
AddOption(ShrinkOnlyOption);
AddOption(ReplaceOption);
AddOption(IgnoreOrientationOption);
AddOption(RemoveMetadataOption);
AddOption(QualityOption);
AddOption(KeepDateModifiedOption);
AddOption(FileNameOption);
AddOption(ProgressLinesOption);
AddArgument(FilesArgument);
}
public HelpOption HelpOption { get; }
public ShowConfigOption ShowConfigOption { get; }
public DestinationOption DestinationOption { get; }
public WidthOption WidthOption { get; }
public HeightOption HeightOption { get; }
public UnitOption UnitOption { get; }
public FitOption FitOption { get; }
public SizeOption SizeOption { get; }
public ShrinkOnlyOption ShrinkOnlyOption { get; }
public ReplaceOption ReplaceOption { get; }
public IgnoreOrientationOption IgnoreOrientationOption { get; }
public RemoveMetadataOption RemoveMetadataOption { get; }
public QualityOption QualityOption { get; }
public KeepDateModifiedOption KeepDateModifiedOption { get; }
public FileNameOption FileNameOption { get; }
public ProgressLinesOption ProgressLinesOption { get; }
public FilesArgument FilesArgument { get; }
}
}

View File

@@ -1,124 +0,0 @@
// 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;
using System.Globalization;
using System.Linq;
using System.Threading;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Executes Image Resizer CLI operations.
/// Instance-based design for better testability and Single Responsibility Principle.
/// </summary>
public class ImageResizerCliExecutor
{
/// <summary>
/// Runs the CLI executor with the provided command-line arguments.
/// </summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>Exit code.</returns>
public int Run(string[] args)
{
var cliOptions = CliOptions.Parse(args);
if (cliOptions.ParseErrors.Count > 0)
{
foreach (var error in cliOptions.ParseErrors)
{
Console.Error.WriteLine(error);
CliLogger.Error($"Parse error: {error}");
}
CliOptions.PrintUsage();
return 1;
}
if (cliOptions.ShowHelp)
{
CliOptions.PrintUsage();
return 0;
}
if (cliOptions.ShowConfig)
{
CliOptions.PrintConfig(Settings.Default);
return 0;
}
if (cliOptions.Files.Count == 0 && string.IsNullOrEmpty(cliOptions.PipeName))
{
Console.WriteLine(Resources.CLI_NoInputFiles);
CliOptions.PrintUsage();
return 1;
}
return RunSilentMode(cliOptions);
}
private int RunSilentMode(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
CliSettingsApplier.Apply(cliOptions, settings);
CliLogger.Info($"CLI mode: processing {batch.Files.Count} files");
// Use accessible line-based progress if requested or detected
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
var errors = batch.Process(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);
if (useLineBasedProgress)
{
// Milestone-based progress (0%, 25%, 50%, 75%, 100%)
int milestone = (progress / 25) * 25;
if (milestone > lastReportedMilestone || completed == (int)total)
{
lastReportedMilestone = milestone;
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total));
}
}
else
{
// Traditional carriage return mode
Console.Write(string.Format(CultureInfo.InvariantCulture, "\r{0}", string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total)));
}
},
settings,
CancellationToken.None);
if (!useLineBasedProgress)
{
Console.WriteLine();
}
var errorList = errors.ToList();
if (errorList.Count > 0)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_CompletedWithErrors, errorList.Count));
CliLogger.Error($"Processing completed with {errorList.Count} error(s)");
foreach (var error in errorList)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1}", error.File, error.Error));
CliLogger.Error($" {error.File}: {error.Error}");
}
return 1;
}
CliLogger.Info("CLI batch completed successfully");
Console.WriteLine(Resources.CLI_AllFilesProcessed);
return 0;
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class DestinationOption : Option<string>
{
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FileNameOption : Option<string>
{
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
{
}
}
}

View File

@@ -1,17 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FilesArgument : Argument<string[]>
{
public FilesArgument()
: base("files", Properties.Resources.CLI_Option_Files)
{
Arity = ArgumentArity.ZeroOrMore;
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FitOption : Option<ImageResizer.Models.ResizeFit?>
{
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HeightOption : Option<double?>
{
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HelpOption : Option<bool>
{
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class IgnoreOrientationOption : Option<bool>
{
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class KeepDateModifiedOption : Option<bool>
{
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ProgressLinesOption : Option<bool>
{
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}
}

View File

@@ -1,26 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class QualityOption : Option<int?>
{
private static readonly string[] _aliases = ["--quality", "-q"];
public QualityOption()
: base(_aliases, Properties.Resources.CLI_Option_Quality)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && (value.Value < 1 || value.Value > 100))
{
result.ErrorMessage = "JPEG quality must be between 1 and 100.";
}
});
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class RemoveMetadataOption : Option<bool>
{
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ReplaceOption : Option<bool>
{
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShowConfigOption : Option<bool>
{
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShrinkOnlyOption : Option<bool>
{
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}
}

View File

@@ -1,26 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class SizeOption : Option<int?>
{
private static readonly string[] _aliases = ["--size"];
public SizeOption()
: base(_aliases, Properties.Resources.CLI_Option_Size)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && value.Value < 0)
{
result.ErrorMessage = "Size index must be a non-negative integer.";
}
});
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class UnitOption : Option<ImageResizer.Models.ResizeUnit?>
{
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
{
}
}
}

View File

@@ -1,18 +0,0 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class WidthOption : Option<double?>
{
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
{
}
}
}

View File

@@ -20,7 +20,6 @@
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA1863</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -52,7 +51,6 @@
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WPF-UI" />
</ItemGroup>

View File

@@ -1,261 +0,0 @@
// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.CommandLine.Parsing;
using System.Globalization;
using ImageResizer.Cli.Commands;
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
namespace ImageResizer.Models
{
/// <summary>
/// Represents the command-line options for ImageResizer CLI mode.
/// </summary>
public class CliOptions
{
/// <summary>
/// Gets or sets a value indicating whether to show help information.
/// </summary>
public bool ShowHelp { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show current configuration.
/// </summary>
public bool ShowConfig { get; set; }
/// <summary>
/// Gets or sets the destination directory for resized images.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// Gets or sets the width of the resized image.
/// </summary>
public double? Width { get; set; }
/// <summary>
/// Gets or sets the height of the resized image.
/// </summary>
public double? Height { get; set; }
/// <summary>
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
/// </summary>
public ResizeUnit? Unit { get; set; }
/// <summary>
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
/// </summary>
public ResizeFit? Fit { get; set; }
/// <summary>
/// Gets or sets the index of the preset size to use.
/// </summary>
public int? SizeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
/// </summary>
public bool? ShrinkOnly { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to replace the original file.
/// </summary>
public bool? Replace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore orientation when resizing.
/// </summary>
public bool? IgnoreOrientation { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remove metadata from the resized image.
/// </summary>
public bool? RemoveMetadata { get; set; }
/// <summary>
/// Gets or sets the JPEG quality level (1-100).
/// </summary>
public int? JpegQualityLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to keep the date modified.
/// </summary>
public bool? KeepDateModified { get; set; }
/// <summary>
/// Gets or sets the output filename format.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
/// </summary>
public bool? ProgressLines { get; set; }
/// <summary>
/// Gets the list of files to process.
/// </summary>
public ICollection<string> Files { get; } = new List<string>();
/// <summary>
/// Gets or sets the pipe name for receiving file list.
/// </summary>
public string PipeName { get; set; }
/// <summary>
/// Gets parse/validation errors produced by System.CommandLine.
/// </summary>
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
/// <summary>
/// Converts a boolean value to nullable bool (true -> true, false -> null).
/// </summary>
private static bool? ToBoolOrNull(bool value) => value ? true : null;
/// <summary>
/// Parses command-line arguments into CliOptions using System.CommandLine.
/// </summary>
/// <param name="args">The command-line arguments.</param>
/// <returns>A CliOptions instance with parsed values.</returns>
public static CliOptions Parse(string[] args)
{
var options = new CliOptions();
var cmd = new ImageResizerRootCommand();
// Parse using System.CommandLine
var parseResult = new Parser(cmd).Parse(args);
if (parseResult.Errors.Count > 0)
{
var errors = new List<string>(parseResult.Errors.Count);
foreach (var error in parseResult.Errors)
{
errors.Add(error.Message);
}
options.ParseErrors = new ReadOnlyCollection<string>(errors);
}
// Extract values from parse result using strongly typed options
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
options.Width = parseResult.GetValueForOption(cmd.WidthOption);
options.Height = parseResult.GetValueForOption(cmd.HeightOption);
options.Unit = parseResult.GetValueForOption(cmd.UnitOption);
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
// Convert bool to nullable bool (true -> true, false -> null)
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
options.RemoveMetadata = ToBoolOrNull(parseResult.GetValueForOption(cmd.RemoveMetadataOption));
options.KeepDateModified = ToBoolOrNull(parseResult.GetValueForOption(cmd.KeepDateModifiedOption));
options.ProgressLines = ToBoolOrNull(parseResult.GetValueForOption(cmd.ProgressLinesOption));
options.JpegQualityLevel = parseResult.GetValueForOption(cmd.QualityOption);
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
// Get files from arguments
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
if (files != null)
{
const string pipeNamePrefix = "\\\\.\\pipe\\";
foreach (var file in files)
{
// Check for pipe name (must be at the start of the path)
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
{
options.PipeName = file.Substring(pipeNamePrefix.Length);
}
else
{
options.Files.Add(file);
}
}
}
return options;
}
/// <summary>
/// Prints current configuration to the console.
/// </summary>
/// <param name="settings">The settings to display.</param>
public static void PrintConfig(ImageResizer.Properties.Settings settings)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
for (int i = 0; i < settings.Sizes.Count; i++)
{
var size = settings.Sizes[i];
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
}
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
{
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
}
}
/// <summary>
/// Prints usage information to the console.
/// </summary>
public static void PrintUsage()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
Console.WriteLine();
var cmd = new ImageResizerRootCommand();
// Print usage line
Console.WriteLine(Properties.Resources.CLI_UsageLine);
Console.WriteLine();
// Print options from the command definition
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
var description = option.Description ?? string.Empty;
Console.WriteLine($" {aliases,-30} {description}");
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
}
}
}

View File

@@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -40,78 +39,44 @@ namespace ImageResizer.Models
_aiSuperResolutionService = null;
}
/// <summary>
/// Validates if a file path is a supported image format.
/// </summary>
/// <param name="path">The file path to validate.</param>
/// <returns>True if the path is valid and points to a supported image file.</returns>
private static bool IsValidImagePath(string path)
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
var batch = new ResizeBatch();
const string pipeNamePrefix = "\\\\.\\pipe\\";
string pipeName = null;
if (!File.Exists(path))
for (var i = 0; i < args?.Length; i++)
{
return false;
}
var ext = Path.GetExtension(path)?.ToLowerInvariant();
var validExtensions = new[]
{
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
};
return validExtensions.Contains(ext);
}
/// <summary>
/// Creates a ResizeBatch from CliOptions.
/// </summary>
/// <param name="standardInput">Standard input stream for reading additional file paths.</param>
/// <param name="options">The parsed CLI options.</param>
/// <returns>A ResizeBatch instance.</returns>
public static ResizeBatch FromCliOptions(TextReader standardInput, CliOptions options)
{
var batch = new ResizeBatch
{
DestinationDirectory = options.DestinationDirectory,
};
foreach (var file in options.Files)
{
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
if (args[i] == "/d")
{
batch.Files.Add(absolutePath);
batch.DestinationDirectory = args[++i];
continue;
}
else if (args[i].Contains(pipeNamePrefix))
{
pipeName = args[i].Substring(pipeNamePrefix.Length);
continue;
}
batch.Files.Add(args[i]);
}
if (string.IsNullOrEmpty(options.PipeName))
if (string.IsNullOrEmpty(pipeName))
{
// NB: We read these from stdin since there are limits on the number of args you can have
// Only read from stdin if it's redirected (piped input), not from interactive terminal
string file;
if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
if (standardInput != null)
{
while ((file = standardInput.ReadLine()) != null)
{
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
batch.Files.Add(file);
}
}
}
else
{
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", options.PipeName, PipeDirection.In))
new NamedPipeClientStream(".", pipeName, PipeDirection.In))
{
// Connect to the pipe or wait until the pipe is available.
pipeClient.Connect();
@@ -123,10 +88,7 @@ namespace ImageResizer.Models
// Display the read text to the console
while ((file = sr.ReadLine()) != null)
{
if (IsValidImagePath(file))
{
batch.Files.Add(file);
}
batch.Files.Add(file);
}
}
}
@@ -135,26 +97,17 @@ namespace ImageResizer.Models
return batch;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
{
var options = CliOptions.Parse(args);
return FromCliOptions(standardInput, options);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
{
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
return Process(reportProgress, Settings.Default, cancellationToken);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
int completed = 0;
var errors = new ConcurrentBag<ResizeError>();
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
var settings = Settings.Default;
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
// APIs and a custom SynchronizationContext
Parallel.ForEach(

View File

@@ -716,437 +716,5 @@ namespace ImageResizer.Properties {
return ResourceManager.GetString("Width", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Processing {0} files....
/// </summary>
public static string CLI_ProcessingFiles {
get {
return ResourceManager.GetString("CLI_ProcessingFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to [{0}%] {1}/{2} completed.
/// </summary>
public static string CLI_ProgressFormat {
get {
return ResourceManager.GetString("CLI_ProgressFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completed with {0} error(s)..
/// </summary>
public static string CLI_CompletedWithErrors {
get {
return ResourceManager.GetString("CLI_CompletedWithErrors", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All files processed successfully!.
/// </summary>
public static string CLI_AllFilesProcessed {
get {
return ResourceManager.GetString("CLI_AllFilesProcessed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No input files or pipe specified. Showing usage..
/// </summary>
public static string CLI_NoInputFiles {
get {
return ResourceManager.GetString("CLI_NoInputFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Warning: Size index {0} is invalid. Using custom size..
/// </summary>
public static string CLI_WarningInvalidSizeIndex {
get {
return ResourceManager.GetString("CLI_WarningInvalidSizeIndex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Current Configuration:.
/// </summary>
public static string CLI_ConfigTitle {
get {
return ResourceManager.GetString("CLI_ConfigTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to General Settings:.
/// </summary>
public static string CLI_ConfigGeneralSettings {
get {
return ResourceManager.GetString("CLI_ConfigGeneralSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shrink only: {0}.
/// </summary>
public static string CLI_ConfigShrinkOnly {
get {
return ResourceManager.GetString("CLI_ConfigShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace original: {0}.
/// </summary>
public static string CLI_ConfigReplaceOriginal {
get {
return ResourceManager.GetString("CLI_ConfigReplaceOriginal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore orientation: {0}.
/// </summary>
public static string CLI_ConfigIgnoreOrientation {
get {
return ResourceManager.GetString("CLI_ConfigIgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove metadata: {0}.
/// </summary>
public static string CLI_ConfigRemoveMetadata {
get {
return ResourceManager.GetString("CLI_ConfigRemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep date modified: {0}.
/// </summary>
public static string CLI_ConfigKeepDateModified {
get {
return ResourceManager.GetString("CLI_ConfigKeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to JPEG quality: {0}.
/// </summary>
public static string CLI_ConfigJpegQuality {
get {
return ResourceManager.GetString("CLI_ConfigJpegQuality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PNG interlace: {0}.
/// </summary>
public static string CLI_ConfigPngInterlace {
get {
return ResourceManager.GetString("CLI_ConfigPngInterlace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TIFF compress: {0}.
/// </summary>
public static string CLI_ConfigTiffCompress {
get {
return ResourceManager.GetString("CLI_ConfigTiffCompress", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Filename format: {0}.
/// </summary>
public static string CLI_ConfigFilenameFormat {
get {
return ResourceManager.GetString("CLI_ConfigFilenameFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom Size:.
/// </summary>
public static string CLI_ConfigCustomSize {
get {
return ResourceManager.GetString("CLI_ConfigCustomSize", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width: {0}.
/// </summary>
public static string CLI_ConfigWidth {
get {
return ResourceManager.GetString("CLI_ConfigWidth", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height: {0}.
/// </summary>
public static string CLI_ConfigHeight {
get {
return ResourceManager.GetString("CLI_ConfigHeight", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fit mode: {0}.
/// </summary>
public static string CLI_ConfigFitMode {
get {
return ResourceManager.GetString("CLI_ConfigFitMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preset Sizes: (* = currently selected).
/// </summary>
public static string CLI_ConfigPresetSizes {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: {1} x {2} ({3}).
/// </summary>
public static string CLI_ConfigPresetSizeFormat {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizeFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to → Custom size selected.
/// </summary>
public static string CLI_ConfigCustomSelected {
get {
return ResourceManager.GetString("CLI_ConfigCustomSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image Resizer CLI.
/// </summary>
public static string CLI_UsageTitle {
get {
return ResourceManager.GetString("CLI_UsageTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Usage: PowerToys.ImageResizer.exe [options] &lt;files&gt;.
/// </summary>
public static string CLI_UsageLine {
get {
return ResourceManager.GetString("CLI_UsageLine", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Options:.
/// </summary>
public static string CLI_UsageOptions {
get {
return ResourceManager.GetString("CLI_UsageOptions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Examples:.
/// </summary>
public static string CLI_UsageExamples {
get {
return ResourceManager.GetString("CLI_UsageExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --help.
/// </summary>
public static string CLI_UsageExampleHelp {
get {
return ResourceManager.GetString("CLI_UsageExampleHelp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --width 800 --height 600 image.jpg.
/// </summary>
public static string CLI_UsageExampleDimensions {
get {
return ResourceManager.GetString("CLI_UsageExampleDimensions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 50 --unit percent *.jpg.
/// </summary>
public static string CLI_UsageExamplePercent {
get {
return ResourceManager.GetString("CLI_UsageExamplePercent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 2 image1.png image2.png.
/// </summary>
public static string CLI_UsageExamplePreset {
get {
return ResourceManager.GetString("CLI_UsageExamplePreset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Destination directory for resized images.
/// </summary>
public static string CLI_Option_Destination {
get {
return ResourceManager.GetString("CLI_Option_Destination", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output filename format (e.g., %1 (%2)).
/// </summary>
public static string CLI_Option_FileName {
get {
return ResourceManager.GetString("CLI_Option_FileName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image files to resize.
/// </summary>
public static string CLI_Option_Files {
get {
return ResourceManager.GetString("CLI_Option_Files", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to How to fit image: fill, fit, stretch.
/// </summary>
public static string CLI_Option_Fit {
get {
return ResourceManager.GetString("CLI_Option_Fit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height of the resized image in pixels.
/// </summary>
public static string CLI_Option_Height {
get {
return ResourceManager.GetString("CLI_Option_Height", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display this help message.
/// </summary>
public static string CLI_Option_Help {
get {
return ResourceManager.GetString("CLI_Option_Help", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore image orientation metadata.
/// </summary>
public static string CLI_Option_IgnoreOrientation {
get {
return ResourceManager.GetString("CLI_Option_IgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preserve the original file modification date.
/// </summary>
public static string CLI_Option_KeepDateModified {
get {
return ResourceManager.GetString("CLI_Option_KeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set JPEG quality level (1-100).
/// </summary>
public static string CLI_Option_Quality {
get {
return ResourceManager.GetString("CLI_Option_Quality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove image metadata during resizing.
/// </summary>
public static string CLI_Option_RemoveMetadata {
get {
return ResourceManager.GetString("CLI_Option_RemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace the original image file.
/// </summary>
public static string CLI_Option_Replace {
get {
return ResourceManager.GetString("CLI_Option_Replace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display current configuration.
/// </summary>
public static string CLI_Option_ShowConfig {
get {
return ResourceManager.GetString("CLI_Option_ShowConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only shrink images, do not enlarge.
/// </summary>
public static string CLI_Option_ShrinkOnly {
get {
return ResourceManager.GetString("CLI_Option_ShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use preset size by index (0-based).
/// </summary>
public static string CLI_Option_Size {
get {
return ResourceManager.GetString("CLI_Option_Size", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unit of measurement: pixel, percent, cm, inch.
/// </summary>
public static string CLI_Option_Unit {
get {
return ResourceManager.GetString("CLI_Option_Unit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width of the resized image in pixels.
/// </summary>
public static string CLI_Option_Width {
get {
return ResourceManager.GetString("CLI_Option_Width", resourceCulture);
}
}
}
}

View File

@@ -347,156 +347,4 @@
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
<value>Upscale images using on-device AI</value>
</data>
<!-- CLI Processing messages -->
<data name="CLI_ProcessingFiles" xml:space="preserve">
<value>Processing {0} file(s)...</value>
</data>
<data name="CLI_ProgressFormat" xml:space="preserve">
<value>Progress: {0}% ({1}/{2})</value>
</data>
<data name="CLI_CompletedWithErrors" xml:space="preserve">
<value>Completed with {0} error(s):</value>
</data>
<data name="CLI_AllFilesProcessed" xml:space="preserve">
<value>All files processed successfully.</value>
</data>
<data name="CLI_WarningInvalidSizeIndex" xml:space="preserve">
<value>Warning: Invalid size index {0}. Using default.</value>
</data>
<data name="CLI_NoInputFiles" xml:space="preserve">
<value>No input files or pipe specified. Showing usage.</value>
</data>
<!-- CLI Config display -->
<data name="CLI_ConfigTitle" xml:space="preserve">
<value>ImageResizer - Current Configuration</value>
</data>
<data name="CLI_ConfigGeneralSettings" xml:space="preserve">
<value>General Settings:</value>
</data>
<data name="CLI_ConfigShrinkOnly" xml:space="preserve">
<value> Shrink Only: {0}</value>
</data>
<data name="CLI_ConfigReplaceOriginal" xml:space="preserve">
<value> Replace Original: {0}</value>
</data>
<data name="CLI_ConfigIgnoreOrientation" xml:space="preserve">
<value> Ignore Orientation: {0}</value>
</data>
<data name="CLI_ConfigRemoveMetadata" xml:space="preserve">
<value> Remove Metadata: {0}</value>
</data>
<data name="CLI_ConfigKeepDateModified" xml:space="preserve">
<value> Keep Date Modified: {0}</value>
</data>
<data name="CLI_ConfigJpegQuality" xml:space="preserve">
<value> JPEG Quality: {0}</value>
</data>
<data name="CLI_ConfigPngInterlace" xml:space="preserve">
<value> PNG Interlace: {0}</value>
</data>
<data name="CLI_ConfigTiffCompress" xml:space="preserve">
<value> TIFF Compress: {0}</value>
</data>
<data name="CLI_ConfigFilenameFormat" xml:space="preserve">
<value> Filename Format: {0}</value>
</data>
<data name="CLI_ConfigCustomSize" xml:space="preserve">
<value>Custom Size:</value>
</data>
<data name="CLI_ConfigWidth" xml:space="preserve">
<value> Width: {0} {1}</value>
</data>
<data name="CLI_ConfigHeight" xml:space="preserve">
<value> Height: {0} {1}</value>
</data>
<data name="CLI_ConfigFitMode" xml:space="preserve">
<value> Fit Mode: {0}</value>
</data>
<data name="CLI_ConfigPresetSizes" xml:space="preserve">
<value>Preset Sizes: (* = currently selected)</value>
</data>
<data name="CLI_ConfigPresetSizeFormat" xml:space="preserve">
<value> [{0}]{1} {2}: {3}x{4} {5} ({6})</value>
</data>
<data name="CLI_ConfigCustomSelected" xml:space="preserve">
<value> [Custom]* {0}x{1} {2} ({3})</value>
</data>
<!-- CLI Usage help -->
<data name="CLI_UsageTitle" xml:space="preserve">
<value>ImageResizer - PowerToys Image Resizer CLI</value>
</data>
<data name="CLI_UsageLine" xml:space="preserve">
<value>Usage: PowerToys.ImageResizerCLI.exe [options] [files...]</value>
</data>
<data name="CLI_UsageOptions" xml:space="preserve">
<value>Options:</value>
</data>
<data name="CLI_UsageExamples" xml:space="preserve">
<value>Examples:</value>
</data>
<data name="CLI_UsageExampleHelp" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --help</value>
</data>
<data name="CLI_UsageExampleDimensions" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --width 800 --height 600 image.jpg</value>
</data>
<data name="CLI_UsageExamplePercent" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe -w 50 -h 50 -u Percent *.jpg</value>
</data>
<data name="CLI_UsageExamplePreset" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" photo.png</value>
</data>
<!-- CLI Option Descriptions -->
<data name="CLI_Option_Destination" xml:space="preserve">
<value>Set destination directory</value>
</data>
<data name="CLI_Option_FileName" xml:space="preserve">
<value>Set output filename format (%1=original name, %2=size name)</value>
</data>
<data name="CLI_Option_Files" xml:space="preserve">
<value>Image files to resize</value>
</data>
<data name="CLI_Option_Fit" xml:space="preserve">
<value>Set fit mode (Fill, Fit, Stretch)</value>
</data>
<data name="CLI_Option_Height" xml:space="preserve">
<value>Set height</value>
</data>
<data name="CLI_Option_Help" xml:space="preserve">
<value>Show help information</value>
</data>
<data name="CLI_Option_IgnoreOrientation" xml:space="preserve">
<value>Ignore image orientation</value>
</data>
<data name="CLI_Option_KeepDateModified" xml:space="preserve">
<value>Keep original date modified</value>
</data>
<data name="CLI_Option_Quality" xml:space="preserve">
<value>Set JPEG quality level (1-100)</value>
</data>
<data name="CLI_Option_Replace" xml:space="preserve">
<value>Replace original files</value>
</data>
<data name="CLI_Option_ShowConfig" xml:space="preserve">
<value>Show current configuration</value>
</data>
<data name="CLI_Option_ShrinkOnly" xml:space="preserve">
<value>Only shrink images, don't enlarge</value>
</data>
<data name="CLI_Option_RemoveMetadata" xml:space="preserve">
<value>Remove metadata from resized images</value>
</data>
<data name="CLI_Option_Size" xml:space="preserve">
<value>Use preset size by index (0-based)</value>
</data>
<data name="CLI_Option_Unit" xml:space="preserve">
<value>Set unit (Pixel, Percent, Inch, Centimeter)</value>
</data>
<data name="CLI_Option_Width" xml:space="preserve">
<value>Set width</value>
</data>
</root>

View File

@@ -15,7 +15,6 @@ using System.IO.Abstractions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Windows.Media.Imaging;
@@ -43,7 +42,6 @@ namespace ImageResizer.Properties
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);

View File

@@ -11,48 +11,6 @@
using std::conditional_t;
using std::regex_error;
/// <summary>
/// Sanitizes the input string by replacing non-breaking spaces with regular spaces and
/// normalizes it to Unicode NFC (precomposed) form.
/// </summary>
/// <param name="input">The input wide string to sanitize and normalize. If empty, it is
/// returned unchanged.</param>
/// <returns>A new std::wstring containing the sanitized and NFC-normalized form of the
/// input. If normalization fails, the function returns the sanitized string (with non-
/// breaking spaces replaced) as-is.</returns>
static std::wstring SanitizeAndNormalize(const std::wstring& input)
{
if (input.empty())
{
return input;
}
std::wstring sanitized = input;
// Replace non-breaking spaces (0xA0) with regular spaces (0x20).
std::replace(sanitized.begin(), sanitized.end(), L'\u00A0', L' ');
// Normalize to NFC (Precomposed).
// Get the size needed for the normalized string, including null terminator.
int size = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
if (size <= 0)
{
return sanitized; // Return unaltered if normalization fails.
}
// Perform the normalization.
std::wstring normalized;
normalized.resize(size);
NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], size);
// Remove the explicit null terminator added by NormalizeString.
if (!normalized.empty() && normalized.back() == L'\0')
{
normalized.pop_back();
}
return normalized;
}
IFACEMETHODIMP_(ULONG)
CPowerRenameRegEx::AddRef()
{
@@ -136,20 +94,18 @@ IFACEMETHODIMP CPowerRenameRegEx::PutSearchTerm(_In_ PCWSTR searchTerm, bool for
HRESULT hr = S_OK;
if (searchTerm)
{
std::wstring normalizedSearchTerm = SanitizeAndNormalize(searchTerm);
CSRWExclusiveAutoLock lock(&m_lock);
if (m_searchTerm == nullptr || lstrcmp(normalizedSearchTerm.c_str(), m_searchTerm) != 0)
if (m_searchTerm == nullptr || lstrcmp(searchTerm, m_searchTerm) != 0)
{
changed = true;
CoTaskMemFree(m_searchTerm);
if (normalizedSearchTerm.empty())
if (lstrcmp(searchTerm, L"") == 0)
{
m_searchTerm = NULL;
}
else
{
hr = SHStrDup(normalizedSearchTerm.c_str(), &m_searchTerm);
hr = SHStrDup(searchTerm, &m_searchTerm);
}
}
}
@@ -282,19 +238,17 @@ IFACEMETHODIMP CPowerRenameRegEx::PutReplaceTerm(_In_ PCWSTR replaceTerm, bool f
HRESULT hr = S_OK;
if (replaceTerm)
{
std::wstring normalizedReplaceTerm = SanitizeAndNormalize(replaceTerm);
CSRWExclusiveAutoLock lock(&m_lock);
if (m_replaceTerm == nullptr || lstrcmp(normalizedReplaceTerm.c_str(), m_RawReplaceTerm.c_str()) != 0)
if (m_replaceTerm == nullptr || lstrcmp(replaceTerm, m_RawReplaceTerm.c_str()) != 0)
{
changed = true;
CoTaskMemFree(m_replaceTerm);
m_RawReplaceTerm = normalizedReplaceTerm;
m_RawReplaceTerm = replaceTerm;
if ((m_flags & RandomizeItems) || (m_flags & EnumerateItems))
hr = _OnEnumerateOrRandomizeItemsChanged();
else
hr = SHStrDup(normalizedReplaceTerm.c_str(), &m_replaceTerm);
hr = SHStrDup(replaceTerm, &m_replaceTerm);
}
}
@@ -443,10 +397,7 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
{
return hr;
}
std::wstring normalizedSource = SanitizeAndNormalize(source);
std::wstring res = normalizedSource;
std::wstring res = source;
try
{
// TODO: creating the regex could be costly. May want to cache this.
@@ -487,8 +438,9 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
}
}
std::wstring sourceToUse = normalizedSource;
std::wstring sourceToUse;
sourceToUse.reserve(MAX_PATH);
sourceToUse = source;
std::wstring searchTerm(m_searchTerm);
std::wstring replaceTerm;
@@ -584,7 +536,7 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
replaceTerm = regex_replace(replaceTerm, zeroGroupRegex, L"$1$$$0");
replaceTerm = regex_replace(replaceTerm, otherGroupsRegex, L"$1$0$4");
res = RegexReplaceDispatch[_useBoostLib](sourceToUse, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, isCaseInsensitive);
res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, isCaseInsensitive);
// Use regex search to determine if a match exists. This is the basis for incrementing
// the counter.
@@ -717,17 +669,17 @@ PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() cons
{
if (m_flags & MetadataSourceXMP)
return PowerRenameLib::MetadataType::XMP;
// Default to EXIF
return PowerRenameLib::MetadataType::EXIF;
}
// Interface method implementation
// Interface method implementation
IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType)
{
if (metadataType == nullptr)
return E_POINTER;
*metadataType = _GetMetadataTypeFromFlags();
return S_OK;
}
@@ -737,3 +689,5 @@ PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
{
return _GetMetadataTypeFromFlags();
}

View File

@@ -647,54 +647,6 @@ TEST_METHOD(VerifyCounterIncrementsWhenResultIsUnchanged)
CoTaskMemFree(result);
}
// Helper function to verify normalization behavior.
void VerifyNormalizationHelper(DWORD flags)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
// 1. Unicode Normalization: NFD source with NFC search term.
PWSTR result = nullptr;
unsigned long index = 0;
// Source: "Test" + U+0438 (Cyrillic small letter i) + U+0306 (combining breve).
std::wstring sourceNFD = L"Test\u0438\u0306";
// Search: "Test" + U+0438 (Cyrillic small letter i with breve).
std::wstring searchNFC = L"Test\u0439";
// A match should occur despite different normalization forms.
Assert::IsTrue(renameRegEx->PutSearchTerm(searchNFC.c_str()) == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"Match") == S_OK);
Assert::IsTrue(renameRegEx->Replace(sourceNFD.c_str(), &result, index) == S_OK);
Assert::AreEqual(L"Match", result, L"Failed to match NFD source with NFC search term.");
CoTaskMemFree(result);
// 2. Whitespace Normalization: test non-breaking space versus regular space.
result = nullptr;
index = 0;
// Source: "Hello" + non-breaking space + "World".
std::wstring sourceNBSP = L"Hello\u00A0World";
// Search: "Hello" + regular space + "World".
std::wstring searchSpace = L"Hello World";
Assert::IsTrue(renameRegEx->PutSearchTerm(searchSpace.c_str()) == S_OK);
Assert::IsTrue(renameRegEx->Replace(sourceNBSP.c_str(), &result, index) == S_OK);
Assert::AreEqual(L"Match", result, L"Failed to match non-breaking space source with regular space search term.");
CoTaskMemFree(result);
}
TEST_METHOD(VerifyUnicodeAndWhitespaceNormalizationSimpleSearch)
{
VerifyNormalizationHelper(0);
}
TEST_METHOD(VerifyUnicodeAndWhitespaceNormalizationRegex)
{
VerifyNormalizationHelper(UseRegularExpressions);
}
#ifndef TESTS_PARTIAL
};
}