chromium/chrome/browser/ash/guest_os/guest_os_terminal.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/guest_os/guest_os_terminal.h"

#include <string_view>

#include "ash/public/cpp/app_menu_constants.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/menu_item_constants.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_installer.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/ash/guest_os/public/guest_os_service.h"
#include "chrome/browser/ash/guest_os/public/guest_os_terminal_provider.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/full_restore_save_handler.h"
#include "components/app_restore/full_restore_utils.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/menu.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/url_util.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/window_open_disposition.h"
#include "ui/color/color_provider_manager.h"
#include "ui/color/color_provider_utils.h"
#include "ui/native_theme/native_theme.h"

namespace guest_os {

// web_app::GenerateAppId(/*manifest_id=*/std::nullopt,
//     GURL("chrome-untrusted://terminal/html/terminal.html"))
const char kTerminalSystemAppId[] = "fhicihalidkgcimdmhpohldehjmcabcf";

const char kTerminalHomePath[] = "html/terminal.html#home";

const char kShortcutKey[] = "shortcut";
const char kShortcutValueSSH[] = "ssh";
const char kShortcutValueTerminal[] = "terminal";
const char kProfileIdKey[] = "profileId";

namespace {

constexpr char kSettingPrefix[] = "/hterm/profiles/default/";
const size_t kSettingPrefixSize = std::size(kSettingPrefix) - 1;

constexpr char kSettingsProfileUrlParam[] = "settings_profile";
constexpr char kSettingsPrefixHterm[] = "hterm";
constexpr char kSettingsPrefixNassh[] = "nassh";
constexpr char kSettingsPrefixVsh[] = "vsh";
constexpr char kSettingsKeyBackgroundColor[] = "background-color";
constexpr char kSettingsKeyTerminalProfile[] = "terminal-profile";
constexpr char kSettingsProfileDefault[] = "default";
constexpr char kDefaultBackgroundColor[] = "#202124";

constexpr char kSettingPassCtrlW[] = "/hterm/profiles/default/pass-ctrl-w";
constexpr bool kDefaultPassCtrlW = false;

std::string GetSettingsKey(const std::string& prefix,
                           const std::string& profile,
                           const std::string& key) {
  return base::StrCat({"/", prefix, "/profiles/", profile, "/", key});
}

void LaunchTerminalImpl(Profile* profile,
                        const GURL& url,
                        apps::AppLaunchParams params) {
  // This function is called asynchronously, so we need to check whether
  // `profile` is still valid first.
  if (!g_browser_process) {
    LOG(WARNING) << "Abort launching terminal, invalid browser process.";
    return;
  }

  auto* profile_manager = g_browser_process->profile_manager();
  if (!profile_manager || !profile_manager->IsValidProfile(profile)) {
    LOG(WARNING) << "Abort launching terminal, invalid profile.";
    return;
  }

  // This LaunchSystemWebAppImpl call is necessary. Terminal App uses its
  // own CrostiniApps publisher for launching. Calling
  // LaunchSystemWebAppAsync would ask AppService to launch the App, which
  // routes the launch request to this function, resulting in a loop.
  //
  // System Web Apps managed by Web App publisher should call
  // LaunchSystemWebAppAsync.

  // Launch without a pinned home tab (settings page).
  if (params.disposition == WindowOpenDisposition::NEW_POPUP) {
    ash::LaunchSystemWebAppImpl(profile, ash::SystemWebAppType::TERMINAL, url,
                                params);
    return;
  }

  // TODO(crbug.com/1308961): Migrate to use PWA pinned home tab when ready.
  // If opening a new tab, first pin home tab.
  full_restore::FullRestoreSaveHandler::GetInstance();
  GURL home(GetTerminalHomeUrl());
  Browser* browser = ash::LaunchSystemWebAppImpl(
      profile, ash::SystemWebAppType::TERMINAL, home, params);
  if (!browser) {
    return;
  }
  if (url != home) {
    chrome::AddTabAt(browser, url, /*index=*/1, /*foreground=*/true);
  }
  auto info = std::make_unique<app_restore::AppLaunchInfo>(
      kTerminalSystemAppId, browser->session_id().id(), params.container,
      params.disposition, params.display_id, std::vector<base::FilePath>{},
      nullptr);
  full_restore::SaveAppLaunchInfo(browser->profile()->GetPath(),
                                  std::move(info));
}

}  // namespace

const std::string& GetTerminalHomeUrl() {
  static const base::NoDestructor<std::string> url(
      base::StrCat({chrome::kChromeUIUntrustedTerminalURL, kTerminalHomePath}));
  return *url;
}

GURL GenerateTerminalURL(Profile* profile,
                         const std::string& settings_profile,
                         const guest_os::GuestId& container_id,
                         const std::string& cwd,
                         const std::vector<std::string>& terminal_args) {
  auto escape = [](std::string param) {
    return base::EscapeQueryParamValue(param, /*use_plus=*/true);
  };
  std::string settings_profile_param;
  if (!settings_profile.empty()) {
    settings_profile_param = base::StrCat(
        {"&", kSettingsProfileUrlParam, "=", escape(settings_profile)});
  }
  std::string start = base::StrCat({chrome::kChromeUIUntrustedTerminalURL,
                                    "html/terminal.html?command=vmshell",
                                    settings_profile_param});
  std::string vm_name_param =
      escape(base::StringPrintf("--vm_name=%s", container_id.vm_name.c_str()));
  std::string container_name_param = escape(base::StringPrintf(
      "--target_container=%s", container_id.container_name.c_str()));
  std::string owner_id_param = escape(base::StringPrintf(
      "--owner_id=%s", crostini::CryptohomeIdForProfile(profile).c_str()));

  std::vector<std::string> pieces = {start, vm_name_param, container_name_param,
                                     owner_id_param};
  if (!cwd.empty()) {
    pieces.push_back(escape(base::StringPrintf("--cwd=%s", cwd.c_str())));
  }
  if (!terminal_args.empty()) {
    // Separates the command args from the args we are passing into the
    // terminal to be executed.
    pieces.push_back("--");
    for (auto arg : terminal_args) {
      pieces.push_back(escape(arg));
    }
  }

  return GURL(base::JoinString(pieces, "&args[]="));
}

void LaunchTerminal(Profile* profile,
                    int64_t display_id,
                    const guest_os::GuestId& container_id,
                    const std::string& cwd,
                    const std::vector<std::string>& terminal_args) {
  GURL url = GenerateTerminalURL(profile, /*settings_profile=*/std::string(),
                                 container_id, cwd, terminal_args);
  LaunchTerminalWithUrl(profile, display_id, /*restore_id=*/0, url);
}

void LaunchTerminalHome(Profile* profile, int64_t display_id, int restore_id) {
  LaunchTerminalWithUrl(profile, display_id, restore_id,
                        GURL(GetTerminalHomeUrl()));
}

void LaunchTerminalWithUrl(Profile* profile,
                           int64_t display_id,
                           int restore_id,
                           const GURL& url) {
  if (url.DeprecatedGetOriginAsURL() != chrome::kChromeUIUntrustedTerminalURL) {
    LOG(ERROR) << "Trying to launch terminal with an invalid url: " << url;
    return;
  }

  crostini::RecordAppLaunchHistogram(
      crostini::CrostiniAppLaunchAppType::kTerminal);
  auto params = ash::CreateSystemWebAppLaunchParams(
      profile, ash::SystemWebAppType::TERMINAL, display_id);
  if (!params.has_value()) {
    LOG(WARNING) << "Empty launch params for terminal";
    return;
  }

  // Terminal Home page will be restored by app service.
  params->omit_from_session_restore = true;

  params->restore_id = restore_id;

  // Always launch asynchronously to avoid disturbing the caller. See
  // https://crbug.com/1262890#c12 for more details.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(LaunchTerminalImpl, profile, url, std::move(*params)));
}

void LaunchTerminalWithIntent(
    Profile* profile,
    int64_t display_id,
    apps::IntentPtr intent,
    base::OnceCallback<void(bool, const std::string&)> callback) {
  // Look for vm_name and container_name in intent->extras, and for backcompat
  // reasons default to the original crostini container if nothing is specified.
  guest_os::GuestId guest_id = crostini::DefaultContainerId();

  // We only have vm and container name, so don't usually know the type. Don't
  // need it though so leave it as unknown.
  guest_id.vm_type = guest_os::VmType::UNKNOWN;
  std::string settings_profile;
  if (intent && !intent->extras.empty()) {
    for (const auto& extra : intent->extras) {
      if (extra.first == "vm_name") {
        guest_id.vm_name = extra.second;
      } else if (extra.first == "container_name") {
        guest_id.container_name = extra.second;
      } else if (extra.first == kSettingsProfileUrlParam) {
        settings_profile = extra.second;
      }
    }
  }

  auto* registry = guest_os::GuestOsService::GetForProfile(profile)
                       ->TerminalProviderRegistry();
  auto* provider = registry->Get(guest_id);

  if (!provider) {
    if (guest_id.vm_name == crostini::DefaultContainerId().vm_name &&
        !crostini::CrostiniFeatures::Get()->IsEnabled(profile)) {
      // It used to be that running the terminal without Crostini installed
      // would bring up the installer, so keep that behaviour. Only applies to
      // the default Crostini VM, anything else is only accessible if the target
      // VM is installed.
      auto* installer = crostini::CrostiniInstaller::GetForProfile(profile);
      if (installer) {
        installer->ShowDialog(crostini::CrostiniUISurface::kAppList);
      }
      return std::move(callback).Run(false, "Crostini not installed");
    } else {
      // Could happen if, e.g. a guest got disabled between listing and
      // selecting targets.
      return std::move(callback).Run(false, "Unrecognised Guest Id");
    }
  }

  std::string message;
  if (provider->RecoveryRequired(display_id)) {
    return std::move(callback).Run(false, "Recovery required");
  }

  // Use first file (if any) as cwd.
  std::string cwd;
  if (intent && !intent->files.empty()) {
    GURL gurl = intent->files[0]->url;
    storage::ExternalMountPoints* mount_points =
        storage::ExternalMountPoints::GetSystemInstance();
    storage::FileSystemURL url = mount_points->CrackURL(
        gurl, blink::StorageKey::CreateFirstParty(url::Origin::Create(gurl)));

    cwd = provider->PrepareCwd(url);
  }

  GURL url = GenerateTerminalURL(profile, settings_profile, guest_id, cwd,
                                 /*terminal_args=*/{});
  LaunchTerminalWithUrl(profile, display_id, /*restore_id=*/0, url);
  std::move(callback).Run(true, "");
}

void LaunchTerminalSettings(Profile* profile, int64_t display_id) {
  auto params = ash::CreateSystemWebAppLaunchParams(
      profile, ash::SystemWebAppType::TERMINAL, display_id);
  if (!params.has_value()) {
    LOG(WARNING) << "Empty launch params for terminal";
    return;
  }
  std::string path = "html/terminal_settings.html";
  // Use an app pop window to host the settings page.
  params->disposition = WindowOpenDisposition::NEW_POPUP;

  // Always launch asynchronously to avoid disturbing the caller. See
  // https://crbug.com/1262890#c12 for more details.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          LaunchTerminalImpl, profile,
          GURL(base::StrCat({chrome::kChromeUIUntrustedTerminalURL, path})),
          std::move(*params)));
}

