Last active
May 5, 2025 01:52
-
-
Save pgaskin/b265bfdf75941161b850179ab7fbde6f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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