-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
90c752c
commit f48c8f1
Showing
29 changed files
with
4,955 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
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) | ||
} |
This file contains 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
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 | ||
} |
This file contains 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
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) | ||
} |
This file contains 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
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 | ||
} |
This file contains 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
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 | ||
} |
This file contains 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
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 | ||
} |
Oops, something went wrong.