provides logger config options. Fix #623
parent
2e05367256
commit
3efb3f2596
5
go.sum
5
go.sum
|
|
@ -4,6 +4,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
|
|
@ -37,6 +38,7 @@ github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+B
|
|||
github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU=
|
||||
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
|
||||
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
|
|
@ -97,6 +99,7 @@ github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
|
|||
github.com/containerd/containerd v1.3.0-beta.2.0.20190823190603-4a2f61c4f2b4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY=
|
||||
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
|
||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||
github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
||||
|
|
@ -320,6 +323,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
|||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
|
|
@ -337,6 +341,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
|
|||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ func (c *Config) Load(path string) error {
|
|||
if cfg.K9s != nil {
|
||||
c.K9s = cfg.K9s
|
||||
}
|
||||
if c.K9s.Logger == nil {
|
||||
c.K9s.Logger = NewLogger()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ func TestConfigLoad(t *testing.T) {
|
|||
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
||||
|
||||
assert.Equal(t, 2, cfg.K9s.RefreshRate)
|
||||
assert.Equal(t, 200, cfg.K9s.LogBufferSize)
|
||||
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
|
||||
assert.Equal(t, 200, cfg.K9s.Logger.TailCount)
|
||||
assert.Equal(t, "minikube", cfg.K9s.CurrentContext)
|
||||
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
|
||||
assert.NotNil(t, cfg.K9s.Clusters)
|
||||
|
|
@ -206,8 +207,8 @@ func TestConfigSaveFile(t *testing.T) {
|
|||
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
||||
cfg.K9s.RefreshRate = 100
|
||||
cfg.K9s.ReadOnly = true
|
||||
cfg.K9s.LogBufferSize = 500
|
||||
cfg.K9s.LogRequestSize = 100
|
||||
cfg.K9s.Logger.TailCount = 500
|
||||
cfg.K9s.Logger.BufferSize = 800
|
||||
cfg.K9s.CurrentContext = "blee"
|
||||
cfg.K9s.CurrentCluster = "blee"
|
||||
cfg.Validate()
|
||||
|
|
@ -262,8 +263,9 @@ var expectedConfig = `k9s:
|
|||
refreshRate: 100
|
||||
headless: false
|
||||
readOnly: true
|
||||
logBufferSize: 500
|
||||
logRequestSize: 100
|
||||
logger:
|
||||
tail: 500
|
||||
buffer: 800
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
|
|
@ -310,8 +312,9 @@ var resetConfig = `k9s:
|
|||
refreshRate: 2
|
||||
headless: false
|
||||
readOnly: false
|
||||
logBufferSize: 200
|
||||
logRequestSize: 200
|
||||
logger:
|
||||
tail: 200
|
||||
buffer: 2000
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ package config
|
|||
import "github.com/derailed/k9s/internal/client"
|
||||
|
||||
const (
|
||||
defaultRefreshRate = 2
|
||||
defaultLogRequestSize = 200
|
||||
defaultLogBufferSize = 1000
|
||||
defaultReadOnly = false
|
||||
defaultRefreshRate = 2
|
||||
defaultReadOnly = false
|
||||
)
|
||||
|
||||
// K9s tracks K9s configuration options.
|
||||
|
|
@ -14,8 +12,7 @@ type K9s struct {
|
|||
RefreshRate int `yaml:"refreshRate"`
|
||||
Headless bool `yaml:"headless"`
|
||||
ReadOnly bool `yaml:"readOnly"`
|
||||
LogBufferSize int `yaml:"logBufferSize"`
|
||||
LogRequestSize int `yaml:"logRequestSize"`
|
||||
Logger *Logger `yaml:"logger"`
|
||||
CurrentContext string `yaml:"currentContext"`
|
||||
CurrentCluster string `yaml:"currentCluster"`
|
||||
FullScreenLogs bool `yaml:"fullScreenLogs"`
|
||||
|
|
@ -30,12 +27,11 @@ type K9s struct {
|
|||
// NewK9s create a new K9s configuration.
|
||||
func NewK9s() *K9s {
|
||||
return &K9s{
|
||||
RefreshRate: defaultRefreshRate,
|
||||
ReadOnly: defaultReadOnly,
|
||||
LogBufferSize: defaultLogBufferSize,
|
||||
LogRequestSize: defaultLogRequestSize,
|
||||
Clusters: make(map[string]*Cluster),
|
||||
Thresholds: NewThreshold(),
|
||||
RefreshRate: defaultRefreshRate,
|
||||
ReadOnly: defaultReadOnly,
|
||||
Logger: NewLogger(),
|
||||
Clusters: make(map[string]*Cluster),
|
||||
Thresholds: NewThreshold(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,14 +102,6 @@ func (k *K9s) validateDefaults() {
|
|||
if k.RefreshRate <= 0 {
|
||||
k.RefreshRate = defaultRefreshRate
|
||||
}
|
||||
|
||||
if k.LogBufferSize <= 0 {
|
||||
k.LogBufferSize = defaultLogBufferSize
|
||||
}
|
||||
|
||||
if k.LogRequestSize <= 0 {
|
||||
k.LogRequestSize = defaultLogRequestSize
|
||||
}
|
||||
}
|
||||
|
||||
func (k *K9s) checkClusters(ks KubeSettings) {
|
||||
|
|
@ -140,6 +128,11 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) {
|
|||
}
|
||||
k.checkClusters(ks)
|
||||
|
||||
if k.Logger == nil {
|
||||
k.Logger = NewLogger()
|
||||
} else {
|
||||
k.Logger.Validate(c, ks)
|
||||
}
|
||||
if k.Thresholds == nil {
|
||||
k.Thresholds = NewThreshold()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ func TestK9sValidate(t *testing.T) {
|
|||
c.Validate(mc, mk)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 1000, c.LogBufferSize)
|
||||
assert.Equal(t, 200, c.LogRequestSize)
|
||||
assert.Equal(t, 50, c.Logger.TailCount)
|
||||
assert.Equal(t, 1_000, c.Logger.BufferSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
assert.Equal(t, 1, len(c.Clusters))
|
||||
|
|
@ -45,8 +45,8 @@ func TestK9sValidateBlank(t *testing.T) {
|
|||
c.Validate(mc, mk)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 1000, c.LogBufferSize)
|
||||
assert.Equal(t, 200, c.LogRequestSize)
|
||||
assert.Equal(t, 50, c.Logger.TailCount)
|
||||
assert.Equal(t, 1_000, c.Logger.BufferSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
assert.Equal(t, 1, len(c.Clusters))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultLoggerTailCount tracks log tail size.
|
||||
DefaultLoggerTailCount = 50
|
||||
// DefaultLoggerBufferSize tracks the buffer size.
|
||||
DefaultLoggerBufferSize = 1_000
|
||||
// MaxLogThreshold sets the max value for log size.
|
||||
MaxLogThreshold = 5_000
|
||||
)
|
||||
|
||||
// Logger tracks logger options
|
||||
type Logger struct {
|
||||
TailCount int `yaml:"tail"`
|
||||
BufferSize int `yaml:"buffer"`
|
||||
}
|
||||
|
||||
// NewLogger returns a new instance.
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
TailCount: DefaultLoggerTailCount,
|
||||
BufferSize: DefaultLoggerBufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks thresholds and make sure we're cool. If not use defaults.
|
||||
func (l *Logger) Validate(_ client.Connection, _ KubeSettings) {
|
||||
if l.TailCount <= 0 {
|
||||
l.TailCount = DefaultLoggerTailCount
|
||||
}
|
||||
if l.TailCount > MaxLogThreshold {
|
||||
l.TailCount = MaxLogThreshold
|
||||
}
|
||||
if l.BufferSize <= 0 {
|
||||
l.BufferSize = DefaultLoggerBufferSize
|
||||
}
|
||||
if l.BufferSize > MaxLogThreshold {
|
||||
l.BufferSize = MaxLogThreshold
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewLogger(t *testing.T) {
|
||||
l := config.NewLogger()
|
||||
l.Validate(nil, nil)
|
||||
|
||||
assert.Equal(t, 50, l.TailCount)
|
||||
assert.Equal(t, 1_000, l.BufferSize)
|
||||
}
|
||||
|
||||
func TestLoggerValidate(t *testing.T) {
|
||||
var l config.Logger
|
||||
l.Validate(nil, nil)
|
||||
|
||||
assert.Equal(t, 50, l.TailCount)
|
||||
assert.Equal(t, 1_000, l.BufferSize)
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
k9s:
|
||||
refreshRate: 2
|
||||
logBufferSize: 200
|
||||
logRequestSize: 200
|
||||
logger:
|
||||
tail: 200
|
||||
buffer: 2000
|
||||
currentContext: minikube
|
||||
currentCluster: minikube
|
||||
clusters:
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
const logMaxBufferSize = 100
|
||||
|
||||
// LogsListener represents a log model listener.
|
||||
type LogsListener interface {
|
||||
// LogChanged notifies the model changed.
|
||||
|
|
@ -39,21 +38,27 @@ type Log struct {
|
|||
cancelFn context.CancelFunc
|
||||
mx sync.RWMutex
|
||||
filter string
|
||||
bufferSize int
|
||||
lastSent int
|
||||
showTimestamp bool
|
||||
timeOut time.Duration
|
||||
flushTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewLog returns a new model.
|
||||
func NewLog(gvr client.GVR, opts dao.LogOptions, timeOut time.Duration) *Log {
|
||||
func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Log {
|
||||
return &Log{
|
||||
gvr: gvr,
|
||||
logOptions: opts,
|
||||
lines: nil,
|
||||
timeOut: timeOut,
|
||||
gvr: gvr,
|
||||
logOptions: opts,
|
||||
lines: nil,
|
||||
flushTimeout: flushTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure sets logger configuration.
|
||||
func (l *Log) Configure(opts *config.Logger) {
|
||||
l.bufferSize, l.logOptions.Lines = opts.BufferSize, int64(opts.TailCount)
|
||||
}
|
||||
|
||||
// GetPath returns resource path.
|
||||
func (l *Log) GetPath() string { return l.logOptions.Path }
|
||||
|
||||
|
|
@ -217,13 +222,13 @@ func (l *Log) updateLogs(ctx context.Context, c <-chan []byte) {
|
|||
var overflow bool
|
||||
l.mx.RLock()
|
||||
{
|
||||
overflow = len(l.lines)-l.lastSent > logMaxBufferSize
|
||||
overflow = len(l.lines)-l.lastSent > l.bufferSize
|
||||
}
|
||||
l.mx.RUnlock()
|
||||
if overflow {
|
||||
l.Notify(true)
|
||||
}
|
||||
case <-time.After(l.timeOut):
|
||||
case <-time.After(l.flushTimeout):
|
||||
l.Notify(true)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func (c *Configurator) RefreshCustomViews() {
|
|||
}
|
||||
|
||||
if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil {
|
||||
log.Debug().Msgf("No view custom configuration file found -- %s", config.K9sViewConfigFile)
|
||||
log.Error().Err(err).Msgf("Custom view load failed %s", config.K9sViewConfigFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
logTitle = "logs"
|
||||
logMessage = "[:orange:b]Waiting for logs...[::]"
|
||||
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||
logFmt = " Logs([fg:bg:]%s) "
|
||||
|
||||
// BOZO!! Canned! Need config tail line counts!
|
||||
tailLineCount = 50
|
||||
defaultTimeout = 200 * time.Millisecond
|
||||
logTitle = "logs"
|
||||
logMessage = "[:orange:b]Waiting for logs...[::]"
|
||||
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||
logFmt = " Logs([fg:bg:]%s) "
|
||||
flushTimeout = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
// Log represents a generic log viewer.
|
||||
|
|
@ -40,6 +37,7 @@ type Log struct {
|
|||
ansiWriter io.Writer
|
||||
cmdBuff *ui.CmdBuff
|
||||
model *model.Log
|
||||
counts int
|
||||
}
|
||||
|
||||
var _ model.Component = (*Log)(nil)
|
||||
|
|
@ -49,7 +47,7 @@ func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
|
|||
l := Log{
|
||||
Flex: tview.NewFlex(),
|
||||
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
|
||||
model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, tailLineCount), defaultTimeout),
|
||||
model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), flushTimeout),
|
||||
}
|
||||
|
||||
return &l
|
||||
|
|
@ -60,6 +58,8 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
if l.app, err = extractApp(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
l.model.Configure(l.app.Config.K9s.Logger)
|
||||
|
||||
l.SetBorder(true)
|
||||
l.SetBorderPadding(0, 0, 1, 1)
|
||||
l.SetDirection(tview.FlexRow)
|
||||
|
|
@ -74,7 +74,7 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
}
|
||||
l.logs.SetText(logMessage)
|
||||
l.logs.SetWrap(false)
|
||||
l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize)
|
||||
l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize)
|
||||
|
||||
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
|
||||
l.AddItem(l.logs, 0, 1, true)
|
||||
|
|
@ -97,6 +97,7 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
|
||||
// LogCleared clears the logs.
|
||||
func (l *Log) LogCleared() {
|
||||
l.counts = 0
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
|
|
|
|||
Loading…
Reference in New Issue