Compare commits

...

39 Commits

Author SHA1 Message Date
Yu Leng
5164a1f861 Improve robust 2025-06-05 16:00:18 +08:00
Yu Leng
553c7eb0c7 Remove unused code 2025-06-05 15:46:37 +08:00
Yu Leng
5323dea50d Remove unused code 2025-06-05 15:39:20 +08:00
Yu Leng
d5e01fc924 merge main 2025-06-05 15:18:50 +08:00
Yu Leng (from Dev Box)
b631615c54 Fix bug 2025-05-08 10:08:08 +08:00
Yu Leng (from Dev Box)
7bbe1f68d4 Remove unused code 2025-05-08 09:47:47 +08:00
Yu Leng (from Dev Box)
897a71ecc3 Remove unused code 2025-05-08 09:46:41 +08:00
Yu Leng (from Dev Box)
44b677b16d Merge branch 'main' into yuleng/cmdpal/bookmarks 2025-05-07 16:50:59 +08:00
Yu Leng (from Dev Box)
425b8c2cf8 update comments 2025-05-07 16:12:11 +08:00
Yu Leng (from Dev Box)
56c8b57fe5 Add folder context menu back 2025-05-07 11:16:58 +08:00
Yu Leng (from Dev Box)
b17d53c485 Merge urlCommand into shellCommand 2025-05-07 11:14:23 +08:00
Yu Leng (from Dev Box)
4a2cb8014f fix typo 2025-05-07 10:49:18 +08:00
Yu Leng (from Dev Box)
dfea4c269c Remove unused string 2025-05-07 10:42:54 +08:00
Yu Leng (from Dev Box)
0aeeebcc74 Fetch icon 2025-05-07 10:37:39 +08:00
Yu Leng (from Dev Box)
a51b222cee Merge folder and file 2025-05-07 10:16:14 +08:00
Yu Leng (from Dev Box)
48184d6240 Remove unused resource 2025-05-06 17:56:46 +08:00
Yu Leng (from Dev Box)
b825f7a0ff Remove unused todo 2025-05-06 17:50:00 +08:00
Yu Leng (from Dev Box)
242dff4a29 Remove setting page 2025-05-06 17:48:40 +08:00
Yu Leng (from Dev Box)
ae0da4ea08 simplify the logic. Just call them directly. 2025-05-06 17:45:46 +08:00
Yu Leng (from Dev Box)
9ee9824ca9 Merge branch 'main' into yuleng/cmdpal/bookmarks 2025-05-06 16:13:25 +08:00
Yu Leng (from Dev Box)
962cb4324f store 2025-04-30 18:02:58 +08:00
Yu Leng (from Dev Box)
9b25427847 merge main and add settings 2025-04-29 15:44:34 +08:00
Yu Leng (from Dev Box)
dc69a83905 Merge branch 'main' into yuleng/cmdpal/bookmarks 2025-04-29 14:40:20 +08:00
Yu Leng (from Dev Box)
d7dbe0d9c9 Fix typo 2025-04-27 15:36:35 +08:00
Yu Leng (from Dev Box)
a0827b0074 Fix typo 2025-04-27 15:28:01 +08:00
Yu Leng (from Dev Box)
0efb642ec0 Fix typo 2025-04-27 15:10:50 +08:00
Yu Leng (from Dev Box)
df252dbb5a Ok, fine. We can detect the bookmark type by ourself. No need to explicitly make customer decide it. 2025-04-27 15:05:21 +08:00
Yu Leng (from Dev Box)
8d2b3f2ec2 Merge main 2025-04-27 15:04:40 +08:00
Yu Leng (from Dev Box)
c39bda96b0 Add error msg as return 2025-04-25 12:50:19 +08:00
Yu Leng (from Dev Box)
d220051419 fix 2025-04-24 19:41:14 +08:00
Yu Leng (from Dev Box)
3db59a30b9 Fix typo issue 2025-04-24 12:14:58 +08:00
Yu Leng (from Dev Box)
681d9a26bd Add subtitle for shell command 2025-04-24 12:05:17 +08:00
Yu Leng (from Dev Box)
038ace0666 Folder not web need directoryPage and OpenInShellCommand 2025-04-24 11:59:24 +08:00
Yu Leng (from Dev Box)
84727c8b47 Remove unused field. 2025-04-24 11:13:04 +08:00
Yu Leng (from Dev Box)
923e33a067 Fix i18n issue 2025-04-24 11:02:23 +08:00
Yu Leng (from Dev Box)
9f8a238e84 Fix cmd issue 2025-04-24 10:43:42 +08:00
Yu Leng (from Dev Box)
e6abca9b73 Fix icon issues 2025-04-23 17:31:51 +08:00
Yu Leng (from Dev Box)
311b8acaa7 Change structure 2025-04-23 17:08:30 +08:00
Yu Leng (from Dev Box)
e5f9d4b643 init 2025-04-23 17:07:02 +08:00
17 changed files with 437 additions and 197 deletions

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
namespace Microsoft.CmdPal.Ext.Bookmarks;

