Merge pull request #34 from zadjii-msft/dev/crutkas/continueFixes

Bookmarks abstracted out of main project
This commit is contained in:
Clint Rutkas
2024-09-04 15:57:48 -07:00
committed by GitHub
17 changed files with 665 additions and 544 deletions

View File

@@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Built-in Extensions", "Buil
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Calc", "src\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{42DB35EE-1EDB-41E4-9C9F-A3520EBC5CC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{C5BADA22-70FF-41D1-9529-28F4891316A8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -150,6 +152,14 @@ Global
{42DB35EE-1EDB-41E4-9C9F-A3520EBC5CC4}.Release|ARM64.Build.0 = Release|ARM64
{42DB35EE-1EDB-41E4-9C9F-A3520EBC5CC4}.Release|x64.ActiveCfg = Release|x64
{42DB35EE-1EDB-41E4-9C9F-A3520EBC5CC4}.Release|x64.Build.0 = Release|x64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Debug|ARM64.Build.0 = Debug|ARM64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Debug|x64.ActiveCfg = Debug|x64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Debug|x64.Build.0 = Debug|x64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Release|ARM64.ActiveCfg = Release|ARM64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Release|ARM64.Build.0 = Release|ARM64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Release|x64.ActiveCfg = Release|x64
{C5BADA22-70FF-41D1-9529-28F4891316A8}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -167,6 +177,7 @@ Global
{EB13FDBD-7DD5-4E7E-8BEB-727B3C9331CB} = {B7FF739F-7716-4FC3-B622-705486187B87}
{65E22130-6A8F-4AB7-80EC-FF75475DE821} = {B7FF739F-7716-4FC3-B622-705486187B87}
{42DB35EE-1EDB-41E4-9C9F-A3520EBC5CC4} = {272D0E9A-8FC3-49F5-8FAD-79ABAE8AB1E4}
{C5BADA22-70FF-41D1-9529-28F4891316A8} = {272D0E9A-8FC3-49F5-8FAD-79ABAE8AB1E4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BC94BFC2-A741-4978-B6A4-9E01B7660E6B}

View File

@@ -0,0 +1,113 @@
// 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;
using System.Text.Json.Nodes;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed class AddBookmarkForm : Form
{
internal event TypedEventHandler<object, object?>? AddedAction;
public override string TemplateJson()
{
var json = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "Name",
"isRequired": true,
"errorMessage": "Name is required"
},
{
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"label": "URL or File Path",
"isRequired": true,
"errorMessage": "URL or File Path is required"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save",
"data": {
"name": "name",
"bookmark": "bookmark"
}
}
]
}
""";
return json;
}
public override string DataJson() => throw new NotImplementedException();
public override string StateJson() => throw new NotImplementedException();
public override ActionResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload);
if (formInput == null)
{
return ActionResult.GoHome();
}
// 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 formData = new BookmarkData()
{
Name = formName.ToString(),
Bookmark = formBookmark.ToString(),
Type = bookmarkType,
};
// Construct a new json blob with the name and url
var jsonPath = BookmarksActionProvider.StateJsonPath();
var data = Bookmarks.ReadFromFile(jsonPath);
data.Data.Add(formData);
Bookmarks.WriteToFile(jsonPath, data);
AddedAction?.Invoke(this, null);
return ActionResult.GoHome();
}
}

View 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 Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed class AddBookmarkPage : FormPage
{
private readonly AddBookmarkForm _addBookmark = new();
internal event TypedEventHandler<object, object?>? AddedAction
{
add => _addBookmark.AddedAction += value;
remove => _addBookmark.AddedAction -= value;
}
public override IForm[] Forms() => [_addBookmark];
public AddBookmarkPage()
{
this.Icon = new("\ued0e");
this.Name = "Add a Bookmark";
}
}

View File

@@ -0,0 +1,14 @@
// 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;
public class BookmarkData
{
public string Name { get; set; } = string.Empty;
public string Bookmark { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,14 @@
// 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.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks;
[JsonSerializable(typeof(BookmarkData))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(string))]
internal sealed partial class BookmarkDataContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,112 @@
// 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.Linq;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed class BookmarkPlaceholderForm : Form
{
private readonly List<string> _placeholderNames;
private readonly string _bookmark = string.Empty;
// TODO pass in an array of placeholders
public BookmarkPlaceholderForm(string name, string url, string type)
{
_bookmark = url;
Regex r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
MatchCollection matches = r.Matches(url);
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
}
public override string TemplateJson()
{
var inputs = _placeholderNames.Select(p =>
{
return $$"""
{
"type": "Input.Text",
"style": "text",
"id": "{{p}}",
"label": "{{p}}",
"isRequired": true,
"errorMessage": "{{p}} is required"
}
""";
}).ToList();
var allInputs = string.Join(",", inputs);
var json = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
""" + allInputs + """
],
"actions": [
{
"type": "Action.Submit",
"title": "Open",
"data": {
"placeholder": "placeholder"
}
}
]
}
""";
return json;
}
public override string DataJson() => throw new NotImplementedException();
public override string StateJson() => throw new NotImplementedException();
public override ActionResult SubmitForm(string payload)
{
var target = _bookmark;
// parse the submitted JSON and then open the link
var formInput = JsonNode.Parse(payload);
var formObject = formInput?.AsObject();
if (formObject == null)
{
return ActionResult.GoHome();
}
foreach (var (key, value) in formObject)
{
var placeholderString = $"{{{key}}}";
var placeholderData = value?.ToString();
target = target.Replace(placeholderString, placeholderData);
}
try
{
Uri? uri = UrlAction.GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
}
return ActionResult.GoHome();
}
}

