Compare commits

...

4 Commits

Author SHA1 Message Date
Mike Griese
74159ddf99 bunch of markdown nits 2026-03-04 14:41:15 -06:00
Mike Griese
372b74a374 open the path relative to the CWD 2026-03-04 14:00:33 -06:00
Mike Griese
19af501f21 add x-open rudimentarily 2026-03-04 13:18:16 -06:00
Mike Griese
26042b28fc first up: persist opened panes 2026-03-04 10:25:13 -06:00
10 changed files with 255 additions and 30 deletions

View File

@@ -85,7 +85,7 @@
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
<uap3:Extension Category="windows.appExtension">
<!-- <uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.windows.console.host"
Id="OpenConsole-Dev"
DisplayName="OpenConsole Dev"
@@ -111,7 +111,7 @@
<com:ComInterface>
<com:ProxyStub Id="DEC4804D-56D1-4F73-9FBE-6828E7C85C56" DisplayName="OpenConsoleHandoffProxy" Path="OpenConsoleProxy.dll"/>
<com:Interface Id="E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
<com:Interface Id="6F23DA90-15C5-4203-9DB0-64E73F1B1B00" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/> <!-- ITerminalHandoff3 -->
<com:Interface Id="6F23DA90-15C5-4203-9DB0-64E73F1B1B00" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
<com:Interface Id="746E6BC0-AB05-4E38-AB14-71E86763141F" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
</com:ComInterface>
</com:Extension>
@@ -137,7 +137,7 @@
<desktop5:Verb Id="OpenTerminalDev" Clsid="52065414-e077-47ec-a3ac-1cc5455e1b54" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
</desktop4:Extension> -->
</Extensions>

View File

@@ -209,6 +209,7 @@ void AppCommandlineArgs::_buildParser()
_buildSwapPaneParser();
_buildFocusPaneParser();
_buildSaveSnippetParser();
_buildOpenFileParser();
}
// Method Description:
@@ -603,6 +604,35 @@ void AppCommandlineArgs::_buildSaveSnippetParser()
setupSubcommand(_saveCommand);
}
void AppCommandlineArgs::_buildOpenFileParser()
{
_openFileCommand = _app.add_subcommand("x-open", RS_A(L"OpenFileDesc"));
auto setupSubcommand = [this](auto* subcommand) {
subcommand->add_option("path", _openFilePath, RS_A(L"OpenFilePathArgDesc"))->required();
subcommand->callback([&, this]() {
// Resolve the path. If the user provided a relative path,
// resolve it against the current working directory.
auto path = winrt::to_hstring(_openFilePath);
auto dataMap = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
dataMap.Insert(L"path", path);
BaseContentArgs contentArgs{ L"x-markdown", dataMap.GetView() };
// Use SplitPane so the file opens as a pane alongside
// the active terminal, rather than replacing it.
SplitPaneArgs args{ SplitDirection::Automatic, contentArgs };
ActionAndArgs splitAction{};
splitAction.Action(ShortcutAction::SplitPane);
splitAction.Args(args);
_startupActions.push_back(splitAction);
});
};
setupSubcommand(_openFileCommand);
}
// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
// that subcommand to support all the properties in a NewTerminalArgs.
@@ -777,7 +807,8 @@ bool AppCommandlineArgs::_noCommandsProvided()
*_focusPaneShort ||
*_newPaneShort.subcommand ||
*_newPaneCommand.subcommand ||
*_saveCommand);
*_saveCommand ||
*_openFileCommand);
}
// Method Description:
@@ -813,6 +844,7 @@ void AppCommandlineArgs::_resetStateToDefault()
_swapPaneDirection = FocusDirection::None;
_focusPaneTarget = -1;
_openFilePath.clear();
_loadPersistedLayoutIdx = -1;
// DON'T clear _launchMode here! This will get called once for every
@@ -1014,9 +1046,9 @@ void AppCommandlineArgs::ValidateStartupCommands()
// If we parsed no commands, or the first command we've parsed is not a new
// tab action, prepend a new-tab command to the front of the list.
// (also, we don't need to do this if the only action is a x-save)
else if (_startupActions.empty() ||
(_startupActions.front().Action() != ShortcutAction::NewTab &&
_startupActions.front().Action() != ShortcutAction::SaveSnippet))
if (_startupActions.empty() ||
(_startupActions.front().Action() != ShortcutAction::NewTab &&
_startupActions.front().Action() != ShortcutAction::SaveSnippet))
{
// Build the NewTab action from the values we've parsed on the commandline.
NewTerminalArgs newTerminalArgs{};
@@ -1099,6 +1131,28 @@ int AppCommandlineArgs::ParseArgs(winrt::array_view<const winrt::hstring> args)
// If all the args were successfully parsed, we'll have some commands
// built in _appArgs, which we'll use when the application starts up.
// If we only have a single x-open command and no explicit -w flag,
// target the current window so we don't spawn a new one just to open a
// file. This needs to happen here (not in ValidateStartupCommands)
// because the emperor reads TargetWindow() right after parsing to
// decide where to route the commandline.
if (_startupActions.size() == 1 &&
_startupActions.front().Action() == ShortcutAction::SplitPane &&
_windowTarget.empty())
{
if (const auto& splitArgs = _startupActions.front().Args().try_as<SplitPaneArgs>())
{
if (const auto& contentArgs = splitArgs.ContentArgs())
{
if (contentArgs.Type() == L"x-markdown")
{
_windowTarget = "0";
}
}
}
}
return 0;
}