void RecordTerminalSettingsChangesUMAs(Profile* profile) {
  static constexpr auto kSettingsMap = base::MakeFixedFlatMap<std::string_view,
                                                              TerminalSetting>({
      {"alt-gr-mode", TerminalSetting::kAltGrMode},
      {"alt-backspace-is-meta-backspace",
       TerminalSetting::kAltBackspaceIsMetaBackspace},
      {"alt-is-meta", TerminalSetting::kAltIsMeta},
      {"alt-sends-what", TerminalSetting::kAltSendsWhat},
      {"audible-bell-sound", TerminalSetting::kAudibleBellSound},
      {"desktop-notification-bell", TerminalSetting::kDesktopNotificationBell},
      {"background-color", TerminalSetting::kBackgroundColor},
      {"background-image", TerminalSetting::kBackgroundImage},
      {"background-size", TerminalSetting::kBackgroundSize},
      {"background-position", TerminalSetting::kBackgroundPosition},
      {"backspace-sends-backspace", TerminalSetting::kBackspaceSendsBackspace},
      {"character-map-overrides", TerminalSetting::kCharacterMapOverrides},
      {"close-on-exit", TerminalSetting::kCloseOnExit},
      {"cursor-blink", TerminalSetting::kCursorBlink},
      {"cursor-blink-cycle", TerminalSetting::kCursorBlinkCycle},
      {"cursor-shape", TerminalSetting::kCursorShape},
      {"cursor-color", TerminalSetting::kCursorColor},
      {"color-palette-overrides", TerminalSetting::kColorPaletteOverrides},
      {"copy-on-select", TerminalSetting::kCopyOnSelect},
      {"use-default-window-copy", TerminalSetting::kUseDefaultWindowCopy},
      {"clear-selection-after-copy", TerminalSetting::kClearSelectionAfterCopy},
      {"ctrl-plus-minus-zero-zoom", TerminalSetting::kCtrlPlusMinusZeroZoom},
      {"ctrl-c-copy", TerminalSetting::kCtrlCCopy},
      {"ctrl-v-paste", TerminalSetting::kCtrlVPaste},
      {"east-asian-ambiguous-as-two-column",
       TerminalSetting::kEastAsianAmbiguousAsTwoColumn},
      {"enable-8-bit-control", TerminalSetting::kEnable8BitControl},
      {"enable-bold", TerminalSetting::kEnableBold},
      {"enable-bold-as-bright", TerminalSetting::kEnableBoldAsBright},
      {"enable-blink", TerminalSetting::kEnableBlink},
      {"enable-clipboard-notice", TerminalSetting::kEnableClipboardNotice},
      {"enable-clipboard-write", TerminalSetting::kEnableClipboardWrite},
      {"enable-dec12", TerminalSetting::kEnableDec12},
      {"enable-csi-j-3", TerminalSetting::kEnableCsiJ3},
      {"environment", TerminalSetting::kEnvironment},
      {"font-family", TerminalSetting::kFontFamily},
      {"font-size", TerminalSetting::kFontSize},
      {"font-smoothing", TerminalSetting::kFontSmoothing},
      {"foreground-color", TerminalSetting::kForegroundColor},
      {"enable-resize-status", TerminalSetting::kEnableResizeStatus},
      {"hide-mouse-while-typing", TerminalSetting::kHideMouseWhileTyping},
      {"home-keys-scroll", TerminalSetting::kHomeKeysScroll},
      {"keybindings", TerminalSetting::kKeybindings},
      {"media-keys-are-fkeys", TerminalSetting::kMediaKeysAreFkeys},
      {"meta-sends-escape", TerminalSetting::kMetaSendsEscape},
      {"mouse-right-click-paste", TerminalSetting::kMouseRightClickPaste},
      {"mouse-paste-button", TerminalSetting::kMousePasteButton},
      {"word-break-match-left", TerminalSetting::kWordBreakMatchLeft},
      {"word-break-match-right", TerminalSetting::kWordBreakMatchRight},
      {"word-break-match-middle", TerminalSetting::kWordBreakMatchMiddle},
      {"page-keys-scroll", TerminalSetting::kPageKeysScroll},
      {"pass-alt-number", TerminalSetting::kPassAltNumber},
      {"pass-ctrl-number", TerminalSetting::kPassCtrlNumber},
      {"pass-ctrl-n", TerminalSetting::kPassCtrlN},
      {"pass-ctrl-t", TerminalSetting::kPassCtrlT},
      {"pass-ctrl-tab", TerminalSetting::kPassCtrlTab},
      {"pass-ctrl-w", TerminalSetting::kPassCtrlW},
      {"pass-meta-number", TerminalSetting::kPassMetaNumber},
      {"pass-meta-v", TerminalSetting::kPassMetaV},
      {"paste-on-drop", TerminalSetting::kPasteOnDrop},
      {"receive-encoding", TerminalSetting::kReceiveEncoding},
      {"scroll-on-keystroke", TerminalSetting::kScrollOnKeystroke},
      {"scroll-on-output", TerminalSetting::kScrollOnOutput},
      {"scrollbar-visible", TerminalSetting::kScrollbarVisible},
      {"scroll-wheel-may-send-arrow-keys",
       TerminalSetting::kScrollWheelMaySendArrowKeys},
      {"scroll-wheel-move-multiplier",
       TerminalSetting::kScrollWheelMoveMultiplier},
      {"terminal-encoding", TerminalSetting::kTerminalEncoding},
      {"shift-insert-paste", TerminalSetting::kShiftInsertPaste},
      {"user-css", TerminalSetting::kUserCss},
      {"user-css-text", TerminalSetting::kUserCssText},
      {"allow-images-inline", TerminalSetting::kAllowImagesInline},
      {"theme", TerminalSetting::kTheme},
      {"theme-variations", TerminalSetting::kThemeVariations},
      {"find-result-color", TerminalSetting::kFindResultColor},
      {"find-result-selected-color", TerminalSetting::kFindResultSelectedColor},
      {"line-height-padding-size", TerminalSetting::kLineHeightPaddingSize},
      {"keybindings-os-defaults", TerminalSetting::kKeybindingsOsDefaults},
      {"screen-padding-size", TerminalSetting::kScreenPaddingSize},
      {"screen-border-size", TerminalSetting::kScreenBorderSize},
      {"screen-border-color", TerminalSetting::kScreenBorderColor},
      {"line-height", TerminalSetting::kLineHeight},
  });

  const base::Value::Dict& settings =
      profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
  for (const auto item : settings) {
    // Only record settings for /hterm/profiles/default/.
    if (!base::StartsWith(item.first, kSettingPrefix,
                          base::CompareCase::SENSITIVE)) {
      continue;
    }
    const auto it = kSettingsMap.find(
        std::string_view(item.first).substr(kSettingPrefixSize));
    base::UmaHistogramEnumeration(
        "Crostini.TerminalSettingsChanged",
        it != kSettingsMap.end() ? it->second : TerminalSetting::kUnknown);
  }
}

