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 }