WIP: add code snippet cards

This commit is contained in:
Akshay Kolli
2026-07-01 15:12:49 -07:00
parent c5bafa3a83
commit ca3bdfbc70
16 changed files with 417 additions and 31 deletions

View File

@@ -10,6 +10,7 @@ enum ClipboardItemKind: Int {
case pdf
case audio
case color
case code
var displayName: String {
switch self {
@@ -22,6 +23,7 @@ enum ClipboardItemKind: Int {
case .pdf: return "PDF"
case .audio: return "audio"
case .color: return "color"
case .code: return "code"
}
}
}
@@ -31,7 +33,7 @@ extension ClipboardItemKind {
switch self {
case .url, .file, .image, .pdf, .audio:
return true
case .text, .richText, .unknown, .color:
case .text, .richText, .unknown, .color, .code:
return false
}
}
@@ -40,7 +42,7 @@ extension ClipboardItemKind {
switch self {
case .file, .image, .pdf, .audio:
return true
case .text, .richText, .unknown, .url, .color:
case .text, .richText, .unknown, .url, .color, .code:
return false
}
}
@@ -49,7 +51,7 @@ extension ClipboardItemKind {
switch self {
case .url, .image, .pdf, .audio, .richText:
return true
case .text, .file, .unknown, .color:
case .text, .file, .unknown, .color, .code:
return false
}
}
@@ -65,8 +67,9 @@ enum ClipboardSortMode: Int {
case files
case audio
case colors
case code
static let allCases: [ClipboardSortMode] = [.mostRecent, .mostUsed, .text, .links, .images, .colors, .audio, .files, .pinned]
static let allCases: [ClipboardSortMode] = [.mostRecent, .mostUsed, .text, .links, .images, .colors, .audio, .files, .pinned, .code]
var title: String {
switch self {
@@ -79,6 +82,7 @@ enum ClipboardSortMode: Int {
case .files: return "Files"
case .audio: return "Audio"
case .colors: return "Colors"
case .code: return "Code"
}
}
}
@@ -151,6 +155,7 @@ struct ClipboardItem {
case .pdf: return "pdf document"
case .audio: return "audio sound"
case .color: return "color swatch hex"
case .code: return "code snippet source programming"
}
}

View File

@@ -0,0 +1,106 @@
import Foundation
enum CodeSnippetPayload {
static func isLikelyCode(_ value: String) -> Bool {
let text = value.clipboardTrimmed
guard text.count >= 12 else { return false }
if text.hasPrefix("```") { return true }
if languageLabel(from: text) != "Code" { return true }
let lines = text.components(separatedBy: .newlines)
let nonEmptyLines = lines.map(\.clipboardTrimmed).filter { !$0.isEmpty }
guard !nonEmptyLines.isEmpty else { return false }
var score = 0
if nonEmptyLines.count >= 2 && lines.contains(where: { $0.hasPrefix(" ") || $0.hasPrefix("\t") }) {
score += 2
}
if text.contains("{") && text.contains("}") { score += 2 }
if text.contains(";") { score += 1 }
if text.contains("=>") || text.contains("->") || text.contains("==") || text.contains("!=") || text.contains("&&") || text.contains("||") {
score += 1
}
if nonEmptyLines.filter({ $0.hasSuffix("{") || $0.hasSuffix("}") || $0.hasSuffix(";") }).count >= 2 {
score += 2
}
if containsCodeKeyword(text) { score += 2 }
if containsAssignment(text) { score += 1 }
return score >= 4
}
static func languageLabel(from value: String) -> String {
let text = value.clipboardTrimmed
let lower = text.lowercased()
if isJSON(text) { return "JSON" }
if lower.contains("<html") || lower.contains("</") && lower.contains(">") {
return "HTML"
}
if lower.contains("#include") { return "C/C++" }
if lower.contains("func ") || lower.contains("let ") || lower.contains("var ") && lower.contains("->") {
return "Swift"
}
if lower.contains("function ") || lower.contains("const ") || lower.contains("let ") && lower.contains("=>") {
return "JavaScript"
}
if lower.contains("def ") || lower.contains("import ") && lower.contains(":") {
return "Python"
}
if lower.range(of: #"\b(select|insert|update|delete|create)\b[\s\S]+\b(from|into|table|set)\b"#, options: .regularExpression) != nil {
return "SQL"
}
if lower.range(of: #"^\s*(git|npm|yarn|pnpm|curl|ssh|docker|kubectl|brew|swift|python|node)\b"#, options: .regularExpression) != nil {
return "Shell"
}
if lower.contains("{") && lower.contains(":") && lower.contains(";") {
return "CSS"
}
return "Code"
}
static func title(from value: String) -> String {
let language = languageLabel(from: value)
guard language != "Code" else { return "Code Snippet" }
return "\(language) Snippet"
}
static func previewText(from value: String, maxLines: Int = 4) -> String {
let lines = value
.components(separatedBy: .newlines)
.map { $0.clipboardTrimmed }
.filter { !$0.isEmpty && $0 != "```" }
let preview = lines.prefix(maxLines).joined(separator: " ")
return preview.isEmpty ? "Code snippet" : String(preview.prefix(180))
}
static func previewLines(from value: String, maxLines: Int = 5) -> [String] {
let lines = value
.components(separatedBy: .newlines)
.map { line in
line.replacingOccurrences(of: "\t", with: " ")
}
.filter { !$0.clipboardTrimmed.isEmpty && $0.clipboardTrimmed != "```" }
return Array(lines.prefix(maxLines))
}
private static func containsCodeKeyword(_ value: String) -> Bool {
value.range(
of: #"\b(import|func|function|class|struct|enum|interface|return|guard|if|else|for|while|switch|case|try|catch|throw|async|await|public|private|static|const|let|var|def)\b"#,
options: [.regularExpression, .caseInsensitive]
) != nil
}
private static func containsAssignment(_ value: String) -> Bool {
value.range(
of: #"\b[A-Za-z_][A-Za-z0-9_]*\s*(=|:=)\s*[^=\n]"#,
options: .regularExpression
) != nil
}
private static func isJSON(_ value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let trimmed = value.clipboardTrimmed
guard trimmed.hasPrefix("{") || trimmed.hasPrefix("[") else { return false }
return (try? JSONSerialization.jsonObject(with: data)) != nil
}
}

View File

@@ -105,7 +105,7 @@ final class ClipboardCacheService {
case .file:
return filePreviewThumbnail(for: item.payload)
case .text, .unknown, .audio, .richText, .color:
case .text, .unknown, .audio, .richText, .color, .code:
return nil
}
}
@@ -187,7 +187,7 @@ final class ClipboardCacheService {
case .file:
let urls = FilePayload.urls(from: item.payload)
return urls.first { fileManager.fileExists(atPath: $0.path) }
case .text, .unknown:
case .text, .code, .unknown:
let text = item.payload.clipboardTrimmed.isEmpty ? item.displayText : item.payload
guard !text.clipboardTrimmed.isEmpty else { return nil }
return writeTemporaryCopy(data: Data(text.utf8), id: item.id, fileExtension: "txt")

View File

@@ -254,17 +254,23 @@ final class ClipboardMonitorService {
return htmlPayload
}
if isIgnored(.text), let string = pasteboard.string(forType: .string) {
if let string = pasteboard.string(forType: .string) {
let trimmed = string.clipboardTrimmed
if !trimmed.isEmpty {
reportReadFailureStatus(ignoredKindMessage(.text))
return nil
if CodeSnippetPayload.isLikelyCode(trimmed), isIgnored(.code) {
reportReadFailureStatus(ignoredKindMessage(.code))
return nil
}
if !CodeSnippetPayload.isLikelyCode(trimmed), isIgnored(.text) {
reportReadFailureStatus(ignoredKindMessage(.text))
return nil
}
}
}
if let string = pasteboard.string(forType: .string),
let item = itemFromString(string, sourceApp: source.name, sourceBundleId: source.bundleId) {
if item.kind == .text, item.payload.isEmpty {
if (item.kind == .text || item.kind == .code), item.payload.isEmpty {
reportReadFailureStatus("Clipboard contains no readable text.")
return nil
}
@@ -305,6 +311,24 @@ final class ClipboardMonitorService {
)
}
if CodeSnippetPayload.isLikelyCode(trimmed) {
return ClipboardItem(
id: UUID(),
kind: .code,
displayText: CodeSnippetPayload.title(from: trimmed),
payload: trimmed,
payloadHash: store.hashString(trimmed),
createdAt: Date(),
lastUsedAt: Date(),
useCount: 1,
sourceApp: sourceApp,
imagePath: nil,
thumbnailPath: nil,
isPinned: false,
sourceAppBundleId: sourceBundleId
)
}
return ClipboardItem(
id: UUID(),
kind: .text,

View File

@@ -111,7 +111,7 @@ final class ClipboardStore {
func updateText(_ id: UUID, text: String) -> Bool {
guard !text.isEmpty,
let index = items.firstIndex(where: { $0.id == id }),
items[index].kind == .text else {
items[index].kind == .text || items[index].kind == .code else {
return false
}

View File

@@ -165,7 +165,7 @@ final class PasteActionService {
}
return [pasteboardItem]
case .text, .unknown:
case .text, .code, .unknown:
guard !item.payload.isEmpty else { return [] }
return [stringPasteboardItem(item.payload)]
}
@@ -222,7 +222,7 @@ final class PasteActionService {
guard !item.payload.isEmpty else { return false }
board.clearContents()
didWrite = writeURL(item.payload, title: item.displayText, to: board)
case .text, .unknown:
case .text, .code, .unknown:
guard !item.payload.isEmpty else { return false }
board.clearContents()
didWrite = board.setString(item.payload, forType: .string)
@@ -236,7 +236,7 @@ final class PasteActionService {
func plainText(for item: ClipboardItem) -> String? {
switch item.kind {
case .text, .unknown:
case .text, .code, .unknown:
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)
case .url, .file:
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)

View File

@@ -83,7 +83,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
22: .files,
26: .pinned,
28: .audio,
25: .colors
25: .colors,
29: .code
]
private let viewModel: ClipboardPanelViewModel

View File

@@ -63,6 +63,7 @@ private enum ClipboardCollectionVisuals {
case .audio: return NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1)
case .files: return NSColor(calibratedRed: 0.11, green: 0.68, blue: 0.36, alpha: 1)
case .pinned: return NSColor(calibratedRed: 0.94, green: 0.12, blue: 0.48, alpha: 1)
case .code: return NSColor(calibratedRed: 0.25, green: 0.38, blue: 0.78, alpha: 1)
}
}
@@ -630,6 +631,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
case .audio: return "Audio"
case .files: return "Files"
case .pinned: return "Pinned"
case .code: return "Code"
}
}
@@ -644,6 +646,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
case .audio: return "music.note"
case .files: return "doc.fill"
case .pinned: return "pin.fill"
case .code: return "chevron.left.forwardslash.chevron.right"
}
}
@@ -1139,6 +1142,8 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
return ("No links yet", "Links are detected from copied URLs.")
case .text:
return ("No text clips yet", "Copied text and rich text appear here.")
case .code:
return ("No code snippets yet", "Copied code snippets appear here.")
case .files:
return ("No files yet", "Copied files and PDFs appear here.")
case .audio:
@@ -3015,27 +3020,27 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
switch itemKind {
case .url, .file, .image, .pdf, .audio:
return true
case .text, .richText, .unknown, .color:
case .text, .richText, .unknown, .color, .code:
return false
}
}
private var canPreview: Bool {
switch itemKind {
case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown, .color:
case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown, .color, .code:
return true
}
}
private var canEditText: Bool {
itemKind == .text
itemKind == .text || itemKind == .code
}
private var canPlainText: Bool {
switch itemKind {
case .url, .image, .richText, .file, .pdf, .audio, .color:
return true
case .text, .unknown:
case .text, .unknown, .code:
return false
}
}
@@ -3044,7 +3049,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
switch itemKind {
case .file, .image, .pdf, .audio:
return true
case .text, .richText, .url, .unknown, .color:
case .text, .richText, .url, .unknown, .color, .code:
return false
}
}
@@ -3621,6 +3626,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return audioPreviewView(for: item)
case .color:
return colorPreviewView(for: item)
case .code:
return codePreviewView(for: item)
case .text, .richText, .image, .unknown:
return textPreviewView(for: item)
}
@@ -4111,6 +4118,82 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return container
}
private func codePreviewView(for item: ClipboardItem) -> NSView {
let container = NSView()
container.wantsLayer = true
container.layer?.backgroundColor = accentColor(for: item.kind).withAlphaComponent(0.10).cgColor
container.translatesAutoresizingMaskIntoConstraints = false
let editor = NSView()
editor.wantsLayer = true
editor.layer?.cornerRadius = 10
editor.layer?.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.88).cgColor
editor.layer?.borderWidth = 0.8
editor.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.22).cgColor
editor.translatesAutoresizingMaskIntoConstraints = false
let language = capsuleLabel(CodeSnippetPayload.languageLabel(from: item.payload), color: accentColor(for: item.kind))
language.translatesAutoresizingMaskIntoConstraints = false
let lines = CodeSnippetPayload.previewLines(from: item.payload)
let lineRows = lines.isEmpty ? ["Code snippet"] : lines
let rowViews = lineRows.enumerated().map { offset, line in
codeLineRow(number: offset + 1, text: line)
}
let rows = NSStackView(views: rowViews)
rows.orientation = .vertical
rows.alignment = .leading
rows.spacing = 3
rows.translatesAutoresizingMaskIntoConstraints = false
editor.addSubview(rows)
container.addSubview(editor)
container.addSubview(language)
for row in rowViews {
row.widthAnchor.constraint(equalTo: rows.widthAnchor).isActive = true
}
NSLayoutConstraint.activate([
editor.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: layout.inset),
editor.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -layout.inset),
editor.topAnchor.constraint(equalTo: container.topAnchor, constant: layout.isCompact ? 13 : 16),
editor.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: layout.isCompact ? -13 : -16),
rows.leadingAnchor.constraint(equalTo: editor.leadingAnchor, constant: 12),
rows.trailingAnchor.constraint(equalTo: editor.trailingAnchor, constant: -12),
rows.topAnchor.constraint(equalTo: editor.topAnchor, constant: 13),
rows.bottomAnchor.constraint(lessThanOrEqualTo: editor.bottomAnchor, constant: -12),
language.trailingAnchor.constraint(equalTo: editor.trailingAnchor, constant: -10),
language.bottomAnchor.constraint(equalTo: editor.bottomAnchor, constant: -9)
])
return container
}
private func codeLineRow(number: Int, text: String) -> NSView {
let numberLabel = NSTextField(labelWithString: "\(number)")
numberLabel.font = .monospacedDigitSystemFont(ofSize: 11, weight: .regular)
numberLabel.textColor = .tertiaryLabelColor
numberLabel.alignment = .right
numberLabel.maximumNumberOfLines = 1
numberLabel.translatesAutoresizingMaskIntoConstraints = false
numberLabel.widthAnchor.constraint(equalToConstant: 20).isActive = true
let codeLabel = NSTextField(labelWithString: text)
codeLabel.font = .monospacedSystemFont(ofSize: layout.isCompact ? 11 : 12, weight: .regular)
codeLabel.textColor = .labelColor
codeLabel.maximumNumberOfLines = 1
codeLabel.lineBreakMode = .byTruncatingTail
codeLabel.toolTip = text
codeLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let row = NSStackView(views: [numberLabel, codeLabel])
row.orientation = .horizontal
row.alignment = .firstBaseline
row.spacing = 8
row.translatesAutoresizingMaskIntoConstraints = false
codeLabel.widthAnchor.constraint(equalTo: row.widthAnchor, constant: -28).isActive = true
return row
}
private func mediaPreviewView(for item: ClipboardItem, thumbnail: NSImage) -> NSView {
let container = NSView()
container.translatesAutoresizingMaskIntoConstraints = false
@@ -4196,6 +4279,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return "audio-preview"
case .color:
return "color-preview"
case .code:
return "code-preview"
case .richText:
return "rich-text-preview"
case .text:
@@ -4375,6 +4460,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return imageTitle(for: item)
case .color:
return ColorPayload.displayHex(from: item.payload)
case .code:
return CodeSnippetPayload.title(from: item.payload)
default:
break
}
@@ -4405,6 +4492,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return "Sound clip"
case .color:
return ColorPayload.componentSummary(from: item.payload)
case .code:
return CodeSnippetPayload.previewText(from: item.payload)
case .richText:
let text = normalized(item.displayText)
return text.isEmpty ? "No preview available" : text
@@ -4438,6 +4527,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return "Audio"
case .color:
return "Color"
case .code:
return CodeSnippetPayload.languageLabel(from: item.payload)
case .image:
if item.ocrText?.clipboardTrimmed.isEmpty == false {
return "OCR text"
@@ -4754,6 +4845,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return NSColor(calibratedRed: 0.93, green: 0.12, blue: 0.34, alpha: 1)
case .color:
return NSColor(calibratedRed: 0.00, green: 0.65, blue: 0.74, alpha: 1)
case .code:
return NSColor(calibratedRed: 0.25, green: 0.38, blue: 0.78, alpha: 1)
case .unknown:
return .systemGray
}
@@ -4769,6 +4862,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
case .pdf: return "doc.text.fill"
case .audio: return "music.note"
case .color: return "paintpalette"
case .code: return "chevron.left.forwardslash.chevron.right"
case .unknown: return "questionmark"
}
}
@@ -4841,6 +4935,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
case .pdf: return "PDF"
case .audio: return "Audio"
case .color: return "Color"
case .code: return "Code"
}
}

