diff --git a/Makefile b/Makefile index 3bb4d0b1..b5e40477 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME) GIT_REV ?= $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH ?= $(shell date +%s) DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") -VERSION ?= v0.25.15 +VERSION ?= v0.25.16 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/assets/k9s-xmas.png b/assets/k9s-xmas.png new file mode 100644 index 00000000..979cf02c Binary files /dev/null and b/assets/k9s-xmas.png differ diff --git a/change_logs/release_v0.25.16.md b/change_logs/release_v0.25.16.md new file mode 100644 index 00000000..c9f698a9 --- /dev/null +++ b/change_logs/release_v0.25.16.md @@ -0,0 +1,81 @@ + + +# Release v0.25.16 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! + +If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +### A Word From Our Sponsors... + +I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`! + +* [Sebastian Racs](https://github.com/sebracs) +* [Timothy C. Arland](https://github.com/tcarland) +* [Julie Ng](https://github.com/julie-ng) + +So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large. + +Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us! + +Thank you!! + +--- + +## โ™ซ Sounds Behind The Release โ™ญ + +[Blue Christmas - Fats Domino](https://www.youtube.com/watch?v=7jeo09zAskc) +[Mele Kalikimaka - Bing Crosby](https://www.youtube.com/watch?v=hEvGKUXW0iI) +[Cause - Rodriguez -- Spreading The Holiday Cheer! ๐Ÿคจ](https://www.youtube.com/watch?v=oKFkc19T3Dk) + +--- + +## ๐ŸŽ…๐ŸŽ„ !!Merry Christmas To All!! ๐ŸŽ„๐ŸŽ… + +I hope you will take this time of the year to relax, re-source and spend quality time with your loved ones. I know it's been a `tad rocky` of recent ;( as I've gotten seriously slammed with work in the last few months... +The fine folks here on this channel have been nothing but kind, patient and willing to help, this humbles me! I feel truly blessed to be affiliated with our great `k9sers` community! +Next month, we'll celebrate our anniversary as we've started out in this venture back in Jan 2019 (Yikes!) so get crack'in and iron out those bow ties already!! + +Best wishes for great health, happiness and continued success for 2022 to you all!! + +-Fernand + +--- + +## A Christmas Story... + +As of this drop, we've added a new feature to override the sort column and order for a given Kubernetes resource. This feature piggy backs of custom column views and add a new attribute namely `sortColumn`. For example say you'd like to set the default sort for pods to age descending vs name/namespace, you can now do the following in your `views.yml` file in the k9s config directory: + +NOTE: This file is live thus you can nav to your fav resource, change the column config and view the resource columns and sort changes... Woot!! + +```yaml +k9s: + views: + v1/endpoints: + columns: + - NAME + - NAMESPACE + - ENDPOINTS + - AGE + v1/pods: + sortColumn: AGE:desc # => suffix [:asc|:desc] for ascending or descending order. + v1/services: + ... +``` + +--- + +## Resolved Issues + +* [Issue #1398](https://github.com/derailed/k9s/issues/1398) Pod logs containing brackets not in k9s logs output +* [Issue #1397](https://github.com/derailed/k9s/issues/1397) Regression: k9s no longer starts in current context namespace since v0.25.12 +* [Issue #1358](https://github.com/derailed/k9s/issues/1358) Namespaces list is empty +* [Issue #956](https://github.com/derailed/k9s/issues/956) Feature request : Default column sort (by resource view) + +--- + + ยฉ 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/root.go b/cmd/root.go index df909c3f..a3c37449 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,9 +2,6 @@ package cmd import ( "fmt" - "os" - "runtime/debug" - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" @@ -14,6 +11,8 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "os" + "runtime/debug" ) const ( @@ -53,6 +52,17 @@ func Execute() { } func run(cmd *cobra.Command, args []string) { + config.EnsurePath(*k9sFlags.LogFile, config.DefaultDirMod) + mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY + file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod) + if err != nil { + panic(err) + } + defer func() { + if file != nil { + _ = file.Close() + } + }() defer func() { if err := recover(); err != nil { log.Error().Msgf("Boom! %v", err) @@ -63,15 +73,6 @@ func run(cmd *cobra.Command, args []string) { } }() - config.EnsurePath(*k9sFlags.LogFile, config.DefaultDirMod) - mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY - file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod) - defer func() { - _ = file.Close() - }() - if err != nil { - panic(err) - } log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel)) @@ -136,6 +137,8 @@ func loadConfiguration() *config.Config { func parseLevel(level string) zerolog.Level { switch level { + case "trace": + return zerolog.TraceLevel case "debug": return zerolog.DebugLevel case "warn": @@ -161,7 +164,7 @@ func initK9sFlags() { k9sFlags.LogLevel, "logLevel", "l", config.DefaultLogLevel, - "Specify a log level (info, warn, debug, error)", + "Specify a log level (info, warn, debug, trace, error)", ) rootCmd.Flags().StringVarP( k9sFlags.LogFile, diff --git a/internal/client/client.go b/internal/client/client.go index 8ce2243a..cb0b80be 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -89,6 +89,7 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { ResourceAttributes: &authorizationv1.ResourceAttributes{ Namespace: ns, Group: res.Group, + Version: res.Version, Resource: res.Resource, Subresource: spec.SubResource(), }, @@ -162,6 +163,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) for _, v := range verbs { sar.Spec.ResourceAttributes.Verb = v resp, err := client.Create(ctx, sar, metav1.CreateOptions{}) + log.Trace().Msgf("[CAN] %s(%s) %v <<%v>>", gvr, verbs, resp, err) if err != nil { log.Warn().Err(err).Msgf(" Dial Failed!") a.cache.Add(key, false, cacheExpiry) diff --git a/internal/client/types.go b/internal/client/types.go index 2a2d8538..c57dbbda 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -21,6 +21,9 @@ const ( // AllNamespaces designates all namespaces. AllNamespaces = "" + // DefaultNamespace designates the default namespace. + DefaultNamespace = "default" + // ClusterScope designates a resource is not namespaced. ClusterScope = "-" diff --git a/internal/config/cluster.go b/internal/config/cluster.go index 3829cb9b..ec0a9c32 100644 --- a/internal/config/cluster.go +++ b/internal/config/cluster.go @@ -34,6 +34,9 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { if c.Namespace == nil { c.Namespace = NewNamespace() } + if c.Namespace.Active == client.AllNamespaces { + c.Namespace.Active = client.NamespaceAll + } if c.FeatureGates == nil { c.FeatureGates = NewFeatureGates() diff --git a/internal/config/config.go b/internal/config/config.go index 5b258b6f..eb103cc6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,32 +90,24 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) } c.K9s.CurrentCluster = context.Cluster - c.K9s.ActivateCluster() + c.K9s.ActivateCluster(context.Namespace) - var cns string - if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil { - cns = cl.Namespace.Active - } - var ns string + var ns = client.DefaultNamespace if k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces) { ns = client.NamespaceAll } else if isSet(flags.Namespace) { ns = *flags.Namespace - } else if context.Namespace != "" { + } else if isSet(flags.Context) { ns = context.Namespace - if cns != "" { - ns = cns - } } else { - ns = cns + ns = c.K9s.ActiveCluster().Namespace.Active } - if ns != "" { - if err := c.SetActiveNamespace(ns); err != nil { - return err - } - flags.Namespace = &ns + if err := c.SetActiveNamespace(ns); err != nil { + return err } + flags.Namespace = &ns + if isSet(flags.ClusterName) { c.K9s.CurrentCluster = *flags.ClusterName } @@ -166,9 +158,6 @@ func (c *Config) ActiveNamespace() string { // ValidateFavorites ensure favorite ns are legit. func (c *Config) ValidateFavorites() { cl := c.K9s.ActiveCluster() - if cl == nil { - cl = NewCluster() - } cl.Validate(c.client, c.settings) cl.Namespace.Validate(c.client, c.settings) } @@ -176,16 +165,14 @@ func (c *Config) ValidateFavorites() { // FavNamespaces returns fav namespaces in the current cluster. func (c *Config) FavNamespaces() []string { cl := c.K9s.ActiveCluster() - if cl == nil { - return nil - } - return c.K9s.ActiveCluster().Namespace.Favorites + + return cl.Namespace.Favorites } // SetActiveNamespace set the active namespace in the current cluster. func (c *Config) SetActiveNamespace(ns string) error { - if c.K9s.ActiveCluster() != nil { - return c.K9s.ActiveCluster().Namespace.SetActive(ns, c.settings) + if cl := c.K9s.ActiveCluster(); cl != nil { + return cl.Namespace.SetActive(ns, c.settings) } err := errors.New("no active cluster. unable to set active namespace") log.Error().Err(err).Msg("SetActiveNamespace") @@ -195,11 +182,11 @@ func (c *Config) SetActiveNamespace(ns string) error { // ActiveView returns the active view in the current cluster. func (c *Config) ActiveView() string { - if c.K9s.ActiveCluster() == nil { + cl := c.K9s.ActiveCluster() + if cl == nil { return defaultView } - - cmd := c.K9s.ActiveCluster().View.Active + cmd := cl.View.Active if c.K9s.manualCommand != nil && *c.K9s.manualCommand != "" { cmd = *c.K9s.manualCommand } @@ -209,8 +196,7 @@ func (c *Config) ActiveView() string { // SetActiveView set the currently cluster active view. func (c *Config) SetActiveView(view string) { - cl := c.K9s.ActiveCluster() - if cl != nil { + if cl := c.K9s.ActiveCluster(); cl != nil { cl.View.Active = view } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3d54f1ce..f7883241 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -30,7 +30,7 @@ func TestConfigRefine(t *testing.T) { issue: false, context: "test1", cluster: "cluster1", - namespace: "default", + namespace: "ns1", }, "overrideNS": { flags: &genericclioptions.ConfigFlags{ diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 621943a5..483cb107 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -47,11 +47,13 @@ func NewK9s() *K9s { } // ActivateCluster initializes the active cluster is not present. -func (k *K9s) ActivateCluster() { +func (k *K9s) ActivateCluster(ns string) { if _, ok := k.Clusters[k.CurrentCluster]; ok { return } - k.Clusters[k.CurrentCluster] = NewCluster() + cl := NewCluster() + cl.Namespace.Active = ns + k.Clusters[k.CurrentCluster] = cl } // OverrideRefreshRate set the refresh rate manually. @@ -59,17 +61,17 @@ func (k *K9s) OverrideRefreshRate(r int) { k.manualRefreshRate = r } -// OverrideHeadless set the headlessness manually. +// OverrideHeadless toggle the header manually. func (k *K9s) OverrideHeadless(b bool) { k.manualHeadless = &b } -// OverrideLogoless set the logolessness manually. +// OverrideLogoless toggle the k9s logo manually. func (k *K9s) OverrideLogoless(b bool) { k.manualLogoless = &b } -// OverrideCrumbsless set the crumbslessness manually. +// OverrideCrumbsless tooh the crumbslessness manually. func (k *K9s) OverrideCrumbsless(b bool) { k.manualCrumbsless = &b } @@ -154,7 +156,6 @@ func (k *K9s) ActiveCluster() *Cluster { if k.Clusters == nil { k.Clusters = map[string]*Cluster{} } - if c, ok := k.Clusters[k.CurrentCluster]; ok { return c } diff --git a/internal/config/ns.go b/internal/config/ns.go index 4b75bbb2..0cde6e2b 100644 --- a/internal/config/ns.go +++ b/internal/config/ns.go @@ -22,7 +22,7 @@ type Namespace struct { func NewNamespace() *Namespace { return &Namespace{ Active: defaultNS, - Favorites: []string{"default"}, + Favorites: []string{defaultNS}, } } diff --git a/internal/config/views.go b/internal/config/views.go index 0d2a8407..4c10a9e9 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -18,7 +18,8 @@ type ViewConfigListener interface { // ViewSetting represents a view configuration. type ViewSetting struct { - Columns []string `yaml:"columns"` + Columns []string `yaml:"columns"` + SortColumn string `yaml:"sortColumn"` } // ViewSettings represent a collection of view configurations. diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index cd3015a2..bd3fd03d 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -130,7 +130,7 @@ func (o *LogOptions) ToLogItem(bytes []byte) *LogItem { func (o *LogOptions) ToErrLogItem(err error) *LogItem { t := time.Now().UTC().Format(time.RFC3339Nano) - item := NewLogItem([]byte(fmt.Sprintf("%s [red::b]%s\n", t, err))) + item := NewLogItem([]byte(fmt.Sprintf("%s [orange::b]%s[::-]\n", t, err))) item.IsError = true return item } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 4cebf749..fa0bc23a 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -313,18 +313,14 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan { var ( - out = make(LogChan, 2) - wg sync.WaitGroup + out = make(LogChan, 2) + wg sync.WaitGroup ) wg.Add(1) go func() { - defer func() { - wg.Done() - log.Debug().Msgf("<<< RETRY-TAIL DONE!!! %s", opts.Info()) - }() + defer wg.Done() podOpts := opts.ToPodLogOptions() - log.Debug().Msgf(">>> RETRY-TAIL START %s", opts.Info()) var stream io.ReadCloser for r := 0; r < logRetryCount; r++ { var e error @@ -345,7 +341,6 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan { select { case <-ctx.Done(): - log.Debug().Msgf("LOG CANCELED %s", opts.Info()) return default: if e != nil { @@ -358,7 +353,6 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan { go func() { wg.Wait() close(out) - log.Debug().Msgf("<<< LOG-TAILER %s DONE!!", opts.Info()) }() return out @@ -369,7 +363,6 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out if err := stream.Close(); err != nil { log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info()) } - log.Debug().Msgf("<<< LOG-READER EXIT!!! %s", opts.Info()) wg.Done() }() @@ -383,11 +376,11 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out if errors.Is(err, io.EOF) { e := fmt.Errorf("Stream closed %w for %s", err, opts.Info()) item = opts.ToErrLogItem(e) - log.Debug().Err(e).Msg("log-reader EOF") + log.Warn().Err(e).Msg("log-reader EOF") } else { e := fmt.Errorf("Stream canceled %w for %s", err, opts.Info()) item = opts.ToErrLogItem(e) - log.Debug().Err(e).Msg("log-reader canceled") + log.Warn().Err(e).Msg("log-reader canceled") } } select { diff --git a/internal/render/header.go b/internal/render/header.go index 0a263713..b17a0ec2 100644 --- a/internal/render/header.go +++ b/internal/render/header.go @@ -147,12 +147,15 @@ func (h Header) HasAge() bool { // IsMetricsCol checks if given column index represents metrics. func (h Header) IsMetricsCol(col int) bool { + if col < 0 || col >= len(h) { + return false + } return h[col].MX } // IsTimeCol checks if given column index represents a timestamp. func (h Header) IsTimeCol(col int) bool { - if col >= len(h) { + if col < 0 || col >= len(h) { return false } diff --git a/internal/ui/table.go b/internal/ui/table.go index 5eca98cd..87f54421 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -191,7 +191,9 @@ func (t *Table) Update(data render.TableData, hasMetrics bool) { func (t *Table) doUpdate(data render.TableData) { if client.IsAllNamespaces(data.Namespace) { t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false) + t.sortCol.name = "NAMESPACE" } else { + t.sortCol.name = "NAME" t.actions.Delete(KeyShiftP) } @@ -203,16 +205,24 @@ func (t *Table) doUpdate(data render.TableData) { cols = t.header.Columns(t.wide) } custData := data.Customize(cols, t.wide) - - if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 && t.sortCol.name != "NONE" { - t.sortCol.name = custData.Header[0].Name - if t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace) { - if idx := custData.Header.IndexOf("NAME", false); idx != -1 { - t.sortCol.name = custData.Header[idx].Name + if t.viewSetting != nil && t.viewSetting.SortColumn != "" { + tokens := strings.Split(t.viewSetting.SortColumn, ":") + if custData.Header.IndexOf(tokens[0], false) >= 0 { + t.sortCol.name, t.sortCol.asc = tokens[0], true + if len(tokens) == 2 && tokens[1] == "desc" { + t.sortCol.asc = false } } } + if t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace) && len(custData.Header) > 0 { + if idx := custData.Header.IndexOf("NAME", false); idx >= 0 { + t.sortCol.name = custData.Header[idx].Name + } else { + t.sortCol.name = custData.Header[0].Name + } + } + t.Clear() fg := t.styles.Table().Header.FgColor.Color() bg := t.styles.Table().Header.BgColor.Color() diff --git a/internal/view/browser.go b/internal/view/browser.go index 003cc02c..12e74cd8 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -140,7 +140,7 @@ func (b *Browser) Start() { b.Table.Start() b.CmdBuff().AddListener(b) if err := b.GetModel().Watch(b.prepareContext()); err != nil { - log.Error().Err(err).Msgf("Watcher failed for %s", b.GVR()) + b.App().Flash().Err(fmt.Errorf("Watcher failed for %s -- %w", b.GVR(), err)) } } diff --git a/internal/view/exec.go b/internal/view/exec.go index 07adfd11..8300b774 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -199,14 +199,17 @@ func ssh(a *App, node string) error { if err := launchShellPod(a, node); err != nil { return err } - ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace + + cl := a.Config.K9s.ActiveCluster() + ns := cl.ShellPod.Namespace sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell) return nil } func sshIn(a *App, fqn, co string) { - cfg := a.Config.K9s.ActiveCluster().ShellPod + cl := a.Config.K9s.ActiveCluster() + cfg := cl.ShellPod os, err := getPodOS(a.factory, fqn) if err != nil { log.Warn().Err(err).Msgf("os detect failed") @@ -232,12 +235,13 @@ func sshIn(a *App, fqn, co string) { } func nukeK9sShell(a *App) error { - cl := a.Config.K9s.CurrentCluster - if !a.Config.K9s.Clusters[cl].FeatureGates.NodeShell { + clName := a.Config.K9s.CurrentCluster + if !a.Config.K9s.Clusters[clName].FeatureGates.NodeShell { return nil } - ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace + cl := a.Config.K9s.ActiveCluster() + ns := cl.ShellPod.Namespace ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() @@ -256,8 +260,9 @@ func nukeK9sShell(a *App) error { func launchShellPod(a *App, node string) error { a.Flash().Infof("Launching node shell on %s...", node) - ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace - spec := k9sShellPod(node, a.Config.K9s.ActiveCluster().ShellPod) + cl := a.Config.K9s.ActiveCluster() + ns := cl.ShellPod.Namespace + spec := k9sShellPod(node, cl.ShellPod) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/internal/view/log.go b/internal/view/log.go index 1d809d1a..8e279b62 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -350,6 +350,7 @@ func (l *Log) Flush(lines [][]byte) { if l.cancelUpdates { break } + log.Debug().Msgf("FLUSH %q", string(lines[i])) _, _ = l.ansiWriter.Write(lines[i]) } if l.follow { diff --git a/internal/view/logger.go b/internal/view/logger.go index 932d13f8..4f977bb2 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -36,7 +36,7 @@ func (l *Logger) Init(_ context.Context) error { if l.title != "" { l.SetBorder(true) } - l.SetScrollable(true).SetWrap(true).SetRegions(true) + l.SetScrollable(true).SetWrap(true) l.SetDynamicColors(true) l.SetHighlightColor(tcell.ColorOrange) l.SetTitleColor(tcell.ColorAqua)