feat: logs column lock (#3669)

* feat: logs column lock

* feat: logs column lock indicator

* feat: fix getting values from config

* feat: removed unnecessary new line
mine
Artem Yadelskyi 2025-11-17 19:53:18 +02:00 committed by GitHub
parent 6bc67e97f4
commit 6f0ecdec9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 85 additions and 12 deletions

View File

@ -464,6 +464,8 @@ You can now override the context portForward default address configuration by se
textWrap: false
# Autoscroll in logs will be disabled. Default is false.
disableAutoscroll: false
# Enable column locking when autoscroll is enabled. Default is false.
columnLock: false
# Toggles log line timestamp info. Default false
showTime: false
# Provide shell pod customization when nodeShell feature gate is enabled!
@ -1115,6 +1117,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -120,6 +120,7 @@
"sinceSeconds": {"type": "integer"},
"textWrap": {"type": "boolean"},
"disableAutoscroll": {"type": "boolean"},
"columnLock": {"type": "boolean"},
"showTime": {"type": "boolean"}
}
},

View File

@ -31,6 +31,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -24,6 +24,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -21,6 +21,7 @@ type Logger struct {
SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"`
TextWrap bool `json:"textWrap" yaml:"textWrap"`
DisableAutoscroll bool `json:"disableAutoscroll" yaml:"disableAutoscroll"`
ColumnLock bool `json:"columnLock" yaml:"columnLock"`
ShowTime bool `json:"showTime" yaml:"showTime"`
}

View File

@ -37,6 +37,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -38,6 +38,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -37,6 +37,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -31,6 +31,7 @@ k9s:
sinceSeconds: -1
textWrap: false
disableAutoscroll: false
columnLock: false
showTime: false
thresholds:
cpu:

View File

@ -49,6 +49,7 @@ type Log struct {
cancelUpdates bool
mx sync.Mutex
follow bool
columnLock bool
requestOneRefresh bool
}
@ -56,13 +57,10 @@ var _ model.Component = (*Log)(nil)
// NewLog returns a new viewer.
func NewLog(gvr *client.GVR, opts *dao.LogOptions) *Log {
l := Log{
Flex: tview.NewFlex(),
model: model.NewLog(gvr, opts, defaultFlushTimeout),
follow: true,
return &Log{
Flex: tview.NewFlex(),
model: model.NewLog(gvr, opts, defaultFlushTimeout),
}
return &l
}
func (*Log) SetCommand(*cmd.Interpreter) {}
@ -105,6 +103,9 @@ func (l *Log) Init(ctx context.Context) (err error) {
l.model.Init(l.app.factory)
l.updateTitle()
l.follow = !l.app.Config.K9s.Logger.DisableAutoscroll
l.columnLock = l.app.Config.K9s.Logger.ColumnLock
l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)
return nil
@ -258,6 +259,7 @@ func (l *Log) bindKeys() {
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.KeyShiftL: ui.NewKeyAction("Toggle ColumnLock", l.toggleColumnLockCmd, true),
ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),
ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
@ -363,7 +365,12 @@ func (l *Log) Flush(lines [][]byte) {
_, _ = l.ansiWriter.Write(lines[i])
}
if l.follow {
l.logs.ScrollToEnd()
if l.columnLock {
// Enables end tracking without resetting column
l.logs.SetScrollable(false).SetScrollable(true)
} else {
l.logs.ScrollToEnd()
}
}
}
@ -464,7 +471,7 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey {
_, _, w, _ := l.GetRect()
_, _ = fmt.Fprintf(l.ansiWriter, "\n[%s:-:b]%s[-:-:-]", l.app.Styles.Views().Log.FgColor.String(), strings.Repeat("-", w-4))
_, _ = fmt.Fprintf(l.ansiWriter, "[%s:-:b]%s[-:-:-]\n", l.app.Styles.Views().Log.FgColor.String(), strings.Repeat("-", w-4))
l.follow = true
return nil
@ -507,6 +514,18 @@ func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (l *Log) toggleColumnLockCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.indicator.ToggleColumnLock()
l.columnLock = l.indicator.ColumnLock()
l.indicator.Refresh()
return nil
}
func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt

View File

@ -25,6 +25,7 @@ type LogIndicator struct {
showTime bool
allContainers bool
shouldDisplayAllContainers bool
columnLock bool
}
// NewLogIndicator returns a new indicator.
@ -38,11 +39,13 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bo
textWrap: cfg.K9s.Logger.TextWrap,
showTime: cfg.K9s.Logger.ShowTime,
shouldDisplayAllContainers: allContainers,
columnLock: cfg.K9s.Logger.ColumnLock,
}
if cfg.K9s.Logger.DisableAutoscroll {
l.scrollStatus = 0
}
l.StylesChanged(styles)
styles.AddListener(&l)
l.SetTextAlign(tview.AlignCenter)
@ -63,6 +66,11 @@ func (l *LogIndicator) AutoScroll() bool {
return atomic.LoadInt32(&l.scrollStatus) == 1
}
// ColumnLock reports the current column lock mode.
func (l *LogIndicator) ColumnLock() bool {
return l.columnLock
}
// Timestamp reports the current timestamp mode.
func (l *LogIndicator) Timestamp() bool {
return l.showTime
@ -78,6 +86,11 @@ func (l *LogIndicator) FullScreen() bool {
return l.fullScreen
}
// ToggleColumnLock toggles the current column lock mode.
func (l *LogIndicator) ToggleColumnLock() {
l.columnLock = !l.columnLock
}
// ToggleTimestamp toggles the current timestamp mode.
func (l *LogIndicator) ToggleTimestamp() {
l.showTime = !l.showTime
@ -140,6 +153,12 @@ func (l *LogIndicator) Refresh() {
l.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, "Autoscroll", spacer)...)
}
if l.ColumnLock() {
l.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, "ColumnLock", spacer)...)
} else {
l.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, "ColumnLock", spacer)...)
}
if l.FullScreen() {
l.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, "FullScreen", spacer)...)
} else {

View File

@ -18,10 +18,10 @@ func TestLogIndicatorRefresh(t *testing.T) {
e string
}{
"all-containers": {
view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]ColumnLock:[gray::d]Off[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
},
"plain": {
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]ColumnLock:[gray::d]Off[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n",
},
}

View File

@ -27,10 +27,34 @@ func TestLogAutoScroll(t *testing.T) {
v.GetModel().Set(ii)
v.GetModel().Notify()
assert.Len(t, v.Hints(), 16)
assert.Len(t, v.Hints(), 17)
v.toggleAutoScrollCmd(nil)
assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true))
assert.Equal(t, "Autoscroll:Off ColumnLock:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true))
}
func TestLogColumnLock(t *testing.T) {
opts := dao.LogOptions{
Path: "fred/p1",
Container: "blee",
}
v := NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext(t)))
buff := dao.NewLogItems()
for i := range 100 {
buff.Add(dao.NewLogItemFromString(fmt.Sprintf("line-%d\n", i)))
}
v.GetModel().Set(buff)
v.toggleColumnLockCmd(nil)
const column = 2
v.Logs().ScrollTo(-1, column)
v.toggleAutoScrollCmd(nil)
r, c := v.Logs().GetScrollOffset()
assert.Equal(t, -1, r)
assert.Equal(t, column, c)
}
func TestLogViewNav(t *testing.T) {