std::string GetTerminalSettingBackgroundColor(
    Profile* profile,
    GURL url,
    std::optional<SkColor> opener_background_color) {
  auto key = [](const std::string& profile) {
    return GetSettingsKey(kSettingsPrefixHterm, profile,
                          kSettingsKeyBackgroundColor);
  };
  const base::Value::Dict& settings =
      profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
  // 1. Use 'settings_profile' url param.
  std::string settings_profile;
  if (net::GetValueForKeyInQuery(url, kSettingsProfileUrlParam,
                                 &settings_profile)) {
    const std::string* result = settings.FindString(key(settings_profile));
    if (result) {
      return *result;
    }
  }

  // 2. Use same color as opener.
  if (opener_background_color) {
    return ui::ConvertSkColorToCSSColor(*opener_background_color);
  }

  // 3. Use 'default' profile color, or default color.
  const std::string* result = settings.FindString(key(kSettingsProfileDefault));
  return result ? *result : kDefaultBackgroundColor;
}

bool GetTerminalSettingPassCtrlW(Profile* profile) {
  const base::Value::Dict& value =
      profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
  return value.FindBool(kSettingPassCtrlW).value_or(kDefaultPassCtrlW);
}

std::string ShortcutIdForSSH(const std::string& profileId) {
  base::Value::Dict dict;
  dict.Set(kShortcutKey, base::Value(kShortcutValueSSH));
  dict.Set(kProfileIdKey, base::Value(profileId));
  std::string shortcut_id;
  base::JSONWriter::Write(dict, &shortcut_id);
  return shortcut_id;
}

