github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/components/components.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package components contains some TUI components for the guided remediation interactive CLI. 16 package components 17 18 import ( 19 "fmt" 20 "strings" 21 "time" 22 23 "github.com/charmbracelet/bubbles/key" 24 "github.com/charmbracelet/bubbles/spinner" 25 tea "github.com/charmbracelet/bubbletea" 26 "github.com/charmbracelet/lipgloss" 27 ) 28 29 // KeyMap holds the key bindings for the guided remediation interactive TUI. 30 type KeyMap struct { 31 Up key.Binding 32 Down key.Binding 33 Left key.Binding 34 Right key.Binding 35 Select key.Binding 36 SwitchView key.Binding 37 Help key.Binding 38 Quit key.Binding 39 } 40 41 // ShortHelp returns the short help for the key map. 42 func (k KeyMap) ShortHelp() []key.Binding { 43 return []key.Binding{k.Help, k.Quit} 44 } 45 46 // FullHelp returns the full help for the key map. 47 func (k KeyMap) FullHelp() [][]key.Binding { 48 return [][]key.Binding{ 49 {k.Up, k.Down}, 50 {k.Select, k.SwitchView}, 51 {k.Help, k.Quit}, 52 } 53 } 54 55 // Keys is the default key map for the guided remediation interactive TUI. 56 var Keys = KeyMap{ 57 Up: key.NewBinding( 58 key.WithKeys("up"), 59 key.WithHelp("↑", "move up"), 60 ), 61 Down: key.NewBinding( 62 key.WithKeys("down"), 63 key.WithHelp("↓", "move down"), 64 ), 65 Left: key.NewBinding( 66 key.WithKeys("left"), 67 ), 68 Right: key.NewBinding( 69 key.WithKeys("right"), 70 ), 71 Select: key.NewBinding( 72 key.WithKeys("enter"), 73 key.WithHelp("enter", "select option"), 74 ), 75 SwitchView: key.NewBinding( 76 key.WithKeys("tab", "i"), 77 key.WithHelp("i/tab", "switch views"), 78 ), 79 Help: key.NewBinding( 80 key.WithKeys("h"), 81 key.WithHelp("h", "toggle help"), 82 ), 83 Quit: key.NewBinding( 84 key.WithKeys("q", "esc"), 85 key.WithHelp("q/esc", "exit"), 86 ), 87 } 88 89 // NewSpinner creates a stylised spinner 90 func NewSpinner() spinner.Model { 91 sp := spinner.New(spinner.WithSpinner(spinner.Line)) 92 // Spinner.FPS is actually the duration of each frame, not the frames per second 93 sp.Spinner.FPS = 200 * time.Millisecond 94 95 return sp 96 } 97 98 // RenderSelectorOption provides an inline selector renderer, 99 // for layouts that don't fit neatly into a list/table 100 func RenderSelectorOption( 101 selected bool, // whether this line is currently highlighted 102 cursor string, // the cursor to display before the line, if it's selected 103 format string, // format string for the content. Should only use `%v` specifier 104 args ...any, // args for the format string. These will be highlighted if the line is selected 105 ) string { 106 if !selected { 107 cursor = strings.Repeat(" ", lipgloss.Width(cursor)) 108 } else { 109 cursor = SelectedTextStyle.Render(cursor) 110 for i := range args { 111 args[i] = SelectedTextStyle.Render(fmt.Sprintf("%v", args[i])) 112 } 113 } 114 115 return fmt.Sprintf(cursor+format, args...) 116 } 117 118 // ViewModel provides a tea-like model for representing the secondary info panel 119 // which allows for resizing 120 type ViewModel interface { 121 Update(msg tea.Msg) (ViewModel, tea.Cmd) 122 View() string 123 Resize(w, h int) ViewModel 124 } 125 126 // ViewModelCloseMsg provides a message to close the ViewModel 127 type ViewModelCloseMsg struct{} 128 129 // CloseViewModel provides a tea command to close the ViewModel. 130 var CloseViewModel tea.Cmd = func() tea.Msg { return ViewModelCloseMsg{} } 131 132 // TextView is a ViewModel for showing non-interactive text. 133 type TextView string 134 135 // Update is a no-op for TextView. 136 func (t TextView) Update(tea.Msg) (ViewModel, tea.Cmd) { return t, nil } 137 138 // View returns the text as a string. 139 func (t TextView) View() string { return string(t) } 140 141 // Resize is a no-op for TextView. 142 func (t TextView) Resize(int, int) ViewModel { return t }