Skip to content

Commit

Permalink
feat: add input
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 8, 2024
1 parent 90c752c commit f48c8f1
Show file tree
Hide file tree
Showing 29 changed files with 4,955 additions and 0 deletions.
14 changes: 14 additions & 0 deletions input/cancelreader_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !windows
// +build !windows

package input

import (
"io"

"github.com/muesli/cancelreader"
)

func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r)
}
217 changes: 217 additions & 0 deletions input/cancelreader_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//go:build windows
// +build windows

package input

import (
"fmt"
"io"
"os"
"sync"
"time"

"github.com/erikgeiser/coninput"
"github.com/muesli/cancelreader"
"golang.org/x/sys/windows"
)

type conInputReader struct {
cancelMixin

conin windows.Handle
cancelEvent windows.Handle

originalMode uint32

// blockingReadSignal is used to signal that a blocking read is in progress.
blockingReadSignal chan struct{}
}

var _ cancelreader.CancelReader = &conInputReader{}

func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) {
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r)
}

var dummy uint32
if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
// If data was piped to the standard input, it does not emit events
// anymore. We can detect this if the console mode cannot be set anymore,
// in this case, we fallback to the default cancelreader implementation.
windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
return fallback(r)
}

conin, err := coninput.NewStdinHandle()
if err != nil {
return fallback(r)
}

originalMode, err := prepareConsole(conin,
windows.ENABLE_MOUSE_INPUT,
windows.ENABLE_WINDOW_INPUT,
windows.ENABLE_EXTENDED_FLAGS,
)
if err != nil {
return nil, fmt.Errorf("failed to prepare console input: %w", err)
}

cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return nil, fmt.Errorf("create stop event: %w", err)
}

return &conInputReader{
conin: conin,
cancelEvent: cancelEvent,
originalMode: originalMode,
blockingReadSignal: make(chan struct{}, 1),
}, nil
}

// Cancel implements cancelreader.CancelReader.
func (r *conInputReader) Cancel() bool {
r.setCanceled()

select {
case r.blockingReadSignal <- struct{}{}:
err := windows.SetEvent(r.cancelEvent)
if err != nil {
return false
}
<-r.blockingReadSignal
case <-time.After(100 * time.Millisecond):
// Read() hangs in a GetOverlappedResult which is likely due to
// WaitForMultipleObjects returning without input being available
// so we cannot cancel this ongoing read.
return false
}

return true
}

// Close implements cancelreader.CancelReader.
func (r *conInputReader) Close() error {
err := windows.CloseHandle(r.cancelEvent)
if err != nil {
return fmt.Errorf("closing cancel event handle: %w", err)
}

if r.originalMode != 0 {
err := windows.SetConsoleMode(r.conin, r.originalMode)
if err != nil {
return fmt.Errorf("reset console mode: %w", err)
}
}

return nil
}

// Read implements cancelreader.CancelReader.
func (r *conInputReader) Read(data []byte) (n int, err error) {
if r.isCanceled() {
return 0, cancelreader.ErrCanceled
}

err = waitForInput(r.conin, r.cancelEvent)
if err != nil {
return 0, err
}

if r.isCanceled() {
return 0, cancelreader.ErrCanceled
}

r.blockingReadSignal <- struct{}{}
n, err = overlappedReader(r.conin).Read(data)
<-r.blockingReadSignal

return
}

func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
err = windows.GetConsoleMode(input, &originalMode)
if err != nil {
return 0, fmt.Errorf("get console mode: %w", err)
}

newMode := coninput.AddInputModes(0, modes...)

err = windows.SetConsoleMode(input, newMode)
if err != nil {
return 0, fmt.Errorf("set console mode: %w", err)
}

return originalMode, nil
}

func waitForInput(conin, cancel windows.Handle) error {
event, err := windows.WaitForMultipleObjects([]windows.Handle{conin, cancel}, false, windows.INFINITE)
switch {
case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2:
if event == windows.WAIT_OBJECT_0+1 {
return cancelreader.ErrCanceled
}

if event == windows.WAIT_OBJECT_0 {
return nil
}

return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0)
case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2:
return fmt.Errorf("abandoned")
case event == uint32(windows.WAIT_TIMEOUT):
return fmt.Errorf("timeout")
case event == windows.WAIT_FAILED:
return fmt.Errorf("failed")
default:
return fmt.Errorf("unexpected error: %w", err)
}
}

