k9s/internal/view/details.go

339 lines
7.9 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package view
import (
"context"
"fmt"
"io"
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/sahilm/fuzzy"
"k8s.io/apimachinery/pkg/labels"
)
const (
detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
contentTXT = "text"
contentYAML = "yaml"
)
// Details represents a generic text viewer.
type Details struct {
*tview.Flex
text *tview.TextView
actions *ui.KeyActions
app *App
title, subject string
cmdBuff *model.FishBuff
model *model.Text
currentRegion, maxRegions int
searchable bool
fullScreen bool
contentType string
}
// NewDetails returns a details viewer.
func NewDetails(app *App, title, subject, contentType string, searchable bool) *Details {
d := Details{
Flex: tview.NewFlex(),
text: tview.NewTextView(),
app: app,
title: title,
subject: subject,
actions: ui.NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
model: model.NewText(),
searchable: searchable,
contentType: contentType,
}
d.AddItem(d.text, 0, 1, true)
return &d
}
func (*Details) SetCommand(*cmd.Interpreter) {}
func (*Details) SetFilter(string, bool) {}
func (*Details) SetLabelSelector(labels.Selector, bool) {}
// Init initializes the viewer.
func (d *Details) Init(_ context.Context) error {
if d.title != "" {
d.SetBorder(true)
}
d.text.SetScrollable(true).SetWrap(true).SetRegions(true)
d.text.SetDynamicColors(true)
d.text.SetHighlightColor(tcell.ColorOrange)
d.SetTitleColor(tcell.ColorAqua)
d.SetInputCapture(d.keyboard)
d.SetBorderPadding(0, 0, 1, 1)
d.updateTitle()
d.app.Styles.AddListener(d)
d.StylesChanged(d.app.Styles)
d.setFullScreen(d.app.Config.K9s.UI.DefaultsToFullScreen)
d.app.Prompt().SetModel(d.cmdBuff)
d.cmdBuff.AddListener(d)
d.bindKeys()
d.SetInputCapture(d.keyboard)
d.model.AddListener(d)
return nil
}
// InCmdMode checks if prompt is active.
func (d *Details) InCmdMode() bool {
return d.cmdBuff.InCmdMode()
}
// TextChanged notifies the model changed.
func (d *Details) TextChanged(lines []string) {
switch d.contentType {
case contentYAML:
d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n")))
default:
d.text.SetText(strings.Join(lines, "\n"))
}
d.text.ScrollToBeginning()
}
// TextFiltered notifies when the filter changed.
func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
d.currentRegion, d.maxRegions = 0, len(matches)
ll := linesWithRegions(lines, matches)
d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
d.text.Highlight()
if len(matches) > 0 {
d.text.Highlight("search_0")
d.text.ScrollToHighlight()
}
}
// BufferChanged indicates the buffer was changed.
func (*Details) BufferChanged(_, _ string) {}
// BufferCompleted indicates input was accepted.
func (d *Details) BufferCompleted(text, _ string) {
d.model.Filter(text)
d.updateTitle()
}
// BufferActive indicates the buff activity changed.
func (d *Details) BufferActive(state bool, k model.BufferKind) {
d.app.BufferActive(state, k)
}
func (d *Details) bindKeys() {
d.actions.Bulk(ui.KeyMap{
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", cpCmd(d.app.Flash(), d.text), true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", d.toggleFullScreenCmd, 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.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 {
if a, ok := d.actions.Get(ui.AsKey(evt)); ok {
return a.Action(evt)
}
return evt
}
// StylesChanged notifies the skin changed.
func (d *Details) StylesChanged(s *config.Styles) {
d.SetBackgroundColor(s.BgColor())
d.text.SetTextColor(s.FgColor())
d.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())
d.TextChanged(d.model.Peek())
}
// Update updates the view content.
func (d *Details) Update(buff string) *Details {
d.model.SetText(buff)
return d
}
func (d *Details) GetWriter() io.Writer {
return d.text
}
// SetSubject updates the subject.
func (d *Details) SetSubject(s string) {
d.subject = s
}
// Actions returns menu actions.
func (d *Details) Actions() *ui.KeyActions {
return d.actions
}
// Name returns the component name.
func (d *Details) Name() string { return d.title }
// Start starts the view updater.
func (*Details) Start() {}
// Stop terminates the updater.
func (d *Details) Stop() {
d.app.Styles.RemoveListener(d)
}
// Hints returns menu hints.
func (d *Details) Hints() model.MenuHints {
return d.actions.Hints()
}
// ExtraHints returns additional hints.
func (*Details) ExtraHints() map[string]string {
return nil
}
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.text.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.text.ScrollToHighlight()
d.updateTitle()
return nil
}
func (d *Details) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.app.InCmdMode() {
return evt
}
d.setFullScreen(!d.fullScreen)
return nil
}
func (d *Details) setFullScreen(isFullScreen bool) {
d.fullScreen = isFullScreen
d.SetFullScreen(isFullScreen)
d.SetBorder(!isFullScreen)
if isFullScreen {
d.SetBorderPadding(0, 0, 0, 0)
} else {
d.SetBorderPadding(0, 0, 1, 1)
}
}
func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.cmdBuff.Empty() {
return evt
}
d.currentRegion--
if d.currentRegion < 0 {
d.currentRegion = d.maxRegions - 1
}
d.text.Highlight(fmt.Sprintf("search_%d", d.currentRegion))
d.text.ScrollToHighlight()
d.updateTitle()
return nil
}
func (d *Details) filterCmd(*tcell.EventKey) *tcell.EventKey {
d.model.Filter(d.cmdBuff.GetText())
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.ResetPrompt(d.cmdBuff)
return nil
}
func (d *Details) eraseCmd(*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.GetText() != "" {
d.model.ClearFilter()
}
d.cmdBuff.SetActive(false)
d.cmdBuff.Reset()
d.updateTitle()
return nil
}
func (d *Details) saveCmd(*tcell.EventKey) *tcell.EventKey {
if path, err := saveYAML(d.app.Config.K9s.ContextScreenDumpDir(), d.title, d.text.GetText(true)); err != nil {
d.app.Flash().Err(err)
} else {
d.app.Flash().Infof("Log %s saved successfully!", path)
}
return nil
}
func (d *Details) updateTitle() {
if d.title == "" {
return
}
fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject)
var (
buff = d.cmdBuff.GetText()
styles = d.app.Styles.Frame()
)
if buff == "" {
d.SetTitle(ui.SkinTitle(fmat, &styles))
return
}
if d.maxRegions != 0 {
buff += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions)
}
fmat += fmt.Sprintf(ui.SearchFmt, buff)
d.SetTitle(ui.SkinTitle(fmat, &styles))
}