From 1c15fed1131299a232bb229cd223faaff923bb73 Mon Sep 17 00:00:00 2001 From: Graham Clark Date: Wed, 1 Sep 2021 23:02:31 -0400 Subject: [PATCH] Help the user upgrade to a more colorful TERM setting If the user runs in a terminal emulator with TERM=xterm or TERM=screen, then termshark will only use 8-colors. This is fine but the experience is better with 256 colors available (or more) - for example, for packet coloring. If the user knows to change TERM to xterm-256color, that's also fine, but not all termshark users will know to do this. This change adds a workflow to help the user run in a higher-color mode, if possible. On startup, termshark will check to see if (a) only 8 colors are in use and (b) if a 256-color version of the TERM variable is available. If so, termshark opens a dialog to suggest switching. If the user hits "No, don't ask", then termshark sets main.disable-term-helper in termshark.toml and won't ask again. If the user hits yes, termshark will restart using the 256-color TERM. After 3 seconds, termshark will open a dialog to ask the user to confirm that everything looks ok - the default is no, and if the user hits that, or if 10 seconds elapse without input, termshark reverts back to the original setting. If the user hits yes, termshark will ask if the user wants to save that TERM as the default to use; if yes, main.term will be set in termshark.toml. --- CHANGELOG.md | 2 + cmd/termshark/termshark.go | 73 ++++++++++++++- go.mod | 2 +- go.sum | 2 + ui/switchterm.go | 177 +++++++++++++++++++++++++++++++++++++ utils.go | 17 +++- 6 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 ui/switchterm.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c9120..796ab15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Added a config option, main.disk-cache-size-mb, that can be set to have termshark limit the size of its pcap cache directory. When the directory size exceeds its limit, termshark deletes oldest pcap files first. +- Added a workflow that helps a user to upgrade from a low-color TERM setting if termshark detects that + there is a 256-color version available in the terminfo database. ### Changed diff --git a/cmd/termshark/termshark.go b/cmd/termshark/termshark.go index 0a79388..216a4e0 100644 --- a/cmd/termshark/termshark.go +++ b/cmd/termshark/termshark.go @@ -64,6 +64,42 @@ func main() { res := cmain() ensureGoroutinesStopWG.Wait() + + if !termshark.ShouldSwitchTerminal && !termshark.ShouldSwitchBack { + os.Exit(res) + } + + os.Clearenv() + for _, e := range termshark.OriginalEnv { + ks := strings.SplitN(e, "=", 2) + if len(ks) == 2 { + os.Setenv(ks[0], ks[1]) + } + } + + exe, err := os.Executable() + if err != nil { + log.Warnf("Unexpected error determining termshark executable: %v", err) + os.Exit(1) + } + + switch { + case termshark.ShouldSwitchTerminal: + os.Setenv("TERMSHARK_ORIGINAL_TERM", os.Getenv("TERM")) + os.Setenv("TERM", fmt.Sprintf("%s-256color", os.Getenv("TERM"))) + case termshark.ShouldSwitchBack: + os.Setenv("TERM", os.Getenv("TERMSHARK_ORIGINAL_TERM")) + os.Setenv("TERMSHARK_ORIGINAL_TERM", "") + } + + // Need exec because we really need to re-initialize everything, including have + // all init() functions be called again + err = syscall.Exec(exe, os.Args, os.Environ()) + if err != nil { + log.Warnf("Unexpected error exec-ing termshark %s: %v", exe, err) + res = 1 + } + os.Exit(res) } @@ -71,6 +107,9 @@ func cmain() int { startedSuccessfully := false // true if we reached the point where packets were received and the UI started. uiSuspended := false // true if the UI was suspended due to SIGTSTP + // Preserve in case we need to re-exec e.g. if the user switches TERM + termshark.OriginalEnv = os.Environ() + sigChan := make(chan os.Signal, 100) // SIGINT and SIGQUIT will arrive only via an external kill command, // not the keyboard, because our line discipline is set up to pass @@ -275,7 +314,7 @@ func cmain() int { // terminal emumlator supports 256 colors. termVar := termshark.ConfString("main.term", "") if termVar != "" { - log.Infof("Configuration file overrides TERM setting, using TERM=%s", termVar) + fmt.Fprintf(os.Stderr, "Configuration file overrides TERM setting, using TERM=%s\n", termVar) os.Setenv("TERM", termVar) } @@ -1098,8 +1137,9 @@ Loop: // Need to do that here because the app won't know how many colors the screen // has (and therefore which variant of the theme to load) until the screen is // activated. - mode := theme.Mode(app.GetColorMode()).String() // more concise - themeName := termshark.ConfString(fmt.Sprintf("main.theme-%s", mode), "default") + mode := app.GetColorMode() + modeStr := theme.Mode(mode) // more concise + themeName := termshark.ConfString(fmt.Sprintf("main.theme-%s", modeStr), "default") loaded := false if themeName != "" { err = theme.Load(themeName, app) @@ -1132,6 +1172,33 @@ Loop: ui.StartUIChan = nil // make sure it's not triggered again + if runtime.GOOS != "windows" { + if mode == gowid.Mode8Colors { + // If exists is true, it means we already tried and then reverted back, so + // just load up termshark normally with no further interruption. + if _, exists := os.LookupEnv("TERMSHARK_ORIGINAL_TERM"); !exists { + if !termshark.ConfBool("main.disable-term-helper", false) { + err = termshark.Does256ColorTermExist() + if err != nil { + log.Infof("Must use 8-color mode because 256-color version of TERM=%s unavailable - %v.", os.Getenv("TERM"), err) + } else { + time.AfterFunc(time.Duration(3)*time.Second, func() { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + ui.SuggestSwitchingTerm(app) + })) + }) + } + } + } + } else if os.Getenv("TERMSHARK_ORIGINAL_TERM") != "" { + time.AfterFunc(time.Duration(3)*time.Second, func() { + app.Run(gowid.RunFunction(func(app gowid.IApp) { + ui.IsTerminalLegible(app) + })) + }) + } + } + defer func() { // Do this to make sure the program quits quickly if quit is invoked // mid-load. It's safe to call this if a pcap isn't being loaded. diff --git a/go.mod b/go.mod index 469d51d..8d1b96e 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gcla/deep v1.0.2 - github.com/gcla/gowid v1.2.1-0.20210821221812-8682c44dd77f + github.com/gcla/gowid v1.2.1-0.20210902022514-507cfa6ef3ed github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 github.com/gdamore/tcell v1.3.1-0.20200115030318-bff4943f9a29 github.com/go-test/deep v1.0.2 // indirect diff --git a/go.sum b/go.sum index b4908e0..2d8ddde 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/gcla/deep v1.0.2 h1:qBOx6eepcOSRYnHJ+f2ih4hP4Vca1YnLtXxp73n5KWI= github.com/gcla/deep v1.0.2/go.mod h1:evE9pbpSGhItmFoBIk8hPOIC/keKTGYhFl6Le1Av+GE= github.com/gcla/gowid v1.2.1-0.20210821221812-8682c44dd77f h1:Yoj7Qn6+Ms9i+8t5/m+P6aywYaw7HsFqFaaoj71oOrc= github.com/gcla/gowid v1.2.1-0.20210821221812-8682c44dd77f/go.mod h1:GL9KUFwKHjWS80G7N4hkA++mpOleVQpNvKIHyW9n0i4= +github.com/gcla/gowid v1.2.1-0.20210902022514-507cfa6ef3ed h1:BQp9rokK2GVxViqKTdJhZVDwytBwbT2KtsYdnq5rWmo= +github.com/gcla/gowid v1.2.1-0.20210902022514-507cfa6ef3ed/go.mod h1:GL9KUFwKHjWS80G7N4hkA++mpOleVQpNvKIHyW9n0i4= github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 h1:3xEhacR7pIJV8daurdBygptxhzTJeYFqJp1V6SDl+pE= github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359/go.mod h1:Wn+pZpM98JHSOYkPDtmdvlqmc0OzQGHWOsHB2d28WtQ= github.com/gcla/tcell v1.1.2-0.20200927150251-decc2045f510 h1:TlEZ0DHOvn0P79nHtkfemw7XFn2h8Lacd6AZpXrPU/o= diff --git a/ui/switchterm.go b/ui/switchterm.go new file mode 100644 index 0000000..c505cbe --- /dev/null +++ b/ui/switchterm.go @@ -0,0 +1,177 @@ +// Copyright 2019-2021 Graham Clark. All rights reserved. Use of this source +// code is governed by the MIT license that can be found in the LICENSE +// file. + +// Package ui contains user-interface functions and helpers for termshark. +package ui + +import ( + "fmt" + "os" + "time" + + "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/dialog" + "github.com/gcla/gowid/widgets/framed" + "github.com/gcla/gowid/widgets/holder" + "github.com/gcla/gowid/widgets/paragraph" + "github.com/gcla/termshark/v2" +) + +//====================================================================== + +// SuggestSwitchingTerm will open a dialog asking the user if they would like to try +// a more colorful TERM setting. +func SuggestSwitchingTerm(app gowid.IApp) { + var switchTerm *dialog.Widget + + Yes := dialog.Button{ + Msg: "Yes", + Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + termshark.ShouldSwitchTerminal = true + switchTerm.Close(app) + RequestQuit() + })), + } + No := dialog.Button{ + Msg: "No", + } + NoAsk := dialog.Button{ + Msg: "No, don't ask", + Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + termshark.SetConf("main.disable-term-helper", true) + switchTerm.Close(app) + })), + } + + term := os.Getenv("TERM") + term256 := term + "-256color" + + switchTerm = dialog.New( + framed.NewSpace(paragraph.New(fmt.Sprintf("Termshark is running with TERM=%s. Your terminal database contains %s. Would you like to switch for a more colorful experience? Termshark will need to restart.", term, term256))), + dialog.Options{ + Buttons: []dialog.Button{Yes, No, NoAsk}, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + BorderStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-button"), + Modal: true, + FocusOnWidget: false, + }, + ) + + switchTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) +} + +//====================================================================== + +// IsTerminalLegible will open up a dialog asking the user to confirm that their +// running termshark is legible, having upgraded the TERM variable to a 256-color +// version and restarted. +func IsTerminalLegible(app gowid.IApp) { + + var saveTerm *dialog.Widget + + YesSave := dialog.Button{ + Msg: "Yes", + Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + termshark.SetConf("main.term", os.Getenv("TERM")) + saveTerm.Close(app) + })), + } + NoSave := dialog.Button{ + Msg: "No", + } + + saveTerm = dialog.New( + framed.NewSpace(paragraph.New(fmt.Sprintf("Do you want to save TERM=%s in termshark's config to use as the default?", os.Getenv("TERM")))), + dialog.Options{ + Buttons: []dialog.Button{YesSave, NoSave}, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + BorderStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-button"), + Modal: true, + FocusOnWidget: false, + }, + ) + + tick := time.NewTicker(time.Duration(1) * time.Second) + stopC := make(chan struct{}) + + var legibleTerm *dialog.Widget + + No := dialog.Button{ + Msg: "No", + Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + close(stopC) + termshark.ShouldSwitchBack = true + legibleTerm.Close(app) + RequestQuit() + })), + } + Yes := dialog.Button{ + Msg: "Yes", + Action: gowid.MakeWidgetCallback("exec", gowid.WidgetChangedFunction(func(app gowid.IApp, w gowid.IWidget) { + close(stopC) + legibleTerm.Close(app) + saveTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) + })), + } + + secs := 10 + + tw := func(count int) *paragraph.Widget { + return paragraph.New(fmt.Sprintf("Is the terminal legible? If no selection is made, termshark will revert to its original TERM setting in %d seconds.", secs)) + } + + message := holder.New(tw(secs)) + + termshark.TrackedGo(func() { + Loop: + for { + select { + case <-tick.C: + secs-- + switch { + case secs >= 0: + app.Run(gowid.RunFunction(func(app gowid.IApp) { + message.SetSubWidget(tw(secs), app) + })) + case secs < 0: + tick.Stop() + close(stopC) + app.Run(gowid.RunFunction(func(app gowid.IApp) { + termshark.ShouldSwitchBack = true + legibleTerm.Close(app) + RequestQuit() + })) + } + case <-stopC: + break Loop + } + } + }, Goroutinewg) + + legibleTerm = dialog.New( + framed.NewSpace(message), + dialog.Options{ + Buttons: []dialog.Button{Yes, No}, + NoShadow: true, + BackgroundStyle: gowid.MakePaletteRef("dialog"), + BorderStyle: gowid.MakePaletteRef("dialog"), + ButtonStyle: gowid.MakePaletteRef("dialog-button"), + Modal: true, + FocusOnWidget: false, + StartIdx: 1, + }, + ) + + legibleTerm.Open(appView, gowid.RenderWithRatio{R: 0.5}, app) +} + +//====================================================================== +// Local Variables: +// mode: Go +// fill-column: 110 +// End: diff --git a/utils.go b/utils.go index 0bdfcab..71918ed 100644 --- a/utils.go +++ b/utils.go @@ -40,6 +40,7 @@ import ( "github.com/gcla/termshark/v2/system" "github.com/gcla/termshark/v2/widgets/resizable" "github.com/gdamore/tcell" + "github.com/gdamore/tcell/terminfo/dynamic" "github.com/mattn/go-isatty" "github.com/pkg/errors" "github.com/shibukawa/configdir" @@ -99,10 +100,13 @@ var InternalErr = InternalError{} //====================================================================== var ( - UserGuideURL string = "https://termshark.io/userguide" - FAQURL string = "https://termshark.io/faq" - BugURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=bug_report.md&title=" - FeatureURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=feature_request.md&title=" + UserGuideURL string = "https://termshark.io/userguide" + FAQURL string = "https://termshark.io/faq" + BugURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=bug_report.md&title=" + FeatureURL string = "https://github.com/gcla/termshark/issues/new?assignees=&labels=&template=feature_request.md&title=" + OriginalEnv []string + ShouldSwitchTerminal bool + ShouldSwitchBack bool ) //====================================================================== @@ -1322,6 +1326,11 @@ func FileSizeDifferentTo(filename string, cur int64) (int64, bool) { return newSize, diff } +func Does256ColorTermExist() error { + _, _, e := dynamic.LoadTerminfo(fmt.Sprintf("%s-256color", os.Getenv("TERM"))) + return e +} + //====================================================================== // Local Variables: // mode: Go