diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 01d6d6058f..76ea66de06 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -68,6 +68,9 @@ A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; }; A5001610 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001611 /* SessionPersistence.swift */; }; A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; }; + A5001650 /* CmuxConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001651 /* CmuxConfig.swift */; }; + A5001652 /* CmuxConfigExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001653 /* CmuxConfigExecutor.swift */; }; + A5001654 /* CmuxDirectoryTrust.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001655 /* CmuxDirectoryTrust.swift */; }; A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; }; A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; }; B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmux.swift */; }; @@ -123,6 +126,7 @@ CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; }; 8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */; }; 2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; }; + C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -237,6 +241,9 @@ A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = ""; }; A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = ""; }; A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = ""; }; + A5001651 /* CmuxConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfig.swift; sourceTree = ""; }; + A5001653 /* CmuxConfigExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigExecutor.swift; sourceTree = ""; }; + A5001655 /* CmuxDirectoryTrust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxDirectoryTrust.swift; sourceTree = ""; }; A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = ""; }; 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = ""; }; B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarHelpMenuUITests.swift; sourceTree = ""; }; @@ -292,6 +299,7 @@ EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = ""; }; 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerSocketSecurityTests.swift; sourceTree = ""; }; 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = ""; }; + C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -458,6 +466,9 @@ A5001222 /* WindowAccessor.swift */, A5001611 /* SessionPersistence.swift */, A5001641 /* RemoteRelayZshBootstrap.swift */, + A5001651 /* CmuxConfig.swift */, + A5001653 /* CmuxConfigExecutor.swift */, + A5001655 /* CmuxDirectoryTrust.swift */, ); path = Sources; sourceTree = ""; @@ -547,6 +558,7 @@ EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */, 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */, 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */, + C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */, ); path = cmuxTests; sourceTree = ""; @@ -753,6 +765,9 @@ A500120C /* WindowAccessor.swift in Sources */, A5001610 /* SessionPersistence.swift in Sources */, A5001640 /* RemoteRelayZshBootstrap.swift in Sources */, + A5001650 /* CmuxConfig.swift in Sources */, + A5001652 /* CmuxConfigExecutor.swift in Sources */, + A5001654 /* CmuxDirectoryTrust.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -810,6 +825,7 @@ CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */, 8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */, 2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */, + C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index be46bab008..14de005d46 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -14236,6 +14236,1136 @@ } } }, + "dialog.cmuxConfig.confirmCommand.cancel": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "إلغاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Otkaži" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Annuller" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abbrechen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annulla" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "キャンセル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "취소" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Avbryt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Anuluj" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Отмена" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ยกเลิก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İptal" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "取消" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "取消" + } + } + } + }, + "dialog.cmuxConfig.confirmCommand.message": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "هل أنت متأكد أنك تريد تشغيل هذا الأمر؟" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Jeste li sigurni da želite pokrenuti ovu naredbu?" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Er du sikker på, at du vil køre denne kommando?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Möchten Sie diesen Befehl wirklich ausführen?" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Are you sure you want to run this command?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Estás seguro de que deseas ejecutar este comando?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Êtes-vous sûr de vouloir exécuter cette commande ?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sei sicuro di voler eseguire questo comando?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このコマンドを実行してもよろしいですか?" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 명령을 실행하시겠습니까?" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Er du sikker på at du vil kjøre denne kommandoen?" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Czy na pewno chcesz uruchomić to polecenie?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tem certeza de que deseja executar este comando?" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Вы уверены, что хотите выполнить эту команду?" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คุณแน่ใจหรือไม่ว่าต้องการรันคำสั่งนี้?" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu komutu çalıştırmak istediğinizden emin misiniz?" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "确定要运行此命令吗?" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "確定要執行此命令嗎?" + } + } + } + }, + "dialog.cmuxConfig.confirmCommand.run": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تشغيل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pokreni" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kør" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausführen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Run" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ejecutar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esegui" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "実行" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "실행" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kjør" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Executar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Выполнить" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Çalıştır" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "运行" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "執行" + } + } + } + }, + "dialog.cmuxConfig.confirmCommand.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تشغيل الأمر" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pokreni naredbu" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kør kommando" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Befehl ausführen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Run Command" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ejecutar comando" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécuter la commande" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esegui comando" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コマンドを実行" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "명령 실행" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kjør kommando" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Uruchom polecenie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Executar comando" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Выполнить команду" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รันคำสั่ง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Komutu Çalıştır" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "运行命令" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "執行命令" + } + } + } + }, + "dialog.cmuxConfig.confirmRestart.cancel": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "إلغاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Otkaži" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Annuller" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abbrechen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annulla" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "キャンセル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "취소" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Avbryt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Anuluj" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Отмена" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ยกเลิก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İptal" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "取消" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "取消" + } + } + } + }, + "dialog.cmuxConfig.confirmRestart.message": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "مساحة العمل بهذا الاسم موجودة بالفعل. هل تريد إغلاقها وإنشاء واحدة جديدة؟" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Radni prostor s ovim imenom već postoji. Zatvoriti ga i kreirati novi?" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Et arbejdsområde med dette navn eksisterer allerede. Luk det og opret et nyt?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Arbeitsbereich mit diesem Namen existiert bereits. Schließen und neu erstellen?" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "A workspace with this name already exists. Close it and create a new one?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya existe un espacio de trabajo con este nombre. ¿Cerrarlo y crear uno nuevo?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un espace de travail portant ce nom existe déjà. Le fermer et en créer un nouveau ?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esiste già un'area di lavoro con questo nome. Chiuderla e crearne una nuova?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この名前のワークスペースは既に存在します。閉じて新しく作成しますか?" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "같은 이름의 워크스페이스가 이미 있습니다. 닫고 새로 만드시겠습니까?" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Et arbeidsområde med dette navnet finnes allerede. Lukke det og opprette et nytt?" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przestrzeń robocza o tej nazwie już istnieje. Zamknąć ją i utworzyć nową?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um espaço de trabalho com este nome já existe. Fechar e criar um novo?" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Рабочая область с таким именем уже существует. Закрыть её и создать новую?" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "พื้นที่ทำงานที่มีชื่อนี้มีอยู่แล้ว ปิดและสร้างใหม่หรือไม่?" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu adda bir çalışma alanı zaten var. Kapatıp yenisini oluşturulsun mu?" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "同名工作区已存在。关闭并新建吗?" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "同名工作區已存在。關閉並新建嗎?" + } + } + } + }, + "dialog.cmuxConfig.confirmRestart.recreate": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة إنشاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ponovo kreiraj" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genskab" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu erstellen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Recreate" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recrear" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recréer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricrea" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再作成" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 만들기" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Opprett på nytt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Utwórz ponownie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recriar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Пересоздать" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สร้างใหม่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeniden Oluştur" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新创建" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新建立" + } + } + } + }, + "dialog.cmuxConfig.confirmRestart.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "مساحة العمل موجودة بالفعل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Radni prostor već postoji" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Arbejdsområdet eksisterer allerede" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeitsbereich existiert bereits" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Workspace Already Exists" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El espacio de trabajo ya existe" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'espace de travail existe déjà" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L'area di lavoro esiste già" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークスペースは既に存在します" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "워크스페이스가 이미 존재합니다" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Arbeidsområdet finnes allerede" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przestrzeń robocza już istnieje" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O espaço de trabalho já existe" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Рабочая область уже существует" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "พื้นที่ทำงานมีอยู่แล้ว" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Çalışma Alanı Zaten Var" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "工作区已存在" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "工作區已存在" + } + } + } + }, + "command.cmuxConfig.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux.json" + } + } + } + }, + "command.cmuxConfig.customTitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "مخصص: %@" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prilagođeno: %@" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Brugerdefineret: %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Benutzerdefiniert: %@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Custom: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Personalizado: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnalisé : %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Personalizzato: %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "カスタム: %@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사용자 정의: %@" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Egendefinert: %@" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Niestandardowe: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Personalizado: %@" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Пользовательское: %@" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "กำหนดเอง: %@" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Özel: %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "自定义: %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "自訂: %@" + } + } + } + }, "command.equalizeSplits.title": { "extractionState": "manual", "localizations": { diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 271cbbf76d..8e599cc0db 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5789,11 +5789,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) let notificationStore = TerminalNotificationStore.shared + let cmuxConfigStore = CmuxConfigStore() + cmuxConfigStore.wireDirectoryTracking(tabManager: tabManager) + cmuxConfigStore.loadAll() + let root = ContentView(updateViewModel: updateViewModel, windowId: windowId) .environmentObject(tabManager) .environmentObject(notificationStore) .environmentObject(sidebarState) .environmentObject(sidebarSelectionState) + .environmentObject(cmuxConfigStore) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 460, height: 360), diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift new file mode 100644 index 0000000000..d57f84794d --- /dev/null +++ b/Sources/CmuxConfig.swift @@ -0,0 +1,590 @@ +import Bonsplit +import Combine +import Foundation + +struct CmuxConfigFile: Codable, Sendable { + var commands: [CmuxCommandDefinition] +} + +struct CmuxCommandDefinition: Codable, Sendable, Identifiable { + var name: String + var description: String? + var keywords: [String]? + var restart: CmuxRestartBehavior? + var workspace: CmuxWorkspaceDefinition? + var command: String? + var confirm: Bool? + + var id: String { + "cmux.config.command." + (name.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? name) + } + + init( + name: String, + description: String? = nil, + keywords: [String]? = nil, + restart: CmuxRestartBehavior? = nil, + workspace: CmuxWorkspaceDefinition? = nil, + command: String? = nil, + confirm: Bool? = nil + ) { + self.name = name + self.description = description + self.keywords = keywords + self.restart = restart + self.workspace = workspace + self.command = command + self.confirm = confirm + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + description = try container.decodeIfPresent(String.self, forKey: .description) + keywords = try container.decodeIfPresent([String].self, forKey: .keywords) + restart = try container.decodeIfPresent(CmuxRestartBehavior.self, forKey: .restart) + workspace = try container.decodeIfPresent(CmuxWorkspaceDefinition.self, forKey: .workspace) + command = try container.decodeIfPresent(String.self, forKey: .command) + confirm = try container.decodeIfPresent(Bool.self, forKey: .confirm) + + if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Command name must not be blank" + ) + ) + } + if let cmd = command, + cmd.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Command '\(name)' must not define a blank 'command'" + ) + ) + } + + if workspace != nil && command != nil { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Command '\(name)' must not define both 'workspace' and 'command'" + ) + ) + } + if workspace == nil && command == nil { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Command '\(name)' must define either 'workspace' or 'command'" + ) + ) + } + } +} + +enum CmuxRestartBehavior: String, Codable, Sendable { + case recreate + case ignore + case confirm +} + +struct CmuxWorkspaceDefinition: Codable, Sendable { + var name: String? + var cwd: String? + var color: String? + var layout: CmuxLayoutNode? +} + +indirect enum CmuxLayoutNode: Codable, Sendable { + case pane(CmuxPaneDefinition) + case split(CmuxSplitDefinition) + + private enum CodingKeys: String, CodingKey { + case pane + case direction + case split + case children + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let hasPane = container.contains(.pane) + let hasDirection = container.contains(.direction) + + if hasPane && hasDirection { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "CmuxLayoutNode must not contain both 'pane' and 'direction' keys" + ) + ) + } + + if hasPane { + let pane = try container.decode(CmuxPaneDefinition.self, forKey: .pane) + self = .pane(pane) + } else if hasDirection { + let splitDef = try CmuxSplitDefinition(from: decoder) + self = .split(splitDef) + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "CmuxLayoutNode must contain either a 'pane' key or a 'direction' key" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + switch self { + case .pane(let pane): + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(pane, forKey: .pane) + case .split(let split): + try split.encode(to: encoder) + } + } +} + +struct CmuxSplitDefinition: Codable, Sendable { + var direction: CmuxSplitDirection + var split: Double? + var children: [CmuxLayoutNode] + + init(direction: CmuxSplitDirection, split: Double? = nil, children: [CmuxLayoutNode]) { + self.direction = direction + self.split = split + self.children = children + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + direction = try container.decode(CmuxSplitDirection.self, forKey: .direction) + split = try container.decodeIfPresent(Double.self, forKey: .split) + children = try container.decode([CmuxLayoutNode].self, forKey: .children) + if children.count != 2 { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Split node requires exactly 2 children, got \(children.count)" + ) + ) + } + } + + var clampedSplitPosition: Double { + let value = split ?? 0.5 + return min(0.9, max(0.1, value)) + } + + var splitOrientation: SplitOrientation { + switch direction { + case .horizontal: return .horizontal + case .vertical: return .vertical + } + } +} + +enum CmuxSplitDirection: String, Codable, Sendable { + case horizontal + case vertical +} + +struct CmuxPaneDefinition: Codable, Sendable { + var surfaces: [CmuxSurfaceDefinition] + + init(surfaces: [CmuxSurfaceDefinition]) { + self.surfaces = surfaces + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + surfaces = try container.decode([CmuxSurfaceDefinition].self, forKey: .surfaces) + if surfaces.isEmpty { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Pane node must contain at least one surface" + ) + ) + } + } +} + +struct CmuxSurfaceDefinition: Codable, Sendable { + var type: CmuxSurfaceType + var name: String? + var command: String? + var cwd: String? + var env: [String: String]? + var url: String? + var focus: Bool? +} + +enum CmuxSurfaceType: String, Codable, Sendable { + case terminal + case browser +} + +@MainActor +final class CmuxConfigStore: ObservableObject { + @Published private(set) var loadedCommands: [CmuxCommandDefinition] = [] + @Published private(set) var configRevision: UInt64 = 0 + + /// Which config file each command came from, keyed by command id. + private(set) var commandSourcePaths: [String: String] = [:] + + private(set) var localConfigPath: String? + let globalConfigPath: String = { + let home = FileManager.default.homeDirectoryForCurrentUser.path + return (home as NSString).appendingPathComponent(".config/cmux/cmux.json") + }() + + private var cancellables = Set() + private var localFileWatchSource: DispatchSourceFileSystemObject? + private var localFileDescriptor: Int32 = -1 + private var globalFileWatchSource: DispatchSourceFileSystemObject? + private var globalFileDescriptor: Int32 = -1 + private let watchQueue = DispatchQueue(label: "com.cmux.config-file-watch") + + private static let maxReattachAttempts = 5 + private static let reattachDelay: TimeInterval = 0.5 + + init() { + startGlobalFileWatcher() + } + + deinit { + localFileWatchSource?.cancel() + globalFileWatchSource?.cancel() + } + + // MARK: - Public API + + func wireDirectoryTracking(tabManager: TabManager) { + cancellables.removeAll() + + tabManager.$selectedTabId + .compactMap { [weak tabManager] tabId -> Workspace? in + guard let tabId, let tabManager else { return nil } + return tabManager.tabs.first(where: { $0.id == tabId }) + } + .removeDuplicates(by: { $0.id == $1.id }) + .map { workspace -> AnyPublisher in + workspace.$currentDirectory.eraseToAnyPublisher() + } + .switchToLatest() + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] directory in + self?.updateLocalConfigPath(directory) + } + .store(in: &cancellables) + + if let directory = tabManager.selectedWorkspace?.currentDirectory { + updateLocalConfigPath(directory) + } + } + + private func updateLocalConfigPath(_ directory: String?) { + let newPath: String? + if let directory, !directory.isEmpty { + newPath = findCmuxConfig(startingFrom: directory) + ?? (directory as NSString).appendingPathComponent("cmux.json") + } else { + newPath = nil + } + + guard newPath != localConfigPath else { return } + stopLocalFileWatcher() + localConfigPath = newPath + if newPath != nil { + startLocalFileWatcher() + } + loadAll() + } + + private func findCmuxConfig(startingFrom directory: String) -> String? { + var current = directory + let fs = FileManager.default + while true { + let candidate = (current as NSString).appendingPathComponent("cmux.json") + if fs.fileExists(atPath: candidate) { + return candidate + } + let parent = (current as NSString).deletingLastPathComponent + if parent == current { break } + current = parent + } + return nil + } + + func loadAll() { + var commands: [CmuxCommandDefinition] = [] + var seenNames = Set() + var sourcePaths: [String: String] = [:] + + // Local config takes precedence + if let localPath = localConfigPath { + if let localConfig = parseConfig(at: localPath) { + for command in localConfig.commands { + if !seenNames.contains(command.name) { + commands.append(command) + seenNames.insert(command.name) + sourcePaths[command.id] = localPath + } + } + } + } + + // Global config fills in the rest + if let globalConfig = parseConfig(at: globalConfigPath) { + for command in globalConfig.commands { + if !seenNames.contains(command.name) { + commands.append(command) + seenNames.insert(command.name) + sourcePaths[command.id] = globalConfigPath + } + } + } + + loadedCommands = commands + commandSourcePaths = sourcePaths + configRevision &+= 1 + } + + // MARK: - Parsing + + private func parseConfig(at path: String) -> CmuxConfigFile? { + guard FileManager.default.fileExists(atPath: path), + let data = FileManager.default.contents(atPath: path), + !data.isEmpty else { + return nil + } + do { + return try JSONDecoder().decode(CmuxConfigFile.self, from: data) + } catch { + NSLog("[CmuxConfig] parse error at %@: %@", path, String(describing: error)) + return nil + } + } + + // MARK: - File watching (local) + + private func startLocalFileWatcher() { + guard let path = localConfigPath else { return } + let fd = open(path, O_EVTONLY) + guard fd >= 0 else { + // File doesn't exist yet — watch the directory instead + startLocalDirectoryWatcher() + return + } + localFileDescriptor = fd + + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fd, + eventMask: [.write, .delete, .rename, .extend], + queue: watchQueue + ) + + source.setEventHandler { [weak self] in + guard let self else { return } + let flags = source.data + if flags.contains(.delete) || flags.contains(.rename) { + DispatchQueue.main.async { + self.stopLocalFileWatcher() + self.loadAll() + self.scheduleLocalReattach(attempt: 1) + } + } else { + DispatchQueue.main.async { + self.loadAll() + } + } + } + + source.setCancelHandler { + Darwin.close(fd) + } + + source.resume() + localFileWatchSource = source + } + + private func startLocalDirectoryWatcher() { + guard let path = localConfigPath else { return } + let dirPath = (path as NSString).deletingLastPathComponent + let fd = open(dirPath, O_EVTONLY) + guard fd >= 0 else { return } + localFileDescriptor = fd + + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fd, + eventMask: [.write, .link, .rename], + queue: watchQueue + ) + + source.setEventHandler { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + guard let configPath = self.localConfigPath, + FileManager.default.fileExists(atPath: configPath) else { return } + // File appeared — switch to file-level watching + self.stopLocalFileWatcher() + self.loadAll() + self.startLocalFileWatcher() + } + } + + source.setCancelHandler { + Darwin.close(fd) + } + + source.resume() + localFileWatchSource = source + } + + private func scheduleLocalReattach(attempt: Int) { + guard attempt <= Self.maxReattachAttempts else { return } + watchQueue.asyncAfter(deadline: .now() + Self.reattachDelay) { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + guard let path = self.localConfigPath else { return } + if FileManager.default.fileExists(atPath: path) { + self.loadAll() + self.startLocalFileWatcher() + } else { + self.startLocalDirectoryWatcher() + } + } + } + } + + private func stopLocalFileWatcher() { + if let source = localFileWatchSource { + source.cancel() + localFileWatchSource = nil + } + localFileDescriptor = -1 + } + + // MARK: - File watching (global) + + private func startGlobalFileWatcher() { + let fd = open(globalConfigPath, O_EVTONLY) + guard fd >= 0 else { + startGlobalDirectoryWatcher() + return + } + globalFileDescriptor = fd + + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fd, + eventMask: [.write, .delete, .rename, .extend], + queue: watchQueue + ) + + source.setEventHandler { [weak self] in + guard let self else { return } + let flags = source.data + if flags.contains(.delete) || flags.contains(.rename) { + DispatchQueue.main.async { + self.stopGlobalFileWatcher() + self.loadAll() + self.scheduleGlobalReattach(attempt: 1) + } + } else { + DispatchQueue.main.async { + self.loadAll() + } + } + } + + source.setCancelHandler { + Darwin.close(fd) + } + + source.resume() + globalFileWatchSource = source + } + + private func scheduleGlobalReattach(attempt: Int) { + guard attempt <= Self.maxReattachAttempts else { + startGlobalDirectoryWatcher() + return + } + watchQueue.asyncAfter(deadline: .now() + Self.reattachDelay) { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + if FileManager.default.fileExists(atPath: self.globalConfigPath) { + self.loadAll() + self.startGlobalFileWatcher() + } else { + self.scheduleGlobalReattach(attempt: attempt + 1) + } + } + } + } + + private func startGlobalDirectoryWatcher() { + let dirPath = (globalConfigPath as NSString).deletingLastPathComponent + let fm = FileManager.default + if !fm.fileExists(atPath: dirPath) { + try? fm.createDirectory(atPath: dirPath, withIntermediateDirectories: true) + } + let fd = open(dirPath, O_EVTONLY) + guard fd >= 0 else { return } + globalFileDescriptor = fd + + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fd, + eventMask: [.write, .link, .rename], + queue: watchQueue + ) + + source.setEventHandler { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + guard FileManager.default.fileExists(atPath: self.globalConfigPath) else { return } + self.stopGlobalFileWatcher() + self.loadAll() + self.startGlobalFileWatcher() + } + } + + source.setCancelHandler { + Darwin.close(fd) + } + + source.resume() + globalFileWatchSource = source + } + + private func stopGlobalFileWatcher() { + if let source = globalFileWatchSource { + source.cancel() + globalFileWatchSource = nil + } + globalFileDescriptor = -1 + } +} + +extension CmuxConfigStore { + static func resolveCwd(_ cwd: String?, relativeTo baseCwd: String) -> String { + guard let cwd, !cwd.isEmpty, cwd != "." else { + return baseCwd + } + if cwd.hasPrefix("~/") || cwd == "~" { + let home = FileManager.default.homeDirectoryForCurrentUser.path + if cwd == "~" { return home } + return (home as NSString).appendingPathComponent(String(cwd.dropFirst(2))) + } + if cwd.hasPrefix("/") { + return cwd + } + return (baseCwd as NSString).appendingPathComponent(cwd) + } +} diff --git a/Sources/CmuxConfigExecutor.swift b/Sources/CmuxConfigExecutor.swift new file mode 100644 index 0000000000..070e57d795 --- /dev/null +++ b/Sources/CmuxConfigExecutor.swift @@ -0,0 +1,130 @@ +import AppKit +import Foundation + +@MainActor +struct CmuxConfigExecutor { + + static func execute( + command: CmuxCommandDefinition, + tabManager: TabManager, + baseCwd: String, + configSourcePath: String?, + globalConfigPath: String + ) { + if let workspace = command.workspace { + executeWorkspaceCommand(command: command, workspace: workspace, tabManager: tabManager, baseCwd: baseCwd) + } else if let shellCommand = command.command { + let needsConfirm = command.confirm ?? false + if needsConfirm, let sourcePath = configSourcePath { + let trusted = CmuxDirectoryTrust.shared.isTrusted( + configPath: sourcePath, + globalConfigPath: globalConfigPath + ) + if !trusted { + guard showConfirmDialog(command: shellCommand, configPath: sourcePath) else { return } + } + } + guard let terminal = tabManager.selectedWorkspace?.focusedTerminalPanel else { return } + terminal.sendInput(shellCommand + "\n") + } + } + + /// Show a confirmation dialog with the command text and a "trust this directory" checkbox. + /// Returns true if the user chose to run, false if cancelled. + private static func showConfirmDialog(command: String, configPath: String) -> Bool { + let alert = NSAlert() + alert.messageText = String( + localized: "dialog.cmuxConfig.confirmCommand.title", + defaultValue: "Run Command" + ) + let messageFormat = String( + localized: "dialog.cmuxConfig.confirmCommand.messageWithCommand", + defaultValue: "This will run the following command:\n\n%@" + ) + alert.informativeText = String(format: messageFormat, sanitizeForDisplay(command)) + alert.alertStyle = .warning + alert.addButton(withTitle: String( + localized: "dialog.cmuxConfig.confirmCommand.run", + defaultValue: "Run" + )) + alert.addButton(withTitle: String( + localized: "dialog.cmuxConfig.confirmCommand.cancel", + defaultValue: "Cancel" + )) + + let checkbox = NSButton(checkboxWithTitle: String( + localized: "dialog.cmuxConfig.confirmCommand.trustDirectory", + defaultValue: "Always trust commands from this folder" + ), target: nil, action: nil) + checkbox.state = .off + alert.accessoryView = checkbox + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { return false } + + if checkbox.state == .on { + CmuxDirectoryTrust.shared.trust(configPath: configPath) + } + + return true + } + + private static func sanitizeForDisplay(_ text: String) -> String { + let dangerous: Set = [ + "\u{200B}", "\u{200C}", "\u{200D}", "\u{200E}", "\u{200F}", + "\u{202A}", "\u{202B}", "\u{202C}", "\u{202D}", "\u{202E}", + "\u{2066}", "\u{2067}", "\u{2068}", "\u{2069}", + "\u{FEFF}", + ] + let filtered = String(text.unicodeScalars.filter { !dangerous.contains($0) }) + return filtered.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private static func executeWorkspaceCommand( + command: CmuxCommandDefinition, + workspace wsDef: CmuxWorkspaceDefinition, + tabManager: TabManager, + baseCwd: String + ) { + let workspaceName = wsDef.name ?? command.name + let restart = command.restart ?? .ignore + + if let existing = tabManager.tabs.first(where: { $0.customTitle == workspaceName }) { + switch restart { + case .ignore: + tabManager.selectWorkspace(existing) + return + case .recreate: + tabManager.closeWorkspace(existing) + case .confirm: + let alert = NSAlert() + alert.messageText = String( + localized: "dialog.cmuxConfig.confirmRestart.title", + defaultValue: "Workspace Already Exists" + ) + alert.informativeText = String( + localized: "dialog.cmuxConfig.confirmRestart.message", + defaultValue: "A workspace with this name already exists. Close it and create a new one?" + ) + alert.alertStyle = .warning + alert.addButton(withTitle: String(localized: "dialog.cmuxConfig.confirmRestart.recreate", defaultValue: "Recreate")) + alert.addButton(withTitle: String(localized: "dialog.cmuxConfig.confirmRestart.cancel", defaultValue: "Cancel")) + guard alert.runModal() == .alertFirstButtonReturn else { + tabManager.selectWorkspace(existing) + return + } + tabManager.closeWorkspace(existing) + } + } + + let resolvedCwd = CmuxConfigStore.resolveCwd(wsDef.cwd, relativeTo: baseCwd) + let newWorkspace = tabManager.addWorkspace(workingDirectory: resolvedCwd) + newWorkspace.setCustomTitle(workspaceName) + if let color = wsDef.color { + newWorkspace.setCustomColor(color) + } + + guard let layout = wsDef.layout else { return } + newWorkspace.applyCustomLayout(layout, baseCwd: resolvedCwd) + } +} diff --git a/Sources/CmuxDirectoryTrust.swift b/Sources/CmuxDirectoryTrust.swift new file mode 100644 index 0000000000..1c906fbada --- /dev/null +++ b/Sources/CmuxDirectoryTrust.swift @@ -0,0 +1,112 @@ +import Foundation + +/// Manages trusted directories for cmux.json command execution. +/// When a directory (or its git repo root) is trusted, `confirm: true` commands +/// from that directory's cmux.json skip the confirmation dialog. +/// Global config (~/.config/cmux/cmux.json) is always trusted. +final class CmuxDirectoryTrust { + static let shared = CmuxDirectoryTrust() + + private let storePath: String + private var trustedPaths: Set + + private init() { + let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ).first!.appendingPathComponent("cmux") + storePath = appSupport.appendingPathComponent("trusted-directories.json").path + + let fm = FileManager.default + if !fm.fileExists(atPath: appSupport.path) { + try? fm.createDirectory(atPath: appSupport.path, withIntermediateDirectories: true) + } + + if let data = fm.contents(atPath: storePath), + let paths = try? JSONDecoder().decode([String].self, from: data) { + trustedPaths = Set(paths) + } else { + trustedPaths = [] + } + } + + /// Check if a cmux.json path is trusted. + /// Global config is always trusted. For local configs, check the git repo root + /// (or the cmux.json parent directory if not in a git repo). + func isTrusted(configPath: String, globalConfigPath: String) -> Bool { + if configPath == globalConfigPath { return true } + let trustKey = Self.trustKey(for: configPath) + return trustedPaths.contains(trustKey) + } + + /// Trust the directory containing a cmux.json. If the cmux.json is inside a git + /// repo, trusts the repo root (covering all subdirectories). + func trust(configPath: String) { + let trustKey = Self.trustKey(for: configPath) + trustedPaths.insert(trustKey) + save() + } + + /// Remove trust for a directory. + func revokeTrust(configPath: String) { + let trustKey = Self.trustKey(for: configPath) + trustedPaths.remove(trustKey) + save() + } + + /// Remove trust by the trust key directly (as stored/displayed in settings). + func revokeTrustByPath(_ path: String) { + trustedPaths.remove(path) + save() + } + + /// All currently trusted paths. + var allTrustedPaths: [String] { + Array(trustedPaths).sorted() + } + + /// Replace all trusted paths (used by Settings textarea save). + func replaceAll(with paths: [String]) { + trustedPaths = Set(paths) + save() + } + + /// Clear all trusted directories. + func clearAll() { + trustedPaths.removeAll() + save() + } + + // MARK: - Private + + /// Resolve the trust key for a cmux.json path: git repo root if inside a repo, + /// otherwise the cmux.json's parent directory. + static func trustKey(for configPath: String) -> String { + let configDir = (configPath as NSString).deletingLastPathComponent + if let gitRoot = findGitRoot(from: configDir) { + return gitRoot + } + return configDir + } + + /// Walk up from `directory` looking for a `.git` directory or file. + private static func findGitRoot(from directory: String) -> String? { + let fm = FileManager.default + var current = directory + while true { + let gitPath = (current as NSString).appendingPathComponent(".git") + if fm.fileExists(atPath: gitPath) { + return current + } + let parent = (current as NSString).deletingLastPathComponent + if parent == current { break } + current = parent + } + return nil + } + + private func save() { + let sorted = trustedPaths.sorted() + guard let data = try? JSONEncoder().encode(sorted) else { return } + FileManager.default.createFile(atPath: storePath, contents: data) + } +} diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 03f4424508..c204c04f4b 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1553,6 +1553,7 @@ struct ContentView: View { @EnvironmentObject var notificationStore: TerminalNotificationStore @EnvironmentObject var sidebarState: SidebarState @EnvironmentObject var sidebarSelectionState: SidebarSelectionState + @EnvironmentObject var cmuxConfigStore: CmuxConfigStore @State private var sidebarWidth: CGFloat = 200 @State private var hoveredResizerHandles: Set = [] @State private var isResizerDragging = false @@ -4659,7 +4660,10 @@ struct ContentView: View { } private func commandPaletteCommandsFingerprint(commandsContext: CommandPaletteCommandsContext) -> Int { - commandsContext.snapshot.fingerprint() + var hasher = Hasher() + hasher.combine(commandsContext.snapshot.fingerprint()) + hasher.combine(cmuxConfigStore.configRevision) + return hasher.finalize() } private func commandPaletteSwitcherEntriesFingerprint(includeSurfaces: Bool) -> Int { @@ -5963,9 +5967,37 @@ struct ContentView: View { ) ) + let cmuxConfigDefaultSubtitle = constant(String(localized: "command.cmuxConfig.subtitle", defaultValue: "cmux.json")) + for command in cmuxConfigStore.loadedCommands { + let commandName = sanitizeCmuxConfigPaletteText(command.name) + let subtitle = command.description + .map { sanitizeCmuxConfigPaletteText($0) } + .flatMap { $0.isEmpty ? nil : constant($0) } + ?? cmuxConfigDefaultSubtitle + contributions.append( + CommandPaletteCommandContribution( + commandId: command.id, + title: constant(String(localized: "command.cmuxConfig.customTitle", defaultValue: "Custom: \(commandName)")), + subtitle: subtitle, + keywords: command.keywords ?? [] + ) + ) + } + return contributions } + private func sanitizeCmuxConfigPaletteText(_ text: String) -> String { + let dangerous: Set = [ + "\u{200B}", "\u{200C}", "\u{200D}", "\u{200E}", "\u{200F}", + "\u{202A}", "\u{202B}", "\u{202C}", "\u{202D}", "\u{202E}", + "\u{2066}", "\u{2067}", "\u{2068}", "\u{2069}", + "\u{FEFF}", + ] + let filtered = String(text.unicodeScalars.filter { !dangerous.contains($0) }) + return filtered.trimmingCharacters(in: .whitespacesAndNewlines) + } + private func registerCommandPaletteHandlers(_ registry: inout CommandPaletteHandlerRegistry) { registry.register(commandId: "palette.newWorkspace") { tabManager.addWorkspace() @@ -6294,6 +6326,24 @@ struct ContentView: View { return } } + + for command in cmuxConfigStore.loadedCommands { + let captured = command + let sourcePath = cmuxConfigStore.commandSourcePaths[command.id] + let globalPath = cmuxConfigStore.globalConfigPath + registry.register(commandId: command.id) { + let rawCwd = tabManager.selectedWorkspace?.currentDirectory + let baseCwd = (rawCwd?.isEmpty == false) ? rawCwd! + : FileManager.default.homeDirectoryForCurrentUser.path + CmuxConfigExecutor.execute( + command: captured, + tabManager: tabManager, + baseCwd: baseCwd, + configSourcePath: sourcePath, + globalConfigPath: globalPath + ) + } + } } private var focusedPanelContext: (workspace: Workspace, panelId: UUID, panel: any Panel)? { diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 32f2b68c7a..b5ce0189e4 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3739,6 +3739,69 @@ final class TerminalSurface: Identifiable, ObservableObject { writeTextData(data, to: surface) } + /// Send text with control characters (Return, Tab, etc.) delivered as key + /// events so the shell processes them, while regular text is sent via the + /// normal key-text path. Mirrors `TerminalController.sendSocketText`. + func sendInput(_ text: String) { + guard let surface = surface else { return } + var bufferedText = "" + var previousWasCR = false + for scalar in text.unicodeScalars { + switch scalar.value { + case 0x0A: // \n — skip if preceded by \r (already sent Return) + if !previousWasCR { + flushText(&bufferedText, surface: surface) + sendKeyEvent(surface: surface, keycode: 0x24) // kVK_Return + } + previousWasCR = false + case 0x0D: + flushText(&bufferedText, surface: surface) + sendKeyEvent(surface: surface, keycode: 0x24) // kVK_Return + previousWasCR = true + case 0x09: + flushText(&bufferedText, surface: surface) + sendKeyEvent(surface: surface, keycode: 0x30) // kVK_Tab + previousWasCR = false + case 0x1B: + flushText(&bufferedText, surface: surface) + sendKeyEvent(surface: surface, keycode: 0x35) // kVK_Escape + previousWasCR = false + default: + bufferedText.unicodeScalars.append(scalar) + previousWasCR = false + } + } + flushText(&bufferedText, surface: surface) + } + + private func flushText(_ buffer: inout String, surface: ghostty_surface_t) { + guard !buffer.isEmpty else { return } + var keyEvent = ghostty_input_key_s() + keyEvent.action = GHOSTTY_ACTION_PRESS + keyEvent.keycode = 0 + keyEvent.mods = GHOSTTY_MODS_NONE + keyEvent.consumed_mods = GHOSTTY_MODS_NONE + keyEvent.unshifted_codepoint = 0 + keyEvent.composing = false + buffer.withCString { ptr in + keyEvent.text = ptr + _ = ghostty_surface_key(surface, keyEvent) + } + buffer.removeAll(keepingCapacity: true) + } + + private func sendKeyEvent(surface: ghostty_surface_t, keycode: UInt32) { + var keyEvent = ghostty_input_key_s() + keyEvent.action = GHOSTTY_ACTION_PRESS + keyEvent.keycode = keycode + keyEvent.mods = GHOSTTY_MODS_NONE + keyEvent.consumed_mods = GHOSTTY_MODS_NONE + keyEvent.unshifted_codepoint = 0 + keyEvent.composing = false + keyEvent.text = nil + _ = ghostty_surface_key(surface, keyEvent) + } + func requestBackgroundSurfaceStartIfNeeded() { if !Thread.isMainThread { DispatchQueue.main.async { [weak self] in diff --git a/Sources/Panels/TerminalPanel.swift b/Sources/Panels/TerminalPanel.swift index 7bc65d51a1..0fabb34a6d 100644 --- a/Sources/Panels/TerminalPanel.swift +++ b/Sources/Panels/TerminalPanel.swift @@ -191,6 +191,10 @@ final class TerminalPanel: Panel, ObservableObject { surface.sendText(text) } + func sendInput(_ text: String) { + surface.sendInput(text) + } + func performBindingAction(_ action: String) -> Bool { surface.performBindingAction(action) } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4762046439..c62fc4d4a0 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -696,6 +696,227 @@ extension Workspace { } } +// MARK: - cmux.json custom layout + +extension Workspace { + + func applyCustomLayout(_ layout: CmuxLayoutNode, baseCwd: String) { + guard let rootPaneId = bonsplitController.allPaneIds.first else { return } + + var leaves: [(paneId: PaneID, surfaces: [CmuxSurfaceDefinition])] = [] + buildCustomLayoutTree(layout, inPane: rootPaneId, leaves: &leaves) + + // First leaf reuses the initial terminal created by addWorkspace; + // subsequent leaves were created via newTerminalSplit which also seeds + // a placeholder terminal. + var focusPanelId: UUID? + for leaf in leaves { + populateCustomPane(leaf.paneId, surfaces: leaf.surfaces, baseCwd: baseCwd, focusPanelId: &focusPanelId) + } + + let liveRoot = bonsplitController.treeSnapshot() + applyCustomDividerPositions(configNode: layout, liveNode: liveRoot) + + if let focusPanelId { + focusPanel(focusPanelId) + } + } + + private func buildCustomLayoutTree( + _ node: CmuxLayoutNode, + inPane paneId: PaneID, + leaves: inout [(paneId: PaneID, surfaces: [CmuxSurfaceDefinition])] + ) { + switch node { + case .pane(let pane): + leaves.append((paneId: paneId, surfaces: pane.surfaces)) + + case .split(let split): + guard split.children.count == 2 else { + NSLog("[CmuxConfig] split node requires exactly 2 children, got %d", split.children.count) + leaves.append((paneId: paneId, surfaces: [])) + return + } + + var anchorPanelId = bonsplitController + .tabs(inPane: paneId) + .compactMap { panelIdFromSurfaceId($0.id) } + .first + + if anchorPanelId == nil { + anchorPanelId = newTerminalSurface(inPane: paneId, focus: false)?.id + } + + guard let anchorPanelId, + let newSplitPanel = newTerminalSplit( + from: anchorPanelId, + orientation: split.splitOrientation, + insertFirst: false, + focus: false + ), + let secondPaneId = self.paneId(forPanelId: newSplitPanel.id) else { + leaves.append((paneId: paneId, surfaces: [])) + return + } + + buildCustomLayoutTree(split.children[0], inPane: paneId, leaves: &leaves) + buildCustomLayoutTree(split.children[1], inPane: secondPaneId, leaves: &leaves) + } + } + + private func populateCustomPane( + _ paneId: PaneID, + surfaces: [CmuxSurfaceDefinition], + baseCwd: String, + focusPanelId: inout UUID? + ) { + let existingPanelIds = bonsplitController + .tabs(inPane: paneId) + .compactMap { panelIdFromSurfaceId($0.id) } + + guard !surfaces.isEmpty else { return } + + let firstSurface = surfaces[0] + if let placeholderPanelId = existingPanelIds.first { + configureExistingSurface( + panelId: placeholderPanelId, + inPane: paneId, + surface: firstSurface, + baseCwd: baseCwd, + focusPanelId: &focusPanelId + ) + } + + for surfaceIndex in 1.. CmuxConfigFile { + let data = json.data(using: .utf8)! + return try JSONDecoder().decode(CmuxConfigFile.self, from: data) + } + + // MARK: Simple commands + + func testDecodeSimpleCommand() throws { + let json = """ + { + "commands": [{ + "name": "Run tests", + "command": "npm test" + }] + } + """ + let config = try decode(json) + XCTAssertEqual(config.commands.count, 1) + XCTAssertEqual(config.commands[0].name, "Run tests") + XCTAssertEqual(config.commands[0].command, "npm test") + XCTAssertNil(config.commands[0].workspace) + } + + func testDecodeSimpleCommandWithAllFields() throws { + let json = """ + { + "commands": [{ + "name": "Deploy", + "description": "Deploy to production", + "keywords": ["ship", "release"], + "command": "make deploy", + "confirm": true + }] + } + """ + let config = try decode(json) + let cmd = config.commands[0] + XCTAssertEqual(cmd.name, "Deploy") + XCTAssertEqual(cmd.description, "Deploy to production") + XCTAssertEqual(cmd.keywords, ["ship", "release"]) + XCTAssertEqual(cmd.command, "make deploy") + XCTAssertEqual(cmd.confirm, true) + } + + func testDecodeMultipleCommands() throws { + let json = """ + { + "commands": [ + { "name": "Build", "command": "make build" }, + { "name": "Test", "command": "make test" }, + { "name": "Lint", "command": "make lint" } + ] + } + """ + let config = try decode(json) + XCTAssertEqual(config.commands.count, 3) + XCTAssertEqual(config.commands.map(\.name), ["Build", "Test", "Lint"]) + } + + func testDecodeEmptyCommandsArray() throws { + let json = """ + { "commands": [] } + """ + let config = try decode(json) + XCTAssertTrue(config.commands.isEmpty) + } + + // MARK: Workspace commands + + func testDecodeWorkspaceCommand() throws { + let json = """ + { + "commands": [{ + "name": "Dev env", + "workspace": { + "name": "Development", + "cwd": "~/projects/app", + "color": "#FF5733" + } + }] + } + """ + let config = try decode(json) + let ws = config.commands[0].workspace + XCTAssertNotNil(ws) + XCTAssertEqual(ws?.name, "Development") + XCTAssertEqual(ws?.cwd, "~/projects/app") + XCTAssertEqual(ws?.color, "#FF5733") + } + + func testDecodeRestartBehaviors() throws { + for behavior in ["recreate", "ignore", "confirm"] { + let json = """ + { + "commands": [{ + "name": "test", + "restart": "\(behavior)", + "workspace": { "name": "ws" } + }] + } + """ + let config = try decode(json) + XCTAssertEqual(config.commands[0].restart?.rawValue, behavior) + } + } + + // MARK: Layout tree + + func testDecodePaneNode() throws { + let json = """ + { + "commands": [{ + "name": "layout", + "workspace": { + "layout": { + "pane": { + "surfaces": [ + { "type": "terminal", "name": "shell" } + ] + } + } + } + }] + } + """ + let config = try decode(json) + let layout = config.commands[0].workspace!.layout! + if case .pane(let pane) = layout { + XCTAssertEqual(pane.surfaces.count, 1) + XCTAssertEqual(pane.surfaces[0].type, .terminal) + XCTAssertEqual(pane.surfaces[0].name, "shell") + } else { + XCTFail("Expected pane node") + } + } + + func testDecodeSplitNode() throws { + let json = """ + { + "commands": [{ + "name": "layout", + "workspace": { + "layout": { + "direction": "horizontal", + "split": 0.3, + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { "pane": { "surfaces": [{ "type": "terminal" }] } } + ] + } + } + }] + } + """ + let config = try decode(json) + let layout = config.commands[0].workspace!.layout! + if case .split(let split) = layout { + XCTAssertEqual(split.direction, .horizontal) + XCTAssertEqual(split.split, 0.3) + XCTAssertEqual(split.children.count, 2) + } else { + XCTFail("Expected split node") + } + } + + func testDecodeNestedSplits() throws { + let json = """ + { + "commands": [{ + "name": "nested", + "workspace": { + "layout": { + "direction": "horizontal", + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { + "direction": "vertical", + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { "pane": { "surfaces": [{ "type": "browser", "url": "http://localhost:3000" }] } } + ] + } + ] + } + } + }] + } + """ + let config = try decode(json) + let layout = config.commands[0].workspace!.layout! + if case .split(let outer) = layout { + XCTAssertEqual(outer.direction, .horizontal) + if case .split(let inner) = outer.children[1] { + XCTAssertEqual(inner.direction, .vertical) + if case .pane(let browserPane) = inner.children[1] { + XCTAssertEqual(browserPane.surfaces[0].type, .browser) + XCTAssertEqual(browserPane.surfaces[0].url, "http://localhost:3000") + } else { + XCTFail("Expected pane node for inner second child") + } + } else { + XCTFail("Expected split node for outer second child") + } + } else { + XCTFail("Expected split node") + } + } + + // MARK: Surface definitions + + func testDecodeTerminalSurfaceAllFields() throws { + let json = """ + { + "commands": [{ + "name": "test", + "workspace": { + "layout": { + "pane": { + "surfaces": [{ + "type": "terminal", + "name": "server", + "command": "npm start", + "cwd": "./backend", + "env": { "NODE_ENV": "development", "PORT": "3000" }, + "focus": true + }] + } + } + } + }] + } + """ + let config = try decode(json) + let surface = config.commands[0].workspace!.layout! + if case .pane(let pane) = surface { + let s = pane.surfaces[0] + XCTAssertEqual(s.type, .terminal) + XCTAssertEqual(s.name, "server") + XCTAssertEqual(s.command, "npm start") + XCTAssertEqual(s.cwd, "./backend") + XCTAssertEqual(s.env, ["NODE_ENV": "development", "PORT": "3000"]) + XCTAssertEqual(s.focus, true) + XCTAssertNil(s.url) + } else { + XCTFail("Expected pane node") + } + } + + func testDecodeBrowserSurface() throws { + let json = """ + { + "commands": [{ + "name": "test", + "workspace": { + "layout": { + "pane": { + "surfaces": [{ + "type": "browser", + "name": "Preview", + "url": "http://localhost:8080" + }] + } + } + } + }] + } + """ + let config = try decode(json) + if case .pane(let pane) = config.commands[0].workspace!.layout! { + let s = pane.surfaces[0] + XCTAssertEqual(s.type, .browser) + XCTAssertEqual(s.url, "http://localhost:8080") + } else { + XCTFail("Expected pane node") + } + } + + func testDecodeMultipleSurfacesInPane() throws { + let json = """ + { + "commands": [{ + "name": "test", + "workspace": { + "layout": { + "pane": { + "surfaces": [ + { "type": "terminal", "name": "shell1" }, + { "type": "terminal", "name": "shell2" }, + { "type": "browser", "name": "web" } + ] + } + } + } + }] + } + """ + let config = try decode(json) + if case .pane(let pane) = config.commands[0].workspace!.layout! { + XCTAssertEqual(pane.surfaces.count, 3) + XCTAssertEqual(pane.surfaces.map(\.name), ["shell1", "shell2", "web"]) + } else { + XCTFail("Expected pane node") + } + } + + // MARK: Decoding errors + + func testDecodeInvalidLayoutNodeThrows() { + let json = """ + { + "commands": [{ + "name": "bad", + "workspace": { + "layout": { "invalid": true } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeMissingCommandsKeyThrows() { + let json = """ + { "notCommands": [] } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeInvalidSurfaceTypeThrows() { + let json = """ + { + "commands": [{ + "name": "test", + "workspace": { + "layout": { + "pane": { + "surfaces": [{ "type": "invalidType" }] + } + } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + // MARK: Command validation + + func testDecodeCommandWithNeitherWorkspaceNorCommandThrows() { + let json = """ + { + "commands": [{ + "name": "empty" + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeCommandWithBothWorkspaceAndCommandThrows() { + let json = """ + { + "commands": [{ + "name": "hybrid", + "command": "echo hi", + "workspace": { "name": "ws" } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + // MARK: Layout validation + + func testDecodeLayoutNodeWithBothPaneAndDirectionThrows() { + let json = """ + { + "commands": [{ + "name": "ambiguous", + "workspace": { + "layout": { + "pane": { "surfaces": [{ "type": "terminal" }] }, + "direction": "horizontal", + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { "pane": { "surfaces": [{ "type": "terminal" }] } } + ] + } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeSplitWithWrongChildrenCountThrows() { + let json = """ + { + "commands": [{ + "name": "bad-split", + "workspace": { + "layout": { + "direction": "horizontal", + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } } + ] + } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeSplitWithThreeChildrenThrows() { + let json = """ + { + "commands": [{ + "name": "bad-split", + "workspace": { + "layout": { + "direction": "vertical", + "children": [ + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { "pane": { "surfaces": [{ "type": "terminal" }] } }, + { "pane": { "surfaces": [{ "type": "terminal" }] } } + ] + } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodePaneWithEmptySurfacesThrows() { + let json = """ + { + "commands": [{ + "name": "empty-pane", + "workspace": { + "layout": { + "pane": { "surfaces": [] } + } + } + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeBlankNameThrows() { + let json = """ + { + "commands": [{ + "name": "", + "command": "echo hi" + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeWhitespaceOnlyNameThrows() { + let json = """ + { + "commands": [{ + "name": " ", + "command": "echo hi" + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeBlankCommandThrows() { + let json = """ + { + "commands": [{ + "name": "test", + "command": "" + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } + + func testDecodeWhitespaceOnlyCommandThrows() { + let json = """ + { + "commands": [{ + "name": "test", + "command": " " + }] + } + """ + XCTAssertThrowsError(try decode(json)) + } +} + +// MARK: - Command identity + +final class CmuxCommandIdentityTests: XCTestCase { + + func testCommandIdIsDeterministic() { + let cmd = CmuxCommandDefinition(name: "Run tests", command: "test") + XCTAssertEqual(cmd.id, "cmux.config.command.Run%20tests") + } + + func testCommandIdEncodesSpecialCharacters() { + let cmd = CmuxCommandDefinition(name: "build & deploy", command: "make") + XCTAssertTrue(cmd.id.hasPrefix("cmux.config.command.")) + XCTAssertFalse(cmd.id.contains("&")) + XCTAssertFalse(cmd.id.contains(" ")) + } + + func testCommandIdIsUniqueForDifferentNames() { + let cmd1 = CmuxCommandDefinition(name: "build", command: "make build") + let cmd2 = CmuxCommandDefinition(name: "test", command: "make test") + XCTAssertNotEqual(cmd1.id, cmd2.id) + } + + func testCommandIdDoesNotCollideWithBuiltinPrefix() { + let cmd = CmuxCommandDefinition(name: "palette.newWorkspace", command: "echo") + XCTAssertTrue(cmd.id.hasPrefix("cmux.config.command.")) + XCTAssertNotEqual(cmd.id, "palette.newWorkspace") + } +} + +// MARK: - Split clamping + +final class CmuxSplitDefinitionTests: XCTestCase { + + func testClampedSplitPositionDefaultsToHalf() { + let split = CmuxSplitDefinition(direction: .horizontal, split: nil, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.5) + } + + func testClampedSplitPositionPassesThroughValidValue() { + let split = CmuxSplitDefinition(direction: .vertical, split: 0.3, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.3, accuracy: 0.001) + } + + func testClampedSplitPositionClampsLow() { + let split = CmuxSplitDefinition(direction: .horizontal, split: 0.01, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.1, accuracy: 0.001) + } + + func testClampedSplitPositionClampsHigh() { + let split = CmuxSplitDefinition(direction: .horizontal, split: 0.99, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.9, accuracy: 0.001) + } + + func testClampedSplitPositionClampsNegative() { + let split = CmuxSplitDefinition(direction: .horizontal, split: -1.0, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.1, accuracy: 0.001) + } + + func testClampedSplitPositionClampsAboveOne() { + let split = CmuxSplitDefinition(direction: .horizontal, split: 2.0, children: []) + XCTAssertEqual(split.clampedSplitPosition, 0.9, accuracy: 0.001) + } + + func testSplitOrientationHorizontal() { + let split = CmuxSplitDefinition(direction: .horizontal, split: nil, children: []) + XCTAssertEqual(split.splitOrientation, .horizontal) + } + + func testSplitOrientationVertical() { + let split = CmuxSplitDefinition(direction: .vertical, split: nil, children: []) + XCTAssertEqual(split.splitOrientation, .vertical) + } +} + +// MARK: - CWD resolution + +@MainActor +final class CmuxConfigCwdResolutionTests: XCTestCase { + + private let baseCwd = "/Users/test/project" + + func testNilCwdReturnsBase() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd(nil, relativeTo: baseCwd), + baseCwd + ) + } + + func testEmptyCwdReturnsBase() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd("", relativeTo: baseCwd), + baseCwd + ) + } + + func testDotCwdReturnsBase() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd(".", relativeTo: baseCwd), + baseCwd + ) + } + + func testAbsolutePathReturnedAsIs() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd("/tmp/other", relativeTo: baseCwd), + "/tmp/other" + ) + } + + func testRelativePathJoinedToBase() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd("backend/src", relativeTo: baseCwd), + "/Users/test/project/backend/src" + ) + } + + func testTildeExpandsToHome() { + let home = FileManager.default.homeDirectoryForCurrentUser.path + XCTAssertEqual( + CmuxConfigStore.resolveCwd("~", relativeTo: baseCwd), + home + ) + } + + func testTildeSlashExpandsToHomePlusPath() { + let home = FileManager.default.homeDirectoryForCurrentUser.path + XCTAssertEqual( + CmuxConfigStore.resolveCwd("~/Documents/work", relativeTo: baseCwd), + (home as NSString).appendingPathComponent("Documents/work") + ) + } + + func testSingleSubdirectory() { + XCTAssertEqual( + CmuxConfigStore.resolveCwd("src", relativeTo: baseCwd), + "/Users/test/project/src" + ) + } +} + +// MARK: - Layout encoding round-trip + +final class CmuxLayoutEncodingTests: XCTestCase { + + func testPaneNodeRoundTrips() throws { + let original = CmuxLayoutNode.pane(CmuxPaneDefinition(surfaces: [ + CmuxSurfaceDefinition(type: .terminal, name: "shell") + ])) + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(CmuxLayoutNode.self, from: data) + + if case .pane(let pane) = decoded { + XCTAssertEqual(pane.surfaces.count, 1) + XCTAssertEqual(pane.surfaces[0].name, "shell") + } else { + XCTFail("Expected pane node after round-trip") + } + } + + func testSplitNodeRoundTrips() throws { + let original = CmuxLayoutNode.split(CmuxSplitDefinition( + direction: .vertical, + split: 0.7, + children: [ + .pane(CmuxPaneDefinition(surfaces: [CmuxSurfaceDefinition(type: .terminal)])), + .pane(CmuxPaneDefinition(surfaces: [CmuxSurfaceDefinition(type: .browser, url: "http://localhost")])) + ] + )) + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(CmuxLayoutNode.self, from: data) + + if case .split(let split) = decoded { + XCTAssertEqual(split.direction, .vertical) + XCTAssertEqual(split.split, 0.7) + XCTAssertEqual(split.children.count, 2) + } else { + XCTFail("Expected split node after round-trip") + } + } +} diff --git a/web/app/[locale]/components/docs-nav-items.ts b/web/app/[locale]/components/docs-nav-items.ts index 631cb7dcd0..58931c3b55 100644 --- a/web/app/[locale]/components/docs-nav-items.ts +++ b/web/app/[locale]/components/docs-nav-items.ts @@ -2,6 +2,7 @@ export const navItems = [ { titleKey: "gettingStarted" as const, href: "/docs/getting-started" }, { titleKey: "concepts" as const, href: "/docs/concepts" }, { titleKey: "configuration" as const, href: "/docs/configuration" }, + { titleKey: "customCommands" as const, href: "/docs/custom-commands" }, { titleKey: "keyboardShortcuts" as const, href: "/docs/keyboard-shortcuts" }, { titleKey: "apiReference" as const, href: "/docs/api" }, { titleKey: "browserAutomation" as const, href: "/docs/browser-automation" }, diff --git a/web/app/[locale]/docs/custom-commands/page.tsx b/web/app/[locale]/docs/custom-commands/page.tsx new file mode 100644 index 0000000000..048f1b042f --- /dev/null +++ b/web/app/[locale]/docs/custom-commands/page.tsx @@ -0,0 +1,293 @@ +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +import { CodeBlock } from "../../components/code-block"; +import { Callout } from "../../components/callout"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "docs.customCommands" }); + return { + title: t("metaTitle"), + description: t("metaDescription"), + }; +} + +export default function CustomCommandsPage() { + const t = useTranslations("docs.customCommands"); + + return ( + <> +

{t("title")}

+

{t("intro")}

+ +

{t("fileLocations")}

+

{t("fileLocationsDesc")}

+
    +
  • + {t("localConfig")} ./cmux.json — {t("localConfigDesc")} +
  • +
  • + {t("globalConfig")} ~/.config/cmux/cmux.json — {t("globalConfigDesc")} +
  • +
+ {t("precedenceNote")} +

{t("liveReload")}

+ +

{t("schema")}

+

{t("schemaDesc")}

+ {`{ + "commands": [ + { + "name": "Start Dev", + "keywords": ["dev", "start"], + "workspace": { ... } + }, + { + "name": "Run Tests", + "command": "npm test", + "confirm": true + } + ] +}`} + +

{t("simpleCommands")}

+

{t("simpleCommandsDesc")}

+ {`{ + "commands": [ + { + "name": "Run Tests", + "keywords": ["test", "check"], + "command": "npm test", + "confirm": true + } + ] +}`} + +

{t("simpleCommandFields")}

+
    +
  • name — {t("fieldName")}
  • +
  • description — {t("fieldDescription")}
  • +
  • keywords — {t("fieldKeywords")}
  • +
  • command — {t("fieldCommand")}
  • +
  • confirm — {t("fieldConfirm")}
  • +
+

{t("simpleCommandCwdNote")} {"cd \"$(git rev-parse --show-toplevel)\" &&"} {t("simpleCommandCwdRepoRoot")} {"cd /your/path &&"} {t("simpleCommandCwdCustomPath")}

+ +

{t("workspaceCommands")}

+

{t("workspaceCommandsDesc")}

+ {`{ + "commands": [ + { + "name": "Dev Environment", + "keywords": ["dev", "fullstack"], + "restart": "confirm", + "workspace": { + "name": "Dev", + "cwd": ".", + "layout": { + "direction": "horizontal", + "split": 0.5, + "children": [ + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Frontend", + "command": "npm run dev", + "focus": true + } + ] + } + }, + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Backend", + "command": "cargo watch -x run", + "cwd": "./server", + "env": { "RUST_LOG": "debug" } + } + ] + } + } + ] + } + } + } + ] +}`} + +

{t("workspaceFields")}

+
    +
  • name — {t("wsFieldName")}
  • +
  • cwd — {t("wsFieldCwd")}
  • +
  • color — {t("wsFieldColor")}
  • +
  • layout — {t("wsFieldLayout")}
  • +
+ +

{t("restartBehavior")}

+

{t("restartBehaviorDesc")}

+
    +
  • "ignore" — {t("restartIgnore")}
  • +
  • "recreate" — {t("restartRecreate")}
  • +
  • "confirm" — {t("restartConfirm")}
  • +
+ +

{t("layoutTree")}

+

{t("layoutTreeDesc")}

+ +

{t("splitNode")}

+

{t("splitNodeDesc")}

+
    +
  • direction"horizontal" {t("or")} "vertical"
  • +
  • split — {t("splitPosition")}
  • +
  • children — {t("splitChildren")}
  • +
+ +

{t("paneNode")}

+

{t("paneNodeDesc")}

+ +

{t("surfaceDefinition")}

+

{t("surfaceDefinitionDesc")}

+
    +
  • type"terminal" {t("or")} "browser"
  • +
  • name — {t("surfaceName")}
  • +
  • command — {t("surfaceCommand")}
  • +
  • cwd — {t("surfaceCwd")}
  • +
  • env — {t("surfaceEnv")}
  • +
  • url — {t("surfaceUrl")}
  • +
  • focus — {t("surfaceFocus")}
  • +
+ +

{t("cwdResolution")}

+
    +
  • . {t("or")} {t("omitted")} — {t("cwdRelative")}
  • +
  • ./subdir — {t("cwdSubdir")}
  • +
  • ~/path — {t("cwdHome")}
  • +
  • {t("absolutePath")} — {t("cwdAbsolute")}
  • +
+ +

{t("fullExample")}

+ {`{ + "commands": [ + { + "name": "Web Dev", + "description": "Docs site with live preview", + "keywords": ["web", "docs", "next", "frontend"], + "restart": "confirm", + "workspace": { + "name": "Web Dev", + "cwd": "./web", + "color": "#3b82f6", + "layout": { + "direction": "horizontal", + "split": 0.5, + "children": [ + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Next.js", + "command": "npm run dev", + "focus": true + } + ] + } + }, + { + "direction": "vertical", + "split": 0.6, + "children": [ + { + "pane": { + "surfaces": [ + { + "type": "browser", + "name": "Preview", + "url": "http://localhost:3777" + } + ] + } + }, + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Shell", + "env": { "NODE_ENV": "development" } + } + ] + } + } + ] + } + ] + } + } + }, + { + "name": "Debug Log", + "description": "Tail the debug event log from the running dev app", + "keywords": ["log", "debug", "tail", "events"], + "restart": "ignore", + "workspace": { + "name": "Debug Log", + "layout": { + "direction": "horizontal", + "split": 0.5, + "children": [ + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Events", + "command": "tail -f /tmp/cmux-debug.log", + "focus": true + } + ] + } + }, + { + "pane": { + "surfaces": [ + { + "type": "terminal", + "name": "Shell" + } + ] + } + } + ] + } + } + }, + { + "name": "Setup", + "description": "Initialize submodules and build dependencies", + "keywords": ["setup", "init", "install"], + "command": "./scripts/setup.sh", + "confirm": true + }, + { + "name": "Reload", + "description": "Build and launch the debug app tagged to the current branch", + "keywords": ["reload", "build", "run", "launch"], + "command": "./scripts/reload.sh --tag $(git branch --show-current)" + }, + { + "name": "Run Unit Tests", + "keywords": ["test", "unit"], + "command": "./scripts/test-unit.sh", + "confirm": true + } + ] +}`} + + ); +} diff --git a/web/messages/ar.json b/web/messages/ar.json index 099b3578ca..e9aa591e7d 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -302,6 +302,70 @@ "exampleConfig": "مثال على الإعدادات", "metaTitle": "الإعدادات" }, + "customCommands": { + "title": "أوامر مخصصة", + "metaTitle": "أوامر مخصصة", + "metaDescription": "تعريف الأوامر المخصصة وتخطيطات مساحة العمل في cmux.json. تكوين لكل مشروع وعالمي مع مراقبة الملفات المباشرة.", + "intro": "عرّف الأوامر المخصصة وتخطيطات مساحة العمل بإضافة ملف cmux.json إلى جذر مشروعك أو ~/.config/cmux/. تظهر الأوامر في لوحة الأوامر.", + "fileLocations": "مواقع الملفات", + "fileLocationsDesc": "يبحث cmux عن التكوين في مكانين:", + "localConfig": "لكل مشروع:", + "localConfigDesc": "يقع في دليل مشروعك، له الأولوية", + "globalConfig": "عالمي:", + "globalConfigDesc": "ينطبق على جميع المشاريع، يملأ الأوامر غير المعرفة محلياً", + "precedenceNote": "الأوامر المحلية تتجاوز الأوامر العالمية بنفس الاسم.", + "liveReload": "يتم اكتشاف التغييرات تلقائياً — لا حاجة لإعادة التشغيل.", + "schema": "المخطط", + "schemaDesc": "يحتوي ملف cmux.json على مصفوفة commands. كل أمر إما أمر shell بسيط أو تعريف كامل لمساحة عمل:", + "simpleCommands": "أوامر بسيطة", + "simpleCommandsDesc": "الأمر البسيط ينفذ أمر shell في الطرفية المحددة حالياً:", + "simpleCommandFields": "الحقول", + "fieldName": "يظهر في لوحة الأوامر (مطلوب)", + "fieldDescription": "وصف اختياري", + "fieldKeywords": "مصطلحات بحث إضافية للوحة الأوامر", + "fieldCommand": "أمر shell للتشغيل في الطرفية المحددة", + "fieldConfirm": "عرض مربع حوار للتأكيد قبل التشغيل", + "simpleCommandCwdNote": "تعمل الأوامر البسيطة في دليل العمل الحالي للطرفية المحددة. إذا كان أمرك يعتمد على مسارات نسبية للمشروع، أضف قبله", + "simpleCommandCwdRepoRoot": "للتشغيل من جذر المستودع، أو", + "simpleCommandCwdCustomPath": "لأي دليل محدد.", + "workspaceCommands": "أوامر مساحة العمل", + "workspaceCommandsDesc": "يُنشئ أمر مساحة العمل مساحة عمل جديدة بتخطيط مخصص من الانقسامات والطرفيات وألواح المتصفح:", + "workspaceFields": "حقول مساحة العمل", + "wsFieldName": "اسم علامة التبويب لمساحة العمل (الافتراضي: اسم الأمر)", + "wsFieldCwd": "دليل العمل لمساحة العمل", + "wsFieldColor": "لون علامة تبويب مساحة العمل", + "wsFieldLayout": "شجرة التخطيط التي تحدد الانقسامات والألواح", + "restartBehavior": "سلوك إعادة التشغيل", + "restartBehaviorDesc": "يتحكم فيما يحدث عندما توجد مساحة عمل بنفس الاسم:", + "restartIgnore": "التبديل إلى مساحة العمل الموجودة (الافتراضي)", + "restartRecreate": "الإغلاق وإعادة الإنشاء دون سؤال", + "restartConfirm": "السؤال قبل إعادة الإنشاء", + "layoutTree": "شجرة التخطيط", + "layoutTreeDesc": "تحدد شجرة التخطيط كيفية ترتيب الألواح باستخدام عقد الانقسام المتكررة:", + "splitNode": "عقدة الانقسام", + "splitNodeDesc": "تقسم المساحة إلى فرعين:", + "or": "أو", + "splitPosition": "موضع الفاصل من 0.1 إلى 0.9 (الافتراضي 0.5)", + "splitChildren": "فرعان بالضبط (انقسام أو لوح)", + "paneNode": "عقدة اللوح", + "paneNodeDesc": "عقدة طرفية تحتوي على واحد أو أكثر من الأسطح (علامات التبويب داخل اللوح).", + "surfaceDefinition": "تعريف السطح", + "surfaceDefinitionDesc": "كل سطح في لوح يمكن أن يكون طرفية أو متصفحاً:", + "surfaceName": "عنوان علامة تبويب مخصص", + "surfaceCommand": "أمر shell للتشغيل التلقائي عند الإنشاء (للطرفية فقط)", + "surfaceCwd": "دليل العمل لهذا السطح", + "surfaceEnv": "متغيرات البيئة كأزواج مفتاح-قيمة", + "surfaceUrl": "رابط للفتح (للمتصفح فقط)", + "surfaceFocus": "التركيز على هذا السطح بعد الإنشاء", + "cwdResolution": "تحليل دليل العمل", + "omitted": "محذوف", + "cwdRelative": "دليل عمل مساحة العمل", + "cwdSubdir": "نسبي إلى دليل عمل مساحة العمل", + "cwdHome": "موسّع إلى الدليل الرئيسي", + "absolutePath": "مسار مطلق", + "cwdAbsolute": "يُستخدم كما هو", + "fullExample": "مثال كامل" + }, "keyboardShortcuts": { "title": "اختصارات لوحة المفاتيح", "description": "جميع اختصارات لوحة المفاتيح المتاحة في cmux، مجمعة حسب الفئة.", @@ -547,6 +611,7 @@ "gettingStarted": "البدء", "concepts": "المفاهيم", "configuration": "الإعدادات", + "customCommands": "أوامر مخصصة", "keyboardShortcuts": "اختصارات لوحة المفاتيح", "apiReference": "مرجع الواجهة البرمجية", "browserAutomation": "أتمتة المتصفح", diff --git a/web/messages/bs.json b/web/messages/bs.json index 6210c4b9a0..0a37eab264 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -302,6 +302,70 @@ "exampleConfig": "Primjer konfiguracije", "metaTitle": "Konfiguracija" }, + "customCommands": { + "title": "Prilagođene komande", + "metaTitle": "Prilagođene komande", + "metaDescription": "Definirajte prilagođene komande i rasporede radnog prostora u cmux.json. Konfiguracija po projektu i globalna konfiguracija s praćenjem promjena datoteka.", + "intro": "Definirajte prilagođene komande i rasporede radnog prostora dodavanjem datoteke cmux.json u korijenski direktorij vašeg projekta ili ~/.config/cmux/. Komande se pojavljuju u paleti komandi.", + "fileLocations": "Lokacije datoteka", + "fileLocationsDesc": "cmux traži konfiguraciju na dva mjesta:", + "localConfig": "Po projektu:", + "localConfigDesc": "nalazi se u vašem projektnom direktoriju, ima prednost", + "globalConfig": "Globalno:", + "globalConfigDesc": "primjenjuje se na sve projekte, dopunjava komande koje nisu definirane lokalno", + "precedenceNote": "Lokalne komande nadjačavaju globalne komande istog naziva.", + "liveReload": "Promjene se automatski preuzimaju — nije potrebno ponovno pokretanje.", + "schema": "Shema", + "schemaDesc": "Datoteka cmux.json sadrži niz commands. Svaka komanda je ili jednostavna shell komanda ili potpuna definicija radnog prostora:", + "simpleCommands": "Jednostavne komande", + "simpleCommandsDesc": "Jednostavna komanda pokreće shell komandu u trenutno fokusiranom terminalu:", + "simpleCommandFields": "Polja", + "fieldName": "Prikazuje se u paleti komandi (obavezno)", + "fieldDescription": "Neobavezan opis", + "fieldKeywords": "Dodatni pojmovi za pretraživanje u paleti komandi", + "fieldCommand": "Shell komanda za pokretanje u fokusiranom terminalu", + "fieldConfirm": "Prikaži dijalog za potvrdu prije pokretanja", + "simpleCommandCwdNote": "Jednostavne komande se pokreću u trenutnom radnom direktoriju fokusiranog terminala. Ako vaša komanda zavisi od putanja relativnih projektu, dodajte prefiks", + "simpleCommandCwdRepoRoot": "za pokretanje iz korijena repozitorija, ili", + "simpleCommandCwdCustomPath": "za bilo koji specifični direktorij.", + "workspaceCommands": "Komande radnog prostora", + "workspaceCommandsDesc": "Komanda radnog prostora kreira novi radni prostor s prilagođenim rasporedom podjela, terminala i panela preglednika:", + "workspaceFields": "Polja radnog prostora", + "wsFieldName": "Naziv kartice radnog prostora (zadano je naziv komande)", + "wsFieldCwd": "Radni direktorij za radni prostor", + "wsFieldColor": "Boja kartice radnog prostora", + "wsFieldLayout": "Stablo rasporeda koje definira podjele i panele", + "restartBehavior": "Ponašanje pri ponovnom pokretanju", + "restartBehaviorDesc": "Kontrolira što se dešava kada radni prostor istog naziva već postoji:", + "restartIgnore": "Prebaci na postojeći radni prostor (zadano)", + "restartRecreate": "Zatvori i ponovo kreiraj bez pitanja", + "restartConfirm": "Pitaj korisnika prije ponovnog kreiranja", + "layoutTree": "Stablo rasporeda", + "layoutTreeDesc": "Stablo rasporeda definira kako su paneli raspoređeni koristeći rekurzivne čvorove podjele:", + "splitNode": "Čvor podjele", + "splitNodeDesc": "Dijeli prostor na dva djeteta:", + "or": "ili", + "splitPosition": "Pozicija razdjeljnika od 0.1 do 0.9 (zadano 0.5)", + "splitChildren": "Točno dva dječja čvora (podjela ili panel)", + "paneNode": "Čvor panela", + "paneNodeDesc": "Listni čvor koji sadrži jednu ili više površina (kartice unutar panela).", + "surfaceDefinition": "Definicija površine", + "surfaceDefinitionDesc": "Svaka površina u panelu može biti terminal ili preglednik:", + "surfaceName": "Prilagođeni naslov kartice", + "surfaceCommand": "Shell komanda za automatsko pokretanje pri kreiranju (samo terminal)", + "surfaceCwd": "Radni direktorij za ovu površinu", + "surfaceEnv": "Varijable okruženja kao parovi ključ-vrijednost", + "surfaceUrl": "URL za otvaranje (samo preglednik)", + "surfaceFocus": "Fokusiraj ovu površinu nakon kreiranja", + "cwdResolution": "Razrješavanje radnog direktorija", + "omitted": "izostavljeno", + "cwdRelative": "radni direktorij radnog prostora", + "cwdSubdir": "relativno u odnosu na radni direktorij radnog prostora", + "cwdHome": "prošireno na kućni direktorij", + "absolutePath": "Apsolutna putanja", + "cwdAbsolute": "koristi se kao takvo", + "fullExample": "Potpuni primjer" + }, "keyboardShortcuts": { "title": "Prečice na tastaturi", "description": "Sve prečice na tastaturi dostupne u cmux-u, grupirane po kategorijama.", @@ -547,6 +611,7 @@ "gettingStarted": "Početak rada", "concepts": "Koncepti", "configuration": "Konfiguracija", + "customCommands": "Prilagođene komande", "keyboardShortcuts": "Prečice na tastaturi", "apiReference": "API Referenca", "browserAutomation": "Automatizacija preglednika", diff --git a/web/messages/da.json b/web/messages/da.json index eeaf4a9320..0f7c374b78 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -302,6 +302,70 @@ "exampleConfig": "Eksempelkonfiguration", "metaTitle": "Konfiguration" }, + "customCommands": { + "title": "Brugerdefinerede kommandoer", + "metaTitle": "Brugerdefinerede kommandoer", + "metaDescription": "Definer brugerdefinerede kommandoer og workspace-layouts i cmux.json. Per-projekt og global konfiguration med live filovervågning.", + "intro": "Definer brugerdefinerede kommandoer og workspace-layouts ved at tilføje en cmux.json-fil til din projektrod eller ~/.config/cmux/. Kommandoer vises i kommandopaletten.", + "fileLocations": "Filplaceringer", + "fileLocationsDesc": "cmux leder efter konfiguration to steder:", + "localConfig": "Per projekt:", + "localConfigDesc": "befinder sig i din projektmappe, har forrang", + "globalConfig": "Global:", + "globalConfigDesc": "gælder for alle projekter, udfylder kommandoer, der ikke er defineret lokalt", + "precedenceNote": "Lokale kommandoer tilsidesætter globale kommandoer med samme navn.", + "liveReload": "Ændringer hentes automatisk — ingen genstart nødvendig.", + "schema": "Skema", + "schemaDesc": "En cmux.json-fil indeholder et commands-array. Hver kommando er enten en simpel shell-kommando eller en fuld workspace-definition:", + "simpleCommands": "Simple kommandoer", + "simpleCommandsDesc": "En simpel kommando kører en shell-kommando i den aktuelt fokuserede terminal:", + "simpleCommandFields": "Felter", + "fieldName": "Vises i kommandopaletten (påkrævet)", + "fieldDescription": "Valgfri beskrivelse", + "fieldKeywords": "Ekstra søgetermer til kommandopaletten", + "fieldCommand": "Shell-kommando der skal køres i den fokuserede terminal", + "fieldConfirm": "Vis en bekræftelsesdialog før kørsel", + "simpleCommandCwdNote": "Simple kommandoer køres i den fokuserede terminals aktuelle arbejdsmappe. Hvis din kommando afhænger af projektrelative stier, sæt foran med", + "simpleCommandCwdRepoRoot": "for at køre fra repo-roden, eller", + "simpleCommandCwdCustomPath": "for enhver specifik mappe.", + "workspaceCommands": "Workspace-kommandoer", + "workspaceCommandsDesc": "En workspace-kommando opretter et nyt workspace med et brugerdefineret layout af opdelte terminaler og browserpaneler:", + "workspaceFields": "Workspace-felter", + "wsFieldName": "Workspace-fanenavn (standard er kommandoens navn)", + "wsFieldCwd": "Arbejdsmappe for workspacet", + "wsFieldColor": "Farve på workspace-fanen", + "wsFieldLayout": "Layouttræ der definerer opdelinger og paneler", + "restartBehavior": "Genstartsadfærd", + "restartBehaviorDesc": "Styrer hvad der sker, når der allerede eksisterer et workspace med samme navn:", + "restartIgnore": "Skift til det eksisterende workspace (standard)", + "restartRecreate": "Luk og genskab uden at spørge", + "restartConfirm": "Spørg brugeren inden genskabelse", + "layoutTree": "Layouttræ", + "layoutTreeDesc": "Layouttræet definerer, hvordan paneler arrangeres ved hjælp af rekursive opdelingsknuder:", + "splitNode": "Opdelingsknude", + "splitNodeDesc": "Deler plads i to børn:", + "or": "eller", + "splitPosition": "Delergitterposition fra 0.1 til 0.9 (standard 0.5)", + "splitChildren": "Præcis to underknuder (opdeling eller panel)", + "paneNode": "Panelknude", + "paneNodeDesc": "En bladknude der indeholder én eller flere overflader (faner inden i panelet).", + "surfaceDefinition": "Overfladedefinition", + "surfaceDefinitionDesc": "Hver overflade i et panel kan være en terminal eller en browser:", + "surfaceName": "Brugerdefineret fanetitel", + "surfaceCommand": "Shell-kommando der automatisk køres ved oprettelse (kun terminal)", + "surfaceCwd": "Arbejdsmappe for denne overflade", + "surfaceEnv": "Miljøvariabler som nøgle-værdi-par", + "surfaceUrl": "URL der skal åbnes (kun browser)", + "surfaceFocus": "Fokuser på denne overflade efter oprettelse", + "cwdResolution": "Opløsning af arbejdsmappe", + "omitted": "udeladt", + "cwdRelative": "workspace-arbejdsmappe", + "cwdSubdir": "relativ til workspace-arbejdsmappe", + "cwdHome": "udvidet til hjemmemappen", + "absolutePath": "Absolut sti", + "cwdAbsolute": "bruges som den er", + "fullExample": "Fuldt eksempel" + }, "keyboardShortcuts": { "title": "Tastaturgenveje", "description": "Alle tastaturgenveje tilgængelige i cmux, grupperet efter kategori.", @@ -547,6 +611,7 @@ "gettingStarted": "Kom i gang", "concepts": "Koncepter", "configuration": "Konfiguration", + "customCommands": "Brugerdefinerede kommandoer", "keyboardShortcuts": "Tastaturgenveje", "apiReference": "API-reference", "browserAutomation": "Browserautomatisering", diff --git a/web/messages/de.json b/web/messages/de.json index df68123fe5..fc4ab103ea 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -302,6 +302,70 @@ "exampleConfig": "Beispielkonfiguration", "metaTitle": "Konfiguration" }, + "customCommands": { + "title": "Benutzerdefinierte Befehle", + "metaTitle": "Benutzerdefinierte Befehle", + "metaDescription": "Benutzerdefinierte Befehle und Workspace-Layouts in cmux.json definieren. Projektspezifische und globale Konfiguration mit Live-Dateiüberwachung.", + "intro": "Definieren Sie benutzerdefinierte Befehle und Workspace-Layouts, indem Sie eine cmux.json-Datei in Ihr Projektstammverzeichnis oder ~/.config/cmux/ hinzufügen. Befehle erscheinen in der Befehlspalette.", + "fileLocations": "Dateispeicherorte", + "fileLocationsDesc": "cmux sucht an zwei Stellen nach Konfiguration:", + "localConfig": "Projektspezifisch:", + "localConfigDesc": "liegt in Ihrem Projektverzeichnis, hat Vorrang", + "globalConfig": "Global:", + "globalConfigDesc": "gilt für alle Projekte, ergänzt lokal nicht definierte Befehle", + "precedenceNote": "Lokale Befehle überschreiben globale Befehle mit demselben Namen.", + "liveReload": "Änderungen werden automatisch übernommen — kein Neustart erforderlich.", + "schema": "Schema", + "schemaDesc": "Eine cmux.json-Datei enthält ein commands-Array. Jeder Befehl ist entweder ein einfacher Shell-Befehl oder eine vollständige Workspace-Definition:", + "simpleCommands": "Einfache Befehle", + "simpleCommandsDesc": "Ein einfacher Befehl führt einen Shell-Befehl im aktuell fokussierten Terminal aus:", + "simpleCommandFields": "Felder", + "fieldName": "Wird in der Befehlspalette angezeigt (erforderlich)", + "fieldDescription": "Optionale Beschreibung", + "fieldKeywords": "Zusätzliche Suchbegriffe für die Befehlspalette", + "fieldCommand": "Shell-Befehl, der im fokussierten Terminal ausgeführt wird", + "fieldConfirm": "Bestätigungsdialog vor der Ausführung anzeigen", + "simpleCommandCwdNote": "Einfache Befehle werden im aktuellen Arbeitsverzeichnis des fokussierten Terminals ausgeführt. Wenn Ihr Befehl projektrelative Pfade benötigt, stellen Sie", + "simpleCommandCwdRepoRoot": "voran, um vom Repository-Stammverzeichnis auszuführen, oder", + "simpleCommandCwdCustomPath": "für ein beliebiges Verzeichnis.", + "workspaceCommands": "Workspace-Befehle", + "workspaceCommandsDesc": "Ein Workspace-Befehl erstellt einen neuen Workspace mit einem benutzerdefinierten Layout aus Aufteilungen, Terminals und Browser-Fenstern:", + "workspaceFields": "Workspace-Felder", + "wsFieldName": "Name des Workspace-Tabs (Standard ist Befehlsname)", + "wsFieldCwd": "Arbeitsverzeichnis für den Workspace", + "wsFieldColor": "Farbe des Workspace-Tabs", + "wsFieldLayout": "Layout-Baum, der Aufteilungen und Fenster definiert", + "restartBehavior": "Neustart-Verhalten", + "restartBehaviorDesc": "Steuert, was passiert, wenn bereits ein Workspace mit demselben Namen existiert:", + "restartIgnore": "Zum vorhandenen Workspace wechseln (Standard)", + "restartRecreate": "Schließen und ohne Rückfrage neu erstellen", + "restartConfirm": "Benutzer vor der Neuerstellung fragen", + "layoutTree": "Layout-Baum", + "layoutTreeDesc": "Der Layout-Baum definiert, wie Fenster mithilfe rekursiver Aufteilungsknoten angeordnet werden:", + "splitNode": "Aufteilungsknoten", + "splitNodeDesc": "Teilt den Platz in zwei Kindelemente auf:", + "or": "oder", + "splitPosition": "Teilerposition von 0.1 bis 0.9 (Standard 0.5)", + "splitChildren": "Genau zwei Kindknoten (Aufteilung oder Fenster)", + "paneNode": "Fensterknoten", + "paneNodeDesc": "Ein Blattknoten, der eine oder mehrere Oberflächen enthält (Tabs innerhalb des Fensters).", + "surfaceDefinition": "Oberflächendefinition", + "surfaceDefinitionDesc": "Jede Oberfläche in einem Fenster kann ein Terminal oder ein Browser sein:", + "surfaceName": "Benutzerdefinierter Tab-Titel", + "surfaceCommand": "Shell-Befehl, der bei der Erstellung automatisch ausgeführt wird (nur Terminal)", + "surfaceCwd": "Arbeitsverzeichnis für diese Oberfläche", + "surfaceEnv": "Umgebungsvariablen als Schlüssel-Wert-Paare", + "surfaceUrl": "URL zum Öffnen (nur Browser)", + "surfaceFocus": "Diese Oberfläche nach der Erstellung fokussieren", + "cwdResolution": "Arbeitsverzeichnis-Auflösung", + "omitted": "weggelassen", + "cwdRelative": "Workspace-Arbeitsverzeichnis", + "cwdSubdir": "relativ zum Workspace-Arbeitsverzeichnis", + "cwdHome": "auf Home-Verzeichnis erweitert", + "absolutePath": "Absoluter Pfad", + "cwdAbsolute": "wird unverändert verwendet", + "fullExample": "Vollständiges Beispiel" + }, "keyboardShortcuts": { "title": "Tastaturkürzel", "description": "Alle in cmux verfügbaren Tastaturkürzel, nach Kategorie gruppiert.", @@ -547,6 +611,7 @@ "gettingStarted": "Erste Schritte", "concepts": "Konzepte", "configuration": "Konfiguration", + "customCommands": "Benutzerdefinierte Befehle", "keyboardShortcuts": "Tastaturkürzel", "apiReference": "API-Referenz", "browserAutomation": "Browser-Automatisierung", diff --git a/web/messages/en.json b/web/messages/en.json index 44437938f7..b699cd9fe5 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -302,6 +302,70 @@ "browserHostsHttp": "HTTP Hosts Allowed in Embedded Browser: applies only to HTTP (non-HTTPS) URLs. Hosts in this list can open in cmux without a warning prompt. Defaults include localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me.", "exampleConfig": "Example config" }, + "customCommands": { + "title": "Custom Commands", + "metaTitle": "Custom Commands", + "metaDescription": "Define custom commands and workspace layouts in cmux.json. Per-project and global configuration with live file watching.", + "intro": "Define custom commands and workspace layouts by adding a cmux.json file to your project root or ~/.config/cmux/. Commands appear in the command palette.", + "fileLocations": "File locations", + "fileLocationsDesc": "cmux looks for configuration in two places:", + "localConfig": "Per-project:", + "localConfigDesc": "lives in your project directory, takes precedence", + "globalConfig": "Global:", + "globalConfigDesc": "applies to all projects, fills in commands not defined locally", + "precedenceNote": "Local commands override global commands with the same name.", + "liveReload": "Changes are picked up automatically — no restart needed.", + "schema": "Schema", + "schemaDesc": "A cmux.json file contains a commands array. Each command is either a simple shell command or a full workspace definition:", + "simpleCommands": "Simple commands", + "simpleCommandsDesc": "A simple command runs a shell command in the currently focused terminal:", + "simpleCommandFields": "Fields", + "fieldName": "Displayed in the command palette (required)", + "fieldDescription": "Optional description", + "fieldKeywords": "Extra search terms for the command palette", + "fieldCommand": "Shell command to run in the focused terminal", + "fieldConfirm": "Show a confirmation dialog before running", + "simpleCommandCwdNote": "Simple commands run in the focused terminal's current working directory. If your command relies on project-relative paths, prefix it with", + "simpleCommandCwdRepoRoot": "to run from the repo root, or", + "simpleCommandCwdCustomPath": "for any specific directory.", + "workspaceCommands": "Workspace commands", + "workspaceCommandsDesc": "A workspace command creates a new workspace with a custom layout of splits, terminals, and browser panes:", + "workspaceFields": "Workspace fields", + "wsFieldName": "Workspace tab name (defaults to command name)", + "wsFieldCwd": "Working directory for the workspace", + "wsFieldColor": "Workspace tab color", + "wsFieldLayout": "Layout tree defining splits and panes", + "restartBehavior": "Restart behavior", + "restartBehaviorDesc": "Controls what happens when a workspace with the same name already exists:", + "restartIgnore": "Switch to the existing workspace (default)", + "restartRecreate": "Close and recreate without asking", + "restartConfirm": "Ask the user before recreating", + "layoutTree": "Layout tree", + "layoutTreeDesc": "The layout tree defines how panes are arranged using recursive split nodes:", + "splitNode": "Split node", + "splitNodeDesc": "Divides space into two children:", + "or": "or", + "splitPosition": "Divider position from 0.1 to 0.9 (default 0.5)", + "splitChildren": "Exactly two child nodes (split or pane)", + "paneNode": "Pane node", + "paneNodeDesc": "A leaf node containing one or more surfaces (tabs within the pane).", + "surfaceDefinition": "Surface definition", + "surfaceDefinitionDesc": "Each surface in a pane can be a terminal or a browser:", + "surfaceName": "Custom tab title", + "surfaceCommand": "Shell command to auto-run on creation (terminal only)", + "surfaceCwd": "Working directory for this surface", + "surfaceEnv": "Environment variables as key-value pairs", + "surfaceUrl": "URL to open (browser only)", + "surfaceFocus": "Focus this surface after creation", + "cwdResolution": "Working directory resolution", + "omitted": "omitted", + "cwdRelative": "workspace working directory", + "cwdSubdir": "relative to workspace working directory", + "cwdHome": "expanded to home directory", + "absolutePath": "Absolute path", + "cwdAbsolute": "used as-is", + "fullExample": "Full example" + }, "keyboardShortcuts": { "title": "Keyboard Shortcuts", "description": "All keyboard shortcuts available in cmux, grouped by category.", @@ -547,6 +611,7 @@ "gettingStarted": "Getting Started", "concepts": "Concepts", "configuration": "Configuration", + "customCommands": "Custom Commands", "keyboardShortcuts": "Keyboard Shortcuts", "apiReference": "API Reference", "browserAutomation": "Browser Automation", diff --git a/web/messages/es.json b/web/messages/es.json index 6913eaaaab..1cd3a00386 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -302,6 +302,70 @@ "exampleConfig": "Configuración de ejemplo", "metaTitle": "Configuración" }, + "customCommands": { + "title": "Comandos personalizados", + "metaTitle": "Comandos personalizados", + "metaDescription": "Define comandos personalizados y diseños de workspace en cmux.json. Configuración por proyecto y global con monitoreo en vivo de archivos.", + "intro": "Define comandos personalizados y diseños de workspace añadiendo un archivo cmux.json a la raíz de tu proyecto o ~/.config/cmux/. Los comandos aparecen en la paleta de comandos.", + "fileLocations": "Ubicaciones de archivos", + "fileLocationsDesc": "cmux busca configuración en dos lugares:", + "localConfig": "Por proyecto:", + "localConfigDesc": "se encuentra en tu directorio de proyecto, tiene prioridad", + "globalConfig": "Global:", + "globalConfigDesc": "se aplica a todos los proyectos, completa los comandos no definidos localmente", + "precedenceNote": "Los comandos locales anulan los comandos globales con el mismo nombre.", + "liveReload": "Los cambios se recogen automáticamente — no se necesita reinicio.", + "schema": "Esquema", + "schemaDesc": "Un archivo cmux.json contiene un array commands. Cada comando es un comando de shell simple o una definición completa de workspace:", + "simpleCommands": "Comandos simples", + "simpleCommandsDesc": "Un comando simple ejecuta un comando de shell en el terminal actualmente enfocado:", + "simpleCommandFields": "Campos", + "fieldName": "Se muestra en la paleta de comandos (requerido)", + "fieldDescription": "Descripción opcional", + "fieldKeywords": "Términos de búsqueda adicionales para la paleta de comandos", + "fieldCommand": "Comando de shell para ejecutar en el terminal enfocado", + "fieldConfirm": "Mostrar un diálogo de confirmación antes de ejecutar", + "simpleCommandCwdNote": "Los comandos simples se ejecutan en el directorio de trabajo actual de la terminal enfocada. Si tu comando depende de rutas relativas al proyecto, prefija con", + "simpleCommandCwdRepoRoot": "para ejecutar desde la raíz del repositorio, o", + "simpleCommandCwdCustomPath": "para cualquier directorio específico.", + "workspaceCommands": "Comandos de workspace", + "workspaceCommandsDesc": "Un comando de workspace crea un nuevo workspace con un diseño personalizado de divisiones, terminales y paneles de navegador:", + "workspaceFields": "Campos de workspace", + "wsFieldName": "Nombre de la pestaña del workspace (por defecto es el nombre del comando)", + "wsFieldCwd": "Directorio de trabajo del workspace", + "wsFieldColor": "Color de la pestaña del workspace", + "wsFieldLayout": "Árbol de diseño que define divisiones y paneles", + "restartBehavior": "Comportamiento de reinicio", + "restartBehaviorDesc": "Controla qué sucede cuando ya existe un workspace con el mismo nombre:", + "restartIgnore": "Cambiar al workspace existente (por defecto)", + "restartRecreate": "Cerrar y recrear sin preguntar", + "restartConfirm": "Preguntar al usuario antes de recrear", + "layoutTree": "Árbol de diseño", + "layoutTreeDesc": "El árbol de diseño define cómo se organizan los paneles usando nodos de división recursivos:", + "splitNode": "Nodo de división", + "splitNodeDesc": "Divide el espacio en dos hijos:", + "or": "o", + "splitPosition": "Posición del divisor de 0.1 a 0.9 (por defecto 0.5)", + "splitChildren": "Exactamente dos nodos hijos (división o panel)", + "paneNode": "Nodo de panel", + "paneNodeDesc": "Un nodo hoja que contiene una o más superficies (pestañas dentro del panel).", + "surfaceDefinition": "Definición de superficie", + "surfaceDefinitionDesc": "Cada superficie en un panel puede ser un terminal o un navegador:", + "surfaceName": "Título de pestaña personalizado", + "surfaceCommand": "Comando de shell para ejecutar automáticamente al crear (solo terminal)", + "surfaceCwd": "Directorio de trabajo para esta superficie", + "surfaceEnv": "Variables de entorno como pares clave-valor", + "surfaceUrl": "URL para abrir (solo navegador)", + "surfaceFocus": "Enfocar esta superficie después de crearla", + "cwdResolution": "Resolución del directorio de trabajo", + "omitted": "omitido", + "cwdRelative": "directorio de trabajo del workspace", + "cwdSubdir": "relativo al directorio de trabajo del workspace", + "cwdHome": "expandido al directorio home", + "absolutePath": "Ruta absoluta", + "cwdAbsolute": "se usa tal cual", + "fullExample": "Ejemplo completo" + }, "keyboardShortcuts": { "title": "Atajos de teclado", "description": "Todos los atajos de teclado disponibles en cmux, agrupados por categoría.", @@ -547,6 +611,7 @@ "gettingStarted": "Primeros pasos", "concepts": "Conceptos", "configuration": "Configuración", + "customCommands": "Comandos personalizados", "keyboardShortcuts": "Atajos de teclado", "apiReference": "Referencia de API", "browserAutomation": "Automatización del navegador", diff --git a/web/messages/fr.json b/web/messages/fr.json index 61426dd90b..35b1d5112c 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -302,6 +302,70 @@ "exampleConfig": "Exemple de configuration", "metaTitle": "Configuration" }, + "customCommands": { + "title": "Commandes personnalisées", + "metaTitle": "Commandes personnalisées", + "metaDescription": "Définissez des commandes personnalisées et des mises en page d'espace de travail dans cmux.json. Configuration par projet et globale avec surveillance en direct des fichiers.", + "intro": "Définissez des commandes personnalisées et des mises en page d'espace de travail en ajoutant un fichier cmux.json à la racine de votre projet ou ~/.config/cmux/. Les commandes apparaissent dans la palette de commandes.", + "fileLocations": "Emplacements des fichiers", + "fileLocationsDesc": "cmux recherche la configuration à deux endroits :", + "localConfig": "Par projet :", + "localConfigDesc": "se trouve dans votre répertoire de projet, a la priorité", + "globalConfig": "Global :", + "globalConfigDesc": "s'applique à tous les projets, complète les commandes non définies localement", + "precedenceNote": "Les commandes locales remplacent les commandes globales du même nom.", + "liveReload": "Les modifications sont prises en compte automatiquement — aucun redémarrage nécessaire.", + "schema": "Schéma", + "schemaDesc": "Un fichier cmux.json contient un tableau commands. Chaque commande est soit une commande shell simple, soit une définition complète d'espace de travail :", + "simpleCommands": "Commandes simples", + "simpleCommandsDesc": "Une commande simple exécute une commande shell dans le terminal actuellement focalisé :", + "simpleCommandFields": "Champs", + "fieldName": "Affiché dans la palette de commandes (requis)", + "fieldDescription": "Description optionnelle", + "fieldKeywords": "Termes de recherche supplémentaires pour la palette de commandes", + "fieldCommand": "Commande shell à exécuter dans le terminal focalisé", + "fieldConfirm": "Afficher une boîte de dialogue de confirmation avant l'exécution", + "simpleCommandCwdNote": "Les commandes simples s'exécutent dans le répertoire de travail actuel du terminal ciblé. Si votre commande utilise des chemins relatifs au projet, préfixez avec", + "simpleCommandCwdRepoRoot": "pour exécuter depuis la racine du dépôt, ou", + "simpleCommandCwdCustomPath": "pour n'importe quel répertoire spécifique.", + "workspaceCommands": "Commandes d'espace de travail", + "workspaceCommandsDesc": "Une commande d'espace de travail crée un nouvel espace de travail avec une mise en page personnalisée de divisions, terminaux et panneaux de navigateur :", + "workspaceFields": "Champs de l'espace de travail", + "wsFieldName": "Nom de l'onglet de l'espace de travail (par défaut, nom de la commande)", + "wsFieldCwd": "Répertoire de travail de l'espace de travail", + "wsFieldColor": "Couleur de l'onglet de l'espace de travail", + "wsFieldLayout": "Arbre de mise en page définissant les divisions et panneaux", + "restartBehavior": "Comportement au redémarrage", + "restartBehaviorDesc": "Contrôle ce qui se passe lorsqu'un espace de travail du même nom existe déjà :", + "restartIgnore": "Basculer vers l'espace de travail existant (par défaut)", + "restartRecreate": "Fermer et recréer sans demander", + "restartConfirm": "Demander à l'utilisateur avant de recréer", + "layoutTree": "Arbre de mise en page", + "layoutTreeDesc": "L'arbre de mise en page définit comment les panneaux sont disposés à l'aide de nœuds de division récursifs :", + "splitNode": "Nœud de division", + "splitNodeDesc": "Divise l'espace en deux enfants :", + "or": "ou", + "splitPosition": "Position du séparateur de 0.1 à 0.9 (par défaut 0.5)", + "splitChildren": "Exactement deux nœuds enfants (division ou panneau)", + "paneNode": "Nœud de panneau", + "paneNodeDesc": "Un nœud feuille contenant une ou plusieurs surfaces (onglets dans le panneau).", + "surfaceDefinition": "Définition de surface", + "surfaceDefinitionDesc": "Chaque surface dans un panneau peut être un terminal ou un navigateur :", + "surfaceName": "Titre d'onglet personnalisé", + "surfaceCommand": "Commande shell à exécuter automatiquement à la création (terminal uniquement)", + "surfaceCwd": "Répertoire de travail pour cette surface", + "surfaceEnv": "Variables d'environnement sous forme de paires clé-valeur", + "surfaceUrl": "URL à ouvrir (navigateur uniquement)", + "surfaceFocus": "Focaliser cette surface après la création", + "cwdResolution": "Résolution du répertoire de travail", + "omitted": "omis", + "cwdRelative": "répertoire de travail de l'espace de travail", + "cwdSubdir": "relatif au répertoire de travail de l'espace de travail", + "cwdHome": "développé vers le répertoire personnel", + "absolutePath": "Chemin absolu", + "cwdAbsolute": "utilisé tel quel", + "fullExample": "Exemple complet" + }, "keyboardShortcuts": { "title": "Raccourcis clavier", "description": "Tous les raccourcis clavier disponibles dans cmux, classés par catégorie.", @@ -547,6 +611,7 @@ "gettingStarted": "Premiers pas", "concepts": "Concepts", "configuration": "Configuration", + "customCommands": "Commandes personnalisées", "keyboardShortcuts": "Raccourcis clavier", "apiReference": "Référence API", "browserAutomation": "Automatisation du navigateur", diff --git a/web/messages/it.json b/web/messages/it.json index b002fd816c..17162d8ce9 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -302,6 +302,70 @@ "exampleConfig": "Esempio di configurazione", "metaTitle": "Configurazione" }, + "customCommands": { + "title": "Comandi personalizzati", + "metaTitle": "Comandi personalizzati", + "metaDescription": "Definisci comandi personalizzati e layout workspace in cmux.json. Configurazione per progetto e globale con monitoraggio in tempo reale dei file.", + "intro": "Definisci comandi personalizzati e layout workspace aggiungendo un file cmux.json alla radice del progetto o ~/.config/cmux/. I comandi appaiono nella palette dei comandi.", + "fileLocations": "Posizioni dei file", + "fileLocationsDesc": "cmux cerca la configurazione in due posti:", + "localConfig": "Per progetto:", + "localConfigDesc": "si trova nella directory del progetto, ha la precedenza", + "globalConfig": "Globale:", + "globalConfigDesc": "si applica a tutti i progetti, integra i comandi non definiti localmente", + "precedenceNote": "I comandi locali sovrascrivono i comandi globali con lo stesso nome.", + "liveReload": "Le modifiche vengono rilevate automaticamente — nessun riavvio necessario.", + "schema": "Schema", + "schemaDesc": "Un file cmux.json contiene un array commands. Ogni comando è un semplice comando shell o una definizione completa di workspace:", + "simpleCommands": "Comandi semplici", + "simpleCommandsDesc": "Un comando semplice esegue un comando shell nel terminale attualmente attivo:", + "simpleCommandFields": "Campi", + "fieldName": "Visualizzato nella palette dei comandi (obbligatorio)", + "fieldDescription": "Descrizione opzionale", + "fieldKeywords": "Termini di ricerca aggiuntivi per la palette dei comandi", + "fieldCommand": "Comando shell da eseguire nel terminale attivo", + "fieldConfirm": "Mostra una finestra di conferma prima dell'esecuzione", + "simpleCommandCwdNote": "I comandi semplici vengono eseguiti nella directory di lavoro corrente del terminale focalizzato. Se il comando dipende da percorsi relativi al progetto, aggiungi il prefisso", + "simpleCommandCwdRepoRoot": "per eseguire dalla radice del repository, o", + "simpleCommandCwdCustomPath": "per qualsiasi directory specifica.", + "workspaceCommands": "Comandi workspace", + "workspaceCommandsDesc": "Un comando workspace crea un nuovo workspace con un layout personalizzato di divisioni, terminali e pannelli browser:", + "workspaceFields": "Campi workspace", + "wsFieldName": "Nome della scheda workspace (predefinito: nome del comando)", + "wsFieldCwd": "Directory di lavoro del workspace", + "wsFieldColor": "Colore della scheda workspace", + "wsFieldLayout": "Albero di layout che definisce divisioni e pannelli", + "restartBehavior": "Comportamento al riavvio", + "restartBehaviorDesc": "Controlla cosa succede quando esiste già un workspace con lo stesso nome:", + "restartIgnore": "Passa al workspace esistente (predefinito)", + "restartRecreate": "Chiudi e ricrea senza chiedere", + "restartConfirm": "Chiedi all'utente prima di ricreare", + "layoutTree": "Albero di layout", + "layoutTreeDesc": "L'albero di layout definisce come i pannelli sono disposti usando nodi di divisione ricorsivi:", + "splitNode": "Nodo di divisione", + "splitNodeDesc": "Divide lo spazio in due figli:", + "or": "o", + "splitPosition": "Posizione del divisore da 0.1 a 0.9 (predefinito 0.5)", + "splitChildren": "Esattamente due nodi figli (divisione o pannello)", + "paneNode": "Nodo pannello", + "paneNodeDesc": "Un nodo foglia contenente una o più superfici (schede all'interno del pannello).", + "surfaceDefinition": "Definizione superficie", + "surfaceDefinitionDesc": "Ogni superficie in un pannello può essere un terminale o un browser:", + "surfaceName": "Titolo scheda personalizzato", + "surfaceCommand": "Comando shell da eseguire automaticamente alla creazione (solo terminale)", + "surfaceCwd": "Directory di lavoro per questa superficie", + "surfaceEnv": "Variabili d'ambiente come coppie chiave-valore", + "surfaceUrl": "URL da aprire (solo browser)", + "surfaceFocus": "Metti il focus su questa superficie dopo la creazione", + "cwdResolution": "Risoluzione della directory di lavoro", + "omitted": "omesso", + "cwdRelative": "directory di lavoro del workspace", + "cwdSubdir": "relativa alla directory di lavoro del workspace", + "cwdHome": "espansa alla directory home", + "absolutePath": "Percorso assoluto", + "cwdAbsolute": "usato così com'è", + "fullExample": "Esempio completo" + }, "keyboardShortcuts": { "title": "Scorciatoie da tastiera", "description": "Tutte le scorciatoie da tastiera disponibili in cmux, raggruppate per categoria.", @@ -547,6 +611,7 @@ "gettingStarted": "Per iniziare", "concepts": "Concetti", "configuration": "Configurazione", + "customCommands": "Comandi personalizzati", "keyboardShortcuts": "Scorciatoie da tastiera", "apiReference": "Riferimento API", "browserAutomation": "Automazione del browser", diff --git a/web/messages/ja.json b/web/messages/ja.json index ea5d43515f..c8580277b7 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -302,6 +302,70 @@ "exampleConfig": "設定例", "metaTitle": "設定" }, + "customCommands": { + "title": "カスタムコマンド", + "metaTitle": "カスタムコマンド", + "metaDescription": "cmux.jsonでカスタムコマンドとワークスペースレイアウトを定義します。プロジェクトごとおよびグローバル設定とライブファイル監視に対応。", + "intro": "プロジェクトルートまたは ~/.config/cmux/ に cmux.json ファイルを追加してカスタムコマンドとワークスペースレイアウトを定義します。コマンドはコマンドパレットに表示されます。", + "fileLocations": "ファイルの場所", + "fileLocationsDesc": "cmux は2か所で設定を検索します:", + "localConfig": "プロジェクトごと:", + "localConfigDesc": "プロジェクトディレクトリに置かれ、優先されます", + "globalConfig": "グローバル:", + "globalConfigDesc": "すべてのプロジェクトに適用され、ローカルで未定義のコマンドを補完します", + "precedenceNote": "ローカルコマンドは同名のグローバルコマンドを上書きします。", + "liveReload": "変更は自動的に反映されます — 再起動は不要です。", + "schema": "スキーマ", + "schemaDesc": "cmux.json ファイルには commands 配列が含まれます。各コマンドはシンプルなシェルコマンドまたは完全なワークスペース定義です:", + "simpleCommands": "シンプルコマンド", + "simpleCommandsDesc": "シンプルコマンドは現在フォーカスされているターミナルでシェルコマンドを実行します:", + "simpleCommandFields": "フィールド", + "fieldName": "コマンドパレットに表示されます(必須)", + "fieldDescription": "任意の説明", + "fieldKeywords": "コマンドパレット用の追加検索キーワード", + "fieldCommand": "フォーカスされたターミナルで実行するシェルコマンド", + "fieldConfirm": "実行前に確認ダイアログを表示する", + "simpleCommandCwdNote": "シンプルコマンドはフォーカスされたターミナルの現在の作業ディレクトリで実行されます。プロジェクト相対パスに依存するコマンドの場合は、先頭に", + "simpleCommandCwdRepoRoot": "を付けてリポジトリのルートから実行するか、", + "simpleCommandCwdCustomPath": "で任意のディレクトリを指定できます。", + "workspaceCommands": "ワークスペースコマンド", + "workspaceCommandsDesc": "ワークスペースコマンドは、分割、ターミナル、ブラウザペインのカスタムレイアウトで新しいワークスペースを作成します:", + "workspaceFields": "ワークスペースフィールド", + "wsFieldName": "ワークスペースのタブ名(デフォルトはコマンド名)", + "wsFieldCwd": "ワークスペースの作業ディレクトリ", + "wsFieldColor": "ワークスペースのタブカラー", + "wsFieldLayout": "分割とペインを定義するレイアウトツリー", + "restartBehavior": "再起動の動作", + "restartBehaviorDesc": "同名のワークスペースが既に存在する場合の動作を制御します:", + "restartIgnore": "既存のワークスペースに切り替える(デフォルト)", + "restartRecreate": "確認なしに閉じて再作成する", + "restartConfirm": "再作成前にユーザーに確認する", + "layoutTree": "レイアウトツリー", + "layoutTreeDesc": "レイアウトツリーは、再帰的な分割ノードを使用してペインの配置を定義します:", + "splitNode": "分割ノード", + "splitNodeDesc": "スペースを2つの子に分割します:", + "or": "または", + "splitPosition": "分割位置(0.1〜0.9、デフォルト0.5)", + "splitChildren": "正確に2つの子ノード(分割またはペイン)", + "paneNode": "ペインノード", + "paneNodeDesc": "1つ以上のサーフェス(ペイン内のタブ)を含むリーフノード。", + "surfaceDefinition": "サーフェス定義", + "surfaceDefinitionDesc": "ペイン内の各サーフェスはターミナルまたはブラウザです:", + "surfaceName": "カスタムタブタイトル", + "surfaceCommand": "作成時に自動実行するシェルコマンド(ターミナルのみ)", + "surfaceCwd": "このサーフェスの作業ディレクトリ", + "surfaceEnv": "キーと値のペアとしての環境変数", + "surfaceUrl": "開くURL(ブラウザのみ)", + "surfaceFocus": "作成後にこのサーフェスにフォーカスする", + "cwdResolution": "作業ディレクトリの解決", + "omitted": "省略", + "cwdRelative": "ワークスペースの作業ディレクトリ", + "cwdSubdir": "ワークスペースの作業ディレクトリからの相対パス", + "cwdHome": "ホームディレクトリに展開", + "absolutePath": "絶対パス", + "cwdAbsolute": "そのまま使用", + "fullExample": "完全な例" + }, "keyboardShortcuts": { "title": "キーボードショートカット", "description": "cmuxで使用可能なすべてのキーボードショートカット(カテゴリ別)。", @@ -547,6 +611,7 @@ "gettingStarted": "はじめに", "concepts": "コンセプト", "configuration": "設定", + "customCommands": "カスタムコマンド", "keyboardShortcuts": "キーボードショートカット", "apiReference": "APIリファレンス", "browserAutomation": "ブラウザ自動化", diff --git a/web/messages/km.json b/web/messages/km.json index 128e214232..85792c5383 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -302,6 +302,70 @@ "exampleConfig": "ឧទាហរណ៍កំណត់រចនាសម្ព័ន្ធ", "metaTitle": "ការកំណត់រចនាសម្ព័ន្ធ" }, + "customCommands": { + "title": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន", + "metaTitle": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន", + "metaDescription": "កំណត់ពាក្យបញ្ជាផ្ទាល់ខ្លួននិងប្លង់ workspace ក្នុង cmux.json។ ការកំណត់រចនាសម្ព័ន្ធតាមគម្រោងនិងសាកល ជាមួយការតាមដានឯកសារដោយផ្ទាល់។", + "intro": "កំណត់ពាក្យបញ្ជាផ្ទាល់ខ្លួននិងប្លង់ workspace ដោយបន្ថែមឯកសារ cmux.json ទៅឫសគម្រោងរបស់អ្នក ឬ ~/.config/cmux/។ ពាក្យបញ្ជាលេចឡើងក្នុងបន្ទះពាក្យបញ្ជា។", + "fileLocations": "ទីតាំងឯកសារ", + "fileLocationsDesc": "cmux រកការកំណត់រចនាសម្ព័ន្ធនៅ២កន្លែង:", + "localConfig": "តាមគម្រោង:", + "localConfigDesc": "ស្ថិតក្នុងថតគម្រោងរបស់អ្នក, មានអាទិភាព", + "globalConfig": "សាកល:", + "globalConfigDesc": "អនុវត្តចំពោះគម្រោងទាំងអស់, បំពេញពាក្យបញ្ជាដែលមិនបានកំណត់ក្នុងតំបន់", + "precedenceNote": "ពាក្យបញ្ជាក្នុងតំបន់បដិសេធពាក្យបញ្ជាសាកលដែលមានឈ្មោះដូចគ្នា។", + "liveReload": "ការផ្លាស់ប្ដូរត្រូវបានទទួលស្គាល់ដោយស្វ័យប្រវត្តិ — មិនចាំបាច់ចាប់ផ្ដើមឡើងវិញ។", + "schema": "ស្គីម៉ា", + "schemaDesc": "ឯកសារ cmux.json មានអារ៉េ commands។ ពាក្យបញ្ជានីមួយៗគឺជាពាក្យបញ្ជា shell សាមញ្ញ ឬនិយាមព workspace ពេញលេញ:", + "simpleCommands": "ពាក្យបញ្ជាសាមញ្ញ", + "simpleCommandsDesc": "ពាក្យបញ្ជាសាមញ្ញដំណើរការពាក្យបញ្ជា shell ក្នុងទែមីណាល​ដែលបានផ្ដោតបច្ចុប្បន្ន:", + "simpleCommandFields": "វាល", + "fieldName": "បង្ហាញក្នុងបន្ទះពាក្យបញ្ជា (ចាំបាច់)", + "fieldDescription": "ការពិពណ៌នាជាជម្រើស", + "fieldKeywords": "ពាក្យស្វែងរកបន្ថែមសម្រាប់បន្ទះពាក្យបញ្ជា", + "fieldCommand": "ពាក្យបញ្ជា shell ដំណើរការក្នុងទែមីណាល​ដែលបានផ្ដោត", + "fieldConfirm": "បង្ហាញប្រអប់បញ្ជាក់មុននឹងដំណើរការ", + "simpleCommandCwdNote": "ពាក្យបញ្ជាសាមញ្ញដំណើរការក្នុងថតការងារបច្ចុប្បន្នរបស់ terminal ដែលកំពុងផ្ដោត។ ប្រសិនបើពាក្យបញ្ជារបស់អ្នកពឹងផ្អែកលើផ្លូវទាក់ទងនឹងគម្រោង សូមបន្ថែមពីមុខ", + "simpleCommandCwdRepoRoot": "ដើម្បីដំណើរការពីឫសនៃ repo ឬ", + "simpleCommandCwdCustomPath": "សម្រាប់ថតណាមួយជាក់លាក់។", + "workspaceCommands": "ពាក្យបញ្ជា workspace", + "workspaceCommandsDesc": "ពាក្យបញ្ជា workspace បង្កើត workspace ថ្មីជាមួយប្លង់ផ្ទាល់ខ្លួននៃការបំបែក, ទែមីណាល, និងបន្ទះកម្មវិធីរុករក:", + "workspaceFields": "វាល workspace", + "wsFieldName": "ឈ្មោះផ្ទាំង workspace (លំនាំដើមគឺឈ្មោះពាក្យបញ្ជា)", + "wsFieldCwd": "ថតការងារសម្រាប់ workspace", + "wsFieldColor": "ពណ៌ផ្ទាំង workspace", + "wsFieldLayout": "ដើមប្លង់ដែលកំណត់ការបំបែកនិងបន្ទះ", + "restartBehavior": "ឥរិយាបថចាប់ផ្ដើមឡើងវិញ", + "restartBehaviorDesc": "គ្រប់គ្រងអ្វីដែលកើតឡើងនៅពេល workspace ដែលមានឈ្មោះដូចគ្នារួចមានស្រាប់:", + "restartIgnore": "ប្ដូរទៅ workspace ដែលមានស្រាប់ (លំនាំដើម)", + "restartRecreate": "បិទហើយបង្កើតឡើងវិញដោយមិនសួរ", + "restartConfirm": "សួរអ្នកប្រើប្រាស់មុននឹងបង្កើតឡើងវិញ", + "layoutTree": "ដើមប្លង់", + "layoutTreeDesc": "ដើមប្លង់កំណត់របៀបដែលបន្ទះត្រូវបានរៀបចំដោយប្រើថ្នាំងការបំបែករៀងគ្នា:", + "splitNode": "ថ្នាំងការបំបែក", + "splitNodeDesc": "ចែកចន្លោះទៅជាកូន២:", + "or": "ឬ", + "splitPosition": "ទីតាំងខ្សែបំបែកពី 0.1 ដល់ 0.9 (លំនាំដើម 0.5)", + "splitChildren": "ថ្នាំងកូនពិតប្រាកដ២ (ការបំបែក ឬបន្ទះ)", + "paneNode": "ថ្នាំងបន្ទះ", + "paneNodeDesc": "ថ្នាំងស្លឹកមួយដែលមាន surface មួយ ឬច្រើន (ផ្ទាំងនៅក្នុងបន្ទះ)។", + "surfaceDefinition": "និយាម surface", + "surfaceDefinitionDesc": "surface នីមួយៗក្នុងបន្ទះអាចជាទែមីណាល​ ឬកម្មវិធីរុករក:", + "surfaceName": "ចំណងជើងផ្ទាំងផ្ទាល់ខ្លួន", + "surfaceCommand": "ពាក្យបញ្ជា shell ដំណើរការដោយស្វ័យប្រវត្តិពេលបង្កើត (ទែមីណាល​តែប៉ុណ្ណោះ)", + "surfaceCwd": "ថតការងារសម្រាប់ surface នេះ", + "surfaceEnv": "អថេរបរិស្ថានជាគូ key-value", + "surfaceUrl": "URL ដែលត្រូវបើក (កម្មវិធីរុករកតែប៉ុណ្ណោះ)", + "surfaceFocus": "ផ្ដោតលើ surface នេះបន្ទាប់ពីបង្កើត", + "cwdResolution": "ការដោះស្រាយថតការងារ", + "omitted": "លុបចោល", + "cwdRelative": "ថតការងារ workspace", + "cwdSubdir": "ទាក់ទងនឹងថតការងារ workspace", + "cwdHome": "ពង្រីកទៅថតផ្ទះ", + "absolutePath": "ផ្លូវដាច់ខាត", + "cwdAbsolute": "ប្រើដូចដែលមាន", + "fullExample": "ឧទាហរណ៍ពេញលេញ" + }, "keyboardShortcuts": { "title": "ផ្លូវកាត់ក្ដារចុច", "description": "ផ្លូវកាត់ក្ដារចុចទាំងអស់ដែលមានក្នុង cmux, ដាក់ជាក្រុមតាមប្រភេទ។", @@ -547,6 +611,7 @@ "gettingStarted": "ចាប់ផ្ដើម", "concepts": "គោលគំនិត", "configuration": "កំណត់រចនាសម្ព័ន្ធ", + "customCommands": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន", "keyboardShortcuts": "ផ្លូវកាត់ក្ដារចុច", "apiReference": "ឯកសារយោង API", "browserAutomation": "ស្វ័យប្រវត្តិកម្មកម្មវិធីរុករក", diff --git a/web/messages/ko.json b/web/messages/ko.json index 156534fd9f..9cc1408151 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -302,6 +302,70 @@ "exampleConfig": "예시 설정", "metaTitle": "설정" }, + "customCommands": { + "title": "사용자 정의 명령어", + "metaTitle": "사용자 정의 명령어", + "metaDescription": "cmux.json에서 사용자 정의 명령어와 워크스페이스 레이아웃을 정의합니다. 실시간 파일 감시와 함께 프로젝트별 및 전역 설정을 지원합니다.", + "intro": "프로젝트 루트 또는 ~/.config/cmux/에 cmux.json 파일을 추가하여 사용자 정의 명령어와 워크스페이스 레이아웃을 정의합니다. 명령어는 명령어 팔레트에 표시됩니다.", + "fileLocations": "파일 위치", + "fileLocationsDesc": "cmux는 두 곳에서 설정을 찾습니다:", + "localConfig": "프로젝트별:", + "localConfigDesc": "프로젝트 디렉터리에 위치하며 우선순위를 가집니다", + "globalConfig": "전역:", + "globalConfigDesc": "모든 프로젝트에 적용되며 로컬에서 정의되지 않은 명령어를 보완합니다", + "precedenceNote": "로컬 명령어는 동일한 이름의 전역 명령어를 덮어씁니다.", + "liveReload": "변경 사항은 자동으로 반영됩니다 — 재시작이 필요 없습니다.", + "schema": "스키마", + "schemaDesc": "cmux.json 파일에는 commands 배열이 포함됩니다. 각 명령어는 단순한 셸 명령어이거나 완전한 워크스페이스 정의입니다:", + "simpleCommands": "단순 명령어", + "simpleCommandsDesc": "단순 명령어는 현재 포커스된 터미널에서 셸 명령어를 실행합니다:", + "simpleCommandFields": "필드", + "fieldName": "명령어 팔레트에 표시됩니다 (필수)", + "fieldDescription": "선택적 설명", + "fieldKeywords": "명령어 팔레트용 추가 검색어", + "fieldCommand": "포커스된 터미널에서 실행할 셸 명령어", + "fieldConfirm": "실행 전 확인 대화상자 표시", + "simpleCommandCwdNote": "단순 명령어는 포커스된 터미널의 현재 작업 디렉토리에서 실행됩니다. 프로젝트 상대 경로에 의존하는 명령어의 경우 앞에", + "simpleCommandCwdRepoRoot": "를 붙여 저장소 루트에서 실행하거나", + "simpleCommandCwdCustomPath": "로 특정 디렉토리를 지정할 수 있습니다.", + "workspaceCommands": "워크스페이스 명령어", + "workspaceCommandsDesc": "워크스페이스 명령어는 분할, 터미널, 브라우저 패널의 사용자 정의 레이아웃으로 새 워크스페이스를 만듭니다:", + "workspaceFields": "워크스페이스 필드", + "wsFieldName": "워크스페이스 탭 이름 (기본값은 명령어 이름)", + "wsFieldCwd": "워크스페이스의 작업 디렉터리", + "wsFieldColor": "워크스페이스 탭 색상", + "wsFieldLayout": "분할과 패널을 정의하는 레이아웃 트리", + "restartBehavior": "재시작 동작", + "restartBehaviorDesc": "동일한 이름의 워크스페이스가 이미 존재할 때 발생하는 동작을 제어합니다:", + "restartIgnore": "기존 워크스페이스로 전환 (기본값)", + "restartRecreate": "묻지 않고 닫고 재생성", + "restartConfirm": "재생성 전 사용자에게 확인", + "layoutTree": "레이아웃 트리", + "layoutTreeDesc": "레이아웃 트리는 재귀적인 분할 노드를 사용하여 패널 배치를 정의합니다:", + "splitNode": "분할 노드", + "splitNodeDesc": "공간을 두 개의 자식으로 나눕니다:", + "or": "또는", + "splitPosition": "0.1에서 0.9 사이의 분할기 위치 (기본값 0.5)", + "splitChildren": "정확히 두 개의 자식 노드 (분할 또는 패널)", + "paneNode": "패널 노드", + "paneNodeDesc": "하나 이상의 서피스(패널 내 탭)를 포함하는 리프 노드.", + "surfaceDefinition": "서피스 정의", + "surfaceDefinitionDesc": "패널 내 각 서피스는 터미널 또는 브라우저가 될 수 있습니다:", + "surfaceName": "사용자 정의 탭 제목", + "surfaceCommand": "생성 시 자동 실행할 셸 명령어 (터미널 전용)", + "surfaceCwd": "이 서피스의 작업 디렉터리", + "surfaceEnv": "키-값 쌍으로 된 환경 변수", + "surfaceUrl": "열 URL (브라우저 전용)", + "surfaceFocus": "생성 후 이 서피스에 포커스", + "cwdResolution": "작업 디렉터리 해석", + "omitted": "생략됨", + "cwdRelative": "워크스페이스 작업 디렉터리", + "cwdSubdir": "워크스페이스 작업 디렉터리 기준 상대 경로", + "cwdHome": "홈 디렉터리로 확장", + "absolutePath": "절대 경로", + "cwdAbsolute": "있는 그대로 사용", + "fullExample": "전체 예시" + }, "keyboardShortcuts": { "title": "키보드 단축키", "description": "카테고리별로 정리된 cmux의 모든 키보드 단축키.", @@ -547,6 +611,7 @@ "gettingStarted": "시작하기", "concepts": "개념", "configuration": "설정", + "customCommands": "사용자 정의 명령어", "keyboardShortcuts": "키보드 단축키", "apiReference": "API 레퍼런스", "browserAutomation": "브라우저 자동화", diff --git a/web/messages/no.json b/web/messages/no.json index b289095e64..84eedb6715 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -302,6 +302,70 @@ "exampleConfig": "Eksempelkonfigurasjon", "metaTitle": "Konfigurasjon" }, + "customCommands": { + "title": "Egendefinerte kommandoer", + "metaTitle": "Egendefinerte kommandoer", + "metaDescription": "Definer egendefinerte kommandoer og workspace-oppsett i cmux.json. Per-prosjekt og global konfigurasjon med live filovervåking.", + "intro": "Definer egendefinerte kommandoer og workspace-oppsett ved å legge til en cmux.json-fil i prosjektets rotmappe eller ~/.config/cmux/. Kommandoer vises i kommandopaletten.", + "fileLocations": "Filplasseringer", + "fileLocationsDesc": "cmux søker etter konfigurasjon to steder:", + "localConfig": "Per prosjekt:", + "localConfigDesc": "ligger i prosjektmappen din, har forrang", + "globalConfig": "Global:", + "globalConfigDesc": "gjelder alle prosjekter, fyller inn kommandoer som ikke er definert lokalt", + "precedenceNote": "Lokale kommandoer overstyrer globale kommandoer med samme navn.", + "liveReload": "Endringer hentes automatisk — ingen omstart nødvendig.", + "schema": "Skjema", + "schemaDesc": "En cmux.json-fil inneholder en commands-array. Hver kommando er enten en enkel shell-kommando eller en fullstendig workspace-definisjon:", + "simpleCommands": "Enkle kommandoer", + "simpleCommandsDesc": "En enkel kommando kjører en shell-kommando i den for øyeblikket fokuserte terminalen:", + "simpleCommandFields": "Felter", + "fieldName": "Vises i kommandopaletten (påkrevd)", + "fieldDescription": "Valgfri beskrivelse", + "fieldKeywords": "Ekstra søkeord for kommandopaletten", + "fieldCommand": "Shell-kommando som kjøres i den fokuserte terminalen", + "fieldConfirm": "Vis en bekreftelsesdialog før kjøring", + "simpleCommandCwdNote": "Enkle kommandoer kjøres i den fokuserte terminalens gjeldende arbeidskatalog. Hvis kommandoen din avhenger av prosjektrelative stier, legg til prefiks", + "simpleCommandCwdRepoRoot": "for å kjøre fra repo-roten, eller", + "simpleCommandCwdCustomPath": "for en hvilken som helst spesifikk katalog.", + "workspaceCommands": "Workspace-kommandoer", + "workspaceCommandsDesc": "En workspace-kommando oppretter et nytt workspace med et egendefinert oppsett av delinger, terminaler og nettleserpaneler:", + "workspaceFields": "Workspace-felter", + "wsFieldName": "Workspace-fanenavn (standard er kommandonavn)", + "wsFieldCwd": "Arbeidsmappe for workspacet", + "wsFieldColor": "Farge på workspace-fanen", + "wsFieldLayout": "Oppsettstre som definerer delinger og paneler", + "restartBehavior": "Omstartsatferd", + "restartBehaviorDesc": "Styrer hva som skjer når et workspace med samme navn allerede eksisterer:", + "restartIgnore": "Bytt til det eksisterende workspacet (standard)", + "restartRecreate": "Lukk og gjenopprett uten å spørre", + "restartConfirm": "Spør brukeren før gjenoppretting", + "layoutTree": "Oppsettstre", + "layoutTreeDesc": "Oppsettstreet definerer hvordan paneler er arrangert ved hjelp av rekursive delingsknuter:", + "splitNode": "Delingsknute", + "splitNodeDesc": "Deler plassen i to barn:", + "or": "eller", + "splitPosition": "Delerposisjon fra 0.1 til 0.9 (standard 0.5)", + "splitChildren": "Nøyaktig to barneknuter (deling eller panel)", + "paneNode": "Panelknute", + "paneNodeDesc": "En bladknute som inneholder én eller flere overflater (faner innen panelet).", + "surfaceDefinition": "Overflatedefinisjon", + "surfaceDefinitionDesc": "Hver overflate i et panel kan være en terminal eller en nettleser:", + "surfaceName": "Egendefinert fanetittel", + "surfaceCommand": "Shell-kommando som kjøres automatisk ved opprettelse (kun terminal)", + "surfaceCwd": "Arbeidsmappe for denne overflaten", + "surfaceEnv": "Miljøvariabler som nøkkel-verdi-par", + "surfaceUrl": "URL som skal åpnes (kun nettleser)", + "surfaceFocus": "Fokuser på denne overflaten etter opprettelse", + "cwdResolution": "Oppløsning av arbeidsmappe", + "omitted": "utelatt", + "cwdRelative": "workspace-arbeidsmappe", + "cwdSubdir": "relativ til workspace-arbeidsmappen", + "cwdHome": "utvidet til hjemmemappen", + "absolutePath": "Absolutt sti", + "cwdAbsolute": "brukes som den er", + "fullExample": "Fullstendig eksempel" + }, "keyboardShortcuts": { "title": "Tastatursnarveier", "description": "Alle tastatursnarveier tilgjengelige i cmux, gruppert etter kategori.", @@ -547,6 +611,7 @@ "gettingStarted": "Kom i gang", "concepts": "Konsepter", "configuration": "Konfigurasjon", + "customCommands": "Egendefinerte kommandoer", "keyboardShortcuts": "Tastatursnarveier", "apiReference": "API-referanse", "browserAutomation": "Nettleserautomatisering", diff --git a/web/messages/pl.json b/web/messages/pl.json index bb52ccbf56..8b505ed958 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -302,6 +302,70 @@ "exampleConfig": "Przykładowa konfiguracja", "metaTitle": "Konfiguracja" }, + "customCommands": { + "title": "Niestandardowe polecenia", + "metaTitle": "Niestandardowe polecenia", + "metaDescription": "Definiuj niestandardowe polecenia i układy workspace w cmux.json. Konfiguracja per-projekt i globalna z obserwacją plików na żywo.", + "intro": "Definiuj niestandardowe polecenia i układy workspace, dodając plik cmux.json do katalogu głównego projektu lub ~/.config/cmux/. Polecenia pojawiają się w palecie poleceń.", + "fileLocations": "Lokalizacje plików", + "fileLocationsDesc": "cmux szuka konfiguracji w dwóch miejscach:", + "localConfig": "Per projekt:", + "localConfigDesc": "znajduje się w katalogu projektu, ma pierwszeństwo", + "globalConfig": "Globalnie:", + "globalConfigDesc": "stosuje się do wszystkich projektów, uzupełnia polecenia niezdefiniowane lokalnie", + "precedenceNote": "Lokalne polecenia zastępują globalne polecenia o tej samej nazwie.", + "liveReload": "Zmiany są pobierane automatycznie — nie trzeba restartować.", + "schema": "Schemat", + "schemaDesc": "Plik cmux.json zawiera tablicę commands. Każde polecenie to albo proste polecenie shell, albo pełna definicja workspace:", + "simpleCommands": "Proste polecenia", + "simpleCommandsDesc": "Proste polecenie uruchamia polecenie shell w aktualnie sfokusowanym terminalu:", + "simpleCommandFields": "Pola", + "fieldName": "Wyświetlane w palecie poleceń (wymagane)", + "fieldDescription": "Opcjonalny opis", + "fieldKeywords": "Dodatkowe terminy wyszukiwania dla palety poleceń", + "fieldCommand": "Polecenie shell do uruchomienia w sfokusowanym terminalu", + "fieldConfirm": "Pokaż okno dialogowe potwierdzenia przed uruchomieniem", + "simpleCommandCwdNote": "Proste polecenia są uruchamiane w bieżącym katalogu roboczym aktywnego terminala. Jeśli polecenie wymaga ścieżek względnych do projektu, dodaj prefiks", + "simpleCommandCwdRepoRoot": "aby uruchomić z katalogu głównego repozytorium, lub", + "simpleCommandCwdCustomPath": "dla dowolnego konkretnego katalogu.", + "workspaceCommands": "Polecenia workspace", + "workspaceCommandsDesc": "Polecenie workspace tworzy nowy workspace z niestandardowym układem podziałów, terminali i paneli przeglądarki:", + "workspaceFields": "Pola workspace", + "wsFieldName": "Nazwa zakładki workspace (domyślnie nazwa polecenia)", + "wsFieldCwd": "Katalog roboczy workspace", + "wsFieldColor": "Kolor zakładki workspace", + "wsFieldLayout": "Drzewo układu definiujące podziały i panele", + "restartBehavior": "Zachowanie przy restarcie", + "restartBehaviorDesc": "Kontroluje co się dzieje, gdy workspace o tej samej nazwie już istnieje:", + "restartIgnore": "Przełącz do istniejącego workspace (domyślnie)", + "restartRecreate": "Zamknij i odtwórz bez pytania", + "restartConfirm": "Zapytaj użytkownika przed odtworzeniem", + "layoutTree": "Drzewo układu", + "layoutTreeDesc": "Drzewo układu definiuje jak panele są rozmieszczone za pomocą rekurencyjnych węzłów podziału:", + "splitNode": "Węzeł podziału", + "splitNodeDesc": "Dzieli przestrzeń na dwoje dzieci:", + "or": "lub", + "splitPosition": "Pozycja dzielnika od 0.1 do 0.9 (domyślnie 0.5)", + "splitChildren": "Dokładnie dwa węzły podrzędne (podział lub panel)", + "paneNode": "Węzeł panelu", + "paneNodeDesc": "Węzeł liścia zawierający jedną lub więcej powierzchni (zakładki wewnątrz panelu).", + "surfaceDefinition": "Definicja powierzchni", + "surfaceDefinitionDesc": "Każda powierzchnia w panelu może być terminalem lub przeglądarką:", + "surfaceName": "Niestandardowy tytuł zakładki", + "surfaceCommand": "Polecenie shell do automatycznego uruchomienia przy tworzeniu (tylko terminal)", + "surfaceCwd": "Katalog roboczy dla tej powierzchni", + "surfaceEnv": "Zmienne środowiskowe jako pary klucz-wartość", + "surfaceUrl": "URL do otwarcia (tylko przeglądarka)", + "surfaceFocus": "Sfokusuj tę powierzchnię po utworzeniu", + "cwdResolution": "Rozwiązanie katalogu roboczego", + "omitted": "pominięty", + "cwdRelative": "katalog roboczy workspace", + "cwdSubdir": "względem katalogu roboczego workspace", + "cwdHome": "rozwinięty do katalogu domowego", + "absolutePath": "Ścieżka bezwzględna", + "cwdAbsolute": "używana bez zmian", + "fullExample": "Pełny przykład" + }, "keyboardShortcuts": { "title": "Skróty klawiszowe", "description": "Wszystkie skróty klawiszowe dostępne w cmux, pogrupowane według kategorii.", @@ -547,6 +611,7 @@ "gettingStarted": "Szybki start", "concepts": "Koncepty", "configuration": "Konfiguracja", + "customCommands": "Niestandardowe polecenia", "keyboardShortcuts": "Skróty klawiszowe", "apiReference": "Dokumentacja API", "browserAutomation": "Automatyzacja przeglądarki", diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 8b2b65f321..340875a156 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -302,6 +302,70 @@ "exampleConfig": "Exemplo de configuração", "metaTitle": "Configuração" }, + "customCommands": { + "title": "Comandos personalizados", + "metaTitle": "Comandos personalizados", + "metaDescription": "Defina comandos personalizados e layouts de workspace em cmux.json. Configuração por projeto e global com monitoramento em tempo real de arquivos.", + "intro": "Defina comandos personalizados e layouts de workspace adicionando um arquivo cmux.json à raiz do seu projeto ou ~/.config/cmux/. Os comandos aparecem na paleta de comandos.", + "fileLocations": "Localizações dos arquivos", + "fileLocationsDesc": "O cmux procura configuração em dois lugares:", + "localConfig": "Por projeto:", + "localConfigDesc": "fica no diretório do seu projeto, tem precedência", + "globalConfig": "Global:", + "globalConfigDesc": "aplica-se a todos os projetos, preenche comandos não definidos localmente", + "precedenceNote": "Comandos locais substituem comandos globais com o mesmo nome.", + "liveReload": "As alterações são capturadas automaticamente — nenhum reinício necessário.", + "schema": "Esquema", + "schemaDesc": "Um arquivo cmux.json contém um array commands. Cada comando é um comando shell simples ou uma definição completa de workspace:", + "simpleCommands": "Comandos simples", + "simpleCommandsDesc": "Um comando simples executa um comando shell no terminal atualmente focado:", + "simpleCommandFields": "Campos", + "fieldName": "Exibido na paleta de comandos (obrigatório)", + "fieldDescription": "Descrição opcional", + "fieldKeywords": "Termos de pesquisa extras para a paleta de comandos", + "fieldCommand": "Comando shell para executar no terminal focado", + "fieldConfirm": "Mostrar um diálogo de confirmação antes de executar", + "simpleCommandCwdNote": "Comandos simples são executados no diretório de trabalho atual do terminal focado. Se seu comando depende de caminhos relativos ao projeto, prefixe com", + "simpleCommandCwdRepoRoot": "para executar a partir da raiz do repositório, ou", + "simpleCommandCwdCustomPath": "para qualquer diretório específico.", + "workspaceCommands": "Comandos de workspace", + "workspaceCommandsDesc": "Um comando de workspace cria um novo workspace com um layout personalizado de divisões, terminais e painéis do navegador:", + "workspaceFields": "Campos de workspace", + "wsFieldName": "Nome da aba do workspace (padrão é o nome do comando)", + "wsFieldCwd": "Diretório de trabalho do workspace", + "wsFieldColor": "Cor da aba do workspace", + "wsFieldLayout": "Árvore de layout definindo divisões e painéis", + "restartBehavior": "Comportamento de reinício", + "restartBehaviorDesc": "Controla o que acontece quando um workspace com o mesmo nome já existe:", + "restartIgnore": "Mudar para o workspace existente (padrão)", + "restartRecreate": "Fechar e recriar sem perguntar", + "restartConfirm": "Perguntar ao usuário antes de recriar", + "layoutTree": "Árvore de layout", + "layoutTreeDesc": "A árvore de layout define como os painéis são organizados usando nós de divisão recursivos:", + "splitNode": "Nó de divisão", + "splitNodeDesc": "Divide o espaço em dois filhos:", + "or": "ou", + "splitPosition": "Posição do divisor de 0.1 a 0.9 (padrão 0.5)", + "splitChildren": "Exatamente dois nós filhos (divisão ou painel)", + "paneNode": "Nó de painel", + "paneNodeDesc": "Um nó folha contendo uma ou mais superfícies (abas dentro do painel).", + "surfaceDefinition": "Definição de superfície", + "surfaceDefinitionDesc": "Cada superfície em um painel pode ser um terminal ou um navegador:", + "surfaceName": "Título de aba personalizado", + "surfaceCommand": "Comando shell para executar automaticamente na criação (apenas terminal)", + "surfaceCwd": "Diretório de trabalho para esta superfície", + "surfaceEnv": "Variáveis de ambiente como pares chave-valor", + "surfaceUrl": "URL para abrir (apenas navegador)", + "surfaceFocus": "Focar nesta superfície após a criação", + "cwdResolution": "Resolução do diretório de trabalho", + "omitted": "omitido", + "cwdRelative": "diretório de trabalho do workspace", + "cwdSubdir": "relativo ao diretório de trabalho do workspace", + "cwdHome": "expandido para o diretório home", + "absolutePath": "Caminho absoluto", + "cwdAbsolute": "usado como está", + "fullExample": "Exemplo completo" + }, "keyboardShortcuts": { "title": "Atalhos de Teclado", "description": "Todos os atalhos de teclado disponíveis no cmux, agrupados por categoria.", @@ -547,6 +611,7 @@ "gettingStarted": "Primeiros Passos", "concepts": "Conceitos", "configuration": "Configuração", + "customCommands": "Comandos personalizados", "keyboardShortcuts": "Atalhos de Teclado", "apiReference": "Referência da API", "browserAutomation": "Automação do Navegador", diff --git a/web/messages/ru.json b/web/messages/ru.json index ed2c38c2a7..6dff086105 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -302,6 +302,70 @@ "exampleConfig": "Пример конфигурации", "metaTitle": "Конфигурация" }, + "customCommands": { + "title": "Пользовательские команды", + "metaTitle": "Пользовательские команды", + "metaDescription": "Определяйте пользовательские команды и макеты рабочего пространства в cmux.json. Конфигурация для каждого проекта и глобальная с живым отслеживанием файлов.", + "intro": "Определяйте пользовательские команды и макеты рабочего пространства, добавив файл cmux.json в корень вашего проекта или ~/.config/cmux/. Команды отображаются в палитре команд.", + "fileLocations": "Расположение файлов", + "fileLocationsDesc": "cmux ищет конфигурацию в двух местах:", + "localConfig": "Для проекта:", + "localConfigDesc": "находится в каталоге проекта, имеет приоритет", + "globalConfig": "Глобально:", + "globalConfigDesc": "применяется ко всем проектам, дополняет команды, не определённые локально", + "precedenceNote": "Локальные команды переопределяют глобальные команды с тем же именем.", + "liveReload": "Изменения применяются автоматически — перезапуск не требуется.", + "schema": "Схема", + "schemaDesc": "Файл cmux.json содержит массив commands. Каждая команда — это либо простая команда shell, либо полное определение рабочего пространства:", + "simpleCommands": "Простые команды", + "simpleCommandsDesc": "Простая команда выполняет команду shell в текущем сфокусированном терминале:", + "simpleCommandFields": "Поля", + "fieldName": "Отображается в палитре команд (обязательно)", + "fieldDescription": "Необязательное описание", + "fieldKeywords": "Дополнительные поисковые термины для палитры команд", + "fieldCommand": "Команда shell для выполнения в сфокусированном терминале", + "fieldConfirm": "Показать диалог подтверждения перед выполнением", + "simpleCommandCwdNote": "Простые команды выполняются в текущей рабочей директории активного терминала. Если команда использует пути относительно проекта, добавьте префикс", + "simpleCommandCwdRepoRoot": "для запуска из корня репозитория, или", + "simpleCommandCwdCustomPath": "для любой конкретной директории.", + "workspaceCommands": "Команды рабочего пространства", + "workspaceCommandsDesc": "Команда рабочего пространства создаёт новое рабочее пространство с настраиваемым макетом из разделений, терминалов и панелей браузера:", + "workspaceFields": "Поля рабочего пространства", + "wsFieldName": "Имя вкладки рабочего пространства (по умолчанию — имя команды)", + "wsFieldCwd": "Рабочий каталог для рабочего пространства", + "wsFieldColor": "Цвет вкладки рабочего пространства", + "wsFieldLayout": "Дерево макета, определяющее разделения и панели", + "restartBehavior": "Поведение при перезапуске", + "restartBehaviorDesc": "Управляет тем, что происходит, когда рабочее пространство с тем же именем уже существует:", + "restartIgnore": "Переключиться на существующее рабочее пространство (по умолчанию)", + "restartRecreate": "Закрыть и пересоздать без запроса", + "restartConfirm": "Спросить пользователя перед пересозданием", + "layoutTree": "Дерево макета", + "layoutTreeDesc": "Дерево макета определяет расположение панелей с помощью рекурсивных узлов разделения:", + "splitNode": "Узел разделения", + "splitNodeDesc": "Делит пространство на двух потомков:", + "or": "или", + "splitPosition": "Положение разделителя от 0.1 до 0.9 (по умолчанию 0.5)", + "splitChildren": "Ровно два дочерних узла (разделение или панель)", + "paneNode": "Узел панели", + "paneNodeDesc": "Листовой узел, содержащий одну или несколько поверхностей (вкладки внутри панели).", + "surfaceDefinition": "Определение поверхности", + "surfaceDefinitionDesc": "Каждая поверхность в панели может быть терминалом или браузером:", + "surfaceName": "Пользовательский заголовок вкладки", + "surfaceCommand": "Команда shell для автоматического запуска при создании (только терминал)", + "surfaceCwd": "Рабочий каталог для этой поверхности", + "surfaceEnv": "Переменные среды в виде пар ключ-значение", + "surfaceUrl": "URL для открытия (только браузер)", + "surfaceFocus": "Сфокусироваться на этой поверхности после создания", + "cwdResolution": "Определение рабочего каталога", + "omitted": "опущено", + "cwdRelative": "рабочий каталог рабочего пространства", + "cwdSubdir": "относительно рабочего каталога рабочего пространства", + "cwdHome": "расширено до домашнего каталога", + "absolutePath": "Абсолютный путь", + "cwdAbsolute": "используется как есть", + "fullExample": "Полный пример" + }, "keyboardShortcuts": { "title": "Горячие клавиши", "description": "Все горячие клавиши в cmux, сгруппированные по категориям.", @@ -547,6 +611,7 @@ "gettingStarted": "Начало работы", "concepts": "Концепции", "configuration": "Конфигурация", + "customCommands": "Пользовательские команды", "keyboardShortcuts": "Горячие клавиши", "apiReference": "Справочник API", "browserAutomation": "Автоматизация браузера", diff --git a/web/messages/th.json b/web/messages/th.json index 7627ba687c..1131353ec7 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -302,6 +302,70 @@ "exampleConfig": "ตัวอย่างคอนฟิก", "metaTitle": "การตั้งค่า" }, + "customCommands": { + "title": "คำสั่งที่กำหนดเอง", + "metaTitle": "คำสั่งที่กำหนดเอง", + "metaDescription": "กำหนดคำสั่งที่กำหนดเองและเลย์เอาต์ workspace ใน cmux.json รองรับการตั้งค่าแบบต่อโปรเจกต์และแบบทั่วไปพร้อมการตรวจสอบไฟล์แบบเรียลไทม์", + "intro": "กำหนดคำสั่งที่กำหนดเองและเลย์เอาต์ workspace โดยเพิ่มไฟล์ cmux.json ลงในรากโปรเจกต์หรือ ~/.config/cmux/ คำสั่งจะปรากฏในแผงคำสั่ง", + "fileLocations": "ตำแหน่งไฟล์", + "fileLocationsDesc": "cmux ค้นหาการตั้งค่าจากสองที่:", + "localConfig": "ต่อโปรเจกต์:", + "localConfigDesc": "อยู่ในไดเรกทอรีโปรเจกต์ของคุณ มีความสำคัญสูงกว่า", + "globalConfig": "ทั่วไป:", + "globalConfigDesc": "ใช้กับทุกโปรเจกต์ เติมคำสั่งที่ยังไม่ได้กำหนดในท้องถิ่น", + "precedenceNote": "คำสั่งท้องถิ่นจะแทนที่คำสั่งทั่วไปที่มีชื่อเดียวกัน", + "liveReload": "การเปลี่ยนแปลงจะถูกรับโดยอัตโนมัติ — ไม่จำเป็นต้องรีสตาร์ท", + "schema": "สคีมา", + "schemaDesc": "ไฟล์ cmux.json มีอาร์เรย์ commands แต่ละคำสั่งเป็นคำสั่ง shell แบบง่ายหรือนิยาม workspace แบบเต็ม:", + "simpleCommands": "คำสั่งแบบง่าย", + "simpleCommandsDesc": "คำสั่งแบบง่ายรันคำสั่ง shell ในเทอร์มินัลที่กำลังโฟกัสอยู่:", + "simpleCommandFields": "ฟิลด์", + "fieldName": "แสดงในแผงคำสั่ง (จำเป็น)", + "fieldDescription": "คำอธิบายเพิ่มเติม (ไม่บังคับ)", + "fieldKeywords": "คำค้นหาเพิ่มเติมสำหรับแผงคำสั่ง", + "fieldCommand": "คำสั่ง shell ที่จะรันในเทอร์มินัลที่โฟกัส", + "fieldConfirm": "แสดงกล่องโต้ตอบยืนยันก่อนรัน", + "simpleCommandCwdNote": "คำสั่งแบบง่ายจะรันในไดเรกทอรีการทำงานปัจจุบันของเทอร์มินัลที่โฟกัสอยู่ หากคำสั่งของคุณใช้พาธสัมพัทธ์กับโปรเจกต์ ให้เพิ่มนำหน้าด้วย", + "simpleCommandCwdRepoRoot": "เพื่อรันจากรูทของ repo หรือ", + "simpleCommandCwdCustomPath": "สำหรับไดเรกทอรีที่ต้องการ", + "workspaceCommands": "คำสั่ง workspace", + "workspaceCommandsDesc": "คำสั่ง workspace สร้าง workspace ใหม่ที่มีเลย์เอาต์กำหนดเองของการแบ่ง เทอร์มินัล และแผงเบราว์เซอร์:", + "workspaceFields": "ฟิลด์ workspace", + "wsFieldName": "ชื่อแท็บ workspace (ค่าเริ่มต้นคือชื่อคำสั่ง)", + "wsFieldCwd": "ไดเรกทอรีทำงานสำหรับ workspace", + "wsFieldColor": "สีแท็บ workspace", + "wsFieldLayout": "ต้นไม้เลย์เอาต์ที่กำหนดการแบ่งและแผง", + "restartBehavior": "พฤติกรรมการรีสตาร์ท", + "restartBehaviorDesc": "ควบคุมสิ่งที่เกิดขึ้นเมื่อ workspace ที่มีชื่อเดียวกันมีอยู่แล้ว:", + "restartIgnore": "สลับไปยัง workspace ที่มีอยู่ (ค่าเริ่มต้น)", + "restartRecreate": "ปิดและสร้างใหม่โดยไม่ถาม", + "restartConfirm": "ถามผู้ใช้ก่อนสร้างใหม่", + "layoutTree": "ต้นไม้เลย์เอาต์", + "layoutTreeDesc": "ต้นไม้เลย์เอาต์กำหนดวิธีจัดเรียงแผงโดยใช้โหนดการแบ่งแบบวนซ้ำ:", + "splitNode": "โหนดการแบ่ง", + "splitNodeDesc": "แบ่งพื้นที่ออกเป็นสองส่วน:", + "or": "หรือ", + "splitPosition": "ตำแหน่งตัวแบ่งตั้งแต่ 0.1 ถึง 0.9 (ค่าเริ่มต้น 0.5)", + "splitChildren": "โหนดลูกสองโหนดพอดี (การแบ่งหรือแผง)", + "paneNode": "โหนดแผง", + "paneNodeDesc": "โหนดใบที่มี surface หนึ่งอันหรือมากกว่า (แท็บภายในแผง)", + "surfaceDefinition": "นิยาม surface", + "surfaceDefinitionDesc": "แต่ละ surface ในแผงสามารถเป็นเทอร์มินัลหรือเบราว์เซอร์:", + "surfaceName": "ชื่อแท็บที่กำหนดเอง", + "surfaceCommand": "คำสั่ง shell ที่รันอัตโนมัติเมื่อสร้าง (เฉพาะเทอร์มินัล)", + "surfaceCwd": "ไดเรกทอรีทำงานสำหรับ surface นี้", + "surfaceEnv": "ตัวแปรสภาพแวดล้อมในรูปแบบคู่คีย์-ค่า", + "surfaceUrl": "URL ที่จะเปิด (เฉพาะเบราว์เซอร์)", + "surfaceFocus": "โฟกัสที่ surface นี้หลังสร้าง", + "cwdResolution": "การแก้ไขไดเรกทอรีทำงาน", + "omitted": "ละไว้", + "cwdRelative": "ไดเรกทอรีทำงานของ workspace", + "cwdSubdir": "สัมพัทธ์กับไดเรกทอรีทำงานของ workspace", + "cwdHome": "ขยายไปยังไดเรกทอรีหลัก", + "absolutePath": "พาธสัมบูรณ์", + "cwdAbsolute": "ใช้ตามที่เป็น", + "fullExample": "ตัวอย่างเต็ม" + }, "keyboardShortcuts": { "title": "คีย์ลัด", "description": "คีย์ลัดทั้งหมดใน cmux จัดกลุ่มตามหมวดหมู่", @@ -547,6 +611,7 @@ "gettingStarted": "เริ่มต้นใช้งาน", "concepts": "แนวคิด", "configuration": "การตั้งค่า", + "customCommands": "คำสั่งที่กำหนดเอง", "keyboardShortcuts": "คีย์ลัด", "apiReference": "เอกสาร API", "browserAutomation": "Browser Automation", diff --git a/web/messages/tr.json b/web/messages/tr.json index cd6c4da07d..bee12cd03a 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -302,6 +302,70 @@ "exampleConfig": "Örnek yapılandırma", "metaTitle": "Yapılandırma" }, + "customCommands": { + "title": "Özel Komutlar", + "metaTitle": "Özel Komutlar", + "metaDescription": "cmux.json'da özel komutlar ve workspace düzenleri tanımlayın. Canlı dosya izleme ile proje başına ve genel yapılandırma.", + "intro": "Proje köküne veya ~/.config/cmux/ dizinine bir cmux.json dosyası ekleyerek özel komutlar ve workspace düzenleri tanımlayın. Komutlar komut paletinde görünür.", + "fileLocations": "Dosya konumları", + "fileLocationsDesc": "cmux yapılandırmayı iki yerde arar:", + "localConfig": "Proje başına:", + "localConfigDesc": "proje dizininde bulunur, önceliğe sahiptir", + "globalConfig": "Global:", + "globalConfigDesc": "tüm projeler için geçerlidir, yerel olarak tanımlanmayan komutları tamamlar", + "precedenceNote": "Yerel komutlar, aynı adlı global komutları geçersiz kılar.", + "liveReload": "Değişiklikler otomatik olarak algılanır — yeniden başlatma gerekmez.", + "schema": "Şema", + "schemaDesc": "Bir cmux.json dosyası bir commands dizisi içerir. Her komut ya basit bir shell komutu ya da tam bir workspace tanımıdır:", + "simpleCommands": "Basit komutlar", + "simpleCommandsDesc": "Basit bir komut, o an odaklanılan terminalde bir shell komutu çalıştırır:", + "simpleCommandFields": "Alanlar", + "fieldName": "Komut paletinde gösterilir (zorunlu)", + "fieldDescription": "İsteğe bağlı açıklama", + "fieldKeywords": "Komut paleti için ekstra arama terimleri", + "fieldCommand": "Odaklanılan terminalde çalıştırılacak shell komutu", + "fieldConfirm": "Çalıştırmadan önce onay iletişim kutusu göster", + "simpleCommandCwdNote": "Basit komutlar, odaklanan terminalin mevcut çalışma dizininde çalıştırılır. Komutunuz projeye göreceli yollar gerektiriyorsa, başına", + "simpleCommandCwdRepoRoot": "ekleyerek repo kökünden çalıştırabilir veya", + "simpleCommandCwdCustomPath": "ile herhangi bir dizini belirtebilirsiniz.", + "workspaceCommands": "Workspace komutları", + "workspaceCommandsDesc": "Bir workspace komutu, bölünmeler, terminaller ve tarayıcı panellerinin özel düzeniyle yeni bir workspace oluşturur:", + "workspaceFields": "Workspace alanları", + "wsFieldName": "Workspace sekme adı (varsayılan komut adıdır)", + "wsFieldCwd": "Workspace için çalışma dizini", + "wsFieldColor": "Workspace sekme rengi", + "wsFieldLayout": "Bölünmeleri ve panelleri tanımlayan düzen ağacı", + "restartBehavior": "Yeniden başlatma davranışı", + "restartBehaviorDesc": "Aynı adda bir workspace zaten mevcut olduğunda ne olacağını kontrol eder:", + "restartIgnore": "Mevcut workspace'e geç (varsayılan)", + "restartRecreate": "Sormadan kapat ve yeniden oluştur", + "restartConfirm": "Yeniden oluşturmadan önce kullanıcıya sor", + "layoutTree": "Düzen ağacı", + "layoutTreeDesc": "Düzen ağacı, özyinelemeli bölünme düğümleri kullanarak panellerin nasıl düzenlendiğini tanımlar:", + "splitNode": "Bölünme düğümü", + "splitNodeDesc": "Alanı iki alt öğeye böler:", + "or": "veya", + "splitPosition": "0.1'den 0.9'a bölücü konumu (varsayılan 0.5)", + "splitChildren": "Tam olarak iki alt düğüm (bölünme veya panel)", + "paneNode": "Panel düğümü", + "paneNodeDesc": "Bir veya daha fazla yüzey (panel içindeki sekmeler) içeren yaprak düğüm.", + "surfaceDefinition": "Yüzey tanımı", + "surfaceDefinitionDesc": "Bir paneldeki her yüzey terminal veya tarayıcı olabilir:", + "surfaceName": "Özel sekme başlığı", + "surfaceCommand": "Oluşturulduğunda otomatik çalıştırılacak shell komutu (yalnızca terminal)", + "surfaceCwd": "Bu yüzey için çalışma dizini", + "surfaceEnv": "Anahtar-değer çiftleri olarak ortam değişkenleri", + "surfaceUrl": "Açılacak URL (yalnızca tarayıcı)", + "surfaceFocus": "Oluşturulduktan sonra bu yüzeye odaklan", + "cwdResolution": "Çalışma dizini çözümlemesi", + "omitted": "atlanmış", + "cwdRelative": "workspace çalışma dizini", + "cwdSubdir": "workspace çalışma dizinine göreli", + "cwdHome": "ana dizine genişletilmiş", + "absolutePath": "Mutlak yol", + "cwdAbsolute": "olduğu gibi kullanılır", + "fullExample": "Tam örnek" + }, "keyboardShortcuts": { "title": "Klavye Kısayolları", "description": "cmux'ta mevcut tüm klavye kısayolları, kategoriye göre gruplandırılmış.", @@ -547,6 +611,7 @@ "gettingStarted": "Başlarken", "concepts": "Kavramlar", "configuration": "Yapılandırma", + "customCommands": "Özel Komutlar", "keyboardShortcuts": "Klavye Kısayolları", "apiReference": "API Referansı", "browserAutomation": "Tarayıcı Otomasyonu", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index e970d95d52..a3a05b7ed3 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -302,6 +302,70 @@ "exampleConfig": "配置示例", "metaTitle": "配置" }, + "customCommands": { + "title": "自定义命令", + "metaTitle": "自定义命令", + "metaDescription": "在 cmux.json 中定义自定义命令和工作区布局。支持按项目配置和全局配置,并具有实时文件监视功能。", + "intro": "通过在项目根目录或 ~/.config/cmux/ 中添加 cmux.json 文件来定义自定义命令和工作区布局。命令将显示在命令面板中。", + "fileLocations": "文件位置", + "fileLocationsDesc": "cmux 在两个地方查找配置:", + "localConfig": "按项目:", + "localConfigDesc": "位于项目目录中,优先级更高", + "globalConfig": "全局:", + "globalConfigDesc": "适用于所有项目,补充本地未定义的命令", + "precedenceNote": "本地命令会覆盖同名的全局命令。", + "liveReload": "更改会自动生效 — 无需重启。", + "schema": "架构", + "schemaDesc": "cmux.json 文件包含一个 commands 数组。每个命令要么是简单的 shell 命令,要么是完整的工作区定义:", + "simpleCommands": "简单命令", + "simpleCommandsDesc": "简单命令在当前聚焦的终端中运行 shell 命令:", + "simpleCommandFields": "字段", + "fieldName": "在命令面板中显示(必填)", + "fieldDescription": "可选描述", + "fieldKeywords": "命令面板的额外搜索词", + "fieldCommand": "在聚焦终端中运行的 shell 命令", + "fieldConfirm": "运行前显示确认对话框", + "simpleCommandCwdNote": "简单命令在当前聚焦终端的工作目录中运行。如果命令依赖于项目相对路径,请在前面加上", + "simpleCommandCwdRepoRoot": "以从仓库根目录运行,或使用", + "simpleCommandCwdCustomPath": "指定任意目录。", + "workspaceCommands": "工作区命令", + "workspaceCommandsDesc": "工作区命令会创建一个新工作区,具有自定义的分屏、终端和浏览器面板布局:", + "workspaceFields": "工作区字段", + "wsFieldName": "工作区标签页名称(默认为命令名称)", + "wsFieldCwd": "工作区的工作目录", + "wsFieldColor": "工作区标签页颜色", + "wsFieldLayout": "定义分屏和面板的布局树", + "restartBehavior": "重启行为", + "restartBehaviorDesc": "控制当同名工作区已存在时的行为:", + "restartIgnore": "切换到现有工作区(默认)", + "restartRecreate": "无需询问直接关闭并重新创建", + "restartConfirm": "重新创建前询问用户", + "layoutTree": "布局树", + "layoutTreeDesc": "布局树使用递归分割节点定义面板的排列方式:", + "splitNode": "分割节点", + "splitNodeDesc": "将空间分割为两个子节点:", + "or": "或", + "splitPosition": "分割线位置,从 0.1 到 0.9(默认 0.5)", + "splitChildren": "恰好两个子节点(分割或面板)", + "paneNode": "面板节点", + "paneNodeDesc": "包含一个或多个 surface(面板内标签页)的叶节点。", + "surfaceDefinition": "Surface 定义", + "surfaceDefinitionDesc": "面板中的每个 surface 可以是终端或浏览器:", + "surfaceName": "自定义标签页标题", + "surfaceCommand": "创建时自动运行的 shell 命令(仅限终端)", + "surfaceCwd": "此 surface 的工作目录", + "surfaceEnv": "以键值对形式表示的环境变量", + "surfaceUrl": "要打开的 URL(仅限浏览器)", + "surfaceFocus": "创建后聚焦此 surface", + "cwdResolution": "工作目录解析", + "omitted": "省略", + "cwdRelative": "工作区工作目录", + "cwdSubdir": "相对于工作区工作目录", + "cwdHome": "展开到主目录", + "absolutePath": "绝对路径", + "cwdAbsolute": "按原样使用", + "fullExample": "完整示例" + }, "keyboardShortcuts": { "title": "快捷键", "description": "cmux 中所有可用的快捷键,按类别分组。", @@ -547,6 +611,7 @@ "gettingStarted": "入门指南", "concepts": "核心概念", "configuration": "配置", + "customCommands": "自定义命令", "keyboardShortcuts": "快捷键", "apiReference": "API 参考", "browserAutomation": "浏览器自动化", diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index c6136785f1..9873bcfcd5 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -302,6 +302,70 @@ "exampleConfig": "範例設定", "metaTitle": "設定" }, + "customCommands": { + "title": "自訂指令", + "metaTitle": "自訂指令", + "metaDescription": "在 cmux.json 中定義自訂指令和工作區版面配置。支援按專案設定和全域設定,並具備即時檔案監視功能。", + "intro": "透過在專案根目錄或 ~/.config/cmux/ 中新增 cmux.json 檔案來定義自訂指令和工作區版面配置。指令將顯示於指令面板中。", + "fileLocations": "檔案位置", + "fileLocationsDesc": "cmux 在兩個地方尋找設定:", + "localConfig": "依專案:", + "localConfigDesc": "位於專案目錄中,優先採用", + "globalConfig": "全域:", + "globalConfigDesc": "適用於所有專案,補充本地未定義的指令", + "precedenceNote": "本地指令會覆蓋同名的全域指令。", + "liveReload": "變更會自動生效 — 無需重新啟動。", + "schema": "結構描述", + "schemaDesc": "cmux.json 檔案包含一個 commands 陣列。每個指令可以是簡單的 shell 指令或完整的工作區定義:", + "simpleCommands": "簡單指令", + "simpleCommandsDesc": "簡單指令會在目前聚焦的終端機中執行 shell 指令:", + "simpleCommandFields": "欄位", + "fieldName": "顯示於指令面板(必填)", + "fieldDescription": "選填說明", + "fieldKeywords": "指令面板的額外搜尋詞", + "fieldCommand": "在聚焦終端機中執行的 shell 指令", + "fieldConfirm": "執行前顯示確認對話框", + "simpleCommandCwdNote": "簡單指令在當前聚焦終端的工作目錄中執行。如果指令依賴於專案相對路徑,請在前面加上", + "simpleCommandCwdRepoRoot": "以從倉庫根目錄執行,或使用", + "simpleCommandCwdCustomPath": "指定任意目錄。", + "workspaceCommands": "工作區指令", + "workspaceCommandsDesc": "工作區指令會建立一個新工作區,具有自訂的分割、終端機和瀏覽器窗格版面配置:", + "workspaceFields": "工作區欄位", + "wsFieldName": "工作區索引標籤名稱(預設為指令名稱)", + "wsFieldCwd": "工作區的工作目錄", + "wsFieldColor": "工作區索引標籤顏色", + "wsFieldLayout": "定義分割和窗格的版面配置樹", + "restartBehavior": "重新啟動行為", + "restartBehaviorDesc": "控制當同名工作區已存在時的行為:", + "restartIgnore": "切換至現有工作區(預設)", + "restartRecreate": "無需詢問直接關閉並重新建立", + "restartConfirm": "重新建立前詢問使用者", + "layoutTree": "版面配置樹", + "layoutTreeDesc": "版面配置樹使用遞迴分割節點定義窗格的排列方式:", + "splitNode": "分割節點", + "splitNodeDesc": "將空間分割為兩個子節點:", + "or": "或", + "splitPosition": "分割線位置,從 0.1 到 0.9(預設 0.5)", + "splitChildren": "恰好兩個子節點(分割或窗格)", + "paneNode": "窗格節點", + "paneNodeDesc": "包含一個或多個 surface(窗格內索引標籤)的葉節點。", + "surfaceDefinition": "Surface 定義", + "surfaceDefinitionDesc": "窗格中的每個 surface 可以是終端機或瀏覽器:", + "surfaceName": "自訂索引標籤標題", + "surfaceCommand": "建立時自動執行的 shell 指令(僅限終端機)", + "surfaceCwd": "此 surface 的工作目錄", + "surfaceEnv": "以鍵值對形式表示的環境變數", + "surfaceUrl": "要開啟的 URL(僅限瀏覽器)", + "surfaceFocus": "建立後聚焦此 surface", + "cwdResolution": "工作目錄解析", + "omitted": "省略", + "cwdRelative": "工作區工作目錄", + "cwdSubdir": "相對於工作區工作目錄", + "cwdHome": "展開至主目錄", + "absolutePath": "絕對路徑", + "cwdAbsolute": "按原樣使用", + "fullExample": "完整範例" + }, "keyboardShortcuts": { "title": "鍵盤快捷鍵", "description": "cmux 中所有可用的鍵盤快捷鍵,依類別分組。", @@ -547,6 +611,7 @@ "gettingStarted": "入門指南", "concepts": "概念", "configuration": "設定", + "customCommands": "自訂指令", "keyboardShortcuts": "鍵盤快捷鍵", "apiReference": "API 參考", "browserAutomation": "瀏覽器自動化",