View File

@@ -0,0 +1,22 @@
// 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 Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed class BookmarkPlaceholderPage : FormPage
{
private readonly IForm _bookmarkPlaceholder;
public override IForm[] Forms() => [_bookmarkPlaceholder];
public BookmarkPlaceholderPage(string name, string url, string type)
{
_Name = name;
Icon = new(UrlAction.IconFromUrl(url, type));
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
}
}

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.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks
{
public List<BookmarkData> Data { get; set; } = [];
private static readonly JsonSerializerOptions _jsonOptions = new()
{
IncludeFields = true,
};
public static Bookmarks ReadFromFile(string path)
{
var data = new Bookmarks();
// if the file exists, load it and append the new item
if (File.Exists(path))
{
var jsonStringReading = File.ReadAllText(path);
if (!string.IsNullOrEmpty(jsonStringReading))
{
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, _jsonOptions) ?? new Bookmarks();
}
}
return data;
}
public static void WriteToFile(string path, Bookmarks data)
{
var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
File.WriteAllText(BookmarksActionProvider.StateJsonPath(), jsonString);
}
}

View File

@@ -0,0 +1,141 @@
// 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.IO;
using System.Linq;
using Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarksActionProvider : ICommandProvider
{
public string DisplayName => $"Bookmarks";
public IconDataType Icon => new(string.Empty);
private readonly List<ICommand> _commands = [];
private readonly AddBookmarkPage _addNewCommand = new();
public BookmarksActionProvider()
{
_addNewCommand.AddedAction += AddNewCommand_AddedAction;
}
private void AddNewCommand_AddedAction(object sender, object? args)
{
_addNewCommand.AddedAction += AddNewCommand_AddedAction;
_commands.Clear();
}
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
public void Dispose() => throw new NotImplementedException();
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
private void LoadCommands()
{
List<ICommand> collected = [];
collected.Add(_addNewCommand);
try
{
var jsonFile = StateJsonPath();
if (File.Exists(jsonFile))
{
var data = Bookmarks.ReadFromFile(jsonFile);
if (data != null)
{
var items = data?.Data;
if (items != null)
{
foreach (var item in items)
{
var nameToken = item.Name;
var urlToken = item.Bookmark;
var typeToken = item.Type;
if (nameToken == null || urlToken == null || typeToken == null)
{
continue;
}
var name = nameToken.ToString();
var url = urlToken.ToString();
var type = typeToken.ToString();
collected.Add((url.Contains('{') && url.Contains('}')) ? new BookmarkPlaceholderPage(name, url, type) : new UrlAction(name, url, type));
}
}
}
}
}
catch (Exception ex)
{
// debug log error
Console.WriteLine($"Error loading commands: {ex.Message}");
}
_commands.Clear();
_commands.AddRange(collected);
}
public IListItem[] TopLevelCommands()
{
if (_commands.Count == 0)
{
LoadCommands();
}
return _commands.Select(action =>
{
var listItem = new ListItem(action);
// Add actions for folder types
if (action is UrlAction urlAction && urlAction.Type == "folder")
{
listItem.MoreCommands = [
new CommandContextItem(new OpenInTerminalAction(urlAction.Url))
];
}
// listItem.Subtitle = "Bookmark";
if (action is not AddBookmarkPage)
{
listItem.Tags = [
new Tag()
{
Text = "Bookmark",
// Icon = new("🔗"),
// Color=Windows.UI.Color.FromArgb(255, 255, 0, 255)
},
// new Tag() {
// Text = "A test",
// //Icon = new("🔗"),
// Color=Windows.UI.Color.FromArgb(255, 255, 0, 0)
// }
];
}
return listItem;
}).ToArray();
}
internal static string StateJsonPath()
{
// Get the path to our exe
var path = System.Reflection.Assembly.GetExecutingAssembly().Location;
// Get the directory of the exe
var directory = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
// now, the state is just next to the exe
return System.IO.Path.Combine(directory, "state.json");
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\extensionsdk\Microsoft.Windows.CommandPalette.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,41 @@
// 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.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed class OpenInTerminalAction : InvokableCommand
{
private readonly string _folder;
public OpenInTerminalAction(string folder)
{
Name = "Open in Terminal";
_folder = folder;
}
public override ICommandResult Invoke()
{
try
{
// Start Windows Terminal with the specified folder
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "wt.exe",
Arguments = $"-d \"{_folder}\"",
UseShellExecute = true,
};
System.Diagnostics.Process.Start(startInfo);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching Windows Terminal: {ex.Message}");
}
return ActionResult.Dismiss();
}
}

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.
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(BookmarkData))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,98 @@
// 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.Windows.CommandPalette.Extensions.Helpers;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class UrlAction : InvokableCommand
{
private bool IsContainsPlaceholder => _url.Contains('{') && _url.Contains('}');
public string Type { get; }
public string Url { get; }
private readonly string _url;
public UrlAction(string name, string url, string type)
{
_url = url;
Icon = new(IconFromUrl(_url, type));
Name = name;
Type = type;
Url = url;
}
public override ActionResult Invoke()
{
var target = _url;
try
{
Uri? uri = GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
}
return ActionResult.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
{
Uri? uri = GetUri(baseString);
if (uri != null)
{
var hostname = uri.Host;
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
return faviconUrl;
}
}
catch (UriFormatException)
{
// return "🔗";
}
return "🔗";
}
}
}

