diff --git a/go.mod b/go.mod index cc52cd8..c41cca6 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( "github.com/mattn/go-isatty" v0.0.3 "github.com/mattn/go-runewidth" v0.0.2 "github.com/nsf/termbox-go" v0.0.0-20180129072728-88b7b944be8b + "github.com/gdamore/tcell" v1.0.0 ) diff --git a/lib/ui/borders.go b/lib/ui/borders.go index 08071ad..38b35fd 100644 --- a/lib/ui/borders.go +++ b/lib/ui/borders.go @@ -1,7 +1,7 @@ package ui import ( - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) const ( @@ -45,27 +45,23 @@ func (bordered *Bordered) Draw(ctx *Context) { y := 0 width := ctx.Width() height := ctx.Height() - cell := tb.Cell{ - Ch: ' ', - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } + style := tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack) if bordered.borders&BORDER_LEFT != 0 { - ctx.Fill(0, 0, 1, ctx.Height(), cell) + ctx.Fill(0, 0, 1, ctx.Height(), ' ', style) x += 1 width -= 1 } if bordered.borders&BORDER_TOP != 0 { - ctx.Fill(0, 0, ctx.Width(), 1, cell) + ctx.Fill(0, 0, ctx.Width(), 1, ' ', style) y += 1 height -= 1 } if bordered.borders&BORDER_RIGHT != 0 { - ctx.Fill(ctx.Width()-1, 0, 1, ctx.Height(), cell) + ctx.Fill(ctx.Width()-1, 0, 1, ctx.Height(), ' ', style) width -= 1 } if bordered.borders&BORDER_BOTTOM != 0 { - ctx.Fill(0, ctx.Height()-1, ctx.Width(), 1, cell) + ctx.Fill(0, ctx.Height()-1, ctx.Width(), 1, ' ', style) height -= 1 } subctx := ctx.Subcontext(x, y, width, height) diff --git a/lib/ui/context.go b/lib/ui/context.go index ca3f452..8031689 100644 --- a/lib/ui/context.go +++ b/lib/ui/context.go @@ -4,73 +4,77 @@ import ( "fmt" "github.com/mattn/go-runewidth" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/views" ) // A context allows you to draw in a sub-region of the terminal type Context struct { - x int - y int - width int - height int + viewport *views.ViewPort } func (ctx *Context) X() int { - return ctx.x + x, _, _, _ := ctx.viewport.GetPhysical() + return x } func (ctx *Context) Y() int { - return ctx.y + _, y, _, _ := ctx.viewport.GetPhysical() + return y } func (ctx *Context) Width() int { - return ctx.width + width, _ := ctx.viewport.Size() + return width } func (ctx *Context) Height() int { - return ctx.height + _, height := ctx.viewport.Size() + return height } -func NewContext(width, height int) *Context { - return &Context{0, 0, width, height} +func NewContext(width, height int, screen tcell.Screen) *Context { + vp := views.NewViewPort(screen, 0, 0, width, height) + return &Context{vp} } func (ctx *Context) Subcontext(x, y, width, height int) *Context { - if x+width > ctx.width || y+height > ctx.height { + vp_width, vp_height := ctx.viewport.Size() + if (x < 0 || y < 0) { + panic(fmt.Errorf("Attempted to create context with negative offset")) + } + if (x + width > vp_width || y + height > vp_height) { panic(fmt.Errorf("Attempted to create context larger than parent")) } - return &Context{ - x: ctx.x + x, - y: ctx.y + y, - width: width, - height: height, - } + vp := views.NewViewPort(ctx.viewport, x, y, width, height) + return &Context{vp} } -func (ctx *Context) SetCell(x, y int, ch rune, fg, bg tb.Attribute) { - if x >= ctx.width || y >= ctx.height { +func (ctx *Context) SetCell(x, y int, ch rune, style tcell.Style) { + width, height := ctx.viewport.Size() + if x >= width || y >= height { panic(fmt.Errorf("Attempted to draw outside of context")) } - tb.SetCell(ctx.x+x, ctx.y+y, ch, fg, bg) + crunes := []rune{} + ctx.viewport.SetContent(x, y, ch, crunes, style) } -func (ctx *Context) Printf(x, y int, ref tb.Cell, +func (ctx *Context) Printf(x, y int, style tcell.Style, format string, a ...interface{}) int { + width, height := ctx.viewport.Size() - if x >= ctx.width || y >= ctx.height { + if x >= width || y >= height { panic(fmt.Errorf("Attempted to draw outside of context")) } str := fmt.Sprintf(format, a...) - x += ctx.x - y += ctx.y old_x := x newline := func() bool { x = old_x y++ - return y < ctx.height + return y < height } for _, ch := range str { if str == " こんにちは " { @@ -84,9 +88,10 @@ func (ctx *Context) Printf(x, y int, ref tb.Cell, case '\r': x = old_x default: - tb.SetCell(x, y, ch, ref.Fg, ref.Bg) + crunes := []rune{} + ctx.viewport.SetContent(x, y, ch, crunes, style) x += runewidth.RuneWidth(ch) - if x == old_x+ctx.width { + if x == old_x + width { if !newline() { return runewidth.StringWidth(str) } @@ -97,13 +102,20 @@ func (ctx *Context) Printf(x, y int, ref tb.Cell, return runewidth.StringWidth(str) } -func (ctx *Context) Fill(x, y, width, height int, ref tb.Cell) { - _x := x - _y := y - for ; y < _y+height && y < ctx.height; y++ { - for ; x < _x+width && x < ctx.width; x++ { - ctx.SetCell(x, y, ref.Ch, ref.Fg, ref.Bg) - } - x = _x - } +//func (ctx *Context) Screen() tcell.Screen { +// return ctx.screen +//} + +func (ctx *Context) Fill(x, y, width, height int, rune rune, style tcell.Style) { + vp := views.NewViewPort(ctx.viewport, x, y, width, height) + vp.Fill(rune, style) +} + +func (ctx *Context) SetCursor(x, y int) { + // FIXME: Cursor needs to be set on tcell.Screen, or layout has to + // provide a CellModel + // cv := views.NewCellView() + // cv.Init() + // cv.SetView(ctx.viewport) + // cv.SetCursor(x, y) } diff --git a/lib/ui/fill.go b/lib/ui/fill.go index 3c6f0a5..4d36478 100644 --- a/lib/ui/fill.go +++ b/lib/ui/fill.go @@ -1,7 +1,7 @@ package ui import ( - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) type Fill rune @@ -13,7 +13,7 @@ func NewFill(f rune) Fill { func (f Fill) Draw(ctx *Context) { for x := 0; x < ctx.Width(); x += 1 { for y := 0; y < ctx.Height(); y += 1 { - ctx.SetCell(x, y, rune(f), tb.ColorDefault, tb.ColorDefault) + ctx.SetCell(x, y, rune(f), tcell.StyleDefault) } } } diff --git a/lib/ui/interactive.go b/lib/ui/interactive.go index efab828..2d4f099 100644 --- a/lib/ui/interactive.go +++ b/lib/ui/interactive.go @@ -1,17 +1,17 @@ package ui import ( - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) type Interactive interface { // Returns true if the event was handled by this component - Event(event tb.Event) bool + Event(event tcell.Event) bool } type Simulator interface { // Queues up the given input events for simulation - Simulate(events []tb.Event) + Simulate(events []tcell.Event) } type DrawableInteractive interface { diff --git a/lib/ui/stack.go b/lib/ui/stack.go index 9f81db8..3c66f5a 100644 --- a/lib/ui/stack.go +++ b/lib/ui/stack.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) type Stack struct { @@ -29,12 +29,7 @@ func (stack *Stack) Draw(ctx *Context) { if len(stack.children) > 0 { stack.Peek().Draw(ctx) } else { - cell := tb.Cell{ - Fg: tb.ColorDefault, - Bg: tb.ColorDefault, - Ch: ' ', - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) } } diff --git a/lib/ui/tab.go b/lib/ui/tab.go index e6a8aa5..d0635c7 100644 --- a/lib/ui/tab.go +++ b/lib/ui/tab.go @@ -1,7 +1,7 @@ package ui import ( - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) type Tabs struct { @@ -72,21 +72,14 @@ func (tabs *Tabs) Select(index int) { func (strip *TabStrip) Draw(ctx *Context) { x := 0 for i, tab := range strip.Tabs { - cell := tb.Cell{ - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } + style := tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack) if strip.Selected == i { - cell.Fg = tb.ColorDefault - cell.Bg = tb.ColorDefault + style = style.Reverse(true) } - x += ctx.Printf(x, 0, cell, " %s ", tab.Name) + x += ctx.Printf(x, 0, style, " %s ", tab.Name) } - cell := tb.Cell{ - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } - ctx.Fill(x, 0, ctx.Width()-x, 1, cell) + style := tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorBlack) + ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style) } func (strip *TabStrip) Invalidate() { diff --git a/lib/ui/text.go b/lib/ui/text.go index 6164837..e2e218c 100644 --- a/lib/ui/text.go +++ b/lib/ui/text.go @@ -2,7 +2,7 @@ package ui import ( "github.com/mattn/go-runewidth" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" ) const ( @@ -14,8 +14,8 @@ const ( type Text struct { text string strategy uint - fg tb.Attribute - bg tb.Attribute + fg tcell.Color + bg tcell.Color onInvalidate func(d Drawable) } @@ -35,7 +35,7 @@ func (t *Text) Strategy(strategy uint) *Text { return t } -func (t *Text) Color(fg tb.Attribute, bg tb.Attribute) *Text { +func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text { t.fg = fg t.bg = bg t.Invalidate() @@ -44,11 +44,6 @@ func (t *Text) Color(fg tb.Attribute, bg tb.Attribute) *Text { func (t *Text) Draw(ctx *Context) { size := runewidth.StringWidth(t.text) - cell := tb.Cell{ - Ch: ' ', - Fg: t.fg, - Bg: t.bg, - } x := 0 if t.strategy == TEXT_CENTER { x = (ctx.Width() - size) / 2 @@ -56,8 +51,9 @@ func (t *Text) Draw(ctx *Context) { if t.strategy == TEXT_RIGHT { x = ctx.Width() - size } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) - ctx.Printf(x, 0, cell, "%s", t.text) + style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Printf(x, 0, style, t.text) } func (t *Text) OnInvalidate(onInvalidate func(d Drawable)) { diff --git a/lib/ui/ui.go b/lib/ui/ui.go index e9b4e9b..d3eacf2 100644 --- a/lib/ui/ui.go +++ b/lib/ui/ui.go @@ -1,7 +1,7 @@ package ui import ( - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" "git.sr.ht/~sircmpwn/aerc2/config" ) @@ -10,30 +10,41 @@ type UI struct { Exit bool Content DrawableInteractive ctx *Context + screen tcell.Screen - tbEvents chan tb.Event + tcEvents chan tcell.Event invalidations chan interface{} } func Initialize(conf *config.AercConfig, content DrawableInteractive) (*UI, error) { - if err := tb.Init(); err != nil { + screen, err := tcell.NewScreen() + if err != nil { return nil, err } - width, height := tb.Size() + + if err = screen.Init(); err != nil { + return nil, err + } + + screen.Clear() + screen.HideCursor() + + width, height := screen.Size() + state := UI{ Content: content, - ctx: NewContext(width, height), + ctx: NewContext(width, height, screen), + screen: screen, - tbEvents: make(chan tb.Event, 10), + tcEvents: make(chan tcell.Event, 10), invalidations: make(chan interface{}), } - tb.SetInputMode(tb.InputEsc | tb.InputMouse) - tb.SetOutputMode(tb.Output256) + //tb.SetOutputMode(tb.Output256) go (func() { for !state.Exit { - state.tbEvents <- tb.PollEvent() + state.tcEvents <- screen.PollEvent() } })() go (func() { state.invalidations <- nil })() @@ -44,27 +55,28 @@ func Initialize(conf *config.AercConfig, } func (state *UI) Close() { - tb.Close() + state.screen.Fini() } func (state *UI) Tick() bool { select { - case event := <-state.tbEvents: - switch event.Type { - case tb.EventKey: + case event := <-state.tcEvents: + switch event := event.(type) { + case *tcell.EventKey: // TODO: temporary - if event.Key == tb.KeyEsc { + if event.Key() == tcell.KeyEsc { state.Exit = true } - case tb.EventResize: - tb.Clear(tb.ColorDefault, tb.ColorDefault) - state.ctx = NewContext(event.Width, event.Height) + case *tcell.EventResize: + state.screen.Clear() + width, height := event.Size() + state.ctx = NewContext(width, height, state.screen) state.Content.Invalidate() } state.Content.Event(event) case <-state.invalidations: state.Content.Draw(state.ctx) - tb.Flush() + state.screen.Show() default: return false } diff --git a/widgets/aerc.go b/widgets/aerc.go index 5563275..19ddfdd 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -5,7 +5,7 @@ import ( "log" "time" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" libui "git.sr.ht/~sircmpwn/aerc2/lib/ui" ) @@ -35,7 +35,7 @@ func NewAerc(logger *log.Logger) *Aerc { // TODO: move sidebar into tab content, probably grid.AddChild(libui.NewText("aerc"). Strategy(libui.TEXT_CENTER). - Color(tb.ColorBlack, tb.ColorWhite)) + Color(tcell.ColorBlack, tcell.ColorWhite)) // sidebar placeholder: grid.AddChild(libui.NewBordered( libui.NewFill('.'), libui.BORDER_RIGHT)).At(1, 0).Span(2, 1) @@ -75,10 +75,10 @@ func (aerc *Aerc) Draw(ctx *libui.Context) { aerc.grid.Draw(ctx) } -func (aerc *Aerc) Event(event tb.Event) bool { - switch event.Type { - case tb.EventKey: - if event.Ch == ':' { +func (aerc *Aerc) Event(event tcell.Event) bool { + switch event := event.(type) { + case *tcell.EventKey: + if event.Rune() == ':' { exline := NewExLine(func(command string) { aerc.statusline.Push(fmt.Sprintf("TODO: execute %s", command), 3 * time.Second) @@ -92,12 +92,10 @@ func (aerc *Aerc) Event(event tb.Event) bool { aerc.statusbar.Push(exline) return true } - fallthrough - default: - if aerc.interactive != nil { - return aerc.interactive.Event(event) - } else { - return false - } + } + if aerc.interactive != nil { + return aerc.interactive.Event(event) + } else { + return false } } diff --git a/widgets/exline.go b/widgets/exline.go index 0522371..de652ba 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -2,7 +2,7 @@ package widgets import ( "github.com/mattn/go-runewidth" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" "git.sr.ht/~sircmpwn/aerc2/lib/ui" ) @@ -40,15 +40,10 @@ func (ex *ExLine) Invalidate() { } func (ex *ExLine) Draw(ctx *ui.Context) { - cell := tb.Cell{ - Fg: tb.ColorDefault, - Bg: tb.ColorDefault, - Ch: ' ', - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) - ctx.Printf(0, 0, cell, ":%s", string(ex.command)) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Printf(0, 0, tcell.StyleDefault, ":%s", string(ex.command)) cells := runewidth.StringWidth(string(ex.command[:ex.index])) - tb.SetCursor(ctx.X()+cells+1, ctx.Y()) + ctx.SetCursor(cells + 1, 0) } func (ex *ExLine) insert(ch rune) { @@ -93,43 +88,41 @@ func (ex *ExLine) backspace() { } } -func (ex *ExLine) Event(event tb.Event) bool { - switch event.Type { - case tb.EventKey: - switch event.Key { - case tb.KeySpace: - ex.insert(' ') - case tb.KeyBackspace, tb.KeyBackspace2: +func (ex *ExLine) Event(event tcell.Event) bool { + switch event := event.(type) { + case *tcell.EventKey: + switch event.Key() { + case tcell.KeyBackspace, tcell.KeyBackspace2: ex.backspace() - case tb.KeyCtrlD, tb.KeyDelete: + case tcell.KeyCtrlD, tcell.KeyDelete: ex.deleteChar() - case tb.KeyCtrlB, tb.KeyArrowLeft: + case tcell.KeyCtrlB, tcell.KeyLeft: if ex.index > 0 { ex.index-- ex.Invalidate() } - case tb.KeyCtrlF, tb.KeyArrowRight: + case tcell.KeyCtrlF, tcell.KeyRight: if ex.index < len(ex.command) { ex.index++ ex.Invalidate() } - case tb.KeyCtrlA, tb.KeyHome: + case tcell.KeyCtrlA, tcell.KeyHome: ex.index = 0 ex.Invalidate() - case tb.KeyCtrlE, tb.KeyEnd: + case tcell.KeyCtrlE, tcell.KeyEnd: ex.index = len(ex.command) ex.Invalidate() - case tb.KeyCtrlW: + case tcell.KeyCtrlW: ex.deleteWord() - case tb.KeyEnter: - tb.HideCursor() + case tcell.KeyEnter: + //ex.ctx.Screen().HideCursor() ex.commit(string(ex.command)) - case tb.KeyEsc, tb.KeyCtrlC: - tb.HideCursor() + case tcell.KeyEsc, tcell.KeyCtrlC: + //ex.ctx.Screen().HideCursor() ex.cancel() default: - if event.Ch != 0 { - ex.insert(event.Ch) + if event.Rune() != 0 { + ex.insert(event.Rune()) } } } diff --git a/widgets/status.go b/widgets/status.go index bb87d33..3b4dbcc 100644 --- a/widgets/status.go +++ b/widgets/status.go @@ -3,7 +3,7 @@ package widgets import ( "time" - tb "github.com/nsf/termbox-go" + "github.com/gdamore/tcell" "git.sr.ht/~sircmpwn/aerc2/lib/ui" ) @@ -16,16 +16,16 @@ type StatusLine struct { } type StatusMessage struct { - bg tb.Attribute - fg tb.Attribute + bg tcell.Color + fg tcell.Color message string } func NewStatusLine() *StatusLine { return &StatusLine{ fallback: StatusMessage{ - bg: tb.ColorWhite, - fg: tb.ColorBlack, + bg: tcell.ColorWhite, + fg: tcell.ColorBlack, message: "Idle", }, } @@ -46,19 +46,15 @@ func (status *StatusLine) Draw(ctx *ui.Context) { if len(status.stack) != 0 { line = status.stack[len(status.stack)-1] } - cell := tb.Cell{ - Fg: line.fg, - Bg: line.bg, - Ch: ' ', - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) - ctx.Printf(0, 0, cell, "%s", line.message) + style := tcell.StyleDefault.Background(line.bg).Foreground(line.fg) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Printf(0, 0, style, "%s", line.message) } func (status *StatusLine) Set(text string) *StatusMessage { status.fallback = StatusMessage{ - bg: tb.ColorWhite, - fg: tb.ColorBlack, + bg: tcell.ColorWhite, + fg: tcell.ColorBlack, message: text, } status.Invalidate() @@ -67,8 +63,8 @@ func (status *StatusLine) Set(text string) *StatusMessage { func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage { msg := &StatusMessage{ - bg: tb.ColorWhite, - fg: tb.ColorBlack, + bg: tcell.ColorWhite, + fg: tcell.ColorBlack, message: text, } status.stack = append(status.stack, msg) @@ -85,7 +81,7 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage return msg } -func (msg *StatusMessage) Color(bg tb.Attribute, fg tb.Attribute) { +func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) { msg.bg = bg msg.fg = fg }