-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Interactive table sorting, trimming, and filtering #152
Changes from all commits
7cde3a0
950d844
36df01c
5bdb983
37ce429
fd2002e
1f0f645
5ec0cb3
4a14554
0d825b0
0ccb857
2826b4b
74f54b2
536fcab
4db1c19
c188e9c
97c6569
606ca51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,19 +15,29 @@ 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 | ||
|
@@ -40,6 +50,9 @@ type BubbleTeaModel struct { | |
|
||
// 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 | ||
|
@@ -49,6 +62,7 @@ func NewBubbleTeaModel(instanceTypes []*instancetypes.Details) BubbleTeaModel { | |
currentState: stateTable, | ||
tableModel: *initTableModel(instanceTypes), | ||
verboseModel: *initVerboseModel(instanceTypes), | ||
sortingModel: *initSortingModel(instanceTypes), | ||
} | ||
} | ||
|
||
|
@@ -62,28 +76,87 @@ func (m BubbleTeaModel) Init() tea.Cmd { | |
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 "enter": | ||
switch m.currentState { | ||
case stateTable: | ||
// switch from table state to verbose state | ||
m.currentState = stateVerbose | ||
|
||
case "e": | ||
// switch from table state to verbose state | ||
if m.currentState == stateTable { | ||
// get focused instance type | ||
rowIndex := m.tableModel.table.GetHighlightedRowIndex() | ||
focusedInstance := m.verboseModel.instanceTypes[rowIndex] | ||
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) | ||
case stateVerbose: | ||
// switch from verbose state to table state | ||
|
||
// 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly here, once we know we're in the sortingView state, we should pass the key msg to sortingView's update() and etc. to group all of the actual sorting logic together. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the sorting, information from the sorting state must be retrieved and then passed on to the table state before the state switch occurs. This requires access to both I tried to keep all logic which dealt with "inter-state" affairs (such as switching states) in the |
||
|
||
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 | ||
} | ||
} | ||
|
@@ -95,23 +168,21 @@ func (m BubbleTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
// 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: | ||
// update table | ||
var cmd tea.Cmd | ||
m.tableModel, cmd = m.tableModel.update(msg) | ||
|
||
return m, cmd | ||
case stateVerbose: | ||
// update viewport | ||
var cmd tea.Cmd | ||
m.verboseModel, cmd = m.verboseModel.update(msg) | ||
return m, cmd | ||
case stateSorting: | ||
m.sortingModel, cmd = m.sortingModel.update(msg) | ||
} | ||
|
||
return m, nil | ||
return m, cmd | ||
} | ||
|
||
// View is used by bubble tea to render the bubble tea model | ||
|
@@ -121,6 +192,8 @@ func (m BubbleTeaModel) View() string { | |
return m.tableModel.view() | ||
case stateVerbose: | ||
return m.verboseModel.view() | ||
case stateSorting: | ||
return m.sortingModel.view() | ||
} | ||
|
||
return "" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about moving this table- and sorting-related logic to the respective view files, and then forwarding the key msg to the
Update()
of tableView and sortingView? Similar to the other "in focus" handling you do there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These if statements exist to prevent the keys used to switch between states from causing a state switch while typing in a text field.