View File

@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Calc</RootNamespace>
<RootNamespace>Microsoft.CmdPal.Ext.Calc</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\extensionsdk\Microsoft.Windows.CommandPalette.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,541 +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.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.Foundation;
using Windows.System;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Run.Bookmarks;
internal sealed class OpenInTerminalAction : InvokableCommand
{
private readonly string _folder;
public OpenInTerminalAction(string folder)
{
Name = "Open in Terminal";
_folder = folder;
}
public override ICommandResult Invoke()
{
try
{
// Start Windows Terminal with the specified folder
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "wt.exe",
Arguments = $"-d \"{_folder}\"",
UseShellExecute = true,
};
System.Diagnostics.Process.Start(startInfo);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching Windows Terminal: {ex.Message}");
}
return ActionResult.Dismiss();
}
}
public class BookmarkData
{
public string Name = string.Empty;
public string Bookmark = string.Empty;
public string Type = string.Empty;
}
public sealed class Bookmarks
{
public List<BookmarkData> Data { get; set; } = [];
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(BookmarkData))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext
{
}
[JsonSerializable(typeof(BookmarkData))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(string))]
internal sealed partial class BookmarkDataContext : JsonSerializerContext
{
}
internal sealed class AddBookmarkForm : Form
{
internal event TypedEventHandler<object, object?>? AddedAction;
public override string TemplateJson()
{
var json = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "Name",
"isRequired": true,
"errorMessage": "Name is required"
},
{
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"label": "URL or File Path",
"isRequired": true,
"errorMessage": "URL or File Path is required"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save",
"data": {
"name": "name",
"bookmark": "bookmark"
}
}
]
}
""";
return json;
}
public override string DataJson() => throw new NotImplementedException();
public override string StateJson() => throw new NotImplementedException();
public override ActionResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload);
if (formInput == null)
{
return ActionResult.GoHome();
}
// 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 formData = new BookmarkData()
{
Name = formName.ToString(),
Bookmark = formBookmark.ToString(),
Type = bookmarkType,
};
// Construct a new json blob with the name and url
var jsonPath = BookmarksActionProvider.StateJsonPath();
Bookmarks data;
// if the file exists, load it and append the new item
if (File.Exists(jsonPath))
{
var jsonStringReading = File.ReadAllText(jsonPath);
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading);
}
else
{
data = new Bookmarks();
}
data.Data.Add(formData);
var options = new JsonSerializerOptions()
{
IncludeFields = true,
};
var jsonString = JsonSerializer.Serialize<Bookmarks>(data, options);
File.WriteAllText(BookmarksActionProvider.StateJsonPath(), jsonString);
AddedAction?.Invoke(this, null);
return ActionResult.GoHome();
}
}
internal sealed class AddBookmarkPage : FormPage
{
private readonly AddBookmarkForm _addBookmark = new();
internal event TypedEventHandler<object, object?>? AddedAction
{
add => _addBookmark.AddedAction += value;
remove => _addBookmark.AddedAction -= value;
}
public override IForm[] Forms() => [_addBookmark];
public AddBookmarkPage()
{
this.Icon = new("\ued0e");
this.Name = "Add a Bookmark";
}
}
internal sealed class BookmarkPlaceholderForm : Microsoft.Windows.CommandPalette.Extensions.Helpers.Form
{
private readonly List<string> _placeholderNames;
private readonly string _bookmark = string.Empty;
// TODO pass in an array of placeholders
public BookmarkPlaceholderForm(string name, string url, string type)
{
_bookmark = url;
Regex r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
MatchCollection matches = r.Matches(url);
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
}
public override string TemplateJson()
{
var inputs = _placeholderNames.Select(p =>
{
return $$"""
{
"type": "Input.Text",
"style": "text",
"id": "{{p}}",
"label": "{{p}}",
"isRequired": true,
"errorMessage": "{{p}} is required"
}
""";
}).ToList();
var allInputs = string.Join(",", inputs);
var json = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
""" + allInputs + """
],
"actions": [
{
"type": "Action.Submit",
"title": "Open",
"data": {
"placeholder": "placeholder"
}
}
]
}
""";
return json;
}
public override string DataJson() => throw new NotImplementedException();
public override string StateJson() => throw new NotImplementedException();
public override ActionResult SubmitForm(string payload)
{
var target = _bookmark;
// parse the submitted JSON and then open the link
var formInput = JsonNode.Parse(payload);
var formObject = formInput?.AsObject();
if (formObject == null)
{
return ActionResult.GoHome();
}
foreach (var (key, value) in formObject)
{
var placeholderString = $"{{{key}}}";
var placeholderData = value?.ToString();
target = target.Replace(placeholderString, placeholderData);
}
try
{
Uri? uri = UrlAction.GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
}
return ActionResult.GoHome();
}
}
internal sealed class BookmarkPlaceholderPage : FormPage
{
private readonly IForm _bookmarkPlaceholder;
public override IForm[] Forms() => [_bookmarkPlaceholder];
public BookmarkPlaceholderPage(string name, string url, string type)
{
_Name = name;
Icon = new(UrlAction.IconFromUrl(url, type));
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
}
}
public class UrlAction : InvokableCommand
{
private bool IsContainsPlaceholder => _url.Contains('{') && _url.Contains('}');
public string Type { get; }
public string Url { get; }
private readonly string _url;
public UrlAction(string name, string url, string type)
{
_url = url;
Icon = new(IconFromUrl(_url, type));
Name = name;
Type = type;
Url = url;
}
public override ActionResult Invoke()
{
var target = _url;
try
{
Uri? uri = GetUri(target);
if (uri != null)
{
_ = Launcher.LaunchUriAsync(uri);
}
else
{
// throw new UriFormatException("The provided URL is not valid.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
}
return ActionResult.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
{
Uri? uri = GetUri(baseString);
if (uri != null)
{
var hostname = uri.Host;
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
return faviconUrl;
}
}
catch (UriFormatException)
{
// return "🔗";
}
return "🔗";
}
}
}
public class BookmarksActionProvider : ICommandProvider
{
public string DisplayName => $"Bookmarks";
public IconDataType Icon => new(string.Empty);
private readonly List<ICommand> _commands = [];
private readonly AddBookmarkPage _addNewCommand = new();
public BookmarksActionProvider()
{
_addNewCommand.AddedAction += AddNewCommand_AddedAction;
}
private void AddNewCommand_AddedAction(object sender, object? args)
{
_addNewCommand.AddedAction += AddNewCommand_AddedAction;
_commands.Clear();
}
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
public void Dispose() => throw new NotImplementedException();
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
private void LoadCommands()
{
List<ICommand> collected = [];
collected.Add(_addNewCommand);
try
{
var jsonFile = StateJsonPath();
if (File.Exists(jsonFile))
{
// Open state.json from the disk and read it
var jsonString = File.ReadAllText(jsonFile);
var options = new JsonSerializerOptions()
{
IncludeFields = true,
};
var data = JsonSerializer.Deserialize<Bookmarks>(jsonString, options);
if (data != null)
{
var items = data?.Data;
foreach (var item in items)
{
var nameToken = item.Name;
var urlToken = item.Bookmark;
var typeToken = item.Type;
if (nameToken == null || urlToken == null || typeToken == null)
{
continue;
}
var name = nameToken.ToString();
var url = urlToken.ToString();
var type = typeToken.ToString();
collected.Add((url.Contains('{') && url.Contains('}')) ? new BookmarkPlaceholderPage(name, url, type) : new UrlAction(name, url, type));
}
}
}
}
catch (Exception ex)
{
// debug log error
Console.WriteLine($"Error loading commands: {ex.Message}");
}
_commands.Clear();
_commands.AddRange(collected);
}
public IListItem[] TopLevelCommands()
{
if (_commands.Count == 0)
{
LoadCommands();
}
return _commands.Select(action =>
{
var listItem = new ListItem(action);
// Add actions for folder types
if (action is UrlAction urlAction && urlAction.Type == "folder")
{
listItem.MoreCommands = [
new CommandContextItem(new OpenInTerminalAction(urlAction.Url))
];
}
// listItem.Subtitle = "Bookmark";
if (action is not AddBookmarkPage)
{
listItem.Tags = [
new Tag()
{
Text = "Bookmark",
// Icon = new("🔗"),
// Color=Windows.UI.Color.FromArgb(255, 255, 0, 255)
},
// new Tag() {
// Text = "A test",
// //Icon = new("🔗"),
// Color=Windows.UI.Color.FromArgb(255, 255, 0, 0)
// }
];
}
return listItem;
}).ToArray();
}
internal static string StateJsonPath()
{
// Get the path to our exe
var path = System.Reflection.Assembly.GetExecutingAssembly().Location;
// Get the directory of the exe
var directory = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
// now, the state is just next to the exe
return System.IO.Path.Combine(directory, "state.json");
}
}

View File

@@ -69,6 +69,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.Windows.CommandPalette.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj">

View File

@@ -9,6 +9,7 @@ using CmdPal.Models;
using DeveloperCommandPalette;
using Microsoft.CmdPal.Common.Extensions;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -49,7 +50,7 @@ public sealed class MainViewModel
internal MainViewModel()
{
_builtInCommands.Add(new Run.Bookmarks.BookmarksActionProvider());
_builtInCommands.Add(new BookmarksActionProvider());
_builtInCommands.Add(new CalculatorActionProvider());
_builtInCommands.Add(new SettingsActionProvider());
_builtInCommands.Add(quitActionProvider);