cleanup and bug fixes

mine
derailed 2020-10-29 09:32:59 -06:00
parent 4045cb56a4
commit 372b4d8e09
17 changed files with 132 additions and 89 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.15
require ( require (
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.1.0 github.com/cenkalti/backoff/v4 v4.1.0
github.com/derailed/popeye v0.8.10 github.com/derailed/popeye v0.8.10
github.com/derailed/tview v0.4.6 github.com/derailed/tview v0.4.6

1
go.sum
View File

@ -111,6 +111,7 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= 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 v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= 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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

View File

@ -264,29 +264,29 @@ func (a *APIClient) HasMetrics() bool {
if !ok || err != nil { if !ok || err != nil {
return false return false
} }
v, ok := a.cache.Get(cacheMXKey) if v, ok := a.cache.Get(cacheMXKey); ok {
if ok {
flag, k := v.(bool) flag, k := v.(bool)
return k && flag return k && flag
} }
var flag bool var metricsOK bool
defer func() {
a.cache.Add(cacheMXKey, metricsOK, cacheExpiry)
}()
dial, err := a.MXDial() dial, err := a.MXDial()
if err != nil { if err != nil {
a.cache.Add(cacheMXKey, flag, cacheExpiry) return metricsOK
return flag
} }
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
defer cancel() defer cancel()
if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil { if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
flag = true metricsOK = true
} else { } else {
log.Error().Err(err).Msgf("List metrics failed") log.Error().Err(err).Msgf("List metrics failed")
} }
a.cache.Add(cacheMXKey, flag, cacheExpiry)
return flag return metricsOK
} }
// Dial returns a handle to api server or die. // Dial returns a handle to api server or die.
@ -412,10 +412,6 @@ func (a *APIClient) reset() {
} }
func (a *APIClient) supportsMetricsResources() (supported bool, err error) { func (a *APIClient) supportsMetricsResources() (supported bool, err error) {
defer func() {
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
}()
if v, ok := a.cache.Get(cacheMXAPIKey); ok { if v, ok := a.cache.Get(cacheMXAPIKey); ok {
flag, k := v.(bool) flag, k := v.(bool)
supported = k && flag supported = k && flag
@ -424,6 +420,9 @@ func (a *APIClient) supportsMetricsResources() (supported bool, err error) {
if a.config == nil || a.config.flags == nil { if a.config == nil || a.config.flags == nil {
return return
} }
defer func() {
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
}()
dial, err := a.CachedDiscovery() dial, err := a.CachedDiscovery()
if err != nil { if err != nil {

View File

@ -229,7 +229,6 @@ func (c *Config) Load(path string) error {
// Save configuration to disk. // Save configuration to disk.
func (c *Config) Save() error { func (c *Config) Save() error {
log.Debug().Msg("[Config] Saving configuration...")
c.Validate() c.Validate()
return c.SaveFile(K9sConfigFile) return c.SaveFile(K9sConfigFile)

View File

@ -261,7 +261,7 @@ func TestSetup(t *testing.T) {
var expectedConfig = `k9s: var expectedConfig = `k9s:
refreshRate: 100 refreshRate: 100
maxConnRetry: 15 maxConnRetry: 5
enableMouse: false enableMouse: false
headless: false headless: false
crumbsless: false crumbsless: false
@ -344,7 +344,7 @@ var expectedConfig = `k9s:
var resetConfig = `k9s: var resetConfig = `k9s:
refreshRate: 2 refreshRate: 2
maxConnRetry: 15 maxConnRetry: 5
enableMouse: false enableMouse: false
headless: false headless: false
crumbsless: false crumbsless: false

View File

@ -4,7 +4,7 @@ import "github.com/derailed/k9s/internal/client"
const ( const (
defaultRefreshRate = 2 defaultRefreshRate = 2
defaultMaxConnRetry = 15 defaultMaxConnRetry = 5
) )
// K9s tracks K9s configuration options. // K9s tracks K9s configuration options.

View File

@ -116,7 +116,7 @@ func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) {
) )
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil { if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil {
log.Warn().Err(err).Msgf("No node metrics") log.Debug().Err(err).Msgf("No node metrics")
} }
} }

View File

@ -16,24 +16,6 @@ import (
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
type ResourceViewerListener interface {
ResourceChanged(lines []string, matches fuzzy.Matches)
ResourceFailed(error)
}
type ToggleOpts map[string]bool
type ResourceViewer interface {
GetPath() string
Filter(string)
ClearFilter()
Peek() []string
SetOptions(context.Context, ToggleOpts)
Watch(context.Context) error
AddListener(ResourceViewerListener)
RemoveListener(ResourceViewerListener)
}
// Describe tracks describeable resources. // Describe tracks describeable resources.
type Describe struct { type Describe struct {
gvr client.GVR gvr client.GVR
@ -50,7 +32,7 @@ func NewDescribe(gvr client.GVR, path string) *Describe {
return &Describe{ return &Describe{
gvr: gvr, gvr: gvr,
path: path, path: path,
refreshRate: 2 * time.Second, refreshRate: defaultReaderRefreshRate,
} }
} }
@ -60,7 +42,7 @@ func (d *Describe) GetPath() string {
} }
// SetOptions toggle model options. // SetOptions toggle model options.
func (d *Describe) SetOptions(context.Context, ToggleOpts) {} func (d *Describe) SetOptions(context.Context, ViewerToggleOpts) {}
// Filter filters the model. // Filter filters the model.
func (d *Describe) Filter(q string) { func (d *Describe) Filter(q string) {
@ -134,27 +116,30 @@ func (d *Describe) Watch(ctx context.Context) error {
func (d *Describe) updater(ctx context.Context) { func (d *Describe) updater(ctx context.Context) {
defer log.Debug().Msgf("Describe canceled -- %q", d.gvr) defer log.Debug().Msgf("Describe canceled -- %q", d.gvr)
bf := backoff.NewExponentialBackOff() backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval delay := defaultReaderRefreshRate
rate := initRefreshRate
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(rate): case <-time.After(delay):
rate = d.refreshRate if err := d.refresh(ctx); err != nil {
err := backoff.Retry(func() error { log.Error().Err(err).Msgf("Describe Failed")
return d.refresh(ctx)
}, backoff.WithContext(bf, ctx))
if err != nil {
log.Error().Err(err).Msgf("Retry failed")
d.fireResourceFailed(err) d.fireResourceFailed(err)
return delay = backOff.NextBackOff()
if delay == backoff.Stop {
log.Error().Err(err).Msgf("Describe done Retrying bailing out!")
return
}
} else {
backOff.Reset()
delay = defaultReaderRefreshRate
} }
} }
} }
} }
func (d *Describe) refresh(ctx context.Context) error { func (d *Describe) refresh(ctx context.Context) error {
log.Debug().Msgf("DESCRefresh %v", time.Now())
if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) { if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...") log.Debug().Msgf("Dropping update...")
return nil return nil

View File

@ -1,6 +1,10 @@
package model package model
import ( import (
"context"
"time"
"github.com/cenkalti/backoff"
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -23,3 +27,10 @@ func FQN(ns, n string) string {
func Truncate(str string, width int) string { func Truncate(str string, width int) string {
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
} }
// NewExpBackOff returns a new exponential backoff timer.
func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext {
bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = start, max
return backoff.WithContext(bf, ctx)
}

View File

@ -183,6 +183,7 @@ func (l *Log) Filter(q string) {
} }
l.filter = q l.filter = q
// BOZO!! No needed since cmdbuff is now throttled!!
if l.filtering { if l.filtering {
return return
} }
@ -352,7 +353,7 @@ func (l *Log) applyFilter(q string) ([][]byte, error) {
func (l *Log) fireLogBuffChanged(lines dao.LogItems) { func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
ll := make([][]byte, len(lines)) ll := make([][]byte, len(lines))
if l.filter == "" { if l.filter == "" {
l.lines.Render(l.logOptions.ShowTimestamp, ll) lines.Render(l.logOptions.ShowTimestamp, ll)
} else { } else {
ff, err := l.applyFilter(l.filter) ff, err := l.applyFilter(l.filter)
if err != nil { if err != nil {

View File

@ -167,7 +167,7 @@ func (t *Table) updater(ctx context.Context) {
defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr)
bf := backoff.NewExponentialBackOff() bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval
rate := initRefreshRate rate := initRefreshRate
for { for {
select { select {

View File

@ -2,14 +2,42 @@ package model
import ( import (
"context" "context"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/sahilm/fuzzy"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
const (
maxReaderRetryInterval = 2 * time.Minute
defaultReaderRefreshRate = 5 * time.Second
)
// ResourceViewerListener listens to viewing resource events.
type ResourceViewerListener interface {
ResourceChanged(lines []string, matches fuzzy.Matches)
ResourceFailed(error)
}
// ToggleOpts represents a collection of viewing options.
type ViewerToggleOpts map[string]bool
// ResourceViewer represents a viewed resource.
type ResourceViewer interface {
GetPath() string
Filter(string)
ClearFilter()
Peek() []string
SetOptions(context.Context, ViewerToggleOpts)
Watch(context.Context) error
AddListener(ResourceViewerListener)
RemoveListener(ResourceViewerListener)
}
// Igniter represents a runnable view. // Igniter represents a runnable view.
type Igniter interface { type Igniter interface {
// Start starts a component. // Start starts a component.

View File

@ -18,31 +18,25 @@ import (
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
const ( // ManageFieldOpts tracks managed fields.
maxRetryInterval = 1 * time.Minute const ManagedFieldsOpts = "ManagedFields"
// ManageFieldOpts tracks managed fields.
ManagedFieldsOpts = "ManagedFields"
)
// YAML tracks yaml resource representations. // YAML tracks yaml resource representations.
type YAML struct { type YAML struct {
gvr client.GVR gvr client.GVR
inUpdate int32 inUpdate int32
path string path string
query string query string
lines []string lines []string
refreshRate time.Duration listeners []ResourceViewerListener
listeners []ResourceViewerListener options ViewerToggleOpts
options ToggleOpts
} }
// NewYAML return a new yaml resource model. // NewYAML return a new yaml resource model.
func NewYAML(gvr client.GVR, path string) *YAML { func NewYAML(gvr client.GVR, path string) *YAML {
return &YAML{ return &YAML{
gvr: gvr, gvr: gvr,
path: path, path: path,
refreshRate: 2 * time.Second,
} }
} }
@ -52,7 +46,7 @@ func (y *YAML) GetPath() string {
} }
// SetOptions toggle model options. // SetOptions toggle model options.
func (y *YAML) SetOptions(ctx context.Context, opts ToggleOpts) { func (y *YAML) SetOptions(ctx context.Context, opts ViewerToggleOpts) {
y.options = opts y.options = opts
if err := y.refresh(ctx); err != nil { if err := y.refresh(ctx); err != nil {
y.fireResourceFailed(err) y.fireResourceFailed(err)
@ -133,28 +127,31 @@ func (y *YAML) Watch(ctx context.Context) error {
func (y *YAML) updater(ctx context.Context) { func (y *YAML) updater(ctx context.Context) {
defer log.Debug().Msgf("YAML canceled -- %q", y.gvr) defer log.Debug().Msgf("YAML canceled -- %q", y.gvr)
bf := backoff.NewExponentialBackOff() backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval delay := defaultReaderRefreshRate
rate := initRefreshRate
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(rate): case <-time.After(delay):
rate = y.refreshRate if err := y.refresh(ctx); err != nil {
err := backoff.Retry(func() error { log.Error().Err(err).Msgf("YAML Failed")
return y.refresh(ctx)
}, backoff.WithContext(bf, ctx))
if err != nil {
log.Error().Err(err).Msgf("Retry failed")
y.fireResourceFailed(err) y.fireResourceFailed(err)
return delay = backOff.NextBackOff()
if delay == backoff.Stop {
log.Error().Err(err).Msgf("YAML done Retrying bailing out!")
return
}
} else {
backOff.Reset()
delay = defaultReaderRefreshRate
} }
} }
} }
} }
func (y *YAML) refresh(ctx context.Context) error { func (y *YAML) refresh(ctx context.Context) error {
log.Debug().Msgf("YAMLRefresh %v", time.Now())
if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) { if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...") log.Debug().Msgf("Dropping update...")
return nil return nil

View File

@ -12,6 +12,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/cenkalti/backoff"
"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/config" "github.com/derailed/k9s/internal/config"
@ -273,19 +274,35 @@ func (a *App) Resume() {
} }
func (a *App) clusterUpdater(ctx context.Context) { func (a *App) clusterUpdater(ctx context.Context) {
a.refreshCluster() if err := a.refreshCluster(); err != nil {
log.Error().Err(err).Msgf("Cluster updater failed!")
return
}
bf := model.NewExpBackOff(ctx, clusterRefresh, 2*time.Minute)
delay := clusterRefresh
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msg("ClusterInfo updater canceled!") log.Debug().Msg("ClusterInfo updater canceled!")
return return
case <-time.After(clusterRefresh): case <-time.After(delay):
a.refreshCluster() if err := a.refreshCluster(); err != nil {
log.Error().Err(err).Msgf("ClusterUpdater failed")
if delay = bf.NextBackOff(); delay == backoff.Stop {
a.BailOut()
return
}
} else {
bf.Reset()
delay = clusterRefresh
}
} }
} }
} }
func (a *App) refreshCluster() { func (a *App) refreshCluster() error {
log.Debug().Msgf("Cluster Refresh %v", time.Now())
c := a.Content.Top() c := a.Content.Top()
if ok := a.Conn().CheckConnectivity(); ok { if ok := a.Conn().CheckConnectivity(); ok {
if atomic.LoadInt32(&a.conRetry) > 0 { if atomic.LoadInt32(&a.conRetry) > 0 {
@ -305,13 +322,13 @@ func (a *App) refreshCluster() {
count, maxConnRetry := atomic.LoadInt32(&a.conRetry), int32(a.Config.K9s.MaxConnRetry) count, maxConnRetry := atomic.LoadInt32(&a.conRetry), int32(a.Config.K9s.MaxConnRetry)
if count >= maxConnRetry { if count >= maxConnRetry {
log.Error().Msgf("Conn check failed (%d/%d). Bailing out!", count, maxConnRetry)
ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", count) ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", count)
a.BailOut() a.BailOut()
} }
if count > 0 { if count > 0 {
log.Warn().Msgf("Conn check failed (%d/%d)", count, maxConnRetry) a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s Toast [%d/%d]", count, maxConnRetry))
a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", count)) return fmt.Errorf("Conn check failed (%d/%d)", count, maxConnRetry)
return
} }
// Reload alias // Reload alias
@ -323,6 +340,8 @@ func (a *App) refreshCluster() {
// Update cluster info // Update cluster info
a.clusterModel.Refresh() a.clusterModel.Refresh()
return nil
} }
func (a *App) switchNS(ns string) error { func (a *App) switchNS(ns string) error {

View File

@ -64,7 +64,7 @@ func (c *Cow) talk() {
func cowTalk(says string) string { func cowTalk(says string) string {
buff := make([]string, 0, len(cow)+3) buff := make([]string, 0, len(cow)+3)
buff = append(buff, " "+strings.Repeat("─", len(says)+8)) buff = append(buff, " "+strings.Repeat("─", len(says)+8))
buff = append(buff, fmt.Sprintf("< [red::b]Ruroh? %s [-::-] >", says)) buff = append(buff, fmt.Sprintf("< [red::b]Ruroh? %s[-::-] >", says))
buff = append(buff, " "+strings.Repeat("─", len(says)+8)) buff = append(buff, " "+strings.Repeat("─", len(says)+8))
spacer := strings.Repeat(" ", len(says)/2-8) spacer := strings.Repeat(" ", len(says)/2-8)
for _, s := range cow { for _, s := range cow {

View File

@ -88,6 +88,7 @@ func (v *LiveView) ResourceFailed(err error) {
// ResourceChanged notifies when the filter changes. // ResourceChanged notifies when the filter changes.
func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) { func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
v.app.QueueUpdateDraw(func() { v.app.QueueUpdateDraw(func() {
v.text.SetTextAlign(tview.AlignLeft)
v.maxRegions = len(matches) v.maxRegions = len(matches)
ll := make([]string, len(lines)) ll := make([]string, len(lines))
copy(ll, lines) copy(ll, lines)
@ -96,9 +97,10 @@ func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
ll[m.Index] = line[:loc[0]] + `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:] ll[m.Index] = line[:loc[0]] + `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:]
} }
if v.maxRegions == 0 { if v.text.GetText(true) == "" {
v.text.ScrollToBeginning() v.text.ScrollToBeginning()
} }
v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(ll, "\n"))) v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
v.text.Highlight() v.text.Highlight()
if v.currentRegion < v.maxRegions { if v.currentRegion < v.maxRegions {

View File

@ -191,7 +191,7 @@ func (l *Log) bindKeys() {
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true), ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true), ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),