mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-23 07:31:03 +00:00
Merge bf0a4ae2ec into 7e7aadc207
This commit is contained in:
commit
6e1c589333
20 changed files with 535 additions and 18 deletions
|
|
@ -64,6 +64,16 @@ logs:
|
|||
timestamps: false
|
||||
since: '60m' # set to '' to show all logs
|
||||
tail: '' # set to 200 to show last 200 lines of logs
|
||||
|
||||
### Logs Filtering
|
||||
|
||||
When viewing container or service logs in the main panel, you can filter the log output in real-time:
|
||||
|
||||
- Press `/` while viewing logs to open the filter prompt
|
||||
- Type a search string to filter log lines (case-sensitive substring match)
|
||||
- Press `Enter` to commit the filter and return to viewing filtered logs
|
||||
- Press `Esc` to cancel the filter and return to unfiltered logs
|
||||
|
||||
commandTemplates:
|
||||
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates
|
||||
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: zurück
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: regresar
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: retour
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: terug
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Globaal
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: powrót
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Globalne
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: retornar
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: dönüş
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## Global
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 返回
|
||||
<kbd>/</kbd>: filter logs
|
||||
</pre>
|
||||
|
||||
## 全局
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||
sidePanelsDirection = boxlayout.ROW
|
||||
}
|
||||
|
||||
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active
|
||||
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active || gui.State.LogsFilter.active
|
||||
infoSectionSize := 0
|
||||
if showInfoSection {
|
||||
infoSectionSize = 1
|
||||
|
|
@ -112,6 +112,19 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
|
|||
}...)
|
||||
}
|
||||
|
||||
if gui.State.LogsFilter.active {
|
||||
return append(result, []*boxlayout.Box{
|
||||
{
|
||||
Window: "logsfilterPrefix",
|
||||
Size: runewidth.StringWidth(gui.logsFilterPrompt()),
|
||||
},
|
||||
{
|
||||
Window: "logsfilter",
|
||||
Weight: 1,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
[]*boxlayout.Box{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
|
@ -136,12 +139,26 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
var filterString string
|
||||
var hasFilter bool
|
||||
|
||||
if gui.State.LogsFilter.active {
|
||||
filterString = gui.State.LogsFilter.needle
|
||||
hasFilter = filterString != ""
|
||||
}
|
||||
|
||||
if ctr.Details.Config.Tty {
|
||||
if hasFilter {
|
||||
return gui.filterTTYLogs(readCloser, writer, filterString, ctx)
|
||||
}
|
||||
_, err = io.Copy(writer, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if hasFilter {
|
||||
return gui.filterNonTTYLogs(readCloser, writer, filterString, ctx)
|
||||
}
|
||||
_, err = stdcopy.StdCopy(writer, writer, readCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -150,3 +167,74 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterTTYLogs filters TTY logs line by line based on the filter string
|
||||
func (gui *Gui) filterTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
|
||||
return streamFilterLines(reader, writer, filter, ctx)
|
||||
}
|
||||
|
||||
// filterNonTTYLogs filters non-TTY logs (stdout/stderr) line by line
|
||||
func (gui *Gui) filterNonTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
pipeWriter.CloseWithError(ctx.Err())
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := stdcopy.StdCopy(pipeWriter, pipeWriter, reader)
|
||||
close(done)
|
||||
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
defer pipeReader.Close()
|
||||
return streamFilterLines(pipeReader, writer, filter, ctx)
|
||||
}
|
||||
|
||||
func streamFilterLines(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 0, 4*1024), 10*1024*1024)
|
||||
|
||||
// Pre-convert filter to bytes for faster comparison when filter is ASCII
|
||||
filterBytes := []byte(filter)
|
||||
isASCIIFilter := len(filterBytes) == len(filter)
|
||||
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
line := scanner.Bytes()
|
||||
|
||||
var shouldWrite bool
|
||||
if isASCIIFilter {
|
||||
shouldWrite = bytes.Contains(line, filterBytes)
|
||||
} else {
|
||||
// For non-ASCII filters, we need string conversion for proper UTF-8 handling
|
||||
shouldWrite = strings.Contains(string(line), filter)
|
||||
}
|
||||
|
||||
if shouldWrite {
|
||||
if _, err := writer.Write(line); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
|
|
|||
182
pkg/gui/container_logs_test.go
Normal file
182
pkg/gui/container_logs_test.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStreamFilterLines(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
filter string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple match - single line",
|
||||
input: "hello world\n",
|
||||
filter: "hello",
|
||||
expected: "hello world\n",
|
||||
},
|
||||
{
|
||||
name: "simple match - multiple lines",
|
||||
input: "hello world\nfoo bar\nbaz qux\n",
|
||||
filter: "foo",
|
||||
expected: "foo bar\n",
|
||||
},
|
||||
{
|
||||
name: "multiple matches",
|
||||
input: "hello world\nhello again\nfoo bar\n",
|
||||
filter: "hello",
|
||||
expected: "hello world\nhello again\n",
|
||||
},
|
||||
{
|
||||
name: "no matches",
|
||||
input: "hello world\nfoo bar\nbaz qux\n",
|
||||
filter: "xyz",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty filter - matches all",
|
||||
input: "hello world\nfoo bar\n",
|
||||
filter: "",
|
||||
expected: "hello world\nfoo bar\n",
|
||||
},
|
||||
{
|
||||
name: "case sensitive match",
|
||||
input: "Hello World\nhello world\nHELLO WORLD\n",
|
||||
filter: "hello",
|
||||
expected: "hello world\n",
|
||||
},
|
||||
{
|
||||
name: "partial word match",
|
||||
input: "hello world\nhelloworld\n",
|
||||
filter: "hello",
|
||||
expected: "hello world\nhelloworld\n",
|
||||
},
|
||||
{
|
||||
name: "filter in middle of line",
|
||||
input: "prefix hello suffix\n",
|
||||
filter: "hello",
|
||||
expected: "prefix hello suffix\n",
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
filter: "hello",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "newline only lines",
|
||||
input: "\n\n\n",
|
||||
filter: "",
|
||||
expected: "\n\n\n",
|
||||
},
|
||||
{
|
||||
name: "filter with special characters",
|
||||
input: "error: something went wrong\ninfo: all good\n",
|
||||
filter: "error:",
|
||||
expected: "error: something went wrong\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.input)
|
||||
writer := &bytes.Buffer{}
|
||||
ctx := context.Background()
|
||||
|
||||
err := streamFilterLines(reader, writer, tt.filter, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, writer.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamFilterLines_ContextCancellation(t *testing.T) {
|
||||
input := strings.Repeat("hello world\n", 1000)
|
||||
reader := strings.NewReader(input)
|
||||
writer := &bytes.Buffer{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
cancel()
|
||||
|
||||
err := streamFilterLines(reader, writer, "hello", ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStreamFilterLines_NonASCIIFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
filter string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "UTF-8 characters",
|
||||
input: "hello 世界\nfoo bar\n",
|
||||
filter: "世界",
|
||||
expected: "hello 世界\n",
|
||||
},
|
||||
{
|
||||
name: "emoji filter",
|
||||
input: "error 🚨 occurred\ninfo: all good\n",
|
||||
filter: "🚨",
|
||||
expected: "error 🚨 occurred\n",
|
||||
},
|
||||
{
|
||||
name: "mixed ASCII and UTF-8",
|
||||
input: "hello 世界 world\nfoo bar\n",
|
||||
filter: "世界",
|
||||
expected: "hello 世界 world\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.input)
|
||||
writer := &bytes.Buffer{}
|
||||
ctx := context.Background()
|
||||
|
||||
err := streamFilterLines(reader, writer, tt.filter, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, writer.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamFilterLines_LongLines(t *testing.T) {
|
||||
longLine := strings.Repeat("a", 100000) + " target " + strings.Repeat("b", 100000) + "\n"
|
||||
shortLine := "target\n"
|
||||
|
||||
input := longLine + shortLine
|
||||
reader := strings.NewReader(input)
|
||||
writer := &bytes.Buffer{}
|
||||
ctx := context.Background()
|
||||
|
||||
err := streamFilterLines(reader, writer, "target", ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, writer.String(), "target")
|
||||
assert.Equal(t, 2, strings.Count(writer.String(), "target"))
|
||||
}
|
||||
|
||||
func TestStreamFilterLines_EmptyLines(t *testing.T) {
|
||||
input := "line with content\n\nanother line\n\n\n"
|
||||
reader := strings.NewReader(input)
|
||||
writer := &bytes.Buffer{}
|
||||
ctx := context.Background()
|
||||
|
||||
err := streamFilterLines(reader, writer, "", ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
lines := strings.Split(writer.String(), "\n")
|
||||
assert.GreaterOrEqual(t, len(lines), 5)
|
||||
}
|
||||
|
|
@ -4,8 +4,37 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
)
|
||||
|
||||
type filterConfig struct {
|
||||
active *bool
|
||||
panel *panels.ISideListPanel
|
||||
needle *string
|
||||
view *gocui.View
|
||||
isLogs bool
|
||||
}
|
||||
|
||||
func (gui *Gui) listFilterConfig() filterConfig {
|
||||
return filterConfig{
|
||||
active: &gui.State.Filter.active,
|
||||
panel: &gui.State.Filter.panel,
|
||||
needle: &gui.State.Filter.needle,
|
||||
view: gui.Views.Filter,
|
||||
isLogs: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) logsFilterConfig() filterConfig {
|
||||
return filterConfig{
|
||||
active: &gui.State.LogsFilter.active,
|
||||
panel: &gui.State.LogsFilter.panel,
|
||||
needle: &gui.State.LogsFilter.needle,
|
||||
view: gui.Views.LogsFilter,
|
||||
isLogs: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenFilter() error {
|
||||
panel, ok := gui.currentListPanel()
|
||||
if !ok {
|
||||
|
|
@ -22,17 +51,70 @@ func (gui *Gui) handleOpenFilter() error {
|
|||
return gui.switchFocus(gui.Views.Filter)
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewFilterNeedle(value string) error {
|
||||
gui.State.Filter.needle = value
|
||||
gui.ResetOrigin(gui.State.Filter.panel.GetView())
|
||||
return gui.State.Filter.panel.RerenderList()
|
||||
func (gui *Gui) handleOpenLogsFilter() error {
|
||||
panel, ok := gui.currentSidePanel()
|
||||
|
||||
// If not focused (e.g. we are in 'main'), try to get the side panel from the view stack
|
||||
if !ok {
|
||||
p, ok := gui.sidePanelByViewName(gui.currentSideViewName())
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
panel = p
|
||||
}
|
||||
|
||||
if !gui.IsCurrentView(gui.GetMainView()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gui.isMainTabActive("-logs") {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.LogsFilter.active = true
|
||||
gui.State.LogsFilter.panel = panel
|
||||
gui.State.LogsFilter.needle = ""
|
||||
|
||||
return gui.switchFocus(gui.Views.LogsFilter)
|
||||
}
|
||||
|
||||
func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
func (gui *Gui) onNewFilterNeedle(value string) error {
|
||||
return gui.handleNewFilterNeedle(gui.listFilterConfig(), value)
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewLogsFilterNeedle(value string) error {
|
||||
return gui.handleNewFilterNeedle(gui.logsFilterConfig(), value)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewFilterNeedle(config filterConfig, value string) error {
|
||||
if config.isLogs && *config.panel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
*config.needle = value
|
||||
|
||||
panel := *config.panel
|
||||
if panel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.isLogs {
|
||||
gui.State.Panels.Main.ObjectKey = ""
|
||||
return panel.HandleSelect()
|
||||
}
|
||||
|
||||
gui.ResetOrigin(panel.GetView())
|
||||
return panel.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) wrapEditorBase(
|
||||
f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool,
|
||||
onNeedleChange func(string) error,
|
||||
) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := f(v, key, ch, mod)
|
||||
if matched {
|
||||
if err := gui.onNewFilterNeedle(v.TextArea.GetContent()); err != nil {
|
||||
if err := onNeedleChange(v.TextArea.GetContent()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,41 +122,90 @@ func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod goc
|
|||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
return gui.wrapEditorBase(f, gui.onNewFilterNeedle)
|
||||
}
|
||||
|
||||
func (gui *Gui) wrapLogsEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
return gui.wrapEditorBase(f, gui.onNewLogsFilterNeedle)
|
||||
}
|
||||
|
||||
func (gui *Gui) escapeFilterPrompt() error {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return gui.escapeFilterPromptWithConfig(gui.listFilterConfig())
|
||||
}
|
||||
|
||||
func (gui *Gui) escapeLogsFilterPrompt() error {
|
||||
return gui.escapeFilterPromptWithConfig(gui.logsFilterConfig())
|
||||
}
|
||||
|
||||
func (gui *Gui) escapeFilterPromptWithConfig(config filterConfig) error {
|
||||
if err := gui.clearFilterWithConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.isLogs {
|
||||
gui.removeViewFromStack(config.view)
|
||||
}
|
||||
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) clearFilter() error {
|
||||
gui.State.Filter.needle = ""
|
||||
gui.State.Filter.active = false
|
||||
panel := gui.State.Filter.panel
|
||||
gui.State.Filter.panel = nil
|
||||
gui.Views.Filter.ClearTextArea()
|
||||
return gui.clearFilterWithConfig(gui.listFilterConfig())
|
||||
}
|
||||
|
||||
func (gui *Gui) clearLogsFilter() error {
|
||||
return gui.clearFilterWithConfig(gui.logsFilterConfig())
|
||||
}
|
||||
|
||||
func (gui *Gui) clearFilterWithConfig(config filterConfig) error {
|
||||
*config.needle = ""
|
||||
*config.active = false
|
||||
panel := *config.panel
|
||||
*config.panel = nil
|
||||
config.view.ClearTextArea()
|
||||
|
||||
if panel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.ResetOrigin(panel.GetView())
|
||||
if config.isLogs {
|
||||
gui.State.Panels.Main.ObjectKey = ""
|
||||
return panel.HandleSelect()
|
||||
}
|
||||
|
||||
gui.ResetOrigin(panel.GetView())
|
||||
return panel.RerenderList()
|
||||
}
|
||||
|
||||
// returns to the list view with the filter still applied
|
||||
func (gui *Gui) commitFilter() error {
|
||||
if gui.State.Filter.needle == "" {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return gui.commitFilterWithConfig(gui.listFilterConfig())
|
||||
}
|
||||
|
||||
// returns to the main view with the filter still applied
|
||||
func (gui *Gui) commitLogsFilter() error {
|
||||
return gui.commitFilterWithConfig(gui.logsFilterConfig())
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilterWithConfig(config filterConfig) error {
|
||||
if *config.needle == "" {
|
||||
if err := gui.clearFilterWithConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if config.isLogs {
|
||||
gui.removeViewFromStack(config.view)
|
||||
}
|
||||
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) filterPrompt() string {
|
||||
return fmt.Sprintf("%s: ", gui.Tr.FilterPrompt)
|
||||
}
|
||||
|
||||
func (gui *Gui) logsFilterPrompt() string {
|
||||
return fmt.Sprintf("%s: ", gui.Tr.LogsFilterPrompt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ func (gui *Gui) newLineFocused(v *gocui.View) error {
|
|||
return nil
|
||||
case "filter":
|
||||
return nil
|
||||
case "logsfilter":
|
||||
return nil
|
||||
default:
|
||||
panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement)
|
||||
}
|
||||
|
|
@ -58,6 +60,12 @@ func (gui *Gui) switchFocusAux(newView *gocui.View) error {
|
|||
}
|
||||
}
|
||||
|
||||
if gui.State.LogsFilter.panel != nil && !lo.Contains(newViewStack, gui.State.LogsFilter.panel.GetView().Name()) {
|
||||
if err := gui.clearLogsFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add 'onFocusLost' hook
|
||||
if !lo.Contains(newViewStack, "menu") {
|
||||
gui.Views.Menu.Visible = false
|
||||
|
|
@ -95,8 +103,8 @@ func (gui *Gui) removeViewFromStack(view *gocui.View) {
|
|||
// Not to be called directly. Use `switchFocus` instead
|
||||
func (gui *Gui) pushView(name string) {
|
||||
// No matter what view we're pushing, we first remove all popup panels from the stack
|
||||
// (unless it's the search view because we may be searching the menu panel)
|
||||
if name != "filter" {
|
||||
// (unless it's the filter views because we may be filtering the panel or logs)
|
||||
if name != "filter" && name != "logsfilter" {
|
||||
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||
return !gui.isPopupPanel(viewName)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ type guiState struct {
|
|||
// Maintains the state of manual filtering i.e. typing in a substring
|
||||
// to filter on in the current panel.
|
||||
Filter filterState
|
||||
|
||||
// Maintains the state of container logs filtering
|
||||
LogsFilter logsFilterState
|
||||
}
|
||||
|
||||
type filterState struct {
|
||||
|
|
@ -99,6 +102,17 @@ type filterState struct {
|
|||
needle string
|
||||
}
|
||||
|
||||
type logsFilterState struct {
|
||||
// If true then we're either currently inside the logs filter view
|
||||
// or we've committed the logs filter and we're back
|
||||
active bool
|
||||
|
||||
panel panels.ISideListPanel
|
||||
|
||||
// The string that we're filtering the logs
|
||||
needle string
|
||||
}
|
||||
|
||||
// screen sizing determines how much space your selected window takes up (window
|
||||
// as in panel, not your terminal's window). Sometimes you want a bit more space
|
||||
// to see the contents of a panel, and this keeps track of how much maximisation
|
||||
|
|
|
|||
|
|
@ -443,6 +443,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollLeftMain,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: '/',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: wrappedHandler(gui.handleOpenLogsFilter),
|
||||
Description: gui.Tr.LcFilterLogs,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
|
|
@ -473,6 +480,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Modifier: gocui.ModNone,
|
||||
Handler: wrappedHandler(gui.escapeFilterPrompt),
|
||||
},
|
||||
{
|
||||
ViewName: "logsfilter",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: wrappedHandler(gui.commitLogsFilter),
|
||||
},
|
||||
{
|
||||
ViewName: "logsfilter",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: wrappedHandler(gui.escapeLogsFilterPrompt),
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: 'J',
|
||||
|
|
|
|||
|
|
@ -349,6 +349,16 @@ func (gui *Gui) currentSidePanel() (panels.ISideListPanel, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
func (gui *Gui) sidePanelByViewName(viewName string) (panels.ISideListPanel, bool) {
|
||||
for _, sidePanel := range gui.allSidePanels() {
|
||||
if sidePanel.GetView().Name() == viewName {
|
||||
return sidePanel, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// returns the current list panel. If no list panel is focused, returns false.
|
||||
func (gui *Gui) currentListPanel() (panels.ISideListPanel, bool) {
|
||||
viewName := gui.currentViewName()
|
||||
|
|
@ -380,3 +390,13 @@ func (gui *Gui) allListPanels() []panels.ISideListPanel {
|
|||
func (gui *Gui) IsCurrentView(view *gocui.View) bool {
|
||||
return view == gui.CurrentView()
|
||||
}
|
||||
|
||||
func (gui *Gui) isMainTabActive(key string) bool {
|
||||
objectKey := gui.State.Panels.Main.ObjectKey
|
||||
if objectKey != "" {
|
||||
if strings.HasSuffix(objectKey, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ type Views struct {
|
|||
// appears next to the SearchPrefix view, it's where you type in the search string
|
||||
Filter *gocui.View
|
||||
|
||||
// text that prompts you to enter text in the LogsFilter view
|
||||
LogsFilter *gocui.View
|
||||
|
||||
// appears next to the SearchPrefix view, it's where you type in the log search string
|
||||
LogsFilterPrefix *gocui.View
|
||||
|
||||
// popups
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
|
|
@ -81,6 +87,8 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
|||
{viewPtr: &gui.Views.Information, name: "information", autoPosition: true},
|
||||
{viewPtr: &gui.Views.Filter, name: "filter", autoPosition: true},
|
||||
{viewPtr: &gui.Views.FilterPrefix, name: "filterPrefix", autoPosition: true},
|
||||
{viewPtr: &gui.Views.LogsFilter, name: "logsfilter", autoPosition: true},
|
||||
{viewPtr: &gui.Views.LogsFilterPrefix, name: "logsfilterPrefix", autoPosition: true},
|
||||
|
||||
// popups.
|
||||
{viewPtr: &gui.Views.Menu, name: "menu", autoPosition: false},
|
||||
|
|
@ -180,6 +188,16 @@ func (gui *Gui) createAllViews() error {
|
|||
gui.Views.Filter.Frame = false
|
||||
gui.Views.Filter.Editor = gocui.EditorFunc(gui.wrapEditor(gocui.SimpleEditor))
|
||||
|
||||
gui.Views.LogsFilterPrefix.BgColor = gocui.ColorDefault
|
||||
gui.Views.LogsFilterPrefix.FgColor = gocui.ColorGreen
|
||||
gui.Views.LogsFilterPrefix.Frame = false
|
||||
|
||||
gui.Views.LogsFilter.BgColor = gocui.ColorDefault
|
||||
gui.Views.LogsFilter.FgColor = gocui.ColorGreen
|
||||
gui.Views.LogsFilter.Editable = true
|
||||
gui.Views.LogsFilter.Frame = false
|
||||
gui.Views.LogsFilter.Editor = gocui.EditorFunc(gui.wrapLogsEditor(gocui.SimpleEditor))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +207,7 @@ func (gui *Gui) setInitialViewContent() error {
|
|||
}
|
||||
|
||||
_ = gui.setViewContent(gui.Views.FilterPrefix, gui.filterPrompt())
|
||||
_ = gui.setViewContent(gui.Views.LogsFilterPrefix, gui.logsFilterPrompt())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,9 @@ type TranslationSet struct {
|
|||
|
||||
LcNextScreenMode string
|
||||
LcPrevScreenMode string
|
||||
LcFilterLogs string
|
||||
FilterPrompt string
|
||||
LogsFilterPrompt string
|
||||
|
||||
FocusProjects string
|
||||
FocusServices string
|
||||
|
|
@ -271,7 +273,9 @@ func englishSet() TranslationSet {
|
|||
|
||||
LcNextScreenMode: "next screen mode (normal/half/fullscreen)",
|
||||
LcPrevScreenMode: "prev screen mode",
|
||||
LcFilterLogs: "filter logs",
|
||||
FilterPrompt: "filter",
|
||||
LogsFilterPrompt: "Log Filter",
|
||||
|
||||
FocusProjects: "focus projects panel",
|
||||
FocusServices: "focus services panel",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue