add liveviews for describe/yaml

mine
derailed 2020-10-27 19:38:54 -06:00
parent dbfa66c9de
commit 087e6643f9
30 changed files with 1072 additions and 233 deletions

View File

@ -14,28 +14,75 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
## ♫ Sound Behind The Release ♭
I figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing this release notes!
I figured why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing this release notes!
[On An Island - David Gilmour With Crosby&Nash](https://www.youtube.com/watch?v=kEa__0wtIRo)
## Our K9s Heroes
Please join me in recognizing and applauding this drop contributors that went the extra mile to make sure K9s is better and more useful for all of us!!
Big ATTA BOY/GIRL! in full effect this week to the good folks below for their efforts and contributions to K9s!!
* [Antoine Méausoone](https://github.com/Ameausoone)
* [Michael Albers](https://github.com/michaeljohnalbers)
* [Wi1dcard](https://github.com/wi1dcard)
* [Saskia Keil](https://github.com/SaskiaKeil)
* [Tomasz Lipinski](https://github.com/tlipinski)
* [Emeric Martineau](https://github.com/emeric-martineau)
* [Eldad Assis](https://github.com/eldada)
* [David Arnold](https://github.com/blaggacao)
* [Peter Parente](https://github.com/parente)
## A Word From Our Sponsors...
First off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!
First off I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!
* [Martin Kemp](https://github.com/MartiUK)
* [William Alexander](https://github.com/carpetfuz)
* [Jiri Valnoha](https://github.com/waldauf)
* [Pavel Tumik](https://github.com/sagor999)
* [Bart Plasmeijer](https://github.com/bplasmeijer)
* [Matt Welke](https://github.com/mattwelke)
* [Stefan Mikolajczyk](https://github.com/stefanmiko)
Contrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets or a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you or your organization and part of your daily Kubernetes lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!
## Full Screen
We've added a new option to enable full screen while describing or viewing a resource YAML namely `f`. This works similarly to the full screen toggle option while viewing logs ie pressing `f` will toggle fullscreen on/off.
Contrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!
## Best Effort... Not!
In this drop, we've added 2 new columns to the Pod/Container views namely `CPU(R:L)` and `MEM(R:L)`. These represents the current request/limit resources specified at either the pod or container level. While in Pod view, you will need to use the wide command `Ctrl-W` to see the resources set at the pod level or you can use K9s column customization feature to volunteer them by default.
In this drop, we've added 2 new columns to the Pod/Container views namely `CPU(R:L)` and `MEM(R:L)`. These represents the current request/limit resources specified at either the pod or container level. While in Pod view, you will need to use the wide command `Ctrl-W` to see the resources set at the pod level or you can leverage K9s column customization feature to volunteer them while in Pod view. In the Container view these columns will be available by default.
## Container Images
You have now the ability to tweak your container images for experimentation, using the new SetImage binding aka `i`. This feature is available for standalone pods, deployments, sts and ds. With a resource selected, pressing `i` will provision an edit dialog listing all init/container images.
You have now the ability to tweak your container images for experimentation, using the new SetImage binding aka `i`. This feature is available for unmanaged pods, deployments, sts and ds. With a resource selected, pressing `i` will provision an edit dialog listing all init/container images.
NOTE! This is a one shot commands applied directly against your cluster and won't survive a new resource deployment.
Big `ATTA Boy!` in effect to [Antoine Méausoone](https://github.com/Ameausoone) for putting forth the effort to make this feature available to all of us!!
## Crumbs On, Crumbs Off, Caterpillar
We've added a new configuration to turn off the crumbs via `crumbsLess` configuration option. You can also toggle the crumbs via the new key option `C`. You can enable/disable this option in your ~/.k9s/config.yml or via command line using `--crumbsless` flag.
```yaml
k9s:
refreshRate: 2
headless: false
crumbsless: false
readOnly: true
...
```
## FILTER...NOT!
Some folks have voiced the desire to use inverse filters while filtering to content in the resource table views. There is now a new filter option available when performing these filtering operations. For example, in order to see all pods that are not named `fred` you can now use `/!fred` as your filtering command.
## Disturbance In the Keyboard Force...
In this drop we've changed the key binding to toggle the header from `Ctrl-E` to `H`
---
## Resolved Issues/Features
@ -54,6 +101,17 @@ Big `ATTA Boy!` in effect to [Antoine Méausoone](https://github.com/Ameausoone)
## Resolved PRs
* [PR #909](https://github.com/derailed/k9s/pull/909) Add support for inverse filtering
* [PR #908](https://github.com/derailed/k9s/pull/908) Remove trailing delta from the scale dialog when replicas are in flux
* [PR #907](https://github.com/derailed/k9s/pull/907) Improve docs on sinceSeconds logger option
* [PR #904](https://github.com/derailed/k9s/pull/904) PVC `UsedBy` list irrelevant statefulsets
* [PR #898](https://github.com/derailed/k9s/pull/898) Use config.CallTimeout in APIClient
* [PR #897](https://github.com/derailed/k9s/pull/897) Use DefaultColorer for aliases rendering
* [PR #896](https://github.com/derailed/k9s/pull/896) Allow remove crumbs
* [PR #894](https://github.com/derailed/k9s/pull/894) Execute plugins and pass context
* [PR #891](https://github.com/derailed/k9s/pull/891) Add command to get the latest stable kubectl version and support for KUBECTL_VERSION as Dockerfile ARG
* [PR #847](https://github.com/derailed/k9s/pull/847) Add ability to set container images
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.15
require (
github.com/atotto/clipboard v0.1.2
github.com/cenkalti/backoff/v4 v4.1.0
github.com/derailed/popeye v0.8.10
github.com/derailed/tview v0.4.6
github.com/drone/envsubst v1.0.2 // indirect

4
go.sum
View File

@ -96,6 +96,10 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=

View File

@ -59,8 +59,8 @@ func (k *K9s) OverrideCommand(cmd string) {
k.manualCommand = &cmd
}
// GetHeadless returns headless setting.
func (k *K9s) GetHeadless() bool {
// IsHeadless returns headless setting.
func (k *K9s) IsHeadless() bool {
h := k.Headless
if k.manualHeadless != nil && *k.manualHeadless {
h = *k.manualHeadless
@ -69,8 +69,8 @@ func (k *K9s) GetHeadless() bool {
return h
}
// GetCrumbsless returns crumbsless setting.
func (k *K9s) GetCrumbsless() bool {
// IsCrumbsless returns crumbsless setting.
func (k *K9s) IsCrumbsless() bool {
h := k.Crumbsless
if k.manualCrumbsless != nil && *k.manualCrumbsless {
h = *k.manualCrumbsless

View File

@ -35,7 +35,6 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error)
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
return "", err
}
log.Debug().Msgf("Describing %q -- %q", ns, n)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
}

View File

@ -78,13 +78,13 @@ func (g *Generic) Describe(path string) (string, error) {
}
// ToYAML returns a resource yaml.
func (g *Generic) ToYAML(path string) (string, error) {
func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
o, err := g.Get(context.Background(), path)
if err != nil {
return "", err
}
raw, err := ToYAML(o)
raw, err := ToYAML(o, showManaged)
if err != nil {
return "", fmt.Errorf("unable to marshal resource %s", err)
}

View File

@ -74,7 +74,7 @@ func (c *Helm) Describe(path string) (string, error) {
}
// ToYAML returns the chart manifest.
func (c *Helm) ToYAML(path string) (string, error) {
func (c *Helm) ToYAML(path string, showManaged bool) (string, error) {
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {

View File

@ -8,6 +8,7 @@ import (
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers"
)
@ -33,7 +34,7 @@ func Truncate(str string, width int) string {
}
// ToYAML converts a resource to its YAML representation.
func ToYAML(o runtime.Object) (string, error) {
func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if o == nil {
return "", errors.New("no object to yamlize")
}
@ -42,6 +43,13 @@ func ToYAML(o runtime.Object) (string, error) {
buff bytes.Buffer
p printers.YAMLPrinter
)
if !showManaged {
o = o.DeepCopyObject()
uo := o.(*unstructured.Unstructured).Object
if meta, ok := uo["metadata"].(map[string]interface{}); ok {
delete(meta, "managedFields")
}
}
err := p.PrintObj(o, &buff)
if err != nil {
log.Error().Msgf("Marshal Error %v", err)

View File

@ -3,6 +3,7 @@ package dao
import (
"context"
"fmt"
"sync"
"github.com/derailed/k9s/internal/client"
"k8s.io/apimachinery/pkg/runtime"
@ -13,15 +14,20 @@ type NonResource struct {
Factory
gvr client.GVR
mx sync.RWMutex
}
// Init initializes the resource.
func (n *NonResource) Init(f Factory, gvr client.GVR) {
n.mx.Lock()
defer n.mx.Unlock()
n.Factory, n.gvr = f, gvr
}
// GVR returns a gvr.
func (n *NonResource) GVR() string {
n.mx.RLock()
defer n.mx.RUnlock()
return n.gvr.String()
}

View File

@ -113,7 +113,7 @@ func (f *OpenFaas) Delete(path string, _, _ bool) error {
}
// ToYAML dumps a function to yaml.
func (f *OpenFaas) ToYAML(path string) (string, error) {
func (f *OpenFaas) ToYAML(path string, _ bool) (string, error) {
return f.Describe(path)
}

View File

@ -39,13 +39,13 @@ func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) {
}
// ToYAML returns a resource yaml.
func (r *Resource) ToYAML(path string) (string, error) {
func (r *Resource) ToYAML(path string, showManaged bool) (string, error) {
o, err := r.Get(context.Background(), path)
if err != nil {
return "", err
}
raw, err := ToYAML(o)
raw, err := ToYAML(o, showManaged)
if err != nil {
return "", fmt.Errorf("unable to marshal resource %s", err)
}

View File

@ -102,7 +102,7 @@ type Describer interface {
Describe(path string) (string, error)
// ToYAML dumps a resource to YAML.
ToYAML(path string) (string, error)
ToYAML(path string, showManaged bool) (string, error)
}
// Scalable represents resources that can scale.

View File

@ -2,13 +2,14 @@ package model
import (
"context"
"sync"
"time"
)
const (
maxBuff = 10
keyEntryDelay = 300 * time.Millisecond
keyEntryDelay = 200 * time.Millisecond
// CommandBuffer represents a command buffer.
CommandBuffer BufferKind = 1 << iota
@ -41,6 +42,7 @@ type CmdBuff struct {
kind BufferKind
active bool
cancel context.CancelFunc
mx sync.RWMutex
}
// NewCmdBuff returns a new command buffer.
@ -82,6 +84,8 @@ func (c *CmdBuff) SetText(cmd string) {
// Add adds a new character to the buffer.
func (c *CmdBuff) Add(r rune) {
c.mx.Lock()
defer c.mx.Unlock()
c.buff = append(c.buff, r)
c.fireBufferChanged()
if c.cancel != nil {
@ -92,13 +96,19 @@ func (c *CmdBuff) Add(r rune) {
go func() {
<-ctx.Done()
c.fireBufferCompleted()
c.cancel = nil
c.mx.Lock()
{
c.fireBufferCompleted()
c.cancel = nil
}
c.mx.Unlock()
}()
}
// Delete removes the last character from the buffer.
func (c *CmdBuff) Delete() {
c.mx.Lock()
defer c.mx.Unlock()
if c.Empty() {
return
}
@ -113,13 +123,19 @@ func (c *CmdBuff) Delete() {
go func() {
<-ctx.Done()
c.fireBufferCompleted()
c.cancel = nil
c.mx.Lock()
{
c.fireBufferCompleted()
c.cancel = nil
}
c.mx.Unlock()
}()
}
// ClearText clears out command buffer.
func (c *CmdBuff) ClearText(fire bool) {
c.mx.Lock()
defer c.mx.Unlock()
c.buff = make([]rune, 0, maxBuff)
if fire {
c.fireBufferCompleted()
@ -143,11 +159,16 @@ func (c *CmdBuff) Empty() bool {
// AddListener registers a cmd buffer listener.
func (c *CmdBuff) AddListener(w BuffWatcher) {
c.mx.Lock()
defer c.mx.Unlock()
c.listeners = append(c.listeners, w)
}
// RemoveListener removes a listener.
func (c *CmdBuff) RemoveListener(l BuffWatcher) {
c.mx.Lock()
defer c.mx.Unlock()
victim := -1
for i, lis := range c.listeners {
if l == lis {
@ -163,14 +184,16 @@ func (c *CmdBuff) RemoveListener(l BuffWatcher) {
}
func (c *CmdBuff) fireBufferCompleted() {
text := c.GetText()
for _, l := range c.listeners {
l.BufferCompleted(c.GetText())
l.BufferCompleted(text)
}
}
func (c *CmdBuff) fireBufferChanged() {
text := c.GetText()
for _, l := range c.listeners {
l.BufferChanged(c.GetText())
l.BufferChanged(text)
}
}

211
internal/model/describe.go Normal file
View File

@ -0,0 +1,211 @@
package model
import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"sync/atomic"
"time"
backoff "github.com/cenkalti/backoff/v4"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
type ResourceViewerListener interface {
ResourceChanged(lines []string, matches fuzzy.Matches)
ResourceFailed(error)
}
type ResourceViewer interface {
GetPath() string
Filter(string)
ClearFilter()
Peek() []string
Watch(context.Context)
AddListener(ResourceViewerListener)
RemoveListener(ResourceViewerListener)
}
// Describe tracks describeable resources.
type Describe struct {
gvr client.GVR
inUpdate int32
path string
query string
lines []string
refreshRate time.Duration
listeners []ResourceViewerListener
}
// NewDescribe returns a new describe resource model.
func NewDescribe(gvr client.GVR, path string) *Describe {
return &Describe{
gvr: gvr,
path: path,
refreshRate: 2 * time.Second,
}
}
// GetPath returns the active resource path.
func (d *Describe) GetPath() string {
return d.path
}
// Filter filters the model.
func (d *Describe) Filter(q string) {
d.query = q
d.filterChanged(d.lines)
}
func (d *Describe) filterChanged(lines []string) {
d.fireResourceChanged(lines, d.filter(d.query, lines))
}
func (d *Describe) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
if dao.IsFuzzySelector(q) {
return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return d.rxFilter(q, lines)
}
func (*Describe) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*Describe) 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
}
func (d *Describe) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range d.listeners {
l.ResourceChanged(lines, matches)
}
}
func (d *Describe) fireResourceFailed(err error) {
for _, l := range d.listeners {
l.ResourceFailed(err)
}
}
// ClearFilter clear out the filter
func (d *Describe) ClearFilter() {
}
// Peek returns current model state.
func (d *Describe) Peek() []string {
return d.lines
}
// Watch watches for describe data changes.
func (d *Describe) Watch(ctx context.Context) {
d.refresh(ctx)
go d.updater(ctx)
}
func (d *Describe) updater(ctx context.Context) {
defer log.Debug().Msgf("Describe canceled -- %q", d.gvr)
bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval
rate := initRefreshRate
for {
select {
case <-ctx.Done():
return
case <-time.After(rate):
rate = d.refreshRate
err := backoff.Retry(func() error {
return d.refresh(ctx)
}, backoff.WithContext(bf, ctx))
if err != nil {
log.Error().Err(err).Msgf("Retry failed")
d.fireResourceFailed(err)
return
}
}
}
}
func (d *Describe) refresh(ctx context.Context) error {
if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return nil
}
defer atomic.StoreInt32(&d.inUpdate, 0)
if err := d.reconcile(ctx); err != nil {
log.Error().Err(err).Msgf("reconcile failed %q", d.gvr)
d.fireResourceFailed(err)
return err
}
return nil
}
func (d *Describe) reconcile(ctx context.Context) error {
s, err := d.describe(ctx, d.gvr, d.path)
if err != nil {
return err
}
lines := strings.Split(s, "\n")
if reflect.DeepEqual(lines, d.lines) {
return nil
}
d.lines = lines
d.fireResourceChanged(d.lines, d.filter(d.query, d.lines))
return nil
}
// Describe describes a given resource.
func (d *Describe) describe(ctx context.Context, gvr client.GVR, path string) (string, error) {
meta, err := getMeta(ctx, gvr)
if err != nil {
return "", err
}
desc, ok := meta.DAO.(dao.Describer)
if !ok {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
return desc.Describe(path)
}
// AddListener adds a new model listener.
func (d *Describe) AddListener(l ResourceViewerListener) {
d.listeners = append(d.listeners, l)
}
// RemoveListener delete a listener from the list.
func (d *Describe) RemoveListener(l ResourceViewerListener) {
victim := -1
for i, lis := range d.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...)
}
}

View File

@ -51,7 +51,9 @@ func NewTable(gvr client.GVR) *Table {
// SetLabelFilter sets the labels filter.
func (t *Table) SetLabelFilter(f string) {
t.mx.Lock()
t.labelFilter = f
t.mx.Unlock()
}
// SetInstance sets a single entry table.
@ -94,7 +96,7 @@ func (t *Table) Refresh(ctx context.Context) {
// Get returns a resource instance if found, else an error.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
meta, err := t.getMeta(ctx)
meta, err := getMeta(ctx, t.gvr)
if err != nil {
return nil, err
}
@ -104,7 +106,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
// Delete deletes a resource.
func (t *Table) Delete(ctx context.Context, path string, cascade, force bool) error {
meta, err := t.getMeta(ctx)
meta, err := getMeta(ctx, t.gvr)
if err != nil {
return err
}
@ -119,7 +121,7 @@ func (t *Table) Delete(ctx context.Context, path string, cascade, force bool) er
// Describe describes a given resource.
func (t *Table) Describe(ctx context.Context, path string) (string, error) {
meta, err := t.getMeta(ctx)
meta, err := getMeta(ctx, t.gvr)
if err != nil {
return "", err
}
@ -134,7 +136,7 @@ func (t *Table) Describe(ctx context.Context, path string) (string, error) {
// ToYAML returns a resource yaml.
func (t *Table) ToYAML(ctx context.Context, path string) (string, error) {
meta, err := t.getMeta(ctx)
meta, err := getMeta(ctx, t.gvr)
if err != nil {
return "", err
}
@ -144,7 +146,7 @@ func (t *Table) ToYAML(ctx context.Context, path string) (string, error) {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
return desc.ToYAML(path)
return desc.ToYAML(path, false)
}
// GetNamespace returns the model namespace.
@ -232,7 +234,9 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
}
func (t *Table) reconcile(ctx context.Context) error {
meta := t.resourceMeta()
t.mx.Lock()
defer t.mx.Unlock()
meta := resourceMeta(t.gvr)
if t.labelFilter != "" {
ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
}
@ -269,8 +273,6 @@ func (t *Table) reconcile(ctx context.Context) error {
}
}
t.mx.Lock()
defer t.mx.Unlock()
// if labelSelector in place might as well clear the model data.
sel, ok := ctx.Value(internal.KeyLabels).(string)
if ok && sel != "" {
@ -286,32 +288,6 @@ func (t *Table) reconcile(ctx context.Context) error {
return nil
}
func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
meta := t.resourceMeta()
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, t.gvr)
return meta, nil
}
func (t *Table) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}
func (t *Table) fireTableChanged(data render.TableData) {
t.mx.RLock()
defer t.mx.RUnlock()

View File

@ -91,7 +91,7 @@ func TestTableMeta(t *testing.T) {
for k := range uu {
u := uu[k]
ta := NewTable(client.NewGVR(u.gvr))
m := ta.resourceMeta()
m := resourceMeta(ta.gvr)
assert.Equal(t, u.accessor, m.DAO)
assert.Equal(t, u.renderer, m.Renderer)

View File

@ -8,6 +8,18 @@ import (
"github.com/sahilm/fuzzy"
)
type Filterable interface {
Filter(string)
ClearFilter()
}
type Textable interface {
Peek() []string
SetText(string)
AddListener(TextListener)
RemoveListener(TextListener)
}
// TextListener represents a text model listener.
type TextListener interface {
// TextChanged notifies the model changed.

View File

@ -155,7 +155,7 @@ func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
return desc.ToYAML(path)
return desc.ToYAML(path, false)
}
func (t *Tree) updater(ctx context.Context) {

233
internal/model/yaml.go Normal file
View File

@ -0,0 +1,233 @@
package model
import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"sync/atomic"
"time"
backoff "github.com/cenkalti/backoff/v4"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
const maxRetryInterval = 1 * time.Minute
// YAML tracks yaml resource representations.
type YAML struct {
gvr client.GVR
inUpdate int32
showManagedFields bool
path string
query string
lines []string
refreshRate time.Duration
listeners []ResourceViewerListener
}
// NewYAML return a new yaml resource model.
func NewYAML(gvr client.GVR, path string) *YAML {
return &YAML{
gvr: gvr,
path: path,
refreshRate: 2 * time.Second,
}
}
// GetPath returns the active resource path.
func (y *YAML) GetPath() string {
return y.path
}
// Filter filters the model.
func (y *YAML) Filter(q string) {
y.query = q
y.filterChanged(y.lines)
}
func (y *YAML) filterChanged(lines []string) {
y.fireResourceChanged(lines, y.filter(y.query, lines))
}
func (y *YAML) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
if dao.IsFuzzySelector(q) {
return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return y.rxFilter(q, lines)
}
func (*YAML) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*YAML) 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
}
func (y *YAML) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range y.listeners {
l.ResourceChanged(lines, matches)
}
}
func (y *YAML) fireResourceFailed(err error) {
for _, l := range y.listeners {
l.ResourceFailed(err)
}
}
// ClearFilter clear out the filter.
func (y *YAML) ClearFilter() {
y.query = ""
}
// Peel returns the current model data.
func (y *YAML) Peek() []string {
return y.lines
}
// Watch watches for YAML changes.
func (y *YAML) Watch(ctx context.Context) {
if err := y.refresh(ctx); err != nil {
log.Error().Err(err).Msgf("YAML Refresh failed")
return
}
go y.updater(ctx)
}
func (y *YAML) updater(ctx context.Context) {
defer log.Debug().Msgf("YAML canceled -- %q", y.gvr)
bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval
rate := initRefreshRate
for {
select {
case <-ctx.Done():
return
case <-time.After(rate):
rate = y.refreshRate
err := backoff.Retry(func() error {
return y.refresh(ctx)
}, backoff.WithContext(bf, ctx))
if err != nil {
log.Error().Err(err).Msgf("Retry failed")
y.fireResourceFailed(err)
return
}
}
}
}
func (y *YAML) refresh(ctx context.Context) error {
if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return nil
}
defer atomic.StoreInt32(&y.inUpdate, 0)
if err := y.reconcile(ctx); err != nil {
log.Error().Err(err).Msgf("reconcile failed %q", y.gvr)
y.fireResourceFailed(err)
return err
}
return nil
}
func (y *YAML) reconcile(ctx context.Context) error {
s, err := y.ToYAML(ctx, y.gvr, y.path, y.showManagedFields)
if err != nil {
return err
}
lines := strings.Split(s, "\n")
if reflect.DeepEqual(lines, y.lines) {
return nil
}
y.lines = lines
y.fireResourceChanged(y.lines, y.filter(y.query, y.lines))
return nil
}
// AddListener adds a new model listener.
func (y *YAML) AddListener(l ResourceViewerListener) {
y.listeners = append(y.listeners, l)
}
// RemoveListener delete a listener from the list.
func (y *YAML) RemoveListener(l ResourceViewerListener) {
victim := -1
for i, lis := range y.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
y.listeners = append(y.listeners[:victim], y.listeners[victim+1:]...)
}
}
// ToYAML returns a resource yaml.
func (y *YAML) ToYAML(ctx context.Context, gvr client.GVR, path string, showManaged bool) (string, error) {
meta, err := getMeta(ctx, gvr)
if err != nil {
return "", err
}
desc, ok := meta.DAO.(dao.Describer)
if !ok {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
return desc.ToYAML(path, showManaged)
}
func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) {
meta := resourceMeta(gvr)
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, gvr)
return meta, nil
}
func resourceMeta(gvr client.GVR) ResourceMeta {
meta, ok := Registry[gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}

View File

@ -35,8 +35,7 @@ func TestAliasSearch(t *testing.T) {
v.App().Prompt().SendStrokes("blee")
assert.Equal(t, 3, v.GetTable().GetColumnCount())
time.Sleep(1_000 * time.Millisecond)
assert.Equal(t, 2, v.GetTable().GetRowCount())
assert.Equal(t, 3, v.GetTable().GetRowCount())
}
func TestAliasGoto(t *testing.T) {

View File

@ -128,12 +128,15 @@ func (a *App) layout(ctx context.Context, version string) {
main := tview.NewFlex().SetDirection(tview.FlexRow)
main.AddItem(a.statusIndicator(), 1, 1, false)
main.AddItem(a.Content, 0, 10, true)
if !a.Config.K9s.IsCrumbsless() {
main.AddItem(a.Crumbs(), 1, 1, false)
}
main.AddItem(flash, 1, 1, false)
a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
a.toggleHeader(!a.Config.K9s.GetHeadless())
a.toggleCrumbs(!a.Config.K9s.GetCrumbsless())
a.toggleHeader(!a.Config.K9s.IsHeadless())
// a.toggleCrumbs(!a.Config.K9s.GetCrumbsless())
}
func (a *App) initSignals() {
@ -182,8 +185,8 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
func (a *App) bindKeys() {
a.AddActions(ui.KeyActions{
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
tcell.KeyCtrlT: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
ui.KeyShiftH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
ui.KeyShiftC: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false),
@ -217,7 +220,9 @@ func (a *App) toggleCrumbs(flag bool) {
log.Fatal().Msg("Expecting valid flex view")
}
if a.showCrumbs {
flex.AddItemAtIndex(2, a.Crumbs(), 1, 1, false)
if _, ok := flex.ItemAt(2).(*ui.Crumbs); !ok {
flex.AddItemAtIndex(2, a.Crumbs(), 1, 1, false)
}
} else {
flex.RemoveItemAtIndex(2)
}
@ -536,7 +541,7 @@ func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *App) meowCmd(msg string) {
if err := a.inject(NewMeow(a, msg)); err != nil {
if err := a.inject(NewCow(a, msg)); err != nil {
a.Flash().Err(err)
}
}
@ -599,7 +604,7 @@ func (a *App) gotoResource(cmd, path string, clearStack bool) error {
return err
}
c := NewMeow(a, err.Error())
c := NewCow(a, err.Error())
_ = c.Init(context.Background())
if clearStack {
a.Content.Stack.Clear()
@ -613,7 +618,7 @@ func (a *App) inject(c model.Component) error {
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
if err := c.Init(ctx); err != nil {
log.Error().Err(err).Msgf("component init failed for %q %v", c.Name(), err)
c = NewMeow(a, err.Error())
c = NewCow(a, err.Error())
_ = c.Init(ctx)
}
a.Content.Push(c)

View File

@ -230,18 +230,10 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
ctx := b.defaultContext()
raw, err := b.GetModel().ToYAML(ctx, path)
if err != nil {
b.App().Flash().Errf("unable to get resource %q -- %s", b.GVR(), err)
return nil
v := NewLiveView(b.app, "YAML", model.NewYAML(b.GVR(), path))
if err := v.app.inject(v); err != nil {
v.app.Flash().Err(err)
}
details := NewDetails(b.app, "YAML", path, true).Update(raw)
if err := b.App().inject(details); err != nil {
b.App().Flash().Err(err)
}
return nil
}

View File

@ -86,10 +86,10 @@ func (c *Command) xrayCmd(cmd string) error {
}
gvr, ok := c.alias.AsGVR(tokens[1])
if !ok {
return fmt.Errorf("Huh? `%s` command not found", cmd)
return fmt.Errorf("`%s` command not found", cmd)
}
if !allowedXRay(gvr) {
return fmt.Errorf("Huh? `%s` command not found", cmd)
return fmt.Errorf("`%s` command not found", cmd)
}
x := NewXray(gvr)
@ -139,7 +139,7 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
return err
}
if !c.alias.Check(cmds[0]) {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
return fmt.Errorf("`%s` Command not found", cmd)
}
return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack)
}
@ -210,7 +210,7 @@ func (c *Command) specialCmd(cmd, path string) bool {
func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
gvr, ok := c.alias.AsGVR(cmd)
if !ok {
return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd)
return "", nil, fmt.Errorf("`%s` command not found", cmd)
}
v, ok := customViewers[gvr]

137
internal/view/cow.go Normal file
View File

@ -0,0 +1,137 @@
package view
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
// Cow represents a bomb viewer
type Cow struct {
*tview.TextView
actions ui.KeyActions
app *App
says string
}
// NewCow returns a have a cow viewer.
func NewCow(app *App, says string) *Cow {
return &Cow{
TextView: tview.NewTextView(),
app: app,
actions: make(ui.KeyActions),
says: says,
}
}
// Init initializes the viewer.
func (c *Cow) Init(_ context.Context) error {
c.SetBorder(true)
c.SetScrollable(true).SetWrap(true).SetRegions(true)
c.SetDynamicColors(true)
c.SetHighlightColor(tcell.ColorOrange)
c.SetTitleColor(tcell.ColorAqua)
c.SetInputCapture(c.keyboard)
c.SetBorderPadding(0, 0, 1, 1)
c.updateTitle()
c.SetTextAlign(tview.AlignCenter)
c.app.Styles.AddListener(c)
c.StylesChanged(c.app.Styles)
c.bindKeys()
c.SetInputCapture(c.keyboard)
c.talk()
return nil
}
func (c *Cow) talk() {
says := c.says
if len(says) == 0 {
says = "Nothing to report here. Please move along..."
}
c.SetText(cowTalk(says))
}
func cowTalk(says string) string {
buff := make([]string, 0, len(cow)+3)
buff = append(buff, " "+strings.Repeat("─", len(says)+8))
buff = append(buff, fmt.Sprintf("< [red::b]Ruroh? %s [-::-] >", says))
buff = append(buff, " "+strings.Repeat("─", len(says)+8))
spacer := strings.Repeat(" ", len(says)/2-8)
for _, s := range cow {
buff = append(buff, "[red::b]"+spacer+s)
}
return strings.Join(buff, "\n")
}
func (c *Cow) bindKeys() {
c.actions.Set(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", c.resetCmd, false),
})
}
func (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if a, ok := c.actions[ui.AsKey(evt)]; ok {
return a.Action(evt)
}
return evt
}
// StylesChanged notifies the skin changes.
func (c *Cow) StylesChanged(s *config.Styles) {
c.SetBackgroundColor(c.app.Styles.BgColor())
c.SetTextColor(c.app.Styles.FgColor())
c.SetBorderFocusColor(c.app.Styles.Frame().Border.FocusColor.Color())
}
func (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
return c.app.PrevCmd(evt)
}
// Actions returns menu actions
func (c *Cow) Actions() ui.KeyActions {
return c.actions
}
// Name returns the component name.
func (c *Cow) Name() string { return "cow" }
// Start starts the view updater.
func (c *Cow) Start() {}
// Stop terminates the updater.
func (c *Cow) Stop() {
c.app.Styles.RemoveListener(c)
}
// Hints returns menu hints.
func (c *Cow) Hints() model.MenuHints {
return c.actions.Hints()
}
// ExtraHints returns additional hints.
func (c *Cow) ExtraHints() map[string]string {
return nil
}
func (c *Cow) updateTitle() {
c.SetTitle(" Error ")
}
var cow = []string{
`\ ^__^ `,
` \ (oo)\_______ `,
` (__)\ )\/\`,
` ||----w | `,
` || || `,
}

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
@ -59,18 +60,12 @@ func defaultEnv(c *client.Config, path string, header render.Header, row render.
return env
}
func describeResource(app *App, model ui.Tabular, gvr, path string) {
func describeResource(app *App, m ui.Tabular, gvr, path string) {
ctx := context.Background()
ctx = context.WithValue(ctx, internal.KeyFactory, app.factory)
yaml, err := model.Describe(ctx, path)
if err != nil {
app.Flash().Errf("Describe command failed: %s", err)
return
}
details := NewDetails(app, "Describe", path, true).Update(yaml)
if err := app.inject(details); err != nil {
v := NewLiveView(app, "Describe", model.NewDescribe(client.NewGVR(gvr), path))
if err := app.inject(v); err != nil {
app.Flash().Err(err)
}
}

311
internal/view/live_view.go Normal file
View File

@ -0,0 +1,311 @@
package view
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/atotto/clipboard"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/sahilm/fuzzy"
)
const liveViewTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
// LiveView represents a live text viewer.
type LiveView struct {
*tview.Flex
text *tview.TextView
actions ui.KeyActions
app *App
title string
cmdBuff *model.FishBuff
model model.ResourceViewer
currentRegion, maxRegions int
fullScreen bool
cancel context.CancelFunc
}
// NewLiveView returns a live viewer.
func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView {
v := LiveView{
Flex: tview.NewFlex(),
text: tview.NewTextView(),
app: app,
title: title,
actions: make(ui.KeyActions),
currentRegion: 0,
maxRegions: 0,
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
model: m,
}
v.AddItem(v.text, 0, 1, true)
return &v
}
// Init initializes the viewer.
func (v *LiveView) Init(_ context.Context) error {
if v.title != "" {
v.SetBorder(true)
}
v.text.SetScrollable(true).SetWrap(true).SetRegions(true)
v.text.SetDynamicColors(true)
v.text.SetHighlightColor(tcell.ColorOrange)
v.SetTitleColor(tcell.ColorAqua)
v.SetInputCapture(v.keyboard)
v.SetBorderPadding(0, 0, 1, 1)
v.updateTitle()
v.app.Styles.AddListener(v)
v.StylesChanged(v.app.Styles)
v.app.Prompt().SetModel(v.cmdBuff)
v.cmdBuff.AddListener(v)
v.bindKeys()
v.SetInputCapture(v.keyboard)
v.model.AddListener(v)
return nil
}
// ResourceFailed notifies when their is an issue.
func (v *LiveView) ResourceFailed(err error) {
v.text.SetTextAlign(tview.AlignCenter)
v.text.SetText(cowTalk(err.Error()))
}
// ResourceChanged notifies when the filter changes.
func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
v.app.QueueUpdateDraw(func() {
v.maxRegions = len(matches)
ll := make([]string, len(lines))
copy(ll, lines)
for i, m := range matches {
loc, line := m.MatchedIndexes, ll[m.Index]
ll[m.Index] = line[:loc[0]] + `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:]
}
if v.maxRegions == 0 {
v.text.ScrollToBeginning()
}
v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
v.text.Highlight()
if v.currentRegion < v.maxRegions {
v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
v.text.ScrollToHighlight()
}
v.updateTitle()
})
}
// BufferChanged indicates the buffer was changed.
func (v *LiveView) BufferChanged(s string) {}
// BufferCompleted indicates input was accepted.
func (v *LiveView) BufferCompleted(s string) {
v.model.Filter(s)
}
// BufferActive indicates the buff activity changed.
func (v *LiveView) BufferActive(state bool, k model.BufferKind) {
v.app.BufferActive(state, k)
}
func (v *LiveView) bindKeys() {
v.actions.Set(ui.KeyActions{
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", v.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", v.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, false),
ui.KeyC: ui.NewKeyAction("Copy", v.cpCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", v.toggleFullScreenCmd, true),
ui.KeyN: ui.NewKeyAction("Next Match", v.nextCmd, true),
ui.KeyShiftN: ui.NewKeyAction("Prev Match", v.prevCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", v.activateCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", v.eraseCmd, false),
})
}
func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if a, ok := v.actions[ui.AsKey(evt)]; ok {
return a.Action(evt)
}
return evt
}
// StylesChanged notifies the skin changed.
func (v *LiveView) StylesChanged(s *config.Styles) {
v.SetBackgroundColor(v.app.Styles.BgColor())
v.text.SetTextColor(v.app.Styles.FgColor())
v.SetBorderFocusColor(v.app.Styles.Frame().Border.FocusColor.Color())
v.ResourceChanged(v.model.Peek(), nil)
}
// Actions returns menu actions
func (v *LiveView) Actions() ui.KeyActions {
return v.actions
}
// Name returns the component name.
func (v *LiveView) Name() string { return v.title }
// Start starts the view updater.
func (v *LiveView) Start() {
var ctx context.Context
ctx, v.cancel = context.WithCancel(context.Background())
ctx = context.WithValue(ctx, internal.KeyFactory, v.app.factory)
v.model.Watch(ctx)
}
// Stop terminates the updater.
func (v *LiveView) Stop() {
if v.cancel != nil {
v.cancel()
v.cancel = nil
}
v.app.Styles.RemoveListener(v)
}
// Hints returns menu hints.
func (v *LiveView) Hints() model.MenuHints {
return v.actions.Hints()
}
// ExtraHints returns additional hints.
func (v *LiveView) ExtraHints() map[string]string {
return nil
}
func (v *LiveView) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.app.InCmdMode() {
return evt
}
v.fullScreen = !v.fullScreen
v.SetFullScreen(v.fullScreen)
v.Box.SetBorder(!v.fullScreen)
return nil
}
func (v *LiveView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.Empty() {
return evt
}
v.currentRegion++
if v.currentRegion >= v.maxRegions {
v.currentRegion = 0
}
v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
v.text.ScrollToHighlight()
v.updateTitle()
return nil
}
func (v *LiveView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.Empty() {
return evt
}
v.currentRegion--
if v.currentRegion < 0 {
v.currentRegion = v.maxRegions - 1
}
v.text.Highlight("search_" + strconv.Itoa(v.currentRegion))
v.text.ScrollToHighlight()
v.updateTitle()
return nil
}
func (v *LiveView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
v.model.Filter(v.cmdBuff.GetText())
v.cmdBuff.SetActive(false)
v.updateTitle()
return nil
}
func (v *LiveView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.app.InCmdMode() {
return evt
}
v.app.ResetPrompt(v.cmdBuff)
return nil
}
func (v *LiveView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.IsActive() {
return nil
}
v.cmdBuff.Delete()
return nil
}
func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.InCmdMode() {
v.cmdBuff.Reset()
return v.app.PrevCmd(evt)
}
if v.cmdBuff.GetText() != "" {
v.model.ClearFilter()
}
v.cmdBuff.SetActive(false)
v.cmdBuff.Reset()
v.updateTitle()
return nil
}
func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveYAML(v.app.Config.K9s.CurrentCluster, v.title, v.text.GetText(true)); err != nil {
v.app.Flash().Err(err)
} else {
v.app.Flash().Infof("Log %s saved successfully!", path)
}
return nil
}
func (v *LiveView) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.Flash().Info("Content copied to clipboard...")
if err := clipboard.WriteAll(v.text.GetText(true)); err != nil {
v.app.Flash().Err(err)
}
return nil
}
func (v *LiveView) updateTitle() {
if v.title == "" {
return
}
fmat := fmt.Sprintf(detailsTitleFmt, v.title, v.model.GetPath())
buff := v.cmdBuff.GetText()
if buff == "" {
v.SetTitle(ui.SkinTitle(fmat, v.app.Styles.Frame()))
return
}
if v.maxRegions > 0 {
buff += fmt.Sprintf("[%d:%d]", v.currentRegion+1, v.maxRegions)
}
fmat += fmt.Sprintf(ui.SearchFmt, buff)
v.SetTitle(ui.SkinTitle(fmat, v.app.Styles.Frame()))
}

View File

@ -1,133 +0,0 @@
package view
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
// Meow represents a bomb viewer
type Meow struct {
*tview.TextView
actions ui.KeyActions
app *App
says string
}
// NewMeow returns a details viewer.
func NewMeow(app *App, says string) *Meow {
return &Meow{
TextView: tview.NewTextView(),
app: app,
actions: make(ui.KeyActions),
says: says,
}
}
// Init initializes the viewer.
func (m *Meow) Init(_ context.Context) error {
m.SetBorder(true)
m.SetScrollable(true).SetWrap(true).SetRegions(true)
m.SetDynamicColors(true)
m.SetHighlightColor(tcell.ColorOrange)
m.SetTitleColor(tcell.ColorAqua)
m.SetInputCapture(m.keyboard)
m.SetBorderPadding(0, 0, 1, 1)
m.updateTitle()
m.SetTextAlign(tview.AlignCenter)
m.app.Styles.AddListener(m)
m.StylesChanged(m.app.Styles)
m.bindKeys()
m.SetInputCapture(m.keyboard)
m.talk()
return nil
}
func (m *Meow) talk() {
says := m.says
if len(says) == 0 {
says = "Nothing to report here. Please move along..."
}
buff := make([]string, 0, len(cow)+3)
buff = append(buff, " "+strings.Repeat("─", len(says)+8))
buff = append(buff, fmt.Sprintf("< [red::b]MEOW! %s [-::-] >", says))
buff = append(buff, " "+strings.Repeat("─", len(says)+8))
spacer := strings.Repeat(" ", len(says)/2-8)
for _, s := range cow {
buff = append(buff, spacer+s)
}
m.SetText(strings.Join(buff, "\n"))
}
func (m *Meow) bindKeys() {
m.actions.Set(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", m.resetCmd, false),
})
}
func (m *Meow) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if a, ok := m.actions[ui.AsKey(evt)]; ok {
return a.Action(evt)
}
return evt
}
// StylesChanged notifies the skin changes.
func (m *Meow) StylesChanged(s *config.Styles) {
m.SetBackgroundColor(m.app.Styles.BgColor())
m.SetTextColor(m.app.Styles.FgColor())
m.SetBorderFocusColor(m.app.Styles.Frame().Border.FocusColor.Color())
}
func (m *Meow) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
return m.app.PrevCmd(evt)
}
// Actions returns menu actions
func (m *Meow) Actions() ui.KeyActions {
return m.actions
}
// Name returns the component name.
func (m *Meow) Name() string { return "cow" }
// Start starts the view updater.
func (m *Meow) Start() {}
// Stop terminates the updater.
func (m *Meow) Stop() {
m.app.Styles.RemoveListener(m)
}
// Hints returns menu hints.
func (m *Meow) Hints() model.MenuHints {
return m.actions.Hints()
}
// ExtraHints returns additional hints.
func (m *Meow) ExtraHints() map[string]string {
return nil
}
func (m *Meow) updateTitle() {
m.SetTitle(" Error ")
}
var cow = []string{
`\ ^__^ `,
` \ (oo)\_______ `,
` (__)\ )\/\`,
` ||----w | `,
` || || `,
}

View File

@ -184,7 +184,7 @@ func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
raw, err := dao.ToYAML(o)
raw, err := dao.ToYAML(o, false)
if err != nil {
n.App().Flash().Errf("Unable to marshal resource %s", err)
return nil

View File

@ -129,7 +129,9 @@ func (t *Table) SetExtraActionsFn(BoostActionsFunc) {}
// BufferCompleted indicates input was accepted.
func (t *Table) BufferCompleted(s string) {
t.Filter(s)
t.app.QueueUpdateDraw(func() {
t.Filter(s)
})
}
// BufferChanged indicates the buffer was changed.

View File

@ -69,7 +69,7 @@ func TestTableViewFilter(t *testing.T) {
v.CmdBuff().SetActive(true)
v.CmdBuff().SetText("blee")
assert.Equal(t, 2, v.GetRowCount())
assert.Equal(t, 3, v.GetRowCount())
}
func TestTableViewSort(t *testing.T) {