kitty/tools/cli/infrastructure.go
2022-11-14 15:41:43 +05:30

363 lines
8.7 KiB
Go

package cli
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"unicode"
"unsafe"
"github.com/fatih/color"
runewidth "github.com/mattn/go-runewidth"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"kitty"
"kitty/tools/utils"
)
var RootCmd *cobra.Command
type Winsize struct {
Rows uint16
Cols uint16
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()
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) {
flags.String(name, choices[0], usage)
cmd.Annotations["choices-"+name] = strings.Join(choices, "\000")
}
func Choices(cmd *cobra.Command, name string, usage string, choices ...string) {
add_choices(cmd, cmd.Flags(), choices, name, usage)
}
func PersistentChoices(cmd *cobra.Command, name string, usage string, choices ...string) {
add_choices(cmd, cmd.PersistentFlags(), choices, name, usage)
}
func key_in_slice(vals []string, key string) bool {
for _, q := range vals {
if q == key {
return true
}
}
return false
}
func ValidateChoices(cmd *cobra.Command, args []string) error {
for key, val := range cmd.Annotations {
if strings.HasPrefix(key, "choices-") {
allowed := strings.Split(val, "\000")
name := key[len("choices-"):]
if cval, err := cmd.Flags().GetString(name); err == nil && !key_in_slice(allowed, cval) {
return fmt.Errorf("%s: Invalid value: %s. Allowed values are: %s", color.YellowString("--"+name), color.RedString(cval), strings.Join(allowed, ", "))
}
}
}
return nil
}
var title_fmt = color.New(color.FgBlue, color.Bold).SprintFunc()
var exe_fmt = color.New(color.FgYellow, color.Bold).SprintFunc()
var opt_fmt = color.New(color.FgGreen).SprintFunc()
var italic_fmt = color.New(color.Italic).SprintFunc()
var bold_fmt = color.New(color.Bold).SprintFunc()
var code_fmt = color.New(color.FgCyan).SprintFunc()
var cyan_fmt = color.New(color.FgCyan).SprintFunc()
var yellow_fmt = color.New(color.FgYellow).SprintFunc()
var blue_fmt = color.New(color.FgBlue).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) {
x := len(indent)
fmt.Print(indent)
in_sgr := false
var current_word strings.Builder
print_word := func(r rune) {
w := runewidth.StringWidth(current_word.String())
if x+w > screen_width {
fmt.Println()
fmt.Print(indent)
x = len(indent)
s := strings.TrimSpace(current_word.String())
current_word.Reset()
current_word.WriteString(s)
}
fmt.Print(current_word.String())
current_word.Reset()
if r > 0 {
current_word.WriteRune(r)
}
x += w
}
for _, r := range text {
if in_sgr {
if r == 'm' {
in_sgr = false
}
fmt.Print(string(r))
continue
}
if r == 0x1b {
in_sgr = true
if current_word.Len() != 0 {
print_word(0)
}
fmt.Print(string(r))
continue
}
if current_word.Len() != 0 && r != 0xa0 && unicode.IsSpace(r) {
print_word(r)
} else {
current_word.WriteRune(r)
}
}
if current_word.Len() != 0 {
print_word(0)
}
if len(text) > 0 {
fmt.Println()
}
}
func ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
result := ""
lastIndex := 0
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
groups := []string{}
for i := 0; i < len(v); i += 2 {
if v[i] == -1 || v[i+1] == -1 {
groups = append(groups, "")
} else {
groups = append(groups, str[v[i]:v[i+1]])
}
}
result += str[lastIndex:v[0]] + repl(groups)
lastIndex = v[1]
}
return result + str[lastIndex:]
}
func website_url(doc string) string {
if doc != "" {
doc = strings.TrimSuffix(doc, "/")
if doc != "" {
doc += "/"
}
}
return kitty.WebsiteBaseUrl + doc
}
var prettify_pat = regexp.MustCompile(":([a-z]+):`([^`]+)`")
var ref_pat = regexp.MustCompile(`\s*<\S+?>`)
func is_atty() bool {
return italic_fmt("") != ""
}
func hyperlink_for_path(path string, text string) string {
if !is_atty() {
return text
}
path = strings.ReplaceAll(utils.Abspath(path), string(os.PathSeparator), "/")
fi, err := os.Stat(path)
if err == nil && fi.IsDir() {
path = strings.TrimSuffix(path, "/") + "/"
}
host, err := os.Hostname()
if err != nil {
host = ""
}
return "\x1b]8;;file://" + host + path + "\x1b\\" + text + "\x1b]8;;\x1b\\"
}
func prettify(text string) string {
return ReplaceAllStringSubmatchFunc(prettify_pat, text, func(groups []string) string {
val := groups[2]
switch groups[1] {
case "file":
if val == "kitty.conf" && is_atty() {
path := filepath.Join(utils.ConfigDir(), val)
val = hyperlink_for_path(path, val)
}
return italic_fmt(val)
case "env", "envvar":
return italic_fmt(val)
case "doc":
return website_url(val)
case "ref":
return ref_pat.ReplaceAllString(val, ``)
case "code":
return code_fmt(val)
case "option":
idx := strings.LastIndex(val, "--")
if idx < 0 {
idx = strings.Index(val, "-")
}
if idx > -1 {
val = val[idx:]
}
return bold_fmt(val)
case "yellow":
return yellow_fmt(val)
case "blue":
return blue_fmt(val)
case "green":
return green_fmt(val)
case "cyan":
return cyan_fmt(val)
case "emph":
return italic_fmt(val)
default:
return val
}
})
}
func print_with_indent(text string, indent string, screen_width int) {
for _, line := range strings.Split(prettify(text), "\n") {
print_line_with_indent(line, indent, screen_width)
}
}
func show_usage(cmd *cobra.Command) error {
ws, tty_size_err := GetTTYSize()
screen_width := 80
if tty_size_err == nil && ws.Cols < 80 {
screen_width = int(ws.Cols)
}
use := cmd.Use
idx := strings.Index(use, " ")
if idx > -1 {
use = use[idx+1:]
}
var parent_names []string
cmd.VisitParents(func(p *cobra.Command) {
parent_names = append(parent_names, p.Name())
})
parent_names = append(parent_names, cmd.Name())
fmt.Println(title_fmt("Usage")+":", exe_fmt(strings.Join(parent_names, " ")), use)
fmt.Println()
if len(cmd.Long) > 0 {
print_with_indent(cmd.Long, "", screen_width)
} else if len(cmd.Short) > 0 {
print_with_indent(cmd.Short, "", screen_width)
}
if cmd.HasAvailableSubCommands() {
fmt.Println()
fmt.Println(title_fmt("Commands") + ":")
for _, child := range cmd.Commands() {
fmt.Println(" ", opt_fmt(child.Name()))
print_with_indent(child.Short, " ", screen_width)
}
fmt.Println()
print_with_indent("Get help for an individual command by running:", "", screen_width)
fmt.Println(" ", cmd.Name(), italic_fmt("command"), "-h")
}
if cmd.HasAvailableFlags() {
options_title := cmd.Annotations["options_title"]
if len(options_title) == 0 {
options_title = "Options"
}
fmt.Println()
fmt.Println(title_fmt(options_title) + ":")
flag_set := cmd.LocalFlags()
flag_set.VisitAll(func(flag *pflag.Flag) {
fmt.Print(opt_fmt(" --" + flag.Name))
if flag.Shorthand != "" {
fmt.Print(", ", opt_fmt("-"+flag.Shorthand))
}
defval := ""
switch flag.Value.Type() {
default:
if flag.DefValue != "" {
defval = fmt.Sprintf("[=%s]", italic_fmt(flag.DefValue))
}
case "bool":
case "count":
}
if defval != "" {
fmt.Print(" ", defval)
}
fmt.Println()
msg := flag.Usage
switch flag.Name {
case "help":
msg = "Print this help message"
case "version":
msg = "Print the version of " + RootCmd.Name() + ": " + italic_fmt(RootCmd.Version)
}
print_with_indent(msg, " ", screen_width)
if cmd.Annotations["choices-"+flag.Name] != "" {
fmt.Println(" Choices:", strings.Join(strings.Split(cmd.Annotations["choices-"+flag.Name], "\000"), ", "))
}
fmt.Println()
})
}
if cmd.HasParent() {
cmd.VisitParents(func(cmd *cobra.Command) {
if !cmd.HasParent() {
print_created_by(cmd)
}
})
} else {
print_created_by(cmd)
}
return nil
}
func CreateCommand(cmd *cobra.Command) *cobra.Command {
cmd.Annotations = make(map[string]string)
if cmd.Run == nil {
cmd.Run = SubCommandRequired
}
return cmd
}
func SubCommandRequired(cmd *cobra.Command, args []string) {
cmd.Usage()
fmt.Fprintln(os.Stderr, color.RedString("\nNo command specified for "+cmd.Name()))
os.Exit(1)
}
func Init(root *cobra.Command) {
RootCmd = root
root.Version = kitty.VersionString
root.PersistentPreRunE = ValidateChoices
root.SetUsageFunc(show_usage)
root.SetHelpTemplate("{{.UsageString}}")
}