jira-cli/pkg/tui/preview.go

294 lines
6.6 KiB
Go

package tui
import (
"os"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const sidebarMaxWidth = 60
// PreviewData is the data to be shown in preview layout.
type PreviewData struct {
Key string
Menu string
Contents func(string) interface{}
}
// Preview is the preview layout.
//
// It contains 2 tables internally, viz: sidebar and contents.
type Preview struct {
screen *Screen
painter *tview.Pages
sidebar *tview.Table
contents *Table
footer *tview.TextView
data []PreviewData
initialText string
footerText string
sidebarSelectedFunc SelectedFunc
contentsCache map[string]interface{}
}
// PreviewOption is a functional option that wraps preview properties.
type PreviewOption func(*Preview)
// NewPreview constructs a new preview layout.
func NewPreview(opts ...PreviewOption) *Preview {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
pv := Preview{
screen: NewScreen(),
sidebar: tview.NewTable(),
contents: NewTable(),
footer: tview.NewTextView(),
contentsCache: make(map[string]interface{}),
}
for _, opt := range opts {
opt(&pv)
}
pv.init()
return &pv
}
// WithInitialText sets initial text that is displayed in the contents screen.
func WithInitialText(text string) PreviewOption {
return func(p *Preview) {
p.initialText = text
}
}
// WithPreviewFooterText sets footer text that is displayed after the preview layout.
func WithPreviewFooterText(text string) PreviewOption {
return func(p *Preview) {
p.footerText = text
}
}
// WithSidebarSelectedFunc sets a function that is called when any option in sidebar is selected.
func WithSidebarSelectedFunc(fn SelectedFunc) PreviewOption {
return func(p *Preview) {
p.sidebarSelectedFunc = fn
}
}
// WithContentTableOpts sets contents table options.
func WithContentTableOpts(opts ...TableOption) PreviewOption {
return func(p *Preview) {
for _, opt := range opts {
opt(p.contents)
}
}
}
// Paint paints the preview layout.
func (pv *Preview) Paint(pd []PreviewData) error {
if len(pd) == 0 {
return errNoData
}
pv.data = pd
pv.sidebar.SetSelectionChangedFunc(func(r, c int) {
pv.contents.view.Clear()
pv.printText("Loading...")
go pv.renderContents(pd[r])
})
if pv.sidebarSelectedFunc != nil {
pv.sidebar.SetSelectedFunc(func(r, c int) {
if r > 0 {
pv.sidebarSelectedFunc(r, c, pd[r])
}
})
}
for i, d := range pd {
style := tcell.StyleDefault
if i == 0 {
style = style.Bold(true)
}
cell := tview.NewTableCell(pad(d.Menu, 1)).
SetMaxWidth(sidebarMaxWidth).
SetStyle(style)
pv.sidebar.SetCell(i, 0, cell)
}
pv.printText(pv.initialText)
return pv.screen.Paint(pv.painter)
}
func (pv *Preview) renderContents(pd PreviewData) {
if pd.Contents == nil {
pv.printText("No contents defined.")
return
}
if _, ok := pv.contentsCache[pd.Key]; !ok {
pv.contentsCache[pd.Key] = pd.Contents(pd.Key)
}
switch v := pv.contentsCache[pd.Key].(type) {
case string:
pv.printText(v)
case TableData:
data := pv.contentsCache[pd.Key].(TableData)
pv.screen.QueueUpdateDraw(func() {
pv.contents.view.Clear()
if len(data) == 1 {
pv.printText("No results to show.")
return
}
pv.contents.render(data)
})
}
}
func (pv *Preview) init() {
pv.initSidebar()
pv.initContents()
pv.initFooter()
grid := tview.NewGrid().
SetRows(0, 1, 2).
SetColumns(sidebarMaxWidth, 1, 0).
AddItem(pv.sidebar, 0, 0, 2, 1, 0, 0, true).
AddItem(pv.contents.view, 0, 2, 2, 1, 0, 0, false).
AddItem(pv.footer, 2, 0, 1, 3, 0, 0, false)
pv.painter = tview.NewPages().
AddPage("primary", grid, true, true).
AddPage("secondary", getInfoModal(), true, false)
pv.initLayout(pv.sidebar)
pv.initLayout(pv.contents.view)
}
func (pv *Preview) initSidebar() {
pv.sidebar.
SetSelectable(true, false).
SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
if ev.Key() == tcell.KeyTab {
pv.screen.SetFocus(pv.contents.view)
pv.contents.view.SetSelectable(true, false).Select(1, 0)
}
if ev.Key() == tcell.KeyRune {
switch ev.Rune() {
case 'q':
pv.screen.Stop()
os.Exit(0)
case 'w':
pv.screen.SetFocus(pv.contents.view)
pv.contents.view.SetSelectable(true, false).Select(1, 0)
default:
pv.contents.view.SetSelectable(false, false)
}
}
return ev
})
}
func (pv *Preview) initContents() {
pv.contents.view.
SetBorder(true).
SetBorderColor(tcell.ColorDarkGray).
SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
contents := func() interface{} {
sr, _ := pv.sidebar.GetSelection()
return pv.contentsCache[pv.data[sr].Key]
}
if ev.Key() == tcell.KeyCtrlK {
if pv.contents.copyKeyFunc == nil {
return ev
}
r, c := pv.contents.view.GetSelection()
pv.contents.copyKeyFunc(r, c, contents())
}
if ev.Key() == tcell.KeyTab {
pv.screen.SetFocus(pv.sidebar)
pv.contents.view.SetSelectable(false, false)
}
if ev.Key() == tcell.KeyRune {
switch ev.Rune() {
case 'q':
pv.screen.Stop()
os.Exit(0)
case 'w':
pv.screen.SetFocus(pv.sidebar)
pv.contents.view.SetSelectable(false, false)
case 'c':
if pv.contents.copyFunc == nil {
break
}
r, c := pv.contents.view.GetSelection()
pv.contents.copyFunc(r, c, contents())
case 'v':
if pv.contents.viewModeFunc == nil {
break
}
sr, _ := pv.sidebar.GetSelection()
r, c := pv.contents.view.GetSelection()
go func() {
func() {
pv.painter.ShowPage("secondary")
defer func() {
pv.painter.HidePage("secondary")
pv.screen.SetFocus(pv.contents.view)
}()
contents := pv.contentsCache[pv.data[sr].Key]
dataFn, renderFn := pv.contents.viewModeFunc(r, c, contents)
out, err := renderFn(dataFn())
if err == nil {
pv.screen.Suspend(func() { _ = PagerOut(out) })
}
}()
// Refresh the screen.
pv.screen.Draw()
}()
}
}
return ev
})
}
func (pv *Preview) initFooter() {
pv.footer.
SetWordWrap(true).
SetText(pad(pv.footerText, 1)).
SetTextColor(tcell.ColorDefault)
}
func (pv *Preview) initLayout(view *tview.Table) {
view.SetSelectedStyle(customTUIStyle(pv.contents.style)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc {
pv.screen.Stop()
}
})
view.SetFixed(1, int(pv.contents.colFixed))
}
func (pv *Preview) printText(s string) {
lines := splitText(s)
for i, line := range lines {
pv.contents.view.SetCell(i, 0, tview.NewTableCell(pad(line, 1)).
SetStyle(tcell.StyleDefault).
SetSelectable(false))
}
}