cleanup and bug fixes
parent
4045cb56a4
commit
372b4d8e09
1
go.mod
1
go.mod
|
|
@ -4,6 +4,7 @@ go 1.15
|
|||
|
||||
require (
|
||||
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/derailed/popeye v0.8.10
|
||||
github.com/derailed/tview v0.4.6
|
||||
|
|
|
|||
1
go.sum
1
go.sum
|
|
@ -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/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/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -264,29 +264,29 @@ func (a *APIClient) HasMetrics() bool {
|
|||
if !ok || err != nil {
|
||||
return false
|
||||
}
|
||||
v, ok := a.cache.Get(cacheMXKey)
|
||||
if ok {
|
||||
if v, ok := a.cache.Get(cacheMXKey); ok {
|
||||
flag, k := v.(bool)
|
||||
return k && flag
|
||||
}
|
||||
|
||||
var flag bool
|
||||
var metricsOK bool
|
||||
defer func() {
|
||||
a.cache.Add(cacheMXKey, metricsOK, cacheExpiry)
|
||||
}()
|
||||
dial, err := a.MXDial()
|
||||
if err != nil {
|
||||
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||
return flag
|
||||
return metricsOK
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
|
||||
defer cancel()
|
||||
if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
|
||||
flag = true
|
||||
metricsOK = true
|
||||
} else {
|
||||
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.
|
||||
|
|
@ -412,10 +412,6 @@ func (a *APIClient) reset() {
|
|||
}
|
||||
|
||||
func (a *APIClient) supportsMetricsResources() (supported bool, err error) {
|
||||
defer func() {
|
||||
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
|
||||
}()
|
||||
|
||||
if v, ok := a.cache.Get(cacheMXAPIKey); ok {
|
||||
flag, k := v.(bool)
|
||||
supported = k && flag
|
||||
|
|
@ -424,6 +420,9 @@ func (a *APIClient) supportsMetricsResources() (supported bool, err error) {
|
|||
if a.config == nil || a.config.flags == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
|
||||
}()
|
||||
|
||||
dial, err := a.CachedDiscovery()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ func (c *Config) Load(path string) error {
|
|||
|
||||
// Save configuration to disk.
|
||||
func (c *Config) Save() error {
|
||||
log.Debug().Msg("[Config] Saving configuration...")
|
||||
c.Validate()
|
||||
|
||||
return c.SaveFile(K9sConfigFile)
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ func TestSetup(t *testing.T) {
|
|||
|
||||
var expectedConfig = `k9s:
|
||||
refreshRate: 100
|
||||
maxConnRetry: 15
|
||||
maxConnRetry: 5
|
||||
enableMouse: false
|
||||
headless: false
|
||||
crumbsless: false
|
||||
|
|
@ -344,7 +344,7 @@ var expectedConfig = `k9s:
|
|||
|
||||
var resetConfig = `k9s:
|
||||
refreshRate: 2
|
||||
maxConnRetry: 15
|
||||
maxConnRetry: 5
|
||||
enableMouse: false
|
||||
headless: false
|
||||
crumbsless: false
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import "github.com/derailed/k9s/internal/client"
|
|||
|
||||
const (
|
||||
defaultRefreshRate = 2
|
||||
defaultMaxConnRetry = 15
|
||||
defaultMaxConnRetry = 5
|
||||
)
|
||||
|
||||
// K9s tracks K9s configuration options.
|
||||
|
|
|
|||
|
|
@ -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 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,24 +16,6 @@ import (
|
|||
"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.
|
||||
type Describe struct {
|
||||
gvr client.GVR
|
||||
|
|
@ -50,7 +32,7 @@ func NewDescribe(gvr client.GVR, path string) *Describe {
|
|||
return &Describe{
|
||||
gvr: gvr,
|
||||
path: path,
|
||||
refreshRate: 2 * time.Second,
|
||||
refreshRate: defaultReaderRefreshRate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +42,7 @@ func (d *Describe) GetPath() string {
|
|||
}
|
||||
|
||||
// SetOptions toggle model options.
|
||||
func (d *Describe) SetOptions(context.Context, ToggleOpts) {}
|
||||
func (d *Describe) SetOptions(context.Context, ViewerToggleOpts) {}
|
||||
|
||||
// Filter filters the model.
|
||||
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) {
|
||||
defer log.Debug().Msgf("Describe canceled -- %q", d.gvr)
|
||||
|
||||
bf := backoff.NewExponentialBackOff()
|
||||
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval
|
||||
rate := initRefreshRate
|
||||
backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
|
||||
delay := defaultReaderRefreshRate
|
||||
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")
|
||||
case <-time.After(delay):
|
||||
if err := d.refresh(ctx); err != nil {
|
||||
log.Error().Err(err).Msgf("Describe Failed")
|
||||
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 {
|
||||
log.Debug().Msgf("DESCRefresh %v", time.Now())
|
||||
if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) {
|
||||
log.Debug().Msgf("Dropping update...")
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/derailed/tview"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ func (l *Log) Filter(q string) {
|
|||
}
|
||||
|
||||
l.filter = q
|
||||
// BOZO!! No needed since cmdbuff is now throttled!!
|
||||
if l.filtering {
|
||||
return
|
||||
}
|
||||
|
|
@ -352,7 +353,7 @@ func (l *Log) applyFilter(q string) ([][]byte, error) {
|
|||
func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
|
||||
ll := make([][]byte, len(lines))
|
||||
if l.filter == "" {
|
||||
l.lines.Render(l.logOptions.ShowTimestamp, ll)
|
||||
lines.Render(l.logOptions.ShowTimestamp, ll)
|
||||
} else {
|
||||
ff, err := l.applyFilter(l.filter)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ func (t *Table) updater(ctx context.Context) {
|
|||
defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr)
|
||||
|
||||
bf := backoff.NewExponentialBackOff()
|
||||
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval
|
||||
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval
|
||||
rate := initRefreshRate
|
||||
for {
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,42 @@ package model
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/sahilm/fuzzy"
|
||||
"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.
|
||||
type Igniter interface {
|
||||
// Start starts a component.
|
||||
|
|
|
|||
|
|
@ -18,31 +18,25 @@ import (
|
|||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRetryInterval = 1 * time.Minute
|
||||
|
||||
// ManageFieldOpts tracks managed fields.
|
||||
ManagedFieldsOpts = "ManagedFields"
|
||||
)
|
||||
// ManageFieldOpts tracks managed fields.
|
||||
const ManagedFieldsOpts = "ManagedFields"
|
||||
|
||||
// YAML tracks yaml resource representations.
|
||||
type YAML struct {
|
||||
gvr client.GVR
|
||||
inUpdate int32
|
||||
path string
|
||||
query string
|
||||
lines []string
|
||||
refreshRate time.Duration
|
||||
listeners []ResourceViewerListener
|
||||
options ToggleOpts
|
||||
gvr client.GVR
|
||||
inUpdate int32
|
||||
path string
|
||||
query string
|
||||
lines []string
|
||||
listeners []ResourceViewerListener
|
||||
options ViewerToggleOpts
|
||||
}
|
||||
|
||||
// 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,
|
||||
gvr: gvr,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +46,7 @@ func (y *YAML) GetPath() string {
|
|||
}
|
||||
|
||||
// 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
|
||||
if err := y.refresh(ctx); err != nil {
|
||||
y.fireResourceFailed(err)
|
||||
|
|
@ -133,28 +127,31 @@ func (y *YAML) Watch(ctx context.Context) error {
|
|||
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
|
||||
backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
|
||||
delay := defaultReaderRefreshRate
|
||||
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")
|
||||
case <-time.After(delay):
|
||||
if err := y.refresh(ctx); err != nil {
|
||||
log.Error().Err(err).Msgf("YAML Failed")
|
||||
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 {
|
||||
log.Debug().Msgf("YAMLRefresh %v", time.Now())
|
||||
if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) {
|
||||
log.Debug().Msgf("Dropping update...")
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
|
|
@ -273,19 +274,35 @@ func (a *App) Resume() {
|
|||
}
|
||||
|
||||
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 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msg("ClusterInfo updater canceled!")
|
||||
return
|
||||
case <-time.After(clusterRefresh):
|
||||
a.refreshCluster()
|
||||
case <-time.After(delay):
|
||||
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()
|
||||
if ok := a.Conn().CheckConnectivity(); ok {
|
||||
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)
|
||||
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)
|
||||
a.BailOut()
|
||||
}
|
||||
if count > 0 {
|
||||
log.Warn().Msgf("Conn check failed (%d/%d)", count, maxConnRetry)
|
||||
a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", count))
|
||||
return
|
||||
a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s Toast [%d/%d]", count, maxConnRetry))
|
||||
return fmt.Errorf("Conn check failed (%d/%d)", count, maxConnRetry)
|
||||
}
|
||||
|
||||
// Reload alias
|
||||
|
|
@ -323,6 +340,8 @@ func (a *App) refreshCluster() {
|
|||
|
||||
// Update cluster info
|
||||
a.clusterModel.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) switchNS(ns string) error {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func (c *Cow) talk() {
|
|||
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, 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 {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ func (v *LiveView) ResourceFailed(err error) {
|
|||
// ResourceChanged notifies when the filter changes.
|
||||
func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
|
||||
v.app.QueueUpdateDraw(func() {
|
||||
v.text.SetTextAlign(tview.AlignLeft)
|
||||
v.maxRegions = len(matches)
|
||||
ll := make([]string, len(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]:]
|
||||
}
|
||||
|
||||
if v.maxRegions == 0 {
|
||||
if v.text.GetText(true) == "" {
|
||||
v.text.ScrollToBeginning()
|
||||
}
|
||||
|
||||
v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
|
||||
v.text.Highlight()
|
||||
if v.currentRegion < v.maxRegions {
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ func (l *Log) bindKeys() {
|
|||
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
|
||||
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, 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.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
|
||||
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),
|
||||
|
|
|
|||
Loading…
Reference in New Issue