// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package outputs import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/sorter" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" ) const ( // can't get terminal dimensions on startup, so use this initialDimensionVal = 30 instanceTypeKey = "instance type" selectedKey = "selected" ) const ( // table states stateTable = "table" stateVerbose = "verbose" stateSorting = "sorting" ) var ( controlsStyle = lipgloss.NewStyle().Faint(true) ) // BubbleTeaModel is used to hold the state of the bubble tea TUI type BubbleTeaModel struct { // holds the output currentState of the model currentState string // the model for the table view tableModel tableModel // holds state for the verbose view verboseModel verboseModel // holds the state for the sorting view sortingModel sortingModel } // NewBubbleTeaModel initializes a new bubble tea Model which represents // a stylized table to display instance types func NewBubbleTeaModel(instanceTypes []*instancetypes.Details) BubbleTeaModel { return BubbleTeaModel{ currentState: stateTable, tableModel: *initTableModel(instanceTypes), verboseModel: *initVerboseModel(instanceTypes), sortingModel: *initSortingModel(instanceTypes), } } // Init is used by bubble tea to initialize a bubble tea table func (m BubbleTeaModel) Init() tea.Cmd { return nil } // Update is used by bubble tea to update the state of the bubble // tea model based on user input func (m BubbleTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // don't listen for input if currently typing into text field if m.tableModel.filterTextInput.Focused() { break } else if m.sortingModel.sortTextInput.Focused() { // see if we should sort and switch states to table if m.currentState == stateSorting && msg.String() == "enter" { jsonPath := m.sortingModel.sortTextInput.Value() sortDirection := sorter.SortAscending if m.sortingModel.isDescending { sortDirection = sorter.SortDescending } var err error m.tableModel, err = m.tableModel.sortTable(jsonPath, sortDirection) if err != nil { m.sortingModel.sortTextInput.SetValue(jsonPathError) break } m.currentState = stateTable m.sortingModel.sortTextInput.Blur() } break } // check for quit or change in state switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "e": // switch from table state to verbose state if m.currentState == stateTable { // get focused instance type focusedRow := m.tableModel.table.HighlightedRow() focusedInstance, ok := focusedRow.Data[instanceTypeKey].(*instancetypes.Details) if !ok { break } // set content of view m.verboseModel.focusedInstanceName = focusedInstance.InstanceType m.verboseModel.viewport.SetContent(VerboseInstanceTypeOutput([]*instancetypes.Details{focusedInstance})[0]) // move viewport to top of printout m.verboseModel.viewport.SetYOffset(0) // switch from table state to verbose state m.currentState = stateVerbose } case "s": // switch from table view to sorting view if m.currentState == stateTable { m.currentState = stateSorting } case "enter": // sort and switch states to table if m.currentState == stateSorting { sortFilter := string(m.sortingModel.shorthandList.SelectedItem().(item)) sortDirection := sorter.SortAscending if m.sortingModel.isDescending { sortDirection = sorter.SortDescending } var err error m.tableModel, err = m.tableModel.sortTable(sortFilter, sortDirection) if err != nil { m.sortingModel.sortTextInput.SetValue("INVALID SHORTHAND VALUE") break } m.currentState = stateTable m.sortingModel.sortTextInput.Blur() } case "esc": // switch from sorting state or verbose state to table state if m.currentState == stateSorting || m.currentState == stateVerbose { m.currentState = stateTable } } case tea.WindowSizeMsg: // This is needed to handle a bug with bubble tea // where resizing causes misprints (https://github.com/Evertras/bubble-table/issues/121) termenv.ClearScreen() // handle screen resizing m.tableModel = m.tableModel.resizeView(msg) m.verboseModel = m.verboseModel.resizeView(msg) m.sortingModel = m.sortingModel.resizeView(msg) } var cmd tea.Cmd // update currently active state switch m.currentState { case stateTable: m.tableModel, cmd = m.tableModel.update(msg) case stateVerbose: m.verboseModel, cmd = m.verboseModel.update(msg) case stateSorting: m.sortingModel, cmd = m.sortingModel.update(msg) } return m, cmd } // View is used by bubble tea to render the bubble tea model func (m BubbleTeaModel) View() string { switch m.currentState { case stateTable: return m.tableModel.view() case stateVerbose: return m.verboseModel.view() case stateSorting: return m.sortingModel.view() } return "" }