mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 18:57:19 +02:00
CmdPal/Clipboard History: Ctrl+O to open links (#42115)
Basically #42109, but with tests added, and no duplicated OpenUrl command. Closes #42108. Tests pass. Tested with both copy as default and paste as default, and things show up as expected.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
|
||||
internal static class UrlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates if a string is a valid URL or file path
|
||||
/// </summary>
|
||||
/// <param name="url">The string to validate</param>
|
||||
/// <returns>True if the string is a valid URL or file path, false otherwise</returns>
|
||||
internal static bool IsValidUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trim whitespace for validation
|
||||
url = url.Trim();
|
||||
|
||||
// URLs should not contain newlines
|
||||
if (url.Contains('\n', StringComparison.Ordinal) || url.Contains('\r', StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a valid file path (local or network)
|
||||
if (IsValidFilePath(url))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!url.Contains('.', StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// eg: 'com', 'org'. We don't think it's a valid url.
|
||||
// This can simplify the logic of checking if the url is valid.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a URL or file path by adding appropriate schema if none is present
|
||||
/// </summary>
|
||||
/// <param name="url">The URL or file path to normalize</param>
|
||||
/// <returns>Normalized URL or file path with schema</returns>
|
||||
internal static string NormalizeUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
url = url.Trim();
|
||||
|
||||
// If it's a valid file path, convert to file:// URI
|
||||
if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert to file URI (path is already absolute since we only accept absolute paths)
|
||||
return new Uri(url).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If conversion fails, return original
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||
{
|
||||
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url = "https://" + url;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string represents a valid file path (local or network)
|
||||
/// </summary>
|
||||
/// <param name="path">The string to check</param>
|
||||
/// <returns>True if the string is a valid file path, false otherwise</returns>
|
||||
private static bool IsValidFilePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check for UNC paths (network paths starting with \\)
|
||||
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Basic UNC path validation: \\server\share or \\server\share\path
|
||||
var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length >= 2; // At minimum: server and share
|
||||
}
|
||||
|
||||
// Check for drive letters (C:\ or C:)
|
||||
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,6 @@
|
||||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
@@ -16,4 +11,6 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
internal static class KeyChords
|
||||
{
|
||||
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
|
||||
|
||||
internal static KeyChord OpenUrl { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.O);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
private readonly CommandContextItem _deleteContextMenuItem;
|
||||
private readonly CommandContextItem? _pasteCommand;
|
||||
private readonly CommandContextItem? _copyCommand;
|
||||
private readonly CommandContextItem? _openUrlCommand;
|
||||
private readonly Lazy<Details> _lazyDetails;
|
||||
|
||||
public override IDetails? Details
|
||||
@@ -72,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
|
||||
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
|
||||
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
|
||||
|
||||
// Check if the text content is a valid URL and add OpenUrl command
|
||||
if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
|
||||
{
|
||||
var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
|
||||
_openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenUrl,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_openUrlCommand = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pasteCommand = null;
|
||||
_copyCommand = null;
|
||||
_openUrlCommand = null;
|
||||
}
|
||||
|
||||
RefreshCommands();
|
||||
@@ -99,12 +115,7 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
{
|
||||
case PrimaryAction.Paste:
|
||||
Command = _pasteCommand?.Command;
|
||||
MoreCommands =
|
||||
[
|
||||
_copyCommand!,
|
||||
new Separator(),
|
||||
_deleteContextMenuItem,
|
||||
];
|
||||
MoreCommands = BuildMoreCommands(_copyCommand);
|
||||
|
||||
if (_item.IsText)
|
||||
{
|
||||
@@ -124,12 +135,7 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
case PrimaryAction.Copy:
|
||||
default:
|
||||
Command = _copyCommand?.Command;
|
||||
MoreCommands =
|
||||
[
|
||||
_pasteCommand!,
|
||||
new Separator(),
|
||||
_deleteContextMenuItem,
|
||||
];
|
||||
MoreCommands = BuildMoreCommands(_pasteCommand);
|
||||
|
||||
if (_item.IsText)
|
||||
{
|
||||
@@ -148,6 +154,26 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
}
|
||||
}
|
||||
|
||||
private IContextItem[] BuildMoreCommands(CommandContextItem? firstCommand)
|
||||
{
|
||||
var commands = new List<IContextItem>();
|
||||
|
||||
if (firstCommand != null)
|
||||
{
|
||||
commands.Add(firstCommand);
|
||||
}
|
||||
|
||||
if (_openUrlCommand != null)
|
||||
{
|
||||
commands.Add(_openUrlCommand);
|
||||
}
|
||||
|
||||
commands.Add(new Separator());
|
||||
commands.Add(_deleteContextMenuItem);
|
||||
|
||||
return commands.ToArray();
|
||||
}
|
||||
|
||||
private Details CreateDetails()
|
||||
{
|
||||
IDetailsElement[] metadata =
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests")]
|
||||
@@ -183,4 +183,7 @@
|
||||
<data name="settings_primary_action_copy" xml:space="preserve">
|
||||
<value>Copy to Clipboard</value>
|
||||
</data>
|
||||
<data name="open_url_command_name" xml:space="preserve">
|
||||
<value>Open URL</value>
|
||||
</data>
|
||||
</root>
|
||||
Reference in New Issue
Block a user