diff --git a/Prototypes/SettingsShellLab/Sources/SettingsShellLab/Resources/Localizable.xcstrings b/Prototypes/SettingsShellLab/Sources/SettingsShellLab/Resources/Localizable.xcstrings index 26fedbef6b9f..ef2eae7ae12a 100644 --- a/Prototypes/SettingsShellLab/Sources/SettingsShellLab/Resources/Localizable.xcstrings +++ b/Prototypes/SettingsShellLab/Sources/SettingsShellLab/Resources/Localizable.xcstrings @@ -26,6 +26,19 @@ "section.sidebar.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Sidebar" } }, "ja": { "stringUnit": { "state": "translated", "value": "サイドバー" } } } }, "section.terminal.detail": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } }, "section.terminal.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } }, + "workspaceGroup.contextMenu.setIcon": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Set Group Icon…" } }, "ja": { "stringUnit": { "state": "translated", "value": "グループアイコンを設定…" } } } }, + "workspaceGroup.icon.clear": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Clear Icon" } }, "ja": { "stringUnit": { "state": "translated", "value": "アイコンをクリア" } } } }, + "workspaceGroup.icon.confirm": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Set Icon" } }, "ja": { "stringUnit": { "state": "translated", "value": "アイコンを設定" } } } }, + "workspaceGroup.icon.invalid.message": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "\"%@\" is not an SF Symbol name available on this Mac." } }, "ja": { "stringUnit": { "state": "translated", "value": "\"%@\" はこの Mac で利用できる SF Symbol 名ではありません。" } } } }, + "workspaceGroup.icon.invalid.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Icon Not Found" } }, "ja": { "stringUnit": { "state": "translated", "value": "アイコンが見つかりません" } } } }, + "workspaceGroup.icon.message": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Enter an SF Symbol name for this group. Leave it empty to use the configured or default icon." } }, "ja": { "stringUnit": { "state": "translated", "value": "このグループの SF Symbol 名を入力してください。空のままにすると、設定済みまたは既定のアイコンを使用します。" } } } }, + "workspaceGroup.icon.placeholder": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "folder.fill" } }, "ja": { "stringUnit": { "state": "translated", "value": "folder.fill" } } } }, + "workspaceGroup.icon.search.placeholder": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Search emoji" } }, "ja": { "stringUnit": { "state": "translated", "value": "絵文字を検索" } } } }, + "workspaceGroup.icon.section.current": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Current" } }, "ja": { "stringUnit": { "state": "translated", "value": "現在" } } } }, + "workspaceGroup.icon.section.emoji": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Emoji" } }, "ja": { "stringUnit": { "state": "translated", "value": "絵文字" } } } }, + "workspaceGroup.icon.section.symbols": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Symbols" } }, "ja": { "stringUnit": { "state": "translated", "value": "シンボル" } } } }, + "workspaceGroup.icon.noResults": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "No icons found" } }, "ja": { "stringUnit": { "state": "translated", "value": "アイコンが見つかりません" } } } }, + "workspaceGroup.icon.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Set Group Icon" } }, "ja": { "stringUnit": { "state": "translated", "value": "グループアイコンを設定" } } } }, "settings.search.prompt": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Search" } }, "ja": { "stringUnit": { "state": "translated", "value": "検索" } } } }, "settings.sidebar.title": { "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Settings" } }, "ja": { "stringUnit": { "state": "translated", "value": "設定" } } } } } diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index e8fa78385183..d5365dd474f3 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -52870,959 +52870,1209 @@ } } }, - "command.showNotifications.subtitle": { + "command.setGroupIcon.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Notifications" + "value": "Workspace Group" } }, "ja": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "ワークスペースグループ" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "工作区组" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "工作區群組" } }, "ko": { "stringUnit": { "state": "translated", - "value": "알림" + "value": "작업 공간 그룹" } }, "de": { "stringUnit": { "state": "translated", - "value": "Benachrichtigungen" + "value": "Arbeitsbereichsgruppe" } }, "es": { "stringUnit": { "state": "translated", - "value": "Notificaciones" + "value": "Grupo de espacios de trabajo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Notifications" + "value": "Groupe d'espaces de travail" } }, "it": { "stringUnit": { "state": "translated", - "value": "Notifiche" + "value": "Gruppo di spazi di lavoro" } }, "da": { "stringUnit": { "state": "translated", - "value": "Notifikationer" + "value": "Arbejdsområdegruppe" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Arbeidsområdegruppe" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Powiadomienia" + "value": "Grupa przestrzeni roboczych" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Уведомления" + "value": "Группа рабочих пространств" } }, - "bs": { + "th": { "stringUnit": { "state": "translated", - "value": "Obavještenja" + "value": "กลุ่มพื้นที่ทำงาน" } }, - "ar": { + "tr": { "stringUnit": { "state": "translated", - "value": "الإشعارات" + "value": "Çalışma alanı grubu" } }, - "nb": { + "uk": { "stringUnit": { "state": "translated", - "value": "Varsler" + "value": "Група робочих просторів" } }, - "pt-BR": { + "ar": { "stringUnit": { "state": "translated", - "value": "Notificações" + "value": "مجموعة مساحات العمل" } }, - "th": { + "bs": { "stringUnit": { "state": "translated", - "value": "การแจ้งเตือน" + "value": "Grupa radnih prostora" } }, - "tr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Bildirimler" + "value": "Grupo de espaços de trabalho" } }, - "uk": { + "km": { "stringUnit": { "state": "translated", - "value": "Сповіщення" + "value": "ក្រុមកន្លែងធ្វើការ" } } } }, - "command.showNotifications.title": { + "command.setGroupIcon.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Show Notifications" + "value": "Set Group Icon…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "通知を表示" + "value": "グループアイコンを設定…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "显示通知" + "value": "设置分组图标…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "顯示通知" + "value": "設定群組圖示…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "알림 표시" + "value": "그룹 아이콘 설정…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Benachrichtigungen anzeigen" + "value": "Gruppensymbol festlegen …" } }, "es": { "stringUnit": { "state": "translated", - "value": "Mostrar notificaciones" + "value": "Definir icono del grupo…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Afficher les notifications" + "value": "Définir l’icône du groupe…" } }, "it": { "stringUnit": { "state": "translated", - "value": "Mostra notifiche" + "value": "Imposta icona gruppo…" } }, "da": { "stringUnit": { "state": "translated", - "value": "Vis notifikationer" + "value": "Indstil gruppeikon…" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Angi gruppeikon …" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Pokaż powiadomienia" + "value": "Ustaw ikonę grupy…" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Показать уведомления" + "value": "Задать значок группы…" } }, - "bs": { + "th": { "stringUnit": { "state": "translated", - "value": "Prikaži obavještenja" + "value": "ตั้งค่าไอคอนกลุ่ม…" } }, - "ar": { + "tr": { "stringUnit": { "state": "translated", - "value": "عرض الإشعارات" + "value": "Grup simgesini ayarla…" } }, - "nb": { + "uk": { "stringUnit": { "state": "translated", - "value": "Vis varsler" + "value": "Установити іконку групи…" } }, - "pt-BR": { + "ar": { "stringUnit": { "state": "translated", - "value": "Mostrar Notificações" + "value": "تعيين أيقونة المجموعة…" } }, - "th": { + "bs": { "stringUnit": { "state": "translated", - "value": "แสดงการแจ้งเตือน" + "value": "Postavi ikonu grupe…" } }, - "tr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Bildirimleri Göster" + "value": "Definir ícone do grupo…" } }, - "uk": { + "km": { "stringUnit": { "state": "translated", - "value": "Показати сповіщення" + "value": "Set Group Icon…" } } } }, - "command.terminalFind.title": { + "command.showNotifications.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Find…" + "value": "Notifications" } }, "ja": { "stringUnit": { "state": "translated", - "value": "検索…" + "value": "通知" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "查找..." + "value": "通知" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尋找..." + "value": "通知" } }, "ko": { "stringUnit": { "state": "translated", - "value": "찾기…" + "value": "알림" } }, "de": { "stringUnit": { "state": "translated", - "value": "Suchen …" + "value": "Benachrichtigungen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Buscar…" + "value": "Notificaciones" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Rechercher..." + "value": "Notifications" } }, "it": { "stringUnit": { "state": "translated", - "value": "Trova…" + "value": "Notifiche" } }, "da": { "stringUnit": { "state": "translated", - "value": "Søg…" + "value": "Notifikationer" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Znajdź…" + "value": "Powiadomienia" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Найти..." + "value": "Уведомления" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Pronađi…" + "value": "Obavještenja" } }, "ar": { "stringUnit": { "state": "translated", - "value": "بحث…" + "value": "الإشعارات" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Finn …" + "value": "Varsler" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Buscar…" + "value": "Notificações" } }, "th": { "stringUnit": { "state": "translated", - "value": "ค้นหา..." + "value": "การแจ้งเตือน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bul…" + "value": "Bildirimler" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Знайти…" + "value": "Сповіщення" } } } }, - "command.terminalFindNext.title": { + "command.showNotifications.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Find Next" + "value": "Show Notifications" } }, "ja": { "stringUnit": { "state": "translated", - "value": "次を検索" + "value": "通知を表示" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "查找下一个" + "value": "显示通知" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尋找下一個" + "value": "顯示通知" } }, "ko": { "stringUnit": { "state": "translated", - "value": "다음 찾기" + "value": "알림 표시" } }, "de": { "stringUnit": { "state": "translated", - "value": "Weitersuchen" + "value": "Benachrichtigungen anzeigen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Buscar siguiente" + "value": "Mostrar notificaciones" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Résultat suivant" + "value": "Afficher les notifications" } }, "it": { "stringUnit": { "state": "translated", - "value": "Trova successivo" + "value": "Mostra notifiche" } }, "da": { "stringUnit": { "state": "translated", - "value": "Find næste" + "value": "Vis notifikationer" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Znajdź następny" + "value": "Pokaż powiadomienia" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Найти далее" + "value": "Показать уведомления" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Pronađi sljedeće" + "value": "Prikaži obavještenja" } }, "ar": { "stringUnit": { "state": "translated", - "value": "البحث عن التالي" + "value": "عرض الإشعارات" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Finn neste" + "value": "Vis varsler" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Buscar Próximo" + "value": "Mostrar Notificações" } }, "th": { "stringUnit": { "state": "translated", - "value": "ค้นหาถัดไป" + "value": "แสดงการแจ้งเตือน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Sonrakini Bul" + "value": "Bildirimleri Göster" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Знайти наступне" + "value": "Показати сповіщення" } } } }, - "command.terminalFindPrevious.title": { + "command.terminalFind.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Find Previous" + "value": "Find…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "前を検索" + "value": "検索…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "查找上一个" + "value": "查找..." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尋找上一個" + "value": "尋找..." } }, "ko": { "stringUnit": { "state": "translated", - "value": "이전 찾기" + "value": "찾기…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Vorheriges suchen" + "value": "Suchen …" } }, "es": { "stringUnit": { "state": "translated", - "value": "Buscar anterior" + "value": "Buscar…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Résultat précédent" + "value": "Rechercher..." } }, "it": { "stringUnit": { "state": "translated", - "value": "Trova precedente" + "value": "Trova…" } }, "da": { "stringUnit": { "state": "translated", - "value": "Find forrige" + "value": "Søg…" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Znajdź poprzedni" + "value": "Znajdź…" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Найти ранее" + "value": "Найти..." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Pronađi prethodno" + "value": "Pronađi…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "البحث عن السابق" + "value": "بحث…" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Finn forrige" + "value": "Finn …" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Buscar Anterior" + "value": "Buscar…" } }, "th": { "stringUnit": { "state": "translated", - "value": "ค้นหาก่อนหน้า" + "value": "ค้นหา..." } }, "tr": { "stringUnit": { "state": "translated", - "value": "Öncekini Bul" + "value": "Bul…" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Знайти попереднє" + "value": "Знайти…" } } } }, - "command.terminalHideFind.title": { + "command.terminalFindNext.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Hide Find Bar" + "value": "Find Next" } }, "ja": { "stringUnit": { "state": "translated", - "value": "検索バーを非表示" + "value": "次を検索" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "隐藏查找栏" + "value": "查找下一个" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "隱藏尋找列" + "value": "尋找下一個" } }, "ko": { "stringUnit": { "state": "translated", - "value": "찾기 막대 숨기기" + "value": "다음 찾기" } }, "de": { "stringUnit": { "state": "translated", - "value": "Suchleiste ausblenden" + "value": "Weitersuchen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Ocultar barra de búsqueda" + "value": "Buscar siguiente" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Masquer la barre de recherche" + "value": "Résultat suivant" } }, "it": { "stringUnit": { "state": "translated", - "value": "Nascondi barra di ricerca" + "value": "Trova successivo" } }, "da": { "stringUnit": { "state": "translated", - "value": "Skjul søgelinje" + "value": "Find næste" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ukryj pasek wyszukiwania" + "value": "Znajdź następny" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Скрыть панель поиска" + "value": "Найти далее" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Sakrij traku za pretragu" + "value": "Pronađi sljedeće" } }, "ar": { "stringUnit": { "state": "translated", - "value": "إخفاء شريط البحث" + "value": "البحث عن التالي" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Skjul søkelinje" + "value": "Finn neste" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Ocultar Barra de Busca" + "value": "Buscar Próximo" } }, "th": { "stringUnit": { "state": "translated", - "value": "ซ่อนแถบค้นหา" + "value": "ค้นหาถัดไป" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Arama Çubuğunu Gizle" + "value": "Sonrakini Bul" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Сховати панель пошуку" + "value": "Знайти наступне" } } } }, - "command.terminalSplitBrowserDown.subtitle": { + "command.terminalFindPrevious.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Terminal Layout" + "value": "Find Previous" } }, "ja": { "stringUnit": { "state": "translated", - "value": "ターミナルレイアウト" + "value": "前を検索" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "终端布局" + "value": "查找上一个" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "終端機版面" + "value": "尋找上一個" } }, "ko": { "stringUnit": { "state": "translated", - "value": "터미널 레이아웃" + "value": "이전 찾기" } }, "de": { "stringUnit": { "state": "translated", - "value": "Terminal-Layout" + "value": "Vorheriges suchen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Disposición del terminal" + "value": "Buscar anterior" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Disposition du terminal" + "value": "Résultat précédent" } }, "it": { "stringUnit": { "state": "translated", - "value": "Layout terminale" + "value": "Trova precedente" } }, "da": { "stringUnit": { "state": "translated", - "value": "Terminallayout" + "value": "Find forrige" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Układ terminala" + "value": "Znajdź poprzedni" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Макет терминала" + "value": "Найти ранее" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Raspored terminala" + "value": "Pronađi prethodno" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تخطيط الطرفية" + "value": "البحث عن السابق" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Terminaloppsett" + "value": "Finn forrige" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Layout do Terminal" + "value": "Buscar Anterior" } }, "th": { "stringUnit": { "state": "translated", - "value": "เค้าโครงเทอร์มินัล" + "value": "ค้นหาก่อนหน้า" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Terminal Düzeni" + "value": "Öncekini Bul" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Макет терміналу" + "value": "Знайти попереднє" } } } }, - "command.terminalSplitBrowserDown.title": { + "command.terminalHideFind.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Split Browser Down" + "value": "Hide Find Bar" } }, "ja": { "stringUnit": { "state": "translated", - "value": "ブラウザを下に分割" + "value": "検索バーを非表示" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "向下拆分浏览器" + "value": "隐藏查找栏" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "向下分割瀏覽器" + "value": "隱藏尋找列" } }, "ko": { "stringUnit": { "state": "translated", - "value": "브라우저를 아래로 분할" + "value": "찾기 막대 숨기기" } }, "de": { "stringUnit": { "state": "translated", - "value": "Browser nach unten teilen" + "value": "Suchleiste ausblenden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Dividir navegador hacia abajo" + "value": "Ocultar barra de búsqueda" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Diviser le navigateur vers le bas" + "value": "Masquer la barre de recherche" } }, "it": { "stringUnit": { "state": "translated", - "value": "Dividi browser in basso" + "value": "Nascondi barra di ricerca" } }, "da": { "stringUnit": { "state": "translated", - "value": "Opdel browser nedad" + "value": "Skjul søgelinje" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Podziel przeglądarkę w dół" + "value": "Ukryj pasek wyszukiwania" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Разделить браузер вниз" + "value": "Скрыть панель поиска" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Podijeli preglednik dolje" + "value": "Sakrij traku za pretragu" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تقسيم المتصفح للأسفل" + "value": "إخفاء شريط البحث" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Del nettleser ned" + "value": "Skjul søkelinje" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Dividir Navegador para Baixo" + "value": "Ocultar Barra de Busca" } }, "th": { "stringUnit": { "state": "translated", - "value": "แยกเบราว์เซอร์ลงล่าง" + "value": "ซ่อนแถบค้นหา" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Tarayıcıyı Aşağı Böl" + "value": "Arama Çubuğunu Gizle" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Розділити браузер вниз" + "value": "Сховати панель пошуку" } } } }, - "command.terminalSplitBrowserRight.subtitle": { + "command.terminalSplitBrowserDown.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Terminal Layout" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ターミナルレイアウト" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "终端布局" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "終端機版面" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "터미널 레이아웃" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Terminal-Layout" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Disposición del terminal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disposition du terminal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Layout terminale" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Terminallayout" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Układ terminala" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Макет терминала" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Raspored terminala" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تخطيط الطرفية" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Terminaloppsett" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Layout do Terminal" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เค้าโครงเทอร์มินัล" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Terminal Düzeni" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Макет терміналу" + } + } + } + }, + "command.terminalSplitBrowserDown.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Split Browser Down" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザを下に分割" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "向下拆分浏览器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "向下分割瀏覽器" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "브라우저를 아래로 분할" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Browser nach unten teilen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dividir navegador hacia abajo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Diviser le navigateur vers le bas" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dividi browser in basso" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Opdel browser nedad" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Podziel przeglądarkę w dół" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Разделить браузер вниз" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Podijeli preglednik dolje" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تقسيم المتصفح للأسفل" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Del nettleser ned" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dividir Navegador para Baixo" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แยกเบราว์เซอร์ลงล่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Tarayıcıyı Aşağı Böl" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розділити браузер вниз" + } + } + } + }, + "command.terminalSplitBrowserRight.subtitle": { "extractionState": "manual", "localizations": { "en": { @@ -167714,6 +167964,131 @@ } } }, + "workspaceGroup.contextMenu.setIcon": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set Group Icon…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "グループアイコンを設定…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "设置分组图标…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "設定群組圖示…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "그룹 아이콘 설정…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gruppensymbol festlegen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Definir icono del grupo…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Définir l’icône du groupe…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Imposta icona gruppo…" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indstil gruppeikon…" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Angi gruppeikon …" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ustaw ikonę grupy…" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Задать значок группы…" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตั้งค่าไอคอนกลุ่ม…" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Grup simgesini ayarla…" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Установити іконку групи…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعيين أيقونة المجموعة…" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Postavi ikonu grupe…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Definir ícone do grupo…" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Set Group Icon…" + } + } + } + }, "workspaceGroup.contextMenu.delete": { "extractionState": "manual", "localizations": { @@ -169589,6 +169964,1506 @@ } } }, + "workspaceGroup.icon.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set Group Icon" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "グループアイコンを設定" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "设置分组图标" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "設定群組圖示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "그룹 아이콘 설정" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gruppensymbol festlegen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Definir icono del grupo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Définir l’icône du groupe" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Imposta icona gruppo" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indstil gruppeikon" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Angi gruppeikon" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ustaw ikonę grupy" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Задать значок группы" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตั้งค่าไอคอนกลุ่ม" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Grup simgesini ayarla" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Установити іконку групи" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعيين أيقونة المجموعة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Postavi ikonu grupe" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Definir ícone do grupo" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Set Group Icon" + } + } + } + }, + "workspaceGroup.icon.search.placeholder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search emoji" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "絵文字を検索" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "搜索表情符号" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "搜尋表情符號" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이모지 검색" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Emoji suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar emoji" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher des emoji" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca emoji" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Søg efter emoji" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Søk etter emoji" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Szukaj emoji" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Поиск эмодзи" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ค้นหาอีโมจิ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Emoji ara" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Пошук емодзі" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ابحث عن الرموز التعبيرية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pretraži emoji" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar emoji" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Search emoji" + } + } + } + }, + "workspaceGroup.icon.section.current": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Current" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "現在" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "当前" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "目前" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "현재" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktuell" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actual" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actuel" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attuale" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Aktuel" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Gjeldende" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Bieżące" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Текущий" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ปัจจุบัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Geçerli" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Поточна" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الحالي" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Trenutno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atual" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Current" + } + } + } + }, + "workspaceGroup.icon.section.emoji": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "絵文字" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "表情符号" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "表情符號" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이모지" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Эмодзи" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "อีโมจิ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Емодзі" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "رموز تعبيرية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Emoji" + } + } + } + }, + "workspaceGroup.icon.section.symbols": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Symbols" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "シンボル" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "符号" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "符號" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "기호" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbole" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Símbolos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Symboles" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Simboli" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Symboler" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Symboler" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Symbole" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Символы" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สัญลักษณ์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Simgeler" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Символи" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "رموز" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Simboli" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Símbolos" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Symbols" + } + } + } + }, + "workspaceGroup.icon.noResults": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No icons found" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アイコンが見つかりません" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未找到图标" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "找不到圖示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "아이콘을 찾을 수 없음" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Symbole gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se encontraron iconos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune icône trouvée" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna icona trovata" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ingen ikoner fundet" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fant ingen ikoner" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie znaleziono ikon" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Значки не найдены" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่พบไอคอน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Simge bulunamadı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Іконки не знайдено" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يتم العثور على أيقونات" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ikone nisu pronađene" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum ícone encontrado" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "No icons found" + } + } + } + }, + "workspaceGroup.icon.message": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enter an SF Symbol name for this group. Leave it empty to use the configured or default icon." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このグループの SF Symbol 名を入力してください。空のままにすると、設定済みまたは既定のアイコンを使用します。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "输入此分组的 SF Symbol 名称。留空将使用已配置或默认图标。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "輸入此群組的 SF Symbol 名稱。留空會使用已設定或預設圖示。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 그룹의 SF Symbol 이름을 입력하세요. 비워 두면 구성된 아이콘이나 기본 아이콘을 사용합니다." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gib einen SF-Symbolnamen für diese Gruppe ein. Leer lassen, um das konfigurierte oder Standardsymbol zu verwenden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Introduce un nombre de SF Symbol para este grupo. Déjalo vacío para usar el icono configurado o predeterminado." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisissez un nom SF Symbol pour ce groupe. Laissez vide pour utiliser l’icône configurée ou par défaut." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inserisci un nome SF Symbol per questo gruppo. Lascia vuoto per usare l’icona configurata o predefinita." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indtast et SF Symbol-navn for denne gruppe. Lad feltet være tomt for at bruge det konfigurerede ikon eller standardikonet." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Skriv inn et SF Symbol-navn for denne gruppen. La feltet stå tomt for å bruke det konfigurerte ikonet eller standardikonet." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wpisz nazwę SF Symbol dla tej grupy. Pozostaw puste, aby użyć skonfigurowanej lub domyślnej ikony." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Введите имя SF Symbol для этой группы. Оставьте поле пустым, чтобы использовать настроенный значок или значок по умолчанию." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ป้อนชื่อ SF Symbol สำหรับกลุ่มนี้ เว้นว่างไว้เพื่อใช้ไอคอนที่กำหนดไว้หรือไอคอนเริ่มต้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu grup için bir SF Symbol adı girin. Yapılandırılmış veya varsayılan simgeyi kullanmak için boş bırakın." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Введіть назву SF Symbol для цієї групи. Залиште поле порожнім, щоб використати налаштовану або типову іконку." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أدخل اسم SF Symbol لهذه المجموعة. اتركه فارغًا لاستخدام الأيقونة المكوّنة أو الافتراضية." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Unesi naziv SF Symbol za ovu grupu. Ostavi prazno za konfiguriranu ili zadanu ikonu." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Digite um nome de SF Symbol para este grupo. Deixe vazio para usar o ícone configurado ou padrão." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Enter an SF Symbol name for this group. Leave it empty to use the configured or default icon." + } + } + } + }, + "workspaceGroup.icon.confirm": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set Icon" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アイコンを設定" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "设置图标" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "設定圖示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "아이콘 설정" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol festlegen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Definir icono" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Définir l’icône" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Imposta icona" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indstil ikon" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Angi ikon" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ustaw ikonę" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Задать значок" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตั้งค่าไอคอน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Simgeyi ayarla" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Установити іконку" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعيين الأيقونة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Postavi ikonu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Definir ícone" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Set Icon" + } + } + } + }, + "workspaceGroup.icon.clear": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear Icon" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アイコンをクリア" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "清除图标" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "清除圖示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "아이콘 지우기" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol löschen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Borrar icono" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer l’icône" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cancella icona" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ryd ikon" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fjern ikon" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wyczyść ikonę" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Очистить значок" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ล้างไอคอน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Simgeyi temizle" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Очистити іконку" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مسح الأيقونة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Očisti ikonu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpar ícone" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Clear Icon" + } + } + } + }, + "workspaceGroup.icon.placeholder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "folder.fill" + } + } + } + }, + "workspaceGroup.icon.invalid.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Icon Not Found" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アイコンが見つかりません" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "找不到图标" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "找不到圖示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "아이콘을 찾을 수 없음" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Icono no encontrado" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône introuvable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona non trovata" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ikon ikke fundet" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fant ikke ikonet" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie znaleziono ikony" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Значок не найден" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่พบไอคอน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Simge bulunamadı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Іконку не знайдено" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يتم العثور على الأيقونة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ikona nije pronađena" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ícone não encontrado" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Icon Not Found" + } + } + } + }, + "workspaceGroup.icon.invalid.message": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" is not an SF Symbol name available on this Mac." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" はこの Mac で使用できる SF Symbol 名ではありません。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" 不是此 Mac 上可用的 SF Symbol 名称。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" 不是此 Mac 上可用的 SF Symbol 名稱。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "\"%@\"은 이 Mac에서 사용할 수 있는 SF Symbol 이름이 아닙니다." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" ist kein auf diesem Mac verfügbarer SF-Symbolname." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" no es un nombre de SF Symbol disponible en este Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" n’est pas un nom SF Symbol disponible sur ce Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" non è un nome SF Symbol disponibile su questo Mac." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" er ikke et SF Symbol-navn, der er tilgængeligt på denne Mac." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" er ikke et SF Symbol-navn som er tilgjengelig på denne Macen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" nie jest nazwą SF Symbol dostępną na tym Macu." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" не является именем SF Symbol, доступным на этом Mac." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" ไม่ใช่ชื่อ SF Symbol ที่มีใน Mac เครื่องนี้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" bu Mac’te kullanılabilen bir SF Symbol adı değil." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" не є назвою SF Symbol, доступною на цьому Mac." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" ليس اسم SF Symbol متاحًا على هذا الـ Mac." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" nije naziv SF Symbol dostupan na ovom Macu." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" não é um nome de SF Symbol disponível neste Mac." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "\"%@\" is not an SF Symbol name available on this Mac." + } + } + } + }, "workspaceGroup.rename.confirm": { "extractionState": "manual", "localizations": { diff --git a/Sources/App/ForeignFirstResponderPolicy.swift b/Sources/App/ForeignFirstResponderPolicy.swift index d60b738cd632..f89dbfd68430 100644 --- a/Sources/App/ForeignFirstResponderPolicy.swift +++ b/Sources/App/ForeignFirstResponderPolicy.swift @@ -39,8 +39,31 @@ func shouldRespectForeignFirstResponder( in window: NSWindow, isRightSidebarOwner: (NSResponder) -> Bool ) -> Bool { - // A stranded responder (detached, or reparented into another window without resigning) no longer - // belongs to this window and must not block the terminal from reclaiming first responder. + // A focused text editor that `window` hosts is being edited by the user, so the terminal must yield + // the keystroke. `NSText` covers AppKit field editors AND SwiftUI's private `_SystemTextFieldFieldEditor` + // (an `NSText` subclass — verified at runtime: the icon-picker field editor logs `isnst=1`). + // + // The editor counts as hosted by `window` when its backing window is `window` itself OR a child + // window `window` presents. SwiftUI hosts a popover/palette overlay's field editor in a separate + // `_NSPopoverWindow` whose `parent` chain reaches `window` (verified: chain `[popover, main]`, + // `isChildOfWin=1`), even though it is `window`'s active first responder — so the strict + // `.window === window` guard rejected it and keyRepair stole the keystroke. Walking the parent + // chain (rather than accepting any window) keeps the #5269 guarantee: a text editor stranded in an + // unrelated window, or fully detached (`window == nil`), never reaches `window` and is reclaimed. + if firstResponder is NSText { + var hostWindow = (firstResponder as? NSView)?.window + var hops = 0 + while let current = hostWindow, hops < 8 { + if current === window { + return true + } + hostWindow = current.parent + hops += 1 + } + } + // Non-text focus owners (right-sidebar / dock / feed hosts) must still belong to this window: a + // stranded host reparented into another window without resigning must not block the terminal from + // reclaiming first responder (issue #5269). guard (firstResponder as? NSView)?.window === window else { return false } - return firstResponder is NSText || isRightSidebarOwner(firstResponder) + return isRightSidebarOwner(firstResponder) } diff --git a/Sources/ContentView+SetGroupIconCommand.swift b/Sources/ContentView+SetGroupIconCommand.swift new file mode 100644 index 000000000000..838a9decff11 --- /dev/null +++ b/Sources/ContentView+SetGroupIconCommand.swift @@ -0,0 +1,28 @@ +import AppKit +import SwiftUI + +extension ContentView { + /// Contributes the "Set Group Icon" command-palette entry. + /// + /// The command is offered whenever the focused workspace belongs to a group (as the group's + /// anchor or as one of its children). Running it opens the icon picker inside the palette, so it + /// keeps the palette open (`dismissOnRun: false`); see `commandPaletteSetGroupIconView`. + func appendSetGroupIconCommandContribution( + to contributions: inout [CommandPaletteCommandContribution] + ) { + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.setGroupIcon", + title: { _ in + String(localized: "command.setGroupIcon.title", defaultValue: "Set Group Icon…") + }, + subtitle: { _ in + String(localized: "command.setGroupIcon.subtitle", defaultValue: "Workspace Group") + }, + keywords: ["group", "icon", "emoji", "symbol", "workspace", "set", "folder"], + dismissOnRun: false, + when: { $0.bool(CommandPaletteContextKeys.workspaceInGroup) } + ) + ) + } +} diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 8e62dd15831f..acf4cb788762 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1166,6 +1166,7 @@ struct ContentView: View { case renameInput(CommandPaletteRenameTarget) case renameConfirm(CommandPaletteRenameTarget, proposedName: String) case workspaceDescriptionInput(CommandPaletteWorkspaceDescriptionTarget) + case iconPicker(groupId: UUID) } enum CommandPalettePendingActivation: Equatable { @@ -1477,6 +1478,7 @@ struct ContentView: View { static let workspaceHasBelow = "workspace.hasBelow" static let workspaceCanMarkRead = "workspace.canMarkRead" static let workspaceCanMarkUnread = "workspace.canMarkUnread" + static let workspaceInGroup = "workspace.inGroup" static let sidebarMatchTerminalBackground = "sidebar.matchTerminalBackground" static let hasFocusedPanel = "panel.hasFocus" static let panelName = "panel.name" @@ -3876,6 +3878,8 @@ struct ContentView: View { target: target, maxEditorHeight: workspaceDescriptionMaxEditorHeight ) + case .iconPicker(let groupId): + commandPaletteSetGroupIconView(groupId: groupId) } } .frame(width: targetWidth) @@ -6600,6 +6604,7 @@ struct ContentView: View { CommandPaletteContextKeys.workspaceCanMarkUnread, notificationStore.canMarkWorkspaceUnread(forTabIds: [workspace.id]) ) + snapshot.setBool(CommandPaletteContextKeys.workspaceInGroup, workspace.groupId != nil) } if let panelContext = focusedPanelContext { @@ -7277,6 +7282,7 @@ struct ContentView: View { ) ) appendMoveTabToNewWorkspaceCommandContribution(to: &contributions, panelSubtitle: panelSubtitle) + appendSetGroupIconCommandContribution(to: &contributions) contributions.append( CommandPaletteCommandContribution( commandId: "palette.toggleTabPin", @@ -8223,6 +8229,10 @@ struct ContentView: View { } registerIdentifierCopyCommandHandlers(®istry) + registry.register(commandId: "palette.setGroupIcon") { + beginSetGroupIconFlow() + } + registry.register(commandId: "palette.renameTab") { beginRenameTabFlow() } @@ -8913,6 +8923,9 @@ struct ContentView: View { target: target, proposedDescription: commandPaletteWorkspaceDescriptionDraft ) + case .iconPicker: + // The icon picker commits on selection (tap), not on Enter. + break } } @@ -9104,6 +9117,8 @@ struct ContentView: View { mode = "rename_confirm" case .workspaceDescriptionInput: mode = "workspace_description_input" + case .iconPicker: + mode = "icon_picker" } let rows = Array(commandPaletteVisibleResults.prefix(20)).map { result in @@ -9451,6 +9466,8 @@ struct ContentView: View { return "renameConfirm" case .workspaceDescriptionInput: return "workspaceDescriptionInput" + case .iconPicker: + return "iconPicker" } } #endif @@ -9550,6 +9567,8 @@ struct ContentView: View { return case .workspaceDescriptionInput: return + case .iconPicker: + return } } guard let window = observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow else { return } @@ -9731,6 +9750,31 @@ struct ContentView: View { syncCommandPaletteDebugStateForObservedWindow() } + /// Opens the group icon picker inside the command palette for the group the focused + /// workspace belongs to (its anchor or a child both resolve to the same group). + private func beginSetGroupIconFlow() { + guard let groupId = tabManager.selectedWorkspace?.groupId, + tabManager.workspaceGroups.contains(where: { $0.id == groupId }) else { + NSSound.beep() + return + } + commandPaletteMode = .iconPicker(groupId: groupId) + syncCommandPaletteDebugStateForObservedWindow() + } + + @ViewBuilder + private func commandPaletteSetGroupIconView(groupId: UUID) -> some View { + let storedIcon = tabManager.workspaceGroups.first(where: { $0.id == groupId })?.iconSymbol + WorkspaceGroupIconPickerView(currentSymbol: storedIcon) { symbol in + _ = tabManager.setWorkspaceGroupIcon(groupId: groupId, symbol: symbol) + dismissCommandPalette() + } + .frame(maxWidth: .infinity) + .onExitCommand { + dismissCommandPalette() + } + } + private func startWorkspaceDescriptionFlow(_ target: CommandPaletteWorkspaceDescriptionTarget) { #if DEBUG cmuxDebugLog( diff --git a/Sources/RenderableSystemSymbol.swift b/Sources/RenderableSystemSymbol.swift index 6da4c476e250..49d8d8067343 100644 --- a/Sources/RenderableSystemSymbol.swift +++ b/Sources/RenderableSystemSymbol.swift @@ -5,6 +5,12 @@ enum RenderableSystemSymbol { static let defaultSurfaceTabIcon = "doc.text" @MainActor private static var renderabilityCache: [String: Bool] = [:] + /// Bounds the renderability cache so user-driven lookups (e.g. icon-picker search text) cannot + /// grow it without limit. Real SF Symbol names are few, so dropping the whole cache when it + /// reaches the cap only forces a handful of cheap relookups. + private static let renderabilityCacheLimit = 512 + /// Real SF Symbol names are short; anything longer is rejected before it reaches AppKit or the cache. + private static let renderabilityNameLengthLimit = 128 static func trimmed(_ raw: String?) -> String? { guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines), @@ -24,12 +30,29 @@ enum RenderableSystemSymbol { } @MainActor - static func resolvedWorkspaceGroupIcon(explicit: String?, configured: String?) -> String { + static func normalizedWorkspaceGroupIcon(_ raw: String?) -> String? { + guard let trimmed = trimmed(raw) else { return nil } + if let emoji = normalizedEmoji(trimmed) { + return emoji + } + return normalized(trimmed) + } + + @MainActor + static func resolvedWorkspaceGroupIconValue(explicit: String?, configured: String?) -> RenderableWorkspaceGroupIcon { for candidate in [explicit, configured] { - guard let normalized = normalized(candidate) else { continue } - return normalized + guard let normalized = normalizedWorkspaceGroupIcon(candidate) else { continue } + if let emoji = normalizedEmoji(normalized) { + return .emoji(emoji) + } + return .systemSymbol(normalized) } - return defaultWorkspaceGroupIcon + return .systemSymbol(defaultWorkspaceGroupIcon) + } + + @MainActor + static func resolvedWorkspaceGroupIcon(explicit: String?, configured: String?) -> String { + resolvedWorkspaceGroupIconValue(explicit: explicit, configured: configured).rawValue } @MainActor @@ -41,18 +64,51 @@ enum RenderableSystemSymbol { @MainActor static func isRenderable(_ symbol: String) -> Bool { + guard symbol.count <= renderabilityNameLengthLimit else { return false } if let cached = renderabilityCache[symbol] { return cached } let resolved = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) != nil + if renderabilityCache.count >= renderabilityCacheLimit { + renderabilityCache.removeAll(keepingCapacity: true) + } renderabilityCache[symbol] = resolved return resolved } + static func normalizedEmoji(_ raw: String?) -> String? { + guard let trimmed = trimmed(raw), + trimmed.count == 1, + let character = trimmed.first else { + return nil + } + let scalars = character.unicodeScalars + let containsEmoji = scalars.contains { $0.properties.isEmoji } + let containsEmojiPresentation = scalars.contains { + $0.properties.isEmojiPresentation || + $0.properties.isEmojiModifier || + $0.properties.isEmojiModifierBase || + $0.value == 0xFE0F + } + let isASCIIOnly = scalars.allSatisfy { $0.value < 128 } + guard containsEmoji, + containsEmojiPresentation || !isASCIIOnly else { + return nil + } + return String(character) + } + #if DEBUG @MainActor static func resetRenderabilityCacheForTesting() { renderabilityCache.removeAll() } + + @MainActor + static func renderabilityCacheCountForTesting() -> Int { + renderabilityCache.count + } + + static var renderabilityCacheLimitForTesting: Int { renderabilityCacheLimit } #endif } diff --git a/Sources/RenderableWorkspaceGroupIcon.swift b/Sources/RenderableWorkspaceGroupIcon.swift new file mode 100644 index 000000000000..6a0c3bfedaf5 --- /dev/null +++ b/Sources/RenderableWorkspaceGroupIcon.swift @@ -0,0 +1,11 @@ +enum RenderableWorkspaceGroupIcon: Equatable { + case systemSymbol(String) + case emoji(String) + + var rawValue: String { + switch self { + case .systemSymbol(let symbol), .emoji(let symbol): + return symbol + } + } +} diff --git a/Sources/SidebarWorkspaceGroupHeaderView.swift b/Sources/SidebarWorkspaceGroupHeaderView.swift index 9c45d697145d..ae52e1248663 100644 --- a/Sources/SidebarWorkspaceGroupHeaderView.swift +++ b/Sources/SidebarWorkspaceGroupHeaderView.swift @@ -11,6 +11,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { lhs.anchorWorkspaceId == rhs.anchorWorkspaceId && lhs.name == rhs.name && lhs.iconSymbol == rhs.iconSymbol && + lhs.storedIconSymbol == rhs.storedIconSymbol && lhs.tintHex == rhs.tintHex && lhs.isCollapsed == rhs.isCollapsed && lhs.isPinned == rhs.isPinned && @@ -35,6 +36,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { let anchorWorkspaceId: UUID let name: String let iconSymbol: String + let storedIconSymbol: String? let tintHex: String? let isCollapsed: Bool let isPinned: Bool @@ -60,6 +62,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { let onTapPlus: () -> Void let onRunResolvedItem: (CmuxResolvedConfigMenuAction) -> Void let onRename: () -> Void + let onSetIcon: (String?) -> Void let onTogglePinned: () -> Void let onUngroup: () -> Void let onDelete: () -> Void @@ -67,6 +70,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { let onOpenDocs: () -> Void @State private var isHovered = false + @State private var isIconPickerPresented = false @State private var rowHeight: CGFloat = 1 private var metrics: SidebarWorkspaceGroupHeaderMetrics { @@ -80,8 +84,8 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { return .secondary } - private var displayedIconSymbol: String { - RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: iconSymbol, configured: nil) + private var displayedIcon: RenderableWorkspaceGroupIcon { + RenderableSystemSymbol.resolvedWorkspaceGroupIconValue(explicit: iconSymbol, configured: nil) } private var shortcutHintPillText: String? { @@ -121,8 +125,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { ) HStack(spacing: 6) { - Image(systemName: displayedIconSymbol) - .font(.system(size: metrics.iconFontSize, weight: .semibold)) + WorkspaceGroupIconPreview(icon: displayedIcon, fontSize: metrics.iconFontSize) .foregroundStyle(iconColor) .frame(width: metrics.iconFrame, height: metrics.iconFrame) .accessibilityHidden(true) @@ -248,6 +251,13 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { ), action: onRename ) + Button( + String( + localized: "workspaceGroup.contextMenu.setIcon", + defaultValue: "Set Group Icon..." + ), + action: { isIconPickerPresented = true } + ) Button( isPinned ? String( @@ -295,6 +305,12 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { ) } } + .popover(isPresented: $isIconPickerPresented, arrowEdge: .trailing) { + WorkspaceGroupIconPickerView(currentSymbol: storedIconSymbol) { symbol in + onSetIcon(symbol) + isIconPickerPresented = false + } + } } } diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index cc5d6231c78c..17adf218c67d 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -4535,7 +4535,7 @@ class TabManager: ObservableObject { @discardableResult func setWorkspaceGroupIcon(groupId: UUID, symbol: String?) -> String? { - let normalized = RenderableSystemSymbol.normalized(symbol) + let normalized = RenderableSystemSymbol.normalizedWorkspaceGroupIcon(symbol) guard let index = workspaceGroups.firstIndex(where: { $0.id == groupId }) else { return nil } guard workspaceGroups[index].iconSymbol != normalized else { return normalized } workspaceGroups[index].iconSymbol = normalized diff --git a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift index e11fb6b8f6b4..7119d2691b87 100644 --- a/Sources/VerticalTabsSidebar+WorkspaceGroups.swift +++ b/Sources/VerticalTabsSidebar+WorkspaceGroups.swift @@ -79,6 +79,7 @@ extension VerticalTabsSidebar { anchorWorkspaceId: group.anchorWorkspaceId, name: group.name, iconSymbol: effectiveIcon, + storedIconSymbol: group.iconSymbol, tintHex: effectiveColor, isCollapsed: group.isCollapsed, isPinned: group.isPinned, @@ -134,6 +135,10 @@ extension VerticalTabsSidebar { currentName: currentName ) }, + onSetIcon: { [weak tabManager, groupId = group.id] symbol in + guard let tabManager else { return } + tabManager.setWorkspaceGroupIcon(groupId: groupId, symbol: symbol) + }, onTogglePinned: { [weak tabManager, groupId = group.id] in tabManager?.toggleWorkspaceGroupPinned(groupId: groupId) }, diff --git a/Sources/WorkspaceGroupEmojiCatalog.swift b/Sources/WorkspaceGroupEmojiCatalog.swift new file mode 100644 index 000000000000..330dab1e2c2c --- /dev/null +++ b/Sources/WorkspaceGroupEmojiCatalog.swift @@ -0,0 +1,47 @@ +import Foundation + +enum WorkspaceGroupEmojiCatalog { + /// Every emoji in browse order (common first), from the baked dataset. No runtime scan. + static let allEmoji: [String] = WorkspaceGroupEmojiData.entries.map(\.value) + + /// Emoji matching `rawQuery`, ranked by match quality. + /// + /// An empty query returns the full browse list. A single pasted emoji echoes itself. + /// Otherwise every whitespace-separated token must appear in an entry's keywords (AND), + /// and results are ordered exact-name, then word-prefix, then substring. + static func search(_ rawQuery: String) -> [String] { + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + guard !query.isEmpty else { return allEmoji } + + if let emoji = RenderableSystemSymbol.normalizedEmoji(rawQuery) { + return [emoji] + } + + let tokens = query.split(separator: " ").map(String.init) + var exact: [String] = [] + var wordPrefix: [String] = [] + var substring: [String] = [] + + for entry in WorkspaceGroupEmojiData.entries { + let keywords = entry.keywords + guard tokens.allSatisfy({ keywords.contains($0) }) else { continue } + + if keywords == query { + exact.append(entry.value) + } else if Self.anyWord(in: keywords, hasPrefix: tokens) { + wordPrefix.append(entry.value) + } else { + substring.append(entry.value) + } + } + + return exact + wordPrefix + substring + } + + /// Whether any keyword word starts with the first query token (a "rocket" query should + /// rank `rocket` above `water pistol` even though both contain the substring). + private static func anyWord(in keywords: String, hasPrefix tokens: [String]) -> Bool { + guard let first = tokens.first else { return false } + return keywords.split(separator: " ").contains { $0.hasPrefix(first) } + } +} diff --git a/Sources/WorkspaceGroupEmojiData.swift b/Sources/WorkspaceGroupEmojiData.swift new file mode 100644 index 000000000000..917406c08613 --- /dev/null +++ b/Sources/WorkspaceGroupEmojiData.swift @@ -0,0 +1,1442 @@ +// GENERATED by scripts/generate-workspace-group-emoji.swift. Do not edit by hand. +// Regenerate after Unicode updates: swift scripts/generate-workspace-group-emoji.swift + +/// Baked, searchable emoji dataset for the workspace group icon picker. +/// +/// Each entry pairs an emoji with its lowercased keyword text (from the Unicode scalar +/// names). Built once at dev time so the picker never scans Unicode ranges at runtime. +enum WorkspaceGroupEmojiData { + /// `(value, keywords)` pairs in browse order (common emoji first). + static let entries: [(value: String, keywords: String)] = [ + ("🚀", "rocket"), + ("💻", "personal computer"), + ("🧠", "brain"), + ("⚙️", "gear"), + ("🔥", "fire"), + ("✅", "white heavy check mark"), + ("📁", "file folder"), + ("🧪", "test tube"), + ("🎯", "direct hit"), + ("✨", "sparkles"), + ("⚡️", "high voltage sign"), + ("⭐️", "white medium star"), + ("🔧", "wrench"), + ("📌", "pushpin"), + ("📝", "memo"), + ("🔍", "left pointing magnifying glass"), + ("🎨", "artist palette"), + ("🧩", "jigsaw puzzle piece"), + ("📦", "package"), + ("🌐", "globe with meridians"), + ("🔒", "lock"), + ("💬", "speech balloon"), + ("🏁", "chequered flag"), + ("📊", "bar chart"), + ("🛠️", "hammer and wrench"), + ("🧰", "toolbox"), + ("📣", "cheering megaphone"), + ("💡", "electric light bulb"), + ("🕹️", "joystick"), + ("🧵", "spool of thread"), + ("🪄", "magic wand"), + ("🧱", "brick"), + ("📎", "paperclip"), + ("🗂️", "card index dividers"), + ("📚", "books"), + ("🔬", "microscope"), + ("‼", "double exclamation mark"), + ("⁉", "exclamation question mark"), + ("™", "trade mark sign"), + ("ℹ", "information source"), + ("↔", "left right arrow"), + ("↕", "up down arrow"), + ("↖", "north west arrow"), + ("↗", "north east arrow"), + ("↘", "south east arrow"), + ("↙", "south west arrow"), + ("↩", "leftwards arrow with hook"), + ("↪", "rightwards arrow with hook"), + ("⌚", "watch"), + ("⌛", "hourglass"), + ("⌨", "keyboard"), + ("⏏", "eject symbol"), + ("⏩", "black right pointing double triangle"), + ("⏪", "black left pointing double triangle"), + ("⏫", "black up pointing double triangle"), + ("⏬", "black down pointing double triangle"), + ("⏭", "black right pointing double triangle with vertical bar"), + ("⏮", "black left pointing double triangle with vertical bar"), + ("⏯", "black right pointing triangle with double vertical bar"), + ("⏰", "alarm clock"), + ("⏱", "stopwatch"), + ("⏲", "timer clock"), + ("⏳", "hourglass with flowing sand"), + ("⏸", "double vertical bar"), + ("⏹", "black square for stop"), + ("⏺", "black circle for record"), + ("Ⓜ", "circled latin capital letter m"), + ("▪", "black small square"), + ("▫", "white small square"), + ("▶", "black right pointing triangle"), + ("◀", "black left pointing triangle"), + ("◻", "white medium square"), + ("◼", "black medium square"), + ("◽", "white medium small square"), + ("◾", "black medium small square"), + ("☀", "black sun with rays"), + ("☁", "cloud"), + ("☂", "umbrella"), + ("☃", "snowman"), + ("☄", "comet"), + ("☎", "black telephone"), + ("☑", "ballot box with check"), + ("☔", "umbrella with rain drops"), + ("☕", "hot beverage"), + ("☘", "shamrock"), + ("☝", "white up pointing index"), + ("☠", "skull and crossbones"), + ("☢", "radioactive sign"), + ("☣", "biohazard sign"), + ("☦", "orthodox cross"), + ("☪", "star and crescent"), + ("☮", "peace symbol"), + ("☯", "yin yang"), + ("☸", "wheel of dharma"), + ("☹", "white frowning face"), + ("☺", "white smiling face"), + ("♀", "female sign"), + ("♂", "male sign"), + ("♈", "aries"), + ("♉", "taurus"), + ("♊", "gemini"), + ("♋", "cancer"), + ("♌", "leo"), + ("♍", "virgo"), + ("♎", "libra"), + ("♏", "scorpius"), + ("♐", "sagittarius"), + ("♑", "capricorn"), + ("♒", "aquarius"), + ("♓", "pisces"), + ("♟", "black chess pawn"), + ("♠", "black spade suit"), + ("♣", "black club suit"), + ("♥", "black heart suit"), + ("♦", "black diamond suit"), + ("♨", "hot springs"), + ("♻", "black universal recycling symbol"), + ("♾", "permanent paper sign"), + ("♿", "wheelchair symbol"), + ("⚒", "hammer and pick"), + ("⚓", "anchor"), + ("⚔", "crossed swords"), + ("⚕", "staff of aesculapius"), + ("⚖", "scales"), + ("⚗", "alembic"), + ("⚙", "gear"), + ("⚛", "atom symbol"), + ("⚜", "fleur de lis"), + ("⚠", "warning sign"), + ("⚡", "high voltage sign"), + ("⚧", "male with stroke and male and female sign"), + ("⚪", "medium white circle"), + ("⚫", "medium black circle"), + ("⚰", "coffin"), + ("⚱", "funeral urn"), + ("⚽", "soccer ball"), + ("⚾", "baseball"), + ("⛄", "snowman without snow"), + ("⛅", "sun behind cloud"), + ("⛈", "thunder cloud and rain"), + ("⛎", "ophiuchus"), + ("⛏", "pick"), + ("⛑", "helmet with white cross"), + ("⛓", "chains"), + ("⛔", "no entry"), + ("⛩", "shinto shrine"), + ("⛪", "church"), + ("⛰", "mountain"), + ("⛱", "umbrella on ground"), + ("⛲", "fountain"), + ("⛳", "flag in hole"), + ("⛴", "ferry"), + ("⛵", "sailboat"), + ("⛷", "skier"), + ("⛸", "ice skate"), + ("⛹", "person with ball"), + ("⛺", "tent"), + ("⛽", "fuel pump"), + ("✂", "black scissors"), + ("✈", "airplane"), + ("✉", "envelope"), + ("✊", "raised fist"), + ("✋", "raised hand"), + ("✌", "victory hand"), + ("✍", "writing hand"), + ("✏", "pencil"), + ("✒", "black nib"), + ("✔", "heavy check mark"), + ("✖", "heavy multiplication x"), + ("✝", "latin cross"), + ("✡", "star of david"), + ("✳", "eight spoked asterisk"), + ("✴", "eight pointed black star"), + ("❄", "snowflake"), + ("❇", "sparkle"), + ("❌", "cross mark"), + ("❎", "negative squared cross mark"), + ("❓", "black question mark ornament"), + ("❔", "white question mark ornament"), + ("❕", "white exclamation mark ornament"), + ("❗", "heavy exclamation mark symbol"), + ("❣", "heavy heart exclamation mark ornament"), + ("❤", "heavy black heart"), + ("➕", "heavy plus sign"), + ("➖", "heavy minus sign"), + ("➗", "heavy division sign"), + ("➡", "black rightwards arrow"), + ("➰", "curly loop"), + ("➿", "double curly loop"), + ("⤴", "arrow pointing rightwards then curving upwards"), + ("⤵", "arrow pointing rightwards then curving downwards"), + ("⬅", "leftwards black arrow"), + ("⬆", "upwards black arrow"), + ("⬇", "downwards black arrow"), + ("⬛", "black large square"), + ("⬜", "white large square"), + ("⭐", "white medium star"), + ("⭕", "heavy large circle"), + ("〰", "wavy dash"), + ("〽", "part alternation mark"), + ("㊗", "circled ideograph congratulation"), + ("㊙", "circled ideograph secret"), + ("🀄", "mahjong tile red dragon"), + ("🃏", "playing card black joker"), + ("🅰", "negative squared latin capital letter a"), + ("🅱", "negative squared latin capital letter b"), + ("🅾", "negative squared latin capital letter o"), + ("🅿", "negative squared latin capital letter p"), + ("🆎", "negative squared ab"), + ("🆑", "squared cl"), + ("🆒", "squared cool"), + ("🆓", "squared free"), + ("🆔", "squared id"), + ("🆕", "squared new"), + ("🆖", "squared ng"), + ("🆗", "squared ok"), + ("🆘", "squared sos"), + ("🆙", "squared up with exclamation mark"), + ("🆚", "squared vs"), + ("🇦", "regional indicator symbol letter a"), + ("🇧", "regional indicator symbol letter b"), + ("🇨", "regional indicator symbol letter c"), + ("🇩", "regional indicator symbol letter d"), + ("🇪", "regional indicator symbol letter e"), + ("🇫", "regional indicator symbol letter f"), + ("🇬", "regional indicator symbol letter g"), + ("🇭", "regional indicator symbol letter h"), + ("🇮", "regional indicator symbol letter i"), + ("🇯", "regional indicator symbol letter j"), + ("🇰", "regional indicator symbol letter k"), + ("🇱", "regional indicator symbol letter l"), + ("🇲", "regional indicator symbol letter m"), + ("🇳", "regional indicator symbol letter n"), + ("🇴", "regional indicator symbol letter o"), + ("🇵", "regional indicator symbol letter p"), + ("🇶", "regional indicator symbol letter q"), + ("🇷", "regional indicator symbol letter r"), + ("🇸", "regional indicator symbol letter s"), + ("🇹", "regional indicator symbol letter t"), + ("🇺", "regional indicator symbol letter u"), + ("🇻", "regional indicator symbol letter v"), + ("🇼", "regional indicator symbol letter w"), + ("🇽", "regional indicator symbol letter x"), + ("🇾", "regional indicator symbol letter y"), + ("🇿", "regional indicator symbol letter z"), + ("🈁", "squared katakana koko"), + ("🈂", "squared katakana sa"), + ("🈚", "squared cjk unified ideograph 7121"), + ("🈯", "squared cjk unified ideograph 6307"), + ("🈲", "squared cjk unified ideograph 7981"), + ("🈳", "squared cjk unified ideograph 7a7a"), + ("🈴", "squared cjk unified ideograph 5408"), + ("🈵", "squared cjk unified ideograph 6e80"), + ("🈶", "squared cjk unified ideograph 6709"), + ("🈷", "squared cjk unified ideograph 6708"), + ("🈸", "squared cjk unified ideograph 7533"), + ("🈹", "squared cjk unified ideograph 5272"), + ("🈺", "squared cjk unified ideograph 55b6"), + ("🉐", "circled ideograph advantage"), + ("🉑", "circled ideograph accept"), + ("🌀", "cyclone"), + ("🌁", "foggy"), + ("🌂", "closed umbrella"), + ("🌃", "night with stars"), + ("🌄", "sunrise over mountains"), + ("🌅", "sunrise"), + ("🌆", "cityscape at dusk"), + ("🌇", "sunset over buildings"), + ("🌈", "rainbow"), + ("🌉", "bridge at night"), + ("🌊", "water wave"), + ("🌋", "volcano"), + ("🌌", "milky way"), + ("🌍", "earth globe europe africa"), + ("🌎", "earth globe americas"), + ("🌏", "earth globe asia australia"), + ("🌑", "new moon symbol"), + ("🌒", "waxing crescent moon symbol"), + ("🌓", "first quarter moon symbol"), + ("🌔", "waxing gibbous moon symbol"), + ("🌕", "full moon symbol"), + ("🌖", "waning gibbous moon symbol"), + ("🌗", "last quarter moon symbol"), + ("🌘", "waning crescent moon symbol"), + ("🌙", "crescent moon"), + ("🌚", "new moon with face"), + ("🌛", "first quarter moon with face"), + ("🌜", "last quarter moon with face"), + ("🌝", "full moon with face"), + ("🌞", "sun with face"), + ("🌟", "glowing star"), + ("🌠", "shooting star"), + ("🌡", "thermometer"), + ("🌤", "white sun with small cloud"), + ("🌥", "white sun behind cloud"), + ("🌦", "white sun behind cloud with rain"), + ("🌧", "cloud with rain"), + ("🌨", "cloud with snow"), + ("🌩", "cloud with lightning"), + ("🌪", "cloud with tornado"), + ("🌫", "fog"), + ("🌬", "wind blowing face"), + ("🌭", "hot dog"), + ("🌮", "taco"), + ("🌯", "burrito"), + ("🌰", "chestnut"), + ("🌱", "seedling"), + ("🌲", "evergreen tree"), + ("🌳", "deciduous tree"), + ("🌴", "palm tree"), + ("🌵", "cactus"), + ("🌶", "hot pepper"), + ("🌷", "tulip"), + ("🌸", "cherry blossom"), + ("🌹", "rose"), + ("🌺", "hibiscus"), + ("🌻", "sunflower"), + ("🌼", "blossom"), + ("🌽", "ear of maize"), + ("🌾", "ear of rice"), + ("🌿", "herb"), + ("🍀", "four leaf clover"), + ("🍁", "maple leaf"), + ("🍂", "fallen leaf"), + ("🍃", "leaf fluttering in wind"), + ("🍄", "mushroom"), + ("🍅", "tomato"), + ("🍆", "aubergine"), + ("🍇", "grapes"), + ("🍈", "melon"), + ("🍉", "watermelon"), + ("🍊", "tangerine"), + ("🍋", "lemon"), + ("🍌", "banana"), + ("🍍", "pineapple"), + ("🍎", "red apple"), + ("🍏", "green apple"), + ("🍐", "pear"), + ("🍑", "peach"), + ("🍒", "cherries"), + ("🍓", "strawberry"), + ("🍔", "hamburger"), + ("🍕", "slice of pizza"), + ("🍖", "meat on bone"), + ("🍗", "poultry leg"), + ("🍘", "rice cracker"), + ("🍙", "rice ball"), + ("🍚", "cooked rice"), + ("🍛", "curry and rice"), + ("🍜", "steaming bowl"), + ("🍝", "spaghetti"), + ("🍞", "bread"), + ("🍟", "french fries"), + ("🍠", "roasted sweet potato"), + ("🍡", "dango"), + ("🍢", "oden"), + ("🍣", "sushi"), + ("🍤", "fried shrimp"), + ("🍥", "fish cake with swirl design"), + ("🍦", "soft ice cream"), + ("🍧", "shaved ice"), + ("🍨", "ice cream"), + ("🍩", "doughnut"), + ("🍪", "cookie"), + ("🍫", "chocolate bar"), + ("🍬", "candy"), + ("🍭", "lollipop"), + ("🍮", "custard"), + ("🍯", "honey pot"), + ("🍰", "shortcake"), + ("🍱", "bento box"), + ("🍲", "pot of food"), + ("🍳", "cooking"), + ("🍴", "fork and knife"), + ("🍵", "teacup without handle"), + ("🍶", "sake bottle and cup"), + ("🍷", "wine glass"), + ("🍸", "cocktail glass"), + ("🍹", "tropical drink"), + ("🍺", "beer mug"), + ("🍻", "clinking beer mugs"), + ("🍼", "baby bottle"), + ("🍽", "fork and knife with plate"), + ("🍾", "bottle with popping cork"), + ("🍿", "popcorn"), + ("🎀", "ribbon"), + ("🎁", "wrapped present"), + ("🎂", "birthday cake"), + ("🎃", "jack o lantern"), + ("🎄", "christmas tree"), + ("🎅", "father christmas"), + ("🎆", "fireworks"), + ("🎇", "firework sparkler"), + ("🎈", "balloon"), + ("🎉", "party popper"), + ("🎊", "confetti ball"), + ("🎋", "tanabata tree"), + ("🎌", "crossed flags"), + ("🎍", "pine decoration"), + ("🎎", "japanese dolls"), + ("🎏", "carp streamer"), + ("🎐", "wind chime"), + ("🎑", "moon viewing ceremony"), + ("🎒", "school satchel"), + ("🎓", "graduation cap"), + ("🎖", "military medal"), + ("🎗", "reminder ribbon"), + ("🎙", "studio microphone"), + ("🎚", "level slider"), + ("🎛", "control knobs"), + ("🎞", "film frames"), + ("🎟", "admission tickets"), + ("🎠", "carousel horse"), + ("🎡", "ferris wheel"), + ("🎢", "roller coaster"), + ("🎣", "fishing pole and fish"), + ("🎤", "microphone"), + ("🎥", "movie camera"), + ("🎦", "cinema"), + ("🎧", "headphone"), + ("🎩", "top hat"), + ("🎪", "circus tent"), + ("🎫", "ticket"), + ("🎬", "clapper board"), + ("🎭", "performing arts"), + ("🎮", "video game"), + ("🎰", "slot machine"), + ("🎱", "billiards"), + ("🎲", "game die"), + ("🎳", "bowling"), + ("🎴", "flower playing cards"), + ("🎵", "musical note"), + ("🎶", "multiple musical notes"), + ("🎷", "saxophone"), + ("🎸", "guitar"), + ("🎹", "musical keyboard"), + ("🎺", "trumpet"), + ("🎻", "violin"), + ("🎼", "musical score"), + ("🎽", "running shirt with sash"), + ("🎾", "tennis racquet and ball"), + ("🎿", "ski and ski boot"), + ("🏀", "basketball and hoop"), + ("🏂", "snowboarder"), + ("🏃", "runner"), + ("🏄", "surfer"), + ("🏅", "sports medal"), + ("🏆", "trophy"), + ("🏇", "horse racing"), + ("🏈", "american football"), + ("🏉", "rugby football"), + ("🏊", "swimmer"), + ("🏋", "weight lifter"), + ("🏌", "golfer"), + ("🏍", "racing motorcycle"), + ("🏎", "racing car"), + ("🏏", "cricket bat and ball"), + ("🏐", "volleyball"), + ("🏑", "field hockey stick and ball"), + ("🏒", "ice hockey stick and puck"), + ("🏓", "table tennis paddle and ball"), + ("🏔", "snow capped mountain"), + ("🏕", "camping"), + ("🏖", "beach with umbrella"), + ("🏗", "building construction"), + ("🏘", "house buildings"), + ("🏙", "cityscape"), + ("🏚", "derelict house building"), + ("🏛", "classical building"), + ("🏜", "desert"), + ("🏝", "desert island"), + ("🏞", "national park"), + ("🏟", "stadium"), + ("🏠", "house building"), + ("🏡", "house with garden"), + ("🏢", "office building"), + ("🏣", "japanese post office"), + ("🏤", "european post office"), + ("🏥", "hospital"), + ("🏦", "bank"), + ("🏧", "automated teller machine"), + ("🏨", "hotel"), + ("🏩", "love hotel"), + ("🏪", "convenience store"), + ("🏫", "school"), + ("🏬", "department store"), + ("🏭", "factory"), + ("🏮", "izakaya lantern"), + ("🏯", "japanese castle"), + ("🏰", "european castle"), + ("🏳", "waving white flag"), + ("🏴", "waving black flag"), + ("🏵", "rosette"), + ("🏷", "label"), + ("🏸", "badminton racquet and shuttlecock"), + ("🏹", "bow and arrow"), + ("🏺", "amphora"), + ("🏻", "emoji modifier fitzpatrick type 1 2"), + ("🏼", "emoji modifier fitzpatrick type 3"), + ("🏽", "emoji modifier fitzpatrick type 4"), + ("🏾", "emoji modifier fitzpatrick type 5"), + ("🏿", "emoji modifier fitzpatrick type 6"), + ("🐀", "rat"), + ("🐁", "mouse"), + ("🐂", "ox"), + ("🐃", "water buffalo"), + ("🐄", "cow"), + ("🐅", "tiger"), + ("🐆", "leopard"), + ("🐇", "rabbit"), + ("🐈", "cat"), + ("🐉", "dragon"), + ("🐊", "crocodile"), + ("🐋", "whale"), + ("🐌", "snail"), + ("🐍", "snake"), + ("🐎", "horse"), + ("🐏", "ram"), + ("🐐", "goat"), + ("🐑", "sheep"), + ("🐒", "monkey"), + ("🐓", "rooster"), + ("🐔", "chicken"), + ("🐕", "dog"), + ("🐖", "pig"), + ("🐗", "boar"), + ("🐘", "elephant"), + ("🐙", "octopus"), + ("🐚", "spiral shell"), + ("🐛", "bug"), + ("🐜", "ant"), + ("🐝", "honeybee"), + ("🐞", "lady beetle"), + ("🐟", "fish"), + ("🐠", "tropical fish"), + ("🐡", "blowfish"), + ("🐢", "turtle"), + ("🐣", "hatching chick"), + ("🐤", "baby chick"), + ("🐥", "front facing baby chick"), + ("🐦", "bird"), + ("🐧", "penguin"), + ("🐨", "koala"), + ("🐩", "poodle"), + ("🐪", "dromedary camel"), + ("🐫", "bactrian camel"), + ("🐬", "dolphin"), + ("🐭", "mouse face"), + ("🐮", "cow face"), + ("🐯", "tiger face"), + ("🐰", "rabbit face"), + ("🐱", "cat face"), + ("🐲", "dragon face"), + ("🐳", "spouting whale"), + ("🐴", "horse face"), + ("🐵", "monkey face"), + ("🐶", "dog face"), + ("🐷", "pig face"), + ("🐸", "frog face"), + ("🐹", "hamster face"), + ("🐺", "wolf face"), + ("🐻", "bear face"), + ("🐼", "panda face"), + ("🐽", "pig nose"), + ("🐾", "paw prints"), + ("🐿", "chipmunk"), + ("👀", "eyes"), + ("👁", "eye"), + ("👂", "ear"), + ("👃", "nose"), + ("👄", "mouth"), + ("👅", "tongue"), + ("👆", "white up pointing backhand index"), + ("👇", "white down pointing backhand index"), + ("👈", "white left pointing backhand index"), + ("👉", "white right pointing backhand index"), + ("👊", "fisted hand sign"), + ("👋", "waving hand sign"), + ("👌", "ok hand sign"), + ("👍", "thumbs up sign"), + ("👎", "thumbs down sign"), + ("👏", "clapping hands sign"), + ("👐", "open hands sign"), + ("👑", "crown"), + ("👒", "womans hat"), + ("👓", "eyeglasses"), + ("👔", "necktie"), + ("👕", "t shirt"), + ("👖", "jeans"), + ("👗", "dress"), + ("👘", "kimono"), + ("👙", "bikini"), + ("👚", "womans clothes"), + ("👛", "purse"), + ("👜", "handbag"), + ("👝", "pouch"), + ("👞", "mans shoe"), + ("👟", "athletic shoe"), + ("👠", "high heeled shoe"), + ("👡", "womans sandal"), + ("👢", "womans boots"), + ("👣", "footprints"), + ("👤", "bust in silhouette"), + ("👥", "busts in silhouette"), + ("👦", "boy"), + ("👧", "girl"), + ("👨", "man"), + ("👩", "woman"), + ("👪", "family"), + ("👫", "man and woman holding hands"), + ("👬", "two men holding hands"), + ("👭", "two women holding hands"), + ("👮", "police officer"), + ("👯", "woman with bunny ears"), + ("👰", "bride with veil"), + ("👱", "person with blond hair"), + ("👲", "man with gua pi mao"), + ("👳", "man with turban"), + ("👴", "older man"), + ("👵", "older woman"), + ("👶", "baby"), + ("👷", "construction worker"), + ("👸", "princess"), + ("👹", "japanese ogre"), + ("👺", "japanese goblin"), + ("👻", "ghost"), + ("👼", "baby angel"), + ("👽", "extraterrestrial alien"), + ("👾", "alien monster"), + ("👿", "imp"), + ("💀", "skull"), + ("💁", "information desk person"), + ("💂", "guardsman"), + ("💃", "dancer"), + ("💄", "lipstick"), + ("💅", "nail polish"), + ("💆", "face massage"), + ("💇", "haircut"), + ("💈", "barber pole"), + ("💉", "syringe"), + ("💊", "pill"), + ("💋", "kiss mark"), + ("💌", "love letter"), + ("💍", "ring"), + ("💎", "gem stone"), + ("💏", "kiss"), + ("💐", "bouquet"), + ("💑", "couple with heart"), + ("💒", "wedding"), + ("💓", "beating heart"), + ("💔", "broken heart"), + ("💕", "two hearts"), + ("💖", "sparkling heart"), + ("💗", "growing heart"), + ("💘", "heart with arrow"), + ("💙", "blue heart"), + ("💚", "green heart"), + ("💛", "yellow heart"), + ("💜", "purple heart"), + ("💝", "heart with ribbon"), + ("💞", "revolving hearts"), + ("💟", "heart decoration"), + ("💠", "diamond shape with a dot inside"), + ("💢", "anger symbol"), + ("💣", "bomb"), + ("💤", "sleeping symbol"), + ("💥", "collision symbol"), + ("💦", "splashing sweat symbol"), + ("💧", "droplet"), + ("💨", "dash symbol"), + ("💩", "pile of poo"), + ("💪", "flexed biceps"), + ("💫", "dizzy symbol"), + ("💭", "thought balloon"), + ("💮", "white flower"), + ("💯", "hundred points symbol"), + ("💰", "money bag"), + ("💱", "currency exchange"), + ("💲", "heavy dollar sign"), + ("💳", "credit card"), + ("💴", "banknote with yen sign"), + ("💵", "banknote with dollar sign"), + ("💶", "banknote with euro sign"), + ("💷", "banknote with pound sign"), + ("💸", "money with wings"), + ("💹", "chart with upwards trend and yen sign"), + ("💺", "seat"), + ("💼", "briefcase"), + ("💽", "minidisc"), + ("💾", "floppy disk"), + ("💿", "optical disc"), + ("📀", "dvd"), + ("📂", "open file folder"), + ("📃", "page with curl"), + ("📄", "page facing up"), + ("📅", "calendar"), + ("📆", "tear off calendar"), + ("📇", "card index"), + ("📈", "chart with upwards trend"), + ("📉", "chart with downwards trend"), + ("📋", "clipboard"), + ("📍", "round pushpin"), + ("📏", "straight ruler"), + ("📐", "triangular ruler"), + ("📑", "bookmark tabs"), + ("📒", "ledger"), + ("📓", "notebook"), + ("📔", "notebook with decorative cover"), + ("📕", "closed book"), + ("📖", "open book"), + ("📗", "green book"), + ("📘", "blue book"), + ("📙", "orange book"), + ("📛", "name badge"), + ("📜", "scroll"), + ("📞", "telephone receiver"), + ("📟", "pager"), + ("📠", "fax machine"), + ("📡", "satellite antenna"), + ("📢", "public address loudspeaker"), + ("📤", "outbox tray"), + ("📥", "inbox tray"), + ("📧", "e mail symbol"), + ("📨", "incoming envelope"), + ("📩", "envelope with downwards arrow above"), + ("📪", "closed mailbox with lowered flag"), + ("📫", "closed mailbox with raised flag"), + ("📬", "open mailbox with raised flag"), + ("📭", "open mailbox with lowered flag"), + ("📮", "postbox"), + ("📯", "postal horn"), + ("📰", "newspaper"), + ("📱", "mobile phone"), + ("📲", "mobile phone with rightwards arrow at left"), + ("📳", "vibration mode"), + ("📴", "mobile phone off"), + ("📵", "no mobile phones"), + ("📶", "antenna with bars"), + ("📷", "camera"), + ("📸", "camera with flash"), + ("📹", "video camera"), + ("📺", "television"), + ("📻", "radio"), + ("📼", "videocassette"), + ("📽", "film projector"), + ("📿", "prayer beads"), + ("🔀", "twisted rightwards arrows"), + ("🔁", "clockwise rightwards and leftwards open circle arrows"), + ("🔂", "clockwise rightwards and leftwards open circle arrows with circled one overlay"), + ("🔃", "clockwise downwards and upwards open circle arrows"), + ("🔄", "anticlockwise downwards and upwards open circle arrows"), + ("🔅", "low brightness symbol"), + ("🔆", "high brightness symbol"), + ("🔇", "speaker with cancellation stroke"), + ("🔈", "speaker"), + ("🔉", "speaker with one sound wave"), + ("🔊", "speaker with three sound waves"), + ("🔋", "battery"), + ("🔌", "electric plug"), + ("🔎", "right pointing magnifying glass"), + ("🔏", "lock with ink pen"), + ("🔐", "closed lock with key"), + ("🔑", "key"), + ("🔓", "open lock"), + ("🔔", "bell"), + ("🔕", "bell with cancellation stroke"), + ("🔖", "bookmark"), + ("🔗", "link symbol"), + ("🔘", "radio button"), + ("🔙", "back with leftwards arrow above"), + ("🔚", "end with leftwards arrow above"), + ("🔛", "on with exclamation mark with left right arrow above"), + ("🔜", "soon with rightwards arrow above"), + ("🔝", "top with upwards arrow above"), + ("🔞", "no one under eighteen symbol"), + ("🔟", "keycap ten"), + ("🔠", "input symbol for latin capital letters"), + ("🔡", "input symbol for latin small letters"), + ("🔢", "input symbol for numbers"), + ("🔣", "input symbol for symbols"), + ("🔤", "input symbol for latin letters"), + ("🔦", "electric torch"), + ("🔨", "hammer"), + ("🔩", "nut and bolt"), + ("🔪", "hocho"), + ("🔫", "pistol"), + ("🔭", "telescope"), + ("🔮", "crystal ball"), + ("🔯", "six pointed star with middle dot"), + ("🔰", "japanese symbol for beginner"), + ("🔱", "trident emblem"), + ("🔲", "black square button"), + ("🔳", "white square button"), + ("🔴", "large red circle"), + ("🔵", "large blue circle"), + ("🔶", "large orange diamond"), + ("🔷", "large blue diamond"), + ("🔸", "small orange diamond"), + ("🔹", "small blue diamond"), + ("🔺", "up pointing red triangle"), + ("🔻", "down pointing red triangle"), + ("🔼", "up pointing small red triangle"), + ("🔽", "down pointing small red triangle"), + ("🕉", "om symbol"), + ("🕊", "dove of peace"), + ("🕋", "kaaba"), + ("🕌", "mosque"), + ("🕍", "synagogue"), + ("🕎", "menorah with nine branches"), + ("🕐", "clock face one oclock"), + ("🕑", "clock face two oclock"), + ("🕒", "clock face three oclock"), + ("🕓", "clock face four oclock"), + ("🕔", "clock face five oclock"), + ("🕕", "clock face six oclock"), + ("🕖", "clock face seven oclock"), + ("🕗", "clock face eight oclock"), + ("🕘", "clock face nine oclock"), + ("🕙", "clock face ten oclock"), + ("🕚", "clock face eleven oclock"), + ("🕛", "clock face twelve oclock"), + ("🕜", "clock face one thirty"), + ("🕝", "clock face two thirty"), + ("🕞", "clock face three thirty"), + ("🕟", "clock face four thirty"), + ("🕠", "clock face five thirty"), + ("🕡", "clock face six thirty"), + ("🕢", "clock face seven thirty"), + ("🕣", "clock face eight thirty"), + ("🕤", "clock face nine thirty"), + ("🕥", "clock face ten thirty"), + ("🕦", "clock face eleven thirty"), + ("🕧", "clock face twelve thirty"), + ("🕯", "candle"), + ("🕰", "mantelpiece clock"), + ("🕳", "hole"), + ("🕴", "man in business suit levitating"), + ("🕵", "sleuth or spy"), + ("🕶", "dark sunglasses"), + ("🕷", "spider"), + ("🕸", "spider web"), + ("🕹", "joystick"), + ("🕺", "man dancing"), + ("🖇", "linked paperclips"), + ("🖊", "lower left ballpoint pen"), + ("🖋", "lower left fountain pen"), + ("🖌", "lower left paintbrush"), + ("🖍", "lower left crayon"), + ("🖐", "raised hand with fingers splayed"), + ("🖕", "reversed hand with middle finger extended"), + ("🖖", "raised hand with part between middle and ring fingers"), + ("🖤", "black heart"), + ("🖥", "desktop computer"), + ("🖨", "printer"), + ("🖱", "three button mouse"), + ("🖲", "trackball"), + ("🖼", "frame with picture"), + ("🗂", "card index dividers"), + ("🗃", "card file box"), + ("🗄", "file cabinet"), + ("🗑", "wastebasket"), + ("🗒", "spiral note pad"), + ("🗓", "spiral calendar pad"), + ("🗜", "compression"), + ("🗝", "old key"), + ("🗞", "rolled up newspaper"), + ("🗡", "dagger knife"), + ("🗣", "speaking head in silhouette"), + ("🗨", "left speech bubble"), + ("🗯", "right anger bubble"), + ("🗳", "ballot box with ballot"), + ("🗺", "world map"), + ("🗻", "mount fuji"), + ("🗼", "tokyo tower"), + ("🗽", "statue of liberty"), + ("🗾", "silhouette of japan"), + ("🗿", "moyai"), + ("😀", "grinning face"), + ("😁", "grinning face with smiling eyes"), + ("😂", "face with tears of joy"), + ("😃", "smiling face with open mouth"), + ("😄", "smiling face with open mouth and smiling eyes"), + ("😅", "smiling face with open mouth and cold sweat"), + ("😆", "smiling face with open mouth and tightly closed eyes"), + ("😇", "smiling face with halo"), + ("😈", "smiling face with horns"), + ("😉", "winking face"), + ("😊", "smiling face with smiling eyes"), + ("😋", "face savouring delicious food"), + ("😌", "relieved face"), + ("😍", "smiling face with heart shaped eyes"), + ("😎", "smiling face with sunglasses"), + ("😏", "smirking face"), + ("😐", "neutral face"), + ("😑", "expressionless face"), + ("😒", "unamused face"), + ("😓", "face with cold sweat"), + ("😔", "pensive face"), + ("😕", "confused face"), + ("😖", "confounded face"), + ("😗", "kissing face"), + ("😘", "face throwing a kiss"), + ("😙", "kissing face with smiling eyes"), + ("😚", "kissing face with closed eyes"), + ("😛", "face with stuck out tongue"), + ("😜", "face with stuck out tongue and winking eye"), + ("😝", "face with stuck out tongue and tightly closed eyes"), + ("😞", "disappointed face"), + ("😟", "worried face"), + ("😠", "angry face"), + ("😡", "pouting face"), + ("😢", "crying face"), + ("😣", "persevering face"), + ("😤", "face with look of triumph"), + ("😥", "disappointed but relieved face"), + ("😦", "frowning face with open mouth"), + ("😧", "anguished face"), + ("😨", "fearful face"), + ("😩", "weary face"), + ("😪", "sleepy face"), + ("😫", "tired face"), + ("😬", "grimacing face"), + ("😭", "loudly crying face"), + ("😮", "face with open mouth"), + ("😯", "hushed face"), + ("😰", "face with open mouth and cold sweat"), + ("😱", "face screaming in fear"), + ("😲", "astonished face"), + ("😳", "flushed face"), + ("😴", "sleeping face"), + ("😵", "dizzy face"), + ("😶", "face without mouth"), + ("😷", "face with medical mask"), + ("😸", "grinning cat face with smiling eyes"), + ("😹", "cat face with tears of joy"), + ("😺", "smiling cat face with open mouth"), + ("😻", "smiling cat face with heart shaped eyes"), + ("😼", "cat face with wry smile"), + ("😽", "kissing cat face with closed eyes"), + ("😾", "pouting cat face"), + ("😿", "crying cat face"), + ("🙀", "weary cat face"), + ("🙁", "slightly frowning face"), + ("🙂", "slightly smiling face"), + ("🙃", "upside down face"), + ("🙄", "face with rolling eyes"), + ("🙅", "face with no good gesture"), + ("🙆", "face with ok gesture"), + ("🙇", "person bowing deeply"), + ("🙈", "see no evil monkey"), + ("🙉", "hear no evil monkey"), + ("🙊", "speak no evil monkey"), + ("🙋", "happy person raising one hand"), + ("🙌", "person raising both hands in celebration"), + ("🙍", "person frowning"), + ("🙎", "person with pouting face"), + ("🙏", "person with folded hands"), + ("🚁", "helicopter"), + ("🚂", "steam locomotive"), + ("🚃", "railway car"), + ("🚄", "high speed train"), + ("🚅", "high speed train with bullet nose"), + ("🚆", "train"), + ("🚇", "metro"), + ("🚈", "light rail"), + ("🚉", "station"), + ("🚊", "tram"), + ("🚋", "tram car"), + ("🚌", "bus"), + ("🚍", "oncoming bus"), + ("🚎", "trolleybus"), + ("🚏", "bus stop"), + ("🚐", "minibus"), + ("🚑", "ambulance"), + ("🚒", "fire engine"), + ("🚓", "police car"), + ("🚔", "oncoming police car"), + ("🚕", "taxi"), + ("🚖", "oncoming taxi"), + ("🚗", "automobile"), + ("🚘", "oncoming automobile"), + ("🚙", "recreational vehicle"), + ("🚚", "delivery truck"), + ("🚛", "articulated lorry"), + ("🚜", "tractor"), + ("🚝", "monorail"), + ("🚞", "mountain railway"), + ("🚟", "suspension railway"), + ("🚠", "mountain cableway"), + ("🚡", "aerial tramway"), + ("🚢", "ship"), + ("🚣", "rowboat"), + ("🚤", "speedboat"), + ("🚥", "horizontal traffic light"), + ("🚦", "vertical traffic light"), + ("🚧", "construction sign"), + ("🚨", "police cars revolving light"), + ("🚩", "triangular flag on post"), + ("🚪", "door"), + ("🚫", "no entry sign"), + ("🚬", "smoking symbol"), + ("🚭", "no smoking symbol"), + ("🚮", "put litter in its place symbol"), + ("🚯", "do not litter symbol"), + ("🚰", "potable water symbol"), + ("🚱", "non potable water symbol"), + ("🚲", "bicycle"), + ("🚳", "no bicycles"), + ("🚴", "bicyclist"), + ("🚵", "mountain bicyclist"), + ("🚶", "pedestrian"), + ("🚷", "no pedestrians"), + ("🚸", "children crossing"), + ("🚹", "mens symbol"), + ("🚺", "womens symbol"), + ("🚻", "restroom"), + ("🚼", "baby symbol"), + ("🚽", "toilet"), + ("🚾", "water closet"), + ("🚿", "shower"), + ("🛀", "bath"), + ("🛁", "bathtub"), + ("🛂", "passport control"), + ("🛃", "customs"), + ("🛄", "baggage claim"), + ("🛅", "left luggage"), + ("🛋", "couch and lamp"), + ("🛌", "sleeping accommodation"), + ("🛍", "shopping bags"), + ("🛎", "bellhop bell"), + ("🛏", "bed"), + ("🛐", "place of worship"), + ("🛑", "octagonal sign"), + ("🛒", "shopping trolley"), + ("🛕", "hindu temple"), + ("🛖", "hut"), + ("🛗", "elevator"), + ("🛘", "landslide"), + ("🛜", "wireless"), + ("🛝", "playground slide"), + ("🛞", "wheel"), + ("🛟", "ring buoy"), + ("🛠", "hammer and wrench"), + ("🛡", "shield"), + ("🛢", "oil drum"), + ("🛣", "motorway"), + ("🛤", "railway track"), + ("🛥", "motor boat"), + ("🛩", "small airplane"), + ("🛫", "airplane departure"), + ("🛬", "airplane arriving"), + ("🛰", "satellite"), + ("🛳", "passenger ship"), + ("🛴", "scooter"), + ("🛵", "motor scooter"), + ("🛶", "canoe"), + ("🛷", "sled"), + ("🛸", "flying saucer"), + ("🛹", "skateboard"), + ("🛺", "auto rickshaw"), + ("🛻", "pickup truck"), + ("🛼", "roller skate"), + ("🟠", "large orange circle"), + ("🟡", "large yellow circle"), + ("🟢", "large green circle"), + ("🟣", "large purple circle"), + ("🟤", "large brown circle"), + ("🟥", "large red square"), + ("🟦", "large blue square"), + ("🟧", "large orange square"), + ("🟨", "large yellow square"), + ("🟩", "large green square"), + ("🟪", "large purple square"), + ("🟫", "large brown square"), + ("🟰", "heavy equals sign"), + ("🤌", "pinched fingers"), + ("🤍", "white heart"), + ("🤎", "brown heart"), + ("🤏", "pinching hand"), + ("🤐", "zipper mouth face"), + ("🤑", "money mouth face"), + ("🤒", "face with thermometer"), + ("🤓", "nerd face"), + ("🤔", "thinking face"), + ("🤕", "face with head bandage"), + ("🤖", "robot face"), + ("🤗", "hugging face"), + ("🤘", "sign of the horns"), + ("🤙", "call me hand"), + ("🤚", "raised back of hand"), + ("🤛", "left facing fist"), + ("🤜", "right facing fist"), + ("🤝", "handshake"), + ("🤞", "hand with index and middle fingers crossed"), + ("🤟", "i love you hand sign"), + ("🤠", "face with cowboy hat"), + ("🤡", "clown face"), + ("🤢", "nauseated face"), + ("🤣", "rolling on the floor laughing"), + ("🤤", "drooling face"), + ("🤥", "lying face"), + ("🤦", "face palm"), + ("🤧", "sneezing face"), + ("🤨", "face with one eyebrow raised"), + ("🤩", "grinning face with star eyes"), + ("🤪", "grinning face with one large and one small eye"), + ("🤫", "face with finger covering closed lips"), + ("🤬", "serious face with symbols covering mouth"), + ("🤭", "smiling face with smiling eyes and hand covering mouth"), + ("🤮", "face with open mouth vomiting"), + ("🤯", "shocked face with exploding head"), + ("🤰", "pregnant woman"), + ("🤱", "breast feeding"), + ("🤲", "palms up together"), + ("🤳", "selfie"), + ("🤴", "prince"), + ("🤵", "man in tuxedo"), + ("🤶", "mother christmas"), + ("🤷", "shrug"), + ("🤸", "person doing cartwheel"), + ("🤹", "juggling"), + ("🤺", "fencer"), + ("🤼", "wrestlers"), + ("🤽", "water polo"), + ("🤾", "handball"), + ("🤿", "diving mask"), + ("🥀", "wilted flower"), + ("🥁", "drum with drumsticks"), + ("🥂", "clinking glasses"), + ("🥃", "tumbler glass"), + ("🥄", "spoon"), + ("🥅", "goal net"), + ("🥇", "first place medal"), + ("🥈", "second place medal"), + ("🥉", "third place medal"), + ("🥊", "boxing glove"), + ("🥋", "martial arts uniform"), + ("🥌", "curling stone"), + ("🥍", "lacrosse stick and ball"), + ("🥎", "softball"), + ("🥏", "flying disc"), + ("🥐", "croissant"), + ("🥑", "avocado"), + ("🥒", "cucumber"), + ("🥓", "bacon"), + ("🥔", "potato"), + ("🥕", "carrot"), + ("🥖", "baguette bread"), + ("🥗", "green salad"), + ("🥘", "shallow pan of food"), + ("🥙", "stuffed flatbread"), + ("🥚", "egg"), + ("🥛", "glass of milk"), + ("🥜", "peanuts"), + ("🥝", "kiwifruit"), + ("🥞", "pancakes"), + ("🥟", "dumpling"), + ("🥠", "fortune cookie"), + ("🥡", "takeout box"), + ("🥢", "chopsticks"), + ("🥣", "bowl with spoon"), + ("🥤", "cup with straw"), + ("🥥", "coconut"), + ("🥦", "broccoli"), + ("🥧", "pie"), + ("🥨", "pretzel"), + ("🥩", "cut of meat"), + ("🥪", "sandwich"), + ("🥫", "canned food"), + ("🥬", "leafy green"), + ("🥭", "mango"), + ("🥮", "moon cake"), + ("🥯", "bagel"), + ("🥰", "smiling face with smiling eyes and three hearts"), + ("🥱", "yawning face"), + ("🥲", "smiling face with tear"), + ("🥳", "face with party horn and party hat"), + ("🥴", "face with uneven eyes and wavy mouth"), + ("🥵", "overheated face"), + ("🥶", "freezing face"), + ("🥷", "ninja"), + ("🥸", "disguised face"), + ("🥹", "face holding back tears"), + ("🥺", "face with pleading eyes"), + ("🥻", "sari"), + ("🥼", "lab coat"), + ("🥽", "goggles"), + ("🥾", "hiking boot"), + ("🥿", "flat shoe"), + ("🦀", "crab"), + ("🦁", "lion face"), + ("🦂", "scorpion"), + ("🦃", "turkey"), + ("🦄", "unicorn face"), + ("🦅", "eagle"), + ("🦆", "duck"), + ("🦇", "bat"), + ("🦈", "shark"), + ("🦉", "owl"), + ("🦊", "fox face"), + ("🦋", "butterfly"), + ("🦌", "deer"), + ("🦍", "gorilla"), + ("🦎", "lizard"), + ("🦏", "rhinoceros"), + ("🦐", "shrimp"), + ("🦑", "squid"), + ("🦒", "giraffe face"), + ("🦓", "zebra face"), + ("🦔", "hedgehog"), + ("🦕", "sauropod"), + ("🦖", "t rex"), + ("🦗", "cricket"), + ("🦘", "kangaroo"), + ("🦙", "llama"), + ("🦚", "peacock"), + ("🦛", "hippopotamus"), + ("🦜", "parrot"), + ("🦝", "raccoon"), + ("🦞", "lobster"), + ("🦟", "mosquito"), + ("🦠", "microbe"), + ("🦡", "badger"), + ("🦢", "swan"), + ("🦣", "mammoth"), + ("🦤", "dodo"), + ("🦥", "sloth"), + ("🦦", "otter"), + ("🦧", "orangutan"), + ("🦨", "skunk"), + ("🦩", "flamingo"), + ("🦪", "oyster"), + ("🦫", "beaver"), + ("🦬", "bison"), + ("🦭", "seal"), + ("🦮", "guide dog"), + ("🦯", "probing cane"), + ("🦰", "emoji component red hair"), + ("🦱", "emoji component curly hair"), + ("🦲", "emoji component bald"), + ("🦳", "emoji component white hair"), + ("🦴", "bone"), + ("🦵", "leg"), + ("🦶", "foot"), + ("🦷", "tooth"), + ("🦸", "superhero"), + ("🦹", "supervillain"), + ("🦺", "safety vest"), + ("🦻", "ear with hearing aid"), + ("🦼", "motorized wheelchair"), + ("🦽", "manual wheelchair"), + ("🦾", "mechanical arm"), + ("🦿", "mechanical leg"), + ("🧀", "cheese wedge"), + ("🧁", "cupcake"), + ("🧂", "salt shaker"), + ("🧃", "beverage box"), + ("🧄", "garlic"), + ("🧅", "onion"), + ("🧆", "falafel"), + ("🧇", "waffle"), + ("🧈", "butter"), + ("🧉", "mate drink"), + ("🧊", "ice cube"), + ("🧋", "bubble tea"), + ("🧌", "troll"), + ("🧍", "standing person"), + ("🧎", "kneeling person"), + ("🧏", "deaf person"), + ("🧐", "face with monocle"), + ("🧑", "adult"), + ("🧒", "child"), + ("🧓", "older adult"), + ("🧔", "bearded person"), + ("🧕", "person with headscarf"), + ("🧖", "person in steamy room"), + ("🧗", "person climbing"), + ("🧘", "person in lotus position"), + ("🧙", "mage"), + ("🧚", "fairy"), + ("🧛", "vampire"), + ("🧜", "merperson"), + ("🧝", "elf"), + ("🧞", "genie"), + ("🧟", "zombie"), + ("🧡", "orange heart"), + ("🧢", "billed cap"), + ("🧣", "scarf"), + ("🧤", "gloves"), + ("🧥", "coat"), + ("🧦", "socks"), + ("🧧", "red gift envelope"), + ("🧨", "firecracker"), + ("🧫", "petri dish"), + ("🧬", "dna double helix"), + ("🧭", "compass"), + ("🧮", "abacus"), + ("🧯", "fire extinguisher"), + ("🧲", "magnet"), + ("🧳", "luggage"), + ("🧴", "lotion bottle"), + ("🧶", "ball of yarn"), + ("🧷", "safety pin"), + ("🧸", "teddy bear"), + ("🧹", "broom"), + ("🧺", "basket"), + ("🧻", "roll of paper"), + ("🧼", "bar of soap"), + ("🧽", "sponge"), + ("🧾", "receipt"), + ("🧿", "nazar amulet"), + ("🩰", "ballet shoes"), + ("🩱", "one piece swimsuit"), + ("🩲", "briefs"), + ("🩳", "shorts"), + ("🩴", "thong sandal"), + ("🩵", "light blue heart"), + ("🩶", "grey heart"), + ("🩷", "pink heart"), + ("🩸", "drop of blood"), + ("🩹", "adhesive bandage"), + ("🩺", "stethoscope"), + ("🩻", "x ray"), + ("🩼", "crutch"), + ("🪀", "yo yo"), + ("🪁", "kite"), + ("🪂", "parachute"), + ("🪃", "boomerang"), + ("🪅", "pinata"), + ("🪆", "nesting dolls"), + ("🪇", "maracas"), + ("🪈", "flute"), + ("🪉", "harp"), + ("🪊", "trombone"), + ("🪎", "treasure chest"), + ("🪏", "shovel"), + ("🪐", "ringed planet"), + ("🪑", "chair"), + ("🪒", "razor"), + ("🪓", "axe"), + ("🪔", "diya lamp"), + ("🪕", "banjo"), + ("🪖", "military helmet"), + ("🪗", "accordion"), + ("🪘", "long drum"), + ("🪙", "coin"), + ("🪚", "carpentry saw"), + ("🪛", "screwdriver"), + ("🪜", "ladder"), + ("🪝", "hook"), + ("🪞", "mirror"), + ("🪟", "window"), + ("🪠", "plunger"), + ("🪡", "sewing needle"), + ("🪢", "knot"), + ("🪣", "bucket"), + ("🪤", "mouse trap"), + ("🪥", "toothbrush"), + ("🪦", "headstone"), + ("🪧", "placard"), + ("🪨", "rock"), + ("🪩", "mirror ball"), + ("🪪", "identification card"), + ("🪫", "low battery"), + ("🪬", "hamsa"), + ("🪭", "folding hand fan"), + ("🪮", "hair pick"), + ("🪯", "khanda"), + ("🪰", "fly"), + ("🪱", "worm"), + ("🪲", "beetle"), + ("🪳", "cockroach"), + ("🪴", "potted plant"), + ("🪵", "wood"), + ("🪶", "feather"), + ("🪷", "lotus"), + ("🪸", "coral"), + ("🪹", "empty nest"), + ("🪺", "nest with eggs"), + ("🪻", "hyacinth"), + ("🪼", "jellyfish"), + ("🪽", "wing"), + ("🪾", "leafless tree"), + ("🪿", "goose"), + ("🫀", "anatomical heart"), + ("🫁", "lungs"), + ("🫂", "people hugging"), + ("🫃", "pregnant man"), + ("🫄", "pregnant person"), + ("🫅", "person with crown"), + ("🫆", "fingerprint"), + ("🫈", "hairy creature"), + ("🫍", "orca"), + ("🫎", "moose"), + ("🫏", "donkey"), + ("🫐", "blueberries"), + ("🫑", "bell pepper"), + ("🫒", "olive"), + ("🫓", "flatbread"), + ("🫔", "tamale"), + ("🫕", "fondue"), + ("🫖", "teapot"), + ("🫗", "pouring liquid"), + ("🫘", "beans"), + ("🫙", "jar"), + ("🫚", "ginger root"), + ("🫛", "pea pod"), + ("🫜", "root vegetable"), + ("🫟", "splatter"), + ("🫠", "melting face"), + ("🫡", "saluting face"), + ("🫢", "face with open eyes and hand over mouth"), + ("🫣", "face with peeking eye"), + ("🫤", "face with diagonal mouth"), + ("🫥", "dotted line face"), + ("🫦", "biting lip"), + ("🫧", "bubbles"), + ("🫨", "shaking face"), + ("🫩", "face with bags under eyes"), + ("🫪", "distorted face"), + ("🫯", "fight cloud"), + ("🫰", "hand with index finger and thumb crossed"), + ("🫱", "rightwards hand"), + ("🫲", "leftwards hand"), + ("🫳", "palm down hand"), + ("🫴", "palm up hand"), + ("🫵", "index pointing at the viewer"), + ("🫶", "heart hands"), + ("🫷", "leftwards pushing hand"), + ("🫸", "rightwards pushing hand"), + ] +} diff --git a/Sources/WorkspaceGroupEmojiPickerButton.swift b/Sources/WorkspaceGroupEmojiPickerButton.swift new file mode 100644 index 000000000000..fa585ab9d080 --- /dev/null +++ b/Sources/WorkspaceGroupEmojiPickerButton.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct WorkspaceGroupEmojiPickerButton: View, Equatable { + let emoji: String + let isSelected: Bool + let action: () -> Void + + nonisolated static func == (lhs: WorkspaceGroupEmojiPickerButton, rhs: WorkspaceGroupEmojiPickerButton) -> Bool { + lhs.emoji == rhs.emoji && lhs.isSelected == rhs.isSelected + } + + var body: some View { + Button(action: action) { + Text(emoji) + .font(.system(size: 18)) + .frame(width: 34, height: 30) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background( + RoundedRectangle(cornerRadius: 5) + .fill(isSelected ? Color.accentColor.opacity(0.16) : Color.secondary.opacity(0.08)) + ) + .help(emoji) + } +} diff --git a/Sources/WorkspaceGroupIconPickerSectionTitle.swift b/Sources/WorkspaceGroupIconPickerSectionTitle.swift new file mode 100644 index 000000000000..6e580609454e --- /dev/null +++ b/Sources/WorkspaceGroupIconPickerSectionTitle.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct WorkspaceGroupIconPickerSectionTitle: View { + let title: String + + var body: some View { + Text(title) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + } +} diff --git a/Sources/WorkspaceGroupIconPickerView.swift b/Sources/WorkspaceGroupIconPickerView.swift new file mode 100644 index 000000000000..0cad8fafa01f --- /dev/null +++ b/Sources/WorkspaceGroupIconPickerView.swift @@ -0,0 +1,168 @@ +import AppKit +import SwiftUI + +struct WorkspaceGroupIconPickerView: View { + private static let pageSize = 120 + + let currentSymbol: String? + let onSelect: (String?) -> Void + + @State private var searchText = "" + @State private var visibleEmojiCount = pageSize + @FocusState private var searchFieldFocused: Bool + + init(currentSymbol: String?, onSelect: @escaping (String?) -> Void) { + self.currentSymbol = currentSymbol + self.onSelect = onSelect + } + + private var selectedIcon: String? { + RenderableSystemSymbol.normalizedWorkspaceGroupIcon(currentSymbol) + } + + private var currentIcon: RenderableWorkspaceGroupIcon? { + guard let selectedIcon else { return nil } + if let emoji = RenderableSystemSymbol.normalizedEmoji(selectedIcon) { + return .emoji(emoji) + } + return .systemSymbol(selectedIcon) + } + + private var typedEmoji: String? { + RenderableSystemSymbol.normalizedEmoji(searchText) + } + + private var typedSystemSymbol: String? { + guard typedEmoji == nil else { return nil } + return RenderableSystemSymbol.normalizedWorkspaceGroupIcon(searchText) + } + + private var matchedEmoji: [String] { + WorkspaceGroupEmojiCatalog.search(searchText) + } + + var body: some View { + // The baked dataset makes search a fast in-memory scan; `visibleEmoji` caps how many + // cells are ever instantiated so view materialization stays bounded. + let matched = matchedEmoji + let visibleEmoji = Array(matched.prefix(visibleEmojiCount)) + + VStack(spacing: 10) { + TextField( + String(localized: "workspaceGroup.icon.search.placeholder", defaultValue: "Search emoji"), + text: $searchText + ) + .textFieldStyle(.roundedBorder) + .frame(height: 24) + .focused($searchFieldFocused) + .onChange(of: searchText) { _, _ in + visibleEmojiCount = Self.pageSize + } + + ScrollView { + LazyVStack(alignment: .leading, spacing: 10) { + if let selectedIcon, + let currentIcon { + WorkspaceGroupIconPickerSectionTitle( + title: String(localized: "workspaceGroup.icon.section.current", defaultValue: "Current") + ) + Button { + onSelect(selectedIcon) + } label: { + HStack(spacing: 8) { + WorkspaceGroupIconPreview(icon: currentIcon) + .frame(width: 22, height: 22) + Text(selectedIcon) + .font(.system(size: 12)) + .lineLimit(1) + .truncationMode(.tail) + Spacer() + } + .padding(.horizontal, 8) + .frame(height: 32) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background( + RoundedRectangle(cornerRadius: 5) + .fill(Color.accentColor.opacity(0.16)) + ) + .help(selectedIcon) + } + + if let typedSystemSymbol { + WorkspaceGroupIconPickerSectionTitle( + title: String(localized: "workspaceGroup.icon.section.symbols", defaultValue: "Symbols") + ) + Button { + onSelect(typedSystemSymbol) + } label: { + HStack(spacing: 8) { + WorkspaceGroupIconPreview(icon: .systemSymbol(typedSystemSymbol)) + .frame(width: 22, height: 22) + Text(typedSystemSymbol) + .font(.system(size: 12)) + .lineLimit(1) + .truncationMode(.tail) + Spacer() + } + .padding(.horizontal, 8) + .frame(height: 32) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background( + RoundedRectangle(cornerRadius: 5) + .fill(selectedIcon == typedSystemSymbol ? Color.accentColor.opacity(0.16) : Color.secondary.opacity(0.08)) + ) + .help(typedSystemSymbol) + } + + if !visibleEmoji.isEmpty { + WorkspaceGroupIconPickerSectionTitle( + title: String(localized: "workspaceGroup.icon.section.emoji", defaultValue: "Emoji") + ) + LazyVGrid( + columns: [GridItem(.adaptive(minimum: 34, maximum: 42), spacing: 6)], + alignment: .leading, + spacing: 6 + ) { + ForEach(visibleEmoji, id: \.self) { emoji in + WorkspaceGroupEmojiPickerButton( + emoji: emoji, + isSelected: selectedIcon == emoji + ) { + onSelect(emoji) + } + .onAppear { + guard emoji == visibleEmoji.last, + visibleEmoji.count < matched.count else { return } + visibleEmojiCount += Self.pageSize + } + } + } + } else if typedEmoji == nil && typedSystemSymbol == nil { + Text(String(localized: "workspaceGroup.icon.noResults", defaultValue: "No icons found")) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, minHeight: 72) + } + } + .padding(.vertical, 2) + } + .frame(width: 318, height: 306) + + HStack { + Button(String(localized: "workspaceGroup.icon.clear", defaultValue: "Clear Icon")) { + onSelect(nil) + } + Spacer() + } + } + .padding(12) + .frame(width: 344, height: 394) + .onAppear { + searchFieldFocused = true + } + } +} diff --git a/Sources/WorkspaceGroupIconPreview.swift b/Sources/WorkspaceGroupIconPreview.swift new file mode 100644 index 000000000000..ac97fdc8b306 --- /dev/null +++ b/Sources/WorkspaceGroupIconPreview.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct WorkspaceGroupIconPreview: View, Equatable { + let icon: RenderableWorkspaceGroupIcon + /// Glyph point size. The sidebar group header passes its metric-derived `iconFontSize` so the icon + /// scales with the configured sidebar font size; other call sites (picker rows) use the default. + var fontSize: CGFloat = 13 + + var body: some View { + switch icon { + case .systemSymbol(let symbol): + Image(systemName: symbol) + .font(.system(size: fontSize, weight: .semibold)) + case .emoji(let emoji): + // Emoji glyphs read slightly small next to SF Symbols at the same point size; nudge up, + // preserving the original 13->14 relationship while still scaling with `fontSize`. + Text(emoji) + .font(.system(size: fontSize + 1)) + } + } +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 860290b8dddb..7dddf5e74a20 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -225,6 +225,7 @@ C0DEF0A90000000000000001 /* ContentView+ForkAgentConversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF0A90000000000000002 /* ContentView+ForkAgentConversation.swift */; }; D7AB00000000000000000003 /* ContentView+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000004 /* ContentView+MoveTabToNewWorkspace.swift */; }; C3408A000000000000000001 /* ContentView+RightSidebarCommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3408A000000000000000002 /* ContentView+RightSidebarCommandPalette.swift */; }; + D5037060000000000000003 /* ContentView+SetGroupIconCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037060000000000000002 /* ContentView+SetGroupIconCommand.swift */; }; C3408A000000000000000005 /* ContentView+ViewCommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3408A000000000000000006 /* ContentView+ViewCommandPalette.swift */; }; A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; }; C0DE32470000000000000001 /* ContentViewIdentifierCopyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */; }; @@ -427,6 +428,7 @@ B9000027A1B2C3D4E5F60719 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; }; E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */; }; D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037010000000000000002 /* RenderableSystemSymbol.swift */; }; + D5037040000000000000003 /* RenderableWorkspaceGroupIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037040000000000000002 /* RenderableWorkspaceGroupIcon.swift */; }; F5410004A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */; }; F5410000A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */; }; F5410002A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */; }; @@ -631,6 +633,12 @@ F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; }; E6FA9084A1B2C3D4E5F60718 /* WorkspaceDescriptionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FA9085A1B2C3D4E5F60718 /* WorkspaceDescriptionUITests.swift */; }; C0DE36230000000000000001 /* WorkspaceFinderDirectoryResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE36230000000000000002 /* WorkspaceFinderDirectoryResolver.swift */; }; + D5037080000000000000003 /* WorkspaceGroupEmojiCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037080000000000000002 /* WorkspaceGroupEmojiCatalog.swift */; }; + D5037070000000000000003 /* WorkspaceGroupEmojiData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037070000000000000002 /* WorkspaceGroupEmojiData.swift */; }; + D5037090000000000000003 /* WorkspaceGroupEmojiPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037090000000000000002 /* WorkspaceGroupEmojiPickerButton.swift */; }; + D50370B0000000000000003 /* WorkspaceGroupIconPickerSectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50370B0000000000000002 /* WorkspaceGroupIconPickerSectionTitle.swift */; }; + D5037020000000000000003 /* WorkspaceGroupIconPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037020000000000000002 /* WorkspaceGroupIconPickerView.swift */; }; + D5037050000000000000003 /* WorkspaceGroupIconPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037050000000000000002 /* WorkspaceGroupIconPreview.swift */; }; C9A57203C9A57203C9A57203 /* WorkspaceGroupMenuSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57204C9A57204C9A57204 /* WorkspaceGroupMenuSnapshot.swift */; }; C9A57001C9A57001C9A57001 /* WorkspaceGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57002C9A57002C9A57002 /* WorkspaceGroupTests.swift */; }; 0F2C25F9170130F8DC09DD1B /* WorkspaceManualUnreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */; }; @@ -905,6 +913,7 @@ C0DEF0A90000000000000002 /* ContentView+ForkAgentConversation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+ForkAgentConversation.swift"; sourceTree = ""; }; D7AB00000000000000000004 /* ContentView+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; C3408A000000000000000002 /* ContentView+RightSidebarCommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+RightSidebarCommandPalette.swift"; sourceTree = ""; }; + D5037060000000000000002 /* ContentView+SetGroupIconCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+SetGroupIconCommand.swift"; sourceTree = ""; }; C3408A000000000000000006 /* ContentView+ViewCommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+ViewCommandPalette.swift"; sourceTree = ""; }; A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewIdentifierCopyCommands.swift; sourceTree = ""; }; @@ -1101,6 +1110,7 @@ A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = ""; }; E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShellSessionParsing.swift; sourceTree = ""; }; D5037010000000000000002 /* RenderableSystemSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderableSystemSymbol.swift; sourceTree = ""; }; + D5037040000000000000002 /* RenderableWorkspaceGroupIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderableWorkspaceGroupIcon.swift; sourceTree = ""; }; F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderHermesTests.swift; sourceTree = ""; }; F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentHookProviderResumeTests.swift; sourceTree = ""; }; F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentNonInteractiveTests.swift; sourceTree = ""; }; @@ -1301,6 +1311,12 @@ F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = ""; }; E6FA9085A1B2C3D4E5F60718 /* WorkspaceDescriptionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceDescriptionUITests.swift; sourceTree = ""; }; C0DE36230000000000000002 /* WorkspaceFinderDirectoryResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceFinderDirectoryResolver.swift; sourceTree = ""; }; + D5037080000000000000002 /* WorkspaceGroupEmojiCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupEmojiCatalog.swift; sourceTree = ""; }; + D5037070000000000000002 /* WorkspaceGroupEmojiData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupEmojiData.swift; sourceTree = ""; }; + D5037090000000000000002 /* WorkspaceGroupEmojiPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupEmojiPickerButton.swift; sourceTree = ""; }; + D50370B0000000000000002 /* WorkspaceGroupIconPickerSectionTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupIconPickerSectionTitle.swift; sourceTree = ""; }; + D5037020000000000000002 /* WorkspaceGroupIconPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupIconPickerView.swift; sourceTree = ""; }; + D5037050000000000000002 /* WorkspaceGroupIconPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupIconPreview.swift; sourceTree = ""; }; C9A57204C9A57204C9A57204 /* WorkspaceGroupMenuSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupMenuSnapshot.swift; sourceTree = ""; }; C9A57002C9A57002C9A57002 /* WorkspaceGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceGroupTests.swift; sourceTree = ""; }; 1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceManualUnreadTests.swift; sourceTree = ""; }; @@ -1585,7 +1601,15 @@ C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */, C9A57302C9A57302C9A57302 /* SidebarWorkspaceGroupHeaderMetrics.swift */, C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */, + D5037040000000000000002 /* RenderableWorkspaceGroupIcon.swift */, D5037010000000000000002 /* RenderableSystemSymbol.swift */, + D5037090000000000000002 /* WorkspaceGroupEmojiPickerButton.swift */, + D5037080000000000000002 /* WorkspaceGroupEmojiCatalog.swift */, + D5037070000000000000002 /* WorkspaceGroupEmojiData.swift */, + D5037050000000000000002 /* WorkspaceGroupIconPreview.swift */, + D50370B0000000000000002 /* WorkspaceGroupIconPickerSectionTitle.swift */, + D5037020000000000000002 /* WorkspaceGroupIconPickerView.swift */, + D5037060000000000000002 /* ContentView+SetGroupIconCommand.swift */, C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */, C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */, C9A5720AC9A5720AC9A5720A /* VerticalTabsSidebar+WorkspaceGroups.swift */, @@ -2549,6 +2573,7 @@ C0DEF0A90000000000000001 /* ContentView+ForkAgentConversation.swift in Sources */, D7AB00000000000000000003 /* ContentView+MoveTabToNewWorkspace.swift in Sources */, C3408A000000000000000001 /* ContentView+RightSidebarCommandPalette.swift in Sources */, + D5037060000000000000003 /* ContentView+SetGroupIconCommand.swift in Sources */, C3408A000000000000000005 /* ContentView+ViewCommandPalette.swift in Sources */, A5001002 /* ContentView.swift in Sources */, C0DE32470000000000000001 /* ContentViewIdentifierCopyCommands.swift in Sources */, @@ -2679,6 +2704,7 @@ A5001640 /* RemoteRelayZshBootstrap.swift in Sources */, E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */, D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */, + D5037040000000000000003 /* RenderableWorkspaceGroupIcon.swift in Sources */, A5001660 /* RestorableAgentSession.swift in Sources */, C13519000000000000000005 /* RestorableAgentTypes.swift in Sources */, B7F9A600B7F9A600B7F9A600 /* RightSidebarChromeGeometryReporting.swift in Sources */, @@ -2808,6 +2834,12 @@ D7AB3605C10DEF0000000003 /* WorkspaceCloseTabsBatching.swift in Sources */, A5001407 /* WorkspaceContentView.swift in Sources */, C0DE36230000000000000001 /* WorkspaceFinderDirectoryResolver.swift in Sources */, + D5037080000000000000003 /* WorkspaceGroupEmojiCatalog.swift in Sources */, + D5037070000000000000003 /* WorkspaceGroupEmojiData.swift in Sources */, + D5037090000000000000003 /* WorkspaceGroupEmojiPickerButton.swift in Sources */, + D50370B0000000000000003 /* WorkspaceGroupIconPickerSectionTitle.swift in Sources */, + D5037020000000000000003 /* WorkspaceGroupIconPickerView.swift in Sources */, + D5037050000000000000003 /* WorkspaceGroupIconPreview.swift in Sources */, C9A57203C9A57203C9A57203 /* WorkspaceGroupMenuSnapshot.swift in Sources */, D0B10004A1B2C3D4E5F60001 /* WorkspacePortalPaneDrop.swift in Sources */, 1A1B2C3D4E5F607180000001 /* WorkspacePromptSubmit.swift in Sources */, diff --git a/cmuxTests/ForeignFirstResponderPolicyTests.swift b/cmuxTests/ForeignFirstResponderPolicyTests.swift index 56439f972f2a..b837696146ac 100644 --- a/cmuxTests/ForeignFirstResponderPolicyTests.swift +++ b/cmuxTests/ForeignFirstResponderPolicyTests.swift @@ -9,7 +9,9 @@ import Testing /// Coverage for ``shouldRespectForeignFirstResponder(_:in:isRightSidebarOwner:)`` — the policy that /// decides whether an active terminal yields to the window's current first responder or reclaims -/// focus. Regression coverage for issue #5269 (a stranded responder must not block focus). +/// focus. Text editors are always respected (the user is typing into them, and they may be hosted in +/// an overlay window); non-text focus owners must belong to the window, preserving the #5269 recovery +/// path for stranded hosts. @MainActor @Suite struct ForeignFirstResponderPolicyTests { private func makeWindow() -> NSWindow { @@ -31,13 +33,27 @@ import Testing #expect(shouldRespectForeignFirstResponder(textView, in: window, isRightSidebarOwner: neverSidebarOwner)) } - /// The #5269 regression: a text responder stranded in another window must NOT be respected, so - /// the terminal can reclaim focus. Without the window-membership check this returns `true`. - @Test func reclaimsFromStrandedTextEditorInAnotherWindow() { + /// A text editor hosted in a *child* window of the policy window is respected. SwiftUI hosts a + /// popover/palette overlay's field editor in a separate `_NSPopoverWindow` whose `parent` chain + /// reaches the main window (verified at runtime), even though it is the main window's active first + /// responder; the strict `.window === window` guard rejected it and keyRepair stole the keystroke. + @Test func respectsTextEditorInChildPopoverWindow() { + let host = makeWindow() + let popover = makeWindow() + host.addChildWindow(popover, ordered: .above) // popover.parent === host, like an NSPopover window + let textView = NSTextView(frame: .zero) + popover.contentView?.addSubview(textView) + #expect(shouldRespectForeignFirstResponder(textView, in: host, isRightSidebarOwner: neverSidebarOwner)) + } + + /// A text editor stranded in an *unrelated* window (not the policy window and not a child it + /// presents) is reclaimed, so a field reparented into another cmux window cannot block the terminal + /// from recovering keyboard focus (issue #5269). + @Test func reclaimsFromTextEditorInUnrelatedWindow() { let windowA = makeWindow() - let windowB = makeWindow() + let windowB = makeWindow() // not a child of windowA let textView = NSTextView(frame: .zero) - windowB.contentView?.addSubview(textView) // belongs to windowB, not windowA + windowB.contentView?.addSubview(textView) #expect(!shouldRespectForeignFirstResponder(textView, in: windowA, isRightSidebarOwner: neverSidebarOwner)) } @@ -58,10 +74,21 @@ import Testing #expect(!shouldRespectForeignFirstResponder(view, in: windowA, isRightSidebarOwner: alwaysSidebarOwner)) } - @Test func reclaimsFromDetachedResponder() { + /// A non-text responder detached from any window is reclaimed (it is neither a real text editor + /// being typed into nor an in-window focus owner), preserving the #5269 recovery path. + @Test func reclaimsFromDetachedNonTextResponder() { + let window = makeWindow() + let view = NSView(frame: .zero) // never added to a window -> .window is nil + #expect(!shouldRespectForeignFirstResponder(view, in: window, isRightSidebarOwner: alwaysSidebarOwner)) + } + + /// A *detached* text editor (no backing window, e.g. a removed field left as first responder) is + /// reclaimed: the overlay exception requires a non-nil window, so a stale editor cannot permanently + /// block terminal focus recovery (issue #5269). + @Test func reclaimsFromDetachedTextEditor() { let window = makeWindow() let textView = NSTextView(frame: .zero) // never added to a window -> .window is nil - #expect(!shouldRespectForeignFirstResponder(textView, in: window, isRightSidebarOwner: alwaysSidebarOwner)) + #expect(!shouldRespectForeignFirstResponder(textView, in: window, isRightSidebarOwner: neverSidebarOwner)) } @Test func doesNotRespectPlainInWindowView() { diff --git a/cmuxTests/WorkspaceGroupTests.swift b/cmuxTests/WorkspaceGroupTests.swift index 606dc44b1577..c998677c85fb 100644 --- a/cmuxTests/WorkspaceGroupTests.swift +++ b/cmuxTests/WorkspaceGroupTests.swift @@ -684,9 +684,11 @@ struct WorkspaceGroupTests { #expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: nil, configured: nil) == "folder.fill") #expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: " ", configured: "leaf.fill") == "leaf.fill") #expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: "not.an.sf.symbol", configured: nil) == "folder.fill") + #expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: " 🚀 ", configured: nil) == "🚀") + #expect(RenderableSystemSymbol.resolvedWorkspaceGroupIcon(explicit: "not.an.sf.symbol", configured: "🔥") == "🔥") } - @Test func setWorkspaceGroupIconDropsInvalidSymbols() { + @Test func setWorkspaceGroupIconDropsInvalidSymbolsAndStoresEmoji() { let manager = makeTabManager() let groupId = manager.createWorkspaceGroup( name: "G", @@ -700,6 +702,18 @@ struct WorkspaceGroupTests { let validStoredIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: " leaf.fill ") #expect(validStoredIcon == "leaf.fill") #expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == "leaf.fill") + + let emojiIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: " 🚀 ") + #expect(emojiIcon == "🚀") + #expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == "🚀") + + let multipleEmojiIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: "🚀🔥") + #expect(multipleEmojiIcon == nil) + #expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == nil) + + let clearedIcon = manager.setWorkspaceGroupIcon(groupId: groupId, symbol: " ") + #expect(clearedIcon == nil) + #expect(manager.workspaceGroups.first { $0.id == groupId }?.iconSymbol == nil) } @Test func surfaceTabIconSymbolResolutionFallsBackForInvalidInput() { @@ -709,6 +723,37 @@ struct WorkspaceGroupTests { #expect(RenderableSystemSymbol.resolvedSurfaceTabIcon(" ") == "doc.text") } + // The icon picker feeds its search field through `isRenderable`, so a long typing session + // would otherwise grow the global renderability cache without bound. The cache must stay capped. + @Test func renderabilityCacheStaysBoundedUnderManyDistinctQueries() { + RenderableSystemSymbol.resetRenderabilityCacheForTesting() + let limit = RenderableSystemSymbol.renderabilityCacheLimitForTesting + for index in 0..<(limit + 200) { + _ = RenderableSystemSymbol.isRenderable("picker.query.\(index)") + } + #expect(RenderableSystemSymbol.renderabilityCacheCountForTesting() <= limit) + } + + @Test func renderabilityRejectsOverlongSymbolNames() { + let overlong = String(repeating: "a", count: 256) + #expect(RenderableSystemSymbol.isRenderable(overlong) == false) + } + + @Test func emojiCatalogSearchesByKeyword() { + // Keyword search is the whole point: typing a name finds the emoji. + #expect(WorkspaceGroupEmojiCatalog.search("rocket").contains("🚀")) + #expect(WorkspaceGroupEmojiCatalog.search("fire").contains("🔥")) + #expect(WorkspaceGroupEmojiCatalog.search("folder").contains("📁")) + // Exact-name matches rank first. + #expect(WorkspaceGroupEmojiCatalog.search("rocket").first == "🚀") + // An empty query browses the whole catalog. + #expect(WorkspaceGroupEmojiCatalog.search("").count > 100) + // A pasted emoji echoes itself. + #expect(WorkspaceGroupEmojiCatalog.search("🚀") == ["🚀"]) + // Non-matching text yields nothing (so the picker shows its empty state). + #expect(WorkspaceGroupEmojiCatalog.search("zzzznotanemoji").isEmpty) + } + // Regression for #5404: renaming a group must update the name shown in // window chrome (the custom title bar / NSWindow title / toolbar label), // not just the sidebar header. The chrome derives a grouped anchor's diff --git a/docs/workspace-groups.md b/docs/workspace-groups.md index 56dae9ebeb13..babbe7169ea5 100644 --- a/docs/workspace-groups.md +++ b/docs/workspace-groups.md @@ -1,6 +1,6 @@ # Workspace Groups -Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has an implicit "anchor workspace," a customizable `+` button for spawning new workspaces inside it, and right-click actions for renaming, pinning, ungrouping, and editing its configuration. +Workspace groups let you nest workspaces into collapsible named sections in the sidebar. Each group has an implicit "anchor workspace," a customizable `+` button for spawning new workspaces inside it, and right-click actions for renaming, setting an icon, pinning, ungrouping, and editing its configuration. ## Concepts @@ -38,7 +38,7 @@ Right-click any workspace in the sidebar, choose **New Group from Workspace** (o ### From the group header context menu -Right-click an existing group's header for: **Rename Group…**, **Pin / Unpin Group**, **Edit Group Config…** (opens `~/.config/cmux/cmux.json`), **Open Workspace Groups Docs**, **Ungroup (Keep Workspaces)**, **Delete Group (Close Workspaces)**. Delete is destructive and prompts for confirmation; ungroup keeps the workspaces and just removes the container. +Right-click an existing group's header for: **Rename Group…**, **Set Group Icon…**, **Pin / Unpin Group**, **Edit Group Config…** (opens `~/.config/cmux/cmux.json`), **Open Workspace Groups Docs**, **Ungroup (Keep Workspaces)**, **Delete Group (Close Workspaces)**. Delete is destructive and prompts for confirmation; ungroup keeps the workspaces and just removes the container. ### From the `+` button on a group header @@ -66,8 +66,12 @@ cmux workspace-group add --group --workspace cmux workspace-group remove --workspace cmux workspace-group set-anchor --group --workspace cmux workspace-group new-workspace [--placement afterCurrent|top|end] +cmux workspace-group set-color [--hex "#RRGGBB"] +cmux workspace-group set-icon [--symbol ] ``` +Omitting `--hex` or `--symbol` clears the custom color or icon and reverts to the default. + `create` returns a group handle (`workspace_group:N` by default). Pass `--json` for the full structured payload. ### Examples diff --git a/scripts/generate-workspace-group-emoji.swift b/scripts/generate-workspace-group-emoji.swift new file mode 100644 index 000000000000..40b2ec385fa6 --- /dev/null +++ b/scripts/generate-workspace-group-emoji.swift @@ -0,0 +1,81 @@ +#!/usr/bin/env swift +// Regenerates Sources/WorkspaceGroupEmojiData.swift, the baked emoji dataset for the +// workspace group icon picker. Run from the repo root after a Unicode/toolchain update: +// +// swift scripts/generate-workspace-group-emoji.swift +// +// Keywords come from the Unicode scalar names so the picker can search by name +// ("rocket" -> 🚀) without scanning Unicode ranges at runtime. +import Foundation + +let commonEmoji = [ + "🚀", "💻", "🧠", "⚙️", "🔥", "✅", "📁", "🧪", "🎯", "✨", "⚡️", "⭐️", + "🔧", "📌", "📝", "🔍", "🎨", "🧩", "📦", "🌐", "🔒", "💬", "🏁", "📊", + "🛠️", "🧰", "📣", "💡", "🕹️", "🧵", "🪄", "🧱", "📎", "🗂️", "📚", "🔬" +] +let ranges: [ClosedRange] = [0x203C ... 0x3299, 0x1F000 ... 0x1FAFF] + +func isEmojiCharacter(_ ch: Character) -> Bool { + let scalars = ch.unicodeScalars + let containsEmoji = scalars.contains { $0.properties.isEmoji } + let containsPresentation = scalars.contains { + $0.properties.isEmojiPresentation || $0.properties.isEmojiModifier || + $0.properties.isEmojiModifierBase || $0.value == 0xFE0F + } + let asciiOnly = scalars.allSatisfy { $0.value < 128 } + return containsEmoji && (containsPresentation || !asciiOnly) +} + +func keywords(for ch: Character) -> String { + var parts: [String] = [] + for s in ch.unicodeScalars { + if s.value == 0xFE0F || s.value == 0x200D { continue } + if let n = s.properties.name { parts.append(n.lowercased()) } + } + let mapped = parts.joined(separator: " ").unicodeScalars.map { + CharacterSet.alphanumerics.contains($0) ? Character($0) : " " + } + return String(mapped).split(separator: " ").joined(separator: " ") +} + +var seen = Set() +var entries: [(String, String)] = [] +func add(_ ch: Character) { + let v = String(ch) + guard !seen.contains(v) else { return } + seen.insert(v) + entries.append((v, keywords(for: ch))) +} + +for e in commonEmoji where e.count == 1 { add(e.first!) } +for r in ranges { + for value in r { + guard let s = Unicode.Scalar(value) else { continue } + let ch = Character(s) + guard isEmojiCharacter(ch) else { continue } + add(ch) + } +} + +var out = """ +// GENERATED by scripts/generate-workspace-group-emoji.swift. Do not edit by hand. +// Regenerate after Unicode updates: swift scripts/generate-workspace-group-emoji.swift + +/// Baked, searchable emoji dataset for the workspace group icon picker. +/// +/// Each entry pairs an emoji with its lowercased keyword text (from the Unicode scalar +/// names). Built once at dev time so the picker never scans Unicode ranges at runtime. +enum WorkspaceGroupEmojiData { + /// `(value, keywords)` pairs in browse order (common emoji first). + static let entries: [(value: String, keywords: String)] = [ + +""" +for (v, k) in entries { + let ev = v.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + let ek = k.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + out += " (\"\(ev)\", \"\(ek)\"),\n" +} +out += " ]\n}\n" + +try! out.write(toFile: "Sources/WorkspaceGroupEmojiData.swift", atomically: true, encoding: .utf8) +FileHandle.standardError.write("generated \(entries.count) emoji entries\n".data(using: .utf8)!)