A no-Xcode AppKit app (swiftc + build.sh): - Plaintext editor: 5 themes, line numbers, soft wrap, font controls, find, live Markdown preview (split mode), selection-aware Markdown toolbar. - Freeform Board mode: zoomable/pannable canvas of page cards (portrait/landscape, US Letter/A4/Legal/Square/Wide/Note), clean boxes, images (paste/drop/panel), freehand pen/shapes, screen-grab eyedropper, nested right-click menu, rich status bar. The Editor follows the selected page; layout persists to a foo.txt.board sidecar; the .txt stays pure plaintext. - scp-on-save to a Tailscale Mac (key auth, in-flight/quit drain). - HTML scraping pipeline: custom selector engine + Discogs profile, ingests to Postgres (psql) into a marketplace_snapshots schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 lines
1.9 KiB
Swift
40 lines
1.9 KiB
Swift
import AppKit
|
|
|
|
/// Selection-aware Markdown insertion used by both the toolbar and the Format
|
|
/// menu. Wraps the selection (or inserts a placeholder), keeping the change on
|
|
/// the text view's own undo stack.
|
|
enum TextMarkup {
|
|
static func wrap(_ textView: NSTextView, prefix: String, suffix: String, placeholder: String) {
|
|
let range = textView.selectedRange()
|
|
let ns = textView.string as NSString
|
|
let selected = ns.substring(with: range)
|
|
let body = selected.isEmpty ? placeholder : selected
|
|
let replacement = prefix + body + suffix
|
|
guard textView.shouldChangeText(in: range, replacementString: replacement) else { return }
|
|
textView.replaceCharacters(in: range, with: replacement)
|
|
textView.didChangeText()
|
|
// Re-select the body so the user can keep typing or toggle again.
|
|
let bodyStart = range.location + (prefix as NSString).length
|
|
textView.setSelectedRange(NSRange(location: bodyStart, length: (body as NSString).length))
|
|
}
|
|
|
|
/// Prefix every line touched by the selection (headings, lists, quotes).
|
|
static func linePrefix(_ textView: NSTextView, prefix: String) {
|
|
let ns = textView.string as NSString
|
|
let lineRange = ns.lineRange(for: textView.selectedRange())
|
|
let block = ns.substring(with: lineRange)
|
|
let prefixed = block
|
|
.components(separatedBy: "\n")
|
|
.enumerated()
|
|
.map { idx, line in
|
|
// Don't add a stray prefix to a trailing empty line from the split.
|
|
(line.isEmpty && idx > 0) ? line : prefix + line
|
|
}
|
|
.joined(separator: "\n")
|
|
guard textView.shouldChangeText(in: lineRange, replacementString: prefixed) else { return }
|
|
textView.replaceCharacters(in: lineRange, with: prefixed)
|
|
textView.didChangeText()
|
|
textView.setSelectedRange(NSRange(location: lineRange.location, length: (prefixed as NSString).length))
|
|
}
|
|
}
|