add searchable yaml/describe. Fix #464, #526

mine
derailed 2020-02-10 14:28:22 -07:00
parent 5b707c656e
commit 67c0702bf2
17 changed files with 453 additions and 60 deletions

View File

@ -143,7 +143,7 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
func (a *Aliases) LoadAliases(path string) error { func (a *Aliases) LoadAliases(path string) error {
f, err := ioutil.ReadFile(path) f, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("No custom aliases found") log.Debug().Err(err).Msgf("No custom aliases found")
return nil return nil
} }

120
internal/model/text.go Normal file
View File

@ -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
}

View File

@ -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]
}
}

View File

@ -175,7 +175,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
a.cmdBuff.Add(evt.Rune()) a.cmdBuff.Add(evt.Rune())
return nil return nil
} }
key = asKey(evt) key = AsKey(evt)
} }
if a, ok := a.actions[key]; ok { if a, ok := a.actions[key]; ok {
@ -258,7 +258,7 @@ func (a *App) Menu() *Menu {
// Helpers... // Helpers...
// AsKey converts rune to keyboard key., // 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()) key := tcell.Key(evt.Rune())
if evt.Modifiers() == tcell.ModAlt { if evt.Modifiers() == tcell.ModAlt {
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))

View File

@ -121,7 +121,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if t.filterInput(evt.Rune()) { if t.filterInput(evt.Rune()) {
return nil return nil
} }
key = asKey(evt) key = AsKey(evt)
} }
if a, ok := t.actions[key]; ok { if a, ok := t.actions[key]; ok {
@ -370,17 +370,17 @@ func (t *Table) styleTitle() string {
} }
} }
buff := t.cmdBuff.String()
var title string var title string
if ns == client.ClusterScope { if ns == client.ClusterScope {
title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame())
} else { } else {
title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame())
} }
buff := t.cmdBuff.String()
if buff == "" { if buff == "" {
return title return title
} }
if IsLabelSelector(buff) { if IsLabelSelector(buff) {
buff = TrimLabelSelector(buff) buff = TrimLabelSelector(buff)
} }

View File

