native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Draw placeholders inside PlainTextEditor

onevcat 68afe90c 40587359

+104 -84
+6 -14
supacode/App/ContentView.swift
··· 164 164 .foregroundStyle(.secondary) 165 165 } 166 166 167 - ZStack(alignment: .topLeading) { 168 - PlainTextEditor( 169 - text: $script, 170 - isMonospaced: true 171 - ) 172 - .frame(minHeight: 160) 173 - if script.isEmpty { 174 - Text("npm run dev") 175 - .foregroundStyle(.secondary) 176 - .padding(.leading, 6) 177 - .font(.body.monospaced()) 178 - .allowsHitTesting(false) 179 - } 180 - } 167 + PlainTextEditor( 168 + text: $script, 169 + isMonospaced: true, 170 + placeholder: "npm run dev" 171 + ) 172 + .frame(minHeight: 160) 181 173 182 174 HStack { 183 175 Spacer()
+22 -67
supacode/Features/Settings/Views/RepositorySettingsView.swift
··· 167 167 168 168 if store.showsSetupScriptSettings { 169 169 Section { 170 - ZStack(alignment: .topLeading) { 171 - if store.settings.setupScript.isEmpty { 172 - Text("claude --dangerously-skip-permissions") 173 - .foregroundStyle(.secondary) 174 - .padding(.leading, plainTextPlaceholderLeading) 175 - .padding(.top, plainTextPlaceholderTop) 176 - .font(.body) 177 - .allowsHitTesting(false) 178 - } 179 - PlainTextEditor( 180 - text: settings.setupScript 181 - ) 182 - .frame(minHeight: 120) 183 - } 170 + PlainTextEditor( 171 + text: settings.setupScript, 172 + placeholder: "claude --dangerously-skip-permissions" 173 + ) 174 + .frame(minHeight: 120) 184 175 } header: { 185 176 VStack(alignment: .leading, spacing: 4) { 186 177 Text("Setup Script") ··· 192 183 193 184 if store.showsArchiveScriptSettings { 194 185 Section { 195 - ZStack(alignment: .topLeading) { 196 - if store.settings.archiveScript.isEmpty { 197 - Text("docker compose down") 198 - .foregroundStyle(.secondary) 199 - .padding(.leading, plainTextPlaceholderLeading) 200 - .padding(.top, plainTextPlaceholderTop) 201 - .font(.body) 202 - .allowsHitTesting(false) 203 - } 204 - PlainTextEditor( 205 - text: settings.archiveScript 206 - ) 207 - .frame(minHeight: 120) 208 - } 186 + PlainTextEditor( 187 + text: settings.archiveScript, 188 + placeholder: "docker compose down" 189 + ) 190 + .frame(minHeight: 120) 209 191 } header: { 210 192 VStack(alignment: .leading, spacing: 4) { 211 193 Text("Archive Script") ··· 217 199 218 200 if store.showsRunScriptSettings { 219 201 Section { 220 - ZStack(alignment: .topLeading) { 221 - if store.settings.runScript.isEmpty { 222 - Text("npm run dev") 223 - .foregroundStyle(.secondary) 224 - .padding(.leading, plainTextPlaceholderLeading) 225 - .padding(.top, plainTextPlaceholderTop) 226 - .font(.body) 227 - .allowsHitTesting(false) 228 - } 229 - PlainTextEditor( 230 - text: settings.runScript 231 - ) 232 - .frame(minHeight: 120) 233 - } 202 + PlainTextEditor( 203 + text: settings.runScript, 204 + placeholder: "npm run dev" 205 + ) 206 + .frame(minHeight: 120) 234 207 } header: { 235 208 VStack(alignment: .leading, spacing: 4) { 236 209 Text("Run Script") ··· 710 683 } 711 684 .pickerStyle(.segmented) 712 685 713 - ZStack(alignment: .topLeading) { 714 - if command.wrappedValue.command.isEmpty { 715 - Text(scriptPlaceholder(for: command.wrappedValue.execution)) 716 - .foregroundStyle(.secondary) 717 - .padding(.leading, plainTextPlaceholderLeading) 718 - .padding(.top, plainTextPlaceholderTop) 719 - .font(.body.monospaced()) 720 - .allowsHitTesting(false) 721 - } 722 - 723 - PlainTextEditor( 724 - text: command.command, 725 - isMonospaced: true, 726 - shouldFocus: true 727 - ) 728 - .frame(height: 140) 729 - } 686 + PlainTextEditor( 687 + text: command.command, 688 + isMonospaced: true, 689 + shouldFocus: true, 690 + placeholder: scriptPlaceholder(for: command.wrappedValue.execution) 691 + ) 692 + .frame(height: 140) 730 693 731 694 Text(scriptDescription(for: command.wrappedValue.execution)) 732 695 .font(.caption) ··· 1008 971 selectedCustomCommandID = normalizedCommands[normalizedCommands.count - 1].id 1009 972 } 1010 973 clearRemovedCommandState(using: normalizedCommands) 1011 - } 1012 - 1013 - private var plainTextPlaceholderLeading: CGFloat { 1014 - 4 1015 - } 1016 - 1017 - private var plainTextPlaceholderTop: CGFloat { 1018 - -2 1019 974 } 1020 975 1021 976 private func clearShortcut(for commandID: UserCustomCommand.ID) {
+76 -3
supacode/Support/PlainTextEditor.swift
··· 5 5 @Binding var text: String 6 6 var isMonospaced: Bool = false 7 7 var shouldFocus: Bool = false 8 + var placeholder: String? = nil 9 + var hidesPlaceholderWhenFocused: Bool = true 8 10 9 11 func makeCoordinator() -> Coordinator { 10 12 Coordinator(text: $text) 11 13 } 12 14 13 15 func makeNSView(context: Context) -> NSScrollView { 14 - let textView = NSTextView(frame: .zero) 16 + let textView = PlaceholderTextView(frame: .zero) 15 17 textView.delegate = context.coordinator 16 18 textView.drawsBackground = false 17 19 textView.isRichText = false ··· 29 31 textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude) 30 32 textView.textContainer?.widthTracksTextView = true 31 33 textView.string = text 34 + textView.placeholder = placeholder 35 + textView.hidesPlaceholderWhenFocused = hidesPlaceholderWhenFocused 32 36 33 37 let scrollView = NSScrollView(frame: .zero) 34 38 scrollView.drawsBackground = false ··· 41 45 } 42 46 43 47 func updateNSView(_ nsView: NSScrollView, context: Context) { 44 - guard let textView = nsView.documentView as? NSTextView else { return } 48 + guard let textView = nsView.documentView as? PlaceholderTextView else { return } 45 49 if textView.string != text { 46 50 textView.string = text 51 + textView.needsDisplay = true 47 52 } 48 53 let updatedFont = editorFont 49 54 if textView.font != updatedFont { 50 55 textView.font = updatedFont 56 + textView.needsDisplay = true 51 57 } 58 + textView.placeholder = placeholder 59 + textView.hidesPlaceholderWhenFocused = hidesPlaceholderWhenFocused 52 60 if shouldFocus, 53 61 textView.window?.firstResponder !== textView 54 62 { ··· 71 79 } 72 80 73 81 func textDidChange(_ notification: Notification) { 74 - guard let textView = notification.object as? NSTextView else { return } 82 + guard let textView = notification.object as? PlaceholderTextView else { return } 75 83 text = textView.string 84 + textView.needsDisplay = true 85 + } 86 + } 87 + 88 + final class PlaceholderTextView: NSTextView { 89 + var placeholder: String? { 90 + didSet { 91 + needsDisplay = true 92 + } 93 + } 94 + var hidesPlaceholderWhenFocused: Bool = true { 95 + didSet { 96 + needsDisplay = true 97 + } 98 + } 99 + 100 + override func becomeFirstResponder() -> Bool { 101 + let didBecomeFirstResponder = super.becomeFirstResponder() 102 + if didBecomeFirstResponder { 103 + needsDisplay = true 104 + } 105 + return didBecomeFirstResponder 106 + } 107 + 108 + override func resignFirstResponder() -> Bool { 109 + let didResignFirstResponder = super.resignFirstResponder() 110 + if didResignFirstResponder { 111 + needsDisplay = true 112 + } 113 + return didResignFirstResponder 114 + } 115 + 116 + override func draw(_ dirtyRect: NSRect) { 117 + super.draw(dirtyRect) 118 + drawPlaceholderIfNeeded() 119 + } 120 + 121 + private func drawPlaceholderIfNeeded() { 122 + guard let placeholder, 123 + !placeholder.isEmpty, 124 + string.isEmpty 125 + else { 126 + return 127 + } 128 + 129 + if hidesPlaceholderWhenFocused, 130 + window?.firstResponder === self 131 + { 132 + return 133 + } 134 + 135 + let lineFragmentPadding = textContainer?.lineFragmentPadding ?? 0 136 + let horizontalInset = textContainerInset.width + lineFragmentPadding 137 + let verticalInset = textContainerInset.height - 2 138 + let placeholderRect = NSRect( 139 + x: bounds.minX + horizontalInset, 140 + y: bounds.minY + verticalInset, 141 + width: max(0, bounds.width - horizontalInset - textContainerInset.width), 142 + height: max(0, bounds.height - verticalInset) 143 + ) 144 + let placeholderAttributes: [NSAttributedString.Key: Any] = [ 145 + .foregroundColor: NSColor.placeholderTextColor, 146 + .font: font ?? NSFont.preferredFont(forTextStyle: .body), 147 + ] 148 + (placeholder as NSString).draw(in: placeholderRect, withAttributes: placeholderAttributes) 76 149 } 77 150 } 78 151 }