-
-
Notifications
You must be signed in to change notification settings - Fork 27
feat(taints): add a picker for widely used taints #565
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
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| "slices" | ||
|
|
||
| tea "github.com/charmbracelet/bubbletea" | ||
| "github.com/charmbracelet/lipgloss" | ||
|
|
||
| "github.com/janosmiko/lfk/internal/model" | ||
| "github.com/janosmiko/lfk/internal/ui" | ||
| ) | ||
|
|
||
| // handleTaintOverlayKey and renderTaintOverlay route the editor and its | ||
| // picker as one feature. The dispatchers they hang off (handleOverlayKeySecondary, | ||
| // renderOverlayContentExtended) sit at the gocyclo cap, so the two overlays | ||
| // share one case there and split here instead. | ||
| func (m Model) handleTaintOverlayKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | ||
| if m.overlay == overlayTaintPresets { | ||
| return m.handleTaintPresetKey(msg) | ||
| } | ||
| return m.handleTaintEditorKey(msg) | ||
| } | ||
|
|
||
| func (m Model) renderTaintOverlay() (string, int, int) { | ||
| if m.overlay == overlayTaintPresets { | ||
| return m.renderOverlayTaintPresets() | ||
| } | ||
| return m.renderOverlayTaintEditor() | ||
| } | ||
|
|
||
| // openTaintPresets opens the common-taint picker over the editor. The editor | ||
| // state stays live underneath so Esc returns to the list exactly as it was. | ||
| func (m Model) openTaintPresets() (tea.Model, tea.Cmd) { | ||
| m.taintEditor.presetCursor = 0 | ||
| m.taintEditor.presetScroll = 0 | ||
| m.overlay = overlayTaintPresets | ||
| return m, nil | ||
| } | ||
|
|
||
| // closeTaintPresets returns to the editor without adopting anything — the | ||
| // picker writes to the add-row only on an explicit selection. | ||
| func (m *Model) closeTaintPresets() { | ||
| m.overlay = overlayTaintEditor | ||
| } | ||
|
|
||
| // taintPresetsMaxVisible caps the picker rows shown at once, shared by the | ||
| // renderer and the scroll clamp so the scrollbar and cursor stay in sync. | ||
| func (m Model) taintPresetsMaxVisible() int { | ||
| return min(12, max(m.height-12, 3)) | ||
| } | ||
|
|
||
| // handleTaintPresetKey drives the common-taint picker: vim navigation, Enter | ||
| // to adopt the highlighted preset, Esc to back out. | ||
| func (m Model) handleTaintPresetKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | ||
| p := &m.taintEditor | ||
| n := len(model.CommonTaints) | ||
| visible := m.taintPresetsMaxVisible() | ||
| switch msg.String() { | ||
| case "esc", "q": | ||
| m.closeTaintPresets() | ||
| return m, nil | ||
| case "enter": | ||
| return m.applyTaintPreset() | ||
| case "j", "down": | ||
| if p.presetCursor < n-1 { | ||
| p.presetCursor++ | ||
| } | ||
| case "k", "up": | ||
| if p.presetCursor > 0 { | ||
| p.presetCursor-- | ||
| } | ||
| case "g", "home": | ||
| p.presetCursor = 0 | ||
| case "G", "end": | ||
| p.presetCursor = max(n-1, 0) | ||
| case "ctrl+d": | ||
| p.presetCursor = min(p.presetCursor+visible/2, max(n-1, 0)) | ||
| case "ctrl+u": | ||
| p.presetCursor = max(p.presetCursor-visible/2, 0) | ||
| case "ctrl+f", "pgdown", "shift+down": | ||
| p.presetCursor = min(p.presetCursor+visible, max(n-1, 0)) | ||
| case "ctrl+b", "pgup", "shift+up": | ||
| p.presetCursor = max(p.presetCursor-visible, 0) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| p.presetCursor = max(0, min(p.presetCursor, n-1)) | ||
| overlayListScroll(&p.presetScroll, p.presetCursor, n, visible) | ||
| return m, nil | ||
| } | ||
|
|
||
| // applyTaintPreset copies the highlighted preset into the add-row and returns | ||
| // to the editor with the key field focused. The preset is a starting point, | ||
| // not a commitment: all three fields stay editable and the taint still goes | ||
| // through the normal validation and staging path. | ||
| func (m Model) applyTaintPreset() (tea.Model, tea.Cmd) { | ||
| p := &m.taintEditor | ||
| if p.presetCursor < 0 || p.presetCursor >= len(model.CommonTaints) { | ||
| m.closeTaintPresets() | ||
| return m, nil | ||
| } | ||
| t := model.CommonTaints[p.presetCursor].Taint | ||
| p.addKey = t.Key | ||
| p.addVal = t.Value | ||
| p.addEff = max(slices.Index(model.ValidTaintEffects, t.Effect), 0) | ||
| p.focus = taintFocusKey | ||
| m.closeTaintPresets() | ||
| return m, nil | ||
| } | ||
|
|
||
| // renderOverlayTaintPresets paints the common-taint picker: each preset in | ||
| // kubectl notation with a note on what applying it does. | ||
| func (m Model) renderOverlayTaintPresets() (string, int, int) { | ||
| if !m.taintEditor.active { | ||
| return "", 0, 0 | ||
| } | ||
| p := m.taintEditor | ||
| items := make([]ui.OverlayListItem, len(model.CommonTaints)) | ||
| for i, preset := range model.CommonTaints { | ||
| items[i] = ui.OverlayListItem{ | ||
| Name: preset.Taint.String(), | ||
| Description: preset.Desc, | ||
| } | ||
| } | ||
| cfg := ui.OverlayListConfig{ | ||
| Title: "Common taints", | ||
| Cursor: p.presetCursor, | ||
| ShowDescription: true, | ||
| Scroll: p.presetScroll, | ||
| MaxVisible: m.taintPresetsMaxVisible(), | ||
| } | ||
|
|
||
| overlayW := ui.OverlayListWidth(items, cfg, max(m.width-10, 1)) | ||
| innerW := max(overlayW-4, 1) | ||
| for i := range items { | ||
| items[i].Name = ui.Truncate(items[i].Name, innerW) | ||
| } | ||
|
|
||
| content := ui.RenderOverlayList(items, cfg, innerW) | ||
| overlayH := min(lipgloss.Height(content)+2, max(m.height-4, 3)) | ||
| return content, overlayW, overlayH | ||
| } | ||
|
|
||
| // taintPresetHints is the picker's bottom hint bar. | ||
| func taintPresetHints() []ui.HintEntry { | ||
| return []ui.HintEntry{ | ||
| {Key: "j/k", Desc: "navigate"}, | ||
| {Key: "enter", Desc: "use taint"}, | ||
| {Key: "esc", Desc: "back"}, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| tea "github.com/charmbracelet/bubbletea" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/janosmiko/lfk/internal/model" | ||
| ) | ||
|
|
||
| // taintPresetKey drives the preset picker, which owns its own overlay and so | ||
| // routes through the overlay dispatcher rather than the editor's handler. | ||
| func taintPresetKey(t *testing.T, m Model, keys ...string) Model { | ||
| t.Helper() | ||
| for _, k := range keys { | ||
| var msg tea.KeyMsg | ||
| switch k { | ||
| case "enter": | ||
| msg = tea.KeyMsg{Type: tea.KeyEnter} | ||
| case "esc": | ||
| msg = tea.KeyMsg{Type: tea.KeyEsc} | ||
| default: | ||
| msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(k)} | ||
| } | ||
| mdl, _ := m.handleTaintPresetKey(msg) | ||
| var ok bool | ||
| m, ok = mdl.(Model) | ||
| require.True(t, ok) | ||
| } | ||
| return m | ||
| } | ||
|
|
||
| func TestTaintEditor_POpensPresetPicker(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
|
|
||
| assert.Equal(t, overlayTaintPresets, m.overlay) | ||
| assert.True(t, m.taintEditor.active, "the editor stays alive underneath") | ||
| } | ||
|
|
||
| // Selecting a preset fills all three add-row fields and drops the user on the | ||
| // key field, so every part stays editable before staging. | ||
| func TestTaintPresets_SelectFillsAddRowAndFocusesKey(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
| m = taintPresetKey(t, m, "j", "enter") | ||
|
|
||
| want := model.CommonTaints[1].Taint | ||
| assert.Equal(t, overlayTaintEditor, m.overlay) | ||
| assert.Equal(t, want.Key, m.taintEditor.addKey) | ||
| assert.Equal(t, want.Value, m.taintEditor.addVal) | ||
| assert.Equal(t, want.Effect, model.ValidTaintEffects[m.taintEditor.addEff]) | ||
| assert.Equal(t, taintFocusKey, m.taintEditor.focus) | ||
| } | ||
|
|
||
| // A picked preset is a starting point, not a commitment: it must still go | ||
| // through the normal validation and staging path. | ||
| func TestTaintPresets_PickedPresetStages(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| before := len(m.taintEditor.rows) | ||
| m = taintKey(t, m, "p") | ||
| m = taintPresetKey(t, m, "enter") | ||
| m = taintKey(t, m, "enter") | ||
|
|
||
| require.Len(t, m.taintEditor.rows, before+1) | ||
| staged := m.taintEditor.rows[before] | ||
| assert.True(t, staged.staged) | ||
| assert.Equal(t, model.CommonTaints[0].Taint.String(), staged.taint.String()) | ||
| } | ||
|
|
||
| // Esc backs out to the editor without adopting the highlighted preset — the | ||
| // picker only writes to the add-row on an explicit selection. | ||
| func TestTaintPresets_EscAdoptsNothing(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
| m = taintPresetKey(t, m, "j", "esc") | ||
|
|
||
| assert.Equal(t, overlayTaintEditor, m.overlay) | ||
| assert.Empty(t, m.taintEditor.addKey, "esc must not fill the add-row") | ||
| assert.Equal(t, taintFocusList, m.taintEditor.focus) | ||
| } | ||
|
|
||
| func TestTaintPresets_CursorClampsAtBothEnds(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
|
|
||
| m = taintPresetKey(t, m, "k") | ||
| assert.Equal(t, 0, m.taintEditor.presetCursor, "cursor clamps at the top") | ||
|
|
||
| m = taintPresetKey(t, m, "G") | ||
| assert.Equal(t, len(model.CommonTaints)-1, m.taintEditor.presetCursor) | ||
|
|
||
| m = taintPresetKey(t, m, "j") | ||
| assert.Equal(t, len(model.CommonTaints)-1, m.taintEditor.presetCursor, "cursor clamps at the bottom") | ||
|
|
||
| m = taintPresetKey(t, m, "g") | ||
| assert.Equal(t, 0, m.taintEditor.presetCursor) | ||
| } | ||
|
|
||
| func TestRenderOverlayTaintPresets_ShowsTaintsAndDescriptions(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
|
|
||
| content, w, h := m.renderOverlayTaintPresets() | ||
| require.NotEmpty(t, content) | ||
| assert.Positive(t, w) | ||
| assert.Positive(t, h) | ||
|
|
||
| plain := stripANSI(content) | ||
| assert.Contains(t, plain, "node-role.kubernetes.io/control-plane:NoSchedule") | ||
| assert.Contains(t, plain, "Reserve control-plane nodes") | ||
| } | ||
|
|
||
| // The picker's hints must describe the picker, not the editor underneath. | ||
| func TestTaintPresets_HintBar(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
| m = taintKey(t, m, "p") | ||
|
|
||
| hints := stripANSI(m.overlayHintBar()) | ||
| assert.Contains(t, hints, "use taint") | ||
| } | ||
|
|
||
| // The editor's own hint bar must advertise the picker, or nobody finds it. | ||
| func TestTaintEditor_HintBarAdvertisesPresets(t *testing.T) { | ||
| m := openTaintEditorLoaded(t, taintEditorTestModel()) | ||
|
|
||
| hints := stripANSI(m.overlayHintBar()) | ||
| assert.Contains(t, hints, "common taints") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.