diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 66126cda15f..7bba13bc4cc 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -450,6 +450,36 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return found != GeneratedActionNames.end() ? found->second : L""; } + // Function Description: + // - This will generate an ID for this ActionAndArgs, based on the ShortcutAction and the Args + // - It will always create the same ID if the ShortcutAction and the Args are the same + // - Note: this should only be called for User-created actions + // - Example: The "SendInput 'abc'" action will have the generated ID "User.sendInput." + // Return Value: + // - The ID, based on the ShortcutAction and the Args + winrt::hstring ActionAndArgs::GenerateID() const + { + if (_Action != ShortcutAction::Invalid) + { + auto actionKeyString = ActionToStringMap.find(_Action)->second; + auto result = fmt::format(FMT_COMPILE(L"User.{}"), actionKeyString); + if (_Args) + { + // If there are args, we need to append the hash of the args + // However, to make it a little more presentable we + // 1. truncate the hash to 32 bits + // 2. convert it to a hex string + // there is a _tiny_ chance of collision because of the truncate but unlikely for + // the number of commands a user is expected to have + const auto argsHash32 = static_cast(_Args.Hash() & 0xFFFFFFFF); + // {0:X} formats the truncated hash to an uppercase hex string + fmt::format_to(std::back_inserter(result), FMT_COMPILE(L".{:X}"), argsHash32); + } + return winrt::hstring{ result }; + } + return L""; + } + winrt::hstring ActionAndArgs::Serialize(const winrt::Windows::Foundation::Collections::IVector& args) { Json::Value json{ Json::objectValue }; diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h index a2afefaa493..9c841ca727c 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h @@ -27,6 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation com_ptr Copy() const; hstring GenerateName() const; + hstring GenerateID() const; WINRT_PROPERTY(ShortcutAction, Action, ShortcutAction::Invalid); WINRT_PROPERTY(IActionArgs, Args, nullptr); diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 76565dff444..220cd825565 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -795,6 +795,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return nullptr; } + bool ActionMap::GenerateIDsForActions() + { + bool fixedUp{ false }; + for (auto actionPair : _ActionMap) + { + auto cmdImpl{ winrt::get_self(actionPair.second) }; + + // Note: this function should ONLY be called for the action map in the user's settings file + // this debug assert should verify that for debug builds + assert(cmdImpl->Origin() == OriginTag::User); + + if (cmdImpl->ID().empty()) + { + fixedUp = cmdImpl->GenerateID() || fixedUp; + } + } + return fixedUp; + } + // Method Description: // - Rebinds a key binding to a new key chord // Arguments: diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index de9b9ca2361..880b9989ff6 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -71,6 +71,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value ToJson() const; // modification + bool GenerateIDsForActions(); bool RebindKeys(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys); void DeleteKeyBinding(const Control::KeyChord& keys); void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 93af43319bc..37112fe76b6 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -504,6 +504,10 @@ bool SettingsLoader::FixupUserSettings() fixedUp = true; } + // we need to generate an ID for a command in the user settings if it doesn't already have one + auto actionMap{ winrt::get_self(userSettings.globals->ActionMap()) }; + fixedUp = actionMap->GenerateIDsForActions() || fixedUp; + return fixedUp; } diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 91082a4f6d7..c20be960a67 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -21,6 +21,7 @@ namespace winrt } static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view IDKey{ "id" }; static constexpr std::string_view IconKey{ "icon" }; static constexpr std::string_view ActionKey{ "command" }; static constexpr std::string_view IterateOnKey{ "iterateOn" }; @@ -39,7 +40,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto command{ winrt::make_self() }; command->_name = _name; - command->_Origin = OriginTag::User; + command->_Origin = _Origin; + command->_ID = _ID; command->_ActionAndArgs = *get_self(_ActionAndArgs)->Copy(); command->_keyMappings = _keyMappings; command->_iconPath = _iconPath; @@ -114,6 +116,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } } + hstring Command::ID() const noexcept + { + return hstring{ _ID }; + } + + bool Command::GenerateID() + { + if (_ActionAndArgs) + { + auto actionAndArgsImpl{ winrt::get_self(_ActionAndArgs) }; + if (const auto generatedID = actionAndArgsImpl->GenerateID(); !generatedID.empty()) + { + _ID = generatedID; + return true; + } + } + return false; + } + void Command::Name(const hstring& value) { if (!_name.has_value() || _name.value() != value) @@ -264,6 +285,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto result = winrt::make_self(); result->_Origin = origin; + JsonUtils::GetValueForKey(json, IDKey, result->_ID); auto nested = false; JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn); @@ -423,6 +445,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value cmdJson{ Json::ValueType::objectValue }; JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + if (!_ID.empty()) + { + JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); + } if (_ActionAndArgs) { @@ -443,6 +469,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // First iteration also writes icon and name JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + if (!_ID.empty()) + { + JsonUtils::SetValueForKey(cmdJson, IDKey, _ID); + } } if (_ActionAndArgs) diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 5e94ae721c8..f22e35348c7 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -61,6 +61,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation hstring Name() const noexcept; void Name(const hstring& name); + hstring ID() const noexcept; + bool GenerateID(); + Control::KeyChord Keys() const noexcept; hstring KeyChordText() const noexcept; std::vector KeyMappings() const noexcept; @@ -84,6 +87,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _subcommands{ nullptr }; std::vector _keyMappings; std::optional _name; + std::wstring _ID; std::optional _iconPath; bool _nestedCommand{ false }; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index c9cec5012a4..aa23458f55d 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -36,6 +36,7 @@ namespace Microsoft.Terminal.Settings.Model Command(); String Name { get; }; + String ID { get; }; ActionAndArgs ActionAndArgs { get; }; Microsoft.Terminal.Control.KeyChord Keys { get; }; void RegisterKey(Microsoft.Terminal.Control.KeyChord keys); diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 7c33e169994..2c79c63b7c8 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -423,145 +423,145 @@ "actions": [ // Application-level Keys - { "command": "closeWindow", "keys": "alt+f4" }, - { "command": "toggleFullscreen", "keys": "alt+enter" }, - { "command": "toggleFullscreen", "keys": "f11" }, - { "command": "toggleFocusMode" }, - { "command": "toggleAlwaysOnTop" }, - { "command": "openNewTabDropdown", "keys": "ctrl+shift+space" }, - { "command": { "action": "openSettings", "target": "settingsUI" }, "keys": "ctrl+," }, - { "command": { "action": "openSettings", "target": "settingsFile" }, "keys": "ctrl+shift+," }, - { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," }, - { "command": "find", "keys": "ctrl+shift+f" }, - { "command": { "action": "findMatch", "direction": "next" } }, - { "command": { "action": "findMatch", "direction": "prev" } }, - { "command": "toggleShaderEffects" }, - { "command": "openTabColorPicker" }, - { "command": "renameTab" }, - { "command": "openTabRenamer" }, - { "command": "commandPalette", "keys":"ctrl+shift+p" }, - { "command": "identifyWindow" }, - { "command": "openWindowRenamer" }, - { "command": "quakeMode", "keys":"win+sc(41)" }, - { "command": "openSystemMenu", "keys": "alt+space" }, - { "command": "quit" }, - { "command": "restoreLastClosed" }, - { "command": "openAbout" }, + { "command": "closeWindow", "keys": "alt+f4", "id": "Terminal.CloseWindow" }, + { "command": "toggleFullscreen", "keys": "alt+enter", "id": "Terminal.ToggleFullscreen" }, + { "command": "toggleFullscreen", "keys": "f11", "id": "Terminal.ToggleFullscreen" }, + { "command": "toggleFocusMode", "id": "Terminal.ToggleFocusMode" }, + { "command": "toggleAlwaysOnTop", "id": "Terminal.ToggleAlwaysOnTop" }, + { "command": "openNewTabDropdown", "keys": "ctrl+shift+space", "id": "Terminal.OpenNewTabDropdown" }, + { "command": { "action": "openSettings", "target": "settingsUI" }, "keys": "ctrl+,", "id": "Terminal.OpenSettingsUI" }, + { "command": { "action": "openSettings", "target": "settingsFile" }, "keys": "ctrl+shift+,", "id": "Terminal.OpenSettingsFile" }, + { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+,", "id": "Terminal.OpenDefaultSettingsFile" }, + { "command": "find", "keys": "ctrl+shift+f", "id": "Terminal.FindText" }, + { "command": { "action": "findMatch", "direction": "next" }, "id": "Terminal.FindNextMatch" }, + { "command": { "action": "findMatch", "direction": "prev" }, "id": "Terminal.FindPrevMatch" }, + { "command": "toggleShaderEffects", "id": "Terminal.ToggleShaderEffects" }, + { "command": "openTabColorPicker", "id": "Terminal.OpenTabColorPicker" }, + { "command": "renameTab", "id": "Terminal.RenameTab" }, + { "command": "openTabRenamer", "id": "Terminal.OpenTabRenamer" }, + { "command": "commandPalette", "keys":"ctrl+shift+p", "id": "Terminal.ToggleCommandPalette" }, + { "command": "identifyWindow", "id": "Terminal.IdentifyWindow" }, + { "command": "openWindowRenamer", "id": "Terminal.OpenWindowRenamer" }, + { "command": "quakeMode", "keys":"win+sc(41)", "id": "Terminal.QuakeMode" }, + { "command": "openSystemMenu", "keys": "alt+space", "id": "Terminal.OpenSystemMenu" }, + { "command": "quit", "id": "Terminal.Quit" }, + { "command": "restoreLastClosed", "id": "Terminal.RestoreLastClosed" }, + { "command": "openAbout", "id": "Terminal.OpenAboutDialog" }, // Tab Management // "command": "closeTab" is unbound by default. // The closeTab command closes a tab without confirmation, even if it has multiple panes. - { "command": "closeOtherTabs" }, - { "command": "closeTabsAfter" }, - { "command": { "action" : "moveTab", "direction": "forward" }}, - { "command": { "action" : "moveTab", "direction": "backward" }}, - { "command": "newTab", "keys": "ctrl+shift+t" }, - { "command": "newWindow", "keys": "ctrl+shift+n" }, - { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1" }, - { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2" }, - { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3" }, - { "command": { "action": "newTab", "index": 3 }, "keys": "ctrl+shift+4" }, - { "command": { "action": "newTab", "index": 4 }, "keys": "ctrl+shift+5" }, - { "command": { "action": "newTab", "index": 5 }, "keys": "ctrl+shift+6" }, - { "command": { "action": "newTab", "index": 6 }, "keys": "ctrl+shift+7" }, - { "command": { "action": "newTab", "index": 7 }, "keys": "ctrl+shift+8" }, - { "command": { "action": "newTab", "index": 8 }, "keys": "ctrl+shift+9" }, - { "command": "duplicateTab", "keys": "ctrl+shift+d" }, - { "command": "nextTab", "keys": "ctrl+tab" }, - { "command": "prevTab", "keys": "ctrl+shift+tab" }, - { "command": { "action": "switchToTab", "index": 0 }, "keys": "ctrl+alt+1" }, - { "command": { "action": "switchToTab", "index": 1 }, "keys": "ctrl+alt+2" }, - { "command": { "action": "switchToTab", "index": 2 }, "keys": "ctrl+alt+3" }, - { "command": { "action": "switchToTab", "index": 3 }, "keys": "ctrl+alt+4" }, - { "command": { "action": "switchToTab", "index": 4 }, "keys": "ctrl+alt+5" }, - { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6" }, - { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7" }, - { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8" }, - { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9" }, - { "command": { "action": "moveTab", "window": "new" }, }, + { "command": "closeOtherTabs", "id": "Terminal.CloseOtherTabs" }, + { "command": "closeTabsAfter", "id": "Terminal.CloseTabsAfter" }, + { "command": { "action" : "moveTab", "direction": "forward" }, "id": "Terminal.MoveTabForward" }, + { "command": { "action" : "moveTab", "direction": "backward" }, "id": "Terminal.MoveTabBackward" }, + { "command": "newTab", "keys": "ctrl+shift+t", "id": "Terminal.OpenNewTab" }, + { "command": "newWindow", "keys": "ctrl+shift+n", "id": "Terminal.OpenNewWindow" }, + { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1", "id": "Terminal.OpenNewTabProfile0" }, + { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2", "id": "Terminal.OpenNewTabProfile1" }, + { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3", "id": "Terminal.OpenNewTabProfile2" }, + { "command": { "action": "newTab", "index": 3 }, "keys": "ctrl+shift+4", "id": "Terminal.OpenNewTabProfile3" }, + { "command": { "action": "newTab", "index": 4 }, "keys": "ctrl+shift+5", "id": "Terminal.OpenNewTabProfile4" }, + { "command": { "action": "newTab", "index": 5 }, "keys": "ctrl+shift+6", "id": "Terminal.OpenNewTabProfile5" }, + { "command": { "action": "newTab", "index": 6 }, "keys": "ctrl+shift+7", "id": "Terminal.OpenNewTabProfile6" }, + { "command": { "action": "newTab", "index": 7 }, "keys": "ctrl+shift+8", "id": "Terminal.OpenNewTabProfile7" }, + { "command": { "action": "newTab", "index": 8 }, "keys": "ctrl+shift+9", "id": "Terminal.OpenNewTabProfile8" }, + { "command": "duplicateTab", "keys": "ctrl+shift+d", "id": "Terminal.DuplicateTab" }, + { "command": "nextTab", "keys": "ctrl+tab", "id": "Terminal.NextTab" }, + { "command": "prevTab", "keys": "ctrl+shift+tab", "id": "Terminal.PrevTab" }, + { "command": { "action": "switchToTab", "index": 0 }, "keys": "ctrl+alt+1", "id": "Terminal.SwitchToTab0" }, + { "command": { "action": "switchToTab", "index": 1 }, "keys": "ctrl+alt+2", "id": "Terminal.SwitchToTab1" }, + { "command": { "action": "switchToTab", "index": 2 }, "keys": "ctrl+alt+3", "id": "Terminal.SwitchToTab2" }, + { "command": { "action": "switchToTab", "index": 3 }, "keys": "ctrl+alt+4", "id": "Terminal.SwitchToTab3" }, + { "command": { "action": "switchToTab", "index": 4 }, "keys": "ctrl+alt+5", "id": "Terminal.SwitchToTab4" }, + { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6", "id": "Terminal.SwitchToTab5" }, + { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7", "id": "Terminal.SwitchToTab6" }, + { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8", "id": "Terminal.SwitchToTab7" }, + { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9", "id": "Terminal.SwitchToLastTab" }, + { "command": { "action": "moveTab", "window": "new" }, "id": "Terminal.MoveTabToNewWindow" }, // Pane Management - { "command": "closeOtherPanes" }, - { "command": "closePane", "keys": "ctrl+shift+w" }, - { "command": { "action": "splitPane", "split": "up" } }, - { "command": { "action": "splitPane", "split": "down" } }, - { "command": { "action": "splitPane", "split": "left" } }, - { "command": { "action": "splitPane", "split": "right" } }, - { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "down" }, "keys": "alt+shift+-" }, - { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "right" }, "keys": "alt+shift+plus" }, - { "command": { "action": "resizePane", "direction": "down" }, "keys": "alt+shift+down" }, - { "command": { "action": "resizePane", "direction": "left" }, "keys": "alt+shift+left" }, - { "command": { "action": "resizePane", "direction": "right" }, "keys": "alt+shift+right" }, - { "command": { "action": "resizePane", "direction": "up" }, "keys": "alt+shift+up" }, - { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down" }, - { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" }, - { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, - { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, - { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left"}, - { "command": { "action": "moveFocus", "direction": "previousInOrder" } }, - { "command": { "action": "moveFocus", "direction": "nextInOrder" } }, - { "command": { "action": "moveFocus", "direction": "first" } }, - { "command": { "action": "moveFocus", "direction": "parent" } }, - { "command": { "action": "moveFocus", "direction": "child" } }, - { "command": { "action": "swapPane", "direction": "down" } }, - { "command": { "action": "swapPane", "direction": "left" } }, - { "command": { "action": "swapPane", "direction": "right" } }, - { "command": { "action": "swapPane", "direction": "up" } }, - { "command": { "action": "swapPane", "direction": "previous"} }, - { "command": { "action": "swapPane", "direction": "previousInOrder"} }, - { "command": { "action": "swapPane", "direction": "nextInOrder"} }, - { "command": { "action": "swapPane", "direction": "first" } }, - { "command": "toggleBroadcastInput" }, - { "command": "togglePaneZoom" }, - { "command": "toggleSplitOrientation" }, - { "command": "toggleReadOnlyMode" }, - { "command": "enableReadOnlyMode" }, - { "command": "disableReadOnlyMode" }, - { "command": { "action": "movePane", "index": 0 } }, - { "command": { "action": "movePane", "index": 1 } }, - { "command": { "action": "movePane", "index": 2 } }, - { "command": { "action": "movePane", "index": 3 } }, - { "command": { "action": "movePane", "index": 4 } }, - { "command": { "action": "movePane", "index": 5 } }, - { "command": { "action": "movePane", "index": 6 } }, - { "command": { "action": "movePane", "index": 7 } }, - { "command": { "action": "movePane", "index": 8 } }, - { "command": { "action": "movePane", "window": "new" }, }, - { "command": "restartConnection" }, + { "command": "closeOtherPanes", "id": "Terminal.CloseOtherPanes" }, + { "command": "closePane", "keys": "ctrl+shift+w", "id": "Terminal.ClosePane" }, + { "command": { "action": "splitPane", "split": "up" }, "id": "Terminal.SplitPaneUp" }, + { "command": { "action": "splitPane", "split": "down" }, "id": "Terminal.SplitPaneDown" }, + { "command": { "action": "splitPane", "split": "left" }, "id": "Terminal.SplitPaneLeft" }, + { "command": { "action": "splitPane", "split": "right" }, "id": "Terminal.SplitPaneRight" }, + { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "down" }, "keys": "alt+shift+-", "id": "Terminal.SplitPaneDuplicateDown" }, + { "command": { "action": "splitPane", "splitMode": "duplicate", "split": "right" }, "keys": "alt+shift+plus", "id": "Terminal.SplitPaneDuplicateRight" }, + { "command": { "action": "resizePane", "direction": "down" }, "keys": "alt+shift+down", "id": "Terminal.ResizePaneDown" }, + { "command": { "action": "resizePane", "direction": "left" }, "keys": "alt+shift+left", "id": "Terminal.ResizePaneLeft" }, + { "command": { "action": "resizePane", "direction": "right" }, "keys": "alt+shift+right", "id": "Terminal.ResizePaneRight" }, + { "command": { "action": "resizePane", "direction": "up" }, "keys": "alt+shift+up", "id": "Terminal.ResizePaneUp" }, + { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down", "id": "Terminal.MoveFocusDown" }, + { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left", "id": "Terminal.MoveFocusLeft" }, + { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right", "id": "Terminal.MoveFocusRight" }, + { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up", "id": "Terminal.MoveFocusUp" }, + { "command": { "action": "moveFocus", "direction": "previous" }, "keys": "ctrl+alt+left", "id": "Terminal.MoveFocusPrevious" }, + { "command": { "action": "moveFocus", "direction": "previousInOrder" }, "id": "Terminal.MoveFocusPreviousInOrder" }, + { "command": { "action": "moveFocus", "direction": "nextInOrder" }, "id": "Terminal.MoveFocusNextInOrder" }, + { "command": { "action": "moveFocus", "direction": "first" }, "id": "Terminal.MoveFocusFirst" }, + { "command": { "action": "moveFocus", "direction": "parent" }, "id": "Terminal.MoveFocusParent" }, + { "command": { "action": "moveFocus", "direction": "child" }, "id": "Terminal.MoveFocusChild" }, + { "command": { "action": "swapPane", "direction": "down" }, "id": "Terminal.SwapPaneDown" }, + { "command": { "action": "swapPane", "direction": "left" }, "id": "Terminal.SwapPaneLeft" }, + { "command": { "action": "swapPane", "direction": "right" }, "id": "Terminal.SwapPaneRight" }, + { "command": { "action": "swapPane", "direction": "up" }, "id": "Terminal.SwapPaneUp" }, + { "command": { "action": "swapPane", "direction": "previous"}, "id": "Terminal.SwapPanePrevious" }, + { "command": { "action": "swapPane", "direction": "previousInOrder"}, "id": "Terminal.SwapPanePreviousInOrder" }, + { "command": { "action": "swapPane", "direction": "nextInOrder"}, "id": "Terminal.SwapPaneNextInOrder" }, + { "command": { "action": "swapPane", "direction": "first" }, "id": "Terminal.SwapPaneFirst" }, + { "command": "toggleBroadcastInput", "id": "Terminal.ToggleBroadcastInput" }, + { "command": "togglePaneZoom", "id": "Terminal.TogglePaneZoom" }, + { "command": "toggleSplitOrientation", "id": "Terminal.ToggleSplitOrientation" }, + { "command": "toggleReadOnlyMode", "id": "Terminal.ToggleReadOnlyMode" }, + { "command": "enableReadOnlyMode", "id": "Terminal.EnableReadOnlyMode" }, + { "command": "disableReadOnlyMode", "id": "Terminal.DisableReadOnlyMode" }, + { "command": { "action": "movePane", "index": 0 }, "id": "Terminal.MovePaneToTab0" }, + { "command": { "action": "movePane", "index": 1 }, "id": "Terminal.MovePaneToTab1" }, + { "command": { "action": "movePane", "index": 2 }, "id": "Terminal.MovePaneToTab2" }, + { "command": { "action": "movePane", "index": 3 }, "id": "Terminal.MovePaneToTab3" }, + { "command": { "action": "movePane", "index": 4 }, "id": "Terminal.MovePaneToTab4" }, + { "command": { "action": "movePane", "index": 5 }, "id": "Terminal.MovePaneToTab5" }, + { "command": { "action": "movePane", "index": 6 }, "id": "Terminal.MovePaneToTab6" }, + { "command": { "action": "movePane", "index": 7 }, "id": "Terminal.MovePaneToTab7" }, + { "command": { "action": "movePane", "index": 8 }, "id": "Terminal.MovePaneToTab8" }, + { "command": { "action": "movePane", "window": "new" }, "id": "Terminal.MovePaneToNewWindow" }, + { "command": "restartConnection", "id": "Terminal.RestartConnection" }, // Clipboard Integration - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" }, - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert" }, - { "command": { "action": "copy", "singleLine": false }, "keys": "enter" }, - { "command": "paste", "keys": "ctrl+shift+v" }, - { "command": "paste", "keys": "shift+insert" }, - { "command": "selectAll", "keys": "ctrl+shift+a" }, - { "command": "markMode", "keys": "ctrl+shift+m" }, - { "command": "toggleBlockSelection" }, - { "command": "switchSelectionEndpoint" }, - { "command": "expandSelectionToWord" }, - { "command": "showContextMenu", "keys": "menu" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c", "id": "Terminal.CopySelectedText" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert", "id": "Terminal.CopySelectedText" }, + { "command": { "action": "copy", "singleLine": false }, "keys": "enter", "id": "Terminal.CopySelectedText" }, + { "command": "paste", "keys": "ctrl+shift+v", "id": "Terminal.PasteFromClipboard" }, + { "command": "paste", "keys": "shift+insert", "id": "Terminal.PasteFromClipboard" }, + { "command": "selectAll", "keys": "ctrl+shift+a", "id": "Terminal.SelectAll" }, + { "command": "markMode", "keys": "ctrl+shift+m", "id": "Terminal.ToggleMarkMode" }, + { "command": "toggleBlockSelection", "id": "Terminal.ToggleBlockSelection" }, + { "command": "switchSelectionEndpoint", "id": "Terminal.SwitchSelectionEndpoint" }, + { "command": "expandSelectionToWord", "id": "Terminal.ExpandSelectionToWord" }, + { "command": "showContextMenu", "keys": "menu", "id": "Terminal.ShowContextMenu" }, // Web Search - { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" } }, + { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" }, "id": "Terminal.SearchWeb" }, // Scrollback - { "command": "scrollDown", "keys": "ctrl+shift+down" }, - { "command": "scrollDownPage", "keys": "ctrl+shift+pgdn" }, - { "command": "scrollUp", "keys": "ctrl+shift+up" }, - { "command": "scrollUpPage", "keys": "ctrl+shift+pgup" }, - { "command": "scrollToTop", "keys": "ctrl+shift+home" }, - { "command": "scrollToBottom", "keys": "ctrl+shift+end" }, - { "command": { "action": "clearBuffer", "clear": "all" } }, - { "command": "exportBuffer" }, + { "command": "scrollDown", "keys": "ctrl+shift+down", "id": "Terminal.ScrollDown" }, + { "command": "scrollDownPage", "keys": "ctrl+shift+pgdn", "id": "Terminal.ScrollDownPage" }, + { "command": "scrollUp", "keys": "ctrl+shift+up", "id": "Terminal.ScrollUp" }, + { "command": "scrollUpPage", "keys": "ctrl+shift+pgup", "id": "Terminal.ScrollUpPage" }, + { "command": "scrollToTop", "keys": "ctrl+shift+home", "id": "Terminal.ScrollToTop" }, + { "command": "scrollToBottom", "keys": "ctrl+shift+end", "id": "Terminal.ScrollToBottom" }, + { "command": { "action": "clearBuffer", "clear": "all" }, "id": "Terminal.ClearBuffer" }, + { "command": "exportBuffer", "id": "Terminal.ExportBuffer" }, // Visual Adjustments - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" }, - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus" }, - { "command": "resetFontSize", "keys": "ctrl+0" }, - { "command": "resetFontSize", "keys": "ctrl+numpad_0" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus", "id": "Terminal.IncreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus", "id": "Terminal.DecreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus", "id": "Terminal.IncreaseFontSize" }, + { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus", "id": "Terminal.DecreaseFontSize" }, + { "command": "resetFontSize", "keys": "ctrl+0", "id": "Terminal.ResetFontSize" }, + { "command": "resetFontSize", "keys": "ctrl+numpad_0", "id": "Terminal.ResetFontSize" }, // Other commands { diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 8cfd4a14ccf..8d9c55b6f89 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -16,6 +16,14 @@ using namespace WEX::Common; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; +// Different architectures will hash the same SendInput command to a different ID +// Check for the correct ID based on the architecture +#if defined(_M_IX86) +#define SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH "56911147" +#else +#define SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH "A020D2" +#endif + namespace SettingsModelUnitTests { class SerializationTests : public JsonTestClass @@ -36,6 +44,10 @@ namespace SettingsModelUnitTests TEST_METHOD(RoundtripUserModifiedColorSchemeCollisionUnusedByProfiles); TEST_METHOD(RoundtripUserDeletedColorSchemeCollision); + TEST_METHOD(RoundtripGenerateActionID); + TEST_METHOD(NoGeneratedIDsForIterableAndNestedCommands); + TEST_METHOD(GeneratedActionIDsEqualForIdenticalCommands); + private: // Method Description: // - deserializes and reserializes a json string representing a settings object model of type T @@ -491,7 +503,7 @@ namespace SettingsModelUnitTests } ], "actions": [ - { "command": { "action": "sendInput", "input": "VT Griese Mode" }, "keys": "ctrl+k" } + { "command": { "action": "sendInput", "input": "VT Griese Mode" }, "id": "User.sendInput.E02B3DF9", "keys": "ctrl+k" } ], "theme": "system", "themes": [] @@ -946,4 +958,127 @@ namespace SettingsModelUnitTests VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); } + + void SerializationTests::RoundtripGenerateActionID() + { + static constexpr std::string_view oldSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "just some input" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + // Key differences: - the sendInput action now has a generated ID + // - this generated ID was created at the time of writing this test, + // and should remain robust (i.e. every time we hash the args we should get the same result) + static constexpr std::string_view newSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "just some input" }, + "keys": "ctrl+shift+w", + "id" : "User.sendInput.)" SEND_INPUT_ARCH_SPECIFIC_ACTION_HASH R"(" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_TRUE(oldLoader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + implementation::SettingsLoader newLoader{ newSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + const auto newResult{ newSettings->ToJson() }; + + VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); + } + + void SerializationTests::NoGeneratedIDsForIterableAndNestedCommands() + { + // for iterable commands, nested commands, and user-defined actions that already have + // an ID, we do not need to generate an ID + static constexpr std::string_view oldSettingsJson{ R"( + { + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w", + "id": "thisIsMyClosePane" + }, + { + "iterateOn": "profiles", + "icon": "${profile.icon}", + "name": "${profile.name}", + "command": { "action": "newTab", "profile": "${profile.name}" } + }, + { + "name": "Change font size...", + "commands": [ + { "command": { "action": "adjustFontSize", "delta": 1.0 } }, + { "command": { "action": "adjustFontSize", "delta": -1.0 } }, + { "command": "resetFontSize" }, + ] + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_FALSE(oldLoader.FixupUserSettings(), L"Validate that there is no need to write back to disk"); + } + + void SerializationTests::GeneratedActionIDsEqualForIdenticalCommands() + { + static constexpr std::string_view settingsJson1{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "this is some other input string" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + // Both settings files define the same action, so the generated ID should be the same for both + static constexpr std::string_view settingsJson2{ R"( + { + "actions": [ + { + "name": "foo", + "command": { "action": "sendInput", "input": "this is some other input string" }, + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader loader1{ settingsJson1, implementation::LoadStringResource(IDR_DEFAULTS) }; + loader1.MergeInboxIntoUserSettings(); + loader1.FinalizeLayering(); + VERIFY_IS_TRUE(loader1.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto settings1 = winrt::make_self(std::move(loader1)); + const auto result1{ settings1->ToJson() }; + + implementation::SettingsLoader loader2{ settingsJson2, implementation::LoadStringResource(IDR_DEFAULTS) }; + loader2.MergeInboxIntoUserSettings(); + loader2.FinalizeLayering(); + VERIFY_IS_TRUE(loader2.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto settings2 = winrt::make_self(std::move(loader2)); + const auto result2{ settings2->ToJson() }; + + VERIFY_ARE_EQUAL(toString(result1), toString(result2)); + } }