View File

@@ -93,6 +93,7 @@ private:
CLI::App* _focusPaneCommand;
CLI::App* _focusPaneShort;
CLI::App* _saveCommand;
CLI::App* _openFileCommand;
// Are you adding a new sub-command? Make sure to update _noCommandsProvided!
@@ -125,6 +126,7 @@ private:
int _focusPaneTarget{ -1 };
std::string _saveInputName;
std::string _keyChordOption;
std::string _openFilePath;
// Are you adding more args here? Make sure to reset them in _resetStateToDefault
const Commandline* _currentCommandline{ nullptr };
@@ -143,6 +145,7 @@ private:
void _addNewTerminalArgs(NewTerminalSubcommand& subcommand);
void _buildParser();
void _buildSaveSnippetParser();
void _buildOpenFileParser();
void _buildNewTabParser();
void _buildSplitPaneParser();
void _buildFocusTabParser();

View File

@@ -35,7 +35,14 @@ namespace winrt::TerminalApp::implementation
INewContentArgs MarkdownPaneContent::GetNewTerminalArgs(BuildStartupKind /*kind*/) const
{
return BaseContentArgs(L"x-markdown");
if (_filePath.empty())
{
return BaseContentArgs(L"x-markdown");
}
auto dataMap = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
dataMap.Insert(L"path", _filePath);
return BaseContentArgs(L"x-markdown", dataMap.GetView());
}
void MarkdownPaneContent::_clearOldNotebook()
@@ -153,6 +160,18 @@ namespace winrt::TerminalApp::implementation
PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"Editing" });
}
void MarkdownPaneContent::_saveTapped(const Windows::Foundation::IInspectable&, const Windows::UI::Xaml::Input::TappedRoutedEventArgs&)
{
if (_filePath.empty())
{
return;
}
const std::filesystem::path filePath{ std::wstring_view{ _filePath } };
const auto content = winrt::to_string(FileContents());
til::io::write_utf8_string_to_file(filePath, content);
}
void MarkdownPaneContent::_closeTapped(const Windows::Foundation::IInspectable&, const Windows::UI::Xaml::Input::TappedRoutedEventArgs&)
{
CloseRequested.raise(*this, nullptr);

View File

@@ -64,6 +64,7 @@ namespace winrt::TerminalApp::implementation
void _loadTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e);
void _editTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e);
void _saveTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e);
void _closeTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e);
};
}

View File

