star/bed/tui/tui.go

197 lines
4.6 KiB
Go
Raw Normal View History

2025-04-26 19:33:14 +08:00
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
}