Compare commits
1
Commits
main
..
c89ff2b68e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c89ff2b68e |
+1
-4
@@ -486,15 +486,12 @@ backends = ["clipboard-paste", "ydotool", "wtype", "clipboard"] # Ordered fallb
|
|||||||
ydotool_timeout = "5s"
|
ydotool_timeout = "5s"
|
||||||
wtype_timeout = "5s"
|
wtype_timeout = "5s"
|
||||||
clipboard_timeout = "3s"
|
clipboard_timeout = "3s"
|
||||||
ctrl_shift_v_classes = ["ghostty"] # Window-class substrings that paste with Ctrl+Shift+V
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Injection Backends
|
### Injection Backends
|
||||||
|
|
||||||
- **`ydotool`**: Uses ydotool (requires `ydotoold` daemon for ydotool v1.0.0+). Most compatible with Chromium/Electron apps.
|
- **`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`.
|
- **`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.
|
||||||
|
|
||||||
`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.
|
|
||||||
- **`wtype`**: Uses wtype for Wayland. May have issues with some Chromium-based apps (known upstream bug).
|
- **`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.
|
- **`clipboard`**: Copies text to clipboard only. Most reliable, but requires manual paste.
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,5 @@ func (c *Config) ToInjectionConfig() injection.Config {
|
|||||||
YdotoolTimeout: c.Injection.YdotoolTimeout,
|
YdotoolTimeout: c.Injection.YdotoolTimeout,
|
||||||
WtypeTimeout: c.Injection.WtypeTimeout,
|
WtypeTimeout: c.Injection.WtypeTimeout,
|
||||||
ClipboardTimeout: c.Injection.ClipboardTimeout,
|
ClipboardTimeout: c.Injection.ClipboardTimeout,
|
||||||
CtrlShiftVClasses: c.Injection.CtrlShiftVClasses,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ func DefaultConfig() *Config {
|
|||||||
YdotoolTimeout: 5 * time.Second,
|
YdotoolTimeout: 5 * time.Second,
|
||||||
WtypeTimeout: 5 * time.Second,
|
WtypeTimeout: 5 * time.Second,
|
||||||
ClipboardTimeout: 3 * time.Second,
|
ClipboardTimeout: 3 * time.Second,
|
||||||
CtrlShiftVClasses: []string{"ghostty"},
|
|
||||||
},
|
},
|
||||||
Notifications: NotificationsConfig{
|
Notifications: NotificationsConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
|
|||||||
+1
-10
@@ -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(" ydotool_timeout = %q\n", cfg.Injection.YdotoolTimeout.String()))
|
||||||
sb.WriteString(fmt.Sprintf(" wtype_timeout = %q\n", cfg.Injection.WtypeTimeout.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(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")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
@@ -344,8 +336,7 @@ keywords = []
|
|||||||
# - "llama-swap": Any chat model configured in your LlamaSwap server
|
# - "llama-swap": Any chat model configured in your LlamaSwap server
|
||||||
#
|
#
|
||||||
# Injection backends:
|
# Injection backends:
|
||||||
# - "clipboard-paste": Temporarily uses the regular clipboard + wtype Ctrl+V, then restores prior text clipboard.
|
# - "clipboard-paste": Uses primary selection + ydotool Shift+Insert; fast, layout-independent, and preserves clipboard.
|
||||||
# - ctrl_shift_v_classes: Window-class substrings that use Ctrl+Shift+V instead (default: ["ghostty"]).
|
|
||||||
# - "ydotool": Uses ydotool (requires ydotoold daemon). Best for Chromium/Electron apps.
|
# - "ydotool": Uses ydotool (requires ydotoold daemon). Best for Chromium/Electron apps.
|
||||||
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium apps.
|
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium apps.
|
||||||
# - "clipboard": Copies to clipboard only (most reliable, requires manual paste).
|
# - "clipboard": Copies to clipboard only (most reliable, requires manual paste).
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ type InjectionConfig struct {
|
|||||||
YdotoolTimeout time.Duration `toml:"ydotool_timeout"`
|
YdotoolTimeout time.Duration `toml:"ydotool_timeout"`
|
||||||
WtypeTimeout time.Duration `toml:"wtype_timeout"`
|
WtypeTimeout time.Duration `toml:"wtype_timeout"`
|
||||||
ClipboardTimeout time.Duration `toml:"clipboard_timeout"`
|
ClipboardTimeout time.Duration `toml:"clipboard_timeout"`
|
||||||
CtrlShiftVClasses []string `toml:"ctrl_shift_v_classes"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationsConfig struct {
|
type NotificationsConfig struct {
|
||||||
|
|||||||
@@ -2,28 +2,27 @@ package injection
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// clipboardPasteBackend temporarily puts text on the regular clipboard, then
|
// clipboardPasteBackend copies text to the primary selection and pastes it
|
||||||
// uses wtype to issue Ctrl+V. This works in applications that do not support
|
// with ydotool's layout-independent Shift+Insert key codes.
|
||||||
// the primary selection, such as browsers and Electron apps. The previous
|
//
|
||||||
// text clipboard is restored after the paste has been delivered.
|
// This preserves the regular clipboard, is independent of the active keyboard
|
||||||
|
// layout, and delivers the whole string at once.
|
||||||
type clipboardPasteBackend struct {
|
type clipboardPasteBackend struct {
|
||||||
clipboard *clipboardBackend
|
clipboard *clipboardBackend
|
||||||
wtype *wtypeBackend
|
ydotool *ydotoolBackend
|
||||||
ctrlShiftVClasses []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClipboardPasteBackend(ctrlShiftVClasses []string) Backend {
|
func NewClipboardPasteBackend() Backend {
|
||||||
return &clipboardPasteBackend{
|
return &clipboardPasteBackend{
|
||||||
clipboard: NewClipboardBackend().(*clipboardBackend),
|
clipboard: NewClipboardBackend().(*clipboardBackend),
|
||||||
wtype: NewWtypeBackend().(*wtypeBackend),
|
ydotool: NewYdotoolBackend().(*ydotoolBackend),
|
||||||
ctrlShiftVClasses: ctrlShiftVClasses,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ func (c *clipboardPasteBackend) Available() error {
|
|||||||
if err := c.clipboard.Available(); err != nil {
|
if err := c.clipboard.Available(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.wtype.Available(); err != nil {
|
if err := c.ydotool.Available(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -48,18 +47,12 @@ func (c *clipboardPasteBackend) Inject(ctx context.Context, text string, timeout
|
|||||||
if err := c.Available(); err != nil {
|
if err := c.Available(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// wl-paste only needs to keep the clipboard owner alive while it reads the
|
// Shift+Insert pastes the primary selection, leaving the user's regular
|
||||||
// selection, so this command returns with the exact existing text content.
|
// clipboard untouched.
|
||||||
// --no-newline prevents wl-paste from adding a terminal-friendly newline.
|
primaryCopy := exec.CommandContext(ctx, "wl-copy", "--primary")
|
||||||
previousClipboard, restore, err := c.readClipboard(ctx)
|
primaryCopy.Stdin = strings.NewReader(text)
|
||||||
if err != nil {
|
if err := primaryCopy.Run(); err != nil {
|
||||||
return err
|
return fmt.Errorf("copy transcription to primary selection: %w", 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)
|
|
||||||
}
|
}
|
||||||
// wl-copy returns before every client has observed the new selection. Give
|
// 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
|
// 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()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// wtype resolves the keysym itself, avoiding ydotool's physical-keycode
|
// Linux input event codes: KEY_LEFTSHIFT=42, KEY_INSERT=110. These keys do
|
||||||
// layout problem on Dvorak and Colemak setups. Terminal emulators typically
|
// not depend on the alphabetic layout, and Shift+Insert is widely supported.
|
||||||
// reserve Ctrl+V, so configured window classes use Ctrl+Shift+V instead.
|
cmd := exec.CommandContext(ctx, "ydotool", "key", "42:1", "110:1", "110:0", "42:0")
|
||||||
args := []string{"-M", "ctrl"}
|
if socketPath := c.ydotool.getSocketPath(); socketPath != "" {
|
||||||
if c.shouldUseCtrlShiftV(ctx) {
|
cmd.Env = append(os.Environ(), "YDOTOOL_SOCKET="+socketPath)
|
||||||
args = append(args, "-M", "shift", "-k", "v", "-m", "shift")
|
|
||||||
} else {
|
|
||||||
args = append(args, "-k", "v")
|
|
||||||
}
|
}
|
||||||
args = append(args, "-m", "ctrl")
|
|
||||||
cmd := exec.CommandContext(ctx, "wtype", args...)
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("paste clipboard with wtype: %w", err)
|
return fmt.Errorf("paste primary selection with ydotool: %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 nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type Config struct {
|
|||||||
YdotoolTimeout time.Duration // Timeout for ydotool commands
|
YdotoolTimeout time.Duration // Timeout for ydotool commands
|
||||||
WtypeTimeout time.Duration // Timeout for wtype commands
|
WtypeTimeout time.Duration // Timeout for wtype commands
|
||||||
ClipboardTimeout time.Duration // Timeout for clipboard operations
|
ClipboardTimeout time.Duration // Timeout for clipboard operations
|
||||||
CtrlShiftVClasses []string // Hyprland window-class substrings that paste with Ctrl+Shift+V
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type injector struct {
|
type injector struct {
|
||||||
@@ -36,7 +35,7 @@ func NewInjector(config Config) Injector {
|
|||||||
case "clipboard":
|
case "clipboard":
|
||||||
backends = append(backends, NewClipboardBackend())
|
backends = append(backends, NewClipboardBackend())
|
||||||
case "clipboard-paste":
|
case "clipboard-paste":
|
||||||
backends = append(backends, NewClipboardPasteBackend(config.CtrlShiftVClasses))
|
backends = append(backends, NewClipboardPasteBackend())
|
||||||
default:
|
default:
|
||||||
log.Printf("Injection: unknown backend %q, skipping", name)
|
log.Printf("Injection: unknown backend %q, skipping", name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func TestClipboardBackend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClipboardPasteBackend(t *testing.T) {
|
func TestClipboardPasteBackend(t *testing.T) {
|
||||||
backend := NewClipboardPasteBackend([]string{"ghostty"})
|
backend := NewClipboardPasteBackend()
|
||||||
if backend.Name() != "clipboard-paste" {
|
if backend.Name() != "clipboard-paste" {
|
||||||
t.Errorf("Name() = %s, want clipboard-paste", backend.Name())
|
t.Errorf("Name() = %s, want clipboard-paste", backend.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,10 +344,6 @@ func (p *pipeline) handleInjectAction(ctx context.Context, recorder recording.Re
|
|||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}, textToInject)
|
}, 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())
|
injector := p.injectorFactory(p.config.ToInjectionConfig())
|
||||||
|
|
||||||
if err := injector.Inject(ctx, textToInject); err != nil {
|
if err := injector.Inject(ctx, textToInject); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user