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 }