Use a pager for displaying usage, matches kitty behavior

Also dont tell less to squeeze lines
This commit is contained in:
Kovid Goyal
2022-08-15 13:45:05 +05:30
parent 46840df1ad
commit c04b002d4e
2 changed files with 66 additions and 78 deletions

View File

@@ -474,7 +474,7 @@ class PrintHelpForSeq:
text = '\n'.join(blocks) + '\n\n' + version() text = '\n'.join(blocks) + '\n\n' + version()
if print_help_for_seq.allow_pager and sys.stdout.isatty(): if print_help_for_seq.allow_pager and sys.stdout.isatty():
import subprocess import subprocess
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE, preexec_fn=clear_handled_signals) p = subprocess.Popen(['less', '-iRXF'], stdin=subprocess.PIPE, preexec_fn=clear_handled_signals)
try: try:
p.communicate(text.encode('utf-8')) p.communicate(text.encode('utf-8'))
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@@ -2,18 +2,20 @@ package cli
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"syscall"
"unicode" "unicode"
"unsafe"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/mattn/go-isatty"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/sys/unix"
"kitty" "kitty"
"kitty/tools/utils" "kitty/tools/utils"
@@ -21,27 +23,11 @@ import (
var RootCmd *cobra.Command var RootCmd *cobra.Command
type Winsize struct { func GetTTYSize() (*unix.Winsize, error) {
Rows uint16 if (stdout_is_terminal) {
Cols uint16 return unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
Xpixels uint16
Ypixels uint16
}
func GetTTYSize() (Winsize, error) {
var ws Winsize
f, err := os.OpenFile("/dev/tty", os.O_RDONLY, 0755)
if err != nil {
return ws, err
} }
fd := f.Fd() return nil, fmt.Errorf("STDOUT is not a TTY")
retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
f.Close()
if int(retCode) == -1 {
return ws, errno
}
return ws, nil
} }
func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) { func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) {
@@ -79,6 +65,7 @@ func ValidateChoices(cmd *cobra.Command, args []string) error {
return nil return nil
} }
var stdout_is_terminal = false
var title_fmt = color.New(color.FgBlue, color.Bold).SprintFunc() var title_fmt = color.New(color.FgBlue, color.Bold).SprintFunc()
var exe_fmt = color.New(color.FgYellow, color.Bold).SprintFunc() var exe_fmt = color.New(color.FgYellow, color.Bold).SprintFunc()
var opt_fmt = color.New(color.FgGreen).SprintFunc() var opt_fmt = color.New(color.FgGreen).SprintFunc()
@@ -90,27 +77,24 @@ var yellow_fmt = color.New(color.FgYellow).SprintFunc()
var blue_fmt = color.New(color.FgBlue).SprintFunc() var blue_fmt = color.New(color.FgBlue).SprintFunc()
var green_fmt = color.New(color.FgGreen).SprintFunc() var green_fmt = color.New(color.FgGreen).SprintFunc()
func print_created_by(root *cobra.Command) {
fmt.Println(italic_fmt(root.Name()), opt_fmt(root.Version), "created by", title_fmt("Kovid Goyal"))
}
func print_line_with_indent(text string, indent string, screen_width int) { func format_line_with_indent(output io.Writer, text string, indent string, screen_width int) {
x := len(indent) x := len(indent)
fmt.Print(indent) fmt.Fprint(output, indent)
in_sgr := false in_escape := 0
var current_word strings.Builder var current_word strings.Builder
print_word := func(r rune) { print_word := func(r rune) {
w := runewidth.StringWidth(current_word.String()) w := runewidth.StringWidth(current_word.String())
if x+w > screen_width { if x+w > screen_width {
fmt.Println() fmt.Fprintln(output)
fmt.Print(indent) fmt.Fprint(output, indent)
x = len(indent) x = len(indent)
s := strings.TrimSpace(current_word.String()) s := strings.TrimSpace(current_word.String())
current_word.Reset() current_word.Reset()
current_word.WriteString(s) current_word.WriteString(s)
} }
fmt.Print(current_word.String()) fmt.Fprint(output, current_word.String())
current_word.Reset() current_word.Reset()
if r > 0 { if r > 0 {
current_word.WriteRune(r) current_word.WriteRune(r)
@@ -118,20 +102,24 @@ func print_line_with_indent(text string, indent string, screen_width int) {
x += w x += w
} }
for _, r := range text { for i, r := range text {
if in_sgr { if in_escape > 0 {
if r == 'm' { if (in_escape == 1 && (r == ']' || r == '[')) {
in_sgr = false in_escape = 2
if (r == ']') { in_escape = 3 }
} }
fmt.Print(string(r)) if (in_escape == 2 && r == 'm') || (in_escape == 3 && r == '\\' && text[i-1] == 0x1b) {
in_escape = 0
}
fmt.Fprint(output, string(r))
continue continue
} }
if r == 0x1b { if r == 0x1b {
in_sgr = true in_escape = 1
if current_word.Len() != 0 { if current_word.Len() != 0 {
print_word(0) print_word(0)
} }
fmt.Print(string(r)) fmt.Fprint(output, string(r))
continue continue
} }
if current_word.Len() != 0 && r != 0xa0 && unicode.IsSpace(r) { if current_word.Len() != 0 && r != 0xa0 && unicode.IsSpace(r) {
@@ -144,7 +132,7 @@ func print_line_with_indent(text string, indent string, screen_width int) {
print_word(0) print_word(0)
} }
if len(text) > 0 { if len(text) > 0 {
fmt.Println() fmt.Fprintln(output)
} }
} }
@@ -182,12 +170,8 @@ func website_url(doc string) string {
var prettify_pat = regexp.MustCompile(":([a-z]+):`([^`]+)`") var prettify_pat = regexp.MustCompile(":([a-z]+):`([^`]+)`")
var ref_pat = regexp.MustCompile(`\s*<\S+?>`) var ref_pat = regexp.MustCompile(`\s*<\S+?>`)
func is_atty() bool {
return italic_fmt("") != ""
}
func hyperlink_for_path(path string, text string) string { func hyperlink_for_path(path string, text string) string {
if !is_atty() { if !stdout_is_terminal {
return text return text
} }
path = strings.ReplaceAll(utils.Abspath(path), string(os.PathSeparator), "/") path = strings.ReplaceAll(utils.Abspath(path), string(os.PathSeparator), "/")
@@ -207,7 +191,7 @@ func prettify(text string) string {
val := groups[2] val := groups[2]
switch groups[1] { switch groups[1] {
case "file": case "file":
if val == "kitty.conf" && is_atty() { if val == "kitty.conf" && stdout_is_terminal {
path := filepath.Join(utils.ConfigDir(), val) path := filepath.Join(utils.ConfigDir(), val)
val = hyperlink_for_path(path, val) val = hyperlink_for_path(path, val)
} }
@@ -246,17 +230,18 @@ func prettify(text string) string {
}) })
} }
func print_with_indent(text string, indent string, screen_width int) { func format_with_indent(output io.Writer, text string, indent string, screen_width int) {
for _, line := range strings.Split(prettify(text), "\n") { for _, line := range strings.Split(prettify(text), "\n") {
print_line_with_indent(line, indent, screen_width) format_line_with_indent(output, line, indent, screen_width)
} }
} }
func show_usage(cmd *cobra.Command) error { func show_usage(cmd *cobra.Command) error {
ws, tty_size_err := GetTTYSize() ws, tty_size_err := GetTTYSize()
var output strings.Builder
screen_width := 80 screen_width := 80
if tty_size_err == nil && ws.Cols < 80 { if tty_size_err == nil && ws.Col < 80 {
screen_width = int(ws.Cols) screen_width = int(ws.Col)
} }
use := cmd.Use use := cmd.Use
idx := strings.Index(use, " ") idx := strings.Index(use, " ")
@@ -268,36 +253,36 @@ func show_usage(cmd *cobra.Command) error {
parent_names = append(parent_names, p.Name()) parent_names = append(parent_names, p.Name())
}) })
parent_names = append(parent_names, cmd.Name()) parent_names = append(parent_names, cmd.Name())
fmt.Println(title_fmt("Usage")+":", exe_fmt(strings.Join(parent_names, " ")), use) fmt.Fprintln(&output, title_fmt("Usage")+":", exe_fmt(strings.Join(parent_names, " ")), use)
fmt.Println() fmt.Fprintln(&output)
if len(cmd.Long) > 0 { if len(cmd.Long) > 0 {
print_with_indent(cmd.Long, "", screen_width) format_with_indent(&output, cmd.Long, "", screen_width)
} else if len(cmd.Short) > 0 { } else if len(cmd.Short) > 0 {
print_with_indent(cmd.Short, "", screen_width) format_with_indent(&output, cmd.Short, "", screen_width)
} }
if cmd.HasAvailableSubCommands() { if cmd.HasAvailableSubCommands() {
fmt.Println() fmt.Fprintln(&output)
fmt.Println(title_fmt("Commands") + ":") fmt.Fprintln(&output, title_fmt("Commands")+":")
for _, child := range cmd.Commands() { for _, child := range cmd.Commands() {
fmt.Println(" ", opt_fmt(child.Name())) fmt.Fprintln(&output, " ", opt_fmt(child.Name()))
print_with_indent(child.Short, " ", screen_width) format_with_indent(&output, child.Short, " ", screen_width)
} }
fmt.Println() fmt.Fprintln(&output)
print_with_indent("Get help for an individual command by running:", "", screen_width) format_with_indent(&output, "Get help for an individual command by running:", "", screen_width)
fmt.Println(" ", cmd.Name(), italic_fmt("command"), "-h") fmt.Fprintln(&output, " ", cmd.Name(), italic_fmt("command"), "-h")
} }
if cmd.HasAvailableFlags() { if cmd.HasAvailableFlags() {
options_title := cmd.Annotations["options_title"] options_title := cmd.Annotations["options_title"]
if len(options_title) == 0 { if len(options_title) == 0 {
options_title = "Options" options_title = "Options"
} }
fmt.Println() fmt.Fprintln(&output)
fmt.Println(title_fmt(options_title) + ":") fmt.Fprintln(&output, title_fmt(options_title)+":")
flag_set := cmd.LocalFlags() flag_set := cmd.LocalFlags()
flag_set.VisitAll(func(flag *pflag.Flag) { flag_set.VisitAll(func(flag *pflag.Flag) {
fmt.Print(opt_fmt(" --" + flag.Name)) fmt.Fprint(&output, opt_fmt(" --"+flag.Name))
if flag.Shorthand != "" { if flag.Shorthand != "" {
fmt.Print(", ", opt_fmt("-"+flag.Shorthand)) fmt.Fprint(&output, ", ", opt_fmt("-"+flag.Shorthand))
} }
defval := "" defval := ""
switch flag.Value.Type() { switch flag.Value.Type() {
@@ -309,9 +294,9 @@ func show_usage(cmd *cobra.Command) error {
case "count": case "count":
} }
if defval != "" { if defval != "" {
fmt.Print(" ", defval) fmt.Fprint(&output, " ", defval)
} }
fmt.Println() fmt.Fprintln(&output)
msg := flag.Usage msg := flag.Usage
switch flag.Name { switch flag.Name {
case "help": case "help":
@@ -319,23 +304,24 @@ func show_usage(cmd *cobra.Command) error {
case "version": case "version":
msg = "Print the version of " + RootCmd.Name() + ": " + italic_fmt(RootCmd.Version) msg = "Print the version of " + RootCmd.Name() + ": " + italic_fmt(RootCmd.Version)
} }
print_with_indent(msg, " ", screen_width) format_with_indent(&output, msg, " ", screen_width)
if cmd.Annotations["choices-"+flag.Name] != "" { if cmd.Annotations["choices-"+flag.Name] != "" {
fmt.Println(" Choices:", strings.Join(strings.Split(cmd.Annotations["choices-"+flag.Name], "\000"), ", ")) fmt.Fprintln(&output, " Choices:", strings.Join(strings.Split(cmd.Annotations["choices-"+flag.Name], "\000"), ", "))
} }
fmt.Println() fmt.Fprintln(&output)
}) })
} }
if cmd.HasParent() { fmt.Fprintln(&output, italic_fmt(RootCmd.Name()), opt_fmt(RootCmd.Version), "created by", title_fmt("Kovid Goyal"))
cmd.VisitParents(func(cmd *cobra.Command) { output_text := output.String()
if !cmd.HasParent() { if stdout_is_terminal && cmd.Annotations["allow-pager"] != "no" {
print_created_by(cmd) pager := exec.Command("less", "-iRXF");
} pager.Stdin = strings.NewReader(output_text)
}) pager.Stdout = os.Stdout
pager.Stderr = os.Stderr
pager.Run()
} else { } else {
print_created_by(cmd) print(output_text)
} }
return nil return nil
} }
@@ -348,12 +334,14 @@ func CreateCommand(cmd *cobra.Command) *cobra.Command {
} }
func SubCommandRequired(cmd *cobra.Command, args []string) { func SubCommandRequired(cmd *cobra.Command, args []string) {
cmd.Annotations["allow-pager"] = "no"
cmd.Usage() cmd.Usage()
fmt.Fprintln(os.Stderr, color.RedString("\nNo command specified for "+cmd.Name())) fmt.Fprintln(os.Stderr, color.RedString("\nNo command specified for "+cmd.Name()))
os.Exit(1) os.Exit(1)
} }
func Init(root *cobra.Command) { func Init(root *cobra.Command) {
stdout_is_terminal = isatty.IsTerminal(os.Stdout.Fd())
RootCmd = root RootCmd = root
root.Version = kitty.VersionString root.Version = kitty.VersionString
root.PersistentPreRunE = ValidateChoices root.PersistentPreRunE = ValidateChoices