Improved text formatting during paste operation

This commit is contained in:
Beingpax 2025-11-28 20:39:43 +05:45
parent 1e612d9987
commit ac1a85c056
5 changed files with 179 additions and 17 deletions

View File

@ -2,17 +2,20 @@ import Foundation
import AppKit
class CursorPaster {
static func pasteAtCursor(_ text: String) {
let pasteboard = NSPasteboard.general
let preserveTranscript = UserDefaults.standard.bool(forKey: "preserveTranscriptInClipboard")
let context = TextInsertionFormatter.getInsertionContext()
let textToInsert = TextInsertionFormatter.formatTextForInsertion(text, context: context)
var savedContents: [(NSPasteboard.PasteboardType, Data)] = []
// Only save clipboard contents if we plan to restore them
if !preserveTranscript {
let currentItems = pasteboard.pasteboardItems ?? []
for item in currentItems {
for type in item.types {
if let data = item.data(forType: type) {
@ -21,8 +24,8 @@ class CursorPaster {
}
}
}
ClipboardManager.setClipboard(text, transient: !preserveTranscript)
ClipboardManager.setClipboard(textToInsert, transient: !preserveTranscript)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
if UserDefaults.standard.bool(forKey: "UseAppleScriptPaste") {

View File

@ -67,10 +67,9 @@ class LastTranscriptionService: ObservableObject {
}
let textToPaste = lastTranscription.text
// Delay to give the user time to release modifier keys (especially Control)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
CursorPaster.pasteAtCursor(textToPaste + " ")
CursorPaster.pasteAtCursor(textToPaste)
}
}
@ -94,9 +93,8 @@ class LastTranscriptionService: ObservableObject {
}
}()
// Delay to allow modifier keys to be released
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
CursorPaster.pasteAtCursor(textToPaste + " ")
CursorPaster.pasteAtCursor(textToPaste)
}
}

View File

@ -0,0 +1,166 @@
import Foundation
import AppKit
class TextInsertionFormatter {
struct InsertionContext {
let textBefore: String
let textAfter: String
let charBeforeCursor: Character?
let charAfterCursor: Character?
}
static func getInsertionContext() -> InsertionContext? {
guard AXIsProcessTrusted() else {
return nil
}
guard let focusedElement = getFocusedElement() else {
return nil
}
var selectedRange: CFTypeRef?
let rangeResult = AXUIElementCopyAttributeValue(focusedElement, kAXSelectedTextRangeAttribute as CFString, &selectedRange)
var textValue: CFTypeRef?
let textResult = AXUIElementCopyAttributeValue(focusedElement, kAXValueAttribute as CFString, &textValue)
guard rangeResult == .success,
textResult == .success,
let range = selectedRange,
let text = textValue as? String else {
return nil
}
var rangeValue = CFRange()
guard AXValueGetValue(range as! AXValue, .cfRange, &rangeValue) else {
return nil
}
let cursorPosition = rangeValue.location
let beforeStart = max(0, cursorPosition - 100)
let textBefore = String(text[text.index(text.startIndex, offsetBy: beforeStart)..<text.index(text.startIndex, offsetBy: cursorPosition)])
let afterEnd = min(text.count, cursorPosition + 100)
let textAfter = String(text[text.index(text.startIndex, offsetBy: cursorPosition)..<text.index(text.startIndex, offsetBy: afterEnd)])
let charBefore = cursorPosition > 0 ? text[text.index(text.startIndex, offsetBy: cursorPosition - 1)] : nil
let charAfter = cursorPosition < text.count ? text[text.index(text.startIndex, offsetBy: cursorPosition)] : nil
return InsertionContext(
textBefore: textBefore,
textAfter: textAfter,
charBeforeCursor: charBefore,
charAfterCursor: charAfter
)
}
private static func getFocusedElement() -> AXUIElement? {
let systemWideElement = AXUIElementCreateSystemWide()
var focusedApp: CFTypeRef?
guard AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedApplicationAttribute as CFString, &focusedApp) == .success else {
return nil
}
var focusedElement: CFTypeRef?
guard AXUIElementCopyAttributeValue(focusedApp as! AXUIElement, kAXFocusedUIElementAttribute as CFString, &focusedElement) == .success else {
return nil
}
return (focusedElement as! AXUIElement)
}
static func formatTextForInsertion(_ text: String, context: InsertionContext?) -> String {
guard let context = context else {
return text + " "
}
var formattedText = text
formattedText = applySmartCapitalization(formattedText, context: context)
formattedText = applySmartSpacing(formattedText, context: context)
return formattedText
}
private static func applySmartSpacing(_ text: String, context: InsertionContext) -> String {
var result = text
if shouldAddSpaceBefore(context: context) {
result = " " + result
}
if let charAfter = context.charAfterCursor {
if !charAfter.isWhitespace && !charAfter.isPunctuation && !(result.last?.isWhitespace ?? false) {
result = result + " "
}
} else {
result = result + " "
}
return result
}
private static func shouldAddSpaceBefore(context: InsertionContext) -> Bool {
guard let charBefore = context.charBeforeCursor else {
return false
}
if charBefore.isWhitespace {
return false
}
if charBefore == "." || charBefore == "!" || charBefore == "?" {
return true
}
if charBefore == "," || charBefore == ";" || charBefore == ":" || charBefore == "-" {
return true
}
if charBefore.isLetter || charBefore.isNumber {
return true
}
return false
}
private static func applySmartCapitalization(_ text: String, context: InsertionContext) -> String {
guard !text.isEmpty else { return text }
let shouldCapitalize = shouldCapitalizeFirstLetter(context: context)
if shouldCapitalize {
return text.prefix(1).uppercased() + text.dropFirst()
} else {
let firstWord = text.prefix(while: { !$0.isWhitespace && !$0.isPunctuation })
let isAcronymOrProper = firstWord.allSatisfy { $0.isUppercase || !$0.isLetter }
if !isAcronymOrProper {
return text.prefix(1).lowercased() + text.dropFirst()
}
}
return text
}
private static func shouldCapitalizeFirstLetter(context: InsertionContext) -> Bool {
if context.textBefore.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
}
let trimmedBefore = context.textBefore.trimmingCharacters(in: .whitespaces)
if trimmedBefore.isEmpty {
return true
}
if let lastChar = trimmedBefore.last {
if lastChar == "." || lastChar == "!" || lastChar == "?" || lastChar == "\n" {
return true
}
}
return false
}
}

View File

@ -300,7 +300,7 @@ struct SettingsView: View {
VStack(alignment: .leading, spacing: 8) {
Text("Select the method used to paste text. Use AppleScript if you have a non-standard keyboard layout.")
.settingsDescription()
Toggle("Use AppleScript Paste Method", isOn: Binding(
get: { UserDefaults.standard.bool(forKey: "UseAppleScriptPaste") },
set: { UserDefaults.standard.set($0, forKey: "UseAppleScriptPaste") }

View File

@ -398,11 +398,6 @@ class WhisperState: NSObject, ObservableObject {
"""
}
let shouldAddSpace = UserDefaults.standard.object(forKey: "AppendTrailingSpace") as? Bool ?? true
if shouldAddSpace {
textToPaste += " "
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
CursorPaster.pasteAtCursor(textToPaste)