mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-09 02:26:45 +00:00
This pull request broadly rewrites how we handle all media resources in the Terminal settings model. ## What is a media resource? A media resource is any JSON property that refers to a file on disk, including: - `icon` on profile - `backgroundImage` on profile (appearance) - `pixelShaderPath` and `pixelShaderImagePath` on profile (appearance) - `icon` on command and the new tab menu entries The last two bullet points were newly discovered during the course of this work. ## Description of Changes In every place the settings model used to store a string for a media path, it now stores an `IMediaResource`. A media resource must be _resolved_ before it's used. When resolved, it can report whether it is `Ok` (found, valid) and what the final normalized path was. This allows the settings model to apply some new behaviors. One of those new behaviors is resolving media paths _relative to the JSON file that referred to them._ This means fragments and user settings can now contain _local_ images, pixel shaders and more and refer to them by filename. Relative path support requires us to track the path from which every media resource "container" was read[^2]. For "big" objects like Profile, we track it directly in the object and for each layer. This means that fragments **updating** a profile pass their relative base path into the mix. For some of the entries such as those in `newTabMenu`, we just wing it (#19191). For everything that is recursively owned by a parent that has a path (say each Command inside an ActionMap), we pass it in from the parent during media resolution. During resolution, we now track _exactly which layer_ an icon, background image, or pixel shader path came from and read the "base path" from only that layer. The base path is not inherited. Another new behavior is in the handling of web and other URLs. Canonical and a few other WSL distributors had to resort to web URLs for icons because we did not support loading them from the package. Julia tried to use `ms-appx://JuliaPackageNameHere/path/to/icon` for the same reason. Neither was intended, and of the two the second _should_ have worked but never could[^1]. For both `http(s?)` URLs and `ms-appx://` URLs which specify a package name, we now strip everything except the filename. As an example... If my fragment specifies `https://example.net/assets/foo.ico`, and my fragment was loaded from `C:\Fragments`, Terminal will look *only* at `C:\Fragments\foo.ico`. This works today for Julia (they put their icon in the fragment folder hoping that one day we would support this.) It will require some work from existing WSL distributors. I'm told that this is similar to how XML schema documents work. Now, icons are special. They support _Emoji_ and _Segoe Icons_. This PR adds an early pass to avoid resolving anything that looks like an emoji. This PR intentionally expands the heuristic definition of an emoji. It used to only cover 1-2 code unit emoji, which prevented the use of any emoji more complicated than "man in business suite levitating." An icon path will now be considered an emoji or symbol icon if it is composed of a single grapheme cluster (as measured by ICU.) This is not perfect, as it errs on the side of allowing too many things... but each of those things is technically a single grapheme cluster and is a perfectly legal FontIcon ;) Profile icons are _even more special_ than icons. They have an additional fallback behavior which we had to preserve. When a profile icon fails validation, or is expressly set to `null`, we fall back to the EXE specified in the command line. Because we do this fallback during resolution, _and the icon may be inherited by any higher profile,_ we can only resolve it against the commandline at the same level as the failed or nulled icon. Therefore, if you specify `icon: null` in your `defaults` profile, it will only ever resolve to `cmd.exe` for any profile that inherits it (unless you change `defaults.commandline`). This change expands support for the magic keywords `desktopWallpaper` and `none` to all media paths (yes, even `pixelShaderPath`... but also, `pixelShaderImagePath`!) It also expands support for _environment variables_ to all of those places. Yes, we had like forty different handlers for different types of string path. They are now uniform. ## Resource Validation Media resources which are not found are "rejected". If a rejected resource lives in _user_ settings, we will generate a warning and display it. In the future, we could detect this in the Settings UI and display a warning inline. ## Surprises I learned that `Windows.Foundation.Uri` parses file paths into `file://` URIs, but does not offer you a way to get the original file path back out. If you pass `C:\hello world`, _`Uri.Path`_ will return `/C:/hello%20world`. I kid you not. As a workaround, we bail out of URL handling if the `:` is too close to the start (indicating an absolute file path). ## Testing I added a narow test hook in the media resource resolver, which is removed completely by link-time code generation. It is a real joy. The test cases are all new and hopefully comprehensive. Closes #19075 Closes #16295 Closes #10359 (except it doesn't support fonts) Supersedes #16949 somewhat (`WT_SETTINGS_DIR`) Refs #18679 Refs #19215 (future work) Refs #19201 (future work) Refs #19191 (future work) [^1]: Handling a `ms-appx` path requires us to _add their package to our dependency graph_ for the entire duration during which the resource will be used. For us, that could be any time (like opening the command palette for the first time!) [^2]: We don't bother tracking where the defaults came from, because we control everything about them.
374 lines
16 KiB
C++
374 lines
16 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "TerminalPaneContent.h"
|
|
|
|
#include <mmsystem.h>
|
|
|
|
#include "../../types/inc/utils.hpp"
|
|
|
|
#include "BellEventArgs.g.cpp"
|
|
#include "TerminalPaneContent.g.cpp"
|
|
|
|
using namespace winrt::Windows::Foundation;
|
|
using namespace winrt::Windows::UI::Xaml;
|
|
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|
using namespace winrt::Microsoft::Terminal::Control;
|
|
using namespace winrt::Microsoft::Terminal::TerminalConnection;
|
|
|
|
namespace winrt::TerminalApp::implementation
|
|
{
|
|
TerminalPaneContent::TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
|
|
const TerminalApp::TerminalSettingsCache& cache,
|
|
const winrt::Microsoft::Terminal::Control::TermControl& control) :
|
|
_control{ control },
|
|
_cache{ cache },
|
|
_profile{ profile }
|
|
{
|
|
_setupControlEvents();
|
|
}
|
|
|
|
void TerminalPaneContent::_setupControlEvents()
|
|
{
|
|
_controlEvents._ConnectionStateChanged = _control.ConnectionStateChanged(winrt::auto_revoke, { this, &TerminalPaneContent::_controlConnectionStateChangedHandler });
|
|
_controlEvents._WarningBell = _control.WarningBell(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlWarningBellHandler });
|
|
_controlEvents._CloseTerminalRequested = _control.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_closeTerminalRequestedHandler });
|
|
_controlEvents._RestartTerminalRequested = _control.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_restartTerminalRequestedHandler });
|
|
|
|
_controlEvents._TitleChanged = _control.TitleChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTitleChanged });
|
|
_controlEvents._TabColorChanged = _control.TabColorChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTabColorChanged });
|
|
_controlEvents._SetTaskbarProgress = _control.SetTaskbarProgress(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlSetTaskbarProgress });
|
|
_controlEvents._ReadOnlyChanged = _control.ReadOnlyChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlReadOnlyChanged });
|
|
_controlEvents._FocusFollowMouseRequested = _control.FocusFollowMouseRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlFocusFollowMouseRequested });
|
|
}
|
|
void TerminalPaneContent::_removeControlEvents()
|
|
{
|
|
_controlEvents = {};
|
|
}
|
|
|
|
winrt::Windows::UI::Xaml::FrameworkElement TerminalPaneContent::GetRoot()
|
|
{
|
|
return _control;
|
|
}
|
|
winrt::Microsoft::Terminal::Control::TermControl TerminalPaneContent::GetTermControl()
|
|
{
|
|
return _control;
|
|
}
|
|
winrt::Windows::Foundation::Size TerminalPaneContent::MinimumSize()
|
|
{
|
|
return _control.MinimumSize();
|
|
}
|
|
void TerminalPaneContent::Focus(winrt::Windows::UI::Xaml::FocusState reason)
|
|
{
|
|
_control.Focus(reason);
|
|
}
|
|
void TerminalPaneContent::Close()
|
|
{
|
|
_removeControlEvents();
|
|
|
|
// Clear out our media player callbacks, and stop any playing media. This
|
|
// will prevent the callback from being triggered after we've closed, and
|
|
// also make sure that our sound stops when we're closed.
|
|
if (_bellPlayer)
|
|
{
|
|
_bellPlayer.Pause();
|
|
_bellPlayer.Source(nullptr);
|
|
_bellPlayer.Close();
|
|
_bellPlayer = nullptr;
|
|
_bellPlayerCreated = false;
|
|
}
|
|
}
|
|
|
|
winrt::hstring TerminalPaneContent::Icon() const
|
|
{
|
|
return _profile.Icon().Resolved();
|
|
}
|
|
|
|
Windows::Foundation::IReference<winrt::Windows::UI::Color> TerminalPaneContent::TabColor() const noexcept
|
|
{
|
|
return _control.TabColor();
|
|
}
|
|
|
|
INewContentArgs TerminalPaneContent::GetNewTerminalArgs(const BuildStartupKind kind) const
|
|
{
|
|
NewTerminalArgs args{};
|
|
const auto& controlSettings = _control.Settings();
|
|
|
|
args.Profile(::Microsoft::Console::Utils::GuidToString(_profile.Guid()));
|
|
// If we know the user's working directory use it instead of the profile.
|
|
if (const auto dir = _control.WorkingDirectory(); !dir.empty())
|
|
{
|
|
args.StartingDirectory(dir);
|
|
}
|
|
else
|
|
{
|
|
args.StartingDirectory(controlSettings.StartingDirectory());
|
|
}
|
|
args.TabTitle(controlSettings.StartingTitle());
|
|
args.Commandline(controlSettings.Commandline());
|
|
args.SuppressApplicationTitle(controlSettings.SuppressApplicationTitle());
|
|
if (controlSettings.TabColor() || controlSettings.StartingTabColor())
|
|
{
|
|
til::color c;
|
|
// StartingTabColor is prioritized over other colors
|
|
if (const auto color = controlSettings.StartingTabColor())
|
|
{
|
|
c = til::color(color.Value());
|
|
}
|
|
else
|
|
{
|
|
c = til::color(controlSettings.TabColor().Value());
|
|
}
|
|
|
|
args.TabColor(winrt::Windows::Foundation::IReference<winrt::Windows::UI::Color>{ static_cast<winrt::Windows::UI::Color>(c) });
|
|
}
|
|
|
|
// TODO:GH#9800 - we used to be able to persist the color scheme that a
|
|
// TermControl was initialized with, by name. With the change to having the
|
|
// control own its own copy of its settings, this isn't possible anymore.
|
|
//
|
|
// We may be able to get around this by storing the Name in the Core::Scheme
|
|
// object. That would work for schemes set by the Terminal, but not ones set
|
|
// by VT, but that seems good enough.
|
|
|
|
switch (kind)
|
|
{
|
|
case BuildStartupKind::Content:
|
|
case BuildStartupKind::MovePane:
|
|
// Only fill in the ContentId if absolutely needed. If you fill in a number
|
|
// here (even 0), we'll serialize that number, AND treat that action as an
|
|
// "attach existing" rather than a "create"
|
|
args.ContentId(_control.ContentId());
|
|
break;
|
|
case BuildStartupKind::PersistAll:
|
|
{
|
|
const auto connection = _control.Connection();
|
|
const auto id = connection ? connection.SessionId() : winrt::guid{};
|
|
|
|
if (id != winrt::guid{})
|
|
{
|
|
const auto settingsDir = CascadiaSettings::SettingsDirectory();
|
|
const auto idStr = ::Microsoft::Console::Utils::GuidToPlainString(id);
|
|
const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr);
|
|
_control.PersistToPath(path);
|
|
args.SessionId(id);
|
|
}
|
|
break;
|
|
}
|
|
case BuildStartupKind::PersistLayout:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
void TerminalPaneContent::_controlTitleChanged(const IInspectable&, const IInspectable&)
|
|
{
|
|
TitleChanged.raise(*this, nullptr);
|
|
}
|
|
void TerminalPaneContent::_controlTabColorChanged(const IInspectable&, const IInspectable&)
|
|
{
|
|
TabColorChanged.raise(*this, nullptr);
|
|
}
|
|
void TerminalPaneContent::_controlSetTaskbarProgress(const IInspectable&, const IInspectable&)
|
|
{
|
|
TaskbarProgressChanged.raise(*this, nullptr);
|
|
}
|
|
void TerminalPaneContent::_controlReadOnlyChanged(const IInspectable&, const IInspectable&)
|
|
{
|
|
ReadOnlyChanged.raise(*this, nullptr);
|
|
}
|
|
void TerminalPaneContent::_controlFocusFollowMouseRequested(const IInspectable&, const IInspectable&)
|
|
{
|
|
FocusRequested.raise(*this, nullptr);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Called when our attached control is closed. Triggers listeners to our close
|
|
// event, if we're a leaf pane.
|
|
// - If this was called, and we became a parent pane (due to work on another
|
|
// thread), this function will do nothing (allowing the control's new parent
|
|
// to handle the event instead).
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
safe_void_coroutine TerminalPaneContent::_controlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender,
|
|
const winrt::Windows::Foundation::IInspectable& args)
|
|
{
|
|
ConnectionStateChanged.raise(sender, args);
|
|
auto newConnectionState = ConnectionState::Closed;
|
|
if (const auto coreState = sender.try_as<ICoreState>())
|
|
{
|
|
newConnectionState = coreState.ConnectionState();
|
|
}
|
|
|
|
const auto previousConnectionState = std::exchange(_connectionState, newConnectionState);
|
|
if (newConnectionState < ConnectionState::Closed)
|
|
{
|
|
// Pane doesn't care if the connection isn't entering a terminal state.
|
|
co_return;
|
|
}
|
|
|
|
const auto weakThis = get_weak();
|
|
co_await wil::resume_foreground(_control.Dispatcher());
|
|
const auto strongThis = weakThis.get();
|
|
if (!strongThis)
|
|
{
|
|
co_return;
|
|
}
|
|
|
|
// It's possible that this event handler started being executed, scheduled
|
|
// on the UI thread, another child got created. So our control is
|
|
// actually no longer _our_ control, and instead could be a descendant.
|
|
//
|
|
// When the control's new Pane takes ownership of the control, the new
|
|
// parent will register its own event handler. That event handler will get
|
|
// fired after this handler returns, and will properly cleanup state.
|
|
|
|
if (previousConnectionState < ConnectionState::Connected && newConnectionState >= ConnectionState::Failed)
|
|
{
|
|
// A failure to complete the connection (before it has _connected_) is not covered by "closeOnExit".
|
|
// This is to prevent a misconfiguration (closeOnExit: always, startingDirectory: garbage) resulting
|
|
// in Terminal flashing open and immediately closed.
|
|
co_return;
|
|
}
|
|
|
|
if (_profile)
|
|
{
|
|
const auto mode = _profile.CloseOnExit();
|
|
|
|
if (
|
|
// This one is obvious: If the user asked for "always" we do just that.
|
|
(mode == CloseOnExitMode::Always) ||
|
|
// Otherwise, and unless the user asked for the opposite of "always",
|
|
// close the pane when the connection closed gracefully (not failed).
|
|
(mode != CloseOnExitMode::Never && newConnectionState == ConnectionState::Closed) ||
|
|
// However, defterm handoff can result in Windows Terminal randomly opening which may be annoying,
|
|
// so by default we should at least always close the pane, even if the command failed.
|
|
// See GH #13325 for discussion.
|
|
(mode == CloseOnExitMode::Automatic && _isDefTermSession))
|
|
{
|
|
CloseRequested.raise(nullptr, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Plays a warning note when triggered by the BEL control character,
|
|
// using the sound configured for the "Critical Stop" system event.`
|
|
// This matches the behavior of the Windows Console host.
|
|
// - Will also flash the taskbar if the bellStyle setting for this profile
|
|
// has the 'visual' flag set
|
|
// Arguments:
|
|
// - <unused>
|
|
void TerminalPaneContent::_controlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
|
const winrt::Windows::Foundation::IInspectable& /*eventArgs*/)
|
|
{
|
|
if (_profile)
|
|
{
|
|
// We don't want to do anything if nothing is set, so check for that first
|
|
if (static_cast<int>(_profile.BellStyle()) != 0)
|
|
{
|
|
if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible))
|
|
{
|
|
// Audible is set, play the sound
|
|
auto sounds{ _profile.BellSound() };
|
|
if (sounds && sounds.Size() > 0)
|
|
{
|
|
winrt::hstring soundPath{ wil::ExpandEnvironmentStringsW<std::wstring>(sounds.GetAt(rand() % sounds.Size()).c_str()) };
|
|
winrt::Windows::Foundation::Uri uri{ soundPath };
|
|
_playBellSound(uri);
|
|
}
|
|
else
|
|
{
|
|
const auto soundAlias = reinterpret_cast<LPCTSTR>(SND_ALIAS_SYSTEMHAND);
|
|
PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY);
|
|
}
|
|
}
|
|
|
|
if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window))
|
|
{
|
|
_control.BellLightOn();
|
|
}
|
|
|
|
// raise the event with the bool value corresponding to the taskbar flag
|
|
BellRequested.raise(*this,
|
|
*winrt::make_self<TerminalApp::implementation::BellEventArgs>(WI_IsFlagSet(_profile.BellStyle(), BellStyle::Taskbar)));
|
|
}
|
|
}
|
|
}
|
|
|
|
safe_void_coroutine TerminalPaneContent::_playBellSound(winrt::Windows::Foundation::Uri uri)
|
|
{
|
|
auto weakThis{ get_weak() };
|
|
co_await wil::resume_foreground(_control.Dispatcher());
|
|
if (auto pane{ weakThis.get() })
|
|
{
|
|
if (!_bellPlayerCreated)
|
|
{
|
|
// The MediaPlayer might not exist on Windows N SKU.
|
|
try
|
|
{
|
|
_bellPlayerCreated = true;
|
|
_bellPlayer = winrt::Windows::Media::Playback::MediaPlayer();
|
|
// GH#12258: The media keys (like play/pause) should have no effect on our bell sound.
|
|
_bellPlayer.CommandManager().IsEnabled(false);
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
if (_bellPlayer)
|
|
{
|
|
const auto source{ winrt::Windows::Media::Core::MediaSource::CreateFromUri(uri) };
|
|
const auto item{ winrt::Windows::Media::Playback::MediaPlaybackItem(source) };
|
|
_bellPlayer.Source(item);
|
|
_bellPlayer.Play();
|
|
}
|
|
}
|
|
}
|
|
void TerminalPaneContent::_closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
|
const winrt::Windows::Foundation::IInspectable& /*args*/)
|
|
{
|
|
CloseRequested.raise(nullptr, nullptr);
|
|
}
|
|
|
|
void TerminalPaneContent::_restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
|
const winrt::Windows::Foundation::IInspectable& /*args*/)
|
|
{
|
|
RestartTerminalRequested.raise(*this, nullptr);
|
|
}
|
|
|
|
void TerminalPaneContent::UpdateSettings(const CascadiaSettings& /*settings*/)
|
|
{
|
|
if (const auto& settings{ _cache.TryLookup(_profile) })
|
|
{
|
|
_control.UpdateControlSettings(settings.DefaultSettings(), settings.UnfocusedSettings());
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Should be called when this pane is created via a default terminal handoff
|
|
// - Finalizes our configuration given the information that we have been
|
|
// created via default handoff
|
|
void TerminalPaneContent::MarkAsDefterm()
|
|
{
|
|
_isDefTermSession = true;
|
|
}
|
|
|
|
winrt::Windows::UI::Xaml::Media::Brush TerminalPaneContent::BackgroundBrush()
|
|
{
|
|
return _control.BackgroundBrush();
|
|
}
|
|
|
|
float TerminalPaneContent::SnapDownToGrid(const TerminalApp::PaneSnapDirection direction, const float sizeToSnap)
|
|
{
|
|
return _control.SnapDimensionToGrid(direction == PaneSnapDirection::Width, sizeToSnap);
|
|
}
|
|
Windows::Foundation::Size TerminalPaneContent::GridUnitSize()
|
|
{
|
|
return _control.CharacterDimensions();
|
|
}
|
|
}
|