parent
5b707c656e
commit
67c0702bf2
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue