Sources/Orgstar/EmacsImportView.swift
130 lines · 5861 bytes
5 symbols in this file
1import OrgApp
2import OrgCore
3import SwiftUI
4
5/// Import from Emacs: what was read from a configuration, to pick from before anything changes.
6struct EmacsImportView: View {
7 let workspace: WorkspaceModel?
8 @Environment(\.dismiss) private var dismiss
9 @State private var location: URL?
10 @State private var result: EmacsImport?
11 @State private var selected: Set<Int> = []
12 @State private var choosing = false
13 @State private var summary: String?
14 @State private var importing = false
15
16 var body: some View {
17 VStack(alignment: .leading, spacing: 0) {
18 header
19 Divider()
20 if let summary {
21 Text(summary).padding(20).frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
22 } else if let result {
23 list(result)
24 } else {
25 ContentUnavailableView("No Emacs configuration found", systemImage: "doc.questionmark",
26 description: Text("Choose your init.el, or the folder holding it (~/.emacs.d, ~/.config/doom)."))
27 }
28 Divider()
29 HStack {
30 if let result, summary == nil {
31 Text(footnote(result)).font(.caption).foregroundStyle(.secondary)
32 }
33 Spacer()
34 if summary == nil {
35 Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction)
36 Button("Import") { runImport() }
37 .keyboardShortcut(.defaultAction)
38 .disabled(selected.isEmpty || importing)
39 } else {
40 Button("Done") { dismiss() }.keyboardShortcut(.defaultAction)
41 }
42 }
43 .padding(12)
44 }
45 .frame(width: 640, height: 560)
46 .onAppear {
47 if location == nil, let first = EmacsImport.defaultLocations().first { read(first) }
48 }
49 .fileImporter(isPresented: $choosing, allowedContentTypes: [.folder, .item]) { chosen in
50 if case .success(let url) = chosen { read(url) }
51 }
52 }
53
54 private var header: some View {
55 HStack {
56 VStack(alignment: .leading, spacing: 2) {
57 Text("Import from Emacs").font(.headline)
58 Text(location.map { "Reading \($0.path)" + ((result?.isDoom ?? false) ? ", with Doom's own org defaults" : "") } ?? "")
59 .font(.caption).foregroundStyle(.secondary).lineLimit(2)
60 }
61 Spacer()
62 Button("Choose…") { choosing = true }
63 }
64 .padding(12)
65 }
66
67 private func list(_ result: EmacsImport) -> some View {
68 let groups: [(String, [EmacsImport.Item])] = [
69 ("Settings", result.items.filter { if case .setting = $0.change { true } else { false } }),
70 ("Folders", result.items.filter { if case .folder = $0.change { true } else { false } }),
71 ("Capture templates", result.items.filter { if case .capture = $0.change { true } else { false } }),
72 ("Key bindings", result.items.filter { if case .binding = $0.change { true } else { false } }),
73 ]
74 return List {
75 ForEach(groups.filter { !$0.1.isEmpty }, id: \.0) { title, items in
76 Section(title) {
77 ForEach(items) { item in
78 Toggle(isOn: Binding { selected.contains(item.id) } set: { on in
79 if on { selected.insert(item.id) } else { selected.remove(item.id) }
80 }) {
81 VStack(alignment: .leading, spacing: 1) {
82 Text(item.summary).font(.system(.body, design: .monospaced))
83 Text([item.name == item.summary ? nil : item.name, item.source.isEmpty ? nil : item.source].compactMap { $0 }.joined(separator: " · "))
84 .font(.caption).foregroundStyle(.secondary)
85 }
86 }
87 }
88 }
89 }
90 let notes = result.unsupported.map { ($0.id, $0.name, $0.reason, $0.source) } + result.unreadable.map { ($0.id, $0.name, $0.reason, $0.source) }
91 if !notes.isEmpty || !result.problems.isEmpty {
92 Section("Not imported") {
93 ForEach(result.problems, id: \.self) { Text($0).foregroundStyle(.red) }
94 ForEach(notes, id: \.0) { note in
95 VStack(alignment: .leading, spacing: 1) {
96 Text(note.1).font(.system(.body, design: .monospaced))
97 Text("\(note.2) · \(note.3)").font(.caption).foregroundStyle(.secondary)
98 }
99 }
100 }
101 }
102 }
103 }
104
105 private func footnote(_ result: EmacsImport) -> String {
106 var parts = ["\(result.skipped) variables not about org left alone"]
107 if result.doomDefaultsLeftOut > 0 { parts.append("\(result.doomDefaultsLeftOut) Doom defaults Orgstar has no use for") }
108 return parts.joined(separator: "; ")
109 }
110
111 private func read(_ url: URL) {
112 let accessing = url.startAccessingSecurityScopedResource()
113 defer { if accessing { url.stopAccessingSecurityScopedResource() } }
114 location = url
115 let scanned = EmacsImport.scan(EmacsImport.files(in: url))
116 result = scanned
117 selected = Set(scanned.items.map(\.id))
118 summary = nil
119 }
120
121 private func runImport() {
122 guard let result else { return }
123 importing = true
124 let chosen = result.items.filter { selected.contains($0.id) }
125 Task {
126 summary = await EmacsImport.apply(chosen, workspace: workspace)
127 importing = false
128 }
129 }
130}