diff --git a/internal/config/alias.go b/internal/config/alias.go index ff8075da..622f94e3 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -143,7 +143,7 @@ func (a *Aliases) Define(gvr string, aliases ...string) { func (a *Aliases) LoadAliases(path string) error { f, err := ioutil.ReadFile(path) if err != nil { - log.Warn().Err(err).Msgf("No custom aliases found") + log.Debug().Err(err).Msgf("No custom aliases found") return nil } diff --git a/internal/model/text.go b/internal/model/text.go new file mode 100644 index 00000000..1da28560 --- /dev/null +++ b/internal/model/text.go @@ -0,0 +1,120 @@ +package model + +import ( + "regexp" + "strings" + + "github.com/sahilm/fuzzy" +) + +// TextListener represents a text model listener. +type TextListener interface { + // TextChanged notifies the model changed. + TextChanged([]string) + + // TextFiltered notifies when the filter changed. + TextFiltered([]string, fuzzy.Matches) +} + +// Text represents a text model. +type Text struct { + lines []string + listeners []TextListener + query string +} + +// NewText returns a new model. +func NewText() *Text { + return &Text{} +} + +// Peek returns the current model state. +func (t *Text) Peek() []string { + return t.lines +} + +// ClearFilter clear out filter. +func (t *Text) ClearFilter() { + t.query = "" + t.filterChanged(t.lines) +} + +// Filter filters out the text. +func (t *Text) Filter(q string) { + t.query = q + t.filterChanged(t.lines) +} + +// SetText sets the current model content. +func (t *Text) SetText(buff string) { + t.lines = strings.Split(buff, "\n") + t.fireTextChanged(t.lines) +} + +// AddListener adds a new model listener. +func (t *Text) AddListener(listener TextListener) { + t.listeners = append(t.listeners, listener) +} + +// RemoveListener delete a listener from the list. +func (t *Text) RemoveListener(listener TextListener) { + victim := -1 + for i, lis := range t.listeners { + if lis == listener { + victim = i + break + } + } + + if victim >= 0 { + t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...) + } +} + +func (t *Text) filterChanged(lines []string) { + t.fireTextFiltered(lines, t.filter(t.query, lines)) +} + +func (t *Text) fireTextChanged(lines []string) { + for _, lis := range t.listeners { + lis.TextChanged(lines) + } +} + +func (t *Text) fireTextFiltered(lines []string, matches fuzzy.Matches) { + for _, lis := range t.listeners { + lis.TextFiltered(lines, matches) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (t *Text) filter(q string, lines []string) fuzzy.Matches { + if q == "" { + return nil + } + if isFuzzySelector(q) { + return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + } + return t.rxFilter(q, lines) +} + +func (*Text) fuzzyFilter(q string, lines []string) fuzzy.Matches { + return fuzzy.Find(q, lines) +} + +func (*Text) rxFilter(q string, lines []string) fuzzy.Matches { + rx, err := regexp.Compile(`(?i)` + q) + if err != nil { + return nil + } + matches := make(fuzzy.Matches, 0, len(lines)) + for i, l := range lines { + if loc := rx.FindStringIndex(l); len(loc) == 2 { + matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) + } + } + + return matches +} diff --git a/internal/model/text_test.go b/internal/model/text_test.go new file mode 100644 index 00000000..d8c2f949 --- /dev/null +++ b/internal/model/text_test.go @@ -0,0 +1,90 @@ +package model_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/sahilm/fuzzy" + "github.com/stretchr/testify/assert" +) + +func TestNewText(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 0, lis.filtered) + assert.Equal(t, 0, lis.matches) +} + +func TestTextFilterRXMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("world") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 1, lis.matches) + assert.Equal(t, 6, lis.index) +} + +func TestTextFilterFuzzyMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("-f world") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 1, lis.matches) + assert.Equal(t, 6, lis.index) +} + +func TestTextFilterNoMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("blee") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 0, lis.matches) + assert.Equal(t, 0, lis.index) +} + +// Helpers... + +type textLis struct { + changed, filtered, matches, lines, index int +} + +func (l *textLis) TextChanged(ll []string) { + l.lines = len(ll) + l.changed++ +} + +func (l *textLis) TextFiltered(ll []string, mm fuzzy.Matches) { + l.matches = len(mm) + l.filtered++ + if len(mm) > 0 && len(mm[0].MatchedIndexes) > 0 { + l.index = mm[0].MatchedIndexes[0] + } +} diff --git a/internal/ui/app.go b/internal/ui/app.go index e200f282..54f2a833 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -175,7 +175,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { a.cmdBuff.Add(evt.Rune()) return nil } - key = asKey(evt) + key = AsKey(evt) } if a, ok := a.actions[key]; ok { @@ -258,7 +258,7 @@ func (a *App) Menu() *Menu { // Helpers... // AsKey converts rune to keyboard key., -func asKey(evt *tcell.EventKey) tcell.Key { +func AsKey(evt *tcell.EventKey) tcell.Key { key := tcell.Key(evt.Rune()) if evt.Modifiers() == tcell.ModAlt { key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) diff --git a/internal/ui/table.go b/internal/ui/table.go index 2c091cf2..a9c8c9e5 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -121,7 +121,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { if t.filterInput(evt.Rune()) { return nil } - key = asKey(evt) + key = AsKey(evt) } if a, ok := t.actions[key]; ok { @@ -370,17 +370,17 @@ func (t *Table) styleTitle() string { } } - buff := t.cmdBuff.String() var title string if ns == client.ClusterScope { title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) } else { title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) } + + buff := t.cmdBuff.String() if buff == "" { return title } - if IsLabelSelector(buff) { buff = TrimLabelSelector(buff) } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 681eddee..7fdc78f2 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -46,7 +46,7 @@ func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr, path string) { return } - details := NewDetails(b.App(), "Benchmark", fileToSubject(path)).Update(data) + details := NewDetails(b.App(), "Benchmark", fileToSubject(path), false).Update(data) if err := app.inject(details); err != nil { app.Flash().Err(err) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 89cb8126..e416df6f 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -175,7 +175,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(b.app, "YAML", path).Update(raw) + details := NewDetails(b.app, "YAML", path, true).Update(raw) if err := b.App().inject(details); err != nil { b.App().Flash().Err(err) } diff --git a/internal/view/details.go b/internal/view/details.go index c1e52f67..76bf5916 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -3,6 +3,7 @@ package view import ( "context" "fmt" + "strings" "github.com/atotto/clipboard" "github.com/derailed/k9s/internal/config" @@ -10,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" + "github.com/sahilm/fuzzy" ) const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " @@ -18,20 +20,26 @@ const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " type Details struct { *tview.TextView - actions ui.KeyActions - app *App - title, subject string - buff string + actions ui.KeyActions + app *App + title, subject string + cmdBuff *ui.CmdBuff + model *model.Text + currentRegion, maxRegions int + searchable bool } // NewDetails returns a details viewer. -func NewDetails(app *App, title, subject string) *Details { +func NewDetails(app *App, title, subject string, searchable bool) *Details { d := Details{ - TextView: tview.NewTextView(), - app: app, - title: title, - subject: subject, - actions: make(ui.KeyActions), + TextView: tview.NewTextView(), + app: app, + title: title, + subject: subject, + actions: make(ui.KeyActions), + cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), + model: model.NewText(), + searchable: searchable, } return &d @@ -42,37 +50,123 @@ func (d *Details) Init(_ context.Context) error { if d.title != "" { d.SetBorder(true) } - d.SetScrollable(true) - d.SetWrap(true) + d.SetScrollable(true).SetWrap(true).SetRegions(true) d.SetDynamicColors(true) d.SetHighlightColor(tcell.ColorOrange) d.SetTitleColor(tcell.ColorAqua) d.SetInputCapture(d.keyboard) - d.bindKeys() d.SetChangedFunc(func() { d.app.Draw() }) d.updateTitle() + d.app.Styles.AddListener(d) d.StylesChanged(d.app.Styles) + d.cmdBuff.AddListener(d.app.Cmd()) + d.cmdBuff.AddListener(d) + + d.bindKeys() + d.SetInputCapture(d.keyboard) + d.model.AddListener(d) + return nil } +func (d *Details) TextChanged(lines []string) { + d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) + d.ScrollToBeginning() +} + +func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { + d.currentRegion, d.maxRegions = 0, 0 + + ll := make([]string, len(lines)) + copy(ll, lines) + for _, m := range matches { + loc, line := m.MatchedIndexes, ll[m.Index] + ll[m.Index] = line[:loc[0]] + fmt.Sprintf(`<<<"search_%d">>>`, d.maxRegions) + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:] + d.maxRegions++ + } + + d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n"))) + d.Highlight() + if d.maxRegions > 0 { + d.Highlight("search_0") + d.ScrollToHighlight() + } +} + +// BufferChanged indicates the buffer was changed. +func (d *Details) BufferChanged(s string) {} + +// BufferActive indicates the buff activity changed. +func (d *Details) BufferActive(state bool, k ui.BufferKind) { + d.app.BufferActive(state, k) +} + +func (d *Details) bindKeys() { + d.actions.Set(ui.KeyActions{ + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), + ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), + ui.KeyN: ui.NewKeyAction("Next Match", d.nextCmd, true), + ui.KeyShiftN: ui.NewKeyAction("Prev Match", d.prevCmd, true), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", d.activateCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", d.clearCmd, false), + tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + tcell.KeyDelete: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + }) + + if !d.searchable { + d.actions.Delete(ui.KeyN, ui.KeyShiftN) + } +} + +func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { + key := evt.Key() + if key == tcell.KeyUp || key == tcell.KeyDown { + return evt + } + + if key == tcell.KeyRune { + if d.filterInput(evt.Rune()) { + return nil + } + key = ui.AsKey(evt) + } + + if a, ok := d.actions[key]; ok { + return a.Action(evt) + } + + return evt +} + +func (d *Details) filterInput(r rune) bool { + if !d.cmdBuff.IsActive() { + return false + } + d.cmdBuff.Add(r) + d.updateTitle() + + return true +} + // StylesChanged notifies the skin changed. func (d *Details) StylesChanged(s *config.Styles) { d.SetBackgroundColor(d.app.Styles.BgColor()) d.SetTextColor(d.app.Styles.FgColor()) d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor)) - d.Update(d.buff) + d.TextChanged(d.model.Peek()) } // Update updates the view content. func (d *Details) Update(buff string) *Details { - d.buff = buff - d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, buff)) - d.ScrollToBeginning() + d.model.SetText(buff) return d } @@ -108,24 +202,89 @@ func (d *Details) ExtraHints() map[string]string { return nil } -func (d *Details) bindKeys() { - d.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), - ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), - }) +func (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.cmdBuff.Empty() { + return evt + } + + d.currentRegion++ + if d.currentRegion >= d.maxRegions { + d.currentRegion = 0 + } + d.Highlight(fmt.Sprintf("search_%d", d.currentRegion)) + d.ScrollToHighlight() + d.updateTitle() + + return nil } -func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - key = tcell.Key(evt.Rune()) - } - if a, ok := d.actions[key]; ok { - return a.Action(evt) +func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.cmdBuff.Empty() { + return evt } - return evt + d.currentRegion-- + if d.currentRegion < 0 { + d.currentRegion = d.maxRegions - 1 + } + d.Highlight(fmt.Sprintf("search_%d", d.currentRegion)) + d.ScrollToHighlight() + d.updateTitle() + + return nil +} + +func (d *Details) filterCmd(evt *tcell.EventKey) *tcell.EventKey { + d.model.Filter(d.cmdBuff.String()) + d.cmdBuff.SetActive(false) + d.updateTitle() + + return nil +} + +func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.app.InCmdMode() { + return evt + } + d.app.Flash().Info("Filter mode activated.") + d.cmdBuff.SetActive(true) + + return nil +} + +func (d *Details) clearCmd(*tcell.EventKey) *tcell.EventKey { + if !d.app.InCmdMode() { + return nil + } + d.cmdBuff.Clear() + + return nil +} + +func (d *Details) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { + if !d.cmdBuff.IsActive() { + return nil + } + d.cmdBuff.Delete() + + return nil +} + +func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + if !d.cmdBuff.InCmdMode() { + d.cmdBuff.Reset() + return d.app.PrevCmd(evt) + } + + if d.cmdBuff.String() != "" { + d.model.ClearFilter() + } + d.app.Flash().Info("Clearing filter...") + d.cmdBuff.SetActive(false) + d.cmdBuff.Reset() + d.updateTitle() + + return nil } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -149,6 +308,19 @@ func (d *Details) updateTitle() { if d.title == "" { return } - title := ui.SkinTitle(fmt.Sprintf(detailsTitleFmt, d.title, d.subject), d.app.Styles.Frame()) - d.SetTitle(title) + fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject) + + buff := d.cmdBuff.String() + if buff == "" { + d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) + return + } + + search := d.cmdBuff.String() + if d.maxRegions != 0 { + search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) + } + fmat += fmt.Sprintf(ui.SearchFmt, search) + + d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) } diff --git a/internal/view/exec.go b/internal/view/exec.go index dc5e39e2..8516e34b 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -13,6 +13,8 @@ import ( "github.com/rs/zerolog/log" ) +const shellCheck = `command -v bash >/dev/null && exec bash || exec sh` + func runK(clear bool, app *App, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { @@ -73,7 +75,7 @@ func execute(clear bool, bin string, bg bool, args ...string) error { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr err = cmd.Run() } - log.Debug().Msgf("Command returned error?? %v", err) + select { case <-ctx.Done(): return errors.New("canceled by operator") diff --git a/internal/view/helpers.go b/internal/view/helpers.go index bafeacfb..58d05ffe 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -67,7 +67,7 @@ func describeResource(app *App, model ui.Tabular, gvr, path string) { return } - details := NewDetails(app, "Describe", path).Update(yaml) + details := NewDetails(app, "Describe", path, true).Update(yaml) if err := app.inject(details); err != nil { app.Flash().Err(err) } diff --git a/internal/view/log.go b/internal/view/log.go index 3865590e..5e7ede41 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -67,7 +67,7 @@ func (l *Log) Init(ctx context.Context) (err error) { l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.AddItem(l.indicator, 1, 1, false) - l.logs = NewDetails(l.app, "", "") + l.logs = NewDetails(l.app, "", "", false) if err = l.logs.Init(ctx); err != nil { return err } @@ -173,7 +173,7 @@ func (l *Log) bindKeys() { ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.clearCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false), tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), diff --git a/internal/view/node.go b/internal/view/node.go index 4d69e677..dafbad67 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -59,7 +59,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(n.App(), "YAML", sel).Update(raw) + details := NewDetails(n.App(), "YAML", sel, true).Update(raw) if err := n.App().inject(details); err != nil { n.App().Flash().Err(err) } diff --git a/internal/view/pod.go b/internal/view/pod.go index cc03de98..2463a808 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" @@ -18,8 +19,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -const shellCheck = "command -v bash >/dev/null && exec bash || exec sh" - // Pod represents a pod viewer. type Pod struct { ResourceViewer diff --git a/internal/view/secret.go b/internal/view/secret.go index a8ea94f0..82eca65c 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -62,7 +62,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(s.App(), "Secret Decoder", path).Update(string(raw)) + details := NewDetails(s.App(), "Secret Decoder", path, false).Update(string(raw)) if err := s.App().inject(details); err != nil { s.App().Flash().Err(err) } diff --git a/internal/view/xray.go b/internal/view/xray.go index d16ce4bc..9613c2d5 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -269,7 +269,7 @@ func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(x.app, "YAML", ref.Path).Update(raw) + details := NewDetails(x.app, "YAML", ref.Path, true).Update(raw) if err := x.app.inject(details); err != nil { x.app.Flash().Err(err) } @@ -321,7 +321,7 @@ func (x *Xray) describe(gvr, path string) { return } - details := NewDetails(x.app, "Describe", path).Update(yaml) + details := NewDetails(x.app, "Describe", path, true).Update(yaml) if err := x.app.inject(details); err != nil { x.app.Flash().Err(err) } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index a25580b9..9ab389e9 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -8,9 +8,8 @@ import ( "strings" "time" - "github.com/derailed/tview" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" "github.com/rs/zerolog/log" ) @@ -26,6 +25,7 @@ const ( ) func colorizeYAML(style config.Yaml, raw string) string { + // lines := strings.Split(raw, "\n") lines := strings.Split(tview.Escape(raw), "\n") fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor, 1) @@ -41,22 +41,26 @@ func colorizeYAML(style config.Yaml, raw string) string { for _, l := range lines { res := keyValRX.FindStringSubmatch(l) if len(res) == 4 { - buff = append(buff, fmt.Sprintf(fullFmt, res[1], res[2], res[3])) + buff = append(buff, enableRegion(fmt.Sprintf(fullFmt, res[1], res[2], res[3]))) continue } res = keyRX.FindStringSubmatch(l) if len(res) == 3 { - buff = append(buff, fmt.Sprintf(keyFmt, res[1], res[2])) + buff = append(buff, enableRegion(fmt.Sprintf(keyFmt, res[1], res[2]))) continue } - buff = append(buff, fmt.Sprintf(valFmt, l)) + buff = append(buff, enableRegion(fmt.Sprintf(valFmt, l))) } return strings.Join(buff, "\n") } +func enableRegion(str string) string { + return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") +} + func saveYAML(cluster, name, data string) (string, error) { dir := filepath.Join(config.K9sDumpDir, cluster) if err := ensureDir(dir); err != nil { diff --git a/internal/view/yaml_test.go b/internal/view/yaml_test.go index 41451559..5e7ed5e9 100644 --- a/internal/view/yaml_test.go +++ b/internal/view/yaml_test.go @@ -13,15 +13,21 @@ func TestYaml(t *testing.T) { }{ { `api: fred - version: v1`, + version: v1`, `[steelblue::b]api[white::-]: [papayawhip::]fred - [steelblue::b]version[white::-]: [papayawhip::]v1`, + [steelblue::b]version[white::-]: [papayawhip::]v1`, + }, + { + `api: <<<"search_0">>>fred<<<"">>> + version: v1`, + `[steelblue::b]api[white::-]: [papayawhip::]["search_0"]fred[""] + [steelblue::b]version[white::-]: [papayawhip::]v1`, }, { `api: - version: v1`, + version: v1`, `[steelblue::b]api[white::-]: - [steelblue::b]version[white::-]: [papayawhip::]v1`, + [steelblue::b]version[white::-]: [papayawhip::]v1`, }, { " fred:blee",