View File

@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
namespace Microsoft.CmdPal.Ext.Bookmarks;

View File

@@ -8,10 +8,14 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Diagnostics.Utilities;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks;
@@ -23,10 +27,6 @@ public partial class BookmarksCommandProvider : CommandProvider
private Bookmarks? _bookmarks;
public static IconInfo DeleteIcon { get; private set; } = new("\uE74D"); // Delete
public static IconInfo EditIcon { get; private set; } = new("\uE70F"); // Edit
public BookmarksCommandProvider()
{
Id = "Bookmarks";
@@ -109,57 +109,23 @@ public partial class BookmarksCommandProvider : CommandProvider
private CommandItem BookmarkToCommandItem(BookmarkData bookmark)
{
ICommand command = bookmark.IsPlaceholder ?
new BookmarkPlaceholderPage(bookmark) :
new UrlCommand(bookmark);
var listItem = new CommandItem(command) { Icon = command.Icon };
List<CommandContextItem> contextMenu = [];
// Add commands for folder types
if (command is UrlCommand urlCommand)
var deleteAction = () =>
{
if (urlCommand.Type == "folder")
if (_bookmarks != null)
{
contextMenu.Add(
new CommandContextItem(new DirectoryPage(urlCommand.Url)));
ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})");
contextMenu.Add(
new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url)));
_bookmarks.Data.Remove(bookmark);
SaveAndUpdateCommands();
}
};
listItem.Subtitle = urlCommand.Url;
if (bookmark.IsPlaceholder)
{
return CreatePlaceholderCommand(bookmark, Edit_AddedCommand, deleteAction);
}
var edit = new AddBookmarkPage(bookmark) { Icon = EditIcon };
edit.AddedCommand += Edit_AddedCommand;
contextMenu.Add(new CommandContextItem(edit));
var delete = new CommandContextItem(
title: Resources.bookmarks_delete_title,
name: Resources.bookmarks_delete_name,
action: () =>
{
if (_bookmarks != null)
{
ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})");
_bookmarks.Data.Remove(bookmark);
SaveAndUpdateCommands();
}
},
result: CommandResult.KeepOpen())
{
IsCritical = true,
Icon = DeleteIcon,
};
contextMenu.Add(delete);
listItem.MoreCommands = contextMenu.ToArray();
return listItem;
return CreateShellCommand(bookmark, Edit_AddedCommand, deleteAction);
}
public override ICommandItem[] TopLevelCommands()
@@ -180,4 +146,64 @@ public partial class BookmarksCommandProvider : CommandProvider
// now, the state is just next to the exe
return System.IO.Path.Combine(directory, "bookmarks.json");
}
private static CommandItem CreatePlaceholderCommand(BookmarkData bookmark, TypedEventHandler<object, BookmarkData> addBookmarkFunc, Action deleteAction)
{
var command = new BookmarkPlaceholderPage(bookmark);
var listItem = new CommandItem(command) { Icon = command.Icon };
List<CommandContextItem> contextMenu = [];
var edit = new AddBookmarkPage(bookmark) { Icon = IconHelper.EditIcon };
edit.AddedCommand += addBookmarkFunc;
contextMenu.Add(new CommandContextItem(edit));
var delete = new CommandContextItem(
title: Resources.bookmarks_delete_title,
name: Resources.bookmarks_delete_name,
action: deleteAction,
result: CommandResult.KeepOpen())
{
IsCritical = true,
Icon = IconHelper.DeleteIcon,
};
contextMenu.Add(delete);
listItem.MoreCommands = contextMenu.ToArray();
return listItem;
}
private static CommandItem CreateShellCommand(BookmarkData bookmark, TypedEventHandler<object, BookmarkData> addBookmarkFunc, Action deleteAction)
{
var invokableCommand = new Command.ShellCommand(bookmark);
var listItem = new CommandItem(invokableCommand) { Icon = invokableCommand.Icon };
List<CommandContextItem> contextMenu = [];
if (bookmark.Type == BookmarkType.Folder)
{
contextMenu.Add(
new CommandContextItem(new DirectoryPage(bookmark.Bookmark)));
contextMenu.Add(
new CommandContextItem(new OpenInTerminalCommand(bookmark.Bookmark)));
}
listItem.Subtitle = invokableCommand.BookmarkData.Bookmark;
var edit = new AddBookmarkPage(bookmark) { Icon = IconHelper.EditIcon };
edit.AddedCommand += addBookmarkFunc;
contextMenu.Add(new CommandContextItem(edit));
var delete = new CommandContextItem(
title: Resources.bookmarks_delete_title,
name: Resources.bookmarks_delete_name,
action: deleteAction,
result: CommandResult.KeepOpen())
{
IsCritical = true,
Icon = IconHelper.DeleteIcon,
};
contextMenu.Add(delete);
listItem.MoreCommands = contextMenu.ToArray();
return listItem;
}
}

