mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 07:11:01 +00:00
Added logs filter state management saperate from the panel list filter, Implemented the stream filtering code to support both tty and non-tty logs with ASCII filter support, Refactor filtering code to support both list filter and logs filter, Added new views for log filter with its new keybindings
This commit is contained in:
parent
f4fc3669ca
commit
8a98325f3a
11 changed files with 526 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 }}'
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
@ -178,6 +186,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
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +205,7 @@ func (gui *Gui) setInitialViewContent() error {
|
|||
}
|
||||
|
||||
_ = gui.setViewContent(gui.Views.FilterPrefix, gui.filterPrompt())
|
||||
_ = gui.setViewContent(gui.Views.LogsFilterPrefix, gui.logsFilterPrompt())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ type TranslationSet struct {
|
|||
|
||||
LcNextScreenMode string
|
||||
LcPrevScreenMode string
|
||||
LcFilterLogs string
|
||||
FilterPrompt string
|
||||
LogsFilterPrompt string
|
||||
|
||||
FocusProjects string
|
||||
FocusServices string
|
||||
|
|
@ -269,7 +271,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