// cancelMixin represents a goroutine-safe cancelation status.
type cancelMixin struct {
unsafeCanceled bool
lock sync.Mutex
}

func (c *cancelMixin) setCanceled() {
c.lock.Lock()
defer c.lock.Unlock()

c.unsafeCanceled = true
}

func (c *cancelMixin) isCanceled() bool {
c.lock.Lock()
defer c.lock.Unlock()

return c.unsafeCanceled
}

type overlappedReader windows.Handle

// Read performs an overlapping read fom a windows.Handle.
func (r overlappedReader) Read(data []byte) (int, error) {
hevent, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, fmt.Errorf("create event: %w", err)
}

overlapped := windows.Overlapped{HEvent: hevent}

var n uint32

err = windows.ReadFile(windows.Handle(r), data, &n, &overlapped)
if err != nil && err != windows.ERROR_IO_PENDING {
return int(n), err
}

err = windows.GetOverlappedResult(windows.Handle(r), &overlapped, &n, true)
if err != nil {
return int(n), nil
}

return int(n), nil
}
9 changes: 9 additions & 0 deletions input/clipboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package input

// ClipboardEvent is a clipboard read event.
type ClipboardEvent string

// String returns the string representation of the clipboard event.
func (e ClipboardEvent) String() string {
return string(e)
}
77 changes: 77 additions & 0 deletions input/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package input

import (
"fmt"
"image/color"
"strconv"
"strings"
)

// ForegroundColorEvent represents a foreground color change event.
type ForegroundColorEvent struct{ color.Color }

// String implements fmt.Stringer.
func (e ForegroundColorEvent) String() string {
return colorToHex(e)
}

// BackgroundColorEvent represents a background color change event.
type BackgroundColorEvent struct{ color.Color }

// String implements fmt.Stringer.
func (e BackgroundColorEvent) String() string {
return colorToHex(e)
}

// CursorColorEvent represents a cursor color change event.
type CursorColorEvent struct{ color.Color }

// String implements fmt.Stringer.
func (e CursorColorEvent) String() string {
return colorToHex(e)
}

type shiftable interface {
~uint | ~uint16 | ~uint32 | ~uint64
}

func shift[T shiftable](x T) T {
if x > 0xff {
x >>= 8
}
return x
}

func colorToHex(c color.Color) string {
r, g, b, _ := c.RGBA()
return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b))
}

func xParseColor(s string) color.Color {
switch {
case strings.HasPrefix(s, "rgb:"):
parts := strings.Split(s[4:], "/")
if len(parts) != 3 {
return color.Black
}

r, _ := strconv.ParseUint(parts[0], 16, 32)
g, _ := strconv.ParseUint(parts[1], 16, 32)
b, _ := strconv.ParseUint(parts[2], 16, 32)

return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), 255}
case strings.HasPrefix(s, "rgba:"):
parts := strings.Split(s[5:], "/")
if len(parts) != 4 {
return color.Black
}

r, _ := strconv.ParseUint(parts[0], 16, 32)
g, _ := strconv.ParseUint(parts[1], 16, 32)
b, _ := strconv.ParseUint(parts[2], 16, 32)
a, _ := strconv.ParseUint(parts[3], 16, 32)

return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), uint8(shift(a))}
}
return color.Black
}
10 changes: 10 additions & 0 deletions input/cursor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package input

// CursorPositionEvent represents a cursor position event.
type CursorPositionEvent struct {
// Row is the row number.
Row int

// Column is the column number.
Column int
}
18 changes: 18 additions & 0 deletions input/da1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package input

import "github.com/charmbracelet/x/ansi"

// PrimaryDeviceAttributesEvent represents a primary device attributes event.
type PrimaryDeviceAttributesEvent []uint

func parsePrimaryDevAttrs(csi *ansi.CsiSequence) Event {
// Primary Device Attributes
da1 := make(PrimaryDeviceAttributesEvent, len(csi.Params))
csi.Range(func(i int, p int, hasMore bool) bool {
if !hasMore {
da1[i] = uint(p)
}
return true
})
return da1
}
Loading

0 comments on commit f48c8f1

Please sign in to comment.