@ -46,7 +46,7 @@ func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr, path string) {
return 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 { if err := app.inject(details); err != nil {
app.Flash().Err(err) app.Flash().Err(err)
} }

View File

@ -175,7 +175,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil 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 { if err := b.App().inject(details); err != nil {
b.App().Flash().Err(err) b.App().Flash().Err(err)
} }

View File

@ -3,6 +3,7 @@ package view
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
@ -10,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/sahilm/fuzzy"
) )
const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " 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 { type Details struct {
*tview.TextView *tview.TextView
actions ui.KeyActions actions ui.KeyActions
app *App app *App
title, subject string title, subject string
buff string cmdBuff *ui.CmdBuff
model *model.Text
currentRegion, maxRegions int
searchable bool
} }
// NewDetails returns a details viewer. // 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{ d := Details{
TextView: tview.NewTextView(), TextView: tview.NewTextView(),
app: app, app: app,
title: title, title: title,
subject: subject, subject: subject,
actions: make(ui.KeyActions), actions: make(ui.KeyActions),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
model: model.NewText(),
searchable: searchable,
} }
return &d return &d
@ -42,37 +50,123 @@ func (d *Details) Init(_ context.Context) error {
if d.title != "" { if d.title != "" {
d.SetBorder(true) d.SetBorder(true)
} }
d.SetScrollable(true) d.SetScrollable(true).SetWrap(true).SetRegions(true)
d.SetWrap(true)
d.SetDynamicColors(true) d.SetDynamicColors(true)
d.SetHighlightColor(tcell.ColorOrange) d.SetHighlightColor(tcell.ColorOrange)
d.SetTitleColor(tcell.ColorAqua) d.SetTitleColor(tcell.ColorAqua)
d.SetInputCapture(d.keyboard) d.SetInputCapture(d.keyboard)
d.bindKeys()
d.SetChangedFunc(func() { d.SetChangedFunc(func() {
d.app.Draw() d.app.Draw()
}) })
d.updateTitle() d.updateTitle()
d.app.Styles.AddListener(d) d.app.Styles.AddListener(d)
d.StylesChanged(d.app.Styles) 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 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. // StylesChanged notifies the skin changed.
func (d *Details) StylesChanged(s *config.Styles) { func (d *Details) StylesChanged(s *config.Styles) {
d.SetBackgroundColor(d.app.Styles.BgColor()) d.SetBackgroundColor(d.app.Styles.BgColor())
d.SetTextColor(d.app.Styles.FgColor()) d.SetTextColor(d.app.Styles.FgColor())
d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor)) d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor))
d.Update(d.buff) d.TextChanged(d.model.Peek())
} }
// Update updates the view content. // Update updates the view content.
func (d *Details) Update(buff string) *Details { func (d *Details) Update(buff string) *Details {
d.buff = buff d.model.SetText(buff)
d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, buff))
d.ScrollToBeginning()
return d return d
} }
@ -108,24 +202,89 @@ func (d *Details) ExtraHints() map[string]string {
return nil return nil
} }
func (d *Details) bindKeys() { func (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
d.actions.Set(ui.KeyActions{ if d.cmdBuff.Empty() {
tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false), return evt
tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), }
ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true),
}) 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 { func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key() if d.cmdBuff.Empty() {
if key == tcell.KeyRune { return evt
key = tcell.Key(evt.Rune())
}
if a, ok := d.actions[key]; ok {
return a.Action(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 { func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
@ -149,6 +308,19 @@ func (d *Details) updateTitle() {
if d.title == "" { if d.title == "" {
return return
} }
title := ui.SkinTitle(fmt.Sprintf(detailsTitleFmt, d.title, d.subject), d.app.Styles.Frame()) fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject)
d.SetTitle(title)
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()))
} }

View File

@ -13,6 +13,8 @@ import (
"github.com/rs/zerolog/log" "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 { func runK(clear bool, app *App, args ...string) bool {
bin, err := exec.LookPath("kubectl") bin, err := exec.LookPath("kubectl")
if err != nil { 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 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
err = cmd.Run() err = cmd.Run()
} }
log.Debug().Msgf("Command returned error?? %v", err)
select { select {
case <-ctx.Done(): case <-ctx.Done():
return errors.New("canceled by operator") return errors.New("canceled by operator")

View File

@ -67,7 +67,7 @@ func describeResource(app *App, model ui.Tabular, gvr, path string) {
return return
} }
details := NewDetails(app, "Describe", path).Update(yaml) details := NewDetails(app, "Describe", path, true).Update(yaml)
if err := app.inject(details); err != nil { if err := app.inject(details); err != nil {
app.Flash().Err(err) app.Flash().Err(err)
} }

View File

@ -67,7 +67,7 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.indicator = NewLogIndicator(l.app.Config, l.app.Styles)
l.AddItem(l.indicator, 1, 1, false) 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 { if err = l.logs.Init(ctx); err != nil {
return err return err
} }
@ -173,7 +173,7 @@ func (l *Log) bindKeys() {
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), 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.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),

View File

@ -59,7 +59,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil 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 { if err := n.App().inject(details); err != nil {
n.App().Flash().Err(err) n.App().Flash().Err(err)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
@ -18,8 +19,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
const shellCheck = "command -v bash >/dev/null && exec bash || exec sh"
// Pod represents a pod viewer. // Pod represents a pod viewer.
type Pod struct { type Pod struct {
ResourceViewer ResourceViewer

View File

@ -62,7 +62,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil 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 { if err := s.App().inject(details); err != nil {
s.App().Flash().Err(err) s.App().Flash().Err(err)
} }

View File

@ -269,7 +269,7 @@ func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil 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 { if err := x.app.inject(details); err != nil {
x.app.Flash().Err(err) x.app.Flash().Err(err)
} }
@ -321,7 +321,7 @@ func (x *Xray) describe(gvr, path string) {
return 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 { if err := x.app.inject(details); err != nil {
x.app.Flash().Err(err) x.app.Flash().Err(err)
} }

View File

@ -8,9 +8,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/derailed/tview"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -26,6 +25,7 @@ const (
) )
func colorizeYAML(style config.Yaml, raw string) string { func colorizeYAML(style config.Yaml, raw string) string {
// lines := strings.Split(raw, "\n")
lines := strings.Split(tview.Escape(raw), "\n") lines := strings.Split(tview.Escape(raw), "\n")
fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor, 1) fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor, 1)
@ -41,22 +41,26 @@ func colorizeYAML(style config.Yaml, raw string) string {
for _, l := range lines { for _, l := range lines {
res := keyValRX.FindStringSubmatch(l) res := keyValRX.FindStringSubmatch(l)
if len(res) == 4 { 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 continue
} }
res = keyRX.FindStringSubmatch(l) res = keyRX.FindStringSubmatch(l)
if len(res) == 3 { 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 continue
} }
buff = append(buff, fmt.Sprintf(valFmt, l)) buff = append(buff, enableRegion(fmt.Sprintf(valFmt, l)))
} }
return strings.Join(buff, "\n") return strings.Join(buff, "\n")
} }
func enableRegion(str string) string {
return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]")
}
func saveYAML(cluster, name, data string) (string, error) { func saveYAML(cluster, name, data string) (string, error) {
dir := filepath.Join(config.K9sDumpDir, cluster) dir := filepath.Join(config.K9sDumpDir, cluster)
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {

View File

@ -13,15 +13,21 @@ func TestYaml(t *testing.T) {
}{ }{
{ {
`api: fred `api: fred
version: v1`, version: v1`,
`[steelblue::b]api[white::-]: [papayawhip::]fred `[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: `api:
version: v1`, version: v1`,
`[steelblue::b]api[white::-]: `[steelblue::b]api[white::-]:
[steelblue::b]version[white::-]: [papayawhip::]v1`, [steelblue::b]version[white::-]: [papayawhip::]v1`,
}, },
{ {
" fred:blee", " fred:blee",