diff --git a/.DS_Store b/.DS_Store index b0365f6..8467506 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Config/LocalGitConfiguration.swift b/Config/LocalGitConfiguration.swift index 6287d9f..cdb5037 100644 --- a/Config/LocalGitConfiguration.swift +++ b/Config/LocalGitConfiguration.swift @@ -78,9 +78,9 @@ git push -u origin main } private struct SetupStep: View { - let step: String; let title: String; let body: String + let step: String; let title: String; let detail: String init(_ step: String, title: String, body: String) { - self.step = step; self.title = title; self.body = body + self.step = step; self.title = title; self.detail = body } var body: some View { HStack(alignment: .top, spacing: 12) { @@ -89,7 +89,7 @@ private struct SetupStep: View { .background(Color.accentColor, in: Circle()) VStack(alignment: .leading, spacing: 4) { Text(title).font(.headline) - Text(body).font(.subheadline).foregroundStyle(.secondary) + Text(detail).font(.subheadline).foregroundStyle(.secondary) } } } diff --git a/Editor/CodeEditorView.swift b/Editor/CodeEditorView.swift index 93482fb..ce3b708 100644 --- a/Editor/CodeEditorView.swift +++ b/Editor/CodeEditorView.swift @@ -91,6 +91,18 @@ struct CodeEditorView: View { .onReceive(NotificationCenter.default.publisher(for: .navigateToRange)) { note in if let range = note.userInfo?["range"] as? NSRange { navigate(to: range) } } + .onReceive(NotificationCenter.default.publisher(for: .editorFontSizeChange)) { note in + if let delta = note.userInfo?["delta"] as? Double { + let newSize = max(8, min(32, editorState.fontSizeStored + delta)) + editorState.fontSizeStored = newSize + } + } + .onReceive(NotificationCenter.default.publisher(for: .editorFindNext)) { _ in + keyboardBridge.triggerFindNext() + } + .onReceive(NotificationCenter.default.publisher(for: .editorFindPrev)) { _ in + keyboardBridge.triggerFindPrev() + } } private func jumpTo(_ line: Int) { diff --git a/Editor/CompletionController.swift b/Editor/CompletionController.swift index b7357a7..e1b2483 100644 --- a/Editor/CompletionController.swift +++ b/Editor/CompletionController.swift @@ -18,7 +18,8 @@ final class CompletionController: ObservableObject { func attach(to tv: Runestone.TextView) { self.textView = tv } func textDidChange(in tv: Runestone.TextView, filePath: String) { - guard let sel = tv.selectedRange, sel.location > 0 else { dismiss(); return } + let sel = tv.selectedRange + guard sel.location > 0 else { dismiss(); return } let text = tv.text as NSString let c = text.character(at: sel.location - 1) let shouldTrigger = autoTriggers.contains(Character(Unicode.Scalar(c)!)) @@ -42,24 +43,29 @@ final class CompletionController: ObservableObject { func dismiss() { debounceTask?.cancel(); items = []; isVisible = false } func insert(_ item: CompletionItem, into tv: Runestone.TextView) { - guard let sel = tv.selectedRange else { dismiss(); return } + let sel = tv.selectedRange let text = tv.text as NSString let wordRange = text.rangeOfCurrentWord(at: sel.location) let insertText = item.insertText ?? item.label tv.replace(wordRange, withText: insertText) if insertText.hasSuffix("()") { - let loc = (tv.selectedRange?.location ?? 1) - 1 + let loc = tv.selectedRange.location > 0 ? tv.selectedRange.location - 1 : 0 tv.selectedRange = NSRange(location: loc, length: 0) } dismiss() } private func requestCompletions(tv: Runestone.TextView, filePath: String) { - guard let sel = tv.selectedRange, - let loc = tv.textLocation(at: sel.location) else { return } - if let rect = tv.caretRect(for: sel.location) { - anchorRect = tv.convert(rect, to: nil) - } + let sel = tv.selectedRange + guard let loc = tv.textLocation(at: sel.location) else { return } + // caretRect(for:) on Runestone.TextView takes UITextPosition, not Int. + // Approximate anchor position from font metrics for the completion popover. + let font = tv.theme.font + let lineHeight = ceil(font.lineHeight * tv.lineHeightMultiplier) + let approxY = CGFloat(loc.lineNumber) * lineHeight + + tv.textContainerInset.top + - tv.contentOffset.y + anchorRect = CGRect(x: 0, y: approxY, width: 0, height: lineHeight) lastId += 1; let id = lastId Task { [weak self] in guard let self else { return } diff --git a/Editor/EditorKeyboardToolbar.swift b/Editor/EditorKeyboardToolbar.swift index d84c77c..3aafdc2 100644 --- a/Editor/EditorKeyboardToolbar.swift +++ b/Editor/EditorKeyboardToolbar.swift @@ -1,31 +1,50 @@ import SwiftUI +import UIKit import Runestone +// MARK: - Keyboard Bridge + final class EditorKeyboardBridge: ObservableObject { weak var activeTextView: Runestone.TextView? + + // Called by CodeEditorView when ⌘G / ⌘⇧G hardware shortcut fires + var onFindNext: (() -> Void)? + var onFindPrev: (() -> Void)? + + func triggerFindNext() { onFindNext?() } + func triggerFindPrev() { onFindPrev?() } } +// MARK: - Toolbar + struct EditorKeyboardToolbar: View { @ObservedObject var bridge: EditorKeyboardBridge @State private var showFindBar = false @State private var searchQuery = "" - @State private var isRegex = false + @State private var lastMatchRange: NSRange = NSRange(location: NSNotFound, length: 0) var body: some View { VStack(spacing: 0) { if showFindBar { - InlineFindBar(query: $searchQuery, isRegex: $isRegex, - onNext: findNext, onPrev: findPrev, - onDismiss: { withAnimation { showFindBar = false } }) - .transition(.move(edge: .top).combined(with: .opacity)) + InlineFindBar( + query: $searchQuery, + onNext: findNext, + onPrev: findPrev, + onDismiss: { + withAnimation { showFindBar = false } + searchQuery = "" + lastMatchRange = NSRange(location: NSNotFound, length: 0) + } + ) + .transition(.move(edge: .top).combined(with: .opacity)) Divider() } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 4) { - KeyToolbarButton(icon: "increase.indent", label: "Indent") { bridge.activeTextView?.shiftRight() } - KeyToolbarButton(icon: "decrease.indent", label: "Unindent") { bridge.activeTextView?.shiftLeft() } + KeyToolbarButton(icon: "increase.indent", label: "Indent") { indent(increase: true) } + KeyToolbarButton(icon: "decrease.indent", label: "Unindent") { indent(increase: false) } KeyToolbarDivider() - KeyToolbarButton(icon: "text.badge.slash", label: "Comment") { toggleComment() } + KeyToolbarButton(icon: "text.badge.slash", label: "Comment") { toggleComment() } KeyToolbarDivider() KeyToolbarText("(") { insertPair("(", ")") } KeyToolbarText("[") { insertPair("[", "]") } @@ -38,86 +57,233 @@ struct EditorKeyboardToolbar: View { KeyToolbarButton(icon: "arrow.left", label: "Left") { moveCursor(-1) } KeyToolbarButton(icon: "arrow.right", label: "Right") { moveCursor(+1) } KeyToolbarDivider() - KeyToolbarButton(icon: "magnifyingglass", label: "Find") { withAnimation { showFindBar.toggle() } } + KeyToolbarButton(icon: "magnifyingglass", label: "Find") { + withAnimation { showFindBar.toggle() } + } Spacer() KeyToolbarButton(icon: "keyboard.chevron.compact.down", label: "Done") { bridge.activeTextView?.resignFirstResponder() } } - .padding(.horizontal, 8).padding(.vertical, 6) + .padding(.horizontal, 8) + .padding(.vertical, 6) } .background(.regularMaterial) } + .onAppear { + bridge.onFindNext = { findNext() } + bridge.onFindPrev = { findPrev() } + } } - private func insert(_ t: String) { bridge.activeTextView?.insertText(t) } - private func insertPair(_ l: String, _ r: String) { bridge.activeTextView?.insertText(l+r); moveCursor(-1) } - private func moveCursor(_ offset: Int) { - guard let tv = bridge.activeTextView, let sel = tv.selectedRange else { return } - tv.selectedRange = NSRange(location: max(0, sel.location + offset), length: 0) + // MARK: - Text Operations + + private func insert(_ text: String) { + bridge.activeTextView?.insertText(text) } - private func toggleComment() { - guard let tv = bridge.activeTextView, let sel = tv.selectedRange else { return } + + private func insertPair(_ open: String, _ close: String) { + bridge.activeTextView?.insertText(open + close) + moveCursor(-1) + } + + private func moveCursor(_ offset: Int) { + guard let tv = bridge.activeTextView else { return } + let sel = tv.selectedRange + let newLocation = max(0, sel.location + offset) + tv.selectedRange = NSRange(location: newLocation, length: 0) + } + + private func indent(increase: Bool) { + guard let tv = bridge.activeTextView else { return } + let sel = tv.selectedRange let text = tv.text as NSString - let lr = text.lineRange(for: sel) - let line = text.substring(with: lr) + let lineRange = text.lineRange(for: sel) + let line = text.substring(with: lineRange) + let modified: String + if increase { + modified = " " + line + } else { + if line.hasPrefix(" ") { + modified = String(line.dropFirst(4)) + } else if line.hasPrefix("\t") { + modified = String(line.dropFirst(1)) + } else { + return + } + } + tv.replace(lineRange, withText: modified) + } + + private func toggleComment() { + guard let tv = bridge.activeTextView else { return } + let sel = tv.selectedRange + let text = tv.text as NSString + let lineRange = text.lineRange(for: sel) + let line = text.substring(with: lineRange) let trimmed = line.trimmingCharacters(in: .whitespaces) let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" })) let newLine: String - if trimmed.hasPrefix("// ") { newLine = line.replacingFirstOccurrence(of: "// ", with: "") } - else if trimmed.hasPrefix("//") { newLine = line.replacingFirstOccurrence(of: "//", with: "") } - else { newLine = indent + "// " + line.dropFirst(indent.count) } - tv.replace(lr, withText: newLine) + if trimmed.hasPrefix("// ") { + newLine = line.replacingFirstOccurrence(of: "// ", with: "") + } else if trimmed.hasPrefix("//") { + newLine = line.replacingFirstOccurrence(of: "//", with: "") + } else { + newLine = indent + "// " + line.dropFirst(indent.count) + } + tv.replace(lineRange, withText: newLine) } + + // MARK: - Find (manual NSString search — no Runestone-specific API) + private func findNext() { guard let tv = bridge.activeTextView, !searchQuery.isEmpty else { return } - tv.selectNextOccurrence(matching: SearchOptions(query: searchQuery)) + let fullText = tv.text as NSString + let totalLength = fullText.length + + // Search from just after the last match, wrapping if needed + let searchStart: Int + if lastMatchRange.location != NSNotFound { + searchStart = min(lastMatchRange.location + lastMatchRange.length, totalLength) + } else { + searchStart = 0 + } + + // Try from searchStart to end + var found = fullText.range(of: searchQuery, + options: .caseInsensitive, + range: NSRange(location: searchStart, + length: totalLength - searchStart)) + + // Wrap around if not found + if found.location == NSNotFound && searchStart > 0 { + found = fullText.range(of: searchQuery, + options: .caseInsensitive, + range: NSRange(location: 0, length: searchStart)) + } + + if found.location != NSNotFound { + lastMatchRange = found + tv.selectedRange = found + tv.scrollRangeToVisible(found) + } } + private func findPrev() { guard let tv = bridge.activeTextView, !searchQuery.isEmpty else { return } - tv.selectPreviousOccurrence(matching: SearchOptions(query: searchQuery)) + let fullText = tv.text as NSString + let totalLength = fullText.length + + // Search backwards from just before the last match + let searchEnd: Int + if lastMatchRange.location != NSNotFound { + searchEnd = lastMatchRange.location + } else { + searchEnd = totalLength + } + + // Try backwards from searchEnd to start + var found = fullText.range(of: searchQuery, + options: [.caseInsensitive, .backwards], + range: NSRange(location: 0, length: searchEnd)) + + // Wrap around to end + if found.location == NSNotFound && searchEnd < totalLength { + found = fullText.range(of: searchQuery, + options: [.caseInsensitive, .backwards], + range: NSRange(location: searchEnd, length: totalLength - searchEnd)) + } + + if found.location != NSNotFound { + lastMatchRange = found + tv.selectedRange = found + tv.scrollRangeToVisible(found) + } } } +// MARK: - Inline Find Bar + struct InlineFindBar: View { - @Binding var query: String; @Binding var isRegex: Bool - var onNext: () -> Void; var onPrev: () -> Void; var onDismiss: () -> Void + @Binding var query: String + var onNext: () -> Void + var onPrev: () -> Void + var onDismiss: () -> Void + var body: some View { HStack(spacing: 8) { - Image(systemName: "magnifyingglass").foregroundStyle(.secondary) - TextField("Find…", text: $query).textFieldStyle(.plain) - .font(.system(.body, design: .monospaced)).autocorrectionDisabled().autocapitalization(.none) - .onSubmit(onNext) - Toggle(isOn: $isRegex) { Image(systemName: "ellipsis.curlybraces") } - .toggleStyle(.button).buttonStyle(.bordered).tint(isRegex ? .accentColor : .secondary) - Button(action: onPrev) { Image(systemName: "chevron.up") }.buttonStyle(.bordered).disabled(query.isEmpty) - Button(action: onNext) { Image(systemName: "chevron.down") }.buttonStyle(.bordered).disabled(query.isEmpty) - Button(action: onDismiss) { Image(systemName: "xmark") }.buttonStyle(.bordered) + Image(systemName: "magnifyingglass") + .foregroundStyle(.secondary) + TextField("Find...", text: $query) + .textFieldStyle(.plain) + .font(.system(.body, design: .monospaced)) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .onSubmit { onNext() } + Button(action: onPrev) { + Image(systemName: "chevron.up") + } + .buttonStyle(.bordered) + .disabled(query.isEmpty) + Button(action: onNext) { + Image(systemName: "chevron.down") + } + .buttonStyle(.bordered) + .disabled(query.isEmpty) + Button(action: onDismiss) { + Image(systemName: "xmark") + } + .buttonStyle(.bordered) } - .padding(.horizontal, 12).padding(.vertical, 6) + .padding(.horizontal, 12) + .padding(.vertical, 6) } } +// MARK: - Toolbar Sub-components + struct KeyToolbarButton: View { - let icon: String; let label: String; let action: () -> Void + let icon: String + let label: String + let action: () -> Void + var body: some View { - Button(action: action) { Image(systemName: icon).frame(minWidth: 32, minHeight: 32).contentShape(Rectangle()) } - .buttonStyle(.plain).foregroundStyle(.primary).help(label) + Button(action: action) { + Image(systemName: icon) + .frame(minWidth: 32, minHeight: 32) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .foregroundStyle(.primary) + .help(label) } } struct KeyToolbarText: View { - let text: String; let action: () -> Void - init(_ text: String, action: @escaping () -> Void) { self.text = text; self.action = action } + let text: String + let action: () -> Void + + init(_ text: String, action: @escaping () -> Void) { + self.text = text + self.action = action + } + var body: some View { Button(action: action) { - Text(text).font(.system(.callout, design: .monospaced).bold()) - .frame(minWidth: 32, minHeight: 32).contentShape(Rectangle()) + Text(text) + .font(.system(.callout, design: .monospaced).bold()) + .frame(minWidth: 32, minHeight: 32) + .contentShape(Rectangle()) } - .buttonStyle(.plain).foregroundStyle(.primary) + .buttonStyle(.plain) + .foregroundStyle(.primary) } } struct KeyToolbarDivider: View { - var body: some View { Divider().frame(height: 20).padding(.horizontal, 2) } + var body: some View { + Divider() + .frame(height: 20) + .padding(.horizontal, 2) + } } diff --git a/Editor/EditorState.swift b/Editor/EditorState.swift index ec1d2bb..358c48f 100644 --- a/Editor/EditorState.swift +++ b/Editor/EditorState.swift @@ -113,5 +113,10 @@ final class EditorState: ObservableObject { } } + + func markSaved(tabId: UUID) { + guard let idx = tabs.firstIndex(where: { $0.id == tabId }) else { return } + tabs[idx].isDirty = false + } var dirtyTabNames: [String] { tabs.filter { $0.isDirty }.map { $0.fileName } } } diff --git a/Editor/GutterAnnotationView.swift b/Editor/GutterAnnotationView.swift index e6b723b..64038cf 100644 --- a/Editor/GutterAnnotationView.swift +++ b/Editor/GutterAnnotationView.swift @@ -1,10 +1,7 @@ -// GutterAnnotationView.swift -// DiagnosticRange is defined in Shared/SharedModels.swift -// No Identifiable conformance extension needed here. import SwiftUI import Runestone -// MARK: - Annotation Model (local to this file — wraps DiagnosticRange with position) +// MARK: - Annotation Model struct GutterAnnotation: Identifiable { let id = UUID() @@ -69,17 +66,19 @@ struct GutterAnnotationOverlay: View { private func computePositions() { guard let tv = textView else { return } var result: [PositionedAnnotation] = [] - for diag in diagnostics { let ann = GutterAnnotation(line: diag.line, severity: diag.severity, message: diag.message) - guard let range = tv.range(atLine: ann.line, column: 0), - let rect = tv.caretRect(for: range.location) else { continue } - let y = rect.midY - tv.contentOffset.y - if y > 0 && y < tv.bounds.height { - result.append(PositionedAnnotation(annotation: ann, y: y)) - } + // caretRect(for:) on Runestone.TextView takes UITextPosition, not Int. + // Compute Y from font metrics instead — reliable for a code editor + // where all lines have equal height. + let font = tv.theme.font + let lineHeight = ceil(font.lineHeight * tv.lineHeightMultiplier) + let y = CGFloat(ann.line - 1) * lineHeight + + tv.textContainerInset.top + - tv.contentOffset.y + guard y > 0 && y < tv.bounds.height else { continue } + result.append(PositionedAnnotation(annotation: ann, y: y)) } - withAnimation(.easeInOut(duration: 0.15)) { annotations = result } } } @@ -108,7 +107,8 @@ struct DiagnosticPopover: View { } } -// MARK: - Hex colour helper (local) +// MARK: - Hex colour helper (private to this file) + private extension Color { init(hex: String) { var h = hex.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Editor/HardwareKeyboardCommandHandler.swift b/Editor/HardwareKeyboardCommandHandler.swift index df3fdb0..9811f8c 100644 --- a/Editor/HardwareKeyboardCommandHandler.swift +++ b/Editor/HardwareKeyboardCommandHandler.swift @@ -87,18 +87,33 @@ final class EditorCommandHandler: UIViewController { @objc func cmdUnindent() { textView?.shiftLeft() } @objc func cmdFind() { onFind?() } @objc func cmdFindProject(){ onFindInProject?() } - @objc func cmdFindNext() {} - @objc func cmdFindPrev() {} + @objc func cmdFindNext() { + NotificationCenter.default.post( + name: .editorFindNext, object: nil) + } + @objc func cmdFindPrev() { + NotificationCenter.default.post( + name: .editorFindPrev, object: nil) + } @objc func cmdJumpToLine() { onJumpToLine?() } @objc func cmdBuild() { onBuild?() } @objc func cmdRun() { onRun?() } @objc func cmdNavigator() { onToggleNavigator?() } @objc func cmdConsole() { onToggleConsole?() } - @objc func cmdFontBigger() {} - @objc func cmdFontSmaller(){} + @objc func cmdFontBigger() { + NotificationCenter.default.post( + name: .editorFontSizeChange, object: nil, + userInfo: ["delta": 1.0]) + } + @objc func cmdFontSmaller() { + NotificationCenter.default.post( + name: .editorFontSizeChange, object: nil, + userInfo: ["delta": -1.0]) + } @objc func cmdComment() { - guard let tv = textView, let sel = tv.selectedRange else { return } + guard let tv = textView else { return } + let sel = tv.selectedRange let text = tv.text as NSString; let lr = text.lineRange(for: sel) let line = text.substring(with: lr); let trimmed = line.trimmingCharacters(in: .whitespaces) let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" })) @@ -110,7 +125,8 @@ final class EditorCommandHandler: UIViewController { } @objc func cmdMoveUp() { - guard let tv = textView, let sel = tv.selectedRange else { return } + guard let tv = textView else { return } + let sel = tv.selectedRange let text = tv.text as NSString; let cur = text.lineRange(for: sel) guard cur.location > 0 else { return } let prev = text.lineRange(for: NSRange(location: cur.location - 1, length: 0)) @@ -120,7 +136,8 @@ final class EditorCommandHandler: UIViewController { } @objc func cmdMoveDown() { - guard let tv = textView, let sel = tv.selectedRange else { return } + guard let tv = textView else { return } + let sel = tv.selectedRange let text = tv.text as NSString; let cur = text.lineRange(for: sel) let end = cur.location + cur.length; guard end < text.length else { return } let next = text.lineRange(for: NSRange(location: end, length: 0)) @@ -130,14 +147,16 @@ final class EditorCommandHandler: UIViewController { } @objc func cmdDuplicate() { - guard let tv = textView, let sel = tv.selectedRange else { return } + guard let tv = textView else { return } + let sel = tv.selectedRange let text = tv.text as NSString; let lr = text.lineRange(for: sel) let line = text.substring(with: lr) tv.replace(NSRange(location: lr.location + lr.length, length: 0), withText: line) } @objc func cmdDeleteLine() { - guard let tv = textView, let sel = tv.selectedRange else { return } + guard let tv = textView else { return } + let sel = tv.selectedRange let text = tv.text as NSString tv.replace(text.lineRange(for: sel), withText: "") } @@ -153,3 +172,44 @@ final class EditorCommandHandler: UIViewController { tv.selectedRange = end; tv.scrollRangeToVisible(end) } } + + +// MARK: - SwiftUI wrapper + +struct EditorCommandHandlerView: UIViewControllerRepresentable { + @Binding var textViewRef: Runestone.TextView? + var onSave: (() -> Void)? + var onBuild: (() -> Void)? + var onRun: (() -> Void)? + var onToggleNavigator: (() -> Void)? + var onToggleConsole: (() -> Void)? + var onFind: (() -> Void)? + var onJumpToLine: (() -> Void)? + var onFindInProject: (() -> Void)? + + func makeUIViewController(context: Context) -> EditorCommandHandler { + let vc = EditorCommandHandler() + vc.textView = textViewRef + vc.onSave = onSave + vc.onBuild = onBuild + vc.onRun = onRun + vc.onToggleNavigator = onToggleNavigator + vc.onToggleConsole = onToggleConsole + vc.onFind = onFind + vc.onJumpToLine = onJumpToLine + vc.onFindInProject = onFindInProject + return vc + } + + func updateUIViewController(_ vc: EditorCommandHandler, context: Context) { + vc.textView = textViewRef + vc.onSave = onSave + vc.onBuild = onBuild + vc.onRun = onRun + vc.onToggleNavigator = onToggleNavigator + vc.onToggleConsole = onToggleConsole + vc.onFind = onFind + vc.onJumpToLine = onJumpToLine + vc.onFindInProject = onFindInProject + } +} diff --git a/Editor/RunestoneEditorView.swift b/Editor/RunestoneEditorView.swift index 1657263..76007a1 100644 --- a/Editor/RunestoneEditorView.swift +++ b/Editor/RunestoneEditorView.swift @@ -2,6 +2,16 @@ import SwiftUI import UIKit import Runestone +// MARK: - CharacterPair concrete implementation +// CharacterPair is a protocol in Runestone — no concrete BasicCharacterPair exists. + +private struct EditorCharacterPair: CharacterPair { + let leading: String + let trailing: String +} + +// MARK: - RunestoneEditorView + struct RunestoneEditorView: UIViewRepresentable { @Binding var content: String @@ -20,7 +30,8 @@ struct RunestoneEditorView: UIViewRepresentable { func makeUIView(context: Context) -> Runestone.TextView { let tv = Runestone.TextView() - configureAppearance(tv); configureEditing(tv) + configureAppearance(tv) + configureEditing(tv) tv.editorDelegate = context.coordinator completionController.attach(to: tv) DispatchQueue.main.async { textViewRef = tv } @@ -29,19 +40,30 @@ struct RunestoneEditorView: UIViewRepresentable { } func updateUIView(_ tv: Runestone.TextView, context: Context) { - if tv.text != content && !context.coordinator.isEditing { loadState(into: tv) } + if tv.text != content && !context.coordinator.isEditing { + loadState(into: tv) + } tv.showLineNumbers = showLineNumbers tv.isLineWrappingEnabled = lineWrapping - tv.indentStrategy = indentWidth == 0 ? .tab : .space(length: indentWidth) + tv.indentStrategy = indentWidth == 0 + ? .tab(length: 4) + : .space(length: indentWidth) applyDiagnostics(to: tv) } private func loadState(into tv: Runestone.TextView) { - let lang = LanguageDetector.language(for: filePath) - let capturedContent = content; let capturedTheme = theme + let lang = LanguageDetector.language(for: filePath) + let capturedText = content + let capturedTheme = theme Task.detached(priority: .userInitiated) { - let state = TextViewState(text: capturedContent, theme: capturedTheme, - language: lang.map { TreeSitterLanguageMode(language: $0) }) + // TextViewState init takes TreeSitterLanguage (non-optional). + // Use two separate inits: one with language, one without (plain text). + let state: TextViewState + if let lang = lang { + state = TextViewState(text: capturedText, theme: capturedTheme, language: lang) + } else { + state = TextViewState(text: capturedText, theme: capturedTheme) + } await MainActor.run { tv.setState(state) } } } @@ -49,43 +71,66 @@ struct RunestoneEditorView: UIViewRepresentable { private func configureAppearance(_ tv: Runestone.TextView) { tv.backgroundColor = UIColor(hex: "#1E1E1E") tv.showLineNumbers = showLineNumbers - tv.showSpaces = false; tv.showTabs = false; tv.showLineBreaks = false + tv.showSpaces = false + tv.showTabs = false + tv.showLineBreaks = false tv.isLineWrappingEnabled = lineWrapping - tv.highlightSelectedLine = true - tv.lineHeightMultiplier = 1.4; tv.kern = 0.3 + // highlightSelectedLine was replaced by lineSelectionDisplayType. + // .line highlights the active line in the text area only. + tv.lineSelectionDisplayType = .line + tv.lineHeightMultiplier = 1.4 + tv.kern = 0.3 tv.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 120, right: 0) tv.verticalOverscrollFactor = 0.5 } private func configureEditing(_ tv: Runestone.TextView) { - tv.isEditable = true; tv.autocorrectionType = .no; tv.autocapitalizationType = .none - tv.smartDashesType = .no; tv.smartQuotesType = .no - tv.smartInsertDeleteType = .no; tv.spellCheckingType = .no + tv.isEditable = true + tv.autocorrectionType = .no + tv.autocapitalizationType = .none + tv.smartDashesType = .no + tv.smartQuotesType = .no + tv.smartInsertDeleteType = .no + tv.spellCheckingType = .no tv.characterPairs = [ - BasicCharacterPair(leading: "(", trailing: ")"), - BasicCharacterPair(leading: "[", trailing: "]"), - BasicCharacterPair(leading: "{", trailing: "}"), - BasicCharacterPair(leading: "\"", trailing: "\""), + EditorCharacterPair(leading: "(", trailing: ")"), + EditorCharacterPair(leading: "[", trailing: "]"), + EditorCharacterPair(leading: "{", trailing: "}"), + EditorCharacterPair(leading: "\"", trailing: "\""), ] tv.characterPairTrailingComponentDeletionMode = .immediatelyFollowingLeadingComponent - tv.indentStrategy = indentWidth == 0 ? .tab : .space(length: indentWidth) + tv.indentStrategy = indentWidth == 0 + ? .tab(length: 4) + : .space(length: indentWidth) } private func applyDiagnostics(to tv: Runestone.TextView) { - guard !diagnosticRanges.isEmpty else { tv.setHighlightedRanges([]); return } - let highlights: [HighlightedRange] = diagnosticRanges.compactMap { diag in - guard let range = tv.range(atLine: diag.line, column: diag.column) else { return nil } - let color: UIColor = diag.severity == .error ? UIColor(hex: "#F44747") : UIColor(hex: "#CCA700") - return HighlightedRange(range: range, color: color, cornerRadius: 0, underlineStyle: .single) + guard !diagnosticRanges.isEmpty else { + tv.highlightedRanges = [] + return } - tv.setHighlightedRanges(highlights) + var highlights: [HighlightedRange] = [] + for diag in diagnosticRanges { + // TextLocation is 0-indexed; compiler diagnostics are 1-indexed. + let textLoc = TextLocation(lineNumber: diag.line - 1, column: diag.column - 1) + guard let charOffset = tv.location(at: textLoc) else { continue } + let nsRange = NSRange(location: charOffset, length: max(1, diag.length)) + let color: UIColor = diag.severity == .error + ? UIColor(hex: "#F44747") + : UIColor(hex: "#CCA700") + // HighlightedRange only takes range and color — no cornerRadius/underlineStyle. + highlights.append(HighlightedRange(range: nsRange, color: color)) + } + tv.highlightedRanges = highlights } // MARK: - Coordinator + @MainActor final class Coordinator: NSObject, TextViewDelegate { - var parent: RunestoneEditorView - var isEditing = false + var parent: RunestoneEditorView + var isEditing: Bool = false + init(_ parent: RunestoneEditorView) { self.parent = parent } func textViewDidChange(_ tv: Runestone.TextView) { @@ -97,12 +142,20 @@ struct RunestoneEditorView: UIViewRepresentable { } func textViewDidChangeSelection(_ tv: Runestone.TextView) { - guard let sel = tv.selectedRange, let loc = tv.textLocation(at: sel.location) else { return } - parent.onCursorPositionChange?(loc.lineNumber, loc.column) + let sel = tv.selectedRange + if let loc = tv.textLocation(at: sel.location) { + parent.onCursorPositionChange?(loc.lineNumber + 1, loc.column + 1) + } if !isEditing { parent.completionController.dismiss() } } - func textViewDidBeginEditing(_ tv: Runestone.TextView) { isEditing = true } - func textViewDidEndEditing(_ tv: Runestone.TextView) { isEditing = false; parent.completionController.dismiss() } + func textViewDidBeginEditing(_ tv: Runestone.TextView) { + isEditing = true + } + + func textViewDidEndEditing(_ tv: Runestone.TextView) { + isEditing = false + parent.completionController.dismiss() + } } } diff --git a/Git/GitPanel.swift b/Git/GitPanel.swift index 7cff82b..87d6a82 100644 --- a/Git/GitPanel.swift +++ b/Git/GitPanel.swift @@ -85,7 +85,7 @@ struct GitPanel: View { Image(systemName: branch.isRemote ? "cloud" : "arrow.triangle.branch").font(.caption).foregroundStyle(.secondary) Text(branch.name).font(.system(.body, design: .monospaced)) Spacer() - if branch.name == gitStore.currentBranch { Image(systemName: "checkmark").foregroundStyle(.accentColor) } + if branch.name == gitStore.currentBranch { Image(systemName: "checkmark").foregroundStyle(Color.accentColor) } } }.buttonStyle(.plain) } @@ -213,12 +213,12 @@ struct GitPanel: View { label: { HStack { Image(systemName: branch.isRemote ? "cloud" : "arrow.triangle.branch") - .font(.caption).foregroundStyle(branch.name == gitStore.currentBranch ? .accentColor : .secondary) + .font(.caption).foregroundStyle(branch.name == gitStore.currentBranch ? Color.accentColor : .secondary) Text(branch.name).font(.system(.body, design: .monospaced)) - .foregroundStyle(branch.name == gitStore.currentBranch ? .accentColor : .primary) + .foregroundStyle(branch.name == gitStore.currentBranch ? Color.accentColor : .primary) .bold(branch.name == gitStore.currentBranch) Spacer() - if branch.name == gitStore.currentBranch { Text("current").font(.caption2).foregroundStyle(.accentColor) } + if branch.name == gitStore.currentBranch { Text("current").font(.caption2).foregroundStyle(Color.accentColor) } } } .buttonStyle(.plain) diff --git a/Info.plist b/Info.plist index 2cc1fe0..e1add6b 100644 --- a/Info.plist +++ b/Info.plist @@ -3,14 +3,22 @@ "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> + CFBundleExecutable + $(EXECUTABLE_NAME) CFBundleName PadXcode + CFBundleDisplayName + PadXcode CFBundleIdentifier - ca.dallasgroot.PadXcode + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundlePackageType + APPL CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) + CFBundleInfoDictionaryVersion + 6.0 UILaunchStoryboardName LaunchScreen CFBundleURLTypes @@ -47,5 +55,12 @@ UIRequiresFullScreen + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + diff --git a/Layout/ContentView.swift b/Layout/ContentView.swift index 837c0d9..28be0a5 100644 --- a/Layout/ContentView.swift +++ b/Layout/ContentView.swift @@ -38,7 +38,7 @@ struct ContentView: View { _buildService = StateObject(wrappedValue: bs) _syncPipeline = StateObject(wrappedValue: FileSyncPipeline( buildService: bs, gitService: gitService, - gitStore: store, config: config)) + gitStore: store)) } var body: some View { @@ -340,7 +340,7 @@ struct ProjectPickerSheet: View { Text("Scheme: \(p.scheme)").font(.caption2).foregroundStyle(.tertiary) } Spacer() - if p.id == activeProject?.id { Image(systemName:"checkmark.circle.fill").foregroundStyle(.accentColor) } + if p.id == activeProject?.id { Image(systemName:"checkmark.circle.fill").foregroundStyle(Color.accentColor) } } }.buttonStyle(.plain) } diff --git a/Layout/EnhancedConsoleView.swift b/Layout/EnhancedConsoleView.swift index e615937..f11b98d 100644 --- a/Layout/EnhancedConsoleView.swift +++ b/Layout/EnhancedConsoleView.swift @@ -61,12 +61,12 @@ struct EnhancedConsoleView: View { } Spacer() Button { withAnimation { showSearch.toggle(); if !showSearch { searchText = "" } } } - label: { Image(systemName: "magnifyingglass").foregroundStyle(showSearch ? .accentColor : .secondary) } + label: { Image(systemName: "magnifyingglass").foregroundStyle(showSearch ? Color.accentColor : Color.secondary) } .buttonStyle(.plain) Button { UIPasteboard.general.string = filtered.map(\.text).joined(separator: "\n") } label: { Image(systemName: "doc.on.doc").foregroundStyle(.secondary) }.buttonStyle(.plain) Button { isPinned.toggle() } - label: { Image(systemName: isPinned ? "pin.fill" : "pin.slash").foregroundStyle(isPinned ? .accentColor : .secondary) } + label: { Image(systemName: isPinned ? "pin.fill" : "pin.slash").foregroundStyle(isPinned ? Color.accentColor : Color.secondary) } .buttonStyle(.plain) if let clear = onClear { Button(action: clear) { Image(systemName: "trash").foregroundStyle(.secondary) }.buttonStyle(.plain) @@ -122,13 +122,13 @@ struct EnhancedConsoleView: View { private func pillColor(_ mode: FilterMode) -> Color { switch mode { case .errors: return .red; case .warnings: return .orange - case .info: return .blue; case .all: return .accentColor + case .info: return .blue; case .all: return Color.accentColor } } private func gutterColor(_ type: ConsoleLine.LineType) -> Color { switch type { case .error: return .red; case .warning: return .orange - case .success: return .green; case .info: return .accentColor; default: return .clear + case .success: return .green; case .info: return Color.accentColor; default: return .clear } } } diff --git a/Onboarding/OnboardingView.swift b/Onboarding/OnboardingView.swift index 5151c03..eab135b 100644 --- a/Onboarding/OnboardingView.swift +++ b/Onboarding/OnboardingView.swift @@ -67,7 +67,7 @@ struct OnboardingView: View { private var welcomeStep: some View { VStack(spacing: 28) { - Image(systemName: "laptopcomputer.and.ipad").font(.system(size:72,weight:.thin)).foregroundStyle(.accentColor).padding(.top,40) + Image(systemName: "laptopcomputer.and.ipad").font(.system(size:72,weight:.thin)).foregroundStyle(Color.accentColor).padding(.top,40) VStack(spacing:10) { Text("Welcome to PadXcode").font(.largeTitle.bold()).multilineTextAlignment(.center) Text("A native Swift IDE on your iPad, backed by your Mac.").font(.body).foregroundStyle(.secondary).multilineTextAlignment(.center).padding(.horizontal,24) diff --git a/PadXcode.xcodeproj/project.pbxproj b/PadXcode.xcodeproj/project.pbxproj index 8868a81..96e855a 100644 --- a/PadXcode.xcodeproj/project.pbxproj +++ b/PadXcode.xcodeproj/project.pbxproj @@ -439,6 +439,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = PadXcode.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; DEVELOPMENT_TEAM = E9C9AGS9K6; @@ -457,6 +458,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = PadXcode.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; DEVELOPMENT_TEAM = E9C9AGS9K6; diff --git a/PadXcode.xcodeproj/project.xcworkspace/xcuserdata/dallasgroot.xcuserdatad/UserInterfaceState.xcuserstate b/PadXcode.xcodeproj/project.xcworkspace/xcuserdata/dallasgroot.xcuserdatad/UserInterfaceState.xcuserstate index 5a08216..ab5fd68 100644 Binary files a/PadXcode.xcodeproj/project.xcworkspace/xcuserdata/dallasgroot.xcuserdatad/UserInterfaceState.xcuserstate and b/PadXcode.xcodeproj/project.xcworkspace/xcuserdata/dallasgroot.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/PadXcodeApp.swift b/PadXcodeApp.swift index f5d1b0a..7d5ab6e 100644 --- a/PadXcodeApp.swift +++ b/PadXcodeApp.swift @@ -3,22 +3,27 @@ import Foundation @main struct PadXcodeApp: App { - @StateObject private var daemonConfig = DaemonConfiguration() - @StateObject private var projectStore = ProjectStore() - @StateObject private var editorState = EditorState() - @StateObject private var gitStore = GitStore() - @StateObject private var lspClient: LSPClient - private let gitService: GitService + @StateObject private var daemonConfig: DaemonConfiguration + @StateObject private var projectStore: ProjectStore + @StateObject private var editorState: EditorState + @StateObject private var gitStore: GitStore + @StateObject private var lspClient: LSPClient + + private let gitService: GitService private let gitCallbackHandler: GitCallbackHandler init() { let config = DaemonConfiguration() - let apiKey = UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? "" let store = GitStore() + let apiKey = UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? "" + _daemonConfig = StateObject(wrappedValue: config) - _lspClient = StateObject(wrappedValue: LSPClient(config: config)) + _projectStore = StateObject(wrappedValue: ProjectStore()) + _editorState = StateObject(wrappedValue: EditorState()) _gitStore = StateObject(wrappedValue: store) + _lspClient = StateObject(wrappedValue: LSPClient(config: config)) + gitService = GitService(apiKey: apiKey) gitCallbackHandler = GitCallbackHandler(store: store) } diff --git a/Settings/SettingsView.swift b/Settings/SettingsView.swift index 12c8bd8..33f302c 100644 --- a/Settings/SettingsView.swift +++ b/Settings/SettingsView.swift @@ -3,8 +3,8 @@ import UIKit struct SettingsView: View { @EnvironmentObject var daemonConfig: DaemonConfiguration - @ObservedObject var editorState: EditorState - @ObservedObject var projectStore: ProjectStore + @ObservedObject var editorState: EditorState + @ObservedObject var projectStore: ProjectStore @State private var pendingHost = "" @State private var pendingPort = "" @@ -12,129 +12,243 @@ struct SettingsView: View { @State private var connStatus: ConnStatus = .untested @State private var showAddProject = false - enum ConnStatus { case untested, testing, success, failure(String) - var label: String { switch self { case .untested: return "Not tested"; case .testing: return "Testing…"; case .success: return "Connected ✓"; case .failure(let m): return "Failed: \(m)" } } - var color: Color { switch self { case .success: return .green; case .failure: return .red; case .testing: return .secondary; default: return .secondary } } - static func ==(a: ConnStatus, b: ConnStatus) -> Bool { - switch (a,b) { case (.success,.success),(.untested,.untested),(.testing,.testing): return true; case (.failure(let x),.failure(let y)): return x==y; default: return false } + // Equatable conformance added so != operator works on line 39 and elsewhere. + enum ConnStatus: Equatable { + case untested, testing, success, failure(String) + + var label: String { + switch self { + case .untested: return "Not tested" + case .testing: return "Testing..." + case .success: return "Connected checkmark" + case .failure(let m): return "Failed: \(m)" + } + } + + var color: Color { + switch self { + case .success: return .green + case .failure: return .red + default: return .secondary + } + } + + // Custom == required because .failure has an associated value. + static func == (a: ConnStatus, b: ConnStatus) -> Bool { + switch (a, b) { + case (.success, .success), (.untested, .untested), (.testing, .testing): return true + case (.failure(let x), .failure(let y)): return x == y + default: return false + } } } var body: some View { NavigationStack { Form { - // ── Daemon ──────────────────────────────────────────────── + + // ── Daemon ────────────────────────────────────────────────── Section { HStack { - Label("Tailscale IP", systemImage:"network"); Spacer() - TextField("100.x.x.x", text:$pendingHost).multilineTextAlignment(.trailing).keyboardType(.numbersAndPunctuation).autocorrectionDisabled().autocapitalization(.none).frame(width:160).onChange(of:pendingHost){_,_ in connStatus = .untested} + Label("Tailscale IP", systemImage: "network") + Spacer() + TextField("100.x.x.x", text: $pendingHost) + .multilineTextAlignment(.trailing) + .keyboardType(.numbersAndPunctuation) + .autocorrectionDisabled() + .autocapitalization(.none) + .frame(width: 160) + .onChange(of: pendingHost) { _, _ in connStatus = .untested } } HStack { - Label("Port", systemImage:"antenna.radiowaves.left.and.right"); Spacer() - TextField("8080", text:$pendingPort).multilineTextAlignment(.trailing).keyboardType(.numberPad).frame(width:80) + Label("Port", systemImage: "antenna.radiowaves.left.and.right") + Spacer() + TextField("8080", text: $pendingPort) + .multilineTextAlignment(.trailing) + .keyboardType(.numberPad) + .frame(width: 80) } - HStack { Text("Status").foregroundStyle(.secondary); Spacer(); Text(connStatus.label).foregroundStyle(connStatus.color).font(.subheadline) } - Button("Test Connection") { Task { await testConnection() } }.disabled(pendingHost.isEmpty) - Button("Save & Apply") { applyConnection() }.buttonStyle(.borderedProminent) + HStack { + Text("Status").foregroundStyle(.secondary) + Spacer() + Text(connStatus.label).foregroundStyle(connStatus.color).font(.subheadline) + } + Button("Test Connection") { Task { await testConnection() } } + .disabled(pendingHost.isEmpty) + Button("Save & Apply") { applyConnection() } + .buttonStyle(.borderedProminent) + // != now works because ConnStatus conforms to Equatable. .disabled(pendingHost.isEmpty || connStatus != .success) } header: { Text("Mac Daemon") } - // ── Projects ────────────────────────────────────────────── + // ── Projects ──────────────────────────────────────────────── Section { ForEach(projectStore.projects) { p in - VStack(alignment:.leading, spacing:2) { + VStack(alignment: .leading, spacing: 2) { Text(p.name).font(.body) Text(p.rootPath).font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.middle) - Text("Scheme: \(p.scheme) · WC: \(p.workingCopyRepoName.isEmpty ? "not set" : p.workingCopyRepoName)").font(.caption2).foregroundStyle(.tertiary) - }.padding(.vertical,2) - }.onDelete { projectStore.remove(atOffsets:$0) } - Button { showAddProject = true } label: { Label("Add Project", systemImage:"plus.circle") } - } header: { Text("Projects") } footer: { Text("Mac path should be the directory containing project.yml or .xcodeproj") } + Text("Scheme: \(p.scheme) · WC: \(p.workingCopyRepoName.isEmpty ? "not set" : p.workingCopyRepoName)") + .font(.caption2).foregroundStyle(.tertiary) + } + .padding(.vertical, 2) + } + .onDelete { projectStore.remove(atOffsets: $0) } + Button { showAddProject = true } label: { + Label("Add Project", systemImage: "plus.circle") + } + } header: { + Text("Projects") + } footer: { + Text("Mac path should be the directory containing project.yml or .xcodeproj") + } - // ── Editor ──────────────────────────────────────────────── + // ── Editor ────────────────────────────────────────────────── Section("Editor Appearance") { HStack { - Label("Font Size", systemImage:"textformat.size"); Spacer() - Stepper("\(Int(editorState.fontSize))pt", value:$editorState.fontSize, in:8...32, step:1).frame(width:160) + Label("Font Size", systemImage: "textformat.size") + Spacer() + // fontSizeStored is the @AppStorage Double backing fontSize. + // Binding to the computed fontSize property is not possible; + // bind to the stored Double and display with Int cast. + Stepper( + "\(Int(editorState.fontSizeStored))pt", + value: $editorState.fontSizeStored, + in: 8.0...32.0, + step: 1.0 + ) + .frame(width: 160) } - Picker(selection:$editorState.selectedTheme) { - ForEach(EditorThemeOption.allCases) { o in Text(o.displayName).tag(o) } - } label: { Label("Theme", systemImage:"paintbrush") } - Toggle(isOn:$editorState.showLineNumbers) { Label("Line Numbers", systemImage:"list.number") } - Toggle(isOn:$editorState.lineWrapping) { Label("Line Wrapping", systemImage:"text.word.spacing") } + // selectedTheme is a computed property on EditorState — + // cannot use $ directly; use an explicit Binding. + Picker(selection: Binding( + get: { editorState.selectedTheme }, + set: { editorState.selectedTheme = $0 } + )) { + ForEach(EditorThemeOption.allCases) { o in + Text(o.displayName).tag(o) + } + } label: { + Label("Theme", systemImage: "paintbrush") + } + Toggle(isOn: $editorState.showLineNumbers) { Label("Line Numbers", systemImage: "list.number") } + Toggle(isOn: $editorState.lineWrapping) { Label("Line Wrapping", systemImage: "text.word.spacing") } HStack { - Label("Indent Width", systemImage:"arrow.right.to.line"); Spacer() - Picker("", selection:$editorState.indentWidth) { - Text("2 spaces").tag(2); Text("4 spaces").tag(4); Text("Tab").tag(0) - }.labelsHidden().pickerStyle(.menu) + Label("Indent Width", systemImage: "arrow.right.to.line") + Spacer() + Picker("", selection: $editorState.indentWidth) { + Text("2 spaces").tag(2) + Text("4 spaces").tag(4) + Text("Tab").tag(0) + } + .labelsHidden() + .pickerStyle(.menu) } } - // ── Build ───────────────────────────────────────────────── + // ── Build ──────────────────────────────────────────────────── Section("Build") { HStack { - Label("Team ID", systemImage:"person.badge.key"); Spacer() - TextField("XXXXXXXXXX", text:$daemonConfig.developmentTeam).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:120) + Label("Team ID", systemImage: "person.badge.key") + Spacer() + TextField("XXXXXXXXXX", text: $daemonConfig.developmentTeam) + .multilineTextAlignment(.trailing) + .autocorrectionDisabled() + .autocapitalization(.none) + .frame(width: 120) + } + Toggle(isOn: $daemonConfig.allowProvisioningUpdates) { + Label("Allow Provisioning Updates", systemImage: "checkmark.seal") + } + Toggle(isOn: $daemonConfig.buildInRelease) { + Label("Build in Release", systemImage: "bolt") } - Toggle(isOn:$daemonConfig.allowProvisioningUpdates) { Label("Allow Provisioning Updates", systemImage:"checkmark.seal") } - Toggle(isOn:$daemonConfig.buildInRelease) { Label("Build in Release", systemImage:"bolt") } } - // ── Working Copy ────────────────────────────────────────── + // ── Working Copy ───────────────────────────────────────────── Section { HStack { - Label("API Key", systemImage:"key.fill"); Spacer() - SecureField("Paste from Working Copy", text:$wcApiKey).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:200) - .onChange(of:wcApiKey){_,k in UserDefaults.standard.set(k, forKey:"workingCopyAPIKey")} + Label("API Key", systemImage: "key.fill") + Spacer() + SecureField("Paste from Working Copy", text: $wcApiKey) + .multilineTextAlignment(.trailing) + .autocorrectionDisabled() + .autocapitalization(.none) + .frame(width: 200) + .onChange(of: wcApiKey) { _, k in + UserDefaults.standard.set(k, forKey: "workingCopyAPIKey") + } } - let installed = UIApplication.shared.canOpenURL(URL(string:"working-copy://")!) - Label(installed ? "Working Copy detected ✓" : "Working Copy not installed", - systemImage: installed ? "checkmark.circle.fill" : "exclamationmark.triangle") - .foregroundStyle(installed ? .green : .orange).font(.caption) + let installed = UIApplication.shared.canOpenURL(URL(string: "working-copy://")!) + Label( + installed ? "Working Copy detected" : "Working Copy not installed", + systemImage: installed ? "checkmark.circle.fill" : "exclamationmark.triangle" + ) + .foregroundStyle(installed ? .green : .orange) + .font(.caption) Text("For local git over Tailscale, clone in Working Copy using:\nssh://user@100.x.x.x/path/to/repo.git") .font(.caption).foregroundStyle(.secondary) - } header: { Text("Working Copy (Git)") } + } header: { + Text("Working Copy (Git)") + } - // ── LSP ─────────────────────────────────────────────────── + // ── LSP ────────────────────────────────────────────────────── Section { - Toggle(isOn:$daemonConfig.lspEnabled) { Label("sourcekit-lsp Proxy", systemImage:"wand.and.stars") } + Toggle(isOn: $daemonConfig.lspEnabled) { + Label("sourcekit-lsp Proxy", systemImage: "wand.and.stars") + } if daemonConfig.lspEnabled { HStack { - Label("Status", systemImage:"circle.fill").foregroundStyle(daemonConfig.lspConnected ? Color.green : Color.red) + Label("Status", systemImage: "circle.fill") + .foregroundStyle(daemonConfig.lspConnected ? Color.green : Color.red) Spacer() - Text(daemonConfig.lspConnected ? "Running" : "Disconnected").foregroundStyle(.secondary) + Text(daemonConfig.lspConnected ? "Running" : "Disconnected") + .foregroundStyle(.secondary) } } } header: { Text("Language Server (LSP)") } - // ── About ───────────────────────────────────────────────── + // ── About ───────────────────────────────────────────────────── Section("About") { - LabeledContent("Version", value:"1.0.0-alpha") - LabeledContent("Editor Engine", value:"Runestone 0.5.1 / Tree-sitter") - LabeledContent("Protocol", value:"REST + WebSocket") + LabeledContent("Version", value: "1.0.0-alpha") + LabeledContent("Editor Engine", value: "Runestone 0.5.1 / Tree-sitter") + LabeledContent("Protocol", value: "REST + WebSocket") } } - .navigationTitle("Settings").navigationBarTitleDisplayMode(.inline) - .onAppear { pendingHost = daemonConfig.host; pendingPort = String(daemonConfig.port) - wcApiKey = UserDefaults.standard.string(forKey:"workingCopyAPIKey") ?? "" } - .sheet(isPresented:$showAddProject) { AddProjectView(projectStore:projectStore) } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + pendingHost = daemonConfig.host + pendingPort = String(daemonConfig.port) + wcApiKey = UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? "" + } + .sheet(isPresented: $showAddProject) { + AddProjectView(projectStore: projectStore) + } } } + // MARK: - Actions + private func testConnection() async { connStatus = .testing - let host = pendingHost.trimmingCharacters(in:.whitespaces) + let host = pendingHost.trimmingCharacters(in: .whitespaces) let port = Int(pendingPort) ?? 8080 - guard let url = URL(string:"http://\(host):\(port)/health") else { connStatus = .failure("Invalid URL"); return } - var req = URLRequest(url:url); req.timeoutInterval = 4 + guard let url = URL(string: "http://\(host):\(port)/health") else { + connStatus = .failure("Invalid URL"); return + } + var req = URLRequest(url: url); req.timeoutInterval = 4 do { - let (_,resp) = try await URLSession.shared.data(for:req) - connStatus = (resp as? HTTPURLResponse)?.statusCode == 200 ? .success : .failure("Unexpected response") - } catch { connStatus = .failure(error.localizedDescription) } + let (_, resp) = try await URLSession.shared.data(for: req) + connStatus = (resp as? HTTPURLResponse)?.statusCode == 200 + ? .success + : .failure("Unexpected status code") + } catch { + connStatus = .failure(error.localizedDescription) + } } private func applyConnection() { - daemonConfig.host = pendingHost.trimmingCharacters(in:.whitespaces) + daemonConfig.host = pendingHost.trimmingCharacters(in: .whitespaces) daemonConfig.port = Int(pendingPort) ?? 8080 } } @@ -144,33 +258,57 @@ struct SettingsView: View { struct AddProjectView: View { @ObservedObject var projectStore: ProjectStore @Environment(\.dismiss) var dismiss - @State private var name = ""; @State private var path = "" - @State private var scheme = ""; @State private var wcRepo = "" + + @State private var name = "" + @State private var path = "" + @State private var scheme = "" + @State private var wcRepo = "" var body: some View { NavigationStack { Form { Section("Project Details") { - TextField("Name (e.g. NavidromePlayer)", text:$name) - TextField("/Users/dallas/Dev/NavidromePlayer/NavidromePlayer", text:$path).autocorrectionDisabled().autocapitalization(.none).keyboardType(.URL) - TextField("Xcode Scheme", text:$scheme).autocorrectionDisabled().autocapitalization(.none) + TextField("Name (e.g. NavidromePlayer)", text: $name) + TextField("/Users/dallas/Dev/NavidromePlayer/NavidromePlayer", text: $path) + .autocorrectionDisabled() + .autocapitalization(.none) + .keyboardType(.URL) + TextField("Xcode Scheme", text: $scheme) + .autocorrectionDisabled() + .autocapitalization(.none) + } + // Section with both header string AND footer requires the + // explicit header:/footer: trailing closure form. + Section { + TextField("Repo name in Working Copy (e.g. NavidromePlayer)", text: $wcRepo) + .autocorrectionDisabled() + .autocapitalization(.none) + } header: { + Text("Working Copy") + } footer: { + Text("Must match the display name in Working Copy's repo list.") } - Section("Working Copy") { - TextField("Repo name in Working Copy (e.g. NavidromePlayer)", text:$wcRepo).autocorrectionDisabled().autocapitalization(.none) - } footer: { Text("Must match the display name in Working Copy's repo list.") } } - .navigationTitle("Add Project").navigationBarTitleDisplayMode(.inline) + .navigationTitle("Add Project") + .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement:.cancellationAction) { Button("Cancel") { dismiss() } } - ToolbarItem(placement:.confirmationAction) { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { Button("Add") { - projectStore.add(SavedProject(name: name.isEmpty ? scheme : name, - rootPath: path, scheme: scheme, - workingCopyRepoName: wcRepo)) + projectStore.add(SavedProject( + name: name.isEmpty ? scheme : name, + rootPath: path, + scheme: scheme, + workingCopyRepoName: wcRepo + )) dismiss() - }.disabled(path.isEmpty || scheme.isEmpty) + } + .disabled(path.isEmpty || scheme.isEmpty) } } - }.presentationDetents([.medium]) + } + .presentationDetents([.medium]) } } diff --git a/Shared/SharedModels.swift b/Shared/SharedModels.swift index b079c07..c9776e3 100644 --- a/Shared/SharedModels.swift +++ b/Shared/SharedModels.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import SwiftUI // MARK: - File System @@ -68,7 +69,7 @@ struct FileContentResponse: Codable { // MARK: - Console -struct ConsoleLine: Identifiable { +struct ConsoleLine: Identifiable, Equatable { let id = UUID() let text: String let type: LineType @@ -159,6 +160,9 @@ extension Notification.Name { static let navigateToRange = Notification.Name("editor.navigate.range") static let triggerCompletion = Notification.Name("editor.trigger.completion") static let openSettings = Notification.Name("padxcode.open.settings") + static let editorFindNext = Notification.Name("editor.find.next") + static let editorFindPrev = Notification.Name("editor.find.prev") + static let editorFontSizeChange = Notification.Name("editor.font.size.change") } // MARK: - String helpers @@ -172,3 +176,20 @@ extension String { return self.replacingCharacters(in: range, with: replacement) } } + +// MARK: - UIColor hex convenience (used by themes and editor views) + +extension UIColor { + convenience init(hex: String) { + var h = hex.trimmingCharacters(in: .whitespacesAndNewlines) + h = h.hasPrefix("#") ? String(h.dropFirst()) : h + var rgb: UInt64 = 0 + Scanner(string: h).scanHexInt64(&rgb) + self.init( + red: CGFloat((rgb >> 16) & 0xFF) / 255.0, + green: CGFloat((rgb >> 8) & 0xFF) / 255.0, + blue: CGFloat( rgb & 0xFF) / 255.0, + alpha: 1.0 + ) + } +} diff --git a/Theme/LanguageDetector.swift b/Theme/LanguageDetector.swift index e39d4ed..6b89f62 100644 --- a/Theme/LanguageDetector.swift +++ b/Theme/LanguageDetector.swift @@ -1,3 +1,4 @@ +import Foundation import Runestone import TreeSitterSwiftRunestone import TreeSitterJSONRunestone diff --git a/Theme/PadXcodeTheme.swift b/Theme/PadXcodeTheme.swift index fa826fa..dccf2f9 100644 --- a/Theme/PadXcodeTheme.swift +++ b/Theme/PadXcodeTheme.swift @@ -1,151 +1,262 @@ import UIKit import Runestone -final class PadXcodeTheme: Theme { - var backgroundColor: UIColor { UIColor(hex: "#1E1E1E") } - var userInterfaceStyle: UIUserInterfaceStyle { .dark } - var font: UIFont { UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) } - var textColor: UIColor { UIColor(hex: "#D4D4D4") } - var gutterBackgroundColor: UIColor { UIColor(hex: "#252526") } - var gutterHairlineColor: UIColor { UIColor(hex: "#3C3C3C") } - var lineNumberColor: UIColor { UIColor(hex: "#6E7681") } - var lineNumberFont: UIFont { UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) } - var selectedLineBackgroundColor: UIColor { UIColor(hex: "#2A2D2E") } - var selectedLinesLineNumberColor: UIColor { UIColor(hex: "#C8C8C8") } - var selectedLinesGutterBackgroundColor: UIColor { UIColor(hex: "#2A2D2E") } - var markedTextBackgroundColor: UIColor { UIColor(hex: "#264F78").withAlphaComponent(0.4) } - var markedTextBackgroundCornerRadius: CGFloat { 2 } - var invisibleCharactersColor: UIColor { UIColor(hex: "#404040") } - var pageGuideHairlineColor: UIColor { UIColor(hex: "#404040") } - var pageGuideBackgroundColor: UIColor { UIColor(hex: "#1A1A1A") } +// MARK: - Xcode Default Dark Theme - func textStyle(for highlightName: String) -> TextStyle? { +final class PadXcodeTheme: Theme { + + // MARK: Required visual properties + + var font: UIFont { + UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + } + + var textColor: UIColor { + UIColor(r: 212, g: 212, b: 212) + } + + var backgroundColor: UIColor { + UIColor(r: 30, g: 30, b: 30) + } + + var userInterfaceStyle: UIUserInterfaceStyle { + .dark + } + + var gutterBackgroundColor: UIColor { + UIColor(r: 37, g: 37, b: 38) + } + + var gutterHairlineColor: UIColor { + UIColor(r: 60, g: 60, b: 60) + } + + var lineNumberColor: UIColor { + UIColor(r: 110, g: 118, b: 129) + } + + var lineNumberFont: UIFont { + UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) + } + + var selectedLineBackgroundColor: UIColor { + UIColor(r: 42, g: 45, b: 46) + } + + var selectedLinesLineNumberColor: UIColor { + UIColor(r: 200, g: 200, b: 200) + } + + var selectedLinesGutterBackgroundColor: UIColor { + UIColor(r: 42, g: 45, b: 46) + } + + var markedTextBackgroundColor: UIColor { + UIColor(r: 38, g: 79, b: 120, alpha: 0.4) + } + + var markedTextBackgroundCornerRadius: CGFloat { + 2 + } + + var invisibleCharactersColor: UIColor { + UIColor(r: 64, g: 64, b: 64) + } + + var pageGuideHairlineColor: UIColor { + UIColor(r: 64, g: 64, b: 64) + } + + var pageGuideBackgroundColor: UIColor { + UIColor(r: 26, g: 26, b: 26) + } + + // MARK: Syntax token colours (required by Theme protocol) + + func textColor(for highlightName: String) -> UIColor? { switch highlightName { - case "keyword", "keyword.control", "keyword.operator", "keyword.other", - "keyword.declaration", "storage.type": - return TextStyle(color: UIColor(hex: "#FF7AB2"), bold: true) - case "storage.modifier": - return TextStyle(color: UIColor(hex: "#FF7AB2")) - case "type", "type.builtin", "entity.name.type", "entity.name.type.class", - "entity.name.type.struct", "entity.name.type.enum", "entity.name.type.protocol": - return TextStyle(color: UIColor(hex: "#DABAFF")) + case "keyword", "keyword.control", "keyword.operator", + "keyword.other", "keyword.declaration", "storage.type", + "storage.modifier": + return UIColor(r: 255, g: 122, b: 178) // pink + + case "type", "type.builtin", "entity.name.type", + "entity.name.type.class", "entity.name.type.struct", + "entity.name.type.enum", "entity.name.type.protocol": + return UIColor(r: 218, g: 186, b: 255) // lavender + case "type.parameter": - return TextStyle(color: UIColor(hex: "#6BDFFF")) - case "function", "entity.name.function", "meta.function-call", "function.method": - return TextStyle(color: UIColor(hex: "#67B7FB")) - case "function.builtin": - return TextStyle(color: UIColor(hex: "#67B7FB"), bold: true) - case "variable", "variable.other", "variable.other.member": - return TextStyle(color: UIColor(hex: "#D4D4D4")) - case "variable.builtin": - return TextStyle(color: UIColor(hex: "#FF7AB2")) + return UIColor(r: 107, g: 223, b: 255) // light blue + + case "function", "entity.name.function", + "meta.function-call", "function.method", "function.builtin": + return UIColor(r: 103, g: 183, b: 251) // blue + case "property", "variable.other.property": - return TextStyle(color: UIColor(hex: "#72B9D5")) + return UIColor(r: 114, g: 185, b: 213) // teal + + case "variable.builtin": + return UIColor(r: 255, g: 122, b: 178) // pink (self, super) + case "constant", "constant.builtin": - return TextStyle(color: UIColor(hex: "#D9C97C"), bold: true) + return UIColor(r: 217, g: 201, b: 124) // gold bold + case "constant.numeric", "number": - return TextStyle(color: UIColor(hex: "#D9C97C")) + return UIColor(r: 217, g: 201, b: 124) // gold + case "string", "string.special": - return TextStyle(color: UIColor(hex: "#FF8170")) + return UIColor(r: 255, g: 129, b: 112) // salmon + case "string.escape": - return TextStyle(color: UIColor(hex: "#FF8170"), bold: true) - case "comment", "comment.line", "comment.block": - return TextStyle(color: UIColor(hex: "#7F8C98"), italic: true) - case "comment.block.documentation": - return TextStyle(color: UIColor(hex: "#6A9955"), italic: true) - case "operator", "punctuation.operator", "punctuation.bracket", "punctuation.delimiter": - return TextStyle(color: UIColor(hex: "#D4D4D4")) + return UIColor(r: 255, g: 129, b: 112) // salmon + + case "comment", "comment.line", "comment.block", + "comment.block.documentation": + return UIColor(r: 127, g: 140, b: 152) // grey + case "attribute": - return TextStyle(color: UIColor(hex: "#BF86C8")) + return UIColor(r: 191, g: 134, b: 200) // purple + + case "operator", "punctuation.operator", + "punctuation.bracket", "punctuation.delimiter": + return UIColor(r: 212, g: 212, b: 212) // default text + default: return nil } } } +// MARK: - Xcode Default Light Theme + final class PadXcodeLightTheme: Theme { - var backgroundColor: UIColor { UIColor(hex: "#FFFFFF") } - var userInterfaceStyle: UIUserInterfaceStyle { .light } - var font: UIFont { UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) } - var textColor: UIColor { UIColor(hex: "#000000") } - var gutterBackgroundColor: UIColor { UIColor(hex: "#F2F2F2") } - var gutterHairlineColor: UIColor { UIColor(hex: "#D8D8D8") } - var lineNumberColor: UIColor { UIColor(hex: "#A0A0A0") } - var lineNumberFont: UIFont { UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) } - var selectedLineBackgroundColor: UIColor { UIColor(hex: "#ECF5FF") } - var selectedLinesLineNumberColor: UIColor { UIColor(hex: "#3A3A3A") } - var selectedLinesGutterBackgroundColor: UIColor { UIColor(hex: "#E8F0FA") } - var markedTextBackgroundColor: UIColor { UIColor(hex: "#B5D0F7").withAlphaComponent(0.4) } - var markedTextBackgroundCornerRadius: CGFloat { 2 } - var invisibleCharactersColor: UIColor { UIColor(hex: "#CCCCCC") } - var pageGuideHairlineColor: UIColor { UIColor(hex: "#DDDDDD") } - var pageGuideBackgroundColor: UIColor { UIColor(hex: "#F9F9F9") } - func textStyle(for highlightName: String) -> TextStyle? { + // MARK: Required visual properties + + var font: UIFont { + UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + } + + var textColor: UIColor { + UIColor(r: 0, g: 0, b: 0) + } + + var backgroundColor: UIColor { + UIColor(r: 255, g: 255, b: 255) + } + + var userInterfaceStyle: UIUserInterfaceStyle { + .light + } + + var gutterBackgroundColor: UIColor { + UIColor(r: 242, g: 242, b: 242) + } + + var gutterHairlineColor: UIColor { + UIColor(r: 216, g: 216, b: 216) + } + + var lineNumberColor: UIColor { + UIColor(r: 160, g: 160, b: 160) + } + + var lineNumberFont: UIFont { + UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) + } + + var selectedLineBackgroundColor: UIColor { + UIColor(r: 236, g: 245, b: 255) + } + + var selectedLinesLineNumberColor: UIColor { + UIColor(r: 58, g: 58, b: 58) + } + + var selectedLinesGutterBackgroundColor: UIColor { + UIColor(r: 232, g: 240, b: 250) + } + + var markedTextBackgroundColor: UIColor { + UIColor(r: 181, g: 208, b: 247, alpha: 0.4) + } + + var markedTextBackgroundCornerRadius: CGFloat { + 2 + } + + var invisibleCharactersColor: UIColor { + UIColor(r: 204, g: 204, b: 204) + } + + var pageGuideHairlineColor: UIColor { + UIColor(r: 221, g: 221, b: 221) + } + + var pageGuideBackgroundColor: UIColor { + UIColor(r: 249, g: 249, b: 249) + } + + // MARK: Syntax token colours (required by Theme protocol) + + func textColor(for highlightName: String) -> UIColor? { switch highlightName { - case "keyword", "keyword.control", "keyword.operator", "keyword.other", - "keyword.declaration", "storage.type": - return TextStyle(color: UIColor(hex: "#AD3DA4"), bold: true) - case "storage.modifier": - return TextStyle(color: UIColor(hex: "#AD3DA4")) - case "type", "type.builtin", "entity.name.type", "entity.name.type.class", - "entity.name.type.struct", "entity.name.type.enum", "entity.name.type.protocol": - return TextStyle(color: UIColor(hex: "#703DAA")) + case "keyword", "keyword.control", "keyword.operator", + "keyword.other", "keyword.declaration", "storage.type", + "storage.modifier": + return UIColor(r: 173, g: 61, b: 164) // purple + + case "type", "type.builtin", "entity.name.type", + "entity.name.type.class", "entity.name.type.struct", + "entity.name.type.enum", "entity.name.type.protocol": + return UIColor(r: 112, g: 61, b: 170) // dark purple + case "type.parameter": - return TextStyle(color: UIColor(hex: "#047CB0")) - case "function", "entity.name.function", "meta.function-call", "function.method": - return TextStyle(color: UIColor(hex: "#3900A0")) - case "function.builtin": - return TextStyle(color: UIColor(hex: "#3900A0"), bold: true) - case "variable", "variable.other", "variable.other.member": - return TextStyle(color: UIColor(hex: "#000000")) - case "variable.builtin": - return TextStyle(color: UIColor(hex: "#AD3DA4")) + return UIColor(r: 4, g: 124, b: 176) // blue + + case "function", "entity.name.function", + "meta.function-call", "function.method", "function.builtin": + return UIColor(r: 57, g: 0, b: 160) // indigo + case "property", "variable.other.property": - return TextStyle(color: UIColor(hex: "#047CB0")) + return UIColor(r: 4, g: 124, b: 176) // blue + + case "variable.builtin": + return UIColor(r: 173, g: 61, b: 164) // purple (self, super) + case "constant", "constant.builtin": - return TextStyle(color: UIColor(hex: "#272AD8"), bold: true) + return UIColor(r: 39, g: 42, b: 216) // blue bold + case "constant.numeric", "number": - return TextStyle(color: UIColor(hex: "#272AD8")) + return UIColor(r: 39, g: 42, b: 216) // blue + case "string", "string.special": - return TextStyle(color: UIColor(hex: "#C41A16")) + return UIColor(r: 196, g: 26, b: 22) // red + case "string.escape": - return TextStyle(color: UIColor(hex: "#C41A16"), bold: true) - case "comment", "comment.line", "comment.block": - return TextStyle(color: UIColor(hex: "#5D6C79"), italic: true) - case "comment.block.documentation": - return TextStyle(color: UIColor(hex: "#265B31"), italic: true) - case "operator", "punctuation.operator", "punctuation.bracket", "punctuation.delimiter": - return TextStyle(color: UIColor(hex: "#000000")) + return UIColor(r: 196, g: 26, b: 22) // red + + case "comment", "comment.line", "comment.block", + "comment.block.documentation": + return UIColor(r: 93, g: 108, b: 121) // grey + case "attribute": - return TextStyle(color: UIColor(hex: "#6C36A9")) + return UIColor(r: 108, g: 54, b: 169) // purple + + case "operator", "punctuation.operator", + "punctuation.bracket", "punctuation.delimiter": + return nil // inherits default textColor + default: return nil } } } -// MARK: - TextStyle helpers +// MARK: - UIColor RGB convenience (private to this file) -extension TextStyle { - init(color: UIColor, bold: Bool = false, italic: Bool = false) { - self.init() - self.color = color - if bold { self.fontTraits.insert(.traitBold) } - if italic { self.fontTraits.insert(.traitItalic) } - } -} - -// MARK: - UIColor hex - -extension UIColor { - convenience init(hex: String) { - var h = hex.trimmingCharacters(in: .whitespacesAndNewlines) - h = h.hasPrefix("#") ? String(h.dropFirst()) : h - var rgb: UInt64 = 0 - Scanner(string: h).scanHexInt64(&rgb) - self.init(red: CGFloat((rgb >> 16) & 0xFF) / 255, - green: CGFloat((rgb >> 8) & 0xFF) / 255, - blue: CGFloat( rgb & 0xFF) / 255, alpha: 1) +private extension UIColor { + convenience init(r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat = 1.0) { + self.init(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: alpha) } } diff --git a/UI/ToastSystem.swift b/UI/ToastSystem.swift index c2b251f..c0bb067 100644 --- a/UI/ToastSystem.swift +++ b/UI/ToastSystem.swift @@ -15,7 +15,7 @@ struct Toast: Identifiable { var color: Color { switch self { case .success: return .green; case .error: return .red - case .warning: return .orange; case .info: return .accentColor; case .git: return .purple + case .warning: return .orange; case .info: return Color.accentColor; case .git: return .purple } } } diff --git a/project.yml b/project.yml index 51d512b..2313ba9 100644 --- a/project.yml +++ b/project.yml @@ -72,6 +72,7 @@ targets: base: INFOPLIST_FILE: Info.plist CODE_SIGN_ENTITLEMENTS: PadXcode.entitlements + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION: YES LD_RUNPATH_SEARCH_PATHS: - "$(inherited)" - "@executable_path/Frameworks"