Skip to content

Instantly share code, notes, and snippets.

@pgaskin
Last active May 5, 2025 01:52
Show Gist options
  • Save pgaskin/b265bfdf75941161b850179ab7fbde6f to your computer and use it in GitHub Desktop.
Save pgaskin/b265bfdf75941161b850179ab7fbde6f to your computer and use it in GitHub Desktop.
// Command i3-multi-input sends input to all selected i3wm windows. It is
// intended to be bound to a keyboard shortcut.
//
// bindsym --release $mod+Shift+a exec --no-startup-id i3-multi-input
package main
import (
"fmt"
"iter"
"os"
"os/exec"
"time"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"go.i3wm.org/i3/v4"
)
// note: this may not work with some clients (e.g., wine, xterm) which block XSendEvent
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
exec.Command("i3-nagbar", "-t", "error", "-m", err.Error()).Start()
os.Exit(1)
}
}
func run() error {
t, err := i3.GetTree()
if err != nil {
return err
}
// TODO: allow selection via marks
var targets []xproto.Window
for c := range findSelectedWindows(t.Root) {
targets = append(targets, xproto.Window(c.Window))
}
time.Sleep(time.Millisecond * 250) // TODO: figure out why it doesn't work with the bindsym (even though it works standalone) if we don't do this (seems to be something to do with mod/shift still being down)
conn, err := xgb.NewConn()
if err != nil {
return err
}
defer conn.Close()
setup := xproto.Setup(conn)
screen := setup.DefaultScreen(conn)
wid, err := xproto.NewWindowId(conn)
if err != nil {
return err
}
if err := xproto.CreateWindowChecked(conn,
0, wid, screen.Root,
0, 0, 1, 1, 0,
xproto.WindowClassInputOnly, 0,
xproto.CwOverrideRedirect, []uint32{1},
).Check(); err != nil {
return err
}
if err := xproto.MapWindowChecked(conn, wid).Check(); err != nil {
return err
}
if _, err := xproto.GrabKeyboard(conn, false, wid, xproto.TimeCurrentTime, xproto.GrabModeAsync, xproto.GrabModeAsync).Reply(); err != nil {
return err
}
defer xproto.UngrabKeyboard(conn, 0)
if _, err := xproto.GrabPointer(conn, false, wid, xproto.EventMaskButtonPress, xproto.GrabModeAsync, xproto.GrabModeAsync, xproto.WindowNone, xproto.CursorNone, xproto.TimeCurrentTime).Reply(); err != nil {
return err
}
defer xproto.UngrabPointer(conn, 0)
for {
event, err := conn.WaitForEvent()
if err != nil {
panic(err)
}
for _, target := range targets {
switch event := event.(type) {
case xproto.ButtonPressEvent:
return nil
case xproto.KeyPressEvent:
if event.Detail == 9 { // escape
return nil
}
b := xproto.KeyReleaseEvent{
Detail: event.Detail,
Time: event.Time,
Root: event.Root,
Event: target,
RootX: event.RootX,
RootY: event.RootY,
EventX: event.EventX,
EventY: event.EventY,
State: event.State,
SameScreen: true,
}.Bytes()
b[0] = xproto.KeyPress
xproto.SendEvent(conn, false, target, xproto.EventMaskNoEvent, string(b))
case xproto.KeyReleaseEvent:
b := xproto.KeyReleaseEvent{
Detail: event.Detail,
Time: event.Time,
Root: event.Root,
Event: target,
RootX: event.RootX,
RootY: event.RootY,
EventX: event.EventX,
EventY: event.EventY,
State: event.State,
SameScreen: true,
}.Bytes()
b[0] = xproto.KeyRelease // work around bug in xgb
xproto.SendEvent(conn, false, target, xproto.EventMaskNoEvent, string(b))
}
}
}
}
func findSelectedWindows(t *i3.Node) iter.Seq[*i3.Node] {
return func(yield func(*i3.Node) bool) {
for c := range findChildren(t) {
if c.Focused {
for c := range findChildren(c) {
if c.Window != 0 {
if !yield(c) {
return
}
}
}
}
}
}
}
func findChildren(n *i3.Node) iter.Seq[*i3.Node] {
return func(yield func(*i3.Node) bool) {
var stack []*i3.Node
stack = append(stack, n)
for len(stack) != 0 {
cur := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if !yield(cur) {
return
}
stack = append(stack, cur.Nodes...)
stack = append(stack, cur.FloatingNodes...)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment