Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions Resources/InfoPlist.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,102 @@
"state": "translated",
"value": "cmux 内で実行中のプログラムが、パスキーとセキュリティキーを検出するために Bluetooth の使用を求めています。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 中运行的程序想要使用蓝牙来发现通行密钥和安全密钥。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 中執行的程式想要使用藍牙來探索通行密鑰和安全密鑰。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "cmux 내에서 실행 중인 프로그램이 패스키와 보안 키를 찾기 위해 Bluetooth를 사용하려고 합니다."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Ein in cmux ausgeführtes Programm möchte Bluetooth verwenden, um Passkeys und Sicherheitsschlüssel zu finden."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Un programa en ejecución dentro de cmux quiere usar Bluetooth para descubrir llaves de acceso y claves de seguridad."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Un programme s'exécutant dans cmux souhaite utiliser le Bluetooth pour détecter des clés d’accès et des clés de sécurité."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Un programma in esecuzione in cmux desidera usare il Bluetooth per rilevare passkey e chiavi di sicurezza."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Et program, der kører i cmux, vil gerne bruge Bluetooth til at finde adgangsnøgler og sikkerhedsnøgler."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Program działający w cmux chciałby użyć Bluetooth do wykrywania kluczy dostępu i kluczy zabezpieczeń."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Программа, запущенная в cmux, хотела бы использовать Bluetooth для обнаружения ключей доступа и ключей безопасности."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Program koji se izvršava unutar cmux želi koristiti Bluetooth za otkrivanje pristupnih ključeva i sigurnosnih ključeva."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "يرغب برنامج يعمل داخل cmux في استخدام Bluetooth لاكتشاف مفاتيح المرور ومفاتيح الأمان."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Et program som kjører i cmux ønsker å bruke Bluetooth til å finne passnøkler og sikkerhetsnøkler."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Um programa em execução no cmux gostaria de usar Bluetooth para descobrir chaves de acesso e chaves de segurança."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โปรแกรมที่ทำงานภายใน cmux ต้องการใช้ Bluetooth เพื่อค้นหาพาสคีย์และคีย์ความปลอดภัย"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "cmux içinde çalışan bir program geçiş anahtarlarını ve güvenlik anahtarlarını bulmak için Bluetooth kullanmak istiyor."
}
}
}
},
Expand All @@ -33,6 +129,102 @@
"state": "translated",
"value": "cmux 内で実行中のプログラムがカメラの使用を求めています。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 中运行的程序想要使用您的摄像头。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 中執行的程式想要使用您的相機。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "cmux 내에서 실행 중인 프로그램이 카메라를 사용하려고 합니다."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Ein in cmux ausgeführtes Programm möchte Ihre Kamera verwenden."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Un programa en ejecución dentro de cmux desea usar tu cámara."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Un programme s'exécutant dans cmux souhaite utiliser votre caméra."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Un programma in esecuzione in cmux desidera utilizzare la fotocamera."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Et program, der kører i cmux, vil gerne bruge dit kamera."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Program działający w cmux chciałby użyć Twojej kamery."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Программа, запущенная в cmux, хотела бы использовать вашу камеру."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Program koji se izvršava unutar cmux želi koristiti vašu kameru."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "يرغب برنامج يعمل داخل cmux في استخدام الكاميرا."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Et program som kjører i cmux ønsker å bruke kameraet ditt."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Um programa em execução no cmux gostaria de usar sua câmera."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โปรแกรมที่ทำงานภายใน cmux ต้องการใช้กล้องของคุณ"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "cmux içinde çalışan bir program kameranızı kullanmak istiyor."
}
}
}
},
Expand Down
29 changes: 27 additions & 2 deletions Sources/App/CmuxMainWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,35 @@ final class MainWindowHostingView<Content: View>: NSHostingView<Content> {
}
}

