diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ce0c929 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/mum4k/termdash + +go 1.13 + +require ( + github.com/gdamore/tcell v1.3.1-0.20200206054723-bac2bbc5b394 + github.com/kylelemons/godebug v1.1.0 + github.com/mattn/go-runewidth v0.0.8 + github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be + golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect + golang.org/x/text v0.3.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b4baf5d --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.1-0.20200206054723-bac2bbc5b394 h1:jpZN87sd1rKHwYDWlCaRzmZklWa35Ft+O8rBBWFLQJ0= +github.com/gdamore/tcell v1.3.1-0.20200206054723-bac2bbc5b394/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be h1:yzmWtPyxEUIKdZg4RcPq64MfS8NA6A5fNOJgYhpR9EQ= +github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/terminal/tcell/cell_options.go b/terminal/tcell/cell_options.go new file mode 100644 index 0000000..5a5dd60 --- /dev/null +++ b/terminal/tcell/cell_options.go @@ -0,0 +1,21 @@ +package tcell + +import ( + "github.com/gdamore/tcell" + "github.com/mum4k/termdash/cell" +) + +// cellColor converts termdash cell color to the tcell format. +func cellColor(c cell.Color) tcell.Color { + return tcell.Color(int(c)&0x1ff) - 1 +} + +// cellOptsToStyle converts termdash cell color to the tcell format. +func cellOptsToStyle(opts *cell.Options) tcell.Style { + fg := cellColor(opts.FgColor) + bg := cellColor(opts.BgColor) + + st := tcell.StyleDefault + st = st.Foreground(fg).Background(bg) + return st +} diff --git a/terminal/tcell/cell_options_test.go b/terminal/tcell/cell_options_test.go new file mode 100644 index 0000000..acbacdb --- /dev/null +++ b/terminal/tcell/cell_options_test.go @@ -0,0 +1,35 @@ +package tcell + +import ( + "testing" + + "github.com/gdamore/tcell" + "github.com/mum4k/termdash/cell" +) + +func TestCellColor(t *testing.T) { + tests := []struct { + color cell.Color + want tcell.Color + }{ + {cell.ColorDefault, tcell.ColorDefault}, + {cell.ColorBlack, tcell.ColorBlack}, + {cell.ColorRed, tcell.ColorMaroon}, + {cell.ColorGreen, tcell.ColorGreen}, + {cell.ColorYellow, tcell.ColorOlive}, + {cell.ColorBlue, tcell.ColorNavy}, + {cell.ColorMagenta, tcell.ColorPurple}, + {cell.ColorCyan, tcell.ColorTeal}, + {cell.ColorWhite, tcell.ColorSilver}, + {cell.ColorNumber(42), tcell.Color(42)}, + } + + for _, tc := range tests { + t.Run(tc.color.String(), func(t *testing.T) { + got := cellColor(tc.color) + if got != tc.want { + t.Errorf("cellColor(%v) => got %v, want %v", tc.color, got, tc.want) + } + }) + } +} diff --git a/terminal/tcell/event.go b/terminal/tcell/event.go new file mode 100644 index 0000000..d8ad96d --- /dev/null +++ b/terminal/tcell/event.go @@ -0,0 +1,166 @@ +package tcell + +import ( + "image" + + "github.com/gdamore/tcell" + "github.com/mum4k/termdash/keyboard" + "github.com/mum4k/termdash/mouse" + "github.com/mum4k/termdash/terminal/terminalapi" +) + +// tcell representation of the space key +var tcellSpaceKey = tcell.Key(' ') + +// tcell representation of the tilde key +var tcellTildeKey = tcell.Key('~') + +// tcellToTd maps tcell key values to the termdash format. +var tcellToTd = map[tcell.Key]keyboard.Key{ + tcellSpaceKey: keyboard.KeySpace, + tcell.KeyF1: keyboard.KeyF1, + tcell.KeyF2: keyboard.KeyF2, + tcell.KeyF3: keyboard.KeyF3, + tcell.KeyF4: keyboard.KeyF4, + tcell.KeyF5: keyboard.KeyF5, + tcell.KeyF6: keyboard.KeyF6, + tcell.KeyF7: keyboard.KeyF7, + tcell.KeyF8: keyboard.KeyF8, + tcell.KeyF9: keyboard.KeyF9, + tcell.KeyF10: keyboard.KeyF10, + tcell.KeyF11: keyboard.KeyF11, + tcell.KeyF12: keyboard.KeyF12, + tcell.KeyInsert: keyboard.KeyInsert, + tcell.KeyDelete: keyboard.KeyDelete, + tcell.KeyHome: keyboard.KeyHome, + tcell.KeyEnd: keyboard.KeyEnd, + tcell.KeyPgUp: keyboard.KeyPgUp, + tcell.KeyPgDn: keyboard.KeyPgDn, + tcell.KeyUp: keyboard.KeyArrowUp, + tcell.KeyDown: keyboard.KeyArrowDown, + tcell.KeyLeft: keyboard.KeyArrowLeft, + tcell.KeyRight: keyboard.KeyArrowRight, + tcell.KeyEnter: keyboard.KeyEnter, + tcellTildeKey: keyboard.KeyCtrlTilde, + tcell.KeyCtrlA: keyboard.KeyCtrlA, + tcell.KeyCtrlB: keyboard.KeyCtrlB, + tcell.KeyCtrlC: keyboard.KeyCtrlC, + tcell.KeyCtrlD: keyboard.KeyCtrlD, + tcell.KeyCtrlE: keyboard.KeyCtrlE, + tcell.KeyCtrlF: keyboard.KeyCtrlF, + tcell.KeyCtrlG: keyboard.KeyCtrlG, + tcell.KeyCtrlJ: keyboard.KeyCtrlJ, + tcell.KeyCtrlK: keyboard.KeyCtrlK, + tcell.KeyCtrlL: keyboard.KeyCtrlL, + tcell.KeyCtrlN: keyboard.KeyCtrlN, + tcell.KeyCtrlO: keyboard.KeyCtrlO, + tcell.KeyCtrlP: keyboard.KeyCtrlP, + tcell.KeyCtrlQ: keyboard.KeyCtrlQ, + tcell.KeyCtrlR: keyboard.KeyCtrlR, + tcell.KeyCtrlS: keyboard.KeyCtrlS, + tcell.KeyCtrlT: keyboard.KeyCtrlT, + tcell.KeyCtrlU: keyboard.KeyCtrlU, + tcell.KeyCtrlV: keyboard.KeyCtrlV, + tcell.KeyCtrlW: keyboard.KeyCtrlW, + tcell.KeyCtrlX: keyboard.KeyCtrlX, + tcell.KeyCtrlY: keyboard.KeyCtrlY, + tcell.KeyCtrlZ: keyboard.KeyCtrlZ, + tcell.KeyBackspace: keyboard.KeyBackspace, + tcell.KeyTab: keyboard.KeyTab, + tcell.KeyEscape: keyboard.KeyEsc, + tcell.KeyCtrlBackslash: keyboard.KeyCtrlBackslash, + tcell.KeyCtrlRightSq: keyboard.KeyCtrlRsqBracket, + tcell.KeyCtrlUnderscore: keyboard.KeyCtrlUnderscore, + tcell.KeyBackspace2: keyboard.KeyBackspace2, +} + +// convKey converts a tcell keyboard event to the termdash format. +func convKey(event *tcell.EventKey) terminalapi.Event { + tcellKey := event.Key() + + if tcellKey == tcell.KeyRune { + ch := event.Rune() + return &terminalapi.Keyboard{ + Key: keyboard.Key(ch), + } + } + + k, ok := tcellToTd[tcellKey] + if !ok { + return terminalapi.NewErrorf("unknown keyboard key '%v' in a keyboard event", tcellKey) + } + + return &terminalapi.Keyboard{ + Key: k, + } +} + +// convMouse converts a tcell mouse event to the termdash format. +func convMouse(event *tcell.EventMouse) terminalapi.Event { + //var button mouse.Button + var button mouse.Button + x, y := event.Position() + + // Get wheel events + tcellBtn := event.Buttons() + if tcellBtn&tcell.WheelUp != 0 { + button = mouse.ButtonWheelUp + } else if tcellBtn&tcell.WheelDown != 0 { + button = mouse.ButtonWheelDown + } + + // Get button events + switch tcellBtn = event.Buttons(); tcellBtn { + case tcell.ButtonNone: + button = mouse.ButtonRelease + case tcell.Button1: + button = mouse.ButtonLeft + case tcell.Button2: + button = mouse.ButtonRight + case tcell.Button3: + button = mouse.ButtonMiddle + default: + return terminalapi.NewErrorf("unknown mouse key %v in a mouse event", tcellBtn) + } + + return &terminalapi.Mouse{ + Position: image.Point{X: x, Y: y}, + Button: button, + } +} + +// convResize converts a tcell resize event to the termdash format. +func convResize(event *tcell.EventResize) terminalapi.Event { + w, h := event.Size() + size := image.Point{X: w, Y: h} + if size.X < 0 || size.Y < 0 { + return terminalapi.NewErrorf("terminal resized to negative size: %v", size) + } + return &terminalapi.Resize{ + Size: size, + } +} + +// toTermdashEvents converts a tcell event to the termdash event format. +func toTermdashEvents(event tcell.Event) []terminalapi.Event { + switch event := event.(type) { + case *tcell.EventInterrupt: + return []terminalapi.Event{ + terminalapi.NewError("event type EventInterrupt isn't supported"), + } + case *tcell.EventKey: + return []terminalapi.Event{convKey(event)} + case *tcell.EventMouse: + return []terminalapi.Event{convMouse(event)} + case *tcell.EventResize: + return []terminalapi.Event{convResize(event)} + case *tcell.EventError: + return []terminalapi.Event{ + terminalapi.NewErrorf("encountered tcell error event: %v", event), + } + default: + return []terminalapi.Event{ + terminalapi.NewErrorf("unknown tcell event type: %v", event), + } + } +} diff --git a/terminal/tcell/tcell.go b/terminal/tcell/tcell.go new file mode 100644 index 0000000..c0fc932 --- /dev/null +++ b/terminal/tcell/tcell.go @@ -0,0 +1,136 @@ +package tcell + +import ( + "context" + "image" + + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/encoding" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/event/eventqueue" + "github.com/mum4k/termdash/terminal/terminalapi" +) + +// Terminal provides input and output to a real terminal. Wraps the +// gdamore/tcell terminal implementation. This object is not thread-safe. +// Implements terminalapi.Terminal. +type Terminal struct { + // events is a queue of input events. + events *eventqueue.Unbound + + // done gets closed when Close() is called. + done chan struct{} + + screen tcell.Screen +} + +// New returns a new tcell based Terminal. +// Call Close() when the terminal isn't required anymore. +func New() (*Terminal, error) { + // Enable full character set support for tcell + encoding.Register() + + screen, err := tcell.NewScreen() + if err != nil { + return nil, err + } + + t := &Terminal{ + events: eventqueue.New(), + done: make(chan struct{}), + screen: screen, + } + + if err = t.screen.Init(); err != nil { + return nil, err + } + + defaultStyle := tcell.StyleDefault. + Foreground(tcell.ColorWhite). + Background(tcell.ColorBlack) + + t.screen.EnableMouse() + t.screen.SetStyle(defaultStyle) + + go t.pollEvents() + return t, nil +} + +// Size implements terminalapi.Terminal.Size. +func (t *Terminal) Size() image.Point { + w, h := t.screen.Size() + return image.Point{ + X: w, + Y: h, + } +} + +// Clear implements terminalapi.Terminal.Clear. +func (t *Terminal) Clear(opts ...cell.Option) error { + o := cell.NewOptions(opts...) + st := cellOptsToStyle(o) + w, h := t.screen.Size() + for row := 0; row < h; row++ { + for col := 0; col < w; col++ { + t.screen.SetContent(col, row, ' ', nil, st) + } + } + return nil +} + +// Flush implements terminalapi.Terminal.Flush. +func (t *Terminal) Flush() error { + t.screen.Show() + return nil +} + +// SetCursor implements terminalapi.Terminal.SetCursor. +func (t *Terminal) SetCursor(p image.Point) { + t.screen.ShowCursor(p.X, p.Y) +} + +// HideCursor implements terminalapi.Terminal.HideCursor. +func (t *Terminal) HideCursor() { + t.screen.HideCursor() +} + +// SetCell implements terminalapi.Terminal.SetCell. +func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error { + o := cell.NewOptions(opts...) + st := cellOptsToStyle(o) + t.screen.SetContent(p.X, p.Y, r, nil, st) + return nil +} + +// pollEvents polls and enqueues the input events. +func (t *Terminal) pollEvents() { + for { + select { + case <-t.done: + return + default: + } + + events := toTermdashEvents(t.screen.PollEvent()) + for _, ev := range events { + t.events.Push(ev) + } + } +} + +// Event implements terminalapi.Terminal.Event. +func (t *Terminal) Event(ctx context.Context) terminalapi.Event { + ev := t.events.Pull(ctx) + if ev == nil { + return nil + } + return ev +} + +// Close closes the terminal, should be called when the terminal isn't required +// anymore to return the screen to a sane state. +// Implements terminalapi.Terminal.Close. +func (t *Terminal) Close() { + close(t.done) + t.screen.Fini() +}