std::string ShortcutIdFromContainerId(Profile* profile,
                                      const guest_os::GuestId& id) {
  base::Value::Dict dict = id.ToDictValue();
  dict.Set(kShortcutKey, base::Value(kShortcutValueTerminal));

  // Find terminal profile from prefs.
  const base::Value::Dict& settings =
      profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
  const base::Value::List* vsh_ids = settings.FindList("/vsh/profile-ids");
  if (vsh_ids) {
    for (const auto& vsh_id : *vsh_ids) {
      if (!vsh_id.is_string()) {
        continue;
      }
      const std::string* vm_name = settings.FindString(
          GetSettingsKey(kSettingsPrefixVsh, vsh_id.GetString(), "vm-name"));
      const std::string* container_name = settings.FindString(GetSettingsKey(
          kSettingsPrefixVsh, vsh_id.GetString(), "container-name"));
      const std::string* settings_profile = settings.FindString(GetSettingsKey(
          kSettingsPrefixVsh, vsh_id.GetString(), "terminal-profile"));
      if (vm_name && *vm_name == id.vm_name && container_name &&
          *container_name == id.container_name && settings_profile) {
        dict.Set(kSettingsProfileUrlParam, *settings_profile);
      }
    }
  }

  std::string shortcut_id;
  base::JSONWriter::Write(dict, &shortcut_id);
  return shortcut_id;
}

