Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Glint/Chrome/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@ private struct GeneralPane: View {
Toggle("", isOn: $store.revealAtRepoRoot)
.toggleStyle(.switch).labelsHidden()
}
SettingsDivider()
SettingsRow("Middle-click closes workspace",
subtitle: "Click a workspace card with the middle mouse button to close it. Same as the context menu's “Close Workspace”.") {
Toggle("", isOn: $store.middleClickClosesWorkspace)
.toggleStyle(.switch).labelsHidden()
}
}

SettingsCard("New terminals") {
Expand Down
10 changes: 10 additions & 0 deletions Glint/Chrome/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,16 @@ private struct WorkspaceCard: View {
// than removing it so the call-site stays a single chain.
including: archived ? .subviews : .all
)
// Middle-click closes the workspace — same path as the context menu's
// "Close Workspace" (confirm dialog included when panes are busy).
// Disabled while renaming and on archived cards (which expose
// Unarchive/Delete instead). Catcher is transparent to left/right/hover
// so existing gestures, popover, and menu are untouched.
.overlay {
if !archived && !isEditing && store.middleClickClosesWorkspace {
MiddleClickCatcher { store.deleteWorkspace(ws.id) }
}
}
}

private func startEditing() {
Expand Down
44 changes: 44 additions & 0 deletions Glint/Chrome/VisualEffectBackground.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ struct NoDragSurface: NSViewRepresentable {
}
}

/// Invisible overlay that fires `onMiddleClick` only on a middle-mouse
/// button press. Left/right clicks, drags, hover, and scroll pass through
/// untouched (the view returns nil from `hitTest` unless the middle button
/// is down), so any SwiftUI gesture / popover / menu underneath keeps
/// working. Mirror of the `WindowDragSurface` / `NoDragSurface` pattern.
struct MiddleClickCatcher: NSViewRepresentable {
let onMiddleClick: () -> Void

func makeNSView(context: Context) -> NSView { CatcherView(onMiddleClick: onMiddleClick) }
func updateNSView(_ nsView: NSView, context: Context) {
(nsView as? CatcherView)?.onMiddleClick = onMiddleClick
}

private final class CatcherView: NSView {
var onMiddleClick: () -> Void

init(onMiddleClick: @escaping () -> Void) {
self.onMiddleClick = onMiddleClick
super.init(frame: .zero)
}
@available(*, unavailable) required init?(coder: NSCoder) { fatalError() }

// Only claim hits when the middle button is pressed — every other
// interaction (left/right click, drag, hover, scroll) falls through
// to the SwiftUI view underneath. `hitTest` is the gate AppKit
// consults before routing any mouse event, so returning nil here
// makes this view genuinely transparent to non-middle input.
override func hitTest(_ point: NSPoint) -> NSView? {
NSEvent.pressedMouseButtons & (1 << 2) != 0 ? self : nil
}

override func otherMouseDown(with event: NSEvent) {
// buttonNumber 2 == middle. Other "other" buttons (back/forward)
// pass through to the responder chain.
guard event.buttonNumber == 2 else { super.otherMouseDown(with: event); return }
onMiddleClick()
}

// Never let the overlay drag the window or steal cursor focus.
override var mouseDownCanMoveWindow: Bool { false }
override var acceptsFirstResponder: Bool { false }
}
}

// MARK: - Liquid Glass (macOS 26)

/// Whether the OS can render Liquid Glass at all. Call sites that keep a
Expand Down
22 changes: 22 additions & 0 deletions Glint/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2715,6 +2715,28 @@
}
}
},
"Middle-click closes workspace": {
"extractionState": "manual",
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "中键关闭工作区"
}
}
}
},
"Click a workspace card with the middle mouse button to close it. Same as the context menu's \u201cClose Workspace\u201d.": {
"extractionState": "manual",
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "用鼠标中键点击工作区卡片即可关闭,等同于右键菜单的「关闭工作区」。"
}
}
}
},
"Missing": {
"extractionState": "manual",
"localizations": {
Expand Down
8 changes: 8 additions & 0 deletions Glint/Workspace/WorkspaceStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,14 @@ final class WorkspaceStore: ObservableObject {
didSet { UserDefaults.standard.set(sortCompletedFirst, forKey: "glint.sortCompletedFirst") }
}

/// Middle-click on a workspace card in the sidebar closes it (same path
/// as the context menu's "Close Workspace"). Defaults to on so the
/// behaviour stays available without configuration; users who find it
/// surprising can disable it here.
@Published var middleClickClosesWorkspace: Bool = (UserDefaults.standard.object(forKey: "glint.middleClickClosesWorkspace") as? Bool) ?? true {
didSet { UserDefaults.standard.set(middleClickClosesWorkspace, forKey: "glint.middleClickClosesWorkspace") }
}

/// Show the "Paste potentially unsafe text?" confirm dialog when the
/// clipboard contains newlines or control characters. The underlying
/// default (`glint.skipUnsafePasteConfirmation`) is inverted so the
Expand Down