diff options
author | dwillist <dthornton@vmware.com> | 2021-01-17 02:18:11 -0500 |
---|---|---|
committer | dwillist <dthornton@vmware.com> | 2021-01-17 02:18:11 -0500 |
commit | 90cc80430ee6e215dfad48cf0718414835e1791f (patch) | |
tree | 6eb180a67b91c6c982fc491d1b204085564b3fc6 | |
parent | d2e5ac1e5ab9bca20acc5ac0b30336e5be4c3010 (diff) |
add keybinding menu
- update displayed keybindings as focus changes
- highlight toggleable keys
Signed-off-by: dwillist <dthornton@vmware.com>
-rw-r--r-- | = | 11 | ||||
-rw-r--r-- | runtime/ui/app.go | 48 | ||||
-rw-r--r-- | runtime/ui/components/cnb_layer_details_view.go | 5 | ||||
-rw-r--r-- | runtime/ui/components/dive_application.go | 56 | ||||
-rw-r--r-- | runtime/ui/components/filetree_primative.go | 30 | ||||
-rw-r--r-- | runtime/ui/components/filter_primative.go | 4 | ||||
-rw-r--r-- | runtime/ui/components/image_details_view.go | 4 | ||||
-rw-r--r-- | runtime/ui/components/key_config.go | 8 | ||||
-rw-r--r-- | runtime/ui/components/keybinding_primitive.go | 89 | ||||
-rw-r--r-- | runtime/ui/components/layer_details_view.go | 5 | ||||
-rw-r--r-- | runtime/ui/components/layers_primative.go | 12 | ||||
-rw-r--r-- | runtime/ui/components/visible_grid.go | 40 | ||||
-rw-r--r-- | runtime/ui/components/wrapper_primative.go | 7 | ||||
-rw-r--r-- | runtime/ui/format/format.go | 2 |
14 files changed, 287 insertions, 34 deletions
@@ -0,0 +1,11 @@ +package componenets + +import "github.com/rivo/tview" + +type Keybinded interface { + GetKeyBindings() []KeyBinding +} + +type KeyMenuPrimitive struct { + *tview.TextView +} diff --git a/runtime/ui/app.go b/runtime/ui/app.go index dfe8063..a7d013a 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -17,21 +17,22 @@ const debug = false // type global var ( - once sync.Once - appSingleton *diveApp + once sync.Once + uiSingleton *UI ) -type diveApp struct { - app *tview.Application +type UI struct { + app *components.DiveApplication layers tview.Primitive fileTree tview.Primitive filterView tview.Primitive } -func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetree.Comparer, isCNB bool) (*diveApp, error) { +func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetree.Comparer, isCNB bool) (*UI, error) { var err error once.Do(func() { config := components.NewKeyConfig() + diveApplication := components.NewDiveApplication(app) // ensure the background color is inherited from the terminal emulator //tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault @@ -41,6 +42,8 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr filterViewModel := viewmodels.NewFilterViewModel(nil) var layerModel viewmodels.LayersModel var layerDetailsBox *components.Wrapper + + // TODO extract the CNB specific logic here for now... if isCNB { cnbLayerViewModel := viewmodels.NewCNBLayersViewModel(analysis.Layers, analysis.BOMMapping) cnbLayerDetailsView := components.NewCNBLayerDetailsView(cnbLayerViewModel).Setup() @@ -75,6 +78,8 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr fileTreeView = fileTreeView.Setup(config) fileTreeBox := components.NewWrapper("Current Layer Contents", "subtitle!", fileTreeView).Setup() + keyMenuView := components.NewKeyMenuView() + // Implementation notes: should we factor out this setup?? // Probably yes, but difficult to make this both easy to setup & mutable leftVisibleGrid := components.NewVisibleFlex() @@ -82,6 +87,11 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr rightVisibleGrid := components.NewVisibleFlex() rightVisibleGrid.SetDirection(tview.FlexRow) totalVisibleGrid := components.NewVisibleFlex() + gridWithFooter := tview.NewGrid(). + SetRows(0, 1). + SetColumns(0). + AddItem(totalVisibleGrid, 0, 0, 1, 1, 0, 0, true). + AddItem(keyMenuView, 1, 0, 1, 1, 0, 0, false) leftVisibleGrid.AddItem(layersBox, 0, 3, true). AddItem(layerDetailsBox, 0, 1, false). @@ -96,12 +106,14 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr totalVisibleGrid.AddItem(leftVisibleGrid, 0, 1, true). AddItem(rightVisibleGrid, 0, 1, false) - appSingleton = &diveApp{ - app: app, + uiSingleton = &UI{ + app: diveApplication, fileTree: fileTreeBox, layers: layersBox, } + keyMenuView.AddBoundViews(diveApplication) + quitBinding, err := config.GetKeyBinding("keybinding.quit") if err != nil { // TODO handle this as an error @@ -118,6 +130,8 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr // TODO handle this as an error panic(err) } + diveApplication.AddBindings(quitBinding, filterBinding, switchBinding) + diveApplication.AddBoundViews(fileTreeBox, layersBox, filterView) switchFocus := func(event *tcell.EventKey) *tcell.EventKey { var result *tcell.EventKey = nil @@ -125,17 +139,17 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr case quitBinding.Match(event): app.Stop() case switchBinding.Match(event): - if appSingleton.app.GetFocus() == appSingleton.layers { - appSingleton.app.SetFocus(appSingleton.fileTree) + if diveApplication.GetFocus() == uiSingleton.layers { + diveApplication.SetFocus(uiSingleton.fileTree) } else { - appSingleton.app.SetFocus(appSingleton.layers) + diveApplication.SetFocus(uiSingleton.layers) } case filterBinding.Match(event): if filterView.HasFocus() { filterView.Blur() - appSingleton.app.SetFocus(fileTreeBox) + diveApplication.SetFocus(fileTreeBox) } else { - appSingleton.app.SetFocus(filterView) + diveApplication.SetFocus(filterView) } default: @@ -144,13 +158,13 @@ func newApp(app *tview.Application, analysis *image.AnalysisResult, cache filetr return result } - totalVisibleGrid.SetInputCapture(switchFocus) + diveApplication.SetInputCapture(switchFocus) - app.SetRoot(totalVisibleGrid, true) - app.SetFocus(totalVisibleGrid) + diveApplication.SetRoot(gridWithFooter, true) + diveApplication.SetFocus(gridWithFooter) }) - return appSingleton, err + return uiSingleton, err } // Run is the UI entrypoint. @@ -174,7 +188,7 @@ func Run(analysis *image.AnalysisResult, treeStack filetree.Comparer, isCNB bool return err } - if err = appSingleton.app.Run(); err != nil { + if err = uiSingleton.app.Run(); err != nil { zap.S().Info("app error: ", err.Error()) return err } diff --git a/runtime/ui/components/cnb_layer_details_view.go b/runtime/ui/components/cnb_layer_details_view.go index 1840bd1..4987a23 100644 --- a/runtime/ui/components/cnb_layer_details_view.go +++ b/runtime/ui/components/cnb_layer_details_view.go @@ -43,6 +43,11 @@ func (lv *CNBLayerDetailsView) Draw(screen tcell.Screen) { lv.TextView.Draw(screen) } + +func (lv *CNBLayerDetailsView) GetKeyBindings() []KeyBindingDisplay { + return []KeyBindingDisplay {} +} + func layerCNBDetailsText(layer *image.Layer, bom lifecycle.BOMEntry) string { lines := []string{} if layer.Names != nil && len(layer.Names) > 0 { diff --git a/runtime/ui/components/dive_application.go b/runtime/ui/components/dive_application.go new file mode 100644 index 0000000..e3dcacc --- /dev/null +++ b/runtime/ui/components/dive_application.go @@ -0,0 +1,56 @@ +package components + +import ( + "github.com/rivo/tview" + "github.com/sirupsen/logrus" +) + +type DiveApplication struct { + *tview.Application + + boundList []BoundView + + // todo remove this + bindings []KeyBinding +} + +func NewDiveApplication(app *tview.Application) *DiveApplication { + return &DiveApplication{ + Application: app, + boundList: []BoundView{}, + } +} + +func (d *DiveApplication) GetKeyBindings() []KeyBindingDisplay { + result := []KeyBindingDisplay{} + for i := 0; i < len(d.bindings); i++ { + binding := d.bindings[i] + logrus.Debug("adding keybinding with name %s", binding.Display) + result = append(result, KeyBindingDisplay{KeyBinding: &binding, Selected: false}) + } + + for _, bound := range d.boundList { + if bound.HasFocus() { + result = append(result, bound.GetKeyBindings()...) + } + } + + return result +} + +func (d *DiveApplication) AddBindings(bindings ...KeyBinding) *DiveApplication { + d.bindings = append(d.bindings, bindings...) + + return d +} + +func (d *DiveApplication) AddBoundViews(views ...BoundView) *DiveApplication { + d.boundList = append(d.boundList, views...) + + return d +} + +// Application always has focus +func (d *DiveApplication) HasFocus() bool { + return true +} diff --git a/runtime/ui/components/filetree_primative.go b/runtime/ui/components/filetree_primative.go index 4f7b3cb..479c872 100644 --- a/runtime/ui/components/filetree_primative.go +++ b/runtime/ui/components/filetree_primative.go @@ -37,7 +37,7 @@ type TreeView struct { inputHandler func(event *tcell.EventKey, setFocus func(p tview.Primitive)) - keyBindings map[string]KeyBinding + bindingArray []KeyBindingDisplay showAttributes bool } @@ -75,8 +75,20 @@ func (t *TreeView) Setup(config KeyBindingConfig) *TreeView { "keybinding.page-down": func() bool { return t.pageDown() }, } - bindingArray := []KeyBinding{} + bindingUpdateSettings := map[string]bool{ + "keybinding.toggle-collapse-dir": false, + "keybinding.toggle-collapse-all-dir": false, + "keybinding.toggle-filetree-attributes": true, + "keybinding.toggle-added-files": true, + "keybinding.toggle-removed-files": true, + "keybinding.toggle-modified-files": true, + "keybinding.toggle-unmodified-files": true, + "keybinding.page-up": false, + "keybinding.page-down": false, + } + actionArray := []keyAction{} + updateArray := []bool{} for keybinding, action := range bindingSettings { binding, err := config.GetKeyBinding(keybinding) @@ -85,8 +97,9 @@ func (t *TreeView) Setup(config KeyBindingConfig) *TreeView { // TODO handle this error //return nil } - bindingArray = append(bindingArray, binding) + t.bindingArray = append(t.bindingArray, KeyBindingDisplay{KeyBinding: &binding, Selected: false}) actionArray = append(actionArray, action) + updateArray = append(updateArray, bindingUpdateSettings[keybinding]) } t.inputHandler = func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { @@ -101,9 +114,12 @@ func (t *TreeView) Setup(config KeyBindingConfig) *TreeView { t.keyLeft() } - for idx, binding := range bindingArray { + for idx, binding := range t.bindingArray { if binding.Match(event) { actionArray[idx]() + if updateArray[idx] { + t.bindingArray[idx].Selected = !t.bindingArray[idx].Selected + } } } } @@ -124,6 +140,12 @@ func (t *TreeView) getInputWrapper() inputFn { return t.InputHandler } +// Keybinding list + +func (t *TreeView) GetKeyBindings() []KeyBindingDisplay { + return t.bindingArray +} + // Implementation note: // what do we want here??? a binding object?? yes func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { diff --git a/runtime/ui/components/filter_primative.go b/runtime/ui/components/filter_primative.go index 596a4b0..21daba6 100644 --- a/runtime/ui/components/filter_primative.go +++ b/runtime/ui/components/filter_primative.go @@ -68,3 +68,7 @@ func (fv *FilterView) Empty() bool { func (fv *FilterView) Visible() bool { return !fv.Empty() || fv.HasFocus() } + +func (fv *FilterView) GetKeyBindings() []KeyBindingDisplay { + return []KeyBindingDisplay{} +} diff --git a/runtime/ui/components/image_details_view.go b/runtime/ui/components/image_details_view.go index 6d88d1c..9457991 100644 --- a/runtime/ui/components/image_details_view.go +++ b/runtime/ui/components/image_details_view.go @@ -68,3 +68,7 @@ func (lv *ImageDetails) imageDetailsText() string { return fmt.Sprintf("%s\n%s\n%s\n%s", imageSizeStr, wastedSpaceStr, effStr, inefficiencyReport) } + +func (lv *ImageDetails) GetKeyBindings() []KeyBindingDisplay { + return []KeyBindingDisplay{} +} diff --git a/runtime/ui/components/key_config.go b/runtime/ui/components/key_config.go index 187c719..4b41cb3 100644 --- a/runtime/ui/components/key_config.go +++ b/runtime/ui/components/key_config.go @@ -16,6 +16,11 @@ type KeyBinding struct{ Display string } +type KeyBindingDisplay struct { + *KeyBinding + Selected bool +} + type keyAction func() bool @@ -58,7 +63,4 @@ func (k *KeyConfig) GetKeyBinding(key string) (result KeyBinding, err error) { return KeyBinding{}, err } return result, err - //if config == "" { - // return "", NewMissingConfigErr(lookupName) - //} } diff --git a/runtime/ui/components/keybinding_primitive.go b/runtime/ui/components/keybinding_primitive.go new file mode 100644 index 0000000..5394d6b --- /dev/null +++ b/runtime/ui/components/keybinding_primitive.go @@ -0,0 +1,89 @@ +package components + +import ( + "fmt" + "strings" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/runtime/ui/format" +) + +type BoundView interface { + HasFocus() bool + GetKeyBindings() []KeyBindingDisplay +} + +type KeyMenuView struct { + *tview.TextView + boundList []BoundView +} + +func NewKeyMenuView() *KeyMenuView { + return &KeyMenuView{ + TextView: tview.NewTextView(), + boundList: []BoundView{}, + } +} + +func (t *KeyMenuView) AddBoundViews(b ...BoundView) *KeyMenuView { + t.boundList = append(t.boundList, b...) + return t +} + +func (t *KeyMenuView) RemoveViews(b ...BoundView) *KeyMenuView { + newBoundList := []BoundView{} + boundSet := map[BoundView]interface{}{} + for _, v := range b { + boundSet[v] = true + } + + for _, bound := range t.boundList { + if _, ok := boundSet[bound]; !ok { + newBoundList = append(newBoundList, bound) + } + } + + t.boundList = newBoundList + return t +} + +func (t *KeyMenuView) GetKeyBindings() []KeyBindingDisplay { + logrus.Debug("Getting binding keys from keybinding primitive") + result := []KeyBindingDisplay{} + for _, view := range t.boundList { + if view.HasFocus() { + result = append(result, view.GetKeyBindings()...) + } + } + + return result +} + +func (t *KeyMenuView) Draw(screen tcell.Screen) { + t.Box.Draw(screen) + x, y, width, _ := t.Box.GetInnerRect() + // TODO: add logic to highlight selected options + + line := []string{} + for _, keyBinding := range t.GetKeyBindings() { + formatter := format.StatusControlNormal + if keyBinding.Selected { + formatter = format.StatusControlSelected + } + line = append(line, formatter(fmt.Sprintf("%s (%s)", keyBinding.Display, keyBinding.Name()))) + } + + format.PrintLine(screen, strings.Join(line, format.StatusControlNormal(" ▏")), x, y, width, tview.AlignLeft, tcell.StyleDefault) + +} + +// for wrappers +func (t *KeyMenuView) getBox() *tview.Box { + return t.Box +} + +func (t *KeyMenuView) getDraw() drawFn { + return t.Draw +} diff --git a/runtime/ui/components/layer_details_view.go b/runtime/ui/components/layer_details_view.go index a38f811..7b41b2c 100644 --- a/runtime/ui/components/layer_details_view.go +++ b/runtime/ui/components/layer_details_view.go @@ -50,6 +50,10 @@ func (lv *LayerDetailsView) Draw(screen tcell.Screen) { lv.TextView.Draw(screen) } +func (lv *LayerDetailsView) GetKeyBindings() []KeyBindingDisplay { + return []KeyBindingDisplay{} +} + func layerDetailsText(layer *image.Layer) string { lines := []string{} if layer.Names != nil && len(layer.Names) > 0 { @@ -67,3 +71,4 @@ func layerDetailsText(layer *image.Layer) string { func boldString(s string) string { return fmt.Sprintf("[::b]%s[::-]", s) } + diff --git a/runtime/ui/components/layers_primative.go b/runtime/ui/components/layers_primative.go index cb9b5b5..81ef1d7 100644 --- a/runtime/ui/components/layers_primative.go +++ b/runtime/ui/components/layers_primative.go @@ -24,6 +24,9 @@ type LayerList struct { cmpIndex int changed LayerListHandler inputHandler func(event *tcell.EventKey, setFocus func(p tview.Primitive)) + + bindingArray []KeyBindingDisplay + LayersViewModel } @@ -58,7 +61,6 @@ func (ll *LayerList) Setup(config KeyBindingConfig) *LayerList { }, } - bindingArray := []KeyBinding{} actionArray := []keyAction{} for keybinding, action := range bindingSettings { @@ -68,7 +70,7 @@ func (ll *LayerList) Setup(config KeyBindingConfig) *LayerList { // TODO handle this error //return nil } - bindingArray = append(bindingArray, binding) + ll.bindingArray = append(ll.bindingArray, KeyBindingDisplay{KeyBinding: &binding, Selected: false}) actionArray = append(actionArray, action) } @@ -88,7 +90,7 @@ func (ll *LayerList) Setup(config KeyBindingConfig) *LayerList { } } - for idx, binding := range bindingArray { + for idx, binding := range ll.bindingArray { if binding.Match(event) { actionArray[idx]() } @@ -97,6 +99,10 @@ func (ll *LayerList) Setup(config KeyBindingConfig) *LayerList { return ll } +func (ll *LayerList) GetKeyBindings() []KeyBindingDisplay { + return ll.bindingArray +} + func (ll *LayerList) getBox() *tview.Box { return ll.Box } diff --git a/runtime/ui/components/visible_grid.go b/runtime/ui/components/visible_grid.go index f0db4bc..7fdbe1c 100644 --- a/runtime/ui/components/visible_grid.go +++ b/runtime/ui/components/visible_grid.go @@ -7,8 +7,13 @@ import ( "github.com/rivo/tview" ) +type GridPrimitive interface { + VisiblePrimitive + GetKeyBindings() []KeyBindingDisplay +} + type flexItem struct { - Item VisiblePrimitive // The item to be positioned. May be nil for an empty item. + Item GridPrimitive // The item to be positioned. May be nil for an empty item. FixedSize int // The item's fixed size which may not be changed, 0 if it has no fixed size. Proportion int // The item's proportion Focus bool // Whether or not this item attracts the layout's focus. @@ -26,6 +31,10 @@ type VisibleFlex struct { direction int visible VisibleFunc + + bindingArray []KeyBinding + + inputHandler func(event *tcell.EventKey, setFocus func(p tview.Primitive)) } func NewVisibleFlex() *VisibleFlex { @@ -36,7 +45,24 @@ func NewVisibleFlex() *VisibleFlex { } } -func (f *VisibleFlex) SetVisibility(visibleFunc VisibleFunc) VisiblePrimitive { +func (f *VisibleFlex) GetKeyBindings() []KeyBindingDisplay { + result := []KeyBindingDisplay{} + + for _, binding := range f.bindingArray { + result = append(result, KeyBindingDisplay{KeyBinding: &binding, Selected: false}) + } + + for _, item := range f.items { + if item.Item.HasFocus() { + result = append(result, item.Item.GetKeyBindings()...) + } + } + + return result +} + + +func (f *VisibleFlex) SetVisibility(visibleFunc VisibleFunc) GridPrimitive { f.visible = visibleFunc return f } @@ -46,7 +72,7 @@ func (f *VisibleFlex) SetDirection(direction int) *VisibleFlex { return f } -func (f *VisibleFlex) AddItem(item VisiblePrimitive, fixedSize, proportion int, focus bool) *VisibleFlex { +func (f *VisibleFlex) AddItem(item GridPrimitive, fixedSize, proportion int, focus bool) *VisibleFlex { f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}) f.consume = append(f.consume, []int{}) return f @@ -54,7 +80,7 @@ func (f *VisibleFlex) AddItem(item VisiblePrimitive, fixedSize, proportion int, // RemoveItem removes all items for the given primitive from the container, // keeping the order of the remaining items intact. -func (f *VisibleFlex) RemoveItem(p VisiblePrimitive) *VisibleFlex { +func (f *VisibleFlex) RemoveItem(p GridPrimitive) *VisibleFlex { for index := len(f.items) - 1; index >= 0; index-- { if f.items[index].Item == p { f.items = append(f.items[:index], f.items[index+1:]...) @@ -82,7 +108,7 @@ func (f *VisibleFlex) ResizeItem(p tview.Primitive, fixedSize, proportion int) * // TODO: update the API here this is pretty rough // Method provided to give configuration that would otherwise not be possible when primitives are repeated -func (f *VisibleFlex) SetConsumersByIndex(p VisiblePrimitive, consumeIndicies []int) *VisibleFlex { +func (f *VisibleFlex) SetConsumersByIndex(p GridPrimitive, consumeIndicies []int) *VisibleFlex { for i, item := range f.items { if item.Item == p { f.consume[i] = consumeIndicies @@ -95,8 +121,8 @@ func (f *VisibleFlex) SetConsumersByIndex(p VisiblePrimitive, consumeIndicies [] // Implementation notes: // we want a list of indicies []int{} where each visible primitive corresponds to the first matching primitive // in our list of items -func (f *VisibleFlex) SetConsumers(p VisiblePrimitive, consumes ...VisiblePrimitive) *VisibleFlex { - indexMap := map[VisiblePrimitive]int{} +func (f *VisibleFlex) SetConsumers(p GridPrimitive, consumes ...GridPrimitive) *VisibleFlex { + indexMap := map[GridPrimitive]int{} for _, item := range f.items { _, ok := indexMap[item.Item] if !ok { diff --git a/runtime/ui/components/wrapper_primative.go b/runtime/ui/components/wrapper_primative.go index ce9e375..83f080f 100644 --- a/runtime/ui/components/wrapper_primative.go +++ b/runtime/ui/components/wrapper_primative.go @@ -14,6 +14,7 @@ type wrapable interface { getBox() *tview.Box getDraw() drawFn getInputWrapper() inputFn + GetKeyBindings() []KeyBindingDisplay } type Wrapper struct { @@ -26,6 +27,7 @@ type Wrapper struct { title string subtitle string visible VisibleFunc + getKeyBindings func() []KeyBindingDisplay } type drawFn func(screen tcell.Screen) @@ -42,6 +44,7 @@ func NewWrapper(title, subtitle string, inner wrapable) *Wrapper { subtitleTextView: tview.NewTextView().SetText(subtitle), inner: inner, visible: Always(true), + getKeyBindings: inner.GetKeyBindings, } w.setTitle(w.inner.getBox().HasFocus()) return w @@ -129,3 +132,7 @@ func (b *Wrapper) SetVisibility(visibleFunc VisibleFunc) *Wrapper { b.visible = visibleFunc return b } + +func (b *Wrapper) GetKeyBindings() []KeyBindingDisplay { + return b.getKeyBindings() +} diff --git a/runtime/ui/format/format.go b/runtime/ui/format/format.go index 8a07c66..690c0e5 100644 --- a/runtime/ui/format/format.go +++ b/runtime/ui/format/format.go @@ -79,6 +79,8 @@ var ( // Styles these are needed to completely color a line SelectedStyle tcell.Style = tcell.Style{}.Bold(true).Reverse(true) + MenuStyle tcell.Style = tcell.Style{}.Reverse(true) + SelectedMenuStyle tcell.Style = tcell.Style{}.Background(tcell.ColorDarkBlue).Foreground(tcell.ColorWhite).Bold(true) ) func PrintLine(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { |