1
1
mirror of https://github.com/walles/moar.git synced 2024-09-19 16:08:02 +03:00
moar/moar.go
Johan Walles 574235bc03 Print log messages on panic()
Before this change, if moar...
1. Logged some messages
2. Panicked

... then only the panic() output would be printed and the log messages
would be lost.

With this change in place, both are printed.
2023-08-27 09:02:31 +02:00

454 lines
14 KiB
Go

package main
import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/styles"
log "github.com/sirupsen/logrus"
"golang.org/x/term"
"github.com/walles/moar/m"
"github.com/walles/moar/twin"
)
var versionString = "Should be set when building, please use build.sh to build"
func printUsage(output io.Writer, flagSet *flag.FlagSet, printCommandline bool) {
// This controls where PrintDefaults() prints, see below
flagSet.SetOutput(output)
// FIXME: Log if any printouts fail?
moarEnv := os.Getenv("MOAR")
if printCommandline {
_, _ = fmt.Fprintln(output, "Commandline: moar", strings.Join(os.Args[1:], " "))
_, _ = fmt.Fprintf(output, "Environment: MOAR=\"%v\"\n", moarEnv)
_, _ = fmt.Fprintln(output)
}
_, _ = fmt.Fprintln(output, "Usage:")
_, _ = fmt.Fprintln(output, " moar [options] <file>")
_, _ = fmt.Fprintln(output, " ... | moar")
_, _ = fmt.Fprintln(output, " moar < file")
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintln(output, "Shows file contents. Compressed files will be transparently decompressed.")
_, _ = fmt.Fprintln(output, "Input is expected to be (possibly compressed) UTF-8 encoded text. Invalid /")
_, _ = fmt.Fprintln(output, "non-printable characters are by default rendered as '?'.")
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintln(output, "More information + source code:")
_, _ = fmt.Fprintln(output, " <https://github.com/walles/moar#readme>")
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintln(output, "Environment:")
if len(moarEnv) == 0 {
_, _ = fmt.Fprintln(output, " Additional options are read from the MOAR environment variable if set.")
_, _ = fmt.Fprintln(output, " But currently, the MOAR environment variable is not set.")
} else {
_, _ = fmt.Fprintln(output, " Additional options are read from the MOAR environment variable.")
_, _ = fmt.Fprintf(output, " Current setting: MOAR=\"%s\"\n", moarEnv)
}
absMoarPath, err := absLookPath(os.Args[0])
if err == nil {
absPagerValue, err := absLookPath(os.Getenv("PAGER"))
if err != nil {
absPagerValue = ""
}
if absPagerValue != absMoarPath {
// We're not the default pager
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintln(output, "Making moar your default pager:")
_, _ = fmt.Fprintln(output, " Put the following line in your ~/.bashrc, ~/.bash_profile or ~/.zshrc")
_, _ = fmt.Fprintln(output, " and moar will be used as the default pager in all new terminal windows:")
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintf(output, " export PAGER=%s\n", getMoarPath())
}
} else {
log.Warn("Unable to find moar binary ", err)
}
_, _ = fmt.Fprintln(output)
_, _ = fmt.Fprintln(output, "Options:")
flagSet.PrintDefaults()
}
// "moar" if we're in the $PATH, otherwise an absolute path
func getMoarPath() string {
moarPath := os.Args[0]
if filepath.IsAbs(moarPath) {
return moarPath
}
if strings.Contains(moarPath, string(os.PathSeparator)) {
// Relative path
moarPath, err := filepath.Abs(moarPath)
if err != nil {
panic(err)
}
return moarPath
}
// Neither absolute nor relative, try PATH
_, err := exec.LookPath(moarPath)
if err != nil {
panic("Unable to find in $PATH: " + moarPath)
}
return moarPath
}
func absLookPath(path string) (string, error) {
lookedPath, err := exec.LookPath(path)
if err != nil {
return "", err
}
absLookedPath, err := filepath.Abs(lookedPath)
if err != nil {
return "", err
}
return absLookedPath, err
}
// printProblemsHeader prints bug reporting information to stderr
func printProblemsHeader() {
fmt.Fprintln(os.Stderr, "Please post the following report at <https://github.com/walles/moar/issues>,")
fmt.Fprintln(os.Stderr, "or e-mail it to johan.walles@gmail.com.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Version:", versionString)
fmt.Fprintln(os.Stderr, "LANG :", os.Getenv("LANG"))
fmt.Fprintln(os.Stderr, "TERM :", os.Getenv("TERM"))
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "GOOS :", runtime.GOOS)
fmt.Fprintln(os.Stderr, "GOARCH :", runtime.GOARCH)
fmt.Fprintln(os.Stderr, "Compiler:", runtime.Compiler)
fmt.Fprintln(os.Stderr, "NumCPU :", runtime.NumCPU())
}
func parseStyleOption(styleOption string) (chroma.Style, error) {
style, ok := styles.Registry[styleOption]
if !ok {
return *styles.Fallback, fmt.Errorf(
"Pick a style from here: https://xyproto.github.io/splash/docs/longer/all.html\n")
}
return *style, nil
}
func parseColorsOption(colorsOption string) (chroma.Formatter, error) {
if strings.ToLower(colorsOption) == "auto" {
colorsOption = "16M"
if strings.Contains(os.Getenv("TERM"), "256") {
// Covers "xterm-256color" as used by the macOS Terminal
colorsOption = "256"
}
}
switch strings.ToUpper(colorsOption) {
case "8":
return formatters.TTY8, nil
case "16":
return formatters.TTY16, nil
case "256":
return formatters.TTY256, nil
case "16M":
return formatters.TTY16m, nil
}
return nil, fmt.Errorf("Valid counts are 8, 16, 256, 16M or auto.")
}
func parseStatusBarStyle(styleOption string) (m.StatusBarStyle, error) {
if styleOption == "inverse" {
return m.STATUSBAR_STYLE_INVERSE, nil
}
if styleOption == "plain" {
return m.STATUSBAR_STYLE_PLAIN, nil
}
if styleOption == "bold" {
return m.STATUSBAR_STYLE_BOLD, nil
}
return 0, fmt.Errorf("good ones are inverse, plain and bold")
}
func parseUnprintableStyle(styleOption string) (m.UnprintableStyle, error) {
if styleOption == "highlight" {
return m.UNPRINTABLE_STYLE_HIGHLIGHT, nil
}
if styleOption == "whitespace" {
return m.UNPRINTABLE_STYLE_WHITESPACE, nil
}
return 0, fmt.Errorf("Good ones are highlight or whitespace")
}
func parseScrollHint(scrollHint string) (twin.Cell, error) {
scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b")
hintParser := m.NewLine(scrollHint)
parsedTokens := hintParser.HighlightedTokens(nil).Cells
if len(parsedTokens) == 1 {
return parsedTokens[0], nil
}
return twin.Cell{}, fmt.Errorf("Expected exactly one (optionally highlighted) character. For example: 'ESC[2m…'")
}
func parseShiftAmount(shiftAmount string) (uint, error) {
value, err := strconv.ParseUint(shiftAmount, 10, 32)
if err != nil {
return 0, err
}
if value < 1 {
return 0, fmt.Errorf("Shift amount must be at least 1, was %d", value)
}
// Let's add an upper bound as well if / when requested
return uint(value), nil
}
func main() {
// FIXME: If we get a CTRL-C, get terminal back into a useful state before terminating
var loglines strings.Builder
log.SetOutput(&loglines)
defer func() {
err := recover()
if len(loglines.String()) == 0 && err == nil {
// No problems
return
}
printProblemsHeader()
if len(loglines.String()) > 0 {
fmt.Fprintln(os.Stderr)
// Consider not printing duplicate log messages more than once
fmt.Fprintf(os.Stderr, "%s", loglines.String())
}
if err != nil {
fmt.Fprintln(os.Stderr)
panic(err)
}
os.Exit(1)
}()
flagSet := flag.NewFlagSet("",
flag.ContinueOnError, // We want to do our own error handling
)
flagSet.SetOutput(io.Discard) // We want to do our own printing
printVersion := flagSet.Bool("version", false, "Prints the moar version number")
debug := flagSet.Bool("debug", false, "Print debug logs after exiting")
trace := flagSet.Bool("trace", false, "Print trace logs after exiting")
wrap := flagSet.Bool("wrap", false, "Wrap long lines")
follow := flagSet.Bool("follow", false, "Follow piped input just like \"tail -f\"")
style := flagSetFunc(flagSet,
"style", *styles.Registry["native"],
"Highlighting style from https://xyproto.github.io/splash/docs/longer/all.html", parseStyleOption)
formatter := flagSetFunc(flagSet,
"colors", formatters.TTY256, "Highlighting palette size: 8, 16, 256, 16M, auto", parseColorsOption)
noLineNumbers := flagSet.Bool("no-linenumbers", false, "Hide line numbers on startup, press left arrow key to show")
noStatusBar := flagSet.Bool("no-statusbar", false, "Hide the status bar, toggle with '='")
quitIfOneScreen := flagSet.Bool("quit-if-one-screen", false, "Don't page if contents fits on one screen")
noClearOnExit := flagSet.Bool("no-clear-on-exit", false, "Retain screen contents when exiting moar")
statusBarStyle := flagSetFunc(flagSet, "statusbar", m.STATUSBAR_STYLE_INVERSE,
"Status bar style: inverse, plain or bold", parseStatusBarStyle)
unprintableStyle := flagSetFunc(flagSet, "render-unprintable", m.UNPRINTABLE_STYLE_HIGHLIGHT,
"How unprintable characters are rendered: highlight or whitespace", parseUnprintableStyle)
scrollLeftHint := flagSetFunc(flagSet, "scroll-left-hint",
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
"Shown when view can scroll left. One character with optional ANSI highlighting.", parseScrollHint)
scrollRightHint := flagSetFunc(flagSet, "scroll-right-hint",
twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
"Shown when view can scroll right. One character with optional ANSI highlighting.", parseScrollHint)
shift := flagSetFunc(flagSet, "shift", 16, "Horizontal scroll amount >=1, defaults to 16", parseShiftAmount)
// Combine flags from environment and from command line
flags := os.Args[1:]
moarEnv := strings.Trim(os.Getenv("MOAR"), " ")
if len(moarEnv) > 0 {
// FIXME: It would be nice if we could debug log that we're doing this,
// but logging is not yet set up and depends on command line parameters.
flags = append(strings.Fields(moarEnv), flags...)
}
err := flagSet.Parse(flags)
if err != nil {
if err == flag.ErrHelp {
printUsage(os.Stdout, flagSet, false)
return
}
boldErrorMessage := "\x1b[1m" + err.Error() + "\x1b[m"
fmt.Fprintln(os.Stderr, "ERROR:", boldErrorMessage)
fmt.Fprintln(os.Stderr)
printUsage(os.Stderr, flagSet, true)
os.Exit(1)
}
if *printVersion {
fmt.Println(versionString)
os.Exit(0)
}
log.SetLevel(log.InfoLevel)
if *trace {
log.SetLevel(log.TraceLevel)
} else if *debug {
log.SetLevel(log.DebugLevel)
}
log.SetFormatter(&log.TextFormatter{
TimestampFormat: time.StampMicro,
})
if len(flagSet.Args()) > 1 {
fmt.Fprintln(os.Stderr, "ERROR: Expected exactly one filename, or data piped from stdin, got:", flagSet.Args())
fmt.Fprintln(os.Stderr)
printUsage(os.Stderr, flagSet, true)
os.Exit(1)
}
stdinIsRedirected := !term.IsTerminal(int(os.Stdin.Fd()))
stdoutIsRedirected := !term.IsTerminal(int(os.Stdout.Fd()))
var inputFilename *string
if len(flagSet.Args()) == 1 {
word := flagSet.Arg(0)
inputFilename = &word
}
if inputFilename == nil && !stdinIsRedirected {
fmt.Fprintln(os.Stderr, "ERROR: Filename or input pipe required")
fmt.Fprintln(os.Stderr)
printUsage(os.Stderr, flagSet, true)
os.Exit(1)
}
if inputFilename != nil && stdoutIsRedirected {
// Pump file to stdout.
//
// If we get both redirected stdin and an input filename, we must prefer
// to copy the file, because that's how less works.
inputFile, err := os.Open(*inputFilename)
if err != nil {
fmt.Fprintln(os.Stderr, "ERROR: Failed to open", inputFile, ": ")
os.Exit(1)
}
_, err = io.Copy(os.Stdout, inputFile)
if err != nil {
log.Fatal("Failed to copy ", inputFilename, " to stdout: ", err)
}
os.Exit(0)
}
if stdinIsRedirected && stdoutIsRedirected {
// Must be done after trying to pump the input filename to stdout to be
// compatible with less, see above.
_, err := io.Copy(os.Stdout, os.Stdin)
if err != nil {
log.Fatal("Failed to copy stdin to stdout: ", err)
}
os.Exit(0)
}
// INVARIANT: At this point, stdout is a terminal and we should proceed with
// paging.
stdoutIsTerminal := !stdoutIsRedirected
if !stdoutIsTerminal {
panic("Invariant broken: stdout is not a terminal")
}
var reader *m.Reader
if stdinIsRedirected {
// Display input pipe contents
reader = m.NewReaderFromStream("", os.Stdin)
} else {
// Display the input file contents
reader, err = m.NewReaderFromFilename(*inputFilename, *style, *formatter)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(1)
}
}
pager := m.NewPager(reader)
pager.WrapLongLines = *wrap
pager.Following = *follow
pager.ShowLineNumbers = !*noLineNumbers
pager.ShowStatusBar = !*noStatusBar
pager.DeInit = !*noClearOnExit
pager.QuitIfOneScreen = *quitIfOneScreen
pager.StatusBarStyle = *statusBarStyle
pager.UnprintableStyle = *unprintableStyle
pager.ScrollLeftHint = *scrollLeftHint
pager.ScrollRightHint = *scrollRightHint
pager.SideScrollAmount = int(*shift)
startPaging(pager)
}
// Define a generic flag with specified name, default value, and usage string.
// The return value is the address of a variable that stores the parsed value of
// the flag.
func flagSetFunc[T any](flagSet *flag.FlagSet, name string, defaultValue T, usage string, parser func(valueString string) (T, error)) *T {
parsed := defaultValue
flagSet.Func(name, usage, func(valueString string) error {
parseResult, err := parser(valueString)
if err != nil {
return err
}
parsed = parseResult
return nil
})
return &parsed
}
func startPaging(pager *m.Pager) {
var screen twin.Screen
defer func() {
// Restore screen...
if screen != nil {
screen.Close()
}
// ... before printing panic() output, otherwise the output will have
// broken linefeeds and be hard to follow.
if err := recover(); err != nil {
panic(err)
}
if !pager.DeInit {
err := pager.ReprintAfterExit()
if err != nil {
log.Error("Failed reprinting pager view after exit", err)
}
}
}()
screen, e := twin.NewScreen()
if e != nil {
panic(e)
}
pager.StartPaging(screen)
}