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
59 changes: 52 additions & 7 deletions JSONViewer/UI/AppShellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import AppKit
#endif

struct AppShellView: View {
@StateObject private var viewModel = AppViewModel()
#if os(macOS)
@StateObject private var tabs = TabsViewModel()
#if os(macOS)
@State private var nsWindow: NSWindow?
#endif
@State private var isInspectorVisible: Bool = false
@State private var isAISidebarVisible: Bool = false
@Environment(\.openWindow) private var openWindow

private var viewModel: AppViewModel {
tabs.selected
}

private var displayText: String {
viewModel.prettyJSON
}
Expand Down Expand Up @@ -47,7 +51,7 @@ struct AppShellView: View {

if viewModel.mode != .none {
CommandBarContainer(
mode: $viewModel.commandMode,
mode: Binding(get: { viewModel.commandMode }, set: { viewModel.commandMode = $0 }),
placeholder: viewModel.commandMode == .jq
? "jq filter (e.g. .items | length)"
: "Use natural language to search or transform"
Expand Down Expand Up @@ -76,10 +80,38 @@ struct AppShellView: View {
}
}
.navigationSplitViewColumnWidth(min: 420, ideal: 680, max: .infinity)
.navigationTitle(viewModel.fileURL?.lastPathComponent ?? "Prism")
.navigationTitle(windowTitle)
}

.toolbar {
// Tabs in the top bar
ToolbarItemGroup {
HStack(spacing: 8) {
Picker("", selection: Binding(get: { tabs.selectedIndex }, set: { tabs.selectedIndex = $0 })) {
ForEach(Array(tabs.tabs.enumerated()), id: \.offset) { idx, vm in
Text(vm.fileURL?.lastPathComponent ?? "Untitled")
.tag(idx)
}
}
.pickerStyle(.segmented)
.help("Switch document tab")

Button {
_ = tabs.newTab()
withAnimation {
isInspectorVisible = false
isAISidebarVisible = false
}
#if os(macOS)
nsWindow?.makeFirstResponder(nil)
#endif
} label: {
Image(systemName: "plus")
}
.help("New Tab")
}
}

// Existing controls
ToolbarItemGroup {
Button {
openFile()
Expand All @@ -95,7 +127,7 @@ struct AppShellView: View {
}
.help("Paste JSON/JSONL from clipboard (⌘V)")

Picker("", selection: $viewModel.presentation) {
Picker("", selection: Binding(get: { viewModel.presentation }, set: { viewModel.presentation = $0 })) {
Image(systemName: "text.alignleft").tag(AppViewModel.ContentPresentation.text)
Image(systemName: "list.bullet.indent").tag(AppViewModel.ContentPresentation.tree)
}
Expand Down Expand Up @@ -156,7 +188,7 @@ struct AppShellView: View {
// Show native title (file name when available)
nsWindow?.titleVisibility = .visible
nsWindow?.representedURL = viewModel.fileURL
nsWindow?.title = viewModel.fileURL?.lastPathComponent ?? "Prism"
nsWindow?.title = windowTitle
// Ensure no text field is focused by default so Cmd+V pastes into the viewer.
nsWindow?.makeFirstResponder(nil)
WindowRegistry.shared.register(viewModel)
Expand All @@ -169,6 +201,11 @@ struct AppShellView: View {
OpenRequests.shared.drain(into: viewModel) { id in
openWindow(id: id)
}
// If there are multiple pending, open them in new tabs in this window.
while OpenRequests.shared.pendingCount > 0 {
let vm = tabs.newTab()
_ = OpenRequests.shared.deliverNext(to: vm)
}
// Ensure no UI element steals focus on first launch so Cmd+V pastes into viewer.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
nsWindow?.makeFirstResponder(nil)
Expand All @@ -183,6 +220,14 @@ struct AppShellView: View {
nsWindow?.representedURL = newURL
nsWindow?.title = newURL?.lastPathComponent ?? "Prism"
}
.onChange(of: tabs.selectedIndex) { _ in
// Update window chrome when switching tabs
nsWindow?.titleVisibility = .visible
nsWindow?.representedURL = viewModel.fileURL
nsWindow?.title = windowTitle
// Do not keep inspector/AI sidebar open automatically across tabs unless desired
// Leave their visibility as-is for now.
}
#endif
}

Expand Down
40 changes: 40 additions & 0 deletions JSONViewer/ViewModels/TabsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import SwiftUI
import Foundation

@MainActor
final class TabsViewModel: ObservableObject {
@Published var tabs: [AppViewModel]
@Published var selectedIndex: Int

init() {
let initial = AppViewModel()
self.tabs = [initial]
self.selectedIndex = 0
}

var selected: AppViewModel {
tabs[selectedIndex]
}

@discardableResult
func newTab() -> AppViewModel {
let vm = AppViewModel()
tabs.append(vm)
selectedIndex = tabs.count - 1
return vm
}

func closeTab(at index: Int) {
guard tabs.indices.contains(index) else { return }
tabs.remove(at: index)
if tabs.isEmpty {
_ = newTab()
} else if selectedIndex >= tabs.count {
selectedIndex = tabs.count - 1
}
}

var titles: [String] {
tabs.map { $0.fileURL?.lastPathComponent ?? "Untitled" }
}
}