base::flat_map<std::string, std::string> ExtrasFromShortcutId(
    const base::Value::Dict& shortcut) {
  base::flat_map<std::string, std::string> extras;
  for (const auto it : shortcut) {
    if (it.second.is_string()) {
      extras[it.first] = it.second.GetString();
    }
  }
  return extras;
}

std::vector<std::pair<std::string, std::string>> GetSSHConnections(
    Profile* profile) {
  std::vector<std::pair<std::string, std::string>> result;
  const base::Value::Dict& settings =
      profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
  const base::Value::List* ids = settings.FindList("/nassh/profile-ids");
  if (!ids) {
    return result;
  }
  for (const auto& id : *ids) {
    if (!id.is_string()) {
      continue;
    }
    const std::string* description = settings.FindString(
        GetSettingsKey(kSettingsPrefixNassh, id.GetString(), "description"));
    if (description) {
      result.emplace_back(id.GetString(), *description);
    }
  }
  return result;
}

void AddTerminalMenuItems(Profile* profile, apps::MenuItems& menu_items) {
  apps::AddCommandItem(ash::SETTINGS, IDS_INTERNAL_APP_SETTINGS, menu_items);
  if (crostini::IsCrostiniRunning(profile)) {
    apps::AddCommandItem(ash::SHUTDOWN_GUEST_OS,
                         IDS_CROSTINI_SHUT_DOWN_LINUX_MENU_ITEM, menu_items);
  }
  if (bruschetta::IsBruschettaRunning(profile)) {
    apps::AddCommandItem(ash::SHUTDOWN_BRUSCHETTA_OS,
                         IDS_BRUSCHETTA_SHUT_DOWN_LINUX_MENU_ITEM, menu_items);
  }
}

void AddTerminalMenuShortcuts(
    Profile* profile,
    int next_command_id,
    apps::MenuItems menu_items,
    base::OnceCallback<void(apps::MenuItems)> callback,
    std::vector<gfx::ImageSkia> images) {
  ui::ColorProvider* color_provider =
      ui::ColorProviderManager::Get().GetColorProviderFor(
          ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
              nullptr));
  auto icon = [color_provider](const gfx::VectorIcon& icon) {
    return ui::ImageModel::FromVectorIcon(icon,
                                          apps::GetColorIdForMenuItemIcon(),
                                          apps::kAppShortcutIconSizeDip)
        .Rasterize(color_provider);
  };
  gfx::ImageSkia terminal_ssh_icon = icon(kTerminalSshIcon);
  gfx::ImageSkia crostini_mascot_icon = icon(kCrostiniMascotIcon);
  std::vector<std::pair<std::string, std::string>> connections =
      GetSSHConnections(profile);
  auto* registry = guest_os::GuestOsService::GetForProfile(profile)
                       ->TerminalProviderRegistry();
  if (connections.size() > 0 || registry->List().size() > 0) {
    apps::AddSeparator(ui::DOUBLE_SEPARATOR, menu_items);
  }

  for (auto id : registry->List()) {
    auto* provider = registry->Get(id);
    apps::AddShortcutCommandItem(
        next_command_id++,
        ShortcutIdFromContainerId(profile, provider->GuestId()),
        provider->Label(), crostini_mascot_icon, menu_items);
  }

  for (const auto& connection : connections) {
    apps::AddShortcutCommandItem(
        next_command_id++, ShortcutIdForSSH(connection.first),
        connection.second, terminal_ssh_icon, menu_items);
  }

  std::move(callback).Run(std::move(menu_items));
}

bool ExecuteTerminalMenuShortcutCommand(Profile* profile,
                                        const std::string& shortcut_id,
                                        int64_t display_id) {
  std::optional<base::Value::Dict> shortcut =
      base::JSONReader::ReadDict(shortcut_id);
  if (!shortcut) {
    return false;
  }
  const std::string* shortcut_value = shortcut->FindString(kShortcutKey);
  if (shortcut_value && *shortcut_value == kShortcutValueSSH) {
    const std::string* profileId = shortcut->FindString(kProfileIdKey);
    if (!profileId) {
      return false;
    }
    const base::Value::Dict& settings =
        profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsTerminalSettings);
    const std::string* settings_profile = settings.FindString(GetSettingsKey(
        kSettingsPrefixNassh, *profileId, kSettingsKeyTerminalProfile));
    auto escape = [](const std::string& v) {
      return base::EscapeQueryParamValue(v, /*use_plus=*/true);
    };
    std::string settings_profile_param;
    if (settings_profile && !settings_profile->empty() &&
        *settings_profile != kSettingsProfileDefault) {
      settings_profile_param = base::StrCat(
          {"?", kSettingsProfileUrlParam, "=", escape(*settings_profile)});
    }
    LaunchTerminalWithUrl(
        profile, display_id, /*restore_id=*/0,
        GURL(base::StrCat({chrome::kChromeUIUntrustedTerminalURL,
                           "html/terminal_ssh.html", settings_profile_param,
                           "#profile-id:", escape(*profileId)})));
    return true;
  }

  if (!shortcut_value || *shortcut_value != kShortcutValueTerminal) {
    return false;
  }
  auto intent = std::make_unique<apps::Intent>(apps_util::kIntentActionView);
  intent->extras = ExtrasFromShortcutId(*shortcut);
  LaunchTerminalWithIntent(profile, display_id, std::move(intent),
                           base::DoNothing());
  return true;
}

}  // namespace guest_os