Ship DMG releases and enhance tab actions
This commit is contained in:
parent
4102567054
commit
b0c61f7d6c
8 changed files with 279 additions and 41 deletions
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -101,19 +101,26 @@ jobs:
|
|||
fi
|
||||
APP_PATH="build/Build/Products/Release/cmux.app"
|
||||
ZIP_SUBMIT="cmux-notary.zip"
|
||||
ZIP_RELEASE="cmux-macos.zip"
|
||||
DMG_RELEASE="cmux-macos.dmg"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_SUBMIT"
|
||||
xcrun notarytool submit "$ZIP_SUBMIT" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
|
||||
xcrun stapler staple "$APP_PATH"
|
||||
xcrun stapler validate "$APP_PATH"
|
||||
spctl -a -vv --type execute "$APP_PATH"
|
||||
rm -f "$ZIP_RELEASE"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_RELEASE"
|
||||
rm -f "$ZIP_SUBMIT"
|
||||
STAGING_DIR="$(mktemp -d)"
|
||||
cp -R "$APP_PATH" "$STAGING_DIR/cmux.app"
|
||||
ln -s /Applications "$STAGING_DIR/Applications"
|
||||
hdiutil create -volname "cmux" -srcfolder "$STAGING_DIR" -ov -format UDZO "$DMG_RELEASE"
|
||||
rm -rf "$STAGING_DIR"
|
||||
xcrun notarytool submit "$DMG_RELEASE" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait
|
||||
xcrun stapler staple "$DMG_RELEASE"
|
||||
xcrun stapler validate "$DMG_RELEASE"
|
||||
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: cmux-macos.zip
|
||||
files: cmux-macos.dmg
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Cleanup keychain
|
||||
|
|
|
|||
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -1,5 +1,15 @@
|
|||
# GhosttyTabs agent notes
|
||||
|
||||
## Local dev
|
||||
|
||||
After making code changes, always run this flow:
|
||||
|
||||
```bash
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' build
|
||||
pkill -x cmux || true
|
||||
open /Users/lawrencechen/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Debug/cmux.app
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Tagging a version triggers the GitHub Actions release workflow and uploads the notarized zip.
|
||||
|
|
@ -13,5 +23,5 @@ gh run watch --repo manaflow-ai/GhosttyTabs
|
|||
Notes:
|
||||
- Requires GitHub secrets: `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`,
|
||||
`APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`.
|
||||
- The release asset is `cmux-macos.zip` attached to the tag.
|
||||
- README download button points to `releases/latest/download/cmux-macos.zip`.
|
||||
- The release asset is `cmux-macos.dmg` attached to the tag.
|
||||
- README download button points to `releases/latest/download/cmux-macos.dmg`.
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -356,7 +356,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -399,7 +399,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -426,10 +426,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -443,10 +443,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
Vertical tabs for Ghostty on macOS, built on libghostty.
|
||||
|
||||
[](releases/latest/download/cmux-macos.zip)
|
||||
[](releases/latest/download/cmux-macos.dmg)
|
||||
|
||||
## Releases
|
||||
|
||||
Tag a version like `v0.1.0` and push it to trigger the GitHub Actions release workflow.
|
||||
The workflow builds `GhosttyKit.xcframework`, builds the Release app, signs, notarizes,
|
||||
staples, and uploads `cmux-macos.zip` to the release.
|
||||
staples, and uploads `cmux-macos.dmg` to the release.
|
||||
|
||||
### Required GitHub secrets
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,17 @@ struct ContentView: View {
|
|||
private let sidebarHandleWidth: CGFloat = 6
|
||||
@FocusState private var focusedTabId: UUID?
|
||||
@State private var sidebarSelection: SidebarSelection = .tabs
|
||||
@State private var selectedTabIds: Set<UUID> = []
|
||||
@State private var lastSidebarSelectionIndex: Int? = nil
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Vertical Tabs Sidebar
|
||||
VerticalTabsSidebar(
|
||||
sidebarWidth: sidebarWidth,
|
||||
selection: $sidebarSelection
|
||||
selection: $sidebarSelection,
|
||||
selectedTabIds: $selectedTabIds,
|
||||
lastSidebarSelectionIndex: $lastSidebarSelectionIndex
|
||||
)
|
||||
.frame(width: sidebarWidth)
|
||||
.background(GeometryReader { proxy in
|
||||
|
|
@ -100,14 +104,37 @@ struct ContentView: View {
|
|||
.onAppear {
|
||||
focusedTabId = tabManager.selectedTabId
|
||||
tabManager.applyWindowBackgroundForSelectedTab()
|
||||
if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId {
|
||||
selectedTabIds = [selectedId]
|
||||
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == selectedId }
|
||||
}
|
||||
}
|
||||
.onChange(of: tabManager.selectedTabId) { newValue in
|
||||
focusedTabId = newValue
|
||||
tabManager.applyWindowBackgroundForSelectedTab()
|
||||
guard let newValue else { return }
|
||||
if selectedTabIds.count <= 1 {
|
||||
selectedTabIds = [newValue]
|
||||
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == newValue }
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in
|
||||
sidebarSelection = .tabs
|
||||
}
|
||||
.onReceive(tabManager.$tabs) { tabs in
|
||||
let existingIds = Set(tabs.map { $0.id })
|
||||
selectedTabIds = selectedTabIds.filter { existingIds.contains($0) }
|
||||
if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId {
|
||||
selectedTabIds = [selectedId]
|
||||
}
|
||||
if let lastIndex = lastSidebarSelectionIndex, lastIndex >= tabs.count {
|
||||
if let selectedId = tabManager.selectedTabId {
|
||||
lastSidebarSelectionIndex = tabs.firstIndex { $0.id == selectedId }
|
||||
} else {
|
||||
lastSidebarSelectionIndex = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(SidebarFramePreferenceKey.self) { frame in
|
||||
sidebarMinX = frame.minX
|
||||
}
|
||||
|
|
@ -119,6 +146,8 @@ struct VerticalTabsSidebar: View {
|
|||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||
let sidebarWidth: CGFloat
|
||||
@Binding var selection: SidebarSelection
|
||||
@Binding var selectedTabIds: Set<UUID>
|
||||
@Binding var lastSidebarSelectionIndex: Int?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -165,8 +194,14 @@ struct VerticalTabsSidebar: View {
|
|||
// Tab List
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(tabManager.tabs) { tab in
|
||||
TabItemView(tab: tab, selection: $selection)
|
||||
ForEach(Array(tabManager.tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
TabItemView(
|
||||
tab: tab,
|
||||
index: index,
|
||||
selection: $selection,
|
||||
selectedTabIds: $selectedTabIds,
|
||||
lastSidebarSelectionIndex: $lastSidebarSelectionIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
|
@ -191,13 +226,20 @@ struct TabItemView: View {
|
|||
@EnvironmentObject var tabManager: TabManager
|
||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||
@ObservedObject var tab: Tab
|
||||
let index: Int
|
||||
@Binding var selection: SidebarSelection
|
||||
@Binding var selectedTabIds: Set<UUID>
|
||||
@Binding var lastSidebarSelectionIndex: Int?
|
||||
@State private var isHovering = false
|
||||
|
||||
var isSelected: Bool {
|
||||
var isActive: Bool {
|
||||
tabManager.selectedTabId == tab.id
|
||||
}
|
||||
|
||||
var isMultiSelected: Bool {
|
||||
selectedTabIds.contains(tab.id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
|
|
@ -205,7 +247,7 @@ struct TabItemView: View {
|
|||
if unreadCount > 0 {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(isSelected ? Color.white.opacity(0.25) : Color.accentColor)
|
||||
.fill(isActive ? Color.white.opacity(0.25) : Color.accentColor)
|
||||
Text("\(unreadCount)")
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
|
|
@ -215,7 +257,7 @@ struct TabItemView: View {
|
|||
|
||||
Text(tab.title)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isSelected ? .white : .primary)
|
||||
.foregroundColor(isActive ? .white : .primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
|
|
@ -224,18 +266,18 @@ struct TabItemView: View {
|
|||
Button(action: { tabManager.closeTab(tab) }) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundColor(isSelected ? .white.opacity(0.7) : .secondary)
|
||||
.foregroundColor(isActive ? .white.opacity(0.7) : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 16, height: 16)
|
||||
.opacity((isHovering || isSelected) && tabManager.tabs.count > 1 ? 1 : 0)
|
||||
.allowsHitTesting((isHovering || isSelected) && tabManager.tabs.count > 1)
|
||||
.opacity((isHovering || isActive || isMultiSelected) && tabManager.tabs.count > 1 ? 1 : 0)
|
||||
.allowsHitTesting((isHovering || isActive || isMultiSelected) && tabManager.tabs.count > 1)
|
||||
}
|
||||
|
||||
if let subtitle = latestNotificationText {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(isSelected ? .white.opacity(0.8) : .secondary)
|
||||
.foregroundColor(isActive ? .white.opacity(0.8) : .secondary)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
|
@ -244,7 +286,7 @@ struct TabItemView: View {
|
|||
if let directories = directorySummary {
|
||||
Text(directories)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(isSelected ? .white.opacity(0.75) : .secondary)
|
||||
.foregroundColor(isActive ? .white.opacity(0.75) : .secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
|
|
@ -253,17 +295,155 @@ struct TabItemView: View {
|
|||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isSelected ? Color.accentColor : (isHovering ? Color(nsColor: .controlBackgroundColor).opacity(0.5) : Color.clear))
|
||||
.fill(backgroundColor)
|
||||
)
|
||||
.padding(.horizontal, 6)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
tabManager.selectTab(tab)
|
||||
selection = .tabs
|
||||
updateSelection()
|
||||
}
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
}
|
||||
.contextMenu {
|
||||
let targetIds = contextTargetIds()
|
||||
Button("Close Tabs") {
|
||||
closeTabs(targetIds)
|
||||
}
|
||||
.disabled(targetIds.isEmpty)
|
||||
|
||||
Button("Close Others") {
|
||||
closeOtherTabs(targetIds)
|
||||
}
|
||||
.disabled(tabManager.tabs.count <= 1 || targetIds.count == tabManager.tabs.count)
|
||||
|
||||
Button("Close Tabs to the Right") {
|
||||
closeTabsToRight(of: tab.id)
|
||||
}
|
||||
.disabled(index >= tabManager.tabs.count - 1)
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Move to Top") {
|
||||
tabManager.moveTabsToTop(Set(targetIds))
|
||||
syncSelectionAfterMutation()
|
||||
}
|
||||
.disabled(targetIds.isEmpty)
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Mark as Read") {
|
||||
markTabsRead(targetIds)
|
||||
}
|
||||
.disabled(!hasUnreadNotifications(in: targetIds))
|
||||
|
||||
Button("Mark as Unread") {
|
||||
markTabsUnread(targetIds)
|
||||
}
|
||||
.disabled(!hasReadNotifications(in: targetIds))
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
if isActive {
|
||||
return Color.accentColor
|
||||
}
|
||||
if isMultiSelected {
|
||||
return Color.accentColor.opacity(0.25)
|
||||
}
|
||||
if isHovering {
|
||||
return Color(nsColor: .controlBackgroundColor).opacity(0.5)
|
||||
}
|
||||
return Color.clear
|
||||
}
|
||||
|
||||
private func updateSelection() {
|
||||
let modifiers = NSEvent.modifierFlags
|
||||
let isCommand = modifiers.contains(.command)
|
||||
let isShift = modifiers.contains(.shift)
|
||||
|
||||
if isShift, let lastIndex = lastSidebarSelectionIndex {
|
||||
let lower = min(lastIndex, index)
|
||||
let upper = max(lastIndex, index)
|
||||
let rangeIds = tabManager.tabs[lower...upper].map { $0.id }
|
||||
if isCommand {
|
||||
selectedTabIds.formUnion(rangeIds)
|
||||
} else {
|
||||
selectedTabIds = Set(rangeIds)
|
||||
}
|
||||
} else if isCommand {
|
||||
if selectedTabIds.contains(tab.id) {
|
||||
selectedTabIds.remove(tab.id)
|
||||
} else {
|
||||
selectedTabIds.insert(tab.id)
|
||||
}
|
||||
} else {
|
||||
selectedTabIds = [tab.id]
|
||||
}
|
||||
|
||||
lastSidebarSelectionIndex = index
|
||||
tabManager.selectTab(tab)
|
||||
selection = .tabs
|
||||
}
|
||||
|
||||
private func contextTargetIds() -> [UUID] {
|
||||
let baseIds: Set<UUID> = selectedTabIds.contains(tab.id) ? selectedTabIds : [tab.id]
|
||||
return tabManager.tabs.compactMap { baseIds.contains($0.id) ? $0.id : nil }
|
||||
}
|
||||
|
||||
private func closeTabs(_ targetIds: [UUID]) {
|
||||
for id in targetIds {
|
||||
if let tab = tabManager.tabs.first(where: { $0.id == id }) {
|
||||
tabManager.closeTab(tab)
|
||||
}
|
||||
}
|
||||
selectedTabIds.subtract(targetIds)
|
||||
syncSelectionAfterMutation()
|
||||
}
|
||||
|
||||
private func closeOtherTabs(_ targetIds: [UUID]) {
|
||||
let keepIds = Set(targetIds)
|
||||
let idsToClose = tabManager.tabs.compactMap { keepIds.contains($0.id) ? nil : $0.id }
|
||||
closeTabs(idsToClose)
|
||||
}
|
||||
|
||||
private func closeTabsToRight(of tabId: UUID) {
|
||||
guard let anchorIndex = tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
||||
let idsToClose = tabManager.tabs.suffix(from: anchorIndex + 1).map { $0.id }
|
||||
closeTabs(idsToClose)
|
||||
}
|
||||
|
||||
private func markTabsRead(_ targetIds: [UUID]) {
|
||||
for id in targetIds {
|
||||
notificationStore.markRead(forTabId: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func markTabsUnread(_ targetIds: [UUID]) {
|
||||
for id in targetIds {
|
||||
notificationStore.markUnread(forTabId: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func hasUnreadNotifications(in targetIds: [UUID]) -> Bool {
|
||||
let targetSet = Set(targetIds)
|
||||
return notificationStore.notifications.contains { targetSet.contains($0.tabId) && !$0.isRead }
|
||||
}
|
||||
|
||||
private func hasReadNotifications(in targetIds: [UUID]) -> Bool {
|
||||
let targetSet = Set(targetIds)
|
||||
return notificationStore.notifications.contains { targetSet.contains($0.tabId) && $0.isRead }
|
||||
}
|
||||
|
||||
private func syncSelectionAfterMutation() {
|
||||
let existingIds = Set(tabManager.tabs.map { $0.id })
|
||||
selectedTabIds = selectedTabIds.filter { existingIds.contains($0) }
|
||||
if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId {
|
||||
selectedTabIds = [selectedId]
|
||||
}
|
||||
if let selectedId = tabManager.selectedTabId {
|
||||
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == selectedId }
|
||||
}
|
||||
}
|
||||
|
||||
private var latestNotificationText: String? {
|
||||
|
|
|
|||
|
|
@ -698,6 +698,11 @@ class TerminalSurface: Identifiable {
|
|||
ghostty_surface_set_focus(surface, focused)
|
||||
}
|
||||
|
||||
func needsConfirmClose() -> Bool {
|
||||
guard let surface = surface else { return false }
|
||||
return ghostty_surface_needs_confirm_quit(surface)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if ownsDisplayLink {
|
||||
GhosttyApp.shared.releaseDisplayLink()
|
||||
|
|
|
|||
|
|
@ -265,6 +265,14 @@ class TabManager: ObservableObject {
|
|||
tabs.insert(tab, at: 0)
|
||||
}
|
||||
|
||||
func moveTabsToTop(_ tabIds: Set<UUID>) {
|
||||
guard !tabIds.isEmpty else { return }
|
||||
let selectedTabs = tabs.filter { tabIds.contains($0.id) }
|
||||
guard !selectedTabs.isEmpty else { return }
|
||||
let remainingTabs = tabs.filter { !tabIds.contains($0.id) }
|
||||
tabs = selectedTabs + remainingTabs
|
||||
}
|
||||
|
||||
func updateSurfaceDirectory(tabId: UUID, surfaceId: UUID, directory: String) {
|
||||
guard let tab = tabs.first(where: { $0.id == tabId }) else { return }
|
||||
let normalized = normalizeDirectory(directory)
|
||||
|
|
@ -308,23 +316,26 @@ class TabManager: ObservableObject {
|
|||
guard let selectedId = selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedId }),
|
||||
let focusedSurfaceId = tab.focusedSurfaceId else { return }
|
||||
guard tab.splitTree.isSplit else { return }
|
||||
guard confirmClose(
|
||||
title: "Close panel?",
|
||||
message: "This will close the current split panel in this tab."
|
||||
) else { return }
|
||||
guard tab.splitTree.isSplit else {
|
||||
closeTabIfRunningProcess(tab)
|
||||
return
|
||||
}
|
||||
|
||||
let focusedSurface = tab.surface(for: focusedSurfaceId)
|
||||
if focusedSurface?.needsConfirmClose() == true {
|
||||
guard confirmClose(
|
||||
title: "Close panel?",
|
||||
message: "This will close the current split panel in this tab."
|
||||
) else { return }
|
||||
}
|
||||
|
||||
_ = tab.closeSurface(focusedSurfaceId)
|
||||
}
|
||||
|
||||
func closeCurrentTabWithConfirmation() {
|
||||
guard tabs.count > 1 else { return }
|
||||
guard let selectedId = selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedId }) else { return }
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab and all of its panels."
|
||||
) else { return }
|
||||
closeTab(tab)
|
||||
closeTabIfRunningProcess(tab)
|
||||
}
|
||||
|
||||
func selectTab(_ tab: Tab) {
|
||||
|
|
@ -341,6 +352,23 @@ class TabManager: ObservableObject {
|
|||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
private func closeTabIfRunningProcess(_ tab: Tab) {
|
||||
guard tabs.count > 1 else { return }
|
||||
if tabNeedsConfirmClose(tab),
|
||||
!confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab and all of its panels."
|
||||
) {
|
||||
return
|
||||
}
|
||||
closeTab(tab)
|
||||
}
|
||||
|
||||
private func tabNeedsConfirmClose(_ tab: Tab) -> Bool {
|
||||
guard let root = tab.splitTree.root else { return false }
|
||||
return root.leaves().contains { $0.needsConfirmClose() }
|
||||
}
|
||||
|
||||
func titleForTab(_ tabId: UUID) -> String? {
|
||||
tabs.first(where: { $0.id == tabId })?.title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,14 @@ final class TerminalNotificationStore: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func markUnread(forTabId tabId: UUID) {
|
||||
for index in notifications.indices {
|
||||
if notifications[index].tabId == tabId {
|
||||
notifications[index].isRead = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(id: UUID) {
|
||||
notifications.removeAll { $0.id == id }
|
||||
center.removeDeliveredNotifications(withIdentifiers: [id.uuidString])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue