diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs index 31fcb37123..2083045e89 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkDataContext.cs @@ -8,7 +8,6 @@ namespace Microsoft.CmdPal.Ext.Bookmarks; [JsonSerializable(typeof(BookmarkData))] [JsonSerializable(typeof(string))] -[JsonSerializable(typeof(string))] internal sealed partial class BookmarkDataContext : JsonSerializerContext { } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index d6b24f1ece..7f0aefb4d9 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -111,13 +111,10 @@ public partial class BookmarksCommandProvider : CommandProvider 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; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe - return System.IO.Path.Combine(directory, "state.json"); + return System.IO.Path.Combine(directory, "bookmarks.json"); } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs index f0156b516e..01511c3d1c 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs @@ -53,11 +53,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs index 2cdfd6b9cb..d6533ef9fb 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Pages/SettingsPage.cs @@ -23,7 +23,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Properties.Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs index 21b8b4507d..96d812a4f7 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs @@ -8,20 +8,21 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Extensions.Helpers; namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; -public class SettingsManager +public class SettingsManager : JsonSettingsManager { - private readonly string _filePath; private readonly string _historyPath; - private readonly Microsoft.CmdPal.Extensions.Helpers.Settings _settings = new(); - private readonly List _choices = new() + private static readonly string _namespace = "websearch"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _choices = new() { new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none), new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1), @@ -30,8 +31,17 @@ public class SettingsManager new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20), }; - private readonly ToggleSetting _globalIfURI = new(nameof(GlobalIfURI), Resources.plugin_global_if_uri, Resources.plugin_global_if_uri, false); - private readonly ChoiceSetSetting _showHistory; + private readonly ToggleSetting _globalIfURI = new( + Namespaced(nameof(GlobalIfURI)), + Resources.plugin_global_if_uri, + Resources.plugin_global_if_uri, + false); + + private readonly ChoiceSetSetting _showHistory = new( + Namespaced(nameof(ShowHistory)), + Resources.plugin_show_history, + Resources.plugin_show_history, + _choices); public bool GlobalIfURI => _globalIfURI.Value; @@ -39,23 +49,17 @@ public class SettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe - return Path.Combine(directory, "websearch_state.json"); + return Path.Combine(directory, "settings.json"); } internal static string HistoryStateJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "websearch_history.json"); @@ -142,12 +146,11 @@ public class SettingsManager public SettingsManager() { - _filePath = SettingsJsonPath(); + FilePath = SettingsJsonPath(); _historyPath = HistoryStateJsonPath(); - _showHistory = new(nameof(ShowHistory), Resources.plugin_show_history, Resources.plugin_show_history, _choices); - _settings.Add(_globalIfURI); - _settings.Add(_showHistory); + Settings.Add(_globalIfURI); + Settings.Add(_showHistory); // Load settings from file upon initialization LoadSettings(); @@ -178,17 +181,11 @@ public class SettingsManager } } - public Microsoft.CmdPal.Extensions.Helpers.Settings GetSettings() => _settings; - - public void SaveSettings() + public override void SaveSettings() { + base.SaveSettings(); try { - // Serialize the main dictionary to JSON and save it to the file - var settingsJson = _settings.ToJson(); - - File.WriteAllText(_filePath, settingsJson); - if (ShowHistory == Resources.history_none) { ClearHistory(); @@ -219,33 +216,4 @@ public class SettingsManager ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); } } - - public void LoadSettings() - { - if (!File.Exists(_filePath)) - { - ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" }); - return; - } - - try - { - // Read the JSON content from the file - var jsonContent = File.ReadAllText(_filePath); - - // Is it valid JSON? - if (JsonNode.Parse(jsonContent) is JsonObject savedSettings) - { - _settings.Update(jsonContent); - } - else - { - ExtensionHost.LogMessage(new LogMessage() { Message = "Failed to parse settings file as JsonObject." }); - } - } - catch (Exception ex) - { - ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); - } - } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs index 986b7d634b..6d753d3b07 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WebSearch/Pages/SettingsPage.cs @@ -24,7 +24,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs index c86d3a2354..b872e522e9 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -90,11 +90,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs index a5836dcaa1..e8f8d62cc0 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs @@ -25,7 +25,7 @@ internal sealed partial class SettingsPage : FormPage Name = Resources.windowwalker_settings_name; Icon = new("\uE713"); // Settings icon _settingsManager = SettingsManager.Instance; - _settings = _settingsManager.GetSettings(); + _settings = _settingsManager.Settings; _settings.SettingsChanged += SettingsChanged; } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs index d386cd3dca..858ccbf704 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs @@ -42,11 +42,8 @@ public class SettingsManager : JsonSettingsManager internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs index 29091583d8..70bb45f0ac 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/SettingsPage.cs @@ -24,7 +24,7 @@ internal sealed partial class SettingsPage : FormPage { Name = Resources.settings_page_name; Icon = new("\uE713"); // Settings icon - _settings = settingsManager.GetSettings(); + _settings = settingsManager.Settings; _settingsManager = settingsManager; _settings.SettingsChanged += SettingsChanged; diff --git a/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs b/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs index 4270458c84..e93dce1189 100644 --- a/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs +++ b/src/modules/cmdpal/Exts/SpongebotExtension/SpongebotPage.cs @@ -90,11 +90,8 @@ public partial class SpongebotPage : MarkdownPage, IFallbackHandler 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 = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "state.json"); diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs index f8ae9c16b2..65450cd995 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs @@ -2,12 +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 System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; namespace YouTubeExtension.Actions; @@ -15,11 +11,8 @@ internal sealed class YouTubeHelper { 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 = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the state is just next to the exe return Path.Combine(directory, "state.json"); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index 3f6ed42eb5..3735f128ec 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.Extensions.Helpers; using Microsoft.CmdPal.UI.ViewModels.Settings; using Windows.Foundation; @@ -119,11 +120,8 @@ public partial class SettingsModel : ObservableObject internal static string SettingsJsonPath() { - // Get the path to our exe - var path = System.Reflection.Assembly.GetExecutingAssembly().Location; - - // Get the directory of the exe - var directory = Path.GetDirectoryName(path) ?? string.Empty; + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); // now, the settings is just next to the exe return Path.Combine(directory, "settings.json"); diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs index 21ce5ee48d..da88a30912 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/JsonSettingsManager.cs @@ -22,12 +22,7 @@ public abstract class JsonSettingsManager Converters = { new JsonStringEnumConverter() }, }; - public Settings GetSettings() - { - return _settings; - } - - public void LoadSettings() + public virtual void LoadSettings() { if (string.IsNullOrEmpty(FilePath)) { @@ -62,7 +57,7 @@ public abstract class JsonSettingsManager } } - public void SaveSettings() + public virtual void SaveSettings() { if (string.IsNullOrEmpty(FilePath)) { diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt index 8423d9d31e..942650356e 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/NativeMethods.txt @@ -2,4 +2,7 @@ GetCurrentThread OpenThreadToken GetPackageFamilyNameFromToken -CoRevertToSelf \ No newline at end of file +CoRevertToSelf +SHGetKnownFolderPath +KNOWN_FOLDER_FLAG +GetCurrentPackageId diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs new file mode 100644 index 0000000000..d5c73d4971 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CmdPal.Extensions.Helpers/Utilities.cs @@ -0,0 +1,83 @@ +// 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.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Microsoft.CmdPal.Extensions.Helpers; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "This file has more than a couple Windows constants in it, which don't make sense to rename")] +public class Utilities +{ + /// + /// Used to produce a path to a settings folder which your app can use. + /// If your app is running packaged, this will return the redirected local + /// app data path (Packages/{your_pfn}/LocalState). If not, it'll return + /// %LOCALAPPDATA%\{settingsFolderName}. + /// + /// Does not ensure that the directory exists. Callers should call + /// CreateDirectory before writing settings files to this directory. + /// + /// + /// var directory = Utilities.BaseSettingsPath("Some.Unique.String.Here"); + /// Directory.CreateDirectory(directory); + /// + /// A fallback directory name to use + /// inside of %LocalAppData%, in the case this app is not currently running + /// in a package context + /// The path to a folder to use for storing settings. + public static string BaseSettingsPath(string settingsFolderName) + { + // KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return + // the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests. + // Using this flag allows us to avoid Windows.Storage.ApplicationData completely. + var FOLDERID_LocalAppData = new Guid("F1B32785-6FBA-4FCF-9D55-7B8E7F157091"); + var hr = PInvoke.SHGetKnownFolderPath( + FOLDERID_LocalAppData, + (uint)KNOWN_FOLDER_FLAG.KF_FLAG_FORCE_APP_DATA_REDIRECTION, + null, + out var localAppDataFolder); + + if (hr.Succeeded) + { + var basePath = new string(localAppDataFolder.ToString()); + if (!IsPackaged()) + { + basePath = Path.Combine(basePath, settingsFolderName); + } + + return basePath; + } + else + { + throw Marshal.GetExceptionForHR(hr.Value)!; + } + } + + /// + /// Can be used to quickly determine if this process is running with package identity. + /// + /// true iff the process is running with package identity + public static bool IsPackaged() + { + uint bufferSize = 0; + var bytes = Array.Empty(); + + // CsWinRT apparently won't generate this constant + var APPMODEL_ERROR_NO_PACKAGE = (WIN32_ERROR)15700; + unsafe + { + fixed (byte* p = bytes) + { + // We don't actually need the package ID. We just need to know + // if we have a package or not, and APPMODEL_ERROR_NO_PACKAGE + // is a quick way to find out. + var win32Error = PInvoke.GetCurrentPackageId(ref bufferSize, p); + return win32Error != APPMODEL_ERROR_NO_PACKAGE; + } + } + } +}