Compare commits

..
1 Commits
Author SHA1 Message Date
thread c89ff2b68e LLMIFY THIS MOFO
CI / Test (push) Successful in 58s
2026-09-01 01:09:59 -04:00
9 changed files with 44 additions and 129 deletions
+1 -4
View File
@@ -486,15 +486,12 @@ backends = ["clipboard-paste", "ydotool", "wtype", "clipboard"] # Ordered fallb
ydotool_timeout = "5s"
wtype_timeout = "5s"
clipboard_timeout = "3s"
ctrl_shift_v_classes = ["ghostty"] # Window-class substrings that paste with Ctrl+Shift+V
```
### Injection Backends
- **`ydotool`**: Uses ydotool (requires `ydotoold` daemon for ydotool v1.0.0+). Most compatible with Chromium/Electron apps.
- **`clipboard-paste`**: Temporarily copies text to the regular clipboard, sends Ctrl+V through wtype, then restores the prior text clipboard. This works in browsers and Electron apps and is layout-independent; recommended for Dvorak/Colemak users. Requires `wl-clipboard` and `wtype`.
`ctrl_shift_v_classes` selects applications that need Ctrl+Shift+V instead of Ctrl+V. Matching is case-insensitive against Hyprland's active-window data, and the default `"ghostty"` handles Ghostty. Add a terminal's class substring to this list when needed.
- **`clipboard-paste`**: Copies text to the primary selection then sends Shift+Insert through ydotool. Fast, layout-independent, and preserves the regular clipboard; recommended for Dvorak/Colemak users.
- **`wtype`**: Uses wtype for Wayland. May have issues with some Chromium-based apps (known upstream bug).
- **`clipboard`**: Copies text to clipboard only. Most reliable, but requires manual paste.
-1
View File
@@ -116,6 +116,5 @@ func (c *Config) ToInjectionConfig() injection.Config {
YdotoolTimeout: c.Injection.YdotoolTimeout,
WtypeTimeout: c.Injection.WtypeTimeout,
ClipboardTimeout: c.Injection.ClipboardTimeout,
CtrlShiftVClasses: c.Injection.CtrlShiftVClasses,
}
}
-1
View File
@@ -24,7 +24,6 @@ func DefaultConfig() *Config {
YdotoolTimeout: 5 * time.Second,
WtypeTimeout: 5 * time.Second,
ClipboardTimeout: 3 * time.Second,
CtrlShiftVClasses: []string{"ghostty"},
},
Notifications: NotificationsConfig{
Enabled: false,
+1 -10
View File
@@ -120,14 +120,6 @@ func Save(cfg *Config) error {
sb.WriteString(fmt.Sprintf(" ydotool_timeout = %q\n", cfg.Injection.YdotoolTimeout.String()))
sb.WriteString(fmt.Sprintf(" wtype_timeout = %q\n", cfg.Injection.WtypeTimeout.String()))
sb.WriteString(fmt.Sprintf(" clipboard_timeout = %q\n", cfg.Injection.ClipboardTimeout.String()))
sb.WriteString(" ctrl_shift_v_classes = [")
for i, class := range cfg.Injection.CtrlShiftVClasses {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(fmt.Sprintf("%q", class))
}
sb.WriteString("]\n")
sb.WriteString("\n")
// Notifications
@@ -344,8 +336,7 @@ keywords = []
# - "llama-swap": Any chat model configured in your LlamaSwap server
#
# Injection backends:
# - "clipboard-paste": Temporarily uses the regular clipboard + wtype Ctrl+V, then restores prior text clipboard.
# - ctrl_shift_v_classes: Window-class substrings that use Ctrl+Shift+V instead (default: ["ghostty"]).
# - "clipboard-paste": Uses primary selection + ydotool Shift+Insert; fast, layout-independent, and preserves clipboard.
# - "ydotool": Uses ydotool (requires ydotoold daemon). Best for Chromium/Electron apps.
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium apps.
# - "clipboard": Copies to clipboard only (most reliable, requires manual paste).
-1
View File
@@ -75,7 +75,6 @@ type InjectionConfig struct {
YdotoolTimeout time.Duration `toml:"ydotool_timeout"`
WtypeTimeout time.Duration `toml:"wtype_timeout"`
ClipboardTimeout time.Duration `toml:"clipboard_timeout"`
CtrlShiftVClasses []string `toml:"ctrl_shift_v_classes"`
}
type NotificationsConfig struct {
+22 -87
View File
@@ -2,28 +2,27 @@ package injection
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// clipboardPasteBackend temporarily puts text on the regular clipboard, then
// uses wtype to issue Ctrl+V. This works in applications that do not support
// the primary selection, such as browsers and Electron apps. The previous
// text clipboard is restored after the paste has been delivered.
// clipboardPasteBackend copies text to the primary selection and pastes it
// with ydotool's layout-independent Shift+Insert key codes.
//
// This preserves the regular clipboard, is independent of the active keyboard
// layout, and delivers the whole string at once.
type clipboardPasteBackend struct {
clipboard *clipboardBackend
wtype *wtypeBackend
ctrlShiftVClasses []string
ydotool *ydotoolBackend
}
func NewClipboardPasteBackend(ctrlShiftVClasses []string) Backend {
func NewClipboardPasteBackend() Backend {
return &clipboardPasteBackend{
clipboard: NewClipboardBackend().(*clipboardBackend),
wtype: NewWtypeBackend().(*wtypeBackend),
ctrlShiftVClasses: ctrlShiftVClasses,
ydotool: NewYdotoolBackend().(*ydotoolBackend),
}
}
@@ -35,7 +34,7 @@ func (c *clipboardPasteBackend) Available() error {
if err := c.clipboard.Available(); err != nil {
return err
}
if err := c.wtype.Available(); err != nil {
if err := c.ydotool.Available(); err != nil {
return err
}
return nil
@@ -48,18 +47,12 @@ func (c *clipboardPasteBackend) Inject(ctx context.Context, text string, timeout
if err := c.Available(); err != nil {
return err
}
// wl-paste only needs to keep the clipboard owner alive while it reads the
// selection, so this command returns with the exact existing text content.
// --no-newline prevents wl-paste from adding a terminal-friendly newline.
previousClipboard, restore, err := c.readClipboard(ctx)
if err != nil {
return err
}
copyText := exec.CommandContext(ctx, "wl-copy")
copyText.Stdin = strings.NewReader(text)
if err := copyText.Run(); err != nil {
return fmt.Errorf("copy transcription to clipboard: %w", err)
// Shift+Insert pastes the primary selection, leaving the user's regular
// clipboard untouched.
primaryCopy := exec.CommandContext(ctx, "wl-copy", "--primary")
primaryCopy.Stdin = strings.NewReader(text)
if err := primaryCopy.Run(); err != nil {
return fmt.Errorf("copy transcription to primary selection: %w", err)
}
// wl-copy returns before every client has observed the new selection. Give
// the compositor a short chance to publish it; this is still perceived as
@@ -70,72 +63,14 @@ func (c *clipboardPasteBackend) Inject(ctx context.Context, text string, timeout
return ctx.Err()
}
// wtype resolves the keysym itself, avoiding ydotool's physical-keycode
// layout problem on Dvorak and Colemak setups. Terminal emulators typically
// reserve Ctrl+V, so configured window classes use Ctrl+Shift+V instead.
args := []string{"-M", "ctrl"}
if c.shouldUseCtrlShiftV(ctx) {
args = append(args, "-M", "shift", "-k", "v", "-m", "shift")
} else {
args = append(args, "-k", "v")
// Linux input event codes: KEY_LEFTSHIFT=42, KEY_INSERT=110. These keys do
// not depend on the alphabetic layout, and Shift+Insert is widely supported.
cmd := exec.CommandContext(ctx, "ydotool", "key", "42:1", "110:1", "110:0", "42:0")
if socketPath := c.ydotool.getSocketPath(); socketPath != "" {
cmd.Env = append(os.Environ(), "YDOTOOL_SOCKET="+socketPath)
}
args = append(args, "-m", "ctrl")
cmd := exec.CommandContext(ctx, "wtype", args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("paste clipboard with wtype: %w", err)
}
if !restore {
return nil
}
// Receiving a Wayland paste is asynchronous. Leave the transcription
// available briefly, then put the user's prior clipboard back.
select {
case <-time.After(300 * time.Millisecond):
case <-ctx.Done():
return ctx.Err()
}
restoreClipboard := exec.CommandContext(ctx, "wl-copy")
restoreClipboard.Stdin = strings.NewReader(previousClipboard)
if err := restoreClipboard.Run(); err != nil {
return fmt.Errorf("restore previous clipboard: %w", err)
return fmt.Errorf("paste primary selection with ydotool: %w", err)
}
return nil
}
func (c *clipboardPasteBackend) shouldUseCtrlShiftV(ctx context.Context) bool {
if len(c.ctrlShiftVClasses) == 0 {
return false
}
cmd := exec.CommandContext(ctx, "hyprctl", "activewindow", "-j")
output, err := cmd.Output()
if err != nil {
return false
}
var activeWindow struct {
Class string `json:"class"`
}
if err := json.Unmarshal(output, &activeWindow); err != nil {
return false
}
class := strings.ToLower(activeWindow.Class)
for _, match := range c.ctrlShiftVClasses {
if match = strings.TrimSpace(strings.ToLower(match)); match != "" && strings.Contains(class, match) {
return true
}
}
return false
}
func (c *clipboardPasteBackend) readClipboard(ctx context.Context) (text string, restore bool, err error) {
cmd := exec.CommandContext(ctx, "wl-paste", "--no-newline")
output, err := cmd.Output()
if err != nil {
// No clipboard owner is normal. There is simply nothing to restore.
if _, ok := err.(*exec.ExitError); ok {
return "", false, nil
}
return "", false, fmt.Errorf("read existing clipboard: %w", err)
}
return string(output), true, nil
}
+1 -2
View File
@@ -16,7 +16,6 @@ type Config struct {
YdotoolTimeout time.Duration // Timeout for ydotool commands
WtypeTimeout time.Duration // Timeout for wtype commands
ClipboardTimeout time.Duration // Timeout for clipboard operations
CtrlShiftVClasses []string // Hyprland window-class substrings that paste with Ctrl+Shift+V
}
type injector struct {
@@ -36,7 +35,7 @@ func NewInjector(config Config) Injector {
case "clipboard":
backends = append(backends, NewClipboardBackend())
case "clipboard-paste":
backends = append(backends, NewClipboardPasteBackend(config.CtrlShiftVClasses))
backends = append(backends, NewClipboardPasteBackend())
default:
log.Printf("Injection: unknown backend %q, skipping", name)
}
+1 -1
View File
@@ -204,7 +204,7 @@ func TestClipboardBackend(t *testing.T) {
}
func TestClipboardPasteBackend(t *testing.T) {
backend := NewClipboardPasteBackend([]string{"ghostty"})
backend := NewClipboardPasteBackend()
if backend.Name() != "clipboard-paste" {
t.Errorf("Name() = %s, want clipboard-paste", backend.Name())
}
-4
View File
@@ -344,10 +344,6 @@ func (p *pipeline) handleInjectAction(ctx context.Context, recorder recording.Re
}
return r
}, textToInject)
// Speech APIs occasionally include leading or trailing whitespace (for
// example Whisper commonly returns a leading space). It is transport
// formatting rather than dictated content, so never inject it.
textToInject = strings.TrimSpace(textToInject)
injector := p.injectorFactory(p.config.ToInjectionConfig())
if err := injector.Inject(ctx, textToInject); err != nil {