388 lines
6.8 KiB
Go
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
|
|
}
|