merge: input-fixes
6 files · +271 -11
M internal/adapters/headlessinput/input.go +9 -1
| @@ -40,7 +40,15 @@ func Run(ctx context.Context, km *xkb.Keymap, script <-chan string, input chan<- | |||
| 40 | 40 | return nil | |
| 41 | 41 | } | |
| 42 | 42 | if shift { | |
| 43 | - | mods = append(mods, 42) | |
| 43 | + | found := false | |
| 44 | + | for _, m := range mods { | |
| 45 | + | if m == 42 || m == 54 { | |
| 46 | + | found = true | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | if !found { | |
| 50 | + | mods = append(mods, 42) | |
| 51 | + | } | |
| 44 | 52 | } | |
| 45 | 53 | for _, m := range mods { | |
| 46 | 54 | if err := emit(m, true); err != nil { | |
| @@ -40,7 +40,15 @@ func Run(ctx context.Context, km *xkb.Keymap, script <-chan string, input chan<- | |||||
| 40 | return nil | 40 | return nil | ||
| 41 | } | 41 | } | ||
| 42 | if shift { | 42 | if shift { | ||
| 43 | - | mods = append(mods, 42) | 43 | + | found := false |
| 44 | + | for _, m := range mods { | |||
| 45 | + | if m == 42 || m == 54 { | |||
| 46 | + | found = true | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | if !found { | |||
| 50 | + | mods = append(mods, 42) | |||
| 51 | + | } | |||
| 44 | } | 52 | } | ||
| 45 | for _, m := range mods { | 53 | for _, m := range mods { | ||
| 46 | if err := emit(m, true); err != nil { | 54 | if err := emit(m, true); err != nil { | ||
M internal/adapters/headlessinput/input_test.go +3 -2
| @@ -16,6 +16,7 @@ func TestScript(t *testing.T) { | |||
| 16 | 16 | codes []uint32 | |
| 17 | 17 | }{ | |
| 18 | 18 | {"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil}, | |
| 19 | + | {"key Shift+A", []string{"Shift_L", "A", "A", "Shift_L"}, []uint32{42, 30, 30, 42}}, | |
| 19 | 20 | {"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}}, | |
| 20 | 21 | } { | |
| 21 | 22 | t.Run(tt.line, func(t *testing.T) { | |
| @@ -44,10 +45,10 @@ func TestScript(t *testing.T) { | |||
| 44 | 45 | if tt.codes != nil && ev.Keycode != tt.codes[i] { | |
| 45 | 46 | t.Errorf("code %d: %d", i, ev.Keycode) | |
| 46 | 47 | } | |
| 47 | - | if tt.codes != nil && i == 1 && ev.Mods&ports.ModSuper == 0 { | |
| 48 | + | if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 { | |
| 48 | 49 | t.Error("Return missing Super") | |
| 49 | 50 | } | |
| 50 | - | if tt.codes != nil && i == 3 && ev.Mods&ports.ModSuper != 0 { | |
| 51 | + | if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 { | |
| 51 | 52 | t.Error("Super not released") | |
| 52 | 53 | } | |
| 53 | 54 | } | |
| @@ -16,6 +16,7 @@ func TestScript(t *testing.T) { | |||||
| 16 | codes []uint32 | 16 | codes []uint32 | ||
| 17 | }{ | 17 | }{ | ||
| 18 | {"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil}, | 18 | {"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil}, | ||
| 19 | + | {"key Shift+A", []string{"Shift_L", "A", "A", "Shift_L"}, []uint32{42, 30, 30, 42}}, | |||
| 19 | {"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}}, | 20 | {"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}}, | ||
| 20 | } { | 21 | } { | ||
| 21 | t.Run(tt.line, func(t *testing.T) { | 22 | t.Run(tt.line, func(t *testing.T) { | ||
| @@ -44,10 +45,10 @@ func TestScript(t *testing.T) { | |||||
| 44 | if tt.codes != nil && ev.Keycode != tt.codes[i] { | 45 | if tt.codes != nil && ev.Keycode != tt.codes[i] { | ||
| 45 | t.Errorf("code %d: %d", i, ev.Keycode) | 46 | t.Errorf("code %d: %d", i, ev.Keycode) | ||
| 46 | } | 47 | } | ||
| 47 | - | if tt.codes != nil && i == 1 && ev.Mods&ports.ModSuper == 0 { | 48 | + | if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 { |
| 48 | t.Error("Return missing Super") | 49 | t.Error("Return missing Super") | ||
| 49 | } | 50 | } | ||
| 50 | - | if tt.codes != nil && i == 3 && ev.Mods&ports.ModSuper != 0 { | 51 | + | if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 { |
| 51 | t.Error("Super not released") | 52 | t.Error("Super not released") | ||
| 52 | } | 53 | } | ||
| 53 | } | 54 | } | ||
M internal/adapters/wayland/protocol_test.go +223 -1
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "errors" | |
| 5 | 6 | "github.com/bnema/nefertty/internal/ports" | |
| 6 | 7 | "path/filepath" | |
| @@ -49,7 +50,14 @@ func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, arg | |||
| 49 | 50 | func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) { | |
| 50 | 51 | t.Helper() | |
| 51 | 52 | done := make(chan error, 1) | |
| 52 | - | go func() { done <- c.Roundtrip() }() | |
| 53 | + | go func() { | |
| 54 | + | for { | |
| 55 | + | if err := c.Dispatch(); err != nil { | |
| 56 | + | done <- err | |
| 57 | + | return | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | }() | |
| 53 | 61 | select { | |
| 54 | 62 | case err := <-done: | |
| 55 | 63 | var displayErr *wlturbo.DisplayError | |
| @@ -333,3 +341,217 @@ func TestBufferContentUnpaddedLastRow(t *testing.T) { | |||
| 333 | 341 | } | |
| 334 | 342 | } | |
| 335 | 343 | } | |
| 344 | + | ||
| 345 | + | type pointerEvents struct { | |
| 346 | + | wlturbo.BaseProxy | |
| 347 | + | enters chan [2]float64 | |
| 348 | + | buttons chan uint32 | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | func (p *pointerEvents) Dispatch(e *wlturbo.Event) { | |
| 352 | + | switch e.Opcode { | |
| 353 | + | case uint16(wayland.PointerEventEnter): | |
| 354 | + | _ = e.Uint32() | |
| 355 | + | _ = e.Uint32() | |
| 356 | + | p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()} | |
| 357 | + | case uint16(wayland.PointerEventButton): | |
| 358 | + | _ = e.Uint32() | |
| 359 | + | _ = e.Uint32() | |
| 360 | + | p.buttons <- e.Uint32() | |
| 361 | + | } | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | func TestPointerProtocol(t *testing.T) { | |
| 365 | + | s, events, commands, dir := lifecycleServer(t) | |
| 366 | + | c := protocolClient(t, s, dir) | |
| 367 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 368 | + | registerProtocol(t, c, seat) | |
| 369 | + | pointer := c.AllocateID() | |
| 370 | + | p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)} | |
| 371 | + | p.SetID(pointer) | |
| 372 | + | c.Context().Register(p) | |
| 373 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 374 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 375 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 376 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 377 | + | registerProtocol(t, c, shm) | |
| 378 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |
| 379 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 380 | + | registerProtocol(t, c, surf) | |
| 381 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 382 | + | serials := make(chan uint32, 1) | |
| 383 | + | xp := &configureProxy{serial: serials} | |
| 384 | + | xp.SetID(xdg) | |
| 385 | + | c.Context().Register(xp) | |
| 386 | + | top := c.AllocateID() | |
| 387 | + | registerProtocol(t, c, top) | |
| 388 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 389 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 390 | + | if err := c.Roundtrip(); err != nil { | |
| 391 | + | t.Fatal(err) | |
| 392 | + | } | |
| 393 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |
| 394 | + | fd, err := unix.MemfdCreate("pointer-buffer", 0) | |
| 395 | + | if err != nil { | |
| 396 | + | t.Fatal(err) | |
| 397 | + | } | |
| 398 | + | defer unix.Close(fd) | |
| 399 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 400 | + | t.Fatal(err) | |
| 401 | + | } | |
| 402 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 403 | + | registerProtocol(t, c, pool) | |
| 404 | + | registerProtocol(t, c, buffer) | |
| 405 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |
| 406 | + | t.Fatal(err) | |
| 407 | + | } | |
| 408 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 409 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 410 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 411 | + | if err := c.Roundtrip(); err != nil { | |
| 412 | + | t.Fatal(err) | |
| 413 | + | } | |
| 414 | + | w := mapped(t, events, 2*time.Second) | |
| 415 | + | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | |
| 416 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | |
| 417 | + | deadline := time.After(2 * time.Second) | |
| 418 | + | dispatched := make(chan error, 1) | |
| 419 | + | go func() { | |
| 420 | + | for { | |
| 421 | + | if err := c.Dispatch(); err != nil { | |
| 422 | + | dispatched <- err | |
| 423 | + | return | |
| 424 | + | } | |
| 425 | + | } | |
| 426 | + | }() | |
| 427 | + | for len(p.enters) == 0 || len(p.buttons) == 0 { | |
| 428 | + | select { | |
| 429 | + | case err := <-dispatched: | |
| 430 | + | t.Fatal(err) | |
| 431 | + | case <-deadline: | |
| 432 | + | t.Fatal("pointer events timeout") | |
| 433 | + | case <-time.After(time.Millisecond): | |
| 434 | + | } | |
| 435 | + | } | |
| 436 | + | select { | |
| 437 | + | case coords := <-p.enters: | |
| 438 | + | if coords != [2]float64{10, 20} { | |
| 439 | + | t.Fatalf("enter: %v", coords) | |
| 440 | + | } | |
| 441 | + | default: | |
| 442 | + | t.Fatal("missing pointer enter") | |
| 443 | + | } | |
| 444 | + | select { | |
| 445 | + | case button := <-p.buttons: | |
| 446 | + | if button != 0x110 { | |
| 447 | + | t.Fatalf("button: %d", button) | |
| 448 | + | } | |
| 449 | + | default: | |
| 450 | + | t.Fatal("missing pointer button") | |
| 451 | + | } | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | type heldKeyProxy struct { | |
| 455 | + | wlturbo.BaseProxy | |
| 456 | + | enters chan []byte | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | func (p *heldKeyProxy) Dispatch(e *wlturbo.Event) { | |
| 460 | + | if e.Opcode == uint16(wayland.KeyboardEventEnter) { | |
| 461 | + | _ = e.Uint32() | |
| 462 | + | _ = e.Uint32() | |
| 463 | + | p.enters <- e.Array() | |
| 464 | + | } | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |
| 468 | + | s, events, commands, dir := keyboardServer(t) | |
| 469 | + | c := protocolClient(t, s, dir) | |
| 470 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 471 | + | registerProtocol(t, c, seat) | |
| 472 | + | keyboard := c.AllocateID() | |
| 473 | + | kp := &heldKeyProxy{enters: make(chan []byte, 4)} | |
| 474 | + | kp.SetID(keyboard) | |
| 475 | + | c.Context().Register(kp) | |
| 476 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboard) | |
| 477 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 478 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 479 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 480 | + | registerProtocol(t, c, shm) | |
| 481 | + | fd, err := unix.MemfdCreate("held-key-buffer", 0) | |
| 482 | + | if err != nil { | |
| 483 | + | t.Fatal(err) | |
| 484 | + | } | |
| 485 | + | defer unix.Close(fd) | |
| 486 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 487 | + | t.Fatal(err) | |
| 488 | + | } | |
| 489 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 490 | + | registerProtocol(t, c, pool) | |
| 491 | + | registerProtocol(t, c, buffer) | |
| 492 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |
| 493 | + | t.Fatal(err) | |
| 494 | + | } | |
| 495 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 496 | + | mapWindow := func() ports.WindowID { | |
| 497 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |
| 498 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 499 | + | registerProtocol(t, c, surf) | |
| 500 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 501 | + | serials := make(chan uint32, 1) | |
| 502 | + | xp := &configureProxy{serial: serials} | |
| 503 | + | xp.SetID(xdg) | |
| 504 | + | c.Context().Register(xp) | |
| 505 | + | registerProtocol(t, c, top) | |
| 506 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 507 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 508 | + | if err := c.Roundtrip(); err != nil { | |
| 509 | + | t.Fatal(err) | |
| 510 | + | } | |
| 511 | + | select { | |
| 512 | + | case serial := <-serials: | |
| 513 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |
| 514 | + | default: | |
| 515 | + | t.Fatal("missing configure") | |
| 516 | + | } | |
| 517 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 518 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 519 | + | if err := c.Roundtrip(); err != nil { | |
| 520 | + | t.Fatal(err) | |
| 521 | + | } | |
| 522 | + | return mapped(t, events, 2*time.Second).ID | |
| 523 | + | } | |
| 524 | + | a := mapWindow() | |
| 525 | + | b := mapWindow() | |
| 526 | + | // Dispatch without sending a new request after focus changes. | |
| 527 | + | dispatched := make(chan error, 1) | |
| 528 | + | go func() { | |
| 529 | + | for { | |
| 530 | + | if err := c.Dispatch(); err != nil { | |
| 531 | + | dispatched <- err | |
| 532 | + | return | |
| 533 | + | } | |
| 534 | + | } | |
| 535 | + | }() | |
| 536 | + | enter := func(want []byte) { | |
| 537 | + | t.Helper() | |
| 538 | + | select { | |
| 539 | + | case got := <-kp.enters: | |
| 540 | + | if !bytes.Equal(got, want) { | |
| 541 | + | t.Fatalf("keyboard enter keys = %v, want %v", got, want) | |
| 542 | + | } | |
| 543 | + | case err := <-dispatched: | |
| 544 | + | t.Fatal(err) | |
| 545 | + | case <-time.After(2 * time.Second): | |
| 546 | + | t.Fatal("keyboard enter timeout") | |
| 547 | + | } | |
| 548 | + | } | |
| 549 | + | commands <- ports.FocusWindow{ID: a} | |
| 550 | + | enter(nil) | |
| 551 | + | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}} | |
| 552 | + | commands <- ports.FocusWindow{ID: b} | |
| 553 | + | enter([]byte{30, 0, 0, 0}) | |
| 554 | + | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}} | |
| 555 | + | commands <- ports.FocusWindow{ID: a} | |
| 556 | + | enter(nil) | |
| 557 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "errors" | 5 | "errors" | ||
| 5 | "github.com/bnema/nefertty/internal/ports" | 6 | "github.com/bnema/nefertty/internal/ports" | ||
| 6 | "path/filepath" | 7 | "path/filepath" | ||
| @@ -49,7 +50,14 @@ func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, arg | |||||
| 49 | func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) { | 50 | func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) { | ||
| 50 | t.Helper() | 51 | t.Helper() | ||
| 51 | done := make(chan error, 1) | 52 | done := make(chan error, 1) | ||
| 52 | - | go func() { done <- c.Roundtrip() }() | 53 | + | go func() { |
| 54 | + | for { | |||
| 55 | + | if err := c.Dispatch(); err != nil { | |||
| 56 | + | done <- err | |||
| 57 | + | return | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | }() | |||
| 53 | select { | 61 | select { | ||
| 54 | case err := <-done: | 62 | case err := <-done: | ||
| 55 | var displayErr *wlturbo.DisplayError | 63 | var displayErr *wlturbo.DisplayError | ||
| @@ -333,3 +341,217 @@ func TestBufferContentUnpaddedLastRow(t *testing.T) { | |||||
| 333 | } | 341 | } | ||
| 334 | } | 342 | } | ||
| 335 | } | 343 | } | ||
| 344 | + | ||||
| 345 | + | type pointerEvents struct { | |||
| 346 | + | wlturbo.BaseProxy | |||
| 347 | + | enters chan [2]float64 | |||
| 348 | + | buttons chan uint32 | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | func (p *pointerEvents) Dispatch(e *wlturbo.Event) { | |||
| 352 | + | switch e.Opcode { | |||
| 353 | + | case uint16(wayland.PointerEventEnter): | |||
| 354 | + | _ = e.Uint32() | |||
| 355 | + | _ = e.Uint32() | |||
| 356 | + | p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()} | |||
| 357 | + | case uint16(wayland.PointerEventButton): | |||
| 358 | + | _ = e.Uint32() | |||
| 359 | + | _ = e.Uint32() | |||
| 360 | + | p.buttons <- e.Uint32() | |||
| 361 | + | } | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | func TestPointerProtocol(t *testing.T) { | |||
| 365 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 366 | + | c := protocolClient(t, s, dir) | |||
| 367 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 368 | + | registerProtocol(t, c, seat) | |||
| 369 | + | pointer := c.AllocateID() | |||
| 370 | + | p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)} | |||
| 371 | + | p.SetID(pointer) | |||
| 372 | + | c.Context().Register(p) | |||
| 373 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 374 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 375 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 376 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 377 | + | registerProtocol(t, c, shm) | |||
| 378 | + | surf, xdg := c.AllocateID(), c.AllocateID() | |||
| 379 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 380 | + | registerProtocol(t, c, surf) | |||
| 381 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 382 | + | serials := make(chan uint32, 1) | |||
| 383 | + | xp := &configureProxy{serial: serials} | |||
| 384 | + | xp.SetID(xdg) | |||
| 385 | + | c.Context().Register(xp) | |||
| 386 | + | top := c.AllocateID() | |||
| 387 | + | registerProtocol(t, c, top) | |||
| 388 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 389 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 390 | + | if err := c.Roundtrip(); err != nil { | |||
| 391 | + | t.Fatal(err) | |||
| 392 | + | } | |||
| 393 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |||
| 394 | + | fd, err := unix.MemfdCreate("pointer-buffer", 0) | |||
| 395 | + | if err != nil { | |||
| 396 | + | t.Fatal(err) | |||
| 397 | + | } | |||
| 398 | + | defer unix.Close(fd) | |||
| 399 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 400 | + | t.Fatal(err) | |||
| 401 | + | } | |||
| 402 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 403 | + | registerProtocol(t, c, pool) | |||
| 404 | + | registerProtocol(t, c, buffer) | |||
| 405 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 406 | + | t.Fatal(err) | |||
| 407 | + | } | |||
| 408 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 409 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 410 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 411 | + | if err := c.Roundtrip(); err != nil { | |||
| 412 | + | t.Fatal(err) | |||
| 413 | + | } | |||
| 414 | + | w := mapped(t, events, 2*time.Second) | |||
| 415 | + | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | |||
| 416 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | |||
| 417 | + | deadline := time.After(2 * time.Second) | |||
| 418 | + | dispatched := make(chan error, 1) | |||
| 419 | + | go func() { | |||
| 420 | + | for { | |||
| 421 | + | if err := c.Dispatch(); err != nil { | |||
| 422 | + | dispatched <- err | |||
| 423 | + | return | |||
| 424 | + | } | |||
| 425 | + | } | |||
| 426 | + | }() | |||
| 427 | + | for len(p.enters) == 0 || len(p.buttons) == 0 { | |||
| 428 | + | select { | |||
| 429 | + | case err := <-dispatched: | |||
| 430 | + | t.Fatal(err) | |||
| 431 | + | case <-deadline: | |||
| 432 | + | t.Fatal("pointer events timeout") | |||
| 433 | + | case <-time.After(time.Millisecond): | |||
| 434 | + | } | |||
| 435 | + | } | |||
| 436 | + | select { | |||
| 437 | + | case coords := <-p.enters: | |||
| 438 | + | if coords != [2]float64{10, 20} { | |||
| 439 | + | t.Fatalf("enter: %v", coords) | |||
| 440 | + | } | |||
| 441 | + | default: | |||
| 442 | + | t.Fatal("missing pointer enter") | |||
| 443 | + | } | |||
| 444 | + | select { | |||
| 445 | + | case button := <-p.buttons: | |||
| 446 | + | if button != 0x110 { | |||
| 447 | + | t.Fatalf("button: %d", button) | |||
| 448 | + | } | |||
| 449 | + | default: | |||
| 450 | + | t.Fatal("missing pointer button") | |||
| 451 | + | } | |||
| 452 | + | } | |||
| 453 | + | ||||
| 454 | + | type heldKeyProxy struct { | |||
| 455 | + | wlturbo.BaseProxy | |||
| 456 | + | enters chan []byte | |||
| 457 | + | } | |||
| 458 | + | ||||
| 459 | + | func (p *heldKeyProxy) Dispatch(e *wlturbo.Event) { | |||
| 460 | + | if e.Opcode == uint16(wayland.KeyboardEventEnter) { | |||
| 461 | + | _ = e.Uint32() | |||
| 462 | + | _ = e.Uint32() | |||
| 463 | + | p.enters <- e.Array() | |||
| 464 | + | } | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |||
| 468 | + | s, events, commands, dir := keyboardServer(t) | |||
| 469 | + | c := protocolClient(t, s, dir) | |||
| 470 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 471 | + | registerProtocol(t, c, seat) | |||
| 472 | + | keyboard := c.AllocateID() | |||
| 473 | + | kp := &heldKeyProxy{enters: make(chan []byte, 4)} | |||
| 474 | + | kp.SetID(keyboard) | |||
| 475 | + | c.Context().Register(kp) | |||
| 476 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboard) | |||
| 477 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 478 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 479 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 480 | + | registerProtocol(t, c, shm) | |||
| 481 | + | fd, err := unix.MemfdCreate("held-key-buffer", 0) | |||
| 482 | + | if err != nil { | |||
| 483 | + | t.Fatal(err) | |||
| 484 | + | } | |||
| 485 | + | defer unix.Close(fd) | |||
| 486 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 487 | + | t.Fatal(err) | |||
| 488 | + | } | |||
| 489 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 490 | + | registerProtocol(t, c, pool) | |||
| 491 | + | registerProtocol(t, c, buffer) | |||
| 492 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 493 | + | t.Fatal(err) | |||
| 494 | + | } | |||
| 495 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 496 | + | mapWindow := func() ports.WindowID { | |||
| 497 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |||
| 498 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 499 | + | registerProtocol(t, c, surf) | |||
| 500 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 501 | + | serials := make(chan uint32, 1) | |||
| 502 | + | xp := &configureProxy{serial: serials} | |||
| 503 | + | xp.SetID(xdg) | |||
| 504 | + | c.Context().Register(xp) | |||
| 505 | + | registerProtocol(t, c, top) | |||
| 506 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 507 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 508 | + | if err := c.Roundtrip(); err != nil { | |||
| 509 | + | t.Fatal(err) | |||
| 510 | + | } | |||
| 511 | + | select { | |||
| 512 | + | case serial := <-serials: | |||
| 513 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |||
| 514 | + | default: | |||
| 515 | + | t.Fatal("missing configure") | |||
| 516 | + | } | |||
| 517 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 518 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 519 | + | if err := c.Roundtrip(); err != nil { | |||
| 520 | + | t.Fatal(err) | |||
| 521 | + | } | |||
| 522 | + | return mapped(t, events, 2*time.Second).ID | |||
| 523 | + | } | |||
| 524 | + | a := mapWindow() | |||
| 525 | + | b := mapWindow() | |||
| 526 | + | // Dispatch without sending a new request after focus changes. | |||
| 527 | + | dispatched := make(chan error, 1) | |||
| 528 | + | go func() { | |||
| 529 | + | for { | |||
| 530 | + | if err := c.Dispatch(); err != nil { | |||
| 531 | + | dispatched <- err | |||
| 532 | + | return | |||
| 533 | + | } | |||
| 534 | + | } | |||
| 535 | + | }() | |||
| 536 | + | enter := func(want []byte) { | |||
| 537 | + | t.Helper() | |||
| 538 | + | select { | |||
| 539 | + | case got := <-kp.enters: | |||
| 540 | + | if !bytes.Equal(got, want) { | |||
| 541 | + | t.Fatalf("keyboard enter keys = %v, want %v", got, want) | |||
| 542 | + | } | |||
| 543 | + | case err := <-dispatched: | |||
| 544 | + | t.Fatal(err) | |||
| 545 | + | case <-time.After(2 * time.Second): | |||
| 546 | + | t.Fatal("keyboard enter timeout") | |||
| 547 | + | } | |||
| 548 | + | } | |||
| 549 | + | commands <- ports.FocusWindow{ID: a} | |||
| 550 | + | enter(nil) | |||
| 551 | + | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}} | |||
| 552 | + | commands <- ports.FocusWindow{ID: b} | |||
| 553 | + | enter([]byte{30, 0, 0, 0}) | |||
| 554 | + | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}} | |||
| 555 | + | commands <- ports.FocusWindow{ID: a} | |||
| 556 | + | enter(nil) | |||
| 557 | + | } | |||
M internal/adapters/wayland/server.go +14 -1
| @@ -50,6 +50,7 @@ type Server struct { | |||
| 50 | 50 | pointers map[server.Client][]*wayland.Pointer | |
| 51 | 51 | modState ports.ModState | |
| 52 | 52 | keyboards map[server.Client][]*wayland.Keyboard | |
| 53 | + | heldKeys map[uint32]bool | |
| 53 | 54 | keymapFD int | |
| 54 | 55 | keymapSize uint32 | |
| 55 | 56 | repeatRate, repeatDelay int | |
| @@ -327,6 +328,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 327 | 328 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | |
| 328 | 329 | return | |
| 329 | 330 | } | |
| 331 | + | if s.heldKeys == nil { | |
| 332 | + | s.heldKeys = make(map[uint32]bool) | |
| 333 | + | } | |
| 334 | + | if c.Key.Pressed { | |
| 335 | + | s.heldKeys[c.Key.Keycode] = true | |
| 336 | + | } else { | |
| 337 | + | delete(s.heldKeys, c.Key.Keycode) | |
| 338 | + | } | |
| 330 | 339 | s.serial++ | |
| 331 | 340 | state := uint32(0) | |
| 332 | 341 | if c.Key.Pressed { | |
| @@ -399,7 +408,11 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 399 | 408 | s.focused = id | |
| 400 | 409 | for _, k := range s.windowKeyboards(w) { | |
| 401 | 410 | s.serial++ | |
| 402 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{}) | |
| 411 | + | var keys []byte | |
| 412 | + | for code := range s.heldKeys { | |
| 413 | + | keys = binary.LittleEndian.AppendUint32(keys, code) | |
| 414 | + | } | |
| 415 | + | k.SendEnter(s.serial, w.xdg.surfaceResource(), keys) | |
| 403 | 416 | s.sendModifiers(k) | |
| 404 | 417 | } | |
| 405 | 418 | } | |
| @@ -50,6 +50,7 @@ type Server struct { | |||||
| 50 | pointers map[server.Client][]*wayland.Pointer | 50 | pointers map[server.Client][]*wayland.Pointer | ||
| 51 | modState ports.ModState | 51 | modState ports.ModState | ||
| 52 | keyboards map[server.Client][]*wayland.Keyboard | 52 | keyboards map[server.Client][]*wayland.Keyboard | ||
| 53 | + | heldKeys map[uint32]bool | |||
| 53 | keymapFD int | 54 | keymapFD int | ||
| 54 | keymapSize uint32 | 55 | keymapSize uint32 | ||
| 55 | repeatRate, repeatDelay int | 56 | repeatRate, repeatDelay int | ||
| @@ -327,6 +328,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 327 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | 328 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | ||
| 328 | return | 329 | return | ||
| 329 | } | 330 | } | ||
| 331 | + | if s.heldKeys == nil { | |||
| 332 | + | s.heldKeys = make(map[uint32]bool) | |||
| 333 | + | } | |||
| 334 | + | if c.Key.Pressed { | |||
| 335 | + | s.heldKeys[c.Key.Keycode] = true | |||
| 336 | + | } else { | |||
| 337 | + | delete(s.heldKeys, c.Key.Keycode) | |||
| 338 | + | } | |||
| 330 | s.serial++ | 339 | s.serial++ | ||
| 331 | state := uint32(0) | 340 | state := uint32(0) | ||
| 332 | if c.Key.Pressed { | 341 | if c.Key.Pressed { | ||
| @@ -399,7 +408,11 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 399 | s.focused = id | 408 | s.focused = id | ||
| 400 | for _, k := range s.windowKeyboards(w) { | 409 | for _, k := range s.windowKeyboards(w) { | ||
| 401 | s.serial++ | 410 | s.serial++ | ||
| 402 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{}) | 411 | + | var keys []byte |
| 412 | + | for code := range s.heldKeys { | |||
| 413 | + | keys = binary.LittleEndian.AppendUint32(keys, code) | |||
| 414 | + | } | |||
| 415 | + | k.SendEnter(s.serial, w.xdg.surfaceResource(), keys) | |||
| 403 | s.sendModifiers(k) | 416 | s.sendModifiers(k) | ||
| 404 | } | 417 | } | ||
| 405 | } | 418 | } | ||
M internal/app/app.go +4 -4
| @@ -30,8 +30,9 @@ type Options struct { | |||
| 30 | 30 | Timeout time.Duration | |
| 31 | 31 | NoTerminal bool | |
| 32 | 32 | ScreenshotDir string | |
| 33 | - | Script io.Reader | |
| 34 | - | testScenes chan<- ports.Scene | |
| 33 | + | // Run closes Script on shutdown; the reader goroutine exits after Close. | |
| 34 | + | Script io.ReadCloser | |
| 35 | + | testScenes chan<- ports.Scene | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | 38 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | |
| @@ -101,9 +102,8 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 101 | 102 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | |
| 102 | 103 | }() | |
| 103 | 104 | if opts.Script != nil { | |
| 104 | - | workers.Add(1) | |
| 105 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |
| 105 | 106 | go func() { | |
| 106 | - | defer workers.Done() | |
| 107 | 107 | defer close(script) | |
| 108 | 108 | scanner := bufio.NewScanner(opts.Script) | |
| 109 | 109 | for scanner.Scan() { | |
| @@ -30,8 +30,9 @@ type Options struct { | |||||
| 30 | Timeout time.Duration | 30 | Timeout time.Duration | ||
| 31 | NoTerminal bool | 31 | NoTerminal bool | ||
| 32 | ScreenshotDir string | 32 | ScreenshotDir string | ||
| 33 | - | Script io.Reader | 33 | + | // Run closes Script on shutdown; the reader goroutine exits after Close. |
| 34 | - | testScenes chan<- ports.Scene | 34 | + | Script io.ReadCloser |
| 35 | + | testScenes chan<- ports.Scene | |||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 37 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | 38 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | ||
| @@ -101,9 +102,8 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 101 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | 102 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | ||
| 102 | }() | 103 | }() | ||
| 103 | if opts.Script != nil { | 104 | if opts.Script != nil { | ||
| 104 | - | workers.Add(1) | 105 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() |
| 105 | go func() { | 106 | go func() { | ||
| 106 | - | defer workers.Done() | |||
| 107 | defer close(script) | 107 | defer close(script) | ||
| 108 | scanner := bufio.NewScanner(opts.Script) | 108 | scanner := bufio.NewScanner(opts.Script) | ||
| 109 | for scanner.Scan() { | 109 | for scanner.Scan() { | ||
M internal/app/app_test.go +18 -2
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "image/color" | |
| 6 | 6 | "image/png" | |
| 7 | + | "io" | |
| 7 | 8 | "os" | |
| 8 | 9 | "os/exec" | |
| 9 | 10 | "path/filepath" | |
| @@ -166,7 +167,7 @@ func TestHeadlessTyping(t *testing.T) { | |||
| 166 | 167 | cfg := config.Defaults() | |
| 167 | 168 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | |
| 168 | 169 | dir := t.TempDir() | |
| 169 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n"), Timeout: 6 * time.Second}) | |
| 170 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: io.NopCloser(strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n")), Timeout: 6 * time.Second}) | |
| 170 | 171 | if err != nil { | |
| 171 | 172 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | |
| 172 | 173 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -218,7 +219,7 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||
| 218 | 219 | cfg := config.Defaults() | |
| 219 | 220 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | |
| 220 | 221 | scenes := make(chan ports.Scene, 128) | |
| 221 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n"), Timeout: 5 * time.Second, testScenes: scenes}) | |
| 222 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes}) | |
| 222 | 223 | if err != nil { | |
| 223 | 224 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | |
| 224 | 225 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -241,3 +242,18 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||
| 241 | 242 | t.Fatalf("two columns=%v first focused=%v", two, focused) | |
| 242 | 243 | } | |
| 243 | 244 | } | |
| 245 | + | ||
| 246 | + | func TestBlockedScriptShutdown(t *testing.T) { | |
| 247 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |
| 248 | + | r, w := io.Pipe() | |
| 249 | + | defer w.Close() | |
| 250 | + | done := make(chan error, 1) | |
| 251 | + | go func() { | |
| 252 | + | done <- Run(context.Background(), Options{Backend: "headless", Config: config.Defaults(), NoTerminal: true, Script: r, Timeout: 300 * time.Millisecond}) | |
| 253 | + | }() | |
| 254 | + | select { | |
| 255 | + | case <-done: | |
| 256 | + | case <-time.After(2 * time.Second): | |
| 257 | + | t.Fatal("blocked script prevented shutdown") | |
| 258 | + | } | |
| 259 | + | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "image/color" | 5 | "image/color" | ||
| 6 | "image/png" | 6 | "image/png" | ||
| 7 | + | "io" | |||
| 7 | "os" | 8 | "os" | ||
| 8 | "os/exec" | 9 | "os/exec" | ||
| 9 | "path/filepath" | 10 | "path/filepath" | ||
| @@ -166,7 +167,7 @@ func TestHeadlessTyping(t *testing.T) { | |||||
| 166 | cfg := config.Defaults() | 167 | cfg := config.Defaults() | ||
| 167 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | 168 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | ||
| 168 | dir := t.TempDir() | 169 | dir := t.TempDir() | ||
| 169 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n"), Timeout: 6 * time.Second}) | 170 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: io.NopCloser(strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n")), Timeout: 6 * time.Second}) |
| 170 | if err != nil { | 171 | if err != nil { | ||
| 171 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | 172 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | ||
| 172 | t.Skipf("Vulkan unavailable: %v", err) | 173 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -218,7 +219,7 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||||
| 218 | cfg := config.Defaults() | 219 | cfg := config.Defaults() | ||
| 219 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | 220 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | ||
| 220 | scenes := make(chan ports.Scene, 128) | 221 | scenes := make(chan ports.Scene, 128) | ||
| 221 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n"), Timeout: 5 * time.Second, testScenes: scenes}) | 222 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes}) |
| 222 | if err != nil { | 223 | if err != nil { | ||
| 223 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | 224 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | ||
| 224 | t.Skipf("Vulkan unavailable: %v", err) | 225 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -241,3 +242,18 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||||
| 241 | t.Fatalf("two columns=%v first focused=%v", two, focused) | 242 | t.Fatalf("two columns=%v first focused=%v", two, focused) | ||
| 242 | } | 243 | } | ||
| 243 | } | 244 | } | ||
| 245 | + | ||||
| 246 | + | func TestBlockedScriptShutdown(t *testing.T) { | |||
| 247 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |||
| 248 | + | r, w := io.Pipe() | |||
| 249 | + | defer w.Close() | |||
| 250 | + | done := make(chan error, 1) | |||
| 251 | + | go func() { | |||
| 252 | + | done <- Run(context.Background(), Options{Backend: "headless", Config: config.Defaults(), NoTerminal: true, Script: r, Timeout: 300 * time.Millisecond}) | |||
| 253 | + | }() | |||
| 254 | + | select { | |||
| 255 | + | case <-done: | |||
| 256 | + | case <-time.After(2 * time.Second): | |||
| 257 | + | t.Fatal("blocked script prevented shutdown") | |||
| 258 | + | } | |||
| 259 | + | } | |||