diff --git a/cmd/root.go b/cmd/root.go index 271618e5..b4d9b085 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -136,6 +136,7 @@ func loadConfiguration() (*config.Config, error) { return nil, err } conn, err := client.InitConnection(k8sCfg) + k9sCfg.SetConnection(conn) if err != nil { log.Error().Err(err).Msgf("failed to connect to cluster") } else { @@ -143,12 +144,14 @@ func loadConfiguration() (*config.Config, error) { if !k9sCfg.GetConnection().CheckConnectivity() { log.Panic().Msgf("K9s can't connect to cluster") } + if !k9sCfg.GetConnection().ConnectionOK() { + panic("No connectivity") + } log.Info().Msg("✅ Kubernetes connectivity") if err := k9sCfg.Save(); err != nil { log.Error().Err(err).Msg("Config save") } } - k9sCfg.SetConnection(conn) return k9sCfg, nil } diff --git a/internal/client/client.go b/internal/client/client.go index db0c9c09..02ee98b1 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -28,7 +28,7 @@ const ( cacheExpiry = 5 * time.Minute cacheMXKey = "metrics" cacheMXAPIKey = "metricsAPI" - checkConnTimeout = 5 * time.Second + checkConnTimeout = 3 * time.Second // CallTimeout represents default api call timeout. CallTimeout = 5 * time.Second @@ -64,9 +64,10 @@ func InitConnection(config *Config) (*APIClient, error) { config: config, cache: cache.NewLRUExpireCache(cacheSize), } + a.connOK = true _, err := a.supportsMetricsResources() - if err == nil { - a.connOK = true + if err != nil { + a.connOK = false } return &a, err @@ -136,7 +137,7 @@ func (a *APIClient) clearCache() { func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { log.Debug().Msgf("Check Access %q::%q", ns, gvr) if !a.connOK { - return false, errors.New("no API server connection") + return false, errors.New("ACCESS -- No API server connection") } if IsClusterWide(ns) { ns = AllNamespaces @@ -148,6 +149,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) } } + log.Debug().Msgf("----> Calling API") dial, err := a.Dial() if err != nil { return false, err @@ -229,12 +231,13 @@ func (a *APIClient) CheckConnectivity() (status bool) { if err != nil { return false } + cfg.Timeout = checkConnTimeout client, err := kubernetes.NewForConfig(cfg) if err != nil { log.Error().Err(err).Msgf("Unable to connect to api server") return } - log.Debug().Msgf("Checking APIServer on %#v", cfg.Host) + log.Debug().Msgf("CONN-CHECK on %#v", cfg.Host) // Check connection if _, err := client.ServerVersion(); err == nil { diff --git a/internal/client/config.go b/internal/client/config.go index 3dd5fdbd..8c5c4f41 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -6,7 +6,6 @@ import ( "strings" "sync" - "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" @@ -299,8 +298,6 @@ func (c *Config) RESTConfig() (*restclient.Config, error) { } c.restConfig.QPS = defaultQPS c.restConfig.Burst = defaultBurst - c.restConfig.Timeout = checkConnTimeout - log.Debug().Msgf("Connecting to API Server %s", c.restConfig.Host) return c.restConfig, nil } diff --git a/internal/color/colorize.go b/internal/color/colorize.go index 72be9de5..a8f84fc9 100644 --- a/internal/color/colorize.go +++ b/internal/color/colorize.go @@ -4,8 +4,10 @@ import ( "fmt" ) -// ColorFmt colorize a string with ansi colors. -const ColorFmt = "\x1b[%dm%s\x1b[0m" +const ( + colorFmt = "\x1b[%dm%s\x1b[0m" + ansiColorFmt = "\033[38;5;%dm%s\033[0m" +) // Paint describes a terminal color. type Paint int @@ -30,5 +32,29 @@ func Colorize(s string, c Paint) string { if c == 0 { return s } - return fmt.Sprintf(ColorFmt, c, s) + return fmt.Sprintf(colorFmt, c, s) +} + +// AnsiColorize colors a string. +func AnsiColorize(s string, c int) string { + return fmt.Sprintf(ansiColorFmt, c, s) +} + +// Highlight colorize bytes at given indices. +func Highlight(bb []byte, ii []int, c int) []byte { + b := make([]byte, 0, len(bb)) + for i, j := 0, 0; i < len(bb); i++ { + if j < len(ii) && ii[j] == i { + b = append(b, colorizeByte(bb[i], 209)...) + j++ + } else { + b = append(b, bb[i]) + } + } + + return b +} + +func colorizeByte(b byte, color int) []byte { + return []byte(AnsiColorize(string(b), color)) } diff --git a/internal/color/colorize_test.go b/internal/color/colorize_test.go index 9a8f6da9..db9b7682 100644 --- a/internal/color/colorize_test.go +++ b/internal/color/colorize_test.go @@ -25,3 +25,26 @@ func TestColorize(t *testing.T) { }) } } + +func TestHighlight(t *testing.T) { + uu := map[string]struct { + text []byte + indices []int + color int + e string + }{ + "white": { + text: []byte("the brown fox"), + color: 209, + indices: []int{4, 5, 6, 7, 8}, + e: "the \x1b[38;5;209mb\x1b[0m\x1b[38;5;209mr\x1b[0m\x1b[38;5;209mo\x1b[0m\x1b[38;5;209mw\x1b[0m\x1b[38;5;209mn\x1b[0m fox", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, string(color.Highlight([]byte(u.text), u.indices, u.color))) + }) + } +} diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index e0b24f56..070ff4b9 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/derailed/k9s/internal/color" "github.com/derailed/tview" "github.com/rs/zerolog/log" "github.com/sahilm/fuzzy" @@ -51,6 +52,18 @@ func (l *LogItem) ID() string { return l.Container } +func (l *LogItem) Clone() *LogItem { + bytes := make([]byte, len(l.Bytes)) + copy(bytes, l.Bytes) + return &LogItem{ + Container: l.Container, + Pod: l.Pod, + Timestamp: l.Timestamp, + SingleContainer: l.SingleContainer, + Bytes: bytes, + } +} + // Info returns pod and container information. func (l *LogItem) Info() string { return fmt.Sprintf("%q::%q", l.Pod, l.Container) @@ -61,26 +74,19 @@ func (l *LogItem) IsEmpty() bool { return len(l.Bytes) == 0 } -const colorFmt = "\033[38;5;%dm%s\033[0m" - -// colorize me -func colorize(s string, c int) string { - return fmt.Sprintf(colorFmt, c, s) -} - // Render returns a log line as string. func (l *LogItem) Render(c int, showTime bool) []byte { bb := make([]byte, 0, 30+len(l.Bytes)+len(l.Info())) if showTime { - bb = append(bb, colorize(fmt.Sprintf("%-30s ", l.Timestamp), 106)...) + bb = append(bb, color.AnsiColorize(fmt.Sprintf("%-30s ", l.Timestamp), 106)...) } if l.Pod != "" { - bb = append(bb, []byte(colorize(l.Pod, c))...) + bb = append(bb, []byte(color.AnsiColorize(l.Pod, c))...) bb = append(bb, ':') } if !l.SingleContainer && l.Container != "" { - bb = append(bb, []byte(colorize(l.Container, c))...) + bb = append(bb, []byte(color.AnsiColorize(l.Container, c))...) bb = append(bb, ' ') } bb = append(bb, []byte(tview.Escape(string(l.Bytes)))...) @@ -139,45 +145,54 @@ func (l LogItems) DumpDebug(m string) { } // Filter filters out log items based on given filter. -func (l LogItems) Filter(q string) ([]int, error) { +func (l LogItems) Filter(q string) ([]int, [][]int, error) { if q == "" { - return nil, nil + return nil, nil, nil } if IsFuzzySelector(q) { - return l.fuzzyFilter(strings.TrimSpace(q[2:])), nil + mm, ii := l.fuzzyFilter(strings.TrimSpace(q[2:])) + return mm, ii, nil } - indexes, err := l.filterLogs(q) + matches, indices, err := l.filterLogs(q) if err != nil { log.Error().Err(err).Msgf("Logs filter failed") - return nil, err + return nil, nil, err } - return indexes, nil + return matches, indices, nil } var fuzzyRx = regexp.MustCompile(`\A\-f`) -func (l LogItems) fuzzyFilter(q string) []int { +func (l LogItems) fuzzyFilter(q string) ([]int, [][]int) { q = strings.TrimSpace(q) - matches := make([]int, 0, len(l)) + matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) mm := fuzzy.Find(q, l.Lines()) for _, m := range mm { matches = append(matches, m.Index) + indices = append(indices, m.MatchedIndexes) } - return matches + return matches, indices } -func (l LogItems) filterLogs(q string) ([]int, error) { +func (l LogItems) filterLogs(q string) ([]int, [][]int, error) { rx, err := regexp.Compile(`(?i)` + q) if err != nil { - return nil, err + return nil, nil, err } - matches := make([]int, 0, len(l)) + matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) for i, line := range l.Lines() { - if rx.MatchString(line) { + if locs := rx.FindStringIndex(line); locs != nil { matches = append(matches, i) + ii := make([]int, 0, 10) + for i := 0; i < len(locs); i += 2 { + for j := locs[i]; j < locs[i+1]; j++ { + ii = append(ii, j) + } + } + indices = append(indices, ii) } } - return matches, nil + return matches, indices, nil } diff --git a/internal/dao/log_item_test.go b/internal/dao/log_item_test.go index ba17b1d9..ffbf2a0a 100644 --- a/internal/dao/log_item_test.go +++ b/internal/dao/log_item_test.go @@ -70,7 +70,7 @@ func TestLogItemsFilter(t *testing.T) { for _, i := range ii { i.Pod, i.Container = n, u.opts.Container } - res, err := ii.Filter(u.q) + res, _, err := ii.Filter(u.q) assert.Equal(t, u.err, err) if err == nil { assert.Equal(t, u.e, res) diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 850ab933..bdbea37b 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -275,7 +275,7 @@ func loadRBAC(m ResourceMetas) { func loadPreferred(f Factory, m ResourceMetas) error { if !f.Client().ConnectionOK() { - log.Error().Msgf("no API server connection") + log.Error().Msgf("PreferredRES - No API server connection") return nil } diff --git a/internal/model/log.go b/internal/model/log.go index ee8adcfc..3a2b6f76 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -8,6 +8,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -277,21 +278,24 @@ func applyFilter(q string, lines dao.LogItems) (dao.LogItems, error) { if q == "" { return lines, nil } - indexes, err := lines.Filter(q) + matches, indices, err := lines.Filter(q) if err != nil { return nil, err } + // No filter! - if indexes == nil { + if matches == nil { return lines, nil } // Blank filter - if len(indexes) == 0 { + if len(matches) == 0 { return nil, nil } - filtered := make(dao.LogItems, 0, len(indexes)) - for _, idx := range indexes { - filtered = append(filtered, lines[idx]) + filtered := make(dao.LogItems, 0, len(matches)) + for i, idx := range matches { + item := lines[idx].Clone() + item.Bytes = color.Highlight(item.Bytes, indices[i], 209) + filtered = append(filtered, item) } return filtered, nil diff --git a/internal/model/log_test.go b/internal/model/log_test.go index 36ed5806..04a30239 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -195,7 +195,8 @@ func TestLogTimedout(t *testing.T) { assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, dao.LogItems{data[0]}, v.data) + const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" + assert.Equal(t, e, string(v.data[0].Bytes)) } // ---------------------------------------------------------------------------- diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index 273f0f6e..1e1db86c 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -3,6 +3,7 @@ package ui import ( "github.com/derailed/tview" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) // SelectTable represents a table with selections. @@ -51,6 +52,17 @@ func (s *SelectTable) GetSelectedItems() []string { return items } +// GetRowID returns the row id at at given location. +func (s *SelectTable) GetRowID(index int) (string, bool) { + cell := s.GetCell(index, 0) + if cell == nil { + return "", false + } + id, ok := cell.GetReference().(string) + + return id, ok +} + // GetSelectedItem returns the currently selected item name. func (s *SelectTable) GetSelectedItem() string { if s.GetSelectedRowIndex() == 0 || s.model.Empty() { @@ -138,6 +150,68 @@ func (s *SelectTable) ToggleMark() { ) } +// ToggleSpanMark toggles marked row +func (s *SelectTable) SpanMark() { + selIndex, prev := s.GetSelectedRowIndex(), -1 + if selIndex <= 0 { + return + } + // Look back to find previous mark + for i := selIndex - 1; i > 0; i-- { + id, ok := s.GetRowID(i) + if !ok { + break + } + if _, ok := s.marks[id]; ok { + prev = i + break + } + } + if prev != -1 { + s.markRange(prev, selIndex) + return + } + + // Look forward to see if we have a mark + for i := selIndex; i < s.GetRowCount(); i++ { + id, ok := s.GetRowID(i) + if !ok { + break + } + if _, ok := s.marks[id]; ok { + prev = i + break + } + } + s.markRange(prev, selIndex) +} + +func (s *SelectTable) markRange(prev, curr int) { + if prev < 0 { + return + } + if prev > curr { + prev, curr = curr, prev + } + log.Debug().Msgf("Span Range %d::%d", prev, curr) + for i := prev + 1; i <= curr; i++ { + id, ok := s.GetRowID(i) + if !ok { + break + } + s.marks[id] = struct{}{} + cell := s.GetCell(s.GetSelectedRowIndex(), 0) + if cell == nil { + break + } + s.SetSelectedStyle( + tcell.ColorBlack, + cell.Color, + tcell.AttrBold, + ) + } +} + // IsMarked returns true if this item was marked. func (s *Table) IsMarked(item string) bool { _, ok := s.marks[item] diff --git a/internal/view/browser.go b/internal/view/browser.go index 27d37447..568e30c9 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -399,13 +399,9 @@ func (b *Browser) defaultContext() context.Context { if b.Path != "" { ctx = context.WithValue(ctx, internal.KeyPath, b.Path) } - // BOZO!! - // ctx = context.WithValue(ctx, internal.KeyLabels, "") if ui.IsLabelSelector(b.CmdBuff().GetText()) { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) } - // BOZO!! - // ctx = context.WithValue(ctx, internal.KeyFields, "") ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) return ctx diff --git a/internal/view/command.go b/internal/view/command.go index 1b12b44f..fb100aca 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -39,6 +39,7 @@ func (c *Command) Init() error { c.alias = dao.NewAlias(c.app.factory) if _, err := c.alias.Ensure(); err != nil { log.Error().Err(err).Msgf("command init failed!") + return err } customViewers = loadCustomViewers() @@ -106,21 +107,6 @@ func (c *Command) xrayCmd(cmd string) error { return c.exec(cmd, "xrays", x, true) } -// BOZO!! -// func (c *Command) checkAccess(gvr string) error { -// m, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr)) -// if err != nil { -// return err -// } -// ns := client.CleanseNamespace(c.app.Config.ActiveNamespace()) -// if dao.IsK8sMeta(m) && c.app.ConOK() { -// if _, e := c.app.factory.CanForResource(ns, gvr, client.MonitorAccess); e != nil { -// return e -// } -// } -// return nil -// } - // Exec the Command by showing associated display. func (c *Command) run(cmd, path string, clearStack bool) error { if c.specialCmd(cmd, path) { @@ -131,9 +117,6 @@ func (c *Command) run(cmd, path string, clearStack bool) error { if err != nil { return err } - //if err := c.checkAccess(gvr); err != nil { - // return err - //} switch cmds[0] { case "ctx", "context", "contexts": @@ -159,6 +142,7 @@ func (c *Command) run(cmd, path string, clearStack bool) error { func (c *Command) defaultCmd() error { if !c.app.Conn().ConnectionOK() { + log.Debug().Msgf("YO!!") return c.run("ctx", "", true) } view := c.app.Config.ActiveView() diff --git a/internal/view/help.go b/internal/view/help.go index 09886b42..9e862a63 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -220,11 +220,11 @@ func (h *Help) showGeneral() model.MenuHints { }, { Mnemonic: "tab", - Description: "Next Field", + Description: "Field Next", }, { Mnemonic: "backtab", - Description: "Previous Field", + Description: "Field Previous", }, { Mnemonic: "Ctrl-r", @@ -232,7 +232,7 @@ func (h *Help) showGeneral() model.MenuHints { }, { Mnemonic: "Ctrl-u", - Description: "Clear command", + Description: "Command Clear", }, { Mnemonic: "Ctrl-e", @@ -248,7 +248,11 @@ func (h *Help) showGeneral() model.MenuHints { }, { Mnemonic: "Ctrl-space", - Description: "Clear Marks", + Description: "Mark Range", + }, + { + Mnemonic: "Ctrl-\\", + Description: "Mark Clear", }, { Mnemonic: "Ctrl-s", diff --git a/internal/view/log.go b/internal/view/log.go index 9242a3fa..771bc815 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -183,6 +183,7 @@ func (l *Log) bindKeys() { ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), ui.KeyC: 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), ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true), @@ -324,6 +325,11 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { return nil } +func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey { + fmt.Fprintln(l.ansiWriter, fmt.Sprintf("[white::b]%s[::]", strings.Repeat("-", 80))) + return nil +} + func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { if l.app.InCmdMode() { return evt diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index 63458ba1..f43dfbfa 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -16,7 +16,7 @@ func TestLogAutoScroll(t *testing.T) { v.GetModel().Set(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")}) v.GetModel().Notify(true) - assert.Equal(t, 13, len(v.Hints())) + assert.Equal(t, 14, len(v.Hints())) v.toggleAutoScrollCmd(nil) assert.Equal(t, "Autoscroll: Off FullScreen: Off Timestamps: Off Wrap: Off", v.Indicator().GetText(true)) @@ -85,7 +85,7 @@ func TestLogFilter(t *testing.T) { l.SendKeys(ui.KeySlash) l.SendStrokes("zorg") - assert.Equal(t, "zorg", list.lines) + assert.Equal(t, "\x1b[38;5;209mz\x1b[0m\x1b[38;5;209mo\x1b[0m\x1b[38;5;209mr\x1b[0m\x1b[38;5;209mg\x1b[0m", list.lines) assert.Equal(t, 5, list.change) assert.Equal(t, 5, list.clear) assert.Equal(t, 0, list.fail) diff --git a/internal/view/table.go b/internal/view/table.go index 15baf047..06b6ca47 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -150,14 +150,15 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { func (t *Table) bindKeys() { t.Actions().Add(ui.KeyActions{ - ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), - tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), - tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), - ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), - tcell.KeyCtrlZ: ui.NewKeyAction("Toggle Faults", t.toggleFaultCmd, false), - tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false), - ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false), + ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), + tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Mark Range", t.markSpanCmd, false), + tcell.KeyCtrlBackslash: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), + tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), + tcell.KeyCtrlZ: ui.NewKeyAction("Toggle Faults", t.toggleFaultCmd, false), + tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false), }) } @@ -188,22 +189,22 @@ func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey { - path := t.GetSelectedItem() - if path == "" { - return evt - } t.ToggleMark() t.Refresh() return nil } +func (t *Table) markSpanCmd(evt *tcell.EventKey) *tcell.EventKey { + t.SpanMark() + t.Refresh() + + return nil +} + func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { - path := t.GetSelectedItem() - if path == "" { - return evt - } t.ClearMarks() + t.Refresh() return nil }