diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c84becdd8..1680262c434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,117 @@ All notable changes to cmux are documented here. +## Ivrix [1.1.3] - 2026-07-31 + +Typing ergonomics for a Hebrew keyboard: a shortcut for the direction toggle, +and shell quotes that survive the Hebrew layout. + +### Added +- **A keyboard shortcut for the text direction.** Flipping between left-to-right + and right-to-left meant reaching for the titlebar button. `Ctrl+Cmd+H` now does + it, and so does View > Toggle Text Direction. Rebind it in Settings > Keyboard + Shortcuts or as `shortcuts.bindings.toggleTextDirection` in `cmux.json`. The + titlebar button's tooltip names the shortcut, and follows a rebind. +- **ASCII quotes when typing on a Hebrew layout.** A Hebrew layout puts geresh + and gershayim (`׳` `״`) on the apostrophe and quote keys, so `echo "hi"` typed + in Hebrew reached the shell as `echo ״hi״` and the shell never saw a quote at + all. Those two characters are now sent as ASCII `'` and `"`. Only for keys you + actually press: pasted text, dictation, and anything inserted programmatically + are untouched. Turn it off in Settings > Terminal to type acronyms such as + צה״ל, which need the real gershayim. + +### Changed +- **Selection editing now asks whether input is marked, not which screen is up.** + Selecting text at a prompt and typing over it was refused outright whenever an + application had taken over the screen. That was a proxy for the real + requirement, which is `OSC 133` marking saying which cells are the edit buffer; + without it there is nothing to count arrow keys and deletes against. It now + checks for the marking directly. An application that emits no marks is refused + exactly as before, so nothing changes today: Claude Code, for one, takes the + screen and emits no marks, and its composer stays copy-only until it does. + +## Ivrix [1.1.2] - 2026-07-29 + +Right-to-left fixes for selecting and moving around inside full-screen +applications. + +### Fixed +- **Selecting Hebrew inside a TUI highlighted the wrong text.** An application + that takes over the screen addresses cells in its own column order and knows + nothing about the reordering applied when drawing, so it was being told the + column the pointer was physically over and acted on a different cell. Mouse + reporting now sends the logical column. +- **Dragging a selection across Hebrew.** Only the press position was mapped + back through the row's order; the end that follows the pointer, the + autoscroll past the window edge, and the prompt click target were not, so a + drag anchored a logical cell to a visual one and landed mirrored. +- **Row width mismatch** between the renderer's map and the inverse used for + the mouse, which shifted every column on rows where the two differed. +- **Arrow hints now agree with the arrow keys.** With `bidi-direction = rtl` + the arrow keys mirror on right-to-left rows, but an application's own hint + such as `press <-` sits in a Latin run, so UAX #9 left the glyph alone and it + named the opposite of the key that performs it. Horizontal arrows now mirror + on those rows too. Turn off with `bidi-mirror-arrows = false`. + +## Ivrix [1.1.1] - 2026-07-28 + +### Fixed +- **The "Update Available" button now updates Ivrix.** It ran Sparkle against + upstream cmux's release feed, so an Ivrix install was offered upstream's + releases; accepting one replaced Ivrix with cmux and lost the Hebrew build. + The feed now names this repository, and the app carries its own signing key + rather than upstream's, so only Ivrix releases can be offered or installed. + Releases publish a signed `appcast.xml`, without which the feed returned + nothing and no update was ever shown. + + Installs of 1.1.0 and earlier carry the old feed and cannot be reached by + this fix. Download 1.1.1 once by hand; updates work from there on. + +### Changed +- `scripts/build-ivrix.sh` refuses to build if the updater feed still points + upstream or the signing key is missing or upstream's. + +## Ivrix [1.1.0] - 2026-07-28 + +Selection and text editing at the prompt, and the bidi fixes needed to make +them correct in Hebrew. + +### Added +- **Selection editing at the prompt.** Select text and type to replace it, or + press backspace/delete to remove it, the way a text editor behaves. A + terminal has no protocol for this, so the terminal places the shell's cursor + with arrow keys and deletes the selected positions itself. Needs `OSC 133` + input marking; controlled by `selection-edit-at-prompt`. +- **Keyboard selection.** `Shift+Left/Right` selects by character and + `Shift+Option+Left/Right` by word. Direction is resolved per row, so on a + Hebrew line `Shift+Left` extends forward through the text, matching the way + the plain arrows already move. +- **Cursor hides during selection**, since the selection is what the next + keystroke acts on. Controlled by `cursor-hide-while-selecting`. + +### Fixed +- **Hyphenated words and numbers no longer scatter on a Hebrew line.** A + neutral character resolving left-to-right inside a right-to-left paragraph + was given embedding level 0 instead of 2, which cut the paragraph run in two + and reordered each half on its own: `max-height` rendered as + `max -height`. Two Latin words separated by a space broke the same + way. +- **Numbers keep their digit order.** Weak types (UAX #9 W1-W7) were never + resolved, so number separators were treated as neutral: `1.5` rendered as + `5.1`, `3,000` as `000,3`, and `192.168.1.1` came out fully reversed. +- **Backgrounds, selection highlight and decorations paint at the visual + column.** Only glyphs went through the bidi map, so on a Hebrew row the text + sat in one place and everything drawn around it sat in another. +- **The mouse lands on the cell under the pointer in Hebrew.** Clicks were + handing a screen column straight in as a logical cell index, so dragging + selected text the user had not dragged over. +- **Wide characters cost one arrow key, not two,** when clicking to move the + cursor past CJK or emoji. + +### Changed +- Dev builds identify themselves as `ivrix-dev` rather than the upstream name. +- The app now carries its own version rather than inheriting cmux's. + ## Ivrix [1.0.0] - 2026-07-28 Ivrix is a Hebrew-first fork of cmux. This release rebases the fork onto the diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift index ab72de774ab..7e230f5194f 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift @@ -44,6 +44,13 @@ public struct TerminalCatalogSection: SettingCatalogSection { userDefaultsKey: HebrewFontFace.settingsPath ) + /// Rewrite Hebrew geresh/gershayim to ASCII `'`/`"` while typing. + public let hebrewAsciiQuotes = DefaultsKey( + id: "terminal.hebrewAsciiQuotes", + defaultValue: true, + userDefaultsKey: "terminal.hebrewAsciiQuotes" + ) + public let copyOnSelect = DefaultsKey( id: "terminal.copyOnSelect", defaultValue: false, diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift index 1a8dfec7977..23221c5feda 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift @@ -123,6 +123,7 @@ extension ShortcutAction { case .attachTextBoxFile: return ShortcutStroke(key: "a", command: true, shift: true, option: true) case .sendCtrlFToTerminal: return nil case .clearScreenKeepScrollback: return ShortcutStroke(key: "k", command: true, shift: true) + case .toggleTextDirection: return ShortcutStroke(key: "h", command: true, control: true) case .toggleRightSidebar: return ShortcutStroke(key: "b", command: true, option: true) case .fileExplorerOpenSelection: return ShortcutStroke(key: "\r") case .fileExplorerOpenSelectionFinderAlias: return ShortcutStroke(key: "↓", command: true) diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift index 279aae3c71f..3138ef6af23 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift @@ -88,6 +88,9 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod case sendCtrlFToTerminal /// Clears the focused terminal's visible screen while preserving scrollback. case clearScreenKeepScrollback + /// Ivrix: flips the terminal print direction between LTR and RTL, the same + /// state the titlebar direction control writes. + case toggleTextDirection // MARK: Panes case focusLeft @@ -194,7 +197,7 @@ extension ShortcutAction { .newWorkspaceGroup, .groupSelectedWorkspaces, .toggleFocusedWorkspaceGroupCollapsed, .reopenClosedBrowserPanel, .newSurface, .toggleTerminalCopyMode, .focusTextBoxInput, .cycleTextBoxSubmitAction, .attachTextBoxFile, .sendCtrlFToTerminal, - .clearScreenKeepScrollback: + .clearScreenKeepScrollback, .toggleTextDirection: return .navigation case .focusLeft, .focusRight, .focusUp, .focusDown, .splitRight, .splitDown, .toggleSplitZoom, .equalizeSplits, .splitBrowserRight, .splitBrowserDown, @@ -399,6 +402,8 @@ extension ShortcutAction { return String(localized: "shortcut.sendCtrlFToTerminal.label", defaultValue: "Send Ctrl-F to Terminal") case .clearScreenKeepScrollback: return String(localized: "shortcut.clearScreenKeepScrollback.label", defaultValue: "Clear Screen (Keep Scrollback)") + case .toggleTextDirection: + return String(localized: "shortcut.toggleTextDirection.label", defaultValue: "Toggle Text Direction (LTR/RTL)") case .focusLeft: return "Focus Pane Left" case .focusRight: return "Focus Pane Right" case .focusUp: return "Focus Pane Up" diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index e21cac1e3b3..fe029190026 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -111,6 +111,7 @@ extension Array where Element == CuratedSettingEntry { synonyms: "terminal.scrollSpeed scroll speed multiplier wheel mouse trackpad sensitivity faster slower" ), .init(section: .terminal, id: "copy-on-select", title: "Copy on Selection", synonyms: "terminal.copyOnSelect copy on selection select clipboard mouse double click triple click iterm"), + .init(section: .terminal, id: "hebrew-ascii-quotes", title: "ASCII Quotes on Hebrew Layout", synonyms: "terminal.hebrewAsciiQuotes hebrew ascii quotes geresh gershayim apostrophe quote punctuation rtl ivrix shell quoting layout"), .init(section: .terminal, id: "agent-auto-resume", title: "Resume Agent Sessions on Reopen", synonyms: "terminal.autoResumeAgentSessions auto resume restore reopen relaunch quit sessions agents claude code codex opencode rovo dev rovodev toggle"), .init(section: .terminal, id: "agent-hibernation", title: "Agent Hibernation", synonyms: "terminal.agentHibernation.enabled idle hibernate suspend background agents claude code codex opencode live terminals"), .init(section: .terminal, id: "agent-hibernation-idle", title: "Hibernate After Idle Seconds", synonyms: "terminal.agentHibernation.idleSeconds idle seconds timeout delay hibernate suspend"), diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift index 6065b914960..d2a7002ff06 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift @@ -24,6 +24,7 @@ public struct TerminalSection: View { @State private var scrollBar: DefaultsValueModel @State private var copyOnSelect: DefaultsValueModel @State private var hebrewFont: DefaultsValueModel + @State private var hebrewAsciiQuotes: DefaultsValueModel @State private var autoResume: DefaultsValueModel @State private var hibernation: DefaultsValueModel @State private var idleSeconds: DefaultsValueModel @@ -51,6 +52,7 @@ public struct TerminalSection: View { _scrollBar = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.showScrollBar)) _copyOnSelect = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.copyOnSelect)) _hebrewFont = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.hebrewFont)) + _hebrewAsciiQuotes = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.hebrewAsciiQuotes)) _autoResume = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.autoResumeAgentSessions)) _hibernation = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationEnabled)) _idleSeconds = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationIdleSeconds)) @@ -80,6 +82,7 @@ public struct TerminalSection: View { scrollBar, copyOnSelect, hebrewFont, + hebrewAsciiQuotes, autoResume, hibernation, idleSeconds, @@ -339,6 +342,25 @@ public struct TerminalSection: View { .accessibilityIdentifier("SettingsHebrewFontPicker") } SettingsCardDivider() + SettingsCardRow( + configurationReview: .json("terminal.hebrewAsciiQuotes"), + String(localized: "settings.terminal.hebrewAsciiQuotes", defaultValue: "ASCII Quotes on Hebrew Layout"), + subtitle: hebrewAsciiQuotes.current + ? String( + localized: "settings.terminal.hebrewAsciiQuotes.subtitleOn", + defaultValue: "Typing geresh or gershayim (׳ ״) on a Hebrew layout sends ASCII ' and \" instead, so shell quoting works." + ) + : String( + localized: "settings.terminal.hebrewAsciiQuotes.subtitleOff", + defaultValue: "Hebrew punctuation is sent as typed. Needed for acronyms such as צה״ל; shell quoting will not work from a Hebrew layout." + ) + ) { + Toggle("", isOn: Binding(get: { hebrewAsciiQuotes.current }, set: { hebrewAsciiQuotes.set($0) })) + .labelsHidden() + .controlSize(.small) + .accessibilityIdentifier("SettingsTerminalHebrewAsciiQuotesToggle") + } + SettingsCardDivider() SettingsCardRow( configurationReview: .json("terminal.scrollSpeed"), String(localized: "settings.terminal.scrollSpeed", defaultValue: "Scroll Speed"), diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index 8fbc87182c9..b03daf5a669 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -127,6 +127,7 @@ struct SettingsRowAnchorResolutionTests { "terminal.rendererRealization.maxWarmRenderers", "terminal.autoResumeAgentSessions", "terminal.copyOnSelect", + "terminal.hebrewAsciiQuotes", "terminal.resumeCommands", "terminal.sessionContentAlignment", "terminal.sessionContentMaxWidth", diff --git a/Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateFeedResolver.swift b/Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateFeedResolver.swift index 3f38790b63c..3e0e9d07111 100644 --- a/Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateFeedResolver.swift +++ b/Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateFeedResolver.swift @@ -34,11 +34,21 @@ public struct UpdateFeedResolver: Sendable { /// The appcast URL used when the `Info.plist` feed URL is missing or empty. public let fallbackFeedURL: String + /// The appcast this fork updates from. + /// + /// This has to be Ivrix's own feed, not the upstream project's. An Ivrix + /// build that queries upstream is offered upstream's releases, and because + /// the two are different applications, installing one replaces Ivrix with + /// cmux and loses the Hebrew build entirely. A wrong value here is worse + /// than no updater at all, so it must never silently fall back upstream. + public static let ivrixFallbackFeedURL = + "https://github.com/DananzMolt/Ivrix/releases/latest/download/appcast.xml" + /// Creates a resolver. /// /// - Parameter fallbackFeedURL: The appcast URL to fall back to when the build-time /// feed URL is absent. Defaults to the project's latest-release appcast. - public init(fallbackFeedURL: String = "https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml") { + public init(fallbackFeedURL: String = UpdateFeedResolver.ivrixFallbackFeedURL) { self.fallbackFeedURL = fallbackFeedURL } diff --git a/Packages/macOS/CmuxUpdater/Tests/CmuxUpdaterTests/UpdateFeedResolverTests.swift b/Packages/macOS/CmuxUpdater/Tests/CmuxUpdaterTests/UpdateFeedResolverTests.swift index ef00c3dcb96..14c503fade6 100644 --- a/Packages/macOS/CmuxUpdater/Tests/CmuxUpdaterTests/UpdateFeedResolverTests.swift +++ b/Packages/macOS/CmuxUpdater/Tests/CmuxUpdaterTests/UpdateFeedResolverTests.swift @@ -31,4 +31,16 @@ import Testing #expect(resolution.isNightly) #expect(!resolution.usedFallback) } + + @Test func defaultFallbackPointsAtIvrixNotUpstream() { + // An Ivrix build that falls back to upstream's appcast is offered + // upstream's releases, and installing one replaces Ivrix with cmux. + // The default therefore has to be this fork's own feed. + let resolver = UpdateFeedResolver() + let resolution = resolver.resolve(infoFeedURL: nil) + #expect(resolution.usedFallback) + #expect(resolution.url == UpdateFeedResolver.ivrixFallbackFeedURL) + #expect(resolution.url.contains("DananzMolt/Ivrix")) + #expect(!resolution.url.contains("manaflow-ai/cmux")) + } } diff --git a/Resources/Info.plist b/Resources/Info.plist index 7bc3c21cacb..a4b355755c8 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -245,7 +245,7 @@ SUEnableAutomaticChecks SUFeedURL - https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml + https://github.com/DananzMolt/Ivrix/releases/latest/download/appcast.xml SUScheduledCheckInterval 86400 SUSendProfileInfo diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index aeadfdb41dc..c5fb6024009 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -186655,6 +186655,631 @@ } } }, + "menu.view.toggleTextDirection": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبديل اتجاه النص (LTR/RTL)" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Promijeni smjer teksta (LTR/RTL)" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Skift tekstretning (LTR/RTL)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Textrichtung umschalten (LTR/RTL)" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle Text Direction (LTR/RTL)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar dirección del texto (LTR/RTL)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer le sens du texte (LTR/RTL)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia direzione del testo (LTR/RTL)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テキスト方向を切り替え(LTR/RTL)" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្ដូរទិសអក្សរ (LTR/RTL)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "텍스트 방향 전환 (LTR/RTL)" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Bytt tekstretning (LTR/RTL)" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przełącz kierunek tekstu (LTR/RTL)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alternar direção do texto (LTR/RTL)" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Переключить направление текста (LTR/RTL)" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สลับทิศทางข้อความ (LTR/RTL)" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Metin yönünü değiştir (LTR/RTL)" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перемкнути напрямок тексту (LTR/RTL)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换文本方向(LTR/RTL)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換文字方向(LTR/RTL)" + } + } + } + }, + "settings.terminal.hebrewAsciiQuotes": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "علامات اقتباس ASCII في تخطيط العبرية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "ASCII navodnici na hebrejskom rasporedu" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "ASCII-anførselstegn på hebraisk tastaturlayout" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ASCII-Anführungszeichen bei hebräischem Layout" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "ASCII Quotes on Hebrew Layout" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comillas ASCII con distribución hebrea" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Guillemets ASCII avec la disposition hébraïque" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Virgolette ASCII con layout ebraico" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヘブライ語配列で ASCII 引用符を使用" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "សញ្ញាសម្រង់ ASCII លើប្លង់ក្ដារចុចហេប្រ៊ូ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "히브리어 자판에서 ASCII 따옴표 사용" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "ASCII-anførselstegn på hebraisk tastaturoppsett" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Cudzysłowy ASCII przy układzie hebrajskim" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aspas ASCII no layout hebraico" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Кавычки ASCII в еврейской раскладке" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เครื่องหมายคำพูด ASCII บนผังแป้นพิมพ์ฮีบรู" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İbranice düzende ASCII tırnak işaretleri" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Лапки ASCII в івритській розкладці" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "希伯来语键盘布局使用 ASCII 引号" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "希伯來文鍵盤配置使用 ASCII 引號" + } + } + } + }, + "settings.terminal.hebrewAsciiQuotes.subtitleOff": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تُرسل علامات الترقيم العبرية كما تُكتب. لازمة للاختصارات مثل צה״ל، ولن يعمل الاقتباس في الصدفة من تخطيط العبرية." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Hebrejski interpunkcijski znakovi šalju se kako su otkucani. Potrebno za skraćenice poput צה״ל; navodnici u shellu neće raditi s hebrejskog rasporeda." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Hebraisk tegnsætning sendes, som den skrives. Nødvendigt til forkortelser som צה״ל; anførselstegn virker ikke i shellen fra et hebraisk layout." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hebräische Satzzeichen werden unverändert gesendet. Nötig für Abkürzungen wie צה״ל; Quoting in der Shell funktioniert dann mit hebräischem Layout nicht." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Hebrew punctuation is sent as typed. Needed for acronyms such as צה״ל; shell quoting will not work from a Hebrew layout." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La puntuación hebrea se envía tal cual. Necesario para siglas como צה״ל; las comillas del intérprete de comandos no funcionarán con distribución hebrea." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La ponctuation hébraïque est envoyée telle quelle. Nécessaire pour les sigles comme צה״ל ; les guillemets du shell ne fonctionneront pas avec la disposition hébraïque." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La punteggiatura ebraica viene inviata così com'è. Serve per acronimi come צה״ל; le virgolette della shell non funzioneranno con layout ebraico." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヘブライ語の約物をそのまま送信します。צה״ל のような略語に必要ですが、ヘブライ語配列ではシェルの引用符が機能しません。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "សញ្ញាវណ្ណយុត្តហេប្រ៊ូត្រូវបានផ្ញើតាមការវាយ។ ចាំបាច់សម្រាប់អក្សរកាត់ដូចជា צה״ל ប៉ុន្តែសញ្ញាសម្រង់ក្នុង shell នឹងមិនដំណើរការពីប្លង់ហេប្រ៊ូទេ។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "히브리어 문장 부호를 입력한 그대로 보냅니다. צה״ל 같은 약어에 필요하지만, 히브리어 자판에서는 셸 따옴표가 동작하지 않습니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Hebraisk tegnsetting sendes slik den skrives. Nødvendig for forkortelser som צה״ל; anførselstegn i skallet vil ikke virke fra et hebraisk oppsett." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Hebrajska interpunkcja jest wysyłana bez zmian. Potrzebne do skrótowców takich jak צה״ל; cudzysłowy w powłoce nie zadziałają z układu hebrajskiego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pontuação hebraica é enviada como digitada. Necessário para siglas como צה״ל; as aspas do shell não funcionarão no layout hebraico." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Еврейская пунктуация отправляется как есть. Нужно для аббревиатур вроде צה״ל; кавычки в оболочке из еврейской раскладки работать не будут." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เครื่องหมายวรรคตอนภาษาฮีบรูจะถูกส่งตามที่พิมพ์ จำเป็นสำหรับอักษรย่อ เช่น צה״ל แต่เครื่องหมายคำพูดในเชลล์จะไม่ทำงานจากผังแป้นพิมพ์ฮีบรู" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İbranice noktalama yazıldığı gibi gönderilir. צה״ל gibi kısaltmalar için gerekir; İbranice düzenden kabuk tırnakları çalışmaz." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Івритська пунктуація надсилається як є. Потрібно для абревіатур на кшталт צה״ל; лапки в оболонці з івритської розкладки не працюватимуть." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "希伯来语标点按输入原样发送。צה״ל 之类的缩写需要它,但在希伯来语布局下 shell 引号将无法使用。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "希伯來文標點按輸入原樣送出。צה״ל 之類的縮寫需要它,但在希伯來文配置下 shell 引號將無法使用。" + } + } + } + }, + "settings.terminal.hebrewAsciiQuotes.subtitleOn": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "كتابة الجيرش أو الجيرشايم (׳ ״) في تخطيط العبرية ترسل ' و \" بترميز ASCII بدلاً منهما، فيعمل الاقتباس في الصدفة." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kucanje gereša ili geršajima (׳ ״) na hebrejskom rasporedu šalje ASCII ' i \", pa navodnici u shellu rade." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Når du skriver geresh eller gershayim (׳ ״) på et hebraisk layout, sendes ASCII ' og \" i stedet, så anførselstegn virker i shellen." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Geresh oder Gerschajim (׳ ״) auf hebräischem Layout senden stattdessen ASCII ' und \", damit Quoting in der Shell funktioniert." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Typing geresh or gershayim (׳ ״) on a Hebrew layout sends ASCII ' and \" instead, so shell quoting works." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Al escribir gueresh o guershayim (׳ ״) con distribución hebrea se envían ' y \" ASCII, para que las comillas funcionen en el intérprete de comandos." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Saisir un guéresh ou un guershayim (׳ ״) avec la disposition hébraïque envoie ' et \" ASCII, pour que les guillemets fonctionnent dans le shell." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Digitando gheresh o ghershayim (׳ ״) con layout ebraico vengono inviati ' e \" ASCII, così le virgolette funzionano nella shell." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヘブライ語配列でゲレシュ・ゲルシャイム(׳ ״)を入力すると ASCII の ' と \" を送信し、シェルの引用符が機能します。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ការវាយ geresh ឬ gershayim (׳ ״) លើប្លង់ហេប្រ៊ូ នឹងផ្ញើ ' និង \" បែប ASCII ជំនួស ដើម្បីឲ្យសញ្ញាសម្រង់ក្នុង shell ដំណើរការ។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "히브리어 자판에서 게레시나 게르샤임(׳ ״)을 입력하면 ASCII ' 와 \" 를 대신 보내 셸 따옴표가 동작합니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Å skrive geresh eller gershayim (׳ ״) på hebraisk oppsett sender ASCII ' og \" i stedet, slik at anførselstegn virker i skallet." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wpisanie gereszu lub gerszajim (׳ ״) w układzie hebrajskim wysyła zamiast nich ASCII ' i \", dzięki czemu cudzysłowy działają w powłoce." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Digitar gueresh ou guershayim (׳ ״) no layout hebraico envia ' e \" ASCII, para que as aspas funcionem no shell." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ввод гереша или гершаима (׳ ״) в еврейской раскладке отправляет ASCII ' и \", поэтому кавычки работают в оболочке." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "การพิมพ์ geresh หรือ gershayim (׳ ״) บนผังแป้นพิมพ์ฮีบรูจะส่ง ' และ \" แบบ ASCII แทน เพื่อให้เครื่องหมายคำพูดในเชลล์ทำงานได้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İbranice düzende geresh veya gershayim (׳ ״) yazmak bunların yerine ASCII ' ve \" gönderir, böylece kabuktaki tırnaklar çalışır." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Введення гереша або гершаїма (׳ ״) в івритській розкладці надсилає ASCII ' і \", тож лапки працюють в оболонці." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在希伯来语键盘布局下输入 geresh 或 gershayim(׳ ״)时改为发送 ASCII 的 ' 和 \",这样 shell 引号才有效。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "在希伯來文鍵盤配置下輸入 geresh 或 gershayim(׳ ״)時改為送出 ASCII 的 ' 與 \",這樣 shell 引號才有效。" + } + } + } + }, + "shortcut.toggleTextDirection.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبديل اتجاه النص (LTR/RTL)" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Promijeni smjer teksta (LTR/RTL)" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Skift tekstretning (LTR/RTL)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Textrichtung umschalten (LTR/RTL)" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle Text Direction (LTR/RTL)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar dirección del texto (LTR/RTL)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer le sens du texte (LTR/RTL)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia direzione del testo (LTR/RTL)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テキスト方向を切り替え(LTR/RTL)" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្ដូរទិសអក្សរ (LTR/RTL)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "텍스트 방향 전환 (LTR/RTL)" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Bytt tekstretning (LTR/RTL)" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Przełącz kierunek tekstu (LTR/RTL)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alternar direção do texto (LTR/RTL)" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Переключить направление текста (LTR/RTL)" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สลับทิศทางข้อความ (LTR/RTL)" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Metin yönünü değiştir (LTR/RTL)" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перемкнути напрямок тексту (LTR/RTL)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换文本方向(LTR/RTL)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換文字方向(LTR/RTL)" + } + } + } + }, "settings.terminal.hebrewFont.subtitle": { "localizations": { "en": { diff --git a/Sources/App/WorkspaceRuntimeSettings.swift b/Sources/App/WorkspaceRuntimeSettings.swift index 0fe60256bc4..f168d10fd16 100644 --- a/Sources/App/WorkspaceRuntimeSettings.swift +++ b/Sources/App/WorkspaceRuntimeSettings.swift @@ -240,6 +240,19 @@ enum TerminalTextDirectionSettings { } } + /// Flips the direction. The single mutation path shared by the titlebar + /// control, the toolbar segmented control, the View menu item, and the + /// `toggleTextDirection` keyboard shortcut. + @discardableResult + static func toggleDirection( + defaults: UserDefaults = .standard, + notificationCenter: NotificationCenter = .default + ) -> Direction { + let next: Direction = direction(defaults: defaults) == .rtl ? .ltr : .rtl + setDirection(next, defaults: defaults, notificationCenter: notificationCenter) + return next + } + static func notifyDidChange(notificationCenter: NotificationCenter = .default) { notificationCenter.post(name: didChangeNotification, object: nil) } diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 1a1650fcc47..d124c9f85b0 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -6197,7 +6197,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { } } - return chars + // Ivrix: the fallback text path, used when `interpretKeyEvents` produced + // no `insertText`. Same rewrite as the accumulator path so a Hebrew + // layout's quote keys reach the shell as ASCII quotes either way. + return HebrewAsciiQuotes.normalized(chars) } /// Get the unshifted codepoint for the key event @@ -11898,6 +11901,13 @@ extension GhosttyNSView: NSTextInputClient { return } + // Ivrix: only rewrite Hebrew quote punctuation for live keystrokes. The + // accumulator is non-nil exactly inside `keyDown`, so paste, dictation, + // and programmatic `NSTextInputClient` callers keep their text verbatim. + if keyTextAccumulator != nil { + chars = HebrewAsciiQuotes.normalized(chars) + } + if keyTextAccumulator != nil, shouldBufferBopomofoInsertedPreedit(chars) { insertBopomofoPreeditText(chars, replacementRange: replacementRange) diff --git a/Sources/HebrewAsciiQuotes.swift b/Sources/HebrewAsciiQuotes.swift new file mode 100644 index 00000000000..2feaed311f4 --- /dev/null +++ b/Sources/HebrewAsciiQuotes.swift @@ -0,0 +1,47 @@ +import Foundation + +/// Ivrix: Hebrew keyboard layouts put HEBREW PUNCTUATION GERESH (U+05F3) and +/// GERSHAYIM (U+05F4) on the apostrophe and quote keys, so a shell command typed +/// with a Hebrew layout active arrives as `echo ״hi״` instead of `echo "hi"` and +/// the shell never sees a quote at all. +/// +/// When enabled (the default) those two scalars are rewritten to ASCII `'` and +/// `"` on the way into the terminal. Turn it off from Settings → Terminal to type +/// Hebrew acronyms such as צה״ל, which need the real gershayim. +enum HebrewAsciiQuotes { + /// Settings path and `UserDefaults` key. Mirrors + /// `SettingCatalog.terminal.hebrewAsciiQuotes`. + static let settingsKey = "terminal.hebrewAsciiQuotes" + static let defaultEnabled = true + + private static let geresh: Unicode.Scalar = "\u{05F3}" + private static let gershayim: Unicode.Scalar = "\u{05F4}" + + static func isEnabled(defaults: UserDefaults = .standard) -> Bool { + guard defaults.object(forKey: settingsKey) != nil else { return defaultEnabled } + return defaults.bool(forKey: settingsKey) + } + + /// Rewrites geresh/gershayim to ASCII `'`/`"`. + /// + /// This runs on the keystroke path, so it early-returns on the scan before + /// touching `UserDefaults`: ordinary typing pays one pass over a one-scalar + /// string and nothing else. + static func normalized(_ text: String, defaults: UserDefaults = .standard) -> String { + guard text.unicodeScalars.contains(where: { $0 == geresh || $0 == gershayim }) else { + return text + } + guard isEnabled(defaults: defaults) else { return text } + + var scalars = String.UnicodeScalarView() + scalars.reserveCapacity(text.unicodeScalars.count) + for scalar in text.unicodeScalars { + switch scalar { + case geresh: scalars.append("'") + case gershayim: scalars.append("\"") + default: scalars.append(scalar) + } + } + return String(scalars) + } +} diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index 82dda89aaa1..b6b4499ad22 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -126,6 +126,7 @@ enum KeyboardShortcutSettings { case focusTextBoxInput, cycleTextBoxSubmitAction, attachTextBoxFile case sendCtrlFToTerminal case clearScreenKeepScrollback + case toggleTextDirection // Panes / splits case focusLeft @@ -263,6 +264,7 @@ enum KeyboardShortcutSettings { case .attachTextBoxFile: return String(localized: "shortcut.attachTextBoxFile.label", defaultValue: "Attach File to TextBox Input") case .sendCtrlFToTerminal: return String(localized: "shortcut.sendCtrlFToTerminal.label", defaultValue: "Send Ctrl-F to Terminal") case .clearScreenKeepScrollback: return String(localized: "shortcut.clearScreenKeepScrollback.label", defaultValue: "Clear Screen (Keep Scrollback)") + case .toggleTextDirection: return String(localized: "shortcut.toggleTextDirection.label", defaultValue: "Toggle Text Direction (LTR/RTL)") case .focusLeft: return String(localized: "shortcut.focusPaneLeft.label", defaultValue: "Focus Pane Left") case .focusRight: return String(localized: "shortcut.focusPaneRight.label", defaultValue: "Focus Pane Right") case .focusUp: return String(localized: "shortcut.focusPaneUp.label", defaultValue: "Focus Pane Up") @@ -512,6 +514,10 @@ enum KeyboardShortcutSettings { // which also wipes scrollback. Shift+K is unbound in both Ghostty defaults and // cmux, and sits next to the full-clear chord. Rebindable in Settings → Keyboard Shortcuts. return StoredShortcut(key: "k", command: true, shift: true, option: false, control: false) + case .toggleTextDirection: + // Ctrl+Cmd+H: "H" for Hebrew, and Cmd+H alone is Hide Application, so the + // Ctrl variant stays free of both AppKit reservations and cmux defaults. + return StoredShortcut(key: "h", command: true, shift: false, option: false, control: true) case .selectWorkspaceByNumber: return StoredShortcut(key: "1", command: true, shift: false, option: false, control: false) case .moveWorkspaceUp: return StoredShortcut(key: "[", command: true, shift: false, option: true, control: true) diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 817f8315e01..c3745e128f6 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -1115,8 +1115,7 @@ struct TitlebarControlsView: View { #if DEBUG cmuxDebugLog("titlebar.textDirection") #endif - let next: TerminalTextDirectionSettings.Direction = textDirection == .rtl ? .ltr : .rtl - TerminalTextDirectionSettings.setDirection(next) + TerminalTextDirectionSettings.toggleDirection() }) { iconLabel( systemName: textDirection == .rtl ? "text.alignright" : "text.alignleft", @@ -1124,9 +1123,11 @@ struct TitlebarControlsView: View { iconGeometryKeyPrefix: "titlebarControl_textDirectionIcon" ) } - .safeHelp(textDirection == .rtl - ? String(localized: "toolbar.textDirection.rtl", defaultValue: "Right-to-left") - : String(localized: "toolbar.textDirection.ltr", defaultValue: "Left-to-right")) + .safeHelp(KeyboardShortcutSettings.Action.toggleTextDirection.tooltip( + textDirection == .rtl + ? String(localized: "toolbar.textDirection.rtl", defaultValue: "Right-to-left") + : String(localized: "toolbar.textDirection.ltr", defaultValue: "Left-to-right") + )) } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index d0402f68a75..d21c4a50979 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1067,6 +1067,15 @@ struct cmuxApp: App { Divider() + splitCommandButton( + title: String(localized: "menu.view.toggleTextDirection", defaultValue: "Toggle Text Direction (LTR/RTL)"), + shortcut: menuShortcut(for: .toggleTextDirection) + ) { + TerminalTextDirectionSettings.toggleDirection() + } + + Divider() + // Numbered workspace selection (9 = last workspace) ForEach(1...9, id: \.self) { number in // `menuShortcut(for:)` already returns `.unbound` when the action diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index f6aef7b7685..d4fe4c07fb3 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -914,6 +914,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources 5E55400000000000000000D1 /* FocusStealingResponderConformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E55400000000000000000D2 /* FocusStealingResponderConformances.swift */; }; C0DEFB100000000000000001 /* FocusSurfaceBroadcaster.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEFB100000000000000002 /* FocusSurfaceBroadcaster.swift */; }; C0DEFB300000000000000001 /* FocusSurfaceBroadcasterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEFB300000000000000002 /* FocusSurfaceBroadcasterTests.swift */; }; + FBFA0000000000000000F002 /* Fonts in Resources */ = {isa = PBXBuildFile; fileRef = FBFA0000000000000000F001 /* Fonts */; }; AA5269A0C0DE0002FACE0002 /* ForeignFirstResponderPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5269A0C0DE0001FACE0001 /* ForeignFirstResponderPolicy.swift */; }; AA5269A0C0DE0004FACE0004 /* ForeignFirstResponderPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5269A0C0DE0003FACE0003 /* ForeignFirstResponderPolicyTests.swift */; }; F0F0CF0F0000000000000001 /* ForkParentFallbackGeneralizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0CF0F0000000000000002 /* ForkParentFallbackGeneralizationTests.swift */; }; @@ -987,6 +988,8 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources A6AC72010000000000000001 /* GPUSpinnerNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC72010000000000000002 /* GPUSpinnerNSView.swift */; }; A6AC72020000000000000001 /* GPUSpinnerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC72020000000000000002 /* GPUSpinnerStyle.swift */; }; C1ADE10002A1B2C3D4E5F719 /* grok in Copy CLI */ = {isa = PBXBuildFile; fileRef = C1ADE10001A1B2C3D4E5F719 /* grok */; }; + D3571A00A1B2C3D4E5F60801 /* HebrewAsciiQuotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3571A01A1B2C3D4E5F60801 /* HebrewAsciiQuotes.swift */; }; + 6190C0136190C0136190C013 /* HebrewAsciiQuotesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6190C0146190C0146190C014 /* HebrewAsciiQuotesTests.swift */; }; C0DE34020000000000000005 /* HelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000006 /* HelpMenuUITests.swift */; }; F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */; }; 4931A11B0000000000000002 /* HiddenRightSidebarContentMountingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4931A11B0000000000000001 /* HiddenRightSidebarContentMountingTests.swift */; }; @@ -1038,7 +1041,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources 8582A1000000000000000002 /* KimiResumeReviewRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8582A1000000000000000001 /* KimiResumeReviewRegressionTests.swift */; }; 87C609D63F03597AC12C8B0A /* LastSurfaceClosePreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2EE0C080FF0FCCE8A8CA930 /* LastSurfaceClosePreferenceTests.swift */; }; A11CE0060000000000000001 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = A11CE0050000000000000001 /* LICENSE */; }; - FBFA0000000000000000F002 /* Fonts in Resources */ = {isa = PBXBuildFile; fileRef = FBFA0000000000000000F001 /* Fonts */; }; DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; }; 53750022A0B1C2D3E4F50022 /* MacAuthComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53750023A0B1C2D3E4F50023 /* MacAuthComposition.swift */; }; D1F0A00600000000000000C1 /* MacPairedMacBackupBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F0A00600000000000000C2 /* MacPairedMacBackupBody.swift */; }; @@ -2347,7 +2349,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources A11CE0030000000000000001 /* AboutLicenseContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutLicenseContent.swift; sourceTree = ""; }; A11CE0020000000000000001 /* AboutLicensesResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutLicensesResourceTests.swift; sourceTree = ""; }; A9E010000000000000000006 /* agent-session-react */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "agent-session-react"; sourceTree = ""; }; - FBFA0000000000000000F001 /* Fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fonts; sourceTree = ""; }; A9E010000000000000000007 /* agent-session-solid */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "agent-session-solid"; sourceTree = ""; }; C7AF10000000000000000006 /* AgentChatArtifactGalleryBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "AgentChatArtifactGalleryBuilder.swift"; sourceTree = ""; }; C7AF10000000000000000002 /* AgentChatArtifactIndex.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "AgentChatArtifactIndex.swift"; sourceTree = ""; }; @@ -3186,6 +3187,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 5E55400000000000000000D2 /* FocusStealingResponderConformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusStealingResponderConformances.swift; sourceTree = ""; }; C0DEFB100000000000000002 /* FocusSurfaceBroadcaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusSurfaceBroadcaster.swift; sourceTree = ""; }; C0DEFB300000000000000002 /* FocusSurfaceBroadcasterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusSurfaceBroadcasterTests.swift; sourceTree = ""; }; + FBFA0000000000000000F001 /* Fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fonts; sourceTree = ""; }; AA5269A0C0DE0001FACE0001 /* ForeignFirstResponderPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/ForeignFirstResponderPolicy.swift; sourceTree = ""; }; AA5269A0C0DE0003FACE0003 /* ForeignFirstResponderPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignFirstResponderPolicyTests.swift; sourceTree = ""; }; F0F0CF0F0000000000000002 /* ForkParentFallbackGeneralizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForkParentFallbackGeneralizationTests.swift; sourceTree = ""; }; @@ -3260,6 +3262,8 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = A6AC72010000000000000002 /* GPUSpinnerNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/GPUSpinnerNSView.swift; sourceTree = ""; }; A6AC72020000000000000002 /* GPUSpinnerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/GPUSpinnerStyle.swift; sourceTree = ""; }; C1ADE10001A1B2C3D4E5F719 /* grok */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Resources/bin/grok; sourceTree = SOURCE_ROOT; }; + D3571A01A1B2C3D4E5F60801 /* HebrewAsciiQuotes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HebrewAsciiQuotes.swift; sourceTree = ""; }; + 6190C0146190C0146190C014 /* HebrewAsciiQuotesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HebrewAsciiQuotesTests.swift; sourceTree = ""; }; C0DE34020000000000000006 /* HelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpMenuUITests.swift; sourceTree = ""; }; F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HermesAgentIndex.swift; sourceTree = ""; }; 4931A11B0000000000000001 /* HiddenRightSidebarContentMountingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenRightSidebarContentMountingTests.swift; sourceTree = ""; }; @@ -5398,6 +5402,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D0B1000BA1B2C3D4E5F60001 /* GhosttyTerminalViewSupport.swift */, D0B10013A1B2C3D4E5F60001 /* GhosttyApp+SurfaceConfigurationReload.swift */, D3571001A1B2C3D4E5F60718 /* GhosttyNSView+IMEComposition.swift */, + D3571A01A1B2C3D4E5F60801 /* HebrewAsciiQuotes.swift */, EC010D02 /* GhosttyApp+TerminalCustomUpload.swift */, 85510004A1B2C3D4E5F60001 /* TerminalShellResolver+CurrentUser.swift */, EC010E02 /* GhosttyNSView+TerminalCustomUpload.swift */, @@ -6538,6 +6543,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = AA11BB22CC33DD44EE55F001 /* TerminalWindowPortalLayoutPassRefreshTests.swift */, 54F4341871F2416AB0E1F5DE /* TerminalWindowPortalEngineDivergenceTests.swift */, 6190C0116190C0116190C011 /* TerminalCopyOnSelectManagedConfigLayeringTests.swift */, + 6190C0146190C0146190C014 /* HebrewAsciiQuotesTests.swift */, 606600010000000000000002 /* WindowTerminalHostViewTitlebarHitTests.swift */, C0DE53360000000000000002 /* TerminalSearchOverlayMouseReleaseTests.swift */, C35610000000000000000002 /* FinderFileDropRegressionTests.swift */, @@ -7930,6 +7936,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 5B11E5A100000000000000B1 /* GPUSpinner.swift in Sources */, A6AC72010000000000000001 /* GPUSpinnerNSView.swift in Sources */, A6AC72020000000000000001 /* GPUSpinnerStyle.swift in Sources */, + D3571A00A1B2C3D4E5F60801 /* HebrewAsciiQuotes.swift in Sources */, F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */, CD0CFE6000000000CD0CFE60 /* HostAccountFlow.swift in Sources */, B1F0C0030000000000000001 /* HostedInspectorDockControlScript.swift in Sources */, @@ -9248,6 +9255,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C13519000000000000000003 /* GhosttyTerminalStartupEnvironmentTests.swift in Sources */, D7AB34400000000000000003 /* GhosttyTerminalViewVisibilityPolicyTests.swift in Sources */, 3865A0053865A0053865A005 /* GlobalSearchShortcutSettingsTests.swift in Sources */, + 6190C0136190C0136190C013 /* HebrewAsciiQuotesTests.swift in Sources */, 4931A11B0000000000000002 /* HiddenRightSidebarContentMountingTests.swift in Sources */, B1F0C0040000000000000001 /* HostedInspectorDockControlScriptTests.swift in Sources */, 809100000000000000000001 /* HostSettingsShortcutNotificationTests.swift in Sources */, @@ -9616,10 +9624,10 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -9703,7 +9711,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = NO; @@ -9712,7 +9720,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -9753,7 +9761,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = CODE_SIGN_ENTITLEMENTS = Resources/cmux.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = NO; @@ -9762,7 +9770,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "-lc++", @@ -9829,10 +9837,10 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -9847,7 +9855,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = buildSettings = { CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; @@ -9856,7 +9864,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = INFOPLIST_KEY_NSPrincipalClass = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app.docktileplugin.debug; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -9872,7 +9880,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = buildSettings = { CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; @@ -9881,7 +9889,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = INFOPLIST_KEY_NSPrincipalClass = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app.docktileplugin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -9896,10 +9904,10 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -9915,10 +9923,10 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 100; + CURRENT_PROJECT_VERSION = 104; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.64.20; + MARKETING_VERSION = 1.1.3; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/cmuxTests/HebrewAsciiQuotesTests.swift b/cmuxTests/HebrewAsciiQuotesTests.swift new file mode 100644 index 00000000000..9b38170cfb7 --- /dev/null +++ b/cmuxTests/HebrewAsciiQuotesTests.swift @@ -0,0 +1,121 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +/// Ivrix: the Hebrew layout puts geresh/gershayim on the quote keys, so a shell +/// command typed in Hebrew never reaches the shell with a real quote. +@Suite +struct HebrewAsciiQuotesTests { + private func makeDefaults() throws -> (UserDefaults, String) { + let suiteName = "cmux-hebrew-ascii-quotes-\(UUID().uuidString)" + return (try #require(UserDefaults(suiteName: suiteName)), suiteName) + } + + @Test + func gershayimBecomesAsciiDoubleQuoteByDefault() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + #expect(HebrewAsciiQuotes.normalized("\u{05F4}", defaults: defaults) == "\"") + } + + @Test + func gereshBecomesAsciiApostropheByDefault() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + #expect(HebrewAsciiQuotes.normalized("\u{05F3}", defaults: defaults) == "'") + } + + @Test + func rewritesEveryOccurrenceAndKeepsSurroundingText() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + let typed = "echo \u{05F4}\u{05E9}\u{05DC}\u{05D5}\u{05DD}\u{05F4}" + let expected = "echo \"\u{05E9}\u{05DC}\u{05D5}\u{05DD}\"" + + #expect(HebrewAsciiQuotes.normalized(typed, defaults: defaults) == expected) + } + + @Test(arguments: ["ls -la", "\u{05E9}\u{05DC}\u{05D5}\u{05DD}", "\"already ascii\"", ""]) + func leavesTextWithoutHebrewQuotePunctuationUntouched(_ text: String) throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + #expect(HebrewAsciiQuotes.normalized(text, defaults: defaults) == text) + } + + /// Turning the setting off is what makes Hebrew acronyms (צה״ל) typeable. + @Test + func disabledSettingSendsHebrewPunctuationAsTyped() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + defaults.set(false, forKey: HebrewAsciiQuotes.settingsKey) + + let acronym = "\u{05E6}\u{05D4}\u{05F4}\u{05DC}" + #expect(HebrewAsciiQuotes.normalized(acronym, defaults: defaults) == acronym) + } + + @Test + func explicitlyEnabledSettingRewrites() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + + defaults.set(true, forKey: HebrewAsciiQuotes.settingsKey) + + #expect(HebrewAsciiQuotes.normalized("\u{05F4}", defaults: defaults) == "\"") + } +} + +/// The titlebar control, the toolbar segmented control, the View menu item, and +/// the `toggleTextDirection` shortcut all flip direction through one path. +@Suite +struct TerminalTextDirectionToggleTests { + @Test + func toggleFlipsLtrToRtlAndBack() throws { + let suiteName = "cmux-text-direction-toggle-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let center = NotificationCenter() + + #expect(TerminalTextDirectionSettings.direction(defaults: defaults) == .ltr) + + #expect( + TerminalTextDirectionSettings.toggleDirection(defaults: defaults, notificationCenter: center) == .rtl + ) + #expect(TerminalTextDirectionSettings.direction(defaults: defaults) == .rtl) + #expect(TerminalTextDirectionSettings.ghosttyConfigContents(defaults: defaults) == "bidi-direction = rtl") + + #expect( + TerminalTextDirectionSettings.toggleDirection(defaults: defaults, notificationCenter: center) == .ltr + ) + #expect(TerminalTextDirectionSettings.direction(defaults: defaults) == .ltr) + } + + @Test + func toggleNotifiesSoLiveSurfacesReload() throws { + let suiteName = "cmux-text-direction-notify-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + + let center = NotificationCenter() + nonisolated(unsafe) var notifications = 0 + let token = center.addObserver( + forName: TerminalTextDirectionSettings.didChangeNotification, + object: nil, + queue: nil + ) { _ in notifications += 1 } + defer { center.removeObserver(token) } + + TerminalTextDirectionSettings.toggleDirection(defaults: defaults, notificationCenter: center) + + #expect(notifications == 1) + } +} diff --git a/cmuxTests/KeyboardShortcutContextTests.swift b/cmuxTests/KeyboardShortcutContextTests.swift index cb7b91cb989..db82276d87f 100644 --- a/cmuxTests/KeyboardShortcutContextTests.swift +++ b/cmuxTests/KeyboardShortcutContextTests.swift @@ -514,6 +514,44 @@ final class KeyboardShortcutContextTests: XCTestCase { ) } + /// Ivrix: the titlebar RTL/LTR button's hover tooltip names the direction + /// *and* the shortcut that flips it, and follows a rebind. + func testTextDirectionTitlebarTooltipShowsConfiguredShortcut() throws { + let originalSettingsFileStore = KeyboardShortcutSettings.settingsFileStore + let directoryURL = try makeTemporaryDirectory() + defer { + KeyboardShortcutSettings.resetAll() + KeyboardShortcutSettings.settingsFileStore = originalSettingsFileStore + try? FileManager.default.removeItem(at: directoryURL) + } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try writeSettingsFile("{}", to: settingsFileURL) + KeyboardShortcutSettings.settingsFileStore = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + additionalFallbackPaths: [], + startWatching: false + ) + KeyboardShortcutSettings.resetAll() + + let defaultShortcut = KeyboardShortcutSettings.shortcut(for: .toggleTextDirection) + XCTAssertFalse(defaultShortcut.isUnbound) + + let defaultTooltip = KeyboardShortcutSettings.Action.toggleTextDirection.tooltip("Right-to-left") + XCTAssertTrue(defaultTooltip.hasPrefix("Right-to-left")) + XCTAssertTrue(defaultTooltip.contains(defaultShortcut.displayString)) + + let remappedShortcut = StoredShortcut(key: "j", command: true, shift: true, option: true, control: false) + KeyboardShortcutSettings.setShortcut(remappedShortcut, for: .toggleTextDirection) + + XCTAssertTrue( + KeyboardShortcutSettings.Action.toggleTextDirection + .tooltip("Left-to-right") + .contains(remappedShortcut.displayString) + ) + } + func testShortcutSettingsFilePreservesConfiguredShortcutWithoutGlobalConflictLookup() throws { let directoryURL = try makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directoryURL) } diff --git a/docs/claude-code-osc133-request.md b/docs/claude-code-osc133-request.md new file mode 100644 index 00000000000..b2747bb1203 --- /dev/null +++ b/docs/claude-code-osc133-request.md @@ -0,0 +1,98 @@ +# Upstream ask: OSC 133 input marking around the Claude Code composer + +Draft of a feature request to file against `anthropics/claude-code`. Kept in +the repo so the terminal-side half and the ask that unblocks it stay together. + +--- + +**Title:** Mark the composer's input with OSC 133 `B` so terminals can treat it as an edit buffer + +**Body:** + +### What I'm asking for + +Emit `OSC 133;B` (`\x1b]133;B\x07`) at the point where the composer's editable +text begins, and `OSC 133;C` when it ends, on each redraw of the input box. + +That's the whole request. No new protocol, no negotiation, no change to how +Claude Code handles keys. + +### Why + +OSC 133 is the semantic prompt marking already spoken by zsh, fish, bash, and +PowerShell shell-integration hooks, and understood by Ghostty, kitty, WezTerm, +iTerm2, and VS Code's terminal. The `B` mark means "user input starts here". + +Terminals use it to tell a command line apart from the output around it, which +is what enables things like click-to-move-cursor, jump-to-previous-prompt, and +selecting a command without its prompt characters. + +Claude Code draws its composer as ordinary program output, so to a terminal +every cell of it is indistinguishable from transcript text. The terminal can +see a box and a cursor; it cannot tell which cells you can edit. + +### The concrete case + +I maintain [Ivrix](https://github.com/DananzMolt/Ivrix), a Ghostty-based +terminal focused on Hebrew and RTL. It has a feature where selecting text at a +prompt and typing replaces the selection, the way any text editor behaves. +There is no terminal protocol for "replace the selection", so it's emulated: +move the cursor to the selection start with arrow keys, issue one forward +delete per selected position, then let the keystroke through as an insert. + +That arithmetic needs to know which cells are input, because the count of +arrows has to match the count of editable positions. `B` marks are what supply +that. At a shell prompt it works in both English and Hebrew. In Claude Code it +is inert, because there are no marks to count against — and inert is the +correct outcome, since guessing where somebody else's input box begins and +issuing deletes based on the guess would corrupt what the user typed. + +With `B` marks the same machinery would work in Claude Code with no +Claude-specific code on the terminal side, and the same is true for every other +OSC 133-aware terminal and every other feature they build on it. + +### Why marks rather than the terminal guessing + +A terminal could try to infer the composer region from the box-drawing +characters and cursor position. I deliberately did not, because a +misidentification edits the user's real input. A mark is an assertion by the +application that owns the buffer; a heuristic is the terminal betting on +someone else's layout, and it will eventually lose. + +### Notes on the alternate screen + +Claude Code takes the alternate screen (`CSI ?1049h`). Some terminals veto OSC +133 there on the theory that a full-screen application implements its own +editing. + +That veto is a reasonable default and Ghostty ships it. It's also a proxy for +the real question. I've changed Ivrix so that selection editing keys off the +presence of input marks rather than off which screen is active +([commit](https://github.com/DananzMolt/ghostty/commit/f3f6bd8)): an +application that emits no marks is refused exactly as before, and one that +marks its input is trusted, on either screen. So on Ivrix this works the day +Claude Code emits the marks. + +I don't know how other terminals would treat marks on the alternate screen, and +that's worth checking before anyone depends on it. But it's a terminal-side +question, and emitting the marks is correct regardless: it costs a handful of +bytes per redraw and is ignored by everything that doesn't care. + +### Sketch + +Around wherever the composer's editable region is rendered: + +``` +\x1b]133;B\x07 \x1b]133;C\x07 +``` + +Worth guarding behind the same check as any other escape output (not a dumb +terminal, not piped, `TERM` sane), and it should be safe to emit +unconditionally otherwise: terminals that don't understand OSC 133 ignore the +sequence. + +### Prior art + +- [OSC 133 / FinalTerm shell integration spec](https://iterm2.com/documentation-escape-codes.html) +- [Ghostty shell integration docs](https://ghostty.org/docs/features/shell-integration) +- [VS Code terminal shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) diff --git a/ghostty b/ghostty index f64bbae74fb..f3f6bd8d081 160000 --- a/ghostty +++ b/ghostty @@ -1 +1 @@ -Subproject commit f64bbae74fb957ce3a38b6a31113eee90360a962 +Subproject commit f3f6bd8d08129bbc1390dd90b974241668258229 diff --git a/scripts/build-ivrix.sh b/scripts/build-ivrix.sh index 2380f861bb4..7af9fcf0577 100755 --- a/scripts/build-ivrix.sh +++ b/scripts/build-ivrix.sh @@ -52,6 +52,41 @@ PLIST="$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleExecutable Ivrix" "$PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.ivrix.app" "$PLIST" +# --- updater --- +# The "Update Available" button reads these two. Both have to name Ivrix. +# +# Getting this wrong is not a cosmetic bug: an Ivrix build carrying upstream's +# feed is offered upstream's releases, and installing one replaces Ivrix with +# cmux. Ivrix 1.1.0 shipped in exactly that state, which is why this is now a +# hard check rather than a comment. +FEED_URL="${IVRIX_SPARKLE_FEED_URL:-https://github.com/DananzMolt/Ivrix/releases/latest/download/appcast.xml}" +case "$FEED_URL" in + *manaflow-ai/cmux*) + echo "ERROR: refusing to build — Sparkle feed still points at upstream cmux:" >&2 + echo " $FEED_URL" >&2 + exit 1 + ;; +esac +/usr/libexec/PlistBuddy -c "Set :SUFeedURL $FEED_URL" "$PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :SUFeedURL string $FEED_URL" "$PLIST" + +# Sparkle refuses an update whose EdDSA signature does not match this key. The +# upstream key is present in the checked-in Info.plist because upstream signs +# with it; shipping it here would mean Ivrix trusts upstream's signature and +# nobody else's, including ours. +CMUX_PUBLIC_KEY="avjcgKibf1FTvhIjLBxhd+0HSpsXU4D0IGlVk8cgqRc=" +IVRIX_KEY="${IVRIX_SPARKLE_PUBLIC_KEY:-}" +if [[ -z "$IVRIX_KEY" || "$IVRIX_KEY" == "$CMUX_PUBLIC_KEY" ]]; then + echo "ERROR: IVRIX_SPARKLE_PUBLIC_KEY is unset or is upstream's key." >&2 + echo " Generate the Ivrix keypair once (private half stays in your Keychain):" >&2 + echo " \$DERIVED/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys" >&2 + echo " then re-run with IVRIX_SPARKLE_PUBLIC_KEY=" >&2 + exit 1 +fi +/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $IVRIX_KEY" "$PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string $IVRIX_KEY" "$PLIST" +echo " updater feed: $FEED_URL" + echo "==> [3/6] Bundling fonts into Resources/Fonts" mkdir -p "$APP/Contents/Resources/Fonts" cp -f Resources/Fonts/*.ttf "$APP/Contents/Resources/Fonts/" diff --git a/scripts/ivrix-appcast.sh b/scripts/ivrix-appcast.sh new file mode 100755 index 00000000000..c67e1b00549 --- /dev/null +++ b/scripts/ivrix-appcast.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Generate and sign the Sparkle appcast for an Ivrix release. +# +# The "Update Available" button reads this file. It must live on the Ivrix +# repo, not upstream's: an Ivrix build pointed at upstream's appcast is offered +# upstream's releases, and installing one replaces Ivrix with cmux. +# +# The EdDSA private key lives in the login Keychain, put there by Sparkle's +# `generate_keys`. It is never passed on the command line and never written to +# the repo; `sign_update` reads it from the Keychain by account name. +# +# Usage: +# scripts/ivrix-appcast.sh 1.1.1 ~/Desktop/Ivrix.dmg +# SPARKLE_ACCOUNT=ivrix scripts/ivrix-appcast.sh 1.1.1 ~/Desktop/Ivrix.dmg +# +# Output: appcast.xml next to the DMG, ready to upload as a release asset. +set -euo pipefail + +VERSION="${1:-}" +DMG="${2:-}" +if [[ -z "$VERSION" || -z "$DMG" ]]; then + echo "usage: $0 " >&2 + exit 2 +fi +test -f "$DMG" || { echo "ERROR: $DMG not found" >&2; exit 1; } + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_SLUG="${IVRIX_REPO_SLUG:-DananzMolt/Ivrix}" +TAG="${IVRIX_TAG:-ivrix-v$VERSION}" +OUT="$(dirname "$DMG")/appcast.xml" + +# Sparkle ships these tools inside the SPM artifact bundle. +SIGN_UPDATE="${SIGN_UPDATE:-}" +if [[ -z "$SIGN_UPDATE" ]]; then + SIGN_UPDATE="$(find "${DERIVED:-/tmp/cmux-ivrix-build}/SourcePackages/artifacts" \ + -name sign_update -type f 2>/dev/null | head -1)" +fi +test -x "$SIGN_UPDATE" || { + echo "ERROR: sign_update not found. Build once, or set SIGN_UPDATE=/path/to/sign_update" >&2 + exit 1 +} + +# Sparkle needs the build number, which is what it actually compares. +BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CURRENT_PROJECT_VERSION' /dev/stdin 2>/dev/null <<<"" || true)" +BUILD="${IVRIX_BUILD:-$(grep -m1 'CURRENT_PROJECT_VERSION = ' "$REPO/cmux.xcodeproj/project.pbxproj" | sed 's/.*= \(.*\);/\1/')}" + +LENGTH="$(stat -f%z "$DMG")" +PUBDATE="$(date -u '+%a, %d %b %Y %H:%M:%S +0000')" +URL="https://github.com/$REPO_SLUG/releases/download/$TAG/$(basename "$DMG")" + +echo "==> signing $DMG" +# `set -u` treats an empty array expansion as unbound on bash 3.2, which is +# what ships with macOS, so guard the expansion rather than the assignment. +SIG_ARGS=() +[[ -n "${SPARKLE_ACCOUNT:-}" ]] && SIG_ARGS+=(--account "$SPARKLE_ACCOUNT") +# sign_update prints: sparkle:edSignature="..." length="..." +SIGN_OUT="$("$SIGN_UPDATE" ${SIG_ARGS[@]+"${SIG_ARGS[@]}"} "$DMG")" +echo " $SIGN_OUT" + +cat > "$OUT" < + + + Ivrix + https://github.com/$REPO_SLUG + Hebrew-first terminal. + en + + $VERSION + $PUBDATE + $BUILD + $VERSION + 13.0 + https://github.com/$REPO_SLUG/releases/tag/$TAG + + + + +XML + +echo "==> wrote $OUT (version=$VERSION build=$BUILD length=$LENGTH)" +echo " upload it to the $TAG release so the feed URL resolves:" +echo " gh release upload $TAG \"$OUT\" --repo $REPO_SLUG" diff --git a/scripts/reload.sh b/scripts/reload.sh index bb3d576d54e..d958842c47b 100755 --- a/scripts/reload.sh +++ b/scripts/reload.sh @@ -10,6 +10,11 @@ source "$SCRIPT_DIR/lib/dev-secrets.sh" APP_NAME="cmux DEV" BUNDLE_ID="com.cmuxterm.app.debug" BASE_APP_NAME="cmux DEV" +# What the app calls itself in the Dock, menu bar and app switcher. Kept +# separate from APP_NAME, which names the .app directory: that has to keep +# matching what xcodebuild emits and what the rest of the dev tooling greps +# for, while this is purely the user-visible label. +DISPLAY_NAME="ivrix-dev" DERIVED_DATA="" NAME_SET=0 BUNDLE_SET=0 @@ -488,6 +493,10 @@ while [[ $# -gt 0 ]]; do echo "error: --name requires a value" >&2 exit 1 fi + # An explicit --name is naming the app, so it wins for the visible + # label too rather than leaving a build called one thing on disk and + # another in the Dock. + DISPLAY_NAME="$APP_NAME" NAME_SET=1 shift 2 ;; @@ -568,6 +577,7 @@ if [[ -n "$TAG" ]]; then TAG_SLUG="$(sanitize_path "$TAG")" if [[ "$NAME_SET" -eq 0 ]]; then APP_NAME="cmux DEV ${TAG_SLUG}" + DISPLAY_NAME="ivrix-dev ${TAG_SLUG}" fi if [[ "$BUNDLE_SET" -eq 0 ]]; then BUNDLE_ID="com.cmuxterm.app.debug.${TAG_ID}" @@ -717,8 +727,8 @@ if [[ "${CMUX_DISABLE_AUTOMATIC_PACKAGE_RESOLUTION:-}" == "1" ]]; then fi if [[ -z "$TAG" ]]; then XCODEBUILD_ARGS+=( - INFOPLIST_KEY_CFBundleName="$APP_NAME" - INFOPLIST_KEY_CFBundleDisplayName="$APP_NAME" + INFOPLIST_KEY_CFBundleName="$DISPLAY_NAME" + INFOPLIST_KEY_CFBundleDisplayName="$DISPLAY_NAME" ) fi XCODEBUILD_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID") @@ -971,10 +981,10 @@ if [[ -n "$TAG" && "$APP_NAME" != "$SEARCH_APP_NAME" ]]; then cp -R "$APP_PATH" "$TAG_APP_STAGING_PATH" INFO_PLIST="$TAG_APP_STAGING_PATH/Contents/Info.plist" if [[ -f "$INFO_PLIST" ]]; then - /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$INFO_PLIST" 2>/dev/null \ - || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $APP_NAME" "$INFO_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$INFO_PLIST" 2>/dev/null \ - || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $APP_NAME" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleName $DISPLAY_NAME" "$INFO_PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $DISPLAY_NAME" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $DISPLAY_NAME" "$INFO_PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $DISPLAY_NAME" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$INFO_PLIST" if [[ -n "${TAG_SLUG:-}" ]]; then diff --git a/skills/cmux-settings/references/all-keys.md b/skills/cmux-settings/references/all-keys.md index 48b62891135..5507b5406c4 100644 --- a/skills/cmux-settings/references/all-keys.md +++ b/skills/cmux-settings/references/all-keys.md @@ -35,6 +35,7 @@ Terminal presentation settings from Settings > Terminal. | Key | Type | Default | Description | |---|---|---|---| | `terminal.showScrollBar` | boolean | `true` | Show the right-edge terminal scroll bar when scrollback is available. cmux automatically suppresses it for alternate-screen style TUI surfaces. | +| `terminal.hebrewAsciiQuotes` | boolean | `true` | Rewrite HEBREW PUNCTUATION GERESH (U+05F3) and GERSHAYIM (U+05F4) typed on a Hebrew keyboard layout to ASCII `'` and `"` so shell quoting works. Set false to send Hebrew punctuation as typed, which is required for acronyms such as צה״ל. | | `terminal.autoResumeAgentSessions` | boolean | `true` | Automatically run agent resume commands for restored terminal sessions when cmux reopens after quit. Set false to restore panes while keeping Claude Code, Codex, OpenCode, and other saved agent sessions idle until you resume them manually. | ## notifications diff --git a/skills/cmux-settings/references/shortcut-actions.md b/skills/cmux-settings/references/shortcut-actions.md index 1418004ae3c..89a0462cdc9 100644 --- a/skills/cmux-settings/references/shortcut-actions.md +++ b/skills/cmux-settings/references/shortcut-actions.md @@ -54,6 +54,7 @@ Values for `shortcuts.bindings.`: - `shortcuts.bindings.newSurface` - `shortcuts.bindings.toggleTerminalCopyMode` - `shortcuts.bindings.clearScreenKeepScrollback` +- `shortcuts.bindings.toggleTextDirection` - `shortcuts.bindings.focusLeft` - `shortcuts.bindings.focusRight` - `shortcuts.bindings.focusUp` diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 5a18a2fe8db..f122fb2c29d 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -274,6 +274,7 @@ export const shortcutCategories: ShortcutCategory[] = [ }, { id: "toggleTerminalCopyMode", combos: [["⌘", "⇧", "M"]], description: { en: "Toggle terminal copy mode", ja: "ターミナルコピーモードを切り替え" } }, { id: "clearScreenKeepScrollback", combos: [["⌘", "⇧", "K"]], description: { en: "Clear screen (keep scrollback)", ja: "画面をクリア(スクロールバックを保持)" } }, + { id: "toggleTextDirection", combos: [["⌃", "⌘", "H"]], description: { en: "Toggle terminal text direction (LTR/RTL)", ja: "ターミナルのテキスト方向を切り替え(LTR/RTL)" } }, { id: "focusTextBoxInput", combos: [["⌘", "⇧", "A"]], description: { en: "Switch focus between terminal and TextBox input", ja: "ターミナルとTextBox入力のフォーカスを切り替え" } }, { id: "cycleTextBoxSubmitAction", combos: [["⇧", "Tab"]], description: { en: "Cycle TextBox submit action", ja: "TextBoxの送信アクションを切り替え" } }, { id: "attachTextBoxFile", combos: [["⌥", "⌘", "⇧", "A"]], description: { en: "Attach file to TextBox input", ja: "TextBox入力にファイルを添付" } }, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 9d32d5f2d96..67b1c3385aa 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -585,6 +585,12 @@ "descriptionKey": "schemaDescriptions.terminal.copyOnSelect", "description": "When true, copy selected terminal text to the system clipboard when the selection is committed. When false, cmux does not emit a Ghostty copy-on-select override; Ghostty config and defaults control selection-clipboard behavior." }, + "hebrewAsciiQuotes": { + "type": "boolean", + "default": true, + "descriptionKey": "schemaDescriptions.terminal.hebrewAsciiQuotes", + "description": "When true, typing HEBREW PUNCTUATION GERESH (U+05F3) or GERSHAYIM (U+05F4) on a Hebrew keyboard layout sends ASCII ' and \" to the terminal instead, so shell quoting works. Set false to send Hebrew punctuation as typed, which is required for acronyms such as צה״ל." + }, "autoResumeAgentSessions": { "type": "boolean", "default": true, @@ -1644,6 +1650,7 @@ "attachTextBoxFile", "sendCtrlFToTerminal", "clearScreenKeepScrollback", + "toggleTextDirection", "focusLeft", "focusRight", "focusUp", diff --git a/web/messages/ar.json b/web/messages/ar.json index 2ab6e3d77e3..d3753d9b5b5 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "حد أقصى اختياري للعرض، بالنقاط، لمحتوى الطرفية ودردشة الوكيل المضمنة. عيّن false لاستخدام عرض الجزء بالكامل.", "sessionContentAlignment": "الموضع الأفقي لمحتوى الطرفية ودردشة الوكيل المضمنة عند تفعيل sessionContentMaxWidth.", "copyOnSelect": "عند true، ينسخ النص المحدد في الطرفية إلى حافظة النظام عند تثبيت التحديد. عند false، لا يصدر cmux تجاوز Ghostty copy-on-select؛ وتتحكم إعدادات Ghostty وقيمه الافتراضية في سلوك حافظة التحديد.", + "hebrewAsciiQuotes": "عند ضبطها على true، تؤدي كتابة الجيرش العبري (U+05F3) أو الجيرشايم (U+05F4) بتخطيط لوحة مفاتيح عبرية إلى إرسال ' و \" بترميز ASCII إلى الطرفية بدلاً منهما، فيعمل الاقتباس في الصدفة. اضبطها على false لإرسال علامات الترقيم العبرية كما تُكتب، وهو مطلوب للاختصارات مثل צה״ל.", "scrollSpeed": "مُضاعِف يُطبَّق على مقدار التمرير بعجلة الفأرة ولوحة اللمس في الطرفية. القيم الأعلى تُمرِّر أسرع، والقيم الأدنى تُمرِّر أبطأ.", "showTextBoxOnNewTerminals": "يعرض إدخال TextBox التجريبي افتراضيًا لمساحات العمل وتبويبات الطرفية وتقسيماتها المنشأة حديثًا.", "focusTextBoxOnNewTerminals": "يركز إدخال TextBox التجريبي افتراضيًا لمساحات العمل وتبويبات الطرفية وتقسيماتها المنشأة حديثًا. التركيز يعرض TextBox أيضًا." diff --git a/web/messages/bs.json b/web/messages/bs.json index 59966be8668..cb6aaf7d1ee 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Opcionalna maksimalna širina, u tačkama, za sadržaj terminala i ugrađenog razgovora s agentom. Postavite false za korištenje pune širine okna.", "sessionContentAlignment": "Horizontalni položaj sadržaja terminala i ugrađenog razgovora s agentom kada je sessionContentMaxWidth omogućen.", "copyOnSelect": "Kada je true, odabrani tekst terminala kopira se u sistemski međuspremnik kada se odabir potvrdi. Kada je false, cmux ne emitira Ghostty copy-on-select nadjačavanje; Ghostty konfiguracija i zadane vrijednosti kontrolišu ponašanje međuspremnika odabira.", + "hebrewAsciiQuotes": "Kada je true, kucanje hebrejskog gereša (U+05F3) ili geršajima (U+05F4) na hebrejskom rasporedu tastature šalje terminalu ASCII ' i \", pa navodnici u shellu rade. Postavite false da se hebrejska interpunkcija šalje kako je otkucana, što je potrebno za skraćenice poput צה״ל.", "scrollSpeed": "Množitelj koji se primjenjuje na pomak točkića miša i dodirne plohe u terminalu. Veće vrijednosti pomiču brže, a manje sporije.", "showTextBoxOnNewTerminals": "Podrazumijevano prikazuje beta TextBox unos za novokreirane radne prostore, terminalske tabove i podjele.", "focusTextBoxOnNewTerminals": "Podrazumijevano fokusira beta TextBox unos za novokreirane radne prostore, terminalske tabove i podjele. Fokusiranje također prikazuje TextBox." diff --git a/web/messages/da.json b/web/messages/da.json index d538c733a8a..6d3798a8289 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Valgfri maksimal bredde i punkter for terminalindhold og den indbyggede agentchat. Angiv false for at bruge hele rudens bredde.", "sessionContentAlignment": "Vandret placering af terminalindhold og den indbyggede agentchat, når sessionContentMaxWidth er aktiveret.", "copyOnSelect": "Når værdien er true, kopieres markeret terminaltekst til systemets udklipsholder, når markeringen bekræftes. Når værdien er false, udsender cmux ikke en Ghostty copy-on-select-tilsidesættelse; Ghostty-konfiguration og standarder styrer markeringsudklipsholderens adfærd.", + "hebrewAsciiQuotes": "Når den er true, sender geresh (U+05F3) eller gershayim (U+05F4) på et hebraisk tastaturlayout i stedet ASCII ' og \" til terminalen, så anførselstegn virker i shellen. Sæt den til false for at sende hebraisk tegnsætning, som den skrives, hvilket kræves til forkortelser som צה״ל.", "scrollSpeed": "Multiplikator anvendt på rulleværdier fra musehjul og pegefelt i terminalen. Højere værdier ruller hurtigere, lavere værdier langsommere.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-input som standard for nyoprettede arbejdsområder, terminalfaner og terminalsplits.", "focusTextBoxOnNewTerminals": "Fokuserer beta-TextBox-input som standard for nyoprettede arbejdsområder, terminalfaner og terminalsplits. Fokus viser også TextBox." diff --git a/web/messages/de.json b/web/messages/de.json index 1dd96275f9e..6d42a965244 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Optionale maximale Breite in Punkten für Terminalinhalte und den integrierten Agentenchat. Mit false wird die gesamte Breite des Bereichs verwendet.", "sessionContentAlignment": "Horizontale Position von Terminalinhalten und integriertem Agentenchat, wenn sessionContentMaxWidth aktiviert ist.", "copyOnSelect": "Bei true wird ausgewählter Terminaltext in die Systemzwischenablage kopiert, sobald die Auswahl bestätigt wird. Bei false gibt cmux keine Ghostty-copy-on-select-Überschreibung aus; Ghostty-Konfiguration und Standardwerte steuern das Verhalten der Auswahlzwischenablage.", + "hebrewAsciiQuotes": "Bei true senden Geresh (U+05F3) und Gerschajim (U+05F4) auf einem hebräischen Tastaturlayout stattdessen ASCII ' und \" an das Terminal, damit Quoting in der Shell funktioniert. Auf false setzen, um hebräische Satzzeichen unverändert zu senden, was für Abkürzungen wie צה״ל nötig ist.", "scrollSpeed": "Multiplikator für die Scroll-Werte von Mausrad und Trackpad im Terminal. Höhere Werte scrollen schneller, niedrigere Werte langsamer.", "showTextBoxOnNewTerminals": "Zeigt die Beta-TextBox-Eingabe standardmaessig fuer neu erstellte Workspaces, Terminal-Tabs und Terminal-Splits an.", "focusTextBoxOnNewTerminals": "Fokussiert die Beta-TextBox-Eingabe standardmaessig fuer neu erstellte Workspaces, Terminal-Tabs und Terminal-Splits. Fokussieren zeigt die TextBox ebenfalls an." diff --git a/web/messages/en.json b/web/messages/en.json index 043eb56de19..dfabfb9483a 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -1723,6 +1723,7 @@ }, "terminal": { "copyOnSelect": "When true, copy selected terminal text to the system clipboard when the selection is committed. When false, cmux does not emit a Ghostty copy-on-select override; Ghostty config and defaults control selection-clipboard behavior.", + "hebrewAsciiQuotes": "When true, typing HEBREW PUNCTUATION GERESH (U+05F3) or GERSHAYIM (U+05F4) on a Hebrew keyboard layout sends ASCII ' and \" to the terminal instead, so shell quoting works. Set false to send Hebrew punctuation as typed, which is required for acronyms such as צה״ל.", "scrollSpeed": "Multiplier applied to terminal scroll wheel and trackpad deltas. Higher values scroll faster; lower values scroll slower.", "sessionContentMaxWidth": "Optional maximum width, in points, for terminal and built-in agent chat content. Set false to use the full pane width.", "sessionContentAlignment": "Horizontal placement for terminal and built-in agent chat content when sessionContentMaxWidth is enabled.", diff --git a/web/messages/es.json b/web/messages/es.json index 7e13ac3dbe3..d2d9d3e772f 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Ancho máximo opcional, en puntos, para el contenido del terminal y del chat de agente integrado. Establece false para usar todo el ancho del panel.", "sessionContentAlignment": "Posición horizontal del contenido del terminal y del chat de agente integrado cuando sessionContentMaxWidth está activado.", "copyOnSelect": "Cuando es true, copia el texto seleccionado del terminal al portapapeles del sistema cuando se confirma la selección. Cuando es false, cmux no emite una anulación de copy-on-select de Ghostty; la configuración y los valores predeterminados de Ghostty controlan el comportamiento del portapapeles de selección.", + "hebrewAsciiQuotes": "Cuando es true, escribir gueresh (U+05F3) o guershayim (U+05F4) con una distribución de teclado hebrea envía al terminal ' y \" ASCII, para que las comillas funcionen en el intérprete de comandos. Ponlo en false para enviar la puntuación hebrea tal cual, necesario para siglas como צה״ל.", "scrollSpeed": "Multiplicador aplicado a los desplazamientos de la rueda de desplazamiento y del panel táctil en el terminal. Los valores más altos desplazan más rápido y los más bajos, más lento.", "showTextBoxOnNewTerminals": "Muestra la entrada TextBox beta de forma predeterminada en espacios de trabajo, pestañas y divisiones de terminal recién creados.", "focusTextBoxOnNewTerminals": "Enfoca la entrada TextBox beta de forma predeterminada en espacios de trabajo, pestañas y divisiones de terminal recién creados. Enfocar también muestra TextBox." diff --git a/web/messages/fr.json b/web/messages/fr.json index 7ba6206c4de..34e373a0a3d 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Largeur maximale facultative, en points, pour le contenu du terminal et du chat d’agent intégré. Définissez false pour utiliser toute la largeur du volet.", "sessionContentAlignment": "Position horizontale du contenu du terminal et du chat d’agent intégré lorsque sessionContentMaxWidth est activé.", "copyOnSelect": "Lorsque la valeur est true, le texte sélectionné dans le terminal est copié dans le presse-papiers système quand la sélection est validée. Lorsque la valeur est false, cmux n’émet pas de remplacement Ghostty copy-on-select; la configuration et les valeurs par défaut de Ghostty contrôlent le comportement du presse-papiers de sélection.", + "hebrewAsciiQuotes": "Lorsque true, saisir un guéresh (U+05F3) ou un guershayim (U+05F4) avec une disposition de clavier hébraïque envoie ' et \" ASCII au terminal, pour que les guillemets fonctionnent dans le shell. Mettez false pour envoyer la ponctuation hébraïque telle quelle, nécessaire aux sigles comme צה״ל.", "scrollSpeed": "Multiplicateur appliqué aux déplacements de la molette de défilement et du pavé tactile dans le terminal. Les valeurs plus élevées font défiler plus vite, les plus basses plus lentement.", "showTextBoxOnNewTerminals": "Affiche l'entrée TextBox bêta par défaut pour les espaces de travail, onglets et divisions de terminal nouvellement créés.", "focusTextBoxOnNewTerminals": "Place le focus sur l'entrée TextBox bêta par défaut pour les espaces de travail, onglets et divisions de terminal nouvellement créés. Le focus affiche aussi TextBox." diff --git a/web/messages/it.json b/web/messages/it.json index 81bb31554ac..588f1bdba0f 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Larghezza massima facoltativa, in punti, per il contenuto del terminale e della chat agente integrata. Imposta false per usare l’intera larghezza del riquadro.", "sessionContentAlignment": "Posizionamento orizzontale del contenuto del terminale e della chat agente integrata quando sessionContentMaxWidth è abilitato.", "copyOnSelect": "Quando è true, il testo selezionato nel terminale viene copiato negli appunti di sistema quando la selezione viene confermata. Quando è false, cmux non emette un override Ghostty copy-on-select; configurazione e valori predefiniti di Ghostty controllano il comportamento degli appunti di selezione.", + "hebrewAsciiQuotes": "Se true, digitando gheresh (U+05F3) o ghershayim (U+05F4) con un layout di tastiera ebraico vengono inviati al terminale ' e \" ASCII, così le virgolette funzionano nella shell. Imposta false per inviare la punteggiatura ebraica così com'è, necessario per acronimi come צה״ל.", "scrollSpeed": "Moltiplicatore applicato agli spostamenti della rotellina di scorrimento e del trackpad nel terminale. Valori più alti scorrono più velocemente, valori più bassi più lentamente.", "showTextBoxOnNewTerminals": "Mostra l'input TextBox beta per impostazione predefinita nei workspace, nelle schede e nelle divisioni del terminale appena creati.", "focusTextBoxOnNewTerminals": "Mette a fuoco l'input TextBox beta per impostazione predefinita nei workspace, nelle schede e nelle divisioni del terminale appena creati. Il focus mostra anche TextBox." diff --git a/web/messages/ja.json b/web/messages/ja.json index fce6bd2c8da..0e4005928cc 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -1646,6 +1646,7 @@ }, "terminal": { "copyOnSelect": "true の場合、選択が確定したときにターミナルで選択したテキストをシステムクリップボードへコピーします。false の場合、cmux は Ghostty の copy-on-select 上書きを出力せず、選択クリップボードの動作は Ghostty の設定と既定値に従います。", + "hebrewAsciiQuotes": "true の場合、ヘブライ語キーボード配列でゲレシュ (U+05F3) やゲルシャイム (U+05F4) を入力すると、代わりに ASCII の ' と \" をターミナルへ送信し、シェルの引用符が機能します。false にするとヘブライ語の約物をそのまま送信します。צה״ל のような略語にはこちらが必要です。", "scrollSpeed": "ターミナルのスクロールホイールとトラックパッドの移動量に適用される倍率です。値を大きくするとスクロールが速くなり、小さくすると遅くなります。", "sessionContentMaxWidth": "ターミナルと組み込みエージェントチャットのコンテンツ幅の上限をポイント単位で指定します。ペインの全幅を使うには false を設定します。", "sessionContentAlignment": "sessionContentMaxWidth が有効な場合の、ターミナルと組み込みエージェントチャットの水平方向の配置です。", diff --git a/web/messages/km.json b/web/messages/km.json index 7de5e51c9c0..4201fbe7ab1 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "ទទឹងអតិបរមាជាជម្រើស គិតជាពិន្ទុ សម្រាប់ខ្លឹមសារស្ថានីយ និងការជជែកជាមួយភ្នាក់ងារដែលភ្ជាប់មកជាមួយ។ កំណត់ false ដើម្បីប្រើទទឹងផ្ទាំងទាំងមូល។", "sessionContentAlignment": "ទីតាំងផ្ដេកនៃខ្លឹមសារស្ថានីយ និងការជជែកជាមួយភ្នាក់ងារដែលភ្ជាប់មកជាមួយ នៅពេលបើក sessionContentMaxWidth។", "copyOnSelect": "នៅពេល true អត្ថបទ terminal ដែលបានជ្រើសនឹងត្រូវចម្លងទៅ clipboard របស់ប្រព័ន្ធ ពេលការជ្រើសត្រូវបានបញ្ជាក់។ នៅពេល false cmux មិនបញ្ចេញការកំណត់ជាន់លើ Ghostty copy-on-select ទេ; ការកំណត់ និងលំនាំដើមរបស់ Ghostty គ្រប់គ្រងអាកប្បកិរិយា clipboard សម្រាប់ការជ្រើស។", + "hebrewAsciiQuotes": "នៅពេល true ការវាយ geresh (U+05F3) ឬ gershayim (U+05F4) លើប្លង់ក្ដារចុចហេប្រ៊ូ នឹងផ្ញើ ' និង \" បែប ASCII ទៅស្ថានីយជំនួស ដើម្បីឲ្យសញ្ញាសម្រង់ក្នុង shell ដំណើរការ។ កំណត់ជា false ដើម្បីផ្ញើសញ្ញាវណ្ណយុត្តហេប្រ៊ូតាមការវាយ ដែលចាំបាច់សម្រាប់អក្សរកាត់ដូចជា צה״ל។", "scrollSpeed": "មេគុណដែលអនុវត្តចំពោះចលនារមូររបស់កង់រមូរ និងផ្ទាំងប៉ះក្នុង terminal។ តម្លៃកាន់តែខ្ពស់ រមូរកាន់តែលឿន ហើយតម្លៃកាន់តែទាប រមូរកាន់តែយឺត។", "showTextBoxOnNewTerminals": "បង្ហាញការបញ្ចូល TextBox បេតាជាលំនាំដើមសម្រាប់ workspace ផ្ទាំងស្ថានីយ និងស្ប្លីតស្ថានីយដែលទើបបង្កើតថ្មី។", "focusTextBoxOnNewTerminals": "ផ្តោតការបញ្ចូល TextBox បេតាជាលំនាំដើមសម្រាប់ workspace ផ្ទាំងស្ថានីយ និងស្ប្លីតស្ថានីយដែលទើបបង្កើតថ្មី។ ការផ្តោតក៏បង្ហាញ TextBox ផងដែរ។" diff --git a/web/messages/ko.json b/web/messages/ko.json index 070b2b2deda..86b59565d10 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "터미널 및 내장 에이전트 채팅 콘텐츠의 선택적 최대 너비(포인트)입니다. 전체 패널 너비를 사용하려면 false로 설정하세요.", "sessionContentAlignment": "sessionContentMaxWidth가 활성화된 경우 터미널 및 내장 에이전트 채팅 콘텐츠의 가로 배치입니다.", "copyOnSelect": "true이면 선택이 확정될 때 선택한 터미널 텍스트를 시스템 클립보드에 복사합니다. false이면 cmux는 Ghostty copy-on-select 재정의를 내보내지 않으며, 선택 클립보드 동작은 Ghostty 설정과 기본값이 제어합니다.", + "hebrewAsciiQuotes": "true이면 히브리어 자판에서 게레시(U+05F3)나 게르샤임(U+05F4)을 입력할 때 터미널로 ASCII ' 와 \" 를 대신 보내 셸 따옴표가 동작합니다. false로 설정하면 히브리어 문장 부호를 입력한 그대로 보내며, צה״ל 같은 약어에는 이 설정이 필요합니다.", "scrollSpeed": "터미널 스크롤 휠과 트랙패드 이동량에 적용되는 배율입니다. 값이 클수록 빠르게 스크롤되고, 작을수록 느리게 스크롤됩니다.", "showTextBoxOnNewTerminals": "새로 생성된 작업공간, 터미널 탭, 터미널 분할에서 베타 TextBox 입력을 기본으로 표시합니다.", "focusTextBoxOnNewTerminals": "새로 생성된 작업공간, 터미널 탭, 터미널 분할에서 베타 TextBox 입력에 기본으로 포커스합니다. 포커스하면 TextBox도 표시됩니다." diff --git a/web/messages/no.json b/web/messages/no.json index 73558ae629d..b2b13f1130b 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Valgfri maksimal bredde i punkter for terminalinnhold og den innebygde agentchatten. Angi false for å bruke hele rutens bredde.", "sessionContentAlignment": "Vannrett plassering av terminalinnhold og den innebygde agentchatten når sessionContentMaxWidth er aktivert.", "copyOnSelect": "Når verdien er true, kopieres markert terminaltekst til systemutklippstavlen når markeringen bekreftes. Når verdien er false, sender ikke cmux ut en Ghostty copy-on-select-overstyring; Ghostty-konfigurasjon og standardverdier styrer oppførselen til markeringsutklippstavlen.", + "hebrewAsciiQuotes": "Når true sender geresh (U+05F3) eller gershayim (U+05F4) på et hebraisk tastaturoppsett i stedet ASCII ' og \" til terminalen, slik at anførselstegn virker i skallet. Sett false for å sende hebraisk tegnsetting slik den skrives, noe som kreves for forkortelser som צה״ל.", "scrollSpeed": "Multiplikator brukt på rulleverdier fra musehjul og styreflate i terminalen. Høyere verdier ruller raskere, lavere verdier saktere.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-inndata som standard for nyopprettede arbeidsområder, terminalfaner og terminalsplitt.", "focusTextBoxOnNewTerminals": "Fokuserer beta-TextBox-inndata som standard for nyopprettede arbeidsområder, terminalfaner og terminalsplitt. Fokus viser også TextBox." diff --git a/web/messages/pl.json b/web/messages/pl.json index c746aaa833e..a3ff489a7dd 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -961,6 +961,7 @@ "sessionContentMaxWidth": "Opcjonalna maksymalna szerokość w punktach dla zawartości terminala i wbudowanego czatu agenta. Ustaw false, aby użyć pełnej szerokości panelu.", "sessionContentAlignment": "Poziome położenie zawartości terminala i wbudowanego czatu agenta po włączeniu sessionContentMaxWidth.", "copyOnSelect": "Gdy wartość to true, zaznaczony tekst terminala jest kopiowany do schowka systemowego po zatwierdzeniu zaznaczenia. Gdy wartość to false, cmux nie emituje nadpisania Ghostty copy-on-select; konfiguracja i wartości domyślne Ghostty sterują zachowaniem schowka zaznaczenia.", + "hebrewAsciiQuotes": "Gdy true, wpisanie geresz (U+05F3) lub gerszajim (U+05F4) w hebrajskim układzie klawiatury wysyła do terminala ASCII ' i \", dzięki czemu cudzysłowy działają w powłoce. Ustaw false, aby wysyłać hebrajską interpunkcję bez zmian, co jest potrzebne do skrótowców takich jak צה״ל.", "scrollSpeed": "Mnożnik stosowany do przewijania kółkiem myszy i gładzikiem w terminalu. Wyższe wartości przewijają szybciej, niższe wolniej.", "showTextBoxOnNewTerminals": "Domyślnie pokazuje wejście TextBox beta dla nowo utworzonych obszarów roboczych, kart terminala i podziałów.", "focusTextBoxOnNewTerminals": "Domyślnie ustawia fokus na wejściu TextBox beta dla nowo utworzonych obszarów roboczych, kart terminala i podziałów. Fokus pokazuje też TextBox." diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 74eba13945b..f0771dbc512 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Largura máxima opcional, em pontos, para o conteúdo do terminal e do chat integrado do agente. Defina como false para usar toda a largura do painel.", "sessionContentAlignment": "Posicionamento horizontal do conteúdo do terminal e do chat integrado do agente quando sessionContentMaxWidth está ativado.", "copyOnSelect": "Quando true, copia o texto selecionado no terminal para a área de transferência do sistema quando a seleção é confirmada. Quando false, o cmux não emite uma substituição copy-on-select do Ghostty; a configuração e os padrões do Ghostty controlam o comportamento da área de transferência de seleção.", + "hebrewAsciiQuotes": "Quando true, digitar gueresh (U+05F3) ou guershayim (U+05F4) em um layout de teclado hebraico envia ' e \" ASCII ao terminal, para que as aspas funcionem no shell. Defina false para enviar a pontuação hebraica como digitada, necessário para siglas como צה״ל.", "scrollSpeed": "Multiplicador aplicado aos deslocamentos da roda de rolagem e do trackpad no terminal. Valores mais altos rolam mais rápido e os mais baixos, mais devagar.", "showTextBoxOnNewTerminals": "Mostra a entrada TextBox beta por padrão em áreas de trabalho, abas e divisões de terminal recém-criadas.", "focusTextBoxOnNewTerminals": "Foca a entrada TextBox beta por padrão em áreas de trabalho, abas e divisões de terminal recém-criadas. Focar também mostra o TextBox." diff --git a/web/messages/ru.json b/web/messages/ru.json index 6389e42e3a8..7461e7294f6 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Необязательная максимальная ширина в пунктах для содержимого терминала и встроенного чата агента. Укажите false, чтобы использовать всю ширину панели.", "sessionContentAlignment": "Горизонтальное расположение содержимого терминала и встроенного чата агента при включённом sessionContentMaxWidth.", "copyOnSelect": "Если значение true, выбранный текст терминала копируется в системный буфер обмена после подтверждения выделения. Если значение false, cmux не выводит переопределение Ghostty copy-on-select; конфигурация и значения по умолчанию Ghostty управляют поведением буфера выделения.", + "hebrewAsciiQuotes": "Если true, ввод гереша (U+05F3) или гершаима (U+05F4) в еврейской раскладке клавиатуры отправляет в терминал ASCII ' и \", поэтому кавычки работают в оболочке. Установите false, чтобы отправлять еврейскую пунктуацию как есть — это нужно для аббревиатур вроде צה״ל.", "scrollSpeed": "Множитель, применяемый к величине прокрутки колесом мыши и трекпадом в терминале. Чем выше значение, тем быстрее прокрутка, чем ниже — тем медленнее.", "showTextBoxOnNewTerminals": "По умолчанию показывает бета-ввод TextBox для новых рабочих пространств, вкладок терминала и разделений.", "focusTextBoxOnNewTerminals": "По умолчанию фокусирует бета-ввод TextBox для новых рабочих пространств, вкладок терминала и разделений. Фокус также показывает TextBox." diff --git a/web/messages/th.json b/web/messages/th.json index a7ba1a0bc6f..e7fced43587 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -961,6 +961,7 @@ "sessionContentMaxWidth": "ความกว้างสูงสุดแบบไม่บังคับในหน่วยพอยต์สำหรับเนื้อหาเทอร์มินัลและแชตเอเจนต์ในตัว ตั้งค่าเป็น false เพื่อใช้ความกว้างเต็มพาเนล", "sessionContentAlignment": "ตำแหน่งแนวนอนของเนื้อหาเทอร์มินัลและแชตเอเจนต์ในตัวเมื่อเปิดใช้ sessionContentMaxWidth", "copyOnSelect": "เมื่อเป็น true ข้อความเทอร์มินัลที่เลือกจะถูกคัดลอกไปยังคลิปบอร์ดของระบบเมื่อยืนยันการเลือก เมื่อเป็น false cmux จะไม่ส่งค่าแทนที่ Ghostty copy-on-select; การกำหนดค่าและค่าเริ่มต้นของ Ghostty จะควบคุมพฤติกรรมของคลิปบอร์ดการเลือก", + "hebrewAsciiQuotes": "เมื่อเป็น true การพิมพ์ geresh (U+05F3) หรือ gershayim (U+05F4) บนผังแป้นพิมพ์ฮีบรูจะส่ง ' และ \" แบบ ASCII ไปยังเทอร์มินัลแทน เพื่อให้เครื่องหมายคำพูดในเชลล์ทำงานได้ ตั้งเป็น false เพื่อส่งเครื่องหมายวรรคตอนภาษาฮีบรูตามที่พิมพ์ ซึ่งจำเป็นสำหรับอักษรย่อ เช่น צה״ל", "scrollSpeed": "ตัวคูณที่ใช้กับระยะการเลื่อนของล้อเลื่อนเมาส์และแทร็กแพดในเทอร์มินัล ค่ายิ่งสูงยิ่งเลื่อนเร็ว ค่ายิ่งต่ำยิ่งเลื่อนช้า", "showTextBoxOnNewTerminals": "แสดงอินพุต TextBox รุ่นเบต้าเป็นค่าเริ่มต้นสำหรับเวิร์กสเปซ แท็บเทอร์มินัล และการแบ่งเทอร์มินัลที่สร้างใหม่", "focusTextBoxOnNewTerminals": "โฟกัสอินพุต TextBox รุ่นเบต้าเป็นค่าเริ่มต้นสำหรับเวิร์กสเปซ แท็บเทอร์มินัล และการแบ่งเทอร์มินัลที่สร้างใหม่ การโฟกัสจะแสดง TextBox ด้วย" diff --git a/web/messages/tr.json b/web/messages/tr.json index bc8f395f380..393b0a0e81c 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Terminal ve yerleşik ajan sohbeti içeriği için puan cinsinden isteğe bağlı maksimum genişlik. Bölmenin tamamını kullanmak için false olarak ayarlayın.", "sessionContentAlignment": "sessionContentMaxWidth etkin olduğunda terminal ve yerleşik ajan sohbeti içeriğinin yatay konumu.", "copyOnSelect": "true olduğunda, seçim kesinleştiğinde seçilen terminal metni sistem panosuna kopyalanır. false olduğunda cmux bir Ghostty copy-on-select geçersiz kılması yaymaz; seçim panosu davranışını Ghostty yapılandırması ve varsayılanları kontrol eder.", + "hebrewAsciiQuotes": "true olduğunda, İbranice klavye düzeninde geresh (U+05F3) veya gershayim (U+05F4) yazmak terminale bunların yerine ASCII ' ve \" gönderir, böylece kabuktaki tırnaklar çalışır. İbranice noktalamayı yazıldığı gibi göndermek için false yapın; צה״ל gibi kısaltmalar için gereklidir.", "scrollSpeed": "Terminalde fare tekerleği ve dokunmatik yüzey kaydırma değerlerine uygulanan çarpan. Yüksek değerler daha hızlı, düşük değerler daha yavaş kaydırır.", "showTextBoxOnNewTerminals": "Yeni oluşturulan çalışma alanları, terminal sekmeleri ve terminal bölmeleri için beta TextBox girişini varsayılan olarak gösterir.", "focusTextBoxOnNewTerminals": "Yeni oluşturulan çalışma alanları, terminal sekmeleri ve terminal bölmeleri için beta TextBox girişini varsayılan olarak odaklar. Odaklamak TextBox'ı da gösterir." diff --git a/web/messages/uk.json b/web/messages/uk.json index 5a23cae817a..6b0a7f6bed4 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "Необов’язкова максимальна ширина в пунктах для вмісту термінала та вбудованого чату агента. Значення false використовує всю ширину панелі.", "sessionContentAlignment": "Горизонтальне розташування вмісту термінала та вбудованого чату агента, коли ввімкнено sessionContentMaxWidth.", "copyOnSelect": "Якщо значення true, вибраний текст термінала копіюється до системного буфера обміну після підтвердження виділення. Якщо значення false, cmux не виводить перевизначення Ghostty copy-on-select; конфігурація та стандартні значення Ghostty керують поведінкою буфера виділення.", + "hebrewAsciiQuotes": "Якщо true, введення гереша (U+05F3) або гершаїма (U+05F4) в івритській розкладці надсилає в термінал ASCII ' і \", тож лапки працюють в оболонці. Встановіть false, щоб надсилати івритську пунктуацію як є — це потрібно для абревіатур на кшталт צה״ל.", "scrollSpeed": "Множник, що застосовується до величини прокручування коліщатком миші та трекпадом у терміналі. Вищі значення прокручують швидше, нижчі — повільніше.", "showTextBoxOnNewTerminals": "Типово показує бета-ввід TextBox для новостворених робочих просторів, вкладок термінала й розділень.", "focusTextBoxOnNewTerminals": "Типово фокусує бета-ввід TextBox для новостворених робочих просторів, вкладок термінала й розділень. Фокусування також показує TextBox." diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 99f359b6d24..2d0b0667867 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "终端和内置代理聊天内容的可选最大宽度,单位为点。设为 false 可使用整个窗格宽度。", "sessionContentAlignment": "启用 sessionContentMaxWidth 时,终端和内置代理聊天内容的水平位置。", "copyOnSelect": "为 true 时,在确认选择后将所选终端文本复制到系统剪贴板。为 false 时,cmux 不会输出 Ghostty copy-on-select 覆盖项;选择剪贴板行为由 Ghostty 配置和默认值控制。", + "hebrewAsciiQuotes": "为 true 时,在希伯来语键盘布局下输入 geresh (U+05F3) 或 gershayim (U+05F4) 会改为向终端发送 ASCII 的 ' 和 \",这样 shell 引号才有效。设为 false 则按输入原样发送希伯来语标点,צה״ל 之类的缩写需要它。", "scrollSpeed": "应用于终端滚轮和触控板滚动量的倍数。值越大滚动越快,值越小滚动越慢。", "showTextBoxOnNewTerminals": "默认在新建的工作区、终端标签页和终端分屏中显示测试版 TextBox 输入。", "focusTextBoxOnNewTerminals": "默认在新建的工作区、终端标签页和终端分屏中聚焦测试版 TextBox 输入。聚焦也会显示 TextBox。" diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index fb21c49d7c8..94e8f4a81bb 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -960,6 +960,7 @@ "sessionContentMaxWidth": "終端機與內建代理程式聊天內容的選用最大寬度,單位為點。設為 false 可使用整個窗格寬度。", "sessionContentAlignment": "啟用 sessionContentMaxWidth 時,終端機與內建代理程式聊天內容的水平位置。", "copyOnSelect": "為 true 時,選取確認後會將所選終端機文字複製到系統剪貼簿。為 false 時,cmux 不會輸出 Ghostty copy-on-select 覆寫;選取剪貼簿行為由 Ghostty 設定與預設值控制。", + "hebrewAsciiQuotes": "為 true 時,在希伯來文鍵盤配置下輸入 geresh (U+05F3) 或 gershayim (U+05F4) 會改為向終端機送出 ASCII 的 ' 與 \",這樣 shell 引號才有效。設為 false 則按輸入原樣送出希伯來文標點,צה״ל 之類的縮寫需要它。", "scrollSpeed": "套用於終端機滾輪與觸控板捲動量的倍率。數值越大捲動越快,數值越小捲動越慢。", "showTextBoxOnNewTerminals": "預設在新建的工作區、終端機分頁和終端機分割中顯示測試版 TextBox 輸入。", "focusTextBoxOnNewTerminals": "預設在新建的工作區、終端機分頁和終端機分割中聚焦測試版 TextBox 輸入。聚焦也會顯示 TextBox。"