diff --git a/CHANGELOG.md b/CHANGELOG.md index 89dd75b27..286104051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to cmux are documented here. ## [0.62.2] - 2026-03-14 ### Added +- Configurable sidebar tint color with separate light/dark mode support via Settings and config file (`sidebar-background`, `sidebar-tint-opacity`) ([#1465](https://github.com/manaflow-ai/cmux/pull/1465)) - Cmd+P all-surfaces search option ([#1382](https://github.com/manaflow-ai/cmux/pull/1382)) - `cmux themes` command with bundled Ghostty themes ([#1334](https://github.com/manaflow-ai/cmux/pull/1334), [#1314](https://github.com/manaflow-ai/cmux/pull/1314)) - Sidebar can now shrink to smaller widths ([#1420](https://github.com/manaflow-ai/cmux/pull/1420)) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 2a67586c1..812aef3bd 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -73580,6 +73580,1475 @@ } } } + }, + "settings.section.sidebarAppearance": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sidebar Appearance" + } + }, + "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": "Seitenleisten-Erscheinungsbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apariencia de la barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apparence de la barre latérale" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aspetto barra laterale" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sidebjælkeudseende" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wygląd paska bocznego" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Внешний вид боковой панели" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Izgled bočne trake" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مظهر الشريط الجانبي" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Sidefelts utseende" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aparência da Barra Lateral" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รูปลักษณ์แถบด้านข้าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kenar Çubuğu Görünümü" + } + } + } + }, + "settings.sidebarAppearance.tintColorLight": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Light Mode Tint" + } + }, + "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": "Farbton im hellen Modus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tinte del modo claro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Teinte du mode clair" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tinta modalità chiara" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Lys tilstand farvetone" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Odcień trybu jasnego" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Оттенок светлого режима" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nijansa svijetlog načina" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون الوضع الفاتح" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fargetone for lys modus" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tonalidade do Modo Claro" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โทนสีโหมดสว่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Açık Mod Renk Tonu" + } + } + } + }, + "settings.sidebarAppearance.tintColorLight.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sidebar tint color when using light appearance." + } + }, + "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": "Seitenleisten-Farbton bei hellem Erscheinungsbild." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color de tinte de la barra lateral en apariencia clara." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur de teinte de la barre latérale en apparence claire." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore tinta della barra laterale in modalità chiara." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sidebjælkens farvetone ved lyst udseende." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor odcienia paska bocznego w jasnym wyglądzie." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет оттенка боковой панели в светлом режиме." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja nijanse bočne trake pri svijetlom izgledu." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون تلوين الشريط الجانبي عند استخدام المظهر الفاتح." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fargetone for sidefeltet med lyst utseende." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor de tonalidade da barra lateral na aparência clara." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีโทนของแถบด้านข้างเมื่อใช้รูปลักษณ์สว่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Açık görünüm kullanılırken kenar çubuğu renk tonu." + } + } + } + }, + "settings.sidebarAppearance.tintColorLight.picker": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Light tint" + } + }, + "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": "Heller Farbton" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tinte claro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Teinte claire" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tinta chiara" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Lys farvetone" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Jasny odcień" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Светлый оттенок" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Svijetla nijansa" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون فاتح" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Lys fargetone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tonalidade clara" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โทนสว่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Açık ton" + } + } + } + }, + "settings.sidebarAppearance.tintColorDark": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dark Mode Tint" + } + }, + "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": "Farbton im dunklen Modus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tinte del modo oscuro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Teinte du mode sombre" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tinta modalità scura" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Mørk tilstand farvetone" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Odcień trybu ciemnego" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Оттенок тёмного режима" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nijansa tamnog načina" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون الوضع الداكن" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fargetone for mørk modus" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tonalidade do Modo Escuro" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โทนสีโหมดมืด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Koyu Mod Renk Tonu" + } + } + } + }, + "settings.sidebarAppearance.tintColorDark.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sidebar tint color when using dark appearance." + } + }, + "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": "Seitenleisten-Farbton bei dunklem Erscheinungsbild." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color de tinte de la barra lateral en apariencia oscura." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur de teinte de la barre latérale en apparence sombre." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore tinta della barra laterale in modalità scura." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sidebjælkens farvetone ved mørkt udseende." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor odcienia paska bocznego w ciemnym wyglądzie." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет оттенка боковой панели в тёмном режиме." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja nijanse bočne trake pri tamnom izgledu." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون تلوين الشريط الجانبي عند استخدام المظهر الداكن." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fargetone for sidefeltet med mørkt utseende." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor de tonalidade da barra lateral na aparência escura." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีโทนของแถบด้านข้างเมื่อใช้รูปลักษณ์มืด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Koyu görünüm kullanılırken kenar çubuğu renk tonu." + } + } + } + }, + "settings.sidebarAppearance.tintColorDark.picker": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dark tint" + } + }, + "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": "Dunkler Farbton" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tinte oscuro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Teinte sombre" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tinta scura" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Mørk farvetone" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ciemny odcień" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Тёмный оттенок" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Tamna nijansa" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون داكن" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Mørk fargetone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tonalidade escura" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โทนมืด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Koyu ton" + } + } + } + }, + "settings.sidebarAppearance.tintOpacity": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tint Opacity" + } + }, + "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": "Farbton-Deckkraft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opacidad del tinte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Opacité de la teinte" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opacità tinta" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Farvetone gennemsigtighed" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Krycie odcienia" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Непрозрачность оттенка" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prozirnost nijanse" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شفافية اللون" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fargetone-opasitet" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opacidade da Tonalidade" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ความทึบของโทนสี" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Renk Tonu Opaklığı" + } + } + } + }, + "settings.sidebarAppearance.tintOpacity.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How strongly the tint color shows over the sidebar material." + } + }, + "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": "Wie stark der Farbton über dem Seitenleisten-Material sichtbar ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Intensidad del color de tinte sobre el material de la barra lateral." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intensité de la teinte sur le matériau de la barre latérale." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Intensità della tinta sul materiale della barra laterale." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Hvor stærkt farvetonen vises over sidebjælkens materiale." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Jak silnie kolor odcienia jest widoczny na materiale paska bocznego." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Насколько сильно оттенок виден на материале боковой панели." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Koliko jako se boja nijanse prikazuje preko materijala bočne trake." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مدى قوة ظهور لون التلوين فوق مادة الشريط الجانبي." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Hvor sterkt fargetonen vises over sidefelts materiale." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Intensidade da cor de tonalidade sobre o material da barra lateral." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ความเข้มของสีโทนที่แสดงบนวัสดุแถบด้านข้าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Renk tonunun kenar çubuğu materyali üzerinde ne kadar güçlü göründüğü." + } + } + } + }, + "settings.sidebarAppearance.reset": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset Sidebar Tint" + } + }, + "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": "Seitenleisten-Farbton zurücksetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restablecer tinte de la barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser la teinte de la barre latérale" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina tinta barra laterale" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Nulstil sidebjælkens farvetone" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Resetuj odcień paska bocznego" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Сбросить оттенок боковой панели" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Resetuj nijansu bočne trake" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تعيين لون الشريط الجانبي" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Tilbakestill sidefelts fargetone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Redefinir Tonalidade da Barra Lateral" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีเซ็ตโทนสีแถบด้านข้าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kenar Çubuğu Renk Tonunu Sıfırla" + } + } + } + }, + "settings.sidebarAppearance.reset.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore default sidebar appearance." + } + }, + "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": "Standard-Seitenleisten-Erscheinungsbild wiederherstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restaurar la apariencia predeterminada de la barra lateral." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer l'apparence par défaut de la barre latérale." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina l'aspetto predefinito della barra laterale." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Gendan standardudseendet for sidebjælken." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przywróć domyślny wygląd paska bocznego." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Восстановить внешний вид боковой панели по умолчанию." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Vrati zadani izgled bočne trake." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استعادة مظهر الشريط الجانبي الافتراضي." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Gjenopprett standard sidefelts utseende." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar aparência padrão da barra lateral." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คืนค่ารูปลักษณ์แถบด้านข้างเป็นค่าเริ่มต้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Varsayılan kenar çubuğu görünümünü geri yükle." + } + } + } + }, + "settings.sidebarAppearance.reset.button": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" + } + }, + "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": "Zurücksetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restablecer" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Nulstil" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Resetuj" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Сбросить" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Resetuj" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تعيين" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Tilbakestill" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Redefinir" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รีเซ็ต" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sıfırla" + } + } + } + }, + "settings.sidebarAppearance.defaultLabel": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default" + } + }, + "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": "Standard" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Predeterminado" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Predefinito" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Domyślny" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "По умолчанию" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Zadano" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "افتراضي" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Padrão" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ค่าเริ่มต้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Varsayılan" + } + } + } } } } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 5f25af790..7d966328b 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -12559,19 +12559,30 @@ private struct TitlebarLeadingInsetReader: NSViewRepresentable { } private struct SidebarBackdrop: View { - @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = 0.18 - @AppStorage("sidebarTintHex") private var sidebarTintHex = "#000000" + @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity + @AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex + @AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String? + @AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String? @AppStorage("sidebarMaterial") private var sidebarMaterial = SidebarMaterialOption.sidebar.rawValue @AppStorage("sidebarBlendMode") private var sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue @AppStorage("sidebarState") private var sidebarState = SidebarStateOption.followWindow.rawValue @AppStorage("sidebarCornerRadius") private var sidebarCornerRadius = 0.0 @AppStorage("sidebarBlurOpacity") private var sidebarBlurOpacity = 1.0 + @Environment(\.colorScheme) private var colorScheme var body: some View { let materialOption = SidebarMaterialOption(rawValue: sidebarMaterial) let blendingMode = SidebarBlendModeOption(rawValue: sidebarBlendMode)?.mode ?? .behindWindow let state = SidebarStateOption(rawValue: sidebarState)?.state ?? .active - let tintColor = (NSColor(hex: sidebarTintHex) ?? .black).withAlphaComponent(sidebarTintOpacity) + let resolvedHex: String = { + if colorScheme == .dark, let dark = sidebarTintHexDark { + return dark + } else if colorScheme == .light, let light = sidebarTintHexLight { + return light + } + return sidebarTintHex + }() + let tintColor = (NSColor(hex: resolvedHex) ?? NSColor(hex: sidebarTintHex) ?? .black).withAlphaComponent(sidebarTintOpacity) let cornerRadius = CGFloat(max(0, sidebarCornerRadius)) let useLiquidGlass = materialOption?.usesLiquidGlass ?? false let useWindowLevelGlass = useLiquidGlass && blendingMode == .behindWindow @@ -12706,6 +12717,11 @@ enum SidebarStateOption: String, CaseIterable, Identifiable { } } +enum SidebarTintDefaults { + static let hex = "#000000" + static let opacity = 0.18 +} + enum SidebarPresetOption: String, CaseIterable, Identifiable { case nativeSidebar case glassBehind diff --git a/Sources/GhosttyConfig.swift b/Sources/GhosttyConfig.swift index 370572ae6..13f78f125 100644 --- a/Sources/GhosttyConfig.swift +++ b/Sources/GhosttyConfig.swift @@ -29,6 +29,13 @@ struct GhosttyConfig { var selectionBackground: NSColor = NSColor(hex: "#57584f")! var selectionForeground: NSColor = NSColor(hex: "#fdfff1")! + // Sidebar appearance + var rawSidebarBackground: String? + var sidebarBackground: NSColor? + var sidebarBackgroundLight: NSColor? + var sidebarBackgroundDark: NSColor? + var sidebarTintOpacity: Double? + // Palette colors (0-15) var palette: [Int: NSColor] = [:] @@ -134,6 +141,54 @@ struct GhosttyConfig { return [] } + mutating func resolveSidebarBackground(preferredColorScheme: ColorSchemePreference) { + guard let raw = rawSidebarBackground else { return } + + let lightResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .light) + let darkResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .dark) + let hasDualMode = lightResolved != darkResolved + + if hasDualMode { + sidebarBackgroundLight = NSColor(hex: lightResolved) + sidebarBackgroundDark = NSColor(hex: darkResolved) + } + + let resolved = Self.resolveThemeName(from: raw, preferredColorScheme: preferredColorScheme) + if let color = NSColor(hex: resolved) { + sidebarBackground = color + } + } + + func applySidebarAppearanceToUserDefaults() { + guard rawSidebarBackground != nil else { + if let opacity = sidebarTintOpacity { + UserDefaults.standard.set(opacity, forKey: "sidebarTintOpacity") + } + return + } + + let defaults = UserDefaults.standard + + if let light = sidebarBackgroundLight { + defaults.set(light.hexString(), forKey: "sidebarTintHexLight") + } else { + defaults.removeObject(forKey: "sidebarTintHexLight") + } + if let dark = sidebarBackgroundDark { + defaults.set(dark.hexString(), forKey: "sidebarTintHexDark") + } else { + defaults.removeObject(forKey: "sidebarTintHexDark") + } + if let color = sidebarBackground { + defaults.set(color.hexString(), forKey: "sidebarTintHex") + } else { + defaults.removeObject(forKey: "sidebarTintHex") + } + if let opacity = sidebarTintOpacity { + defaults.set(opacity, forKey: "sidebarTintOpacity") + } + } + private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig { var config = GhosttyConfig() @@ -161,6 +216,9 @@ struct GhosttyConfig { ) } + config.resolveSidebarBackground(preferredColorScheme: preferredColorScheme) + config.applySidebarAppearanceToUserDefaults() + return config } @@ -240,6 +298,12 @@ struct GhosttyConfig { if let color = NSColor(hex: value) { splitDividerColor = color } + case "sidebar-background": + rawSidebarBackground = value + case "sidebar-tint-opacity": + if let opacity = Double(value) { + sidebarTintOpacity = min(max(opacity, 0), 1) + } default: break } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 90899b569..5db472ee9 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1526,6 +1526,8 @@ private enum DebugWindowConfigSnapshot { sidebarState=\(stringValue(defaults, key: "sidebarState", fallback: SidebarStateOption.followWindow.rawValue)) sidebarBlurOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarBlurOpacity", fallback: 1.0))) sidebarTintHex=\(stringValue(defaults, key: "sidebarTintHex", fallback: "#000000")) + sidebarTintHexLight=\(stringValue(defaults, key: "sidebarTintHexLight", fallback: "(nil)")) + sidebarTintHexDark=\(stringValue(defaults, key: "sidebarTintHexDark", fallback: "(nil)")) sidebarTintOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarTintOpacity", fallback: 0.18))) sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0))) sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout)) @@ -2153,8 +2155,10 @@ private struct AboutPanelView: View { private struct SidebarDebugView: View { @AppStorage("sidebarPreset") private var sidebarPreset = SidebarPresetOption.nativeSidebar.rawValue - @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = 0.18 - @AppStorage("sidebarTintHex") private var sidebarTintHex = "#000000" + @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity + @AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex + @AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String? + @AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String? @AppStorage("sidebarMaterial") private var sidebarMaterial = SidebarMaterialOption.sidebar.rawValue @AppStorage("sidebarBlendMode") private var sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue @AppStorage("sidebarState") private var sidebarState = SidebarStateOption.followWindow.rawValue @@ -2308,7 +2312,9 @@ private struct SidebarDebugView: View { HStack(spacing: 12) { Button("Reset Tint") { sidebarTintOpacity = 0.62 - sidebarTintHex = "#000000" + sidebarTintHex = SidebarTintDefaults.hex + sidebarTintHexLight = nil + sidebarTintHexDark = nil } Button("Reset Blur") { sidebarMaterial = SidebarMaterialOption.hudWindow.rawValue @@ -2389,6 +2395,8 @@ private struct SidebarDebugView: View { sidebarState=\(sidebarState) sidebarBlurOpacity=\(String(format: "%.2f", sidebarBlurOpacity)) sidebarTintHex=\(sidebarTintHex) + sidebarTintHexLight=\(sidebarTintHexLight ?? "(nil)") + sidebarTintHexDark=\(sidebarTintHexDark ?? "(nil)") sidebarTintOpacity=\(String(format: "%.2f", sidebarTintOpacity)) sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius)) sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout) @@ -2416,6 +2424,8 @@ private struct SidebarDebugView: View { sidebarTintOpacity = preset.tintOpacity sidebarCornerRadius = preset.cornerRadius sidebarBlurOpacity = preset.blurOpacity + sidebarTintHexLight = nil + sidebarTintHexDark = nil } } @@ -3108,6 +3118,10 @@ struct SettingsView: View { @AppStorage("sidebarShowLog") private var sidebarShowLog = true @AppStorage("sidebarShowProgress") private var sidebarShowProgress = true @AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true + @AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex + @AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String? + @AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String? + @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity @ObservedObject private var notificationStore = TerminalNotificationStore.shared @State private var shortcutResetToken = UUID() @State private var topBlurOpacity: Double = 0 @@ -3182,6 +3196,30 @@ struct SettingsView: View { ) } + private var settingsSidebarTintLightBinding: Binding { + Binding( + get: { + Color(nsColor: NSColor(hex: sidebarTintHexLight ?? sidebarTintHex) ?? .black) + }, + set: { newColor in + let nsColor = NSColor(newColor) + sidebarTintHexLight = nsColor.hexString() + } + ) + } + + private var settingsSidebarTintDarkBinding: Binding { + Binding( + get: { + Color(nsColor: NSColor(hex: sidebarTintHexDark ?? sidebarTintHex) ?? .black) + }, + set: { newColor in + let nsColor = NSColor(newColor) + sidebarTintHexDark = nsColor.hexString() + } + ) + } + private var hasSocketPasswordConfigured: Bool { SocketControlPasswordStore.hasConfiguredPassword() } @@ -3939,6 +3977,83 @@ struct SettingsView: View { } } + SettingsSectionHeader(title: String(localized: "settings.section.sidebarAppearance", defaultValue: "Sidebar Appearance")) + SettingsCard { + SettingsCardRow( + String(localized: "settings.sidebarAppearance.tintColorLight", defaultValue: "Light Mode Tint"), + subtitle: String(localized: "settings.sidebarAppearance.tintColorLight.subtitle", defaultValue: "Sidebar tint color when using light appearance.") + ) { + HStack(spacing: 8) { + ColorPicker( + String(localized: "settings.sidebarAppearance.tintColorLight.picker", defaultValue: "Light tint"), + selection: settingsSidebarTintLightBinding, + supportsOpacity: false + ) + .labelsHidden() + .frame(width: 38) + + Text(sidebarTintHexLight ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default")) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(.secondary) + .frame(width: 76, alignment: .trailing) + } + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.sidebarAppearance.tintColorDark", defaultValue: "Dark Mode Tint"), + subtitle: String(localized: "settings.sidebarAppearance.tintColorDark.subtitle", defaultValue: "Sidebar tint color when using dark appearance.") + ) { + HStack(spacing: 8) { + ColorPicker( + String(localized: "settings.sidebarAppearance.tintColorDark.picker", defaultValue: "Dark tint"), + selection: settingsSidebarTintDarkBinding, + supportsOpacity: false + ) + .labelsHidden() + .frame(width: 38) + + Text(sidebarTintHexDark ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default")) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(.secondary) + .frame(width: 76, alignment: .trailing) + } + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.sidebarAppearance.tintOpacity", defaultValue: "Tint Opacity"), + subtitle: String(localized: "settings.sidebarAppearance.tintOpacity.subtitle", defaultValue: "How strongly the tint color shows over the sidebar material.") + ) { + HStack(spacing: 8) { + Slider(value: $sidebarTintOpacity, in: 0...1) + .frame(width: 140) + Text(String(format: "%.0f%%", sidebarTintOpacity * 100)) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(.secondary) + .frame(width: 36, alignment: .trailing) + } + } + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.sidebarAppearance.reset", defaultValue: "Reset Sidebar Tint"), + subtitle: String(localized: "settings.sidebarAppearance.reset.subtitle", defaultValue: "Restore default sidebar appearance.") + ) { + Button(String(localized: "settings.sidebarAppearance.reset.button", defaultValue: "Reset")) { + sidebarTintHexLight = nil + sidebarTintHexDark = nil + sidebarTintHex = SidebarTintDefaults.hex + sidebarTintOpacity = SidebarTintDefaults.opacity + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + SettingsSectionHeader(title: String(localized: "settings.section.automation", defaultValue: "Automation")) SettingsCard { SettingsPickerRow( @@ -4503,6 +4618,10 @@ struct SettingsView: View { sidebarShowLog = true sidebarShowProgress = true sidebarShowMetadata = true + sidebarTintHex = SidebarTintDefaults.hex + sidebarTintHexLight = nil + sidebarTintHexDark = nil + sidebarTintOpacity = SidebarTintDefaults.opacity showOpenAccessConfirmation = false pendingOpenAccessMode = nil socketPasswordDraft = "" diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index 40ff74045..7e74a25b2 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -1614,6 +1614,157 @@ final class GhosttyMouseFocusTests: XCTestCase { } } +final class SidebarBackgroundConfigTests: XCTestCase { + + func testParseSidebarBackgroundSingleHex() { + var config = GhosttyConfig() + config.parse("sidebar-background = #336699") + XCTAssertEqual(config.rawSidebarBackground, "#336699") + } + + func testParseSidebarBackgroundDualMode() { + var config = GhosttyConfig() + config.parse("sidebar-background = light:#fbf3db,dark:#103c48") + XCTAssertEqual(config.rawSidebarBackground, "light:#fbf3db,dark:#103c48") + } + + func testParseSidebarTintOpacity() { + var config = GhosttyConfig() + config.parse("sidebar-tint-opacity = 0.4") + XCTAssertEqual(config.sidebarTintOpacity ?? -1, 0.4, accuracy: 0.0001) + } + + func testParseSidebarTintOpacityClampedAboveOne() { + var config = GhosttyConfig() + config.parse("sidebar-tint-opacity = 1.5") + XCTAssertEqual(config.sidebarTintOpacity ?? -1, 1.0, accuracy: 0.0001) + } + + func testParseSidebarTintOpacityClampedBelowZero() { + var config = GhosttyConfig() + config.parse("sidebar-tint-opacity = -0.3") + XCTAssertEqual(config.sidebarTintOpacity ?? -1, 0.0, accuracy: 0.0001) + } + + func testResolveSidebarBackgroundSingleHex() { + var config = GhosttyConfig() + config.rawSidebarBackground = "#336699" + config.resolveSidebarBackground(preferredColorScheme: .light) + + XCTAssertNotNil(config.sidebarBackground) + XCTAssertNil(config.sidebarBackgroundLight) + XCTAssertNil(config.sidebarBackgroundDark) + } + + func testResolveSidebarBackgroundDualModeSetsLightAndDark() { + var config = GhosttyConfig() + config.rawSidebarBackground = "light:#fbf3db,dark:#103c48" + config.resolveSidebarBackground(preferredColorScheme: .light) + + XCTAssertNotNil(config.sidebarBackgroundLight) + XCTAssertNotNil(config.sidebarBackgroundDark) + XCTAssertNotNil(config.sidebarBackground) + } + + func testResolveSidebarBackgroundNilWhenNoRaw() { + var config = GhosttyConfig() + config.resolveSidebarBackground(preferredColorScheme: .dark) + + XCTAssertNil(config.sidebarBackground) + XCTAssertNil(config.sidebarBackgroundLight) + XCTAssertNil(config.sidebarBackgroundDark) + } + + func testApplyToUserDefaultsSkipsWritesWhenNoConfig() { + let defaults = UserDefaults.standard + let testKey = "sidebarTintHex" + let original = defaults.string(forKey: testKey) + defer { restoreDefaultsValue(original, key: testKey, defaults: defaults) } + + defaults.set("#AAAAAA", forKey: testKey) + + var config = GhosttyConfig() + config.applySidebarAppearanceToUserDefaults() + + XCTAssertEqual(defaults.string(forKey: testKey), "#AAAAAA", + "Should not overwrite UserDefaults when rawSidebarBackground is nil") + } + + func testApplyToUserDefaultsWritesHexWhenConfigSet() { + let defaults = UserDefaults.standard + let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark"] + let originals = keys.map { defaults.object(forKey: $0) } + defer { + for (key, original) in zip(keys, originals) { + restoreDefaultsValue(original, key: key, defaults: defaults) + } + } + + var config = GhosttyConfig() + config.rawSidebarBackground = "#336699" + config.resolveSidebarBackground(preferredColorScheme: .light) + config.applySidebarAppearanceToUserDefaults() + + XCTAssertEqual(defaults.string(forKey: "sidebarTintHex"), "#336699") + XCTAssertNil(defaults.string(forKey: "sidebarTintHexLight")) + XCTAssertNil(defaults.string(forKey: "sidebarTintHexDark")) + } + + func testApplyToUserDefaultsClearsStaleKeysOnSwitchFromDualToSingle() { + let defaults = UserDefaults.standard + let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark"] + let originals = keys.map { defaults.object(forKey: $0) } + defer { + for (key, original) in zip(keys, originals) { + restoreDefaultsValue(original, key: key, defaults: defaults) + } + } + + defaults.set("#AAAAAA", forKey: "sidebarTintHexLight") + defaults.set("#BBBBBB", forKey: "sidebarTintHexDark") + + var config = GhosttyConfig() + config.rawSidebarBackground = "#222222" + config.resolveSidebarBackground(preferredColorScheme: .light) + config.applySidebarAppearanceToUserDefaults() + + XCTAssertEqual(defaults.string(forKey: "sidebarTintHex"), "#222222") + XCTAssertNil(defaults.string(forKey: "sidebarTintHexLight"), + "Stale light key should be cleared") + XCTAssertNil(defaults.string(forKey: "sidebarTintHexDark"), + "Stale dark key should be cleared") + } + + func testApplyToUserDefaultsOnlyWritesOpacityWhenExplicit() { + let defaults = UserDefaults.standard + let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark", "sidebarTintOpacity"] + let originals = keys.map { defaults.object(forKey: $0) } + defer { + for (key, original) in zip(keys, originals) { + restoreDefaultsValue(original, key: key, defaults: defaults) + } + } + + defaults.set(0.18, forKey: "sidebarTintOpacity") + + var config = GhosttyConfig() + config.rawSidebarBackground = "#336699" + config.resolveSidebarBackground(preferredColorScheme: .light) + config.applySidebarAppearanceToUserDefaults() + + XCTAssertEqual(defaults.double(forKey: "sidebarTintOpacity"), 0.18, accuracy: 0.0001, + "Should not overwrite opacity when config doesn't set sidebar-tint-opacity") + } + + private func restoreDefaultsValue(_ value: Any?, key: String, defaults: UserDefaults) { + if let value = value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } +} + final class ZshShellIntegrationHandoffTests: XCTestCase { func testGhosttyPromptHooksLoadWhenCmuxRequestsZshIntegration() throws { let output = try runInteractiveZsh(cmuxLoadGhosttyIntegration: true)