mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 16:36:40 +01:00
Compare commits
7 Commits
copilot/su
...
shawn/fixI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04a59d0c8 | ||
|
|
673cd5aba3 | ||
|
|
97997035f7 | ||
|
|
59962ffd3a | ||
|
|
3f106344b3 | ||
|
|
ab531b2620 | ||
|
|
48e95caf39 |
3
.github/actions/spell-check/allow/code.txt
vendored
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,6 +330,9 @@ HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
# Unicode
|
||||
precomposed
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
|
||||
"PowerToys.ImageResizer.exe",
|
||||
"PowerToys.ImageResizer.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
||||
"PowerToys.ImageResizerExt.dll",
|
||||
"PowerToys.ImageResizerContextMenu.dll",
|
||||
"ImageResizerContextMenuPackage.msix",
|
||||
|
||||
@@ -444,6 +444,10 @@ _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
|
||||
|
||||
@@ -459,6 +459,10 @@
|
||||
<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">
|
||||
|
||||
93
doc/devdocs/cli-conventions.md
Normal file
93
doc/devdocs/cli-conventions.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 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`).
|
||||
@@ -39,7 +39,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
var softlineBreak = new Markdig.Extensions.Hardlines.SoftlineBreakAsHardlineExtension();
|
||||
|
||||
MarkdownPipelineBuilder pipelineBuilder;
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics();
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics().DisableHtml();
|
||||
pipelineBuilder.Extensions.Add(extension);
|
||||
pipelineBuilder.Extensions.Add(softlineBreak);
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ 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);
|
||||
|
||||
|
||||
@@ -49,5 +49,8 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,24 @@ 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);
|
||||
@@ -107,116 +125,97 @@ namespace Awake
|
||||
Bridge.GetPwrCapabilities(out _powerCapabilities);
|
||||
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
|
||||
|
||||
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;
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -264,6 +263,7 @@ namespace Awake
|
||||
if (pid == 0 && !useParentPid)
|
||||
{
|
||||
Logger.LogInfo("No PID specified. Allocating console...");
|
||||
Bridge.FreeConsole();
|
||||
AllocateLocalConsole();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.CommandLine.Invocation;
|
||||
|
||||
using FancyZonesCLI;
|
||||
using FancyZonesCLI.CommandLine;
|
||||
using FancyZonesCLI.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
@@ -24,12 +26,14 @@ 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($"Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.{Environment.NewLine}");
|
||||
context.Console.Error.Write($"{Properties.Resources.error_fancyzones_not_running}{Environment.NewLine}");
|
||||
context.ExitCode = 1;
|
||||
LogTelemetry(successful: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,6 +41,7 @@ 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}");
|
||||
@@ -52,6 +57,28 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetActiveLayoutCommand()
|
||||
: base("get-active-layout", "Show currently active layout")
|
||||
: base("get-active-layout", Properties.Resources.cmd_get_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("Could not get current monitor information.");
|
||||
throw new InvalidOperationException(Properties.Resources.get_active_layout_no_monitor_info);
|
||||
}
|
||||
|
||||
// Read applied layouts.
|
||||
@@ -36,11 +36,11 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
|
||||
|
||||
if (appliedLayouts.AppliedLayouts == null)
|
||||
{
|
||||
return "No layouts configured.";
|
||||
return Properties.Resources.get_active_layout_no_layouts;
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
|
||||
sb.AppendLine($"\n{Properties.Resources.get_active_layout_header}\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(" No layout applied");
|
||||
sb.AppendLine(Properties.Resources.get_active_layout_no_layout);
|
||||
}
|
||||
|
||||
if (i < editorParams.Monitors.Count - 1)
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetHotkeysCommand()
|
||||
: base("get-hotkeys", "List all layout hotkeys")
|
||||
: base("get-hotkeys", Properties.Resources.cmd_get_hotkeys)
|
||||
{
|
||||
AddAlias("hk");
|
||||
}
|
||||
@@ -26,12 +26,12 @@ internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
|
||||
|
||||
if (hotkeys.LayoutHotkeys == null || hotkeys.LayoutHotkeys.Count == 0)
|
||||
{
|
||||
return "No hotkeys configured.";
|
||||
return Properties.Resources.get_hotkeys_no_hotkeys;
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("=== Layout Hotkeys ===\n");
|
||||
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
|
||||
sb.AppendLine($"{Properties.Resources.get_hotkeys_header}\n");
|
||||
sb.AppendLine($"{Properties.Resources.get_hotkeys_instruction}\n");
|
||||
|
||||
foreach (var hotkey in hotkeys.LayoutHotkeys.OrderBy(h => h.Key))
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetLayoutsCommand()
|
||||
: base("get-layouts", "List available layouts")
|
||||
: base("get-layouts", Properties.Resources.cmd_get_layouts)
|
||||
{
|
||||
AddAlias("ls");
|
||||
}
|
||||
@@ -61,7 +61,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
|
||||
|
||||
if (customLayouts.CustomLayouts != null)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.CustomLayouts.Count} total) ===");
|
||||
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_layouts_custom_header, customLayouts.CustomLayouts.Count));
|
||||
|
||||
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 Note: Canvas layout preview is approximate.");
|
||||
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
|
||||
sb.AppendLine($"\n {Properties.Resources.get_layouts_canvas_note}");
|
||||
sb.AppendLine($" {Properties.Resources.get_layouts_canvas_detail}");
|
||||
}
|
||||
|
||||
if (i < customLayouts.CustomLayouts.Count - 1)
|
||||
@@ -102,7 +102,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
|
||||
sb.AppendLine($"\n{Properties.Resources.get_layouts_usage}");
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetMonitorsCommand()
|
||||
: base("get-monitors", "List monitors and FancyZones metadata")
|
||||
: base("get-monitors", Properties.Resources.cmd_get_monitors)
|
||||
{
|
||||
AddAlias("m");
|
||||
}
|
||||
@@ -31,19 +31,19 @@ internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to read monitor information. {ex.Message}{Environment.NewLine}Note: Ensure FancyZones is running to get current monitor information.", ex);
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_error, ex.Message), ex);
|
||||
}
|
||||
|
||||
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
|
||||
{
|
||||
return "No monitors found.";
|
||||
return Properties.Resources.get_monitors_no_monitors;
|
||||
}
|
||||
|
||||
// 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(CultureInfo.InvariantCulture, $"=== Monitors ({editorParams.Monitors.Count} total) ===");
|
||||
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_header, editorParams.Monitors.Count));
|
||||
sb.AppendLine();
|
||||
|
||||
for (int i = 0; i < editorParams.Monitors.Count; i++)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
@@ -13,7 +14,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public OpenEditorCommand()
|
||||
: base("open-editor", "Launch FancyZones layout editor")
|
||||
: base("open-editor", Properties.Resources.cmd_open_editor)
|
||||
{
|
||||
AddAlias("e");
|
||||
}
|
||||
@@ -38,7 +39,7 @@ internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to request FancyZones Editor launch. {ex.Message}", ex);
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_editor_error, ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
@@ -12,7 +13,7 @@ namespace FancyZonesCLI.CommandLine.Commands;
|
||||
internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public OpenSettingsCommand()
|
||||
: base("open-settings", "Open FancyZones settings page")
|
||||
: base("open-settings", Properties.Resources.cmd_open_settings)
|
||||
{
|
||||
AddAlias("settings");
|
||||
}
|
||||
@@ -37,14 +38,14 @@ internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
|
||||
|
||||
if (process == null)
|
||||
{
|
||||
throw new InvalidOperationException("PowerToys.exe failed to start.");
|
||||
throw new InvalidOperationException(Properties.Resources.open_settings_error_not_started);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to open FancyZones Settings. {ex.Message}", ex);
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_settings_error, ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
@@ -16,11 +17,11 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
|
||||
private readonly Argument<int> _key;
|
||||
|
||||
public RemoveHotkeyCommand()
|
||||
: base("remove-hotkey", "Remove hotkey assignment")
|
||||
: base("remove-hotkey", Properties.Resources.cmd_remove_hotkey)
|
||||
{
|
||||
AddAlias("rhk");
|
||||
|
||||
_key = new Argument<int>("key", "Hotkey index (0-9)");
|
||||
_key = new Argument<int>("key", Properties.Resources.remove_hotkey_arg_key);
|
||||
AddArgument(_key);
|
||||
}
|
||||
|
||||
@@ -33,14 +34,14 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
|
||||
|
||||
if (hotkeysWrapper.LayoutHotkeys == null)
|
||||
{
|
||||
return "No hotkeys configured.";
|
||||
return Properties.Resources.remove_hotkey_no_hotkeys;
|
||||
}
|
||||
|
||||
var hotkeysList = hotkeysWrapper.LayoutHotkeys;
|
||||
var removed = hotkeysList.RemoveAll(h => h.Key == key);
|
||||
if (removed == 0)
|
||||
{
|
||||
return $"No hotkey assigned to key {key}";
|
||||
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.remove_hotkey_not_found, key);
|
||||
}
|
||||
|
||||
// Save.
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
@@ -19,12 +20,12 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
private readonly Argument<string> _layout;
|
||||
|
||||
public SetHotkeyCommand()
|
||||
: base("set-hotkey", "Assign hotkey (0-9) to a custom layout")
|
||||
: base("set-hotkey", Properties.Resources.cmd_set_hotkey)
|
||||
{
|
||||
AddAlias("shk");
|
||||
|
||||
_key = new Argument<int>("key", "Hotkey index (0-9)");
|
||||
_layout = new Argument<string>("layout", "Custom layout UUID");
|
||||
_key = new Argument<int>("key", Properties.Resources.set_hotkey_arg_key);
|
||||
_layout = new Argument<string>("layout", Properties.Resources.set_hotkey_arg_layout);
|
||||
|
||||
AddArgument(_key);
|
||||
AddArgument(_layout);
|
||||
@@ -38,7 +39,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
|
||||
if (key < 0 || key > 9)
|
||||
{
|
||||
throw new InvalidOperationException("Key must be between 0 and 9.");
|
||||
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
|
||||
}
|
||||
|
||||
// Editor only allows assigning hotkeys to existing custom layouts.
|
||||
@@ -59,7 +60,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
|
||||
if (!matchedLayout.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"Layout '{layout}' is not a custom layout UUID.");
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
|
||||
}
|
||||
|
||||
string layoutName = matchedLayout.Value.Name;
|
||||
|
||||
@@ -26,14 +26,14 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
private readonly Option<bool> _all;
|
||||
|
||||
public SetLayoutCommand()
|
||||
: base("set-layout", "Set layout by UUID or template name")
|
||||
: base("set-layout", Properties.Resources.cmd_set_layout)
|
||||
{
|
||||
AddAlias("s");
|
||||
|
||||
_layoutId = new Argument<string>("layout", "Layout UUID or template type (e.g. focus, columns)");
|
||||
_layoutId = new Argument<string>("layout", Properties.Resources.set_layout_arg_layout);
|
||||
AddArgument(_layoutId);
|
||||
|
||||
_monitor = new Option<int?>(AliasesMonitor, "Apply to monitor N (1-based)");
|
||||
_monitor = new Option<int?>(AliasesMonitor, Properties.Resources.set_layout_opt_monitor);
|
||||
_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 = "Monitor index must be >= 1.";
|
||||
result.ErrorMessage = Properties.Resources.set_layout_error_monitor_index;
|
||||
}
|
||||
});
|
||||
|
||||
_all = new Option<bool>(AliasesAll, "Apply to all monitors");
|
||||
_all = new Option<bool>(AliasesAll, Properties.Resources.set_layout_opt_all);
|
||||
|
||||
AddOption(_monitor);
|
||||
AddOption(_all);
|
||||
@@ -60,7 +60,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
|
||||
if (monitor.HasValue && all)
|
||||
{
|
||||
commandResult.ErrorMessage = "Cannot specify both --monitor and --all.";
|
||||
commandResult.ErrorMessage = Properties.Resources.set_layout_error_both_options;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -97,15 +97,15 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
{
|
||||
if (all)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to all monitors.", layout);
|
||||
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_all, layout);
|
||||
}
|
||||
|
||||
if (monitor.HasValue)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor {1}.", layout, monitor.Value);
|
||||
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);
|
||||
return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_default, layout);
|
||||
}
|
||||
|
||||
private static (CustomLayouts.CustomLayoutWrapper? TargetCustomLayout, LayoutTemplates.TemplateLayoutWrapper? TargetTemplate) ResolveTargetLayout(string layout)
|
||||
@@ -127,10 +127,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
|
||||
if (!targetCustomLayout.HasValue && !targetTemplate.HasValue)
|
||||
{
|
||||
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'");
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_not_found, layout));
|
||||
}
|
||||
|
||||
return (targetCustomLayout, targetTemplate);
|
||||
@@ -197,7 +194,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($"Monitor {monitor.Value} not found. Available monitors: 1-{editorParams.Monitors.Count}");
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_monitor_not_found, monitor.Value, editorParams.Monitors.Count));
|
||||
}
|
||||
|
||||
result.Add(monitorIndex);
|
||||
@@ -250,7 +247,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
|
||||
if (newLayouts.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Internal error - no monitors to update.");
|
||||
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_monitors);
|
||||
}
|
||||
|
||||
return newLayouts;
|
||||
@@ -306,7 +303,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported custom layout type '{targetCustomLayout.Value.Type}'.");
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_unsupported_type, targetCustomLayout.Value.Type));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -329,7 +326,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
targetTemplate.Value.SensitivityRadius);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Internal error - no layout selected.");
|
||||
throw new InvalidOperationException(Properties.Resources.set_layout_error_no_layout);
|
||||
}
|
||||
|
||||
private static AppliedLayouts.AppliedLayoutsListWrapper MergeWithHistoricalLayouts(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
<AssemblyName>FancyZonesCLI</AssemblyName>
|
||||
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852;CA1863;CA1305</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -24,6 +24,22 @@
|
||||
|
||||
<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 -->
|
||||
|
||||
353
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs
generated
Normal file
353
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,353 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx
Normal file
233
src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx
Normal file
@@ -0,0 +1,233 @@
|
||||
<?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 <UUID>' 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 >= 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 + <number> 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>
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// 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;
|
||||
@@ -60,7 +61,7 @@ internal static class EditorParametersRefresh
|
||||
var finalParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
if (finalParams.Monitors == null || finalParams.Monitors.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not get current monitor information (timed out after {maxWaitMilliseconds}ms waiting for '{Path.GetFileName(filePath)}').");
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.editor_params_timeout, maxWaitMilliseconds, Path.GetFileName(filePath)));
|
||||
}
|
||||
|
||||
return finalParams;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
50
src/modules/imageresizer/ImageResizerCLI/Program.cs
Normal file
50
src/modules/imageresizer/ImageResizerCLI/Program.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/modules/imageresizer/tests/Cli/CliSettingsApplierTests.cs
Normal file
320
src/modules/imageresizer/tests/Cli/CliSettingsApplierTests.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/modules/imageresizer/tests/Models/CliOptionsTests.cs
Normal file
268
src/modules/imageresizer/tests/Models/CliOptionsTests.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,27 @@ 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 =
|
||||
"Image1.jpg" + EOL +
|
||||
"Image2.jpg";
|
||||
file1 + EOL +
|
||||
file2;
|
||||
var args = new[]
|
||||
{
|
||||
"/d", "OutputDir",
|
||||
"Image3.jpg",
|
||||
file3,
|
||||
};
|
||||
|
||||
var result = ResizeBatch.FromCommandLine(
|
||||
new StringReader(standardInput),
|
||||
args);
|
||||
|
||||
CollectionAssert.AreEquivalent(new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" }, result.Files.ToArray());
|
||||
var files = result.Files.Select(Path.GetFileName).ToArray();
|
||||
CollectionAssert.AreEquivalent(new List<string> { "Test.jpg", "Test.png", "Test.gif" }, files);
|
||||
|
||||
Assert.AreEqual("OutputDir", result.DestinationDirectory);
|
||||
}
|
||||
|
||||
28
src/modules/imageresizer/ui/Cli/CliLogger.cs
Normal file
28
src/modules/imageresizer/ui/Cli/CliLogger.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
122
src/modules/imageresizer/ui/Cli/CliSettingsApplier.cs
Normal file
122
src/modules/imageresizer/ui/Cli/CliSettingsApplier.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
124
src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
Normal file
124
src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/DestinationOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/DestinationOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/FileNameOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/FileNameOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/modules/imageresizer/ui/Cli/Options/FilesArgument.cs
Normal file
17
src/modules/imageresizer/ui/Cli/Options/FilesArgument.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/FitOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/FitOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/HeightOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/HeightOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/HelpOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/HelpOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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%)")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/imageresizer/ui/Cli/Options/QualityOption.cs
Normal file
26
src/modules/imageresizer/ui/Cli/Options/QualityOption.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ReplaceOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ReplaceOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ShowConfigOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ShowConfigOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/ShrinkOnlyOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/ShrinkOnlyOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/imageresizer/ui/Cli/Options/SizeOption.cs
Normal file
26
src/modules/imageresizer/ui/Cli/Options/SizeOption.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/UnitOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/UnitOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui/Cli/Options/WidthOption.cs
Normal file
18
src/modules/imageresizer/ui/Cli/Options/WidthOption.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<NoWarn>CA1863</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -51,6 +52,7 @@
|
||||
<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>
|
||||
|
||||
261
src/modules/imageresizer/ui/Models/CliOptions.cs
Normal file
261
src/modules/imageresizer/ui/Models/CliOptions.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -39,44 +40,78 @@ namespace ImageResizer.Models
|
||||
_aiSuperResolutionService = null;
|
||||
}
|
||||
|
||||
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
|
||||
/// <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)
|
||||
{
|
||||
var batch = new ResizeBatch();
|
||||
const string pipeNamePrefix = "\\\\.\\pipe\\";
|
||||
string pipeName = null;
|
||||
|
||||
for (var i = 0; i < args?.Length; i++)
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
if (args[i] == "/d")
|
||||
{
|
||||
batch.DestinationDirectory = args[++i];
|
||||
continue;
|
||||
}
|
||||
else if (args[i].Contains(pipeNamePrefix))
|
||||
{
|
||||
pipeName = args[i].Substring(pipeNamePrefix.Length);
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.Files.Add(args[i]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(pipeName))
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
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))
|
||||
{
|
||||
batch.Files.Add(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(options.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)
|
||||
if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
|
||||
{
|
||||
while ((file = standardInput.ReadLine()) != null)
|
||||
{
|
||||
batch.Files.Add(file);
|
||||
// Convert relative paths to absolute paths
|
||||
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
|
||||
if (IsValidImagePath(absolutePath))
|
||||
{
|
||||
batch.Files.Add(absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (NamedPipeClientStream pipeClient =
|
||||
new NamedPipeClientStream(".", pipeName, PipeDirection.In))
|
||||
new NamedPipeClientStream(".", options.PipeName, PipeDirection.In))
|
||||
{
|
||||
// Connect to the pipe or wait until the pipe is available.
|
||||
pipeClient.Connect();
|
||||
@@ -88,7 +123,10 @@ namespace ImageResizer.Models
|
||||
// Display the read text to the console
|
||||
while ((file = sr.ReadLine()) != null)
|
||||
{
|
||||
batch.Files.Add(file);
|
||||
if (IsValidImagePath(file))
|
||||
{
|
||||
batch.Files.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,17 +135,26 @@ 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(
|
||||
|
||||
@@ -716,5 +716,437 @@ 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] <files>.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,4 +347,156 @@
|
||||
<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>
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
|
||||
@@ -42,6 +43,7 @@ namespace ImageResizer.Properties
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
WriteIndented = true,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||
};
|
||||
|
||||
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
|
||||
|
||||
@@ -130,6 +130,7 @@ namespace Peek.FilePreviewer.Previewers
|
||||
}
|
||||
else if (isMarkdown)
|
||||
{
|
||||
IsDevFilePreview = false;
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,48 @@
|
||||
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()
|
||||
{
|
||||
@@ -94,18 +136,20 @@ 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(searchTerm, m_searchTerm) != 0)
|
||||
if (m_searchTerm == nullptr || lstrcmp(normalizedSearchTerm.c_str(), m_searchTerm) != 0)
|
||||
{
|
||||
changed = true;
|
||||
CoTaskMemFree(m_searchTerm);
|
||||
if (lstrcmp(searchTerm, L"") == 0)
|
||||
if (normalizedSearchTerm.empty())
|
||||
{
|
||||
m_searchTerm = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
hr = SHStrDup(searchTerm, &m_searchTerm);
|
||||
hr = SHStrDup(normalizedSearchTerm.c_str(), &m_searchTerm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,17 +282,19 @@ 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(replaceTerm, m_RawReplaceTerm.c_str()) != 0)
|
||||
if (m_replaceTerm == nullptr || lstrcmp(normalizedReplaceTerm.c_str(), m_RawReplaceTerm.c_str()) != 0)
|
||||
{
|
||||
changed = true;
|
||||
CoTaskMemFree(m_replaceTerm);
|
||||
m_RawReplaceTerm = replaceTerm;
|
||||
m_RawReplaceTerm = normalizedReplaceTerm;
|
||||
|
||||
if ((m_flags & RandomizeItems) || (m_flags & EnumerateItems))
|
||||
hr = _OnEnumerateOrRandomizeItemsChanged();
|
||||
else
|
||||
hr = SHStrDup(replaceTerm, &m_replaceTerm);
|
||||
hr = SHStrDup(normalizedReplaceTerm.c_str(), &m_replaceTerm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +443,10 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
{
|
||||
return hr;
|
||||
}
|
||||
std::wstring res = source;
|
||||
|
||||
std::wstring normalizedSource = SanitizeAndNormalize(source);
|
||||
|
||||
std::wstring res = normalizedSource;
|
||||
try
|
||||
{
|
||||
// TODO: creating the regex could be costly. May want to cache this.
|
||||
@@ -438,9 +487,8 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring sourceToUse;
|
||||
std::wstring sourceToUse = normalizedSource;
|
||||
sourceToUse.reserve(MAX_PATH);
|
||||
sourceToUse = source;
|
||||
|
||||
std::wstring searchTerm(m_searchTerm);
|
||||
std::wstring replaceTerm;
|
||||
@@ -536,7 +584,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](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, isCaseInsensitive);
|
||||
res = RegexReplaceDispatch[_useBoostLib](sourceToUse, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, isCaseInsensitive);
|
||||
|
||||
// Use regex search to determine if a match exists. This is the basis for incrementing
|
||||
// the counter.
|
||||
@@ -669,17 +717,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;
|
||||
}
|
||||
@@ -689,5 +737,3 @@ PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
|
||||
{
|
||||
return _GetMetadataTypeFromFlags();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -647,6 +647,54 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -794,6 +794,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
endpoint = string.Empty;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
// If endpoint is required but not provided, use placeholder.
|
||||
endpoint = GetEndpointPlaceholder(serviceKind);
|
||||
}
|
||||
|
||||
// For endpoint-based services, keep empty if the user didn't provide a value.
|
||||
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
|
||||
|
||||
Reference in New Issue
Block a user