package textoutput
|
|
import (
|
"strings"
|
"sync"
|
"time"
|
"unicode/utf8"
|
"voicesnap/internal/axinput"
|
"voicesnap/internal/logger"
|
)
|
|
type Paster interface {
|
Paste(text string, keepClipboard bool) error
|
TypeText(text string) error
|
}
|
|
type Result struct {
|
OK bool `json:"ok"`
|
Text string `json:"text,omitempty"`
|
Method string `json:"method"`
|
Message string `json:"message"`
|
Target string `json:"target,omitempty"`
|
NeedsPermission bool `json:"needsPermission"`
|
DirectInserted bool `json:"directInserted"`
|
FallbackAvailable bool `json:"fallbackAvailable"`
|
CreatedAt time.Time `json:"createdAt"`
|
}
|
|
type AutoPasteGuard struct {
|
OK bool
|
Identity string
|
Message string
|
}
|
|
type OutputOptions struct {
|
KeepClipboard bool
|
AutoPasteExperiment bool
|
AutoPasteGuard AutoPasteGuard
|
}
|
|
type Router struct {
|
mu sync.Mutex
|
paster Paster
|
driver axinput.Driver
|
last Result
|
}
|
|
func NewRouter(paster Paster) *Router {
|
return newPlatformRouter(paster)
|
}
|
|
func (r *Router) LastResult() Result {
|
r.mu.Lock()
|
defer r.mu.Unlock()
|
return r.last
|
}
|
|
func (r *Router) CaptureAutoPasteGuard() AutoPasteGuard {
|
if r.driver == nil {
|
return AutoPasteGuard{OK: false, Message: "AX driver is unavailable"}
|
}
|
identity := r.driver.FrontmostAppIdentity()
|
return AutoPasteGuard{OK: identity.OK, Identity: identity.Identity, Message: identity.Message}
|
}
|
|
func (r *Router) remember(result Result) Result {
|
if result.CreatedAt.IsZero() {
|
result.CreatedAt = time.Now()
|
}
|
r.mu.Lock()
|
r.last = result
|
r.mu.Unlock()
|
return result
|
}
|
|
func (r *Router) PrepareLastClipboardFallback() axinput.ClipboardResult {
|
r.mu.Lock()
|
last := r.last
|
r.mu.Unlock()
|
if !last.FallbackAvailable || last.Text == "" {
|
logger.Info("Text output fallback prepare route status=no_pending fallback_available=%t has_text=%t", last.FallbackAvailable, last.Text != "")
|
return axinput.ClipboardResult{OK: false, Message: "no pending text output fallback is available"}
|
}
|
if !clipboardFallbackSafeForTarget(last) {
|
logger.Info("Text output fallback prepare route status=blocked_non_writable text_runes=%d", utf8.RuneCountInString(last.Text))
|
return axinput.ClipboardResult{OK: false, Message: "clipboard fallback is disabled for this non-writable target"}
|
}
|
if r.driver == nil {
|
logger.Info("Text output fallback prepare route status=no_driver text_runes=%d", utf8.RuneCountInString(last.Text))
|
return axinput.ClipboardResult{OK: false, Message: "clipboard fallback is unavailable for this output route"}
|
}
|
result := r.driver.PrepareClipboardFallback(last.Text)
|
logger.Info(
|
"Text output fallback prepare route status=driver_result ok=%t prepared=%t token_present=%t text_runes=%d",
|
result.OK,
|
result.Prepared,
|
result.Token != "",
|
utf8.RuneCountInString(last.Text),
|
)
|
return result
|
}
|
|
func (r *Router) RestoreClipboardFallback(token string) axinput.ClipboardResult {
|
if r.driver == nil {
|
logger.Info("Text output fallback restore route status=no_driver token_present=%t", token != "")
|
return axinput.ClipboardResult{OK: false, Message: "clipboard fallback is unavailable for this output route"}
|
}
|
result := r.driver.RestoreClipboardFallback(token)
|
logger.Info(
|
"Text output fallback restore route status=driver_result ok=%t restored=%t token_present=%t",
|
result.OK,
|
result.Restored,
|
token != "",
|
)
|
return result
|
}
|
|
func clipboardFallbackSafeForTarget(result Result) bool {
|
target := strings.TrimSpace(strings.ToLower(result.Target))
|
if target == "" || target == "target=unknown" {
|
return true
|
}
|
for _, marker := range []string{
|
"app=terminal",
|
"app=iterm",
|
"app=iterm2",
|
"app=warp",
|
"role=axterminal",
|
"subrole=axterminal",
|
"console",
|
"shell",
|
} {
|
if strings.Contains(target, marker) {
|
return false
|
}
|
}
|
return true
|
}
|
|
func autoPasteSafeForIdentity(identity string) bool {
|
normalized := strings.ToLower(strings.TrimSpace(identity))
|
if normalized == "" {
|
return false
|
}
|
for _, marker := range []string{
|
"com.apple.terminal",
|
"com.googlecode.iterm2",
|
"iterm",
|
"terminal",
|
"warp",
|
"shell",
|
"console",
|
} {
|
if strings.Contains(normalized, marker) {
|
return false
|
}
|
}
|
return true
|
}
|