talc/pkg/menu/menu_view.go
2020-03-18 05:16:25 +01:00

388 lines
6.8 KiB
Go

package menu
import (
"os"
"os/exec"
"runtime"
"sync"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/views"
runewidth "github.com/mattn/go-runewidth"
)
type MenuView struct {
app *views.Application
style tcell.Style
selectedStyle tcell.Style
handleEvents bool
model MenuModel
view views.View
port *views.ViewPort
views.WidgetWatchers
once sync.Once
}
func (mv *MenuView) HandleEvent(e tcell.Event) bool {
if !mv.handleEvents {
return true
}
if mv.model == nil {
return false
}
switch e := e.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyBackspace, tcell.KeyBackspace2:
mv.keyBack()
return true
case tcell.KeyEnter:
mv.keyEnter()
return true
case tcell.KeyUp, tcell.KeyCtrlP:
mv.keyUp()
return true
case tcell.KeyDown, tcell.KeyCtrlN:
mv.keyDown()
return true
case tcell.KeyLeft:
mv.keyBack()
return true
case tcell.KeyRight:
mv.keyEnter()
return true
case tcell.KeyPgDn, tcell.KeyCtrlF:
mv.keyPgDn()
return true
case tcell.KeyPgUp, tcell.KeyCtrlB:
mv.keyPgUp()
return true
case tcell.KeyEnd:
mv.keyEnd()
return true
case tcell.KeyHome:
mv.keyHome()
return true
case tcell.KeyRune:
switch e.Rune() {
case 'h':
mv.keyBack()
return true
case 'j':
mv.keyDown()
return true
case 'k':
mv.keyUp()
return true
case 'l':
mv.keyEnter()
return true
case 'g':
mv.keyHome()
return true
case 'G':
mv.keyEnd()
return true
case 'q':
c := mv.model.GetCurrent()
p := c.parent
if p == nil {
mv.keyBack()
return true
}
return true
}
}
}
return false
}
func (mv *MenuView) Draw() {
port := mv.port
port.Fill(' ', mv.style)
model := mv.model
if mv.view == nil {
return
}
if model == nil {
return
}
vw, vh := mv.view.Size()
for y := 0; y < vh; y++ {
for x := 0; x < vw; x++ {
mv.view.SetContent(x, y, ' ', nil, mv.style)
}
}
_, ey := model.GetBounds()
_, vy := port.Size()
if ey < vy {
ey = vy
}
cy, en, sh := mv.model.GetCursor()
for y := 0; y < ey; y++ {
opt := mv.model.GetOption(y)
style := mv.style
if en && y == cy && sh {
style = mv.selectedStyle
}
puts(port, style, 0, y, opt.String())
}
}
func puts(s views.View, style tcell.Style, x, y int, str string) {
i := 0
var deferred []rune
dwidth := 0
zwj := false
for _, r := range str {
if r == '\u200d' {
if len(deferred) == 0 {
deferred = append(deferred, ' ')
dwidth = 1
}
deferred = append(deferred, r)
zwj = true
continue
}
if zwj {
deferred = append(deferred, r)
zwj = false
continue
}
switch runewidth.RuneWidth(r) {
case 0:
if len(deferred) == 0 {
deferred = append(deferred, ' ')
dwidth = 1
}
case 1:
if len(deferred) != 0 {
s.SetContent(x+i, y, deferred[0], deferred[1:], style)
i += dwidth
}
deferred = nil
dwidth = 1
case 2:
if len(deferred) != 0 {
s.SetContent(x+i, y, deferred[0], deferred[1:], style)
i += dwidth
}
deferred = nil
dwidth = 2
}
deferred = append(deferred, r)
}
if len(deferred) != 0 {
s.SetContent(x+i, y, deferred[0], deferred[1:], style)
i += dwidth
}
}
func (mv *MenuView) keyBack() {
cur := mv.model.GetCurrent()
if cur.parent == nil {
return
}
mv.model.SetCurrent(*cur.parent)
w, h := mv.model.GetBounds()
mv.port.SetContentSize(w, h, true)
mv.model.SetCursor(cur.y)
mv.port.Center(0, cur.y)
}
func (mv *MenuView) keyEnter() {
var y int
var en bool
if y, en, _ = mv.model.GetCursor(); !en {
return
}
y, _, _ = mv.model.GetCursor()
cur := mv.model.GetCurrent()
opt := mv.model.GetOption(y)
opt.y = y
opt.parent = &cur
switch opt.Action {
case ActionMenu:
if len(opt.Options) == 0 {
return
}
mv.model.SetCurrent(opt)
mv.model.SetCursor(0)
w, h := mv.model.GetBounds()
mv.port.SetContentSize(w, h, true)
case ActionExec:
if len(opt.Args) == 0 {
return
}
cmd := exec.Command(opt.Args[0], opt.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
mv.handleEvents = false
runtime.LockOSThread()
_ = cmd.Run()
runtime.UnlockOSThread()
mv.handleEvents = true
mv.port.Clear()
mv.app.Refresh()
}
}
func (mv *MenuView) keyUp() {
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollUp(1)
return
}
mv.model.MoveCursor(-1)
mv.MakeCursorVisible()
}
func (mv *MenuView) keyDown() {
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollDown(1)
return
}
mv.model.MoveCursor(1)
mv.MakeCursorVisible()
}
func (mv *MenuView) keyPgUp() {
_, vy := mv.port.Size()
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollUp(vy)
return
}
mv.model.MoveCursor(-vy)
mv.MakeCursorVisible()
}
func (mv *MenuView) keyPgDn() {
_, vy := mv.port.Size()
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollDown(vy)
return
}
mv.model.MoveCursor(+vy)
mv.MakeCursorVisible()
}
func (mv *MenuView) keyHome() {
vx, vy := mv.model.GetBounds()
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollUp(vy)
mv.port.ScrollLeft(vx)
return
}
mv.model.SetCursor(0)
mv.MakeCursorVisible()
}
func (mv *MenuView) keyEnd() {
vx, vy := mv.model.GetBounds()
if _, en, _ := mv.model.GetCursor(); !en {
mv.port.ScrollDown(vy)
mv.port.ScrollRight(vx)
return
}
mv.model.SetCursor(vy)
mv.MakeCursorVisible()
}
func (mv *MenuView) MakeCursorVisible() {
if mv.model == nil {
return
}
y, enabled, _ := mv.model.GetCursor()
if enabled {
mv.MakeVisible(y)
}
}
func (mv *MenuView) Size() (int, int) {
w, h := mv.model.GetBounds()
if w > 2 {
w = 2
}
if h > 2 {
h = 2
}
return w, h
}
func (mv *MenuView) SetModel(model MenuModel) {
w, h := model.GetBounds()
model.SetCursor(0)
mv.model = model
mv.port.SetContentSize(w, h, true)
mv.port.ValidateView()
mv.PostEventWidgetContent(mv)
}
func (mv *MenuView) SetView(view views.View) {
port := mv.port
port.SetView(view)
mv.view = view
if view == nil {
return
}
width, height := view.Size()
mv.port.Resize(0, 0, width, height)
if mv.model != nil {
w, h := mv.model.GetBounds()
mv.port.SetContentSize(w, h, true)
}
mv.Resize()
}
func (mv *MenuView) Resize() {
width, height := mv.view.Size()
mv.port.Resize(0, 0, width, height)
mv.port.ValidateView()
mv.MakeCursorVisible()
}
func (mv *MenuView) SetCursor(y int) {
mv.model.SetCursor(y)
}
func (mv *MenuView) MakeVisible(y int) {
mv.port.MakeVisible(0, y)
}
func (mv *MenuView) SetStyle(s tcell.Style) {
mv.style = s
}
func (mv *MenuView) SetSelectedStyle(s tcell.Style) {
mv.selectedStyle = s
}
func (mv *MenuView) Init() {
mv.selectedStyle = mv.style.Reverse(true)
mv.handleEvents = true
mv.once.Do(func() {
mv.port = views.NewViewPort(nil, 0, 0, 0, 0)
mv.style = tcell.StyleDefault
})
}
func NewMenuView() *MenuView {
mv := &MenuView{}
mv.Init()
return mv
}