package tui import ( "fmt" "git.milar.in/milarin/adverr" "git.milar.in/milarin/buf2d" "git.milar.in/milarin/ds" "github.com/gdamore/tcell" ) type Screen struct { EventTmpl scr tcell.Screen buf *ViewBuffer stopCh chan error redrawCh chan struct{} started bool modals ds.Stack[View] // Root is the root view which is currently shown on screen Root View } func NewScreen(root View) (*Screen, error) { scr, err := tcell.NewScreen() if err != nil { return nil, err } s := &Screen{ Root: root, scr: scr, stopCh: make(chan error, 1), redrawCh: make(chan struct{}, 1), modals: ds.NewArrayStack[View](), } s.KeyPressed = CloseOnCtrlC(s) return s, nil } func (s *Screen) Start() error { err := s.scr.Init() if err != nil { return err } defer s.scr.Fini() defer close(s.redrawCh) s.scr.EnableMouse() go s.eventloop() go s.drawloop() s.started = true return <-s.stopCh } func (s *Screen) Stop() { s.StopWithError(nil) } func (s *Screen) StopWithError(err error) { s.stopCh <- err } func (s *Screen) onKeyPressed(event *KeyEvent) { if !s.modals.Empty() { s.modals.Peek().OnKeyPressed(event) return } if s.KeyPressed == nil || !s.KeyPressed(event) { s.Root.OnKeyPressed(event) } s.Redraw() } func (s *Screen) onMouseEvent(event *MouseEvent) { if event.Button != MouseButtonNone { defer s.Redraw() } if !s.modals.Empty() { s.modals.Peek().OnMouseEvent(event) return } if s.MouseEvent != nil && s.MouseEvent(event) { return } s.Root.OnMouseEvent(event) } func convertMouseEvent(original *tcell.EventMouse) *MouseEvent { x, y := original.Position() return &MouseEvent{ Position: P(x, y), Button: convertMouseButton(original.Buttons()), Modifiers: original.Modifiers(), } } func (s *Screen) Redraw() { if s.started { s.redrawCh <- struct{}{} } } func (s *Screen) eventloop() { defer s.handlePanic("panicked while handling event") for evt := s.scr.PollEvent(); evt != nil; evt = s.scr.PollEvent() { switch event := evt.(type) { case *tcell.EventResize: s.Redraw() case *tcell.EventKey: s.startPanicSafeThread("panicked while handling key event", func() { s.onKeyPressed(event) }) case *tcell.EventMouse: s.startPanicSafeThread("panicked while handling mouse event", func() { s.onMouseEvent(convertMouseEvent(event)) }) default: s.StopWithError(fmt.Errorf("%#v", event)) } } } func (s *Screen) startPanicSafeThread(errorMessage string, f func()) { go func() { defer s.handlePanic(errorMessage) f() }() } func (s *Screen) drawloop() { defer s.handlePanic("panicked while redrawing") for range s.redrawCh { w, h := s.scr.Size() if s.buf == nil || s.buf.Width() != w || s.buf.Height() != h { s.buf = buf2d.NewBuffer(w, h, DefaultRune) } else { s.buf.Fill(DefaultRune) } // draw root view rw, rh := s.Root.Layout() s.Root.Draw(truncateBuffer(s.buf, rw, rh)) // draw modals for i := 0; i < s.modals.Size(); i++ { v := s.modals.PeekAt(i) mw, mh := v.Layout() v.Draw(s.buf.Sub(0, 0, iff(mw >= 0, mw, s.buf.Width()), iff(mh >= 0, mh, s.buf.Height()))) } // draw buffer onto screen drawBuffer(s.scr, s.buf) } } func (s *Screen) handlePanic(msg string) { if err := recover(); err != nil { if e, ok := err.(error); ok { s.StopWithError(adverr.Wrap(msg, e)) } else { s.StopWithError(adverr.Wrap(msg, fmt.Errorf("%v", err))) } } } func (s *Screen) OpenModal(v View) { s.modals.Push(v) s.Redraw() } func (s *Screen) CloseModal() { s.modals.Pop() s.Redraw() }