View File

@@ -4,6 +4,7 @@
using System;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,6 +18,7 @@ internal sealed partial class OpenInTerminalCommand : InvokableCommand
public OpenInTerminalCommand(string folder)
{
Name = Resources.bookmarks_open_in_terminal_name;
Icon = IconHelper.CommandIcon;
_folder = folder;
}
@@ -35,7 +37,7 @@ internal sealed partial class OpenInTerminalCommand : InvokableCommand
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
ExtensionHost.LogMessage(new LogMessage() { Message = $"Error launching Windows Terminal: {ex.Message}" });
}
return CommandResult.Dismiss();

View File

@@ -0,0 +1,96 @@
// 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.Data;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks.Command;
public sealed partial class ShellCommand : InvokableCommand
{
public BookmarkData BookmarkData { get; }
public ShellCommand(BookmarkData data)
{
BookmarkData = data;
Name = data.Name;
Icon = IconHelper.CreateIcon(data.Bookmark, data.Type, false);
}
public override CommandResult Invoke()
{
return ShellCommand.Invoke(BookmarkData.Type, BookmarkData.Bookmark);
}
public static CommandResult Invoke(BookmarkType bookmarkType, string bookmarkValue)
{
if (bookmarkType == BookmarkType.Web)
{
var uri = OpenInShellHelper.GetUri(bookmarkValue);
if (uri != null)
{
if (!OpenInShellHelper.OpenInShell(uri.ToString(), null, null, OpenInShellHelper.ShellRunAsType.None, false, out var errMsg))
{
ExtensionHost.LogMessage($"Failed to open {bookmarkValue} in shell. Ex: {errMsg}");
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
return CommandResult.Dismiss();
}
else
{
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
}
// if it's a file or folder bookmark, call them directly.
if (bookmarkType == BookmarkType.File || bookmarkType == BookmarkType.Folder)
{
if (!OpenInShellHelper.OpenInShell(bookmarkValue, null, null, OpenInShellHelper.ShellRunAsType.None, false, out var errMsg))
{
ExtensionHost.LogMessage($"Failed to open {bookmarkValue} in shell. Ex: {errMsg}");
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
return CommandResult.Dismiss();
}
// We assume all command bookmarks will follow the same format.
// For example: "python test.py" or "pwsh test.ps1"
// So, we can split the command and get the first part as the command name.
var splittedBookmarkValue = bookmarkValue.Split(" ");
if (splittedBookmarkValue.Length == 0)
{
ExtensionHost.LogMessage($"Failed to open {bookmarkValue} in shell. Empty bookmark value.");
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
if (splittedBookmarkValue.Length == 1)
{
// directly call. Because it maybe a command with no args. eg: haproxy.exe or cmd.exe
if (!OpenInShellHelper.OpenInShell(splittedBookmarkValue[0], null, null, OpenInShellHelper.ShellRunAsType.None, false, out var errMsg))
{
ExtensionHost.LogMessage($"Failed to open {bookmarkValue} in shell. Ex: {errMsg}");
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
return CommandResult.Dismiss();
}
// args = without the first part and join with space
var args = splittedBookmarkValue[1..];
if (!OpenInShellHelper.OpenInShell(splittedBookmarkValue[0], string.Join(" ", args), null, OpenInShellHelper.ShellRunAsType.None, false, out var errorMessage))
{
ExtensionHost.LogMessage($"Failed to open {bookmarkValue} in shell. Ex: {errorMessage}");
return CommandResult.ShowToast(new ToastArgs() { Message = Resources.bookmarks_command_invoke_failed_message });
}
return CommandResult.Dismiss();
}
}

View File

@@ -0,0 +1,44 @@
// 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 Microsoft.CmdPal.Ext.Bookmarks.Models;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
public static partial class BookmarkTypeHelper
{
/*
* Summary:
* If it's a valid uri, we assume it's a Web Link.
* Otherwise, we check if it's a existing folder or file.
* By default, we assume it's a command type.
*/
public static BookmarkType GetBookmarkTypeFromValue(string bookmark)
{
// judge if the bookmark is a url
var uri = OpenInShellHelper.GetUri(bookmark);
if (uri?.Scheme == Uri.UriSchemeHttp || uri?.Scheme == Uri.UriSchemeHttps)
{
return BookmarkType.Web;
}
// judge if the bookmark is a existing folder
if (System.IO.Directory.Exists(bookmark))
{
return BookmarkType.Folder;
}
// ok, fine. Actually, it's also have the possibility to be a shell command.
// Such as 'test.cmd' or 'test.ps1'. Try to catch this case.
if (System.IO.File.Exists(bookmark))
{
return BookmarkType.File;
}
// by default. we assume it's a command type
return BookmarkType.Command;
}
}

View File

@@ -0,0 +1,95 @@
// 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 Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
public static class IconHelper
{
public static IconInfo DeleteIcon { get; private set; } = new("\uE74D"); // Delete
public static IconInfo EditIcon { get; private set; } = new("\uE70F"); // Edit
public static IconInfo UrlIcon { get; private set; } = new("🔗"); // Web
public static IconInfo FolderIcon { get; private set; } = new("📁"); // Folder
public static IconInfo FileIcon { get; private set; } = new("📄"); // File
public static IconInfo CommandIcon { get; private set; } = new("\uE756"); // Command
public static IconInfo GetIconByType(BookmarkType type)
{
return type switch
{
BookmarkType.Web => UrlIcon,
BookmarkType.Folder => FolderIcon,
BookmarkType.File => FileIcon,
BookmarkType.Command => CommandIcon,
_ => UrlIcon, // Default icon
};
}
public static IconInfo CreateIcon(string bookmark, BookmarkType bookmarkType, bool isPlaceholder)
{
// In some case, we want to use placeholder, but we can still get the favicon.
// eg: "https://google.com?q={query}"
if (bookmarkType == BookmarkType.Web)
{
// Get the base url up to the first placeholder
var placeholderIndex = bookmark.IndexOf('{');
var baseString = placeholderIndex > 0 ? bookmark.Substring(0, placeholderIndex) : bookmark;
try
{
var uri = OpenInShellHelper.GetUri(baseString);
if (uri != null)
{
var hostname = uri.Host;
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
return new IconInfo(faviconUrl);
}
}
catch (UriFormatException ex)
{
ExtensionHost.LogMessage(new LogMessage() { Message = $"Create Icon failed. {ex}: {ex.Message}" });
}
}
if (isPlaceholder)
{
// If it's a placeholder bookmark, we don't need to get the icon.
// Just use the default icon.
return GetIconByType(bookmarkType);
}
if (bookmarkType == BookmarkType.File || bookmarkType == BookmarkType.Folder)
{
// try to get the file icon first. If not, use the default file icon.
try
{
// To be honest, I don't like to block thread.
// We need to refactor in the future.
// But now, it's ok. Only image file will trigger the async path.
var stream = ThumbnailHelper.GetThumbnail(bookmark).Result;
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
return new IconInfo(data, data);
}
}
catch
{
ExtensionHost.LogMessage(new LogMessage() { Message = $"Create Icon failed. {bookmarkType}: {bookmark}" });
}
}
// If we can't get the icon, just use the default icon.
return GetIconByType(bookmarkType);
}
}

View File

@@ -0,0 +1,68 @@
// 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.ComponentModel;
using System.Diagnostics;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
// TODO: ok fine, we have two OpenInShellHelper classes, one in CmdPal and one in System.
// We should probably merge them into one. and move it to a common location.
public static partial class OpenInShellHelper
{
public static bool OpenInShell(string path, string? arguments, string? workingDir, ShellRunAsType runAs, bool runWithHiddenWindow, out string errorMessage)
{
errorMessage = string.Empty;
using var process = new Process();
process.StartInfo.FileName = path;
process.StartInfo.WorkingDirectory = string.IsNullOrWhiteSpace(workingDir) ? string.Empty : workingDir;
process.StartInfo.Arguments = string.IsNullOrWhiteSpace(arguments) ? string.Empty : arguments;
process.StartInfo.WindowStyle = runWithHiddenWindow ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal;
process.StartInfo.UseShellExecute = true;
if (runAs == ShellRunAsType.Administrator)
{
process.StartInfo.Verb = "RunAs";
}
else if (runAs == ShellRunAsType.OtherUser)
{
process.StartInfo.Verb = "RunAsUser";
}
try
{
process.Start();
return true;
}
catch (Win32Exception ex)
{
ExtensionHost.LogMessage(new LogMessage() { Message = $"Unable to open {path}: {ex.Message}" });
errorMessage = ex.Message;
return false;
}
}
public static Uri? GetUri(string url)
{
Uri? uri;
if (!Uri.TryCreate(url, UriKind.Absolute, out uri))
{
if (!Uri.TryCreate("https://" + url, UriKind.Absolute, out uri))
{
return null;
}
}
return uri;
}
public enum ShellRunAsType
{
None,
Administrator,
OtherUser,
}
}

View File

@@ -4,7 +4,7 @@
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks;
namespace Microsoft.CmdPal.Ext.Bookmarks.Models;
public class BookmarkData
{
@@ -12,7 +12,8 @@ public class BookmarkData
public string Bookmark { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
[JsonConverter(typeof(JsonStringEnumConverter<BookmarkType>))]
public BookmarkType Type { get; set; } = BookmarkType.Web;
[JsonIgnore]
public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}');

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Bookmarks.Models;
public enum BookmarkType
{
Web,
File,
Folder,
Command,
}

View File

@@ -2,10 +2,10 @@
// 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;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -23,6 +23,7 @@ internal sealed partial class AddBookmarkForm : FormContent
_bookmark = bookmark;
var name = _bookmark?.Name ?? string.Empty;
var url = _bookmark?.Bookmark ?? string.Empty;
TemplateJson = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
@@ -33,25 +34,25 @@ internal sealed partial class AddBookmarkForm : FormContent
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"label": {{JsonSerializer.Serialize(Resources.bookmarks_form_name_label, BookmarkSerializationContext.Default.String)}},
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
"errorMessage": "{{JsonSerializer.Serialize(Resources.bookmarks_form_name_required, BookmarkSerializationContext.Default.String)}}"
},
{
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"label": {{JsonSerializer.Serialize(Resources.bookmarks_form_bookmark_label, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
"errorMessage": "{{JsonSerializer.Serialize(Resources.bookmarks_form_bookmark_required, BookmarkSerializationContext.Default.String)}}"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_save}}",
"title": {{JsonSerializer.Serialize(Resources.bookmarks_form_save, BookmarkSerializationContext.Default.String)}},
"data": {
"name": "name",
"bookmark": "bookmark"
@@ -73,33 +74,11 @@ internal sealed partial class AddBookmarkForm : FormContent
// get the name and url out of the values
var formName = formInput["name"] ?? string.Empty;
var formBookmark = formInput["bookmark"] ?? string.Empty;
var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}');
// Determine the type of the bookmark
string bookmarkType;
if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
bookmarkType = "web";
}
else if (File.Exists(formBookmark.ToString()))
{
bookmarkType = "file";
}
else if (Directory.Exists(formBookmark.ToString()))
{
bookmarkType = "folder";
}
else
{
// Default to web if we can't determine the type
bookmarkType = "web";
}
var updated = _bookmark ?? new BookmarkData();
updated.Name = formName.ToString();
updated.Bookmark = formBookmark.ToString();
updated.Type = bookmarkType;
updated.Type = BookmarkTypeHelper.GetBookmarkTypeFromValue(formBookmark.ToString());
AddedCommand?.Invoke(this, updated);
return CommandResult.GoHome();

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -7,12 +7,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Bookmarks.Command;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Bookmarks;
@@ -24,10 +26,14 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
private readonly string _bookmark = string.Empty;
private BookmarkType _bookmarkType;
// TODO pass in an array of placeholders
public BookmarkPlaceholderForm(string name, string url, string type)
public BookmarkPlaceholderForm(string name, string url, BookmarkType bookmarkType)
{
_bookmark = url;
_bookmarkType = bookmarkType;
var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
var matches = r.Matches(url);
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
@@ -38,10 +44,10 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
{
"type": "Input.Text",
"style": "text",
"id": "{{p}}",
"label": "{{p}}",
"id": "{{JsonSerializer.Serialize(p, BookmarkSerializationContext.Default.String)}}",
"label": "{{JsonSerializer.Serialize(p, BookmarkSerializationContext.Default.String)}}",
"isRequired": true,
"errorMessage": "{{errorMessage}}"
"errorMessage": "{{JsonSerializer.Serialize(errorMessage, BookmarkSerializationContext.Default.String)}}"
}
""";
}).ToList();
@@ -59,7 +65,7 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_open}}",
"title": {{JsonSerializer.Serialize(Resources.bookmarks_form_open, BookmarkSerializationContext.Default.String)}},
"data": {
"placeholder": "placeholder"
}
@@ -90,15 +96,7 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
try
{
var uri = UrlCommand.GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
return ShellCommand.Invoke(_bookmarkType, target);
}
catch (Exception ex)
{

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,10 +20,10 @@ internal sealed partial class BookmarkPlaceholderPage : ContentPage
{
}
public BookmarkPlaceholderPage(string name, string url, string type)
public BookmarkPlaceholderPage(string name, string url, BookmarkType type)
{
Name = name;
Icon = new IconInfo(UrlCommand.IconFromUrl(url, type));
Icon = IconHelper.CreateIcon(url, type, true);
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
}
}

View File

@@ -78,6 +78,15 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Unable to call command. Please check your configuration..
/// </summary>
public static string bookmarks_command_invoke_failed_message {
get {
return ResourceManager.GetString("bookmarks_command_invoke_failed_message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>

View File

@@ -161,4 +161,7 @@
<value>{0} is required</value>
<comment>{0} will be replaced by a parameter name provided by the user</comment>
</data>
<data name="bookmarks_command_invoke_failed_message" xml:space="preserve">
<value>Unable to call command. Please check your configuration.</value>
</data>
</root>

View File

@@ -1,99 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public partial class UrlCommand : InvokableCommand
{
public string Type { get; }
public string Url { get; }
public UrlCommand(BookmarkData data)
: this(data.Name, data.Bookmark, data.Type)
{
}
public UrlCommand(string name, string url, string type)
{
Name = name;
Type = type;
Url = url;
Icon = new IconInfo(IconFromUrl(Url, type));
}
public override CommandResult Invoke()
{
var target = Url;
try
{
var uri = GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
return CommandResult.Dismiss();
}
internal static Uri? GetUri(string url)
{
Uri? uri;
if (!Uri.TryCreate(url, UriKind.Absolute, out uri))
{
if (!Uri.TryCreate("https://" + url, UriKind.Absolute, out uri))
{
return null;
}
}
return uri;
}
internal static string IconFromUrl(string url, string type)
{
switch (type)
{
case "file":
return "📄";
case "folder":
return "📁";
case "web":
default:
// Get the base url up to the first placeholder
var placeholderIndex = url.IndexOf('{');
var baseString = placeholderIndex > 0 ? url.Substring(0, placeholderIndex) : url;
try
{
var uri = GetUri(baseString);
if (uri != null)
{
var hostname = uri.Host;
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
return faviconUrl;
}
}
catch (UriFormatException ex)
{
Logger.LogError(ex.Message);
}
return "🔗";
}
}
}