okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
2f6c51d
main
1package tui 2 3import ( 4"cmp" 5"slices" 6"strings" 7 8"github.com/charmbracelet/x/ansi" 9) 10 11// listFrame is the layout every list screen shares: the title rule, an optional 12// filter, the list beside the highlighted row's detail pane when there is 13// room, and the footer, all clipped to the terminal once. A screen supplies 14// the parts and none of the arithmetic. 15type listFrame struct { 16title ,right string // the title rule's two ends, styled 17filter string // the search line as drawn; empty for a screen without one 18list func (width ,height int ) []string 19// pane is the detail beside the list; nil for a list alone. 20pane func (width ,height int ) []string 21// paneWidth is the pane's width at a terminal width; nil is sidePane. 22paneWidth func (width int )int 23// summary is the detail's one-line stand-in when the pane has no room at all. 24summary string 25// footer is the finished bottom rows: hints, status, or a confirmation. 26footer string 27} 28 29// Side by side needs this much terminal; a narrower one with this many body 30// rows stacks the pane under the list instead, which gets the larger part. 31const ( 32paneMinWidth = 96 33paneMinHeight = 14 34stackMinBody = 12 35) 36 37// sidePane is a narrow detail column for facts about the highlighted row. 38func sidePane (width int )int {return min (max (width / 3 ,34 ),48 ) } 39 40// halfPane is half the terminal, for previews that read like text. 41func halfPane (width int )int {return width - width / 2 - ansi .StringWidth (svSep ()) } 42 43// view draws the frame at exactly the terminal's size or less; an unsized 44// terminal is 80 by 24. 45func (f listFrame )view (width ,height int )string { 46width ,height = cmp .Or (width ,80 ),cmp .Or (height ,24 ) 47roomy := height >= 12 48 49lines := []string {" " + titleRule (width - 1 ,f .title ,f .right )} 50if roomy { 51lines = append (lines ,"" ) 52 } 53if f .filter != "" { 54lines = append (lines ," " + f .filter ) 55if height >= 9 { 56lines = append (lines ,"" ) 57 } 58 } 59 60paned := f .pane != nil && width >= paneMinWidth && height >= paneMinHeight 61var tail []string 62if roomy { 63tail = append (tail ,"" ) 64 } 65tail = append (tail ,strings .Split (f .footer ,"\n" )... ) 66body := max (1 ,height - len (lines )- len (tail )) 67stacked := ! paned && f .pane != nil && body >= stackMinBody 68if ! paned && ! stacked && f .summary != "" && height >= 16 { 69tail = slices .Insert (tail ,len (tail )- strings .Count (f .footer ,"\n" )- 1 ," " + f .summary ) 70body = max (1 ,height - len (lines )- len (tail )) 71 } 72 73if paned { 74paneW := sidePane (width ) 75if f .paneWidth != nil { 76paneW = f .paneWidth (width ) 77 } 78list := f .list (width - paneW - ansi .StringWidth (svSep ()),body ) 79pane := f .pane (paneW ,body ) 80for i := range body { 81lines = append (lines ,list [i ]+ svSep ()+ pane [i ]) 82 } 83 }else if stacked { 84paneH := body * 2 / 5 85lines = append (lines ,f .list (width ,body - paneH - 1 )... ) 86lines = append (lines ," " + DefaultStyles .Decor .Render (strings .Repeat ("─" ,max (0 ,width - 2 )))) 87for _ ,line := range f .pane (width - 1 ,paneH ) { 88lines = append (lines ," " + line ) 89 } 90 }else { 91lines = append (lines ,f .list (width ,body )... ) 92 } 93lines = append (lines ,tail ... ) 94 95if len (lines )> height { 96lines = append (lines [:max (0 ,height - 1 )],lines [len (lines )- 1 ]) 97 } 98for i ,line := range lines { 99lines [i ]= ansi .Truncate (line ,width ,"…" ) 100 } 101return strings .Join (lines ,"\n" ) 102} 103 104// footerLine is the keys on the left and a status on the right. When both 105// do not fit, an urgent status (an error, a notice, work in flight) takes 106// a row of its own above the keys, and any other status gives way to them. 107func footerLine (width int ,hints []hint ,status string ,urgent bool )string { 108if len (hints )== 0 { 109return " " + status 110 } 111room := width - 1 112if status != "" { 113room = width - ansi .StringWidth (status )- 3 114 } 115left := fitHints (hints ,room ) 116switch { 117case left != "" && status != "" : 118return left + strings .Repeat (" " ,width - ansi .StringWidth (left )- ansi .StringWidth (status )- 1 )+ status 119case left != "" : 120return left 121 } 122keys := fitHints (hints ,width - 1 ) 123if keys == "" { 124keys = " " + ansi .Truncate (keyHints (hints ... ),max (1 ,width - 2 ),"…" ) 125 } 126if urgent && status != "" { 127return " " + ansi .Truncate (status ,max (1 ,width - 2 ),"…" )+ "\n" + keys 128 } 129return keys 130}