From 389b051ca1c0d5062480d70425db650b741f0fc2 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 10 Apr 2026 22:47:39 +0200 Subject: [PATCH] Syntax highlighting --- .DS_Store | Bin 0 -> 8196 bytes .../UserInterfaceState.xcuserstate | Bin 15758 -> 23041 bytes dock-g/dock-g/Utilities/ENVHighlighter.swift | 134 +++++++++++++ .../Utilities/SyntaxHighlightingEditor.swift | 64 +++++++ dock-g/dock-g/Utilities/YAMLHighlighter.swift | 179 ++++++++++++++++++ .../dock-g/Views/Stacks/ComposeTabView.swift | 26 ++- original-source/dockge | 1 - 7 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 .DS_Store create mode 100644 dock-g/dock-g/Utilities/ENVHighlighter.swift create mode 100644 dock-g/dock-g/Utilities/SyntaxHighlightingEditor.swift create mode 100644 dock-g/dock-g/Utilities/YAMLHighlighter.swift delete mode 160000 original-source/dockge diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3c7c73c41c09dafaa71403402defe90bf2783515 GIT binary patch literal 8196 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 IJ}T$_KSrMZnE(I) delta 7516 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 diff --git a/dock-g/dock-g/Utilities/ENVHighlighter.swift b/dock-g/dock-g/Utilities/ENVHighlighter.swift new file mode 100644 index 0000000..4a4ce6c --- /dev/null +++ b/dock-g/dock-g/Utilities/ENVHighlighter.swift @@ -0,0 +1,134 @@ +import UIKit +import SwiftUI + +/// Tokenises .env file text into a syntax-highlighted NSAttributedString. +/// Handles KEY=value pairs, comments, and quoted values. +enum ENVHighlighter { + + // MARK: - Palette + + private static let keyColor = UIColor(hex: "#3b82f6") // blue — variable names + private static let stringColor = UIColor(hex: "#22c55e") // green — quoted values + private static let numberColor = UIColor(hex: "#f59e0b") // amber — numeric values + private static let boolColor = UIColor(hex: "#c084fc") // violet — true/false + private static let commentColor = UIColor(hex: "#6b7280") // gray — # comments + private static let eqColor = UIColor(hex: "#94a3b8") // slate — = sign + private static let plainColor = UIColor(hex: "#e2e8f0") // slate — plain values + + private static let monoFont = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + + // MARK: - Public API + + static func nsAttributedString(from env: String) -> NSAttributedString { + let result = NSMutableAttributedString() + let lines = env.components(separatedBy: "\n") + for (i, line) in lines.enumerated() { + result.append(processLine(line)) + if i < lines.count - 1 { + result.append(seg("\n", plainColor)) + } + } + return result + } + + static func attributedString(from env: String) -> AttributedString { + (try? AttributedString(nsAttributedString(from: env), including: \.uiKit)) ?? AttributedString(env) + } + + // MARK: - Line processing + + private static func processLine(_ line: String) -> NSAttributedString { + guard !line.isEmpty else { return seg("", plainColor) } + + let stripped = line.trimmingCharacters(in: .whitespaces) + + // Blank / whitespace-only + if stripped.isEmpty { return seg(line, plainColor) } + + // Comment + if stripped.hasPrefix("#") { return seg(line, commentColor) } + + // KEY=value or export KEY=value + var workLine = line + let result = NSMutableAttributedString() + + // Strip optional "export " prefix + if workLine.hasPrefix("export ") { + result.append(seg("export ", eqColor)) + workLine = String(workLine.dropFirst(7)) + } + + if let eqIdx = workLine.firstIndex(of: "=") { + let key = String(workLine[.. UIColor { + let v = raw.trimmingCharacters(in: .whitespaces) + guard !v.isEmpty else { return plainColor } + + // Quoted strings + if v.count >= 2, + (v.hasPrefix("\"") && v.hasSuffix("\"")) || + (v.hasPrefix("'") && v.hasSuffix("'")) { return stringColor } + + // Booleans + switch v.lowercased() { + case "true", "false", "yes", "no", "1", "0": return boolColor + default: break + } + + // Numbers + if Int(v) != nil || Double(v) != nil { return numberColor } + + return plainColor + } + + private static func inlineCommentIndex(in text: String) -> String.Index? { + var inSingle = false, inDouble = false, prev: Character = " " + for idx in text.indices { + let ch = text[idx] + if ch == "\"" && !inSingle { inDouble.toggle() } + else if ch == "'" && !inDouble { inSingle.toggle() } + else if ch == "#" && !inSingle && !inDouble && prev == " " { + return text.index(before: idx) + } + prev = ch + } + return nil + } + + // MARK: - Helpers + + private static func seg(_ text: String, _ color: UIColor) -> NSAttributedString { + NSAttributedString(string: text, attributes: [ + .font: monoFont, + .foregroundColor: color + ]) + } +} diff --git a/dock-g/dock-g/Utilities/SyntaxHighlightingEditor.swift b/dock-g/dock-g/Utilities/SyntaxHighlightingEditor.swift new file mode 100644 index 0000000..0166291 --- /dev/null +++ b/dock-g/dock-g/Utilities/SyntaxHighlightingEditor.swift @@ -0,0 +1,64 @@ +import SwiftUI +import UIKit + +/// A UITextView-backed editor that applies syntax highlighting on every keystroke. +/// Pass any `(String) -> NSAttributedString` highlighter — shared by YAML and ENV. +struct SyntaxHighlightingEditor: UIViewRepresentable { + @Binding var text: String + let highlight: (String) -> NSAttributedString + + func makeUIView(context: Context) -> UITextView { + let tv = UITextView() + tv.delegate = context.coordinator + tv.backgroundColor = UIColor(Color.terminalBg) + tv.isScrollEnabled = false // outer ScrollView drives scrolling + tv.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) + tv.autocorrectionType = .no + tv.autocapitalizationType = .none + tv.spellCheckingType = .no + tv.smartDashesType = .no + tv.smartQuotesType = .no + tv.keyboardType = .asciiCapable + tv.tintColor = UIColor(Color.appAccent) + tv.attributedText = highlight(text) + context.coordinator.lastText = text + return tv + } + + func updateUIView(_ tv: UITextView, context: Context) { + // Only re-highlight when text changed from outside (e.g. initial load / save) + guard context.coordinator.lastText != text else { return } + context.coordinator.lastText = text + let sel = tv.selectedRange + tv.attributedText = highlight(text) + // Clamp selection to new length + let len = (tv.text as NSString).length + tv.selectedRange = NSRange(location: min(sel.location, len), length: 0) + } + + func makeCoordinator() -> Coordinator { Coordinator(self) } + + // MARK: - Coordinator + + final class Coordinator: NSObject, UITextViewDelegate { + var parent: SyntaxHighlightingEditor + var lastText: String = "" + + init(_ parent: SyntaxHighlightingEditor) { self.parent = parent } + + func textViewDidChange(_ tv: UITextView) { + let newText = tv.text ?? "" + lastText = newText + parent.text = newText + + // Re-apply highlighting, preserving cursor position + let sel = tv.selectedRange + tv.attributedText = parent.highlight(newText) + let len = (tv.text as NSString).length + tv.selectedRange = NSRange( + location: min(sel.location, len), + length: min(sel.length, max(0, len - sel.location)) + ) + } + } +} diff --git a/dock-g/dock-g/Utilities/YAMLHighlighter.swift b/dock-g/dock-g/Utilities/YAMLHighlighter.swift new file mode 100644 index 0000000..f33e554 --- /dev/null +++ b/dock-g/dock-g/Utilities/YAMLHighlighter.swift @@ -0,0 +1,179 @@ +import UIKit +import SwiftUI + +/// Tokenises YAML text into a syntax-highlighted NSAttributedString. +/// Designed for docker-compose files displayed on a dark terminal background. +enum YAMLHighlighter { + + // MARK: - Palette (dark-terminal optimised, UIColor) + + private static let keyColor = UIColor(hex: "#3b82f6") // blue — keys + private static let stringColor = UIColor(hex: "#22c55e") // green — quoted strings / block scalars + private static let numberColor = UIColor(hex: "#f59e0b") // amber — numbers + private static let boolColor = UIColor(hex: "#c084fc") // violet — true/false/yes/no + private static let nullColor = UIColor(hex: "#6b7280") // gray — null / ~ + private static let commentColor = UIColor(hex: "#6b7280") // gray — # comments + private static let anchorColor = UIColor(hex: "#fb923c") // orange — &anchors / *aliases + private static let plainColor = UIColor(hex: "#e2e8f0") // slate — plain values + + private static let monoFont = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + + // MARK: - Public API + + static func nsAttributedString(from yaml: String) -> NSAttributedString { + let result = NSMutableAttributedString() + let lines = yaml.components(separatedBy: "\n") + for (i, line) in lines.enumerated() { + result.append(processLine(line)) + if i < lines.count - 1 { + result.append(seg("\n", plainColor)) + } + } + return result + } + + /// SwiftUI AttributedString wrapper — use for Text() display. + static func attributedString(from yaml: String) -> AttributedString { + (try? AttributedString(nsAttributedString(from: yaml), including: \.uiKit)) ?? AttributedString(yaml) + } + + // MARK: - Line processing + + private static func processLine(_ line: String) -> NSAttributedString { + guard !line.isEmpty else { return seg("", plainColor) } + + let indentEnd = line.firstIndex(where: { $0 != " " && $0 != "\t" }) ?? line.endIndex + let indent = String(line[.. 2 { + result.append(colorAsKeyValueOrValue(String(content.dropFirst(2)))) + } + return result + } + + result.append(colorAsKeyValueOrValue(content)) + return result + } + + /// Tries "key: value"; falls back to plain value colouring. + private static func colorAsKeyValueOrValue(_ text: String) -> NSAttributedString { + guard let colonIdx = keySeparatorIndex(in: text) else { + return colorValue(text) + } + let key = String(text[.. String.Index? { + var idx = text.startIndex + while idx < text.endIndex { + let ch = text[idx] + if ch == " " || ch == "\t" || ch == "\"" || ch == "'" { return nil } + if ch == ":" { + guard idx != text.startIndex else { return nil } + let next = text.index(after: idx) + if next == text.endIndex || text[next] == " " || text[next] == "\t" { + return idx + } + return nil // colon inside a value (e.g. nginx:latest) + } + idx = text.index(after: idx) + } + return nil + } + + // MARK: - Value colouring + + private static func colorValue(_ raw: String) -> NSAttributedString { + if let commentStart = inlineCommentIndex(in: raw) { + let valuePart = String(raw[.. String.Index? { + var inSingle = false, inDouble = false, prev: Character = " " + for idx in text.indices { + let ch = text[idx] + if ch == "\"" && !inSingle { inDouble.toggle() } + else if ch == "'" && !inDouble { inSingle.toggle() } + else if ch == "#" && !inSingle && !inDouble && prev == " " { + return text.index(before: idx) // include the preceding space + } + prev = ch + } + return nil + } + + private static func valueColor(_ v: String) -> UIColor { + guard !v.isEmpty else { return plainColor } + + if v == "|" || v == ">" || v.hasPrefix("|-") || v.hasPrefix("|+") + || v.hasPrefix(">-") || v.hasPrefix(">+") { return stringColor } + + if v.count >= 2, + (v.hasPrefix("\"") && v.hasSuffix("\"")) || + (v.hasPrefix("'") && v.hasSuffix("'")) { return stringColor } + + switch v.lowercased() { + case "true", "false", "yes", "no", "on", "off": return boolColor + default: break + } + + if v.lowercased() == "null" || v == "~" { return nullColor } + + if Int(v) != nil || Double(v) != nil { return numberColor } + if v.lowercased().hasPrefix("0x") && Int(v.dropFirst(2), radix: 16) != nil { return numberColor } + + if v.hasPrefix("&") || v.hasPrefix("*") { return anchorColor } + + return plainColor + } + + // MARK: - Helpers + + private static func seg(_ text: String, _ color: UIColor) -> NSAttributedString { + NSAttributedString(string: text, attributes: [ + .font: monoFont, + .foregroundColor: color + ]) + } +} diff --git a/dock-g/dock-g/Views/Stacks/ComposeTabView.swift b/dock-g/dock-g/Views/Stacks/ComposeTabView.swift index 9ec73f8..1714245 100644 --- a/dock-g/dock-g/Views/Stacks/ComposeTabView.swift +++ b/dock-g/dock-g/Views/Stacks/ComposeTabView.swift @@ -5,24 +5,32 @@ struct ComposeTabView: View { var isEditing: Bool var onSave: () async -> Void + @State private var highlighted = AttributedString() + var body: some View { ScrollView { if isEditing { - TextEditor(text: $yaml) - .font(.monoBody) - .foregroundStyle(Color.terminalText) - .scrollContentBackground(.hidden) - .frame(minHeight: 400) - .padding(8) + SyntaxHighlightingEditor( + text: $yaml, + highlight: YAMLHighlighter.nsAttributedString(from:) + ) + .frame(minHeight: 400) } else { - Text(yaml.isEmpty ? " " : yaml) - .font(.monoBody) - .foregroundStyle(Color.terminalText) + Text(highlighted) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(8) } } .background(Color.terminalBg) + .onAppear { reHighlight() } + .onChange(of: yaml) { _, _ in if !isEditing { reHighlight() } } + .onChange(of: isEditing) { _, editing in if !editing { reHighlight() } } + } + + private func reHighlight() { + highlighted = yaml.isEmpty + ? { var s = AttributedString(" "); s.font = .monoBody; return s }() + : YAMLHighlighter.attributedString(from: yaml) } } diff --git a/original-source/dockge b/original-source/dockge deleted file mode 160000 index cc18056..0000000 --- a/original-source/dockge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cc180562fcd534de7c0890633494cde2c9658d97