// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "AppLogic.h" #include "../inc/WindowingBehavior.h" #include "AppLogic.g.cpp" #include "FindTargetWindowResult.g.cpp" #include #include using namespace winrt::Windows::ApplicationModel; using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::System; using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Control; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::TerminalApp; namespace winrt { namespace MUX = Microsoft::UI::Xaml; using IInspectable = Windows::Foundation::IInspectable; } static const winrt::hstring StartupTaskName = L"StartTerminalOnLoginTask"; // clang-format off // !!! IMPORTANT !!! // Make sure that these keys are in the same order as the // SettingsLoadWarnings/Errors enum is! static const std::array(SettingsLoadWarnings::WARNINGS_SIZE)> settingsLoadWarningsLabels { USES_RESOURCE(L"MissingDefaultProfileText"), USES_RESOURCE(L"DuplicateProfileText"), USES_RESOURCE(L"UnknownColorSchemeText"), USES_RESOURCE(L"InvalidBackgroundImage"), USES_RESOURCE(L"InvalidIcon"), USES_RESOURCE(L"AtLeastOneKeybindingWarning"), USES_RESOURCE(L"TooManyKeysForChord"), USES_RESOURCE(L"MissingRequiredParameter"), USES_RESOURCE(L"LegacyGlobalsProperty"), USES_RESOURCE(L"FailedToParseCommandJson"), USES_RESOURCE(L"FailedToWriteToSettings"), USES_RESOURCE(L"InvalidColorSchemeInCmd"), USES_RESOURCE(L"InvalidSplitSize"), USES_RESOURCE(L"FailedToParseStartupActions"), USES_RESOURCE(L"FailedToParseSubCommands"), }; static const std::array(SettingsLoadErrors::ERRORS_SIZE)> settingsLoadErrorsLabels { USES_RESOURCE(L"NoProfilesText"), USES_RESOURCE(L"AllProfilesHiddenText") }; // clang-format on // Function Description: // - General-purpose helper for looking up a localized string for a // warning/error. First will look for the given key in the provided map of // keys->strings, where the values in the map are ResourceKeys. If it finds // one, it will lookup the localized string from that ResourceKey. // - If it does not find a key, it'll return an empty string // Arguments: // - key: the value to use to look for a resource key in the given map // - map: A map of keys->Resource keys. // Return Value: // - the localized string for the given type, if it exists. template static winrt::hstring _GetMessageText(uint32_t index, std::array keys) { if (index < keys.size()) { return GetLibraryResourceString(keys.at(index)); } return {}; } // Function Description: // - Gets the text from our ResourceDictionary for the given // SettingsLoadWarning. If there is no such text, we'll return nullptr. // - The warning should have an entry in settingsLoadWarningsLabels. // Arguments: // - warning: the SettingsLoadWarnings value to get the localized text for. // Return Value: // - localized text for the given warning static winrt::hstring _GetWarningText(SettingsLoadWarnings warning) { return _GetMessageText(static_cast(warning), settingsLoadWarningsLabels); } // Function Description: // - Gets the text from our ResourceDictionary for the given // SettingsLoadError. If there is no such text, we'll return nullptr. // - The warning should have an entry in settingsLoadErrorsLabels. // Arguments: // - error: the SettingsLoadErrors value to get the localized text for. // Return Value: // - localized text for the given error static winrt::hstring _GetErrorText(SettingsLoadErrors error) { return _GetMessageText(static_cast(error), settingsLoadErrorsLabels); } // Function Description: // - Creates a Run of text to display an error message. The text is yellow or // red for dark/light theme, respectively. // Arguments: // - text: The text of the error message. // - resources: The application's resource loader. // Return Value: // - The fully styled text run. static Documents::Run _BuildErrorRun(const winrt::hstring& text, const ResourceDictionary& resources) { Documents::Run textRun; textRun.Text(text); // Color the text red (light theme) or yellow (dark theme) based on the system theme winrt::IInspectable key = winrt::box_value(L"ErrorTextBrush"); if (resources.HasKey(key)) { winrt::IInspectable g = resources.Lookup(key); auto brush = g.try_as(); textRun.Foreground(brush); } return textRun; } // Method Description: // - Returns whether the user is either a member of the Administrators group or // is currently elevated. // Return Value: // - true if the user is an administrator static bool _isUserAdmin() noexcept try { SID_IDENTIFIER_AUTHORITY ntAuthority{ SECURITY_NT_AUTHORITY }; wil::unique_sid adminGroupSid{}; THROW_IF_WIN32_BOOL_FALSE(AllocateAndInitializeSid(&ntAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminGroupSid)); BOOL b; THROW_IF_WIN32_BOOL_FALSE(CheckTokenMembership(NULL, adminGroupSid.get(), &b)); return !!b; } catch (...) { LOG_CAUGHT_EXCEPTION(); return false; } namespace winrt::TerminalApp::implementation { // Function Description: // - Get the AppLogic for the current active Xaml application, or null if there isn't one. // Return value: // - A pointer (bare) to the AppLogic, or nullptr. The app logic outlives all other objects, // unless the application is in a terrible way, so this is "safe." AppLogic* AppLogic::Current() noexcept try { if (auto currentXamlApp{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) { if (auto appLogicPointer{ winrt::get_self(currentXamlApp.Logic()) }) { return appLogicPointer; } } return nullptr; } catch (...) { LOG_CAUGHT_EXCEPTION(); return nullptr; } // Method Description: // - Returns the settings currently in use by the entire Terminal application. // - IMPORTANT! This can throw! Make sure to try/catch this, so that the // LocalTests don't crash (because their Application::Current() won't be a // AppLogic) // Throws: // - HR E_INVALIDARG if the app isn't up and running. const CascadiaSettings AppLogic::CurrentAppSettings() { auto appLogic{ ::winrt::TerminalApp::implementation::AppLogic::Current() }; THROW_HR_IF_NULL(E_INVALIDARG, appLogic); return appLogic->GetSettings(); } AppLogic::AppLogic() : _dialogLock{}, _loadedInitialSettings{ false }, _settingsLoadedResult{ S_OK } { // For your own sanity, it's better to do setup outside the ctor. // If you do any setup in the ctor that ends up throwing an exception, // then it might look like App just failed to activate, which will // cause you to chase down the rabbit hole of "why is App not // registered?" when it definitely is. // The TerminalPage has to be constructed during our construction, to // make sure that there's a terminal page for callers of // SetTitleBarContent _isElevated = _isUserAdmin(); _root = winrt::make_self(); } // Method Description: // - Implements the IInitializeWithWindow interface from shobjidl_core. HRESULT AppLogic::Initialize(HWND hwnd) { return _root->Initialize(hwnd); } // Method Description: // - Called around the codebase to discover if this is a UWP where we need to turn off specific settings. // Arguments: // - - reports internal state // Return Value: // - True if UWP, false otherwise. bool AppLogic::IsUwp() const noexcept { return _isUwp; } // Method Description: // - Called around the codebase to discover if Terminal is running elevated // Arguments: // - - reports internal state // Return Value: // - True if elevated, false otherwise. bool AppLogic::IsElevated() const noexcept { return _isElevated; } // Method Description: // - Called by UWP context invoker to let us know that we may have to change some of our behaviors // for being a UWP // Arguments: // - (sets to UWP = true, one way change) // Return Value: // - void AppLogic::RunAsUwp() { _isUwp = true; } // Method Description: // - Build the UI for the terminal app. Before this method is called, it // should not be assumed that the TerminalApp is usable. The Settings // should be loaded before this is called, either with LoadSettings or // GetLaunchDimensions (which will call LoadSettings) // Arguments: // - // Return Value: // - void AppLogic::Create() { // Assert that we've already loaded our settings. We have to do // this as a MTA, before the app is Create()'d WINRT_ASSERT(_loadedInitialSettings); _root->DialogPresenter(*this); // In UWP mode, we cannot handle taking over the title bar for tabs, // so this setting is overridden to false no matter what the preference is. if (_isUwp) { _settings.GlobalSettings().ShowTabsInTitlebar(false); } // Pay attention, that even if some command line arguments were parsed (like launch mode), // we will not use the startup actions from settings. // While this simplifies the logic, we might want to reconsider this behavior in the future. if (!_hasCommandLineArguments && _hasSettingsStartupActions) { _root->SetStartupActions(_settingsAppArgs.GetStartupActions()); } _root->SetSettings(_settings, false); _root->Loaded({ this, &AppLogic::_OnLoaded }); _root->Initialized([this](auto&&, auto&&) { // GH#288 - When we finish initialization, if the user wanted us // launched _fullscreen_, toggle fullscreen mode. This will make sure // that the window size is _first_ set up as something sensible, so // leaving fullscreen returns to a reasonable size. // // We know at the start, that the root TerminalPage definitely isn't // in focus nor fullscreen mode. So "Toggle" here will always work // to "enable". const auto launchMode = this->GetLaunchMode(); if (IsQuakeWindow()) { _root->ToggleFocusMode(); } else if (launchMode == LaunchMode::FullscreenMode) { _root->ToggleFullscreen(); } else if (launchMode == LaunchMode::FocusMode || launchMode == LaunchMode::MaximizedFocusMode) { _root->ToggleFocusMode(); } }); _root->Create(); _ApplyLanguageSettingChange(); _ApplyTheme(_settings.GlobalSettings().Theme()); _ApplyStartupTaskStateChange(); TraceLoggingWrite( g_hTerminalAppProvider, "AppCreated", TraceLoggingDescription("Event emitted when the application is started"), TraceLoggingBool(_settings.GlobalSettings().ShowTabsInTitlebar(), "TabsInTitlebar"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } // Method Description: // - Show a ContentDialog with buttons to take further action. Uses the // FrameworkElements provided as the title and content of this dialog, and // displays buttons (or a single button). Two buttons (primary and secondary) // will be displayed if this is an warning dialog for closing the terminal, // this allows the users to abandon the closing action. Otherwise, a single // close button will be displayed. // - Only one dialog can be visible at a time. If another dialog is visible // when this is called, nothing happens. // Arguments: // - dialog: the dialog object that is going to show up // Return value: // - an IAsyncOperation with the dialog result winrt::Windows::Foundation::IAsyncOperation AppLogic::ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog) { // DON'T release this lock in a wil::scope_exit. The scope_exit will get // called when we await, which is not what we want. std::unique_lock lock{ _dialogLock, std::try_to_lock }; if (!lock) { // Another dialog is visible. co_return ContentDialogResult::None; } // IMPORTANT: This is necessary as documented in the ContentDialog MSDN docs. // Since we're hosting the dialog in a Xaml island, we need to connect it to the // xaml tree somehow. dialog.XamlRoot(_root->XamlRoot()); // IMPORTANT: Set the requested theme of the dialog, because the // PopupRoot isn't directly in the Xaml tree of our root. So the dialog // won't inherit our RequestedTheme automagically. // GH#5195, GH#3654 Because we cannot set RequestedTheme at the application level, // we occasionally run into issues where parts of our UI end up themed incorrectly. // Dialogs, for example, live under a different Xaml root element than the rest of // our application. This makes our popup menus and buttons "disappear" when the // user wants Terminal to be in a different theme than the rest of the system. // This hack---and it _is_ a hack--walks up a dialog's ancestry and forces the // theme on each element up to the root. We're relying a bit on Xaml's implementation // details here, but it does have the desired effect. // It's not enough to set the theme on the dialog alone. auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) { auto theme{ _settings.GlobalSettings().Theme() }; auto element{ sender.try_as() }; while (element) { element.RequestedTheme(theme); element = element.Parent().try_as(); } } }; themingLambda(dialog, nullptr); // if it's already in the tree auto loadedRevoker{ dialog.Loaded(winrt::auto_revoke, themingLambda) }; // if it's not yet in the tree // Display the dialog. co_return co_await dialog.ShowAsync(Controls::ContentDialogPlacement::Popup); // After the dialog is dismissed, the dialog lock (held by `lock`) will // be released so another can be shown } // Method Description: // - Displays a dialog for errors found while loading or validating the // settings. Uses the resources under the provided title and content keys // as the title and first content of the dialog, then also displays a // message for whatever exception was found while validating the settings. // - Only one dialog can be visible at a time. If another dialog is visible // when this is called, nothing happens. See ShowDialog for details // Arguments: // - titleKey: The key to use to lookup the title text from our resources. // - contentKey: The key to use to lookup the content text from our resources. void AppLogic::_ShowLoadErrorsDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey, HRESULT settingsLoadedResult) { auto title = GetLibraryResourceString(titleKey); auto buttonText = RS_(L"Ok"); Controls::TextBlock warningsTextBlock; // Make sure you can copy-paste warningsTextBlock.IsTextSelectionEnabled(true); // Make sure the lines of text wrap warningsTextBlock.TextWrapping(TextWrapping::Wrap); winrt::Windows::UI::Xaml::Documents::Run errorRun; const auto errorLabel = GetLibraryResourceString(contentKey); errorRun.Text(errorLabel); warningsTextBlock.Inlines().Append(errorRun); warningsTextBlock.Inlines().Append(Documents::LineBreak{}); if (FAILED(settingsLoadedResult)) { if (!_settingsLoadExceptionText.empty()) { warningsTextBlock.Inlines().Append(_BuildErrorRun(_settingsLoadExceptionText, ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Resources())); warningsTextBlock.Inlines().Append(Documents::LineBreak{}); } } // Add a note that we're using the default settings in this case. winrt::Windows::UI::Xaml::Documents::Run usingDefaultsRun; const auto usingDefaultsText = RS_(L"UsingDefaultSettingsText"); usingDefaultsRun.Text(usingDefaultsText); warningsTextBlock.Inlines().Append(Documents::LineBreak{}); warningsTextBlock.Inlines().Append(usingDefaultsRun); Controls::ContentDialog dialog; dialog.Title(winrt::box_value(title)); dialog.Content(winrt::box_value(warningsTextBlock)); dialog.CloseButtonText(buttonText); dialog.DefaultButton(Controls::ContentDialogButton::Close); ShowDialog(dialog); } // Method Description: // - Displays a dialog for warnings found while loading or validating the // settings. Displays messages for whatever warnings were found while // validating the settings. // - Only one dialog can be visible at a time. If another dialog is visible // when this is called, nothing happens. See ShowDialog for details void AppLogic::_ShowLoadWarningsDialog() { auto title = RS_(L"SettingsValidateErrorTitle"); auto buttonText = RS_(L"Ok"); Controls::TextBlock warningsTextBlock; // Make sure you can copy-paste warningsTextBlock.IsTextSelectionEnabled(true); // Make sure the lines of text wrap warningsTextBlock.TextWrapping(TextWrapping::Wrap); for (const auto& warning : _warnings) { // Try looking up the warning message key for each warning. const auto warningText = _GetWarningText(warning); if (!warningText.empty()) { warningsTextBlock.Inlines().Append(_BuildErrorRun(warningText, ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Resources())); // The "LegacyGlobalsProperty" warning is special - it has a URL // that goes with it. So we need to manually construct a // Hyperlink and insert it along with the warning text. if (warning == SettingsLoadWarnings::LegacyGlobalsProperty) { // Add the URL here too const auto legacyGlobalsLinkLabel = RS_(L"LegacyGlobalsPropertyHrefLabel"); const auto legacyGlobalsLinkUriValue = RS_(L"LegacyGlobalsPropertyHrefUrl"); winrt::Windows::UI::Xaml::Documents::Run legacyGlobalsLinkText; winrt::Windows::UI::Xaml::Documents::Hyperlink legacyGlobalsLink; winrt::Windows::Foundation::Uri legacyGlobalsLinkUri{ legacyGlobalsLinkUriValue }; legacyGlobalsLinkText.Text(legacyGlobalsLinkLabel); legacyGlobalsLink.NavigateUri(legacyGlobalsLinkUri); legacyGlobalsLink.Inlines().Append(legacyGlobalsLinkText); warningsTextBlock.Inlines().Append(legacyGlobalsLink); } warningsTextBlock.Inlines().Append(Documents::LineBreak{}); } } Controls::ContentDialog dialog; dialog.Title(winrt::box_value(title)); dialog.Content(winrt::box_value(warningsTextBlock)); dialog.CloseButtonText(buttonText); dialog.DefaultButton(Controls::ContentDialogButton::Close); ShowDialog(dialog); } // Method Description: // - Triggered when the application is finished loading. If we failed to load // the settings, then this will display the error dialog. This is done // here instead of when loading the settings, because we need our UI to be // visible to display the dialog, and when we're loading the settings, // the UI might not be visible yet. // Arguments: // - void AppLogic::_OnLoaded(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { if (_settings.GlobalSettings().InputServiceWarning()) { const auto keyboardServiceIsDisabled = !_IsKeyboardServiceEnabled(); if (keyboardServiceIsDisabled) { _root->ShowKeyboardServiceWarning(); } } if (FAILED(_settingsLoadedResult)) { const winrt::hstring titleKey = USES_RESOURCE(L"InitialJsonParseErrorTitle"); const winrt::hstring textKey = USES_RESOURCE(L"InitialJsonParseErrorText"); _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); } else if (_settingsLoadedResult == S_FALSE) { _ShowLoadWarningsDialog(); } } // Method Description: // - Helper for determining if the "Touch Keyboard and Handwriting Panel // Service" is enabled. If it isn't, we want to be able to display a // warning to the user, because they won't be able to type in the // Terminal. // Return Value: // - true if the service is enabled, or if we fail to query the service. We // return true in that case, to be less noisy (though, that is unexpected) bool AppLogic::_IsKeyboardServiceEnabled() { if (IsUwp()) { return true; } // If at any point we fail to open the service manager, the service, // etc, then just quick return true to disable the dialog. We'd rather // not be noisy with this dialog if we failed for some reason. // Open the service manager. This will return 0 if it failed. wil::unique_schandle hManager{ OpenSCManager(nullptr, nullptr, 0) }; if (LOG_LAST_ERROR_IF(!hManager.is_valid())) { return true; } // Get a handle to the keyboard service wil::unique_schandle hService{ OpenService(hManager.get(), TabletInputServiceKey.data(), SERVICE_QUERY_STATUS) }; if (LOG_LAST_ERROR_IF(!hService.is_valid())) { return true; } // Get the current state of the service SERVICE_STATUS status{ 0 }; if (!LOG_IF_WIN32_BOOL_FALSE(QueryServiceStatus(hService.get(), &status))) { return true; } const auto state = status.dwCurrentState; return (state == SERVICE_RUNNING || state == SERVICE_START_PENDING); } // Method Description: // - Get the size in pixels of the client area we'll need to launch this // terminal app. This method will use the default profile's settings to do // this calculation, as well as the _system_ dpi scaling. See also // TermControl::GetProposedDimensions. // Arguments: // - // Return Value: // - a point containing the requested dimensions in pixels. winrt::Windows::Foundation::Size AppLogic::GetLaunchDimensions(uint32_t dpi) { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } // Use the default profile to determine how big of a window we need. const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) }; auto proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), dpi); const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); // GH#2061 - If the global setting "Always show tab bar" is // set or if "Show tabs in title bar" is set, then we'll need to add // the height of the tab bar here. if (_settings.GlobalSettings().ShowTabsInTitlebar()) { // If we're showing the tabs in the titlebar, we need to use a // TitlebarControl here to calculate how much space to reserve. // // We'll create a fake TitlebarControl, and we'll propose an // available size to it with Measure(). After Measure() is called, // the TitlebarControl's DesiredSize will contain the _unscaled_ // size that the titlebar would like to use. We'll use that as part // of the height calculation here. auto titlebar = TitlebarControl{ static_cast(0) }; titlebar.Measure({ SHRT_MAX, SHRT_MAX }); proposedSize.Height += (titlebar.DesiredSize().Height) * scale; } else if (_settings.GlobalSettings().AlwaysShowTabs()) { // Otherwise, let's use a TabRowControl to calculate how much extra // space we'll need. // // Similarly to above, we'll measure it with an arbitrarily large // available space, to make sure we get all the space it wants. auto tabControl = TabRowControl(); tabControl.Measure({ SHRT_MAX, SHRT_MAX }); // For whatever reason, there's about 10px of unaccounted-for space // in the application. I couldn't tell you where these 10px are // coming from, but they need to be included in this math. proposedSize.Height += (tabControl.DesiredSize().Height + 10) * scale; } return proposedSize; } // Method Description: // - Get the launch mode in json settings file. Now there // two launch mode: default, maximized. Default means the window // will launch according to the launch dimensions provided. Maximized // means the window will launch as a maximized window // Arguments: // - // Return Value: // - LaunchMode enum that indicates the launch mode LaunchMode AppLogic::GetLaunchMode() { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } // GH#4620/#5801 - If the user passed --maximized or --fullscreen on the // commandline, then use that to override the value from the settings. const auto valueFromSettings = _settings.GlobalSettings().LaunchMode(); const auto valueFromCommandlineArgs = _appArgs.GetLaunchMode(); return valueFromCommandlineArgs.has_value() ? valueFromCommandlineArgs.value() : valueFromSettings; } // Method Description: // - Get the user defined initial position from Json settings file. // This position represents the top left corner of the Terminal window. // This setting is optional, if not provided, we will use the system // default size, which is provided in IslandWindow::MakeWindow. // Arguments: // - defaultInitialX: the system default x coordinate value // - defaultInitialY: the system default y coordinate value // Return Value: // - a point containing the requested initial position in pixels. TerminalApp::InitialPosition AppLogic::GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY) { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } const auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; return { initialPosition.X ? initialPosition.X.Value() : defaultInitialX, initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY }; } bool AppLogic::CenterOnLaunch() { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } return _settings.GlobalSettings().CenterOnLaunch(); } winrt::Windows::UI::Xaml::ElementTheme AppLogic::GetRequestedTheme() { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } return _settings.GlobalSettings().Theme(); } bool AppLogic::GetShowTabsInTitlebar() { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } return _settings.GlobalSettings().ShowTabsInTitlebar(); } bool AppLogic::GetInitialAlwaysOnTop() { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } return _settings.GlobalSettings().AlwaysOnTop(); } // Method Description: // - See Pane::CalcSnappedDimension float AppLogic::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { return _root->CalcSnappedDimension(widthOrHeight, dimension); } // Method Description: // - Attempt to load the settings. If we fail for any reason, returns an error. // Return Value: // - S_OK if we successfully parsed the settings, otherwise an appropriate HRESULT. [[nodiscard]] HRESULT AppLogic::_TryLoadSettings() noexcept { HRESULT hr = E_FAIL; try { auto newSettings = _isUwp ? CascadiaSettings::LoadUniversal() : CascadiaSettings::LoadAll(); _settings = newSettings; if (_settings.GetLoadingError()) { _settingsLoadExceptionText = _GetErrorText(_settings.GetLoadingError().Value()); return E_INVALIDARG; } else if (!_settings.GetSerializationErrorMessage().empty()) { _settingsLoadExceptionText = _settings.GetSerializationErrorMessage(); return E_INVALIDARG; } _warnings.clear(); for (uint32_t i = 0; i < _settings.Warnings().Size(); i++) { _warnings.push_back(_settings.Warnings().GetAt(i)); } _hasSettingsStartupActions = false; const auto startupActions = _settings.GlobalSettings().StartupActions(); if (!startupActions.empty()) { _settingsAppArgs.FullResetState(); ExecuteCommandlineArgs args{ _settings.GlobalSettings().StartupActions() }; auto result = _settingsAppArgs.ParseArgs(args); if (result == 0) { _hasSettingsStartupActions = true; // Validation also injects new-tab command if implicit new-tab was provided. _settingsAppArgs.ValidateStartupCommands(); } else { _warnings.push_back(SettingsLoadWarnings::FailedToParseStartupActions); } } hr = _warnings.empty() ? S_OK : S_FALSE; } catch (const winrt::hresult_error& e) { hr = e.code(); _settingsLoadExceptionText = e.message(); LOG_HR(hr); } catch (...) { hr = wil::ResultFromCaughtException(); LOG_HR(hr); } return hr; } // Method Description: // - Initialized our settings. See CascadiaSettings for more details. // Additionally hooks up our callbacks for keybinding events to the // keybindings object. // NOTE: This must be called from a MTA if we're running as a packaged // application. The Windows.Storage APIs require a MTA. If this isn't // happening during startup, it'll need to happen on a background thread. void AppLogic::LoadSettings() { auto start = std::chrono::high_resolution_clock::now(); TraceLoggingWrite( g_hTerminalAppProvider, "SettingsLoadStarted", TraceLoggingDescription("Event emitted before loading the settings"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); // Attempt to load the settings. // If it fails, // - use Default settings, // - don't persist them (LoadAll won't save them in this case). // - _settingsLoadedResult will be set to an error, indicating that // we should display the loading error. // * We can't display the error now, because we might not have a // UI yet. We'll display the error in _OnLoaded. _settingsLoadedResult = _TryLoadSettings(); if (FAILED(_settingsLoadedResult)) { _settings = CascadiaSettings::LoadDefaults(); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration delta = end - start; TraceLoggingWrite( g_hTerminalAppProvider, "SettingsLoadComplete", TraceLoggingDescription("Event emitted when loading the settings is finished"), TraceLoggingFloat64(delta.count(), "Duration"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); _loadedInitialSettings = true; // Register for directory change notification. _RegisterSettingsChange(); Jumplist::UpdateJumplist(_settings); } // Method Description: // - Registers for changes to the settings folder and upon a updated settings // profile calls _ReloadSettings(). // Arguments: // - // Return Value: // - void AppLogic::_RegisterSettingsChange() { // Get the containing folder. const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } }; const auto folder = settingsPath.parent_path(); _reader.create(folder.c_str(), false, wil::FolderChangeEvents::All, [this, settingsPath](wil::FolderChangeEvent event, PCWSTR fileModified) { // We want file modifications, AND when files are renamed to be // settings.json. This second case will oftentimes happen with text // editors, who will write a temp file, then rename it to be the // actual file you wrote. So listen for that too. if (!(event == wil::FolderChangeEvent::Modified || event == wil::FolderChangeEvent::RenameNewName || event == wil::FolderChangeEvent::Removed)) { return; } std::filesystem::path modifiedFilePath = fileModified; // Getting basename (filename.ext) const auto settingsBasename = settingsPath.filename(); const auto modifiedBasename = modifiedFilePath.filename(); if (settingsBasename == modifiedBasename) { this->_DispatchReloadSettings(); } }); } // Method Description: // - Dispatches a settings reload with debounce. // Text editors implement Save in a bunch of different ways, so // this stops us from reloading too many times or too quickly. fire_and_forget AppLogic::_DispatchReloadSettings() { static constexpr auto FileActivityQuiesceTime{ std::chrono::milliseconds(50) }; if (!_settingsReloadQueued.exchange(true)) { co_await winrt::resume_after(FileActivityQuiesceTime); _ReloadSettings(); _settingsReloadQueued.store(false); } } void AppLogic::_ApplyLanguageSettingChange() { using ApplicationLanguages = winrt::Windows::Globalization::ApplicationLanguages; const auto language = _settings.GlobalSettings().Language(); const auto primaryLanguageOverride = ApplicationLanguages::PrimaryLanguageOverride(); if (primaryLanguageOverride != language) { ApplicationLanguages::PrimaryLanguageOverride(language); } } fire_and_forget AppLogic::_LoadErrorsDialogRoutine() { co_await winrt::resume_foreground(_root->Dispatcher()); const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); } fire_and_forget AppLogic::_ShowLoadWarningsDialogRoutine() { co_await winrt::resume_foreground(_root->Dispatcher()); _ShowLoadWarningsDialog(); } fire_and_forget AppLogic::_RefreshThemeRoutine() { co_await winrt::resume_foreground(_root->Dispatcher()); // Refresh the UI theme _ApplyTheme(_settings.GlobalSettings().Theme()); } // Function Description: // Returns the current app package or nullptr. // TRANSITIONAL // Exists to work around a compiler bug. This function encapsulates the // exception handling that we used to keep around calls to Package::Current, // so that when it's called inside a coroutine and fails it doesn't explode // terribly. static winrt::Windows::ApplicationModel::Package GetCurrentPackageNoThrow() noexcept { try { return winrt::Windows::ApplicationModel::Package::Current(); } catch (...) { // discard any exception -- literally pretend we're not in a package } return nullptr; } fire_and_forget AppLogic::_ApplyStartupTaskStateChange() try { // First, make sure we're running in a packaged context. This method // won't work, and will crash mysteriously if we're running unpackaged. const auto package{ GetCurrentPackageNoThrow() }; if (package == nullptr) { return; } auto weakThis{ get_weak() }; co_await winrt::resume_foreground(_root->Dispatcher(), CoreDispatcherPriority::Normal); if (auto page{ weakThis.get() }) { StartupTaskState state; bool tryEnableStartupTask = _settings.GlobalSettings().StartOnUserLogin(); StartupTask task = co_await StartupTask::GetAsync(StartupTaskName); state = task.State(); switch (state) { case StartupTaskState::Disabled: { if (tryEnableStartupTask) { co_await task.RequestEnableAsync(); } break; } case StartupTaskState::DisabledByUser: { // TODO: GH#6254: define UX for other StartupTaskStates break; } case StartupTaskState::Enabled: { if (!tryEnableStartupTask) { task.Disable(); } break; } } } } CATCH_LOG(); // Method Description: // - Reloads the settings from the settings.json file. void AppLogic::_ReloadSettings() { // Attempt to load our settings. // If it fails, // - don't change the settings (and don't actually apply the new settings) // - don't persist them. // - display a loading error _settingsLoadedResult = _TryLoadSettings(); if (FAILED(_settingsLoadedResult)) { _LoadErrorsDialogRoutine(); return; } else if (_settingsLoadedResult == S_FALSE) { _ShowLoadWarningsDialogRoutine(); } // Here, we successfully reloaded the settings, and created a new // TerminalSettings object. // Update the settings in TerminalPage _root->SetSettings(_settings, true); _ApplyLanguageSettingChange(); _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); Jumplist::UpdateJumplist(_settings); _SettingsChangedHandlers(*this, nullptr); } // Method Description: // - Returns a pointer to the global shared settings. [[nodiscard]] CascadiaSettings AppLogic::GetSettings() const noexcept { return _settings; } // Method Description: // - Update the current theme of the application. This will trigger our // RequestedThemeChanged event, to have our host change the theme of the // root of the application. // Arguments: // - newTheme: The ElementTheme to apply to our elements. void AppLogic::_ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme) { // Propagate the event to the host layer, so it can update its own UI _RequestedThemeChangedHandlers(*this, newTheme); } UIElement AppLogic::GetRoot() noexcept { return _root.as(); } // Method Description: // - Gets the title of the currently focused terminal control. If there // isn't a control selected for any reason, returns "Windows Terminal" // Arguments: // - // Return Value: // - the title of the focused control if there is one, else "Windows Terminal" hstring AppLogic::Title() { if (_root) { return _root->Title(); } return { L"Windows Terminal" }; } // Method Description: // - Used to tell the app that the titlebar has been clicked. The App won't // actually receive any clicks in the titlebar area, so this is a helper // to clue the app in that a click has happened. The App will use this as // a indicator that it needs to dismiss any open flyouts. // Arguments: // - // Return Value: // - void AppLogic::TitlebarClicked() { if (_root) { _root->TitlebarClicked(); } } // Method Description: // - Implements the F7 handler (per GH#638) // - Implements the Alt handler (per GH#6421) // Return value: // - whether the key was handled bool AppLogic::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { if (_root) { // Manually bubble the OnDirectKeyEvent event up through the focus tree. auto xamlRoot{ _root->XamlRoot() }; auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) }; do { if (auto keyListener{ focusedObject.try_as() }) { if (keyListener.OnDirectKeyEvent(vkey, scanCode, down)) { return true; } // otherwise, keep walking. bubble the event manually. } if (auto focusedElement{ focusedObject.try_as() }) { focusedObject = focusedElement.Parent(); // Parent() seems to return null when the focusedElement is created from an ItemTemplate. // Use the VisualTreeHelper's GetParent as a fallback. if (!focusedObject) { focusedObject = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElement); } } else { break; // we hit a non-FE object, stop bubbling. } } while (focusedObject); } return false; } // Method Description: // - Used to tell the app that the 'X' button has been clicked and // the user wants to close the app. We kick off the close warning // experience. // Arguments: // - // Return Value: // - void AppLogic::WindowCloseButtonClicked() { if (_root) { _root->CloseWindow(); } } // Method Description: // - Gets the taskbar state value from the last active control // Return Value: // - The taskbar state of the last active control size_t AppLogic::GetLastActiveControlTaskbarState() { if (_root) { return _root->GetLastActiveControlTaskbarState(); } return {}; } // Method Description: // - Gets the taskbar progress value from the last active control // Return Value: // - The taskbar progress of the last active control size_t AppLogic::GetLastActiveControlTaskbarProgress() { if (_root) { return _root->GetLastActiveControlTaskbarProgress(); } return {}; } // Method Description: // - Sets the initial commandline to process on startup, and attempts to // parse it. Commands will be parsed into a list of ShortcutActions that // will be processed on TerminalPage::Create(). // - This function will have no effective result after Create() is called. // - This function returns 0, unless a there was a non-zero result from // trying to parse one of the commands provided. In that case, no commands // after the failing command will be parsed, and the non-zero code // returned. // Arguments: // - args: an array of strings to process as a commandline. These args can contain spaces // Return Value: // - the result of the first command who's parsing returned a non-zero code, // or 0. (see AppLogic::_ParseArgs) int32_t AppLogic::SetStartupCommandline(array_view args) { const auto result = _appArgs.ParseArgs(args); if (result == 0) { // If the size of the arguments list is 1, // then it contains only the executable name and no other arguments. _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); _root->SetStartupActions(_appArgs.GetStartupActions()); // Check if we were started as a COM server for inbound connections of console sessions // coming out of the operating system default application feature. If so, // tell TerminalPage to start the listener as we have to make sure it has the chance // to register a handler to hear about the requests first and is all ready to receive // them before the COM server registers itself. Otherwise, the request might come // in and be routed to an event with no handlers or a non-ready Page. if (_appArgs.IsHandoffListener()) { _root->SetInboundListener(true); } } return result; } // Method Description: // - Triggers the setup of the listener for incoming console connections // from the operating system. // Arguments: // - // Return Value: // - void AppLogic::SetInboundListener() { _root->SetInboundListener(false); } // Method Description: // - Parse the provided commandline arguments into actions, and try to // perform them immediately. // - This function returns 0, unless a there was a non-zero result from // trying to parse one of the commands provided. In that case, no commands // after the failing command will be parsed, and the non-zero code // returned. // - If a non-empty cwd is provided, the entire terminal exe will switch to // that CWD while we handle these actions, then return to the original // CWD. // Arguments: // - args: an array of strings to process as a commandline. These args can contain spaces // - cwd: The directory to use as the CWD while performing these actions. // Return Value: // - the result of the first command who's parsing returned a non-zero code, // or 0. (see AppLogic::_ParseArgs) int32_t AppLogic::ExecuteCommandline(array_view args, const winrt::hstring& cwd) { ::TerminalApp::AppCommandlineArgs appArgs; auto result = appArgs.ParseArgs(args); if (result == 0) { auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); _root->ProcessStartupActions(actions, false, cwd); } // Return the result of parsing with commandline, though it may or may not be used. return result; } // Method Description: // - Parse the given commandline args in an attempt to find the specified // window. The rest of the args are ignored for now (they'll be handled // whenever the commandline gets to the window it was intended for). // - Note that this function will only ever be called by the monarch. A // return value of `0` in this case does not mean "run the commandline in // _this_ process", rather it means "run the commandline in the current // process", whoever that may be. // Arguments: // - args: an array of strings to process as a commandline. These args can contain spaces // Return Value: // - 0: We should handle the args "in the current window". // - WindowingBehaviorUseNew: We should handle the args in a new window // - WindowingBehaviorUseExisting: We should handle the args "in // the current window ON THIS DESKTOP" // - WindowingBehaviorUseAnyExisting: We should handle the args "in the current // window ON ANY DESKTOP" // - anything else: We should handle the commandline in the window with the given ID. TerminalApp::FindTargetWindowResult AppLogic::FindTargetWindow(array_view args) { if (!_loadedInitialSettings) { // Load settings if we haven't already LoadSettings(); } return AppLogic::_doFindTargetWindow(args, _settings.GlobalSettings().WindowingBehavior()); } // The main body of this function is a static helper, to facilitate unit-testing TerminalApp::FindTargetWindowResult AppLogic::_doFindTargetWindow(array_view args, const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior) { ::TerminalApp::AppCommandlineArgs appArgs; const auto result = appArgs.ParseArgs(args); if (result == 0) { if (!appArgs.GetExitMessage().empty()) { return winrt::make(WindowingBehaviorUseNew); } const std::string parsedTarget{ appArgs.GetTargetWindow() }; // If the user did not provide any value on the commandline, // then lookup our windowing behavior to determine what to do // now. if (parsedTarget.empty()) { int32_t windowId = WindowingBehaviorUseNew; switch (windowingBehavior) { case WindowingMode::UseNew: windowId = WindowingBehaviorUseNew; break; case WindowingMode::UseExisting: windowId = WindowingBehaviorUseExisting; break; case WindowingMode::UseAnyExisting: windowId = WindowingBehaviorUseAnyExisting; break; } return winrt::make(windowId); } // Here, the user _has_ provided a window-id on the commandline. // What is it? Let's start by checking if it's an int, for the // window's ID: try { int32_t windowId = ::base::saturated_cast(std::stoi(parsedTarget)); // If the user provides _any_ negative number, then treat it as // -1, for "use a new window". if (windowId < 0) { windowId = -1; } // Hooray! This is a valid integer. The set of possible values // here is {-1, 0, ℤ+}. Let's return that window ID. return winrt::make(windowId); } catch (...) { // Value was not a valid int. It could be any other string to // use as a title though! // // First, check the reserved keywords: if (parsedTarget == "new") { return winrt::make(WindowingBehaviorUseNew); } else if (parsedTarget == "last") { return winrt::make(WindowingBehaviorUseExisting); } else { // The string they provided wasn't an int, it wasn't "new" // or "last", so whatever it is, that's the name they get. winrt::hstring winrtName{ til::u8u16(parsedTarget) }; return winrt::make(WindowingBehaviorUseName, winrtName); } } } // Any unsuccessful parse will be a new window. That new window will try // to handle the commandline itself, and find that the commandline // failed to parse. When that happens, the new window will display the // message box. // // This will also work for the case where the user specifies an invalid // commandline in conjunction with `-w 0`. This function will determine // that the commandline has a parse error, and indicate that we should // create a new window. Then, in that new window, we'll try to set the // StartupActions, which will again fail, returning the correct error // message. return winrt::make(WindowingBehaviorUseNew); } // Method Description: // - If there were any errors parsing the commandline that was used to // initialize the terminal, this will return a string containing that // message. If there were no errors, this message will be blank. // - If the user requested help on any command (using --help), this will // contain the help message. // - If the user requested the version number (using --version), this will // contain the version string. // Arguments: // - // Return Value: // - the help text or error message for the provided commandline, if one // exists, otherwise the empty string. winrt::hstring AppLogic::ParseCommandlineMessage() { return winrt::to_hstring(_appArgs.GetExitMessage()); } // Method Description: // - Returns true if we should exit the application before even starting the // window. We might want to do this if we're displaying an error message or // the version string, or if we want to open the settings file. // Arguments: // - // Return Value: // - true iff we should exit the application before even starting the window bool AppLogic::ShouldExitEarly() { return _appArgs.ShouldExitEarly(); } bool AppLogic::FocusMode() const { return _root ? _root->FocusMode() : false; } bool AppLogic::Fullscreen() const { return _root ? _root->Fullscreen() : false; } bool AppLogic::AlwaysOnTop() const { return _root ? _root->AlwaysOnTop() : false; } Windows::Foundation::Collections::IMapView AppLogic::GlobalHotkeys() { return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } void AppLogic::IdentifyWindow() { if (_root) { _root->IdentifyWindow(); } } winrt::hstring AppLogic::WindowName() { return _root ? _root->WindowName() : L""; } void AppLogic::WindowName(const winrt::hstring& name) { if (_root) { _root->WindowName(name); } } uint64_t AppLogic::WindowId() { return _root ? _root->WindowId() : 0; } void AppLogic::WindowId(const uint64_t& id) { if (_root) { _root->WindowId(id); } } void AppLogic::RenameFailed() { if (_root) { _root->RenameFailed(); } } bool AppLogic::IsQuakeWindow() const noexcept { return _root->IsQuakeWindow(); } }