View File

@@ -358,14 +358,14 @@ final class ClipboardPanelViewModel {
}
func editableTextForSelected() -> String? {
guard let item = selectedItem, item.kind == .text else { return nil }
guard let item = selectedItem, item.kind == .text || item.kind == .code else { return nil }
return item.payload
}
func editableTextForItem(at index: Int) -> String? {
guard index >= 0 && index < visibleItems.count else { return nil }
let item = visibleItems[index]
guard item.kind == .text else { return nil }
guard item.kind == .text || item.kind == .code else { return nil }
return item.payload
}
@@ -388,7 +388,7 @@ final class ClipboardPanelViewModel {
}
func updateSelectedText(to text: String) {
guard let item = selectedItem, item.kind == .text else { return }
guard let item = selectedItem, item.kind == .text || item.kind == .code else { return }
let trimmed = text.clipboardTrimmed
guard !trimmed.isEmpty else {
statusMessage = "Text clip cannot be empty"
@@ -401,7 +401,7 @@ final class ClipboardPanelViewModel {
selectedItemID = item.id
if store.updateText(item.id, text: text) {
statusMessage = "Updated text clip"
statusMessage = item.kind == .code ? "Updated code clip" : "Updated text clip"
}
}
@@ -755,7 +755,7 @@ final class ClipboardPanelViewModel {
case .text:
return collectionFiltered
.filter { $0.1.kind == .text || $0.1.kind == .richText }
.filter { $0.1.kind == .text || $0.1.kind == .richText || $0.1.kind == .code }
.sorted(by: sortByUsage)
.map(\.1)
@@ -777,6 +777,12 @@ final class ClipboardPanelViewModel {
.sorted(by: sortByUsage)
.map(\.1)
case .code:
return collectionFiltered
.filter { $0.1.kind == .code }
.sorted(by: sortByUsage)
.map(\.1)
case .pinned:
return collectionFiltered
.filter { $0.1.isPinned }
@@ -977,6 +983,8 @@ final class ClipboardPanelViewModel {
return [.richText]
case "note", "notes", "writing":
return [.text, .richText]
case "code", "snippet", "snippets", "source", "programming", "script", "scripts", "json", "css", "sql":
return [.code]
case "link", "links", "url", "urls", "web":
return [.url]
case "image", "images", "photo", "photos", "picture", "pictures":

View File

@@ -167,6 +167,7 @@ final class SettingsWindowController: NSObject, NSTextFieldDelegate, NSTextViewD
let allowedRows = [
kindCheckbox("Text", .text),
kindCheckbox("Code", .code),
kindCheckbox("Links", .url),
kindCheckbox("Images", .image),
kindCheckbox("Colors", .color),