@@ -49,24 +49,28 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="FilePathInput"
x:Uid="MarkdownPaneFilePathInput"
Grid.Column="0"
Margin="4"
PlaceholderText="Enter a path to a markdown file..."
Text="Z:\dev\simple-test.md">
PlaceholderText="Enter a path to a markdown file...">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<StackPanel Grid.Column="1"
Orientation="Horizontal">
<Button Margin="4"
<Button x:Uid="MarkdownPaneOpenFileButton"
Margin="4"
AutomationProperties.Name="Open file"
Tapped="_loadTapped">
<FontIcon FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
FontSize="12"
Glyph="&#xe8e5;" />
<!-- OpenFile -->
</Button>
<Button Margin="4"
<Button x:Uid="MarkdownPaneEditButton"
Margin="4"
AutomationProperties.Name="Edit"
Tapped="_editTapped">
<FontIcon x:Name="EditIcon"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
@@ -74,7 +78,19 @@
Glyph="&#xe932;" />
<!-- Label -->
</Button>
<Button Margin="4"
<Button x:Uid="MarkdownPaneSaveButton"
Margin="4"
AutomationProperties.Name="Save"
Tapped="_saveTapped"
Visibility="{x:Bind Editing, Mode=OneWay}">
<FontIcon FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
FontSize="12"
Glyph="&#xe74e;" />
<!-- Save -->
</Button>
<Button x:Uid="MarkdownPaneCloseButton"
Margin="4"
AutomationProperties.Name="Close"
Tapped="_closeTapped">
<FontIcon FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
FontSize="12"
@@ -94,13 +110,6 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="InProcContent"
Grid.Column="0"
Padding="16"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#ff0000" />
<TextBox x:Name="_editor"
Grid.Column="1"
Padding="3"
@@ -110,7 +119,7 @@
FontFamily="Cascadia Code"
IsSpellCheckEnabled="False"
Text="{x:Bind FileContents, Mode=TwoWay}"
Visibility="{x:Bind Editing}">
Visibility="{x:Bind Editing, Mode=OneWay}">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
@@ -126,7 +135,7 @@
IsVerticalScrollChainingEnabled="True">
<StackPanel x:Name="RenderedMarkdown"
Grid.Column="1"
Padding="16"
Padding="16,0,16,16"
HorizontalAlignment="Stretch"
Background="Transparent"
Orientation="Vertical" />

View File

@@ -302,6 +302,12 @@
<data name="KeyChordArgDesc" xml:space="preserve">
<value>An optional argument</value>
</data>
<data name="OpenFileDesc" xml:space="preserve">
<value>Open a file in a new pane</value>
</data>
<data name="OpenFilePathArgDesc" xml:space="preserve">
<value>The path to the file to open</value>
</data>
<data name="CmdFocusTabDesc" xml:space="preserve">
<value>Move focus to another tab</value>
</data>
@@ -923,4 +929,25 @@
<data name="InvalidRegex" xml:space="preserve">
<value>An invalid regular expression was found.</value>
</data>
<data name="MarkdownPaneFilePathInput.PlaceholderText" xml:space="preserve">
<value>Enter a path to a markdown file...</value>
</data>
<data name="MarkdownPaneOpenFileButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Open file</value>
</data>
<data name="MarkdownPaneOpenFileButton.AutomationProperties.Name" xml:space="preserve">
<value>Open file</value>
</data>
<data name="MarkdownPaneEditButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Edit</value>
</data>
<data name="MarkdownPaneEditButton.AutomationProperties.Name" xml:space="preserve">
<value>Edit</value>
</data>
<data name="MarkdownPaneCloseButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Close</value>
</data>
<data name="MarkdownPaneCloseButton.AutomationProperties.Name" xml:space="preserve">
<value>Close</value>
</data>
</root>

View File

@@ -3706,7 +3706,31 @@ namespace winrt::TerminalApp::implementation
{
if (Feature_MarkdownPane::IsEnabled())
{
const auto& markdownContent{ winrt::make_self<MarkdownPaneContent>(L"") };
// Extract the file path from the content args data map.
// If it's relative, resolve it against the virtual CWD
// (which is set by ProcessStartupActions to the caller's CWD).
winrt::hstring filePath;
if (const auto& data = contentArgs.Data())
{
if (data.HasKey(L"path"))
{
filePath = data.Lookup(L"path");
std::filesystem::path fsPath{ std::wstring_view{ filePath } };
if (fsPath.is_relative())
{
const auto cwd = _WindowProperties.VirtualWorkingDirectory();
if (!cwd.empty())
{
fsPath = std::filesystem::path{ std::wstring_view{ cwd } } / fsPath;
// now normalize it
fsPath = fsPath.lexically_normal();
filePath = winrt::hstring{ fsPath.wstring() };
}
}
}
}
const auto& markdownContent{ winrt::make_self<MarkdownPaneContent>(filePath) };
markdownContent->UpdateSettings(_settings);
markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler });

View File

@@ -333,14 +333,48 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
BaseContentArgs() :
BaseContentArgs(L"") {}
BaseContentArgs(winrt::hstring type, Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::hstring> data) :
_Type{ type },
_Data{ data } {}
ACTION_ARG(winrt::hstring, Type, L"");
static constexpr std::string_view TypeKey{ "type" };
public:
Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::hstring> Data() const noexcept
{
return _Data;
}
bool Equals(INewContentArgs other) const
{
return other.Type() == _Type;
if (other.Type() != _Type)
{
return false;
}
auto otherData = other.Data();
if (_Data == nullptr && otherData == nullptr)
{
return true;
}
if (_Data == nullptr || otherData == nullptr)
{
return false;
}
if (_Data.Size() != otherData.Size())
{
return false;
}
for (const auto& [key, value] : _Data)
{
if (!otherData.HasKey(key) || otherData.Lookup(key) != value)
{
return false;
}
}
return true;
}
size_t Hash() const
{
@@ -351,11 +385,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void Hash(til::hasher& h) const
{
h.write(Type());
if (_Data)
{
for (const auto& [key, value] : _Data)
{
h.write(key);
h.write(value);
}
}
}
INewContentArgs Copy() const
{
auto copy{ winrt::make_self<BaseContentArgs>() };
copy->_Type = _Type;
if (_Data)
{
auto dataCopy = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
for (const auto& [key, value] : _Data)
{
dataCopy.Insert(key, value);
}
copy->_Data = dataCopy.GetView();
}
return *copy;
}
winrt::hstring GenerateName() const { return GenerateName(GetLibraryResourceLoader().ResourceContext()); }
@@ -372,8 +423,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto args{ get_self<BaseContentArgs>(val) };
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, TypeKey, args->_Type);
if (args->_Data)
{
for (const auto& [key, value] : args->_Data)
{
json[winrt::to_string(key)] = winrt::to_string(value);
}
}
return json;
}
private:
Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::hstring> _Data{ nullptr };
};
// Although it may _seem_ like NewTerminalArgs can use ACTION_ARG_BODY, it
@@ -398,6 +459,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
hstring GenerateName(const winrt::Windows::ApplicationModel::Resources::Core::ResourceContext&) const;
hstring ToCommandline() const;
// NewTerminalArgs doesn't use the Data map - it has its own specific properties
Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::hstring> Data() const noexcept
{
return nullptr;
}
bool Equals(const Model::INewContentArgs& other)
{
auto otherAsUs = other.try_as<NewTerminalArgs>();
@@ -513,9 +580,23 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return { terminalArgs, {} };
}
// For now, we don't support any other concrete types of content
// with args. Just return a placeholder type that only includes the type
return { *winrt::make_self<BaseContentArgs>(type), {} };
// Extract all other keys (except "type") into a data map
auto dataMap = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
for (auto it = json.begin(); it != json.end(); ++it)
{
const auto key = it.name();
if (key != "type" && it->isString())
{
dataMap.Insert(winrt::to_hstring(key), winrt::to_hstring(it->asString()));
}
}
auto baseArgs = winrt::make_self<BaseContentArgs>(type);
if (dataMap.Size() > 0)
{
baseArgs = winrt::make_self<BaseContentArgs>(type, dataMap.GetView());
}
return { *baseArgs, {} };
}
static Json::Value ContentArgsToJson(const Model::INewContentArgs& contentArgs)
{
@@ -529,9 +610,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return winrt::Microsoft::Terminal::Settings::Model::implementation::NewTerminalArgs::ToJson(contentArgs.try_as<Model::NewTerminalArgs>());
}
// For now, we don't support any other concrete types of content
// with args. Just return a placeholder.
auto base{ winrt::make_self<BaseContentArgs>(contentArgs.Type()) };
// For non-terminal content types, serialize via BaseContentArgs which handles the Data map
if (auto baseArgs = contentArgs.try_as<Model::BaseContentArgs>())
{
return BaseContentArgs::ToJson(baseArgs);
}
// Fallback: create a BaseContentArgs with the type and data
auto base = winrt::make_self<BaseContentArgs>(contentArgs.Type(), contentArgs.Data());
return BaseContentArgs::ToJson(*base);
}

View File

@@ -151,6 +151,7 @@ namespace Microsoft.Terminal.Settings.Model
interface INewContentArgs {
String Type { get; };
Windows.Foundation.Collections.IMapView<String, String> Data { get; };
Boolean Equals(INewContentArgs other);
UInt64 Hash();
INewContentArgs Copy();
@@ -161,6 +162,7 @@ namespace Microsoft.Terminal.Settings.Model
runtimeclass BaseContentArgs : [default] INewContentArgs {
BaseContentArgs();
BaseContentArgs(String type);
BaseContentArgs(String type, Windows.Foundation.Collections.IMapView<String, String> data);
};
runtimeclass NewTerminalArgs : INewContentArgs, IActionArgsDescriptorAccess {