197 lines
4.6 KiB
Go
197 lines
4.6 KiB
Go
|
package tui
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/gdamore/tcell"
|
||
|
"github.com/mattn/go-runewidth"
|
||
|
|
||
|
"b612.me/apps/b612/bed/event"
|
||
|
"b612.me/apps/b612/bed/key"
|
||
|
"b612.me/apps/b612/bed/layout"
|
||
|
"b612.me/apps/b612/bed/mode"
|
||
|
"b612.me/apps/b612/bed/state"
|
||
|
)
|
||
|
|
||
|
// Tui implements UI
|
||
|
type Tui struct {
|
||
|
eventCh chan<- event.Event
|
||
|
mode mode.Mode
|
||
|
screen tcell.Screen
|
||
|
waitCh chan struct{}
|
||
|
mu *sync.Mutex
|
||
|
}
|
||
|
|
||
|
// NewTui creates a new Tui.
|
||
|
func NewTui() *Tui {
|
||
|
return &Tui{mu: new(sync.Mutex)}
|
||
|
}
|
||
|
|
||
|
// Init initializes the Tui.
|
||
|
func (ui *Tui) Init(eventCh chan<- event.Event) (err error) {
|
||
|
ui.mu.Lock()
|
||
|
defer ui.mu.Unlock()
|
||
|
ui.eventCh = eventCh
|
||
|
ui.mode = mode.Normal
|
||
|
if ui.screen, err = tcell.NewScreen(); err != nil {
|
||
|
return
|
||
|
}
|
||
|
ui.waitCh = make(chan struct{})
|
||
|
return ui.screen.Init()
|
||
|
}
|
||
|
|
||
|
// Run the Tui.
|
||
|
func (ui *Tui) Run(kms map[mode.Mode]*key.Manager) {
|
||
|
for {
|
||
|
e := ui.screen.PollEvent()
|
||
|
switch ev := e.(type) {
|
||
|
case *tcell.EventKey:
|
||
|
var e event.Event
|
||
|
if km, ok := kms[ui.getMode()]; ok {
|
||
|
e = km.Press(eventToKey(ev))
|
||
|
}
|
||
|
if e.Type != event.Nop {
|
||
|
ui.eventCh <- e
|
||
|
} else {
|
||
|
ui.eventCh <- event.Event{Type: event.Rune, Rune: ev.Rune()}
|
||
|
}
|
||
|
case *tcell.EventResize:
|
||
|
if ui.eventCh != nil {
|
||
|
ui.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case nil:
|
||
|
close(ui.waitCh)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) getMode() mode.Mode {
|
||
|
ui.mu.Lock()
|
||
|
defer ui.mu.Unlock()
|
||
|
return ui.mode
|
||
|
}
|
||
|
|
||
|
// Size returns the size for the screen.
|
||
|
func (ui *Tui) Size() (int, int) {
|
||
|
return ui.screen.Size()
|
||
|
}
|
||
|
|
||
|
// Redraw redraws the state.
|
||
|
func (ui *Tui) Redraw(s state.State) error {
|
||
|
ui.mu.Lock()
|
||
|
defer ui.mu.Unlock()
|
||
|
ui.mode = s.Mode
|
||
|
ui.screen.Clear()
|
||
|
ui.drawWindows(s.WindowStates, s.Layout)
|
||
|
ui.drawCmdline(s)
|
||
|
ui.screen.Show()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) setLine(line, offset int, str string, style tcell.Style) {
|
||
|
for _, c := range str {
|
||
|
ui.screen.SetContent(offset, line, c, nil, style)
|
||
|
offset += runewidth.RuneWidth(c)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) drawWindows(windowStates map[int]*state.WindowState, l layout.Layout) {
|
||
|
switch l := l.(type) {
|
||
|
case layout.Window:
|
||
|
r := fromLayout(l)
|
||
|
if ws, ok := windowStates[l.Index]; ok && r.valid() {
|
||
|
ui.newTuiWindow(r).drawWindow(ws,
|
||
|
l.Active && ui.mode != mode.Cmdline && ui.mode != mode.Search)
|
||
|
}
|
||
|
case layout.Horizontal:
|
||
|
ui.drawWindows(windowStates, l.Top)
|
||
|
ui.drawWindows(windowStates, l.Bottom)
|
||
|
case layout.Vertical:
|
||
|
ui.drawWindows(windowStates, l.Left)
|
||
|
ui.drawWindows(windowStates, l.Right)
|
||
|
ui.drawVerticalSplit(fromLayout(l.Left))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) newTuiWindow(region region) *tuiWindow {
|
||
|
return &tuiWindow{region: region, screen: ui.screen}
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) drawVerticalSplit(region region) {
|
||
|
for i := range region.height {
|
||
|
ui.setLine(region.top+i, region.left+region.width, "|", tcell.StyleDefault.Reverse(true))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) drawCmdline(s state.State) {
|
||
|
var cmdline string
|
||
|
style := tcell.StyleDefault
|
||
|
width, height := ui.Size()
|
||
|
switch {
|
||
|
case s.Error != nil:
|
||
|
cmdline = s.Error.Error()
|
||
|
if s.ErrorType == state.MessageInfo {
|
||
|
style = style.Foreground(tcell.ColorYellow)
|
||
|
} else {
|
||
|
style = style.Foreground(tcell.ColorRed)
|
||
|
}
|
||
|
case s.Mode == mode.Cmdline:
|
||
|
if len(s.CompletionResults) > 0 {
|
||
|
ui.drawCompletionResults(s.CompletionResults, s.CompletionIndex, width, height)
|
||
|
}
|
||
|
ui.screen.ShowCursor(1+runewidth.StringWidth(string(s.Cmdline[:s.CmdlineCursor])), height-1)
|
||
|
fallthrough
|
||
|
case s.PrevMode == mode.Cmdline && len(s.Cmdline) > 0:
|
||
|
cmdline = ":" + string(s.Cmdline)
|
||
|
case s.Mode == mode.Search:
|
||
|
ui.screen.ShowCursor(1+runewidth.StringWidth(string(s.Cmdline[:s.CmdlineCursor])), height-1)
|
||
|
fallthrough
|
||
|
case s.SearchMode != '\x00':
|
||
|
cmdline = string(s.SearchMode) + string(s.Cmdline)
|
||
|
default:
|
||
|
return
|
||
|
}
|
||
|
ui.setLine(height-1, 0, cmdline, style)
|
||
|
}
|
||
|
|
||
|
func (ui *Tui) drawCompletionResults(results []string, index, width, height int) {
|
||
|
var line bytes.Buffer
|
||
|
var left, right int
|
||
|
for i, result := range results {
|
||
|
size := runewidth.StringWidth(result) + 2
|
||
|
if i <= index {
|
||
|
left, right = right, right+size
|
||
|
if right > width {
|
||
|
line.Reset()
|
||
|
left, right = 0, size
|
||
|
}
|
||
|
} else if right < width {
|
||
|
right += size
|
||
|
} else {
|
||
|
break
|
||
|
}
|
||
|
line.WriteString(" ")
|
||
|
line.WriteString(result)
|
||
|
line.WriteString(" ")
|
||
|
}
|
||
|
line.WriteString(strings.Repeat(" ", max(width-right, 0)))
|
||
|
ui.setLine(height-2, 0, line.String(), tcell.StyleDefault.Reverse(true))
|
||
|
if index >= 0 {
|
||
|
ui.setLine(height-2, left, " "+results[index]+" ",
|
||
|
tcell.StyleDefault.Foreground(tcell.ColorGrey).Reverse(true))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Close terminates the Tui.
|
||
|
func (ui *Tui) Close() error {
|
||
|
ui.mu.Lock()
|
||
|
defer ui.mu.Unlock()
|
||
|
ui.eventCh = nil
|
||
|
ui.screen.Fini()
|
||
|
<-ui.waitCh
|
||
|
return nil
|
||
|
}
|