/// Applies the main-window AppKit movability baseline for the active presentation mode.
///
/// Standard mode keeps native OS-level window movability enabled for macOS
/// tiling and third-party window managers. Minimal mode disables native
/// movability so cmux-owned chrome can decide exactly when to call
/// `performDrag`. If a protected drag suppression sequence is active, the
/// window remains immovable and only the post-suppression restore baseline is
/// updated.
@MainActor
func configureCmuxMainWindowDragBehavior(_ window: NSWindow) {
func configureCmuxMainWindowDragBehavior(
_ window: NSWindow,
defaults: UserDefaults = .standard
) {
// Keep background dragging disabled so app content gestures and titlebar
// controls receive clicks. In standard mode, leave the OS-level movable bit
// enabled for macOS tiling and third-party window managers. In minimal mode,
// there is no native titlebar: app-owned chrome must explicitly call
// performDrag so Bonsplit pane tabs cannot be stolen by AppKit window moves.
window.isMovableByWindowBackground = false
window.isMovable = false
let baselineIsMovable = !WorkspacePresentationModeSettings.isMinimal(defaults: defaults)
if activeWindowMoveSuppressionSequenceReason(window: window) == nil {
window.isMovable = baselineIsMovable
} else {
updateActiveWindowMoveSuppressionSequencePreviousMovableState(
window: window,
previousMovableState: baselineIsMovable
)
ensureWindowMoveSuppressionSequenceIsImmovable(window: window)
}
Comment on lines +51 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale previousMovableState after mode change mid-suppression

beginWindowMoveSuppressionSequence captures previousMovableState = window.isMovable at suppression start — now true in standard mode. When the mode transitions from standard → minimal while a suppression is active, this branch calls ensureWindowMoveSuppressionSequenceIsImmovable (correct during the drag) but never updates the stored previousMovableState. When the suppression ends, finishWindowMoveSuppressionSequence → restoreWindowDragging(previousMovableState: true) leaves the window movable, even though minimal mode expects isMovable = false. Before this PR this was harmless because previousMovableState was always false; the new standard-mode true makes it observable whenever a mode change fires mid-drag (e.g., via the command palette keyboard shortcut).

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

}

@MainActor
Expand Down
4 changes: 4 additions & 0 deletions Sources/App/ShortcutRoutingSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ enum WindowMoveSuppressionReason: String {
case bonsplitPaneTabDrag
}

@MainActor
func shouldSuppressWindowMoveForBonsplitPaneTabDrag(window: NSWindow, event: NSEvent) -> Bool {
guard event.type == .leftMouseDown else {
return false
Expand All @@ -797,6 +798,7 @@ func shouldSuppressWindowMoveForBonsplitPaneTabDrag(window: NSWindow, event: NSE
return BonsplitTabItemHitRegionRegistry.containsWindowPoint(event.locationInWindow, in: window)
}

@MainActor
func windowMoveSuppressionReason(window: NSWindow, event: NSEvent) -> WindowMoveSuppressionReason? {
if shouldSuppressWindowMoveForFolderDrag(window: window, event: event) {
return .folderDrag
Expand All @@ -807,6 +809,7 @@ func windowMoveSuppressionReason(window: NSWindow, event: NSEvent) -> WindowMove
return nil
}

@MainActor
func beginOrContinueWindowMoveSuppressionSequenceForEvent(
window: NSWindow,
event: NSEvent,
Expand All @@ -829,6 +832,7 @@ func beginOrContinueWindowMoveSuppressionSequenceForEvent(
return beginWindowMoveSuppressionSequence(window: window, reason: reason)
}

@MainActor
func shouldFinishWindowMoveSuppressionSequenceAfterDispatch(window: NSWindow, event: NSEvent) -> Bool {
activeWindowMoveSuppressionSequenceReason(window: window) != nil && event.type == .leftMouseUp
}
8 changes: 5 additions & 3 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3256,9 +3256,11 @@ struct ContentView: View {
window.isRestorable = false
setMinimalModeSidebarTitlebarControlsAvailable(sidebarState.isVisible, in: window)
window.titlebarAppearsTransparent = true
// Native AppKit titlebar dragging steals pane-tab drags in minimal
// mode. Keep the main window immovable by default; explicit chrome
// drag zones temporarily enable performDrag for real app moves.
// Keep background dragging disabled so app content gestures and
// titlebar controls receive clicks. Standard mode keeps AppKit
// movability for macOS tiling/window managers; minimal mode keeps
// the native movable bit off so Bonsplit tabs cannot become window
// drags. Explicit cmux drag zones call performDrag when needed.
configureCmuxMainWindowDragBehavior(window)
window.styleMask.insert(.fullSizeContentView)

Expand Down
2 changes: 2 additions & 0 deletions Sources/DetachedFolderDragIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ final class DraggableFolderNSView: NSView, NSDraggingSource {
}

func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
finishWindowMoveSuppressionSequence(window: window, matching: .folderDrag)
#if DEBUG
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
let windowOrigin = window.map { formatPoint($0.frame.origin) } ?? "nil"
Expand Down Expand Up @@ -142,6 +143,7 @@ final class DraggableFolderNSView: NSView, NSDraggingSource {

override func mouseUp(with event: NSEvent) {
clearPendingDrag()
finishWindowMoveSuppressionSequence(window: window, matching: .folderDrag)
super.mouseUp(with: event)
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/WindowDecorationsController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit

@MainActor
final class WindowDecorationsController {
private var observers: [NSObjectProtocol] = []
private var didStart = false
Expand Down Expand Up @@ -36,7 +37,11 @@ final class WindowDecorationsController {
}

func apply(to window: NSWindow) {
if isMainWorkspaceWindow(window), WorkspacePresentationModeSettings.isMinimal() {
let isMainWindow = isMainWorkspaceWindow(window)
if isMainWindow {
configureCmuxMainWindowDragBehavior(window)
}
if isMainWindow, WorkspacePresentationModeSettings.isMinimal() {
WindowMouseMovedEventsCoordinator.enable(for: window, owner: self)
} else {
WindowMouseMovedEventsCoordinator.disable(for: window, owner: self)
Expand Down
Loading
Loading