This commit is contained in:
1 parent
bbc62e4dcf
commit
ce34fce16b
7 files changed
+693
-12
No files matched your search
@@ -145,14 +145,38 @@ ktui [flags]
|
|||||||
ktui status [flags]
|
ktui status [flags]
|
||||||
ktui export <markdown|csv|json> [flags]
|
ktui export <markdown|csv|json> [flags]
|
||||||
ktui config <init|path|show|set|help>
|
ktui config <init|path|show|set|help>
|
||||||
ktui profile <list|current|use|add|rename|remove>
|
ktui keys
|
||||||
ktui update <check|install>
|
ktui profile <list|current|use|add|rename|remove|help>
|
||||||
|
ktui update <check|install|help>
|
||||||
|
ktui completion <bash|zsh|fish|powershell>
|
||||||
ktui version
|
ktui version
|
||||||
ktui help [status|config|profile|keys|update|export]
|
ktui help [command]
|
||||||
```
|
```
|
||||||
|
|
||||||
连接到多个 Komari 站点时使用 `ktui profile ...` 管理,并可用 `--profile name` 临时选择。TUI 专属显示参数使用 `--mode sheet|line`。`--realtime-window 1m|5m|10m` 控制 realtime 图表横轴时间范围。一次性拉取摘要使用 `ktui status`,更新检查和安装分别使用 `ktui update check`、`ktui update install`。
|
连接到多个 Komari 站点时使用 `ktui profile ...` 管理,并可用 `--profile name` 临时选择。TUI 专属显示参数使用 `--mode sheet|line`。`--realtime-window 1m|5m|10m` 控制 realtime 图表横轴时间范围。一次性拉取摘要使用 `ktui status`,更新检查和安装分别使用 `ktui update check`、`ktui update install`。
|
||||||
|
|
||||||
|
## Shell 补全
|
||||||
|
|
||||||
|
`ktui completion` 可以生成 Tab 补全脚本。加载后可以补全子命令、常用 flags、枚举值、配置键和已保存的 profile 名称。
|
||||||
|
|
||||||
|
当前终端临时启用:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
source <(ktui completion bash)
|
||||||
|
# zsh:
|
||||||
|
source <(ktui completion zsh)
|
||||||
|
# fish:
|
||||||
|
ktui completion fish | source
|
||||||
|
```
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ktui completion powershell | Out-String | Invoke-Expression
|
||||||
|
```
|
||||||
|
|
||||||
|
持久启用时,把对应命令放进 `~/.bashrc`、`~/.zshrc`、fish 配置或 PowerShell profile;也可以把 `ktui completion zsh` 输出保存为 `$fpath` 目录里的 `_ktui` 文件。
|
||||||
|
|
||||||
## 配置文件
|
## 配置文件
|
||||||
|
|
||||||
初始化配置:
|
初始化配置:
|
||||||
|
|||||||
@@ -0,0 +1,541 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"ktui/internal/config"
|
||||||
|
"ktui/internal/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
completionShells = []string{"bash", "zsh", "fish", "powershell"}
|
||||||
|
|
||||||
|
cobraExportFormats = []string{"markdown", "csv", "json"}
|
||||||
|
cobraConfigKeys = []string{
|
||||||
|
"profile",
|
||||||
|
"url",
|
||||||
|
"api-key",
|
||||||
|
"interval",
|
||||||
|
"timeout",
|
||||||
|
"realtime-window",
|
||||||
|
"chart-y-axis",
|
||||||
|
"warn-cpu",
|
||||||
|
"warn-ram",
|
||||||
|
"warn-disk",
|
||||||
|
"warn-expiry-days",
|
||||||
|
"mode",
|
||||||
|
"ascii",
|
||||||
|
"no-color",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func execute() error {
|
||||||
|
return newRootCommand().Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRootCommand() *cobra.Command {
|
||||||
|
root := &cobra.Command{
|
||||||
|
Use: "ktui",
|
||||||
|
Short: "Komari terminal UI",
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleTUI(changedFlagArgs(cmd))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
root.CompletionOptions.DisableDefaultCmd = true
|
||||||
|
root.PersistentFlags().String("config", defaultConfigPath(), "config file path")
|
||||||
|
must(root.MarkPersistentFlagFilename("config"))
|
||||||
|
|
||||||
|
addTUIFlags(root)
|
||||||
|
addConnectionFlagCompletions(root)
|
||||||
|
addTUIFlagCompletions(root)
|
||||||
|
|
||||||
|
root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return setConfigEnvFromCommand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.AddCommand(
|
||||||
|
newStatusCommand(),
|
||||||
|
newExportCommand(),
|
||||||
|
newConfigCommand(),
|
||||||
|
newProfileCommand(),
|
||||||
|
newUpdateCommand(),
|
||||||
|
newVersionCommand(),
|
||||||
|
newKeysCommand(),
|
||||||
|
newCompletionCommand(),
|
||||||
|
)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStatusCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Fetch once and print a node summary",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleStatus(changedFlagArgs(cmd))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addStatusFlags(cmd)
|
||||||
|
addConnectionFlagCompletions(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExportCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "export <markdown|csv|json>",
|
||||||
|
Short: "Export current node status",
|
||||||
|
ValidArgs: cobraExportFormats,
|
||||||
|
Args: cobra.RangeArgs(0, 1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return handleExport(nil)
|
||||||
|
}
|
||||||
|
next := append([]string{args[0]}, changedFlagArgs(cmd)...)
|
||||||
|
return handleExport(next)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addStatusFlags(cmd)
|
||||||
|
cmd.Flags().StringP("output", "o", "", "write export to a file instead of stdout")
|
||||||
|
must(cmd.MarkFlagFilename("output"))
|
||||||
|
addConnectionFlagCompletions(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigCommand() *cobra.Command {
|
||||||
|
initCmd := &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Create the config file",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig(append([]string{"init"}, changedFlagArgs(cmd)...))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
initCmd.Flags().Bool("force", false, "overwrite existing config")
|
||||||
|
must(initCmd.RegisterFlagCompletionFunc("force", cobra.NoFileCompletions))
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage persistent settings",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig(nil)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
initCmd,
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "path",
|
||||||
|
Short: "Print the config path",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig([]string{"path"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Print the config file with secrets redacted",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig([]string{"show"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "set <key> <value>",
|
||||||
|
Short: "Set a config value",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: configSetCompletions,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig([]string{"set", args[0], args[1]})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "help",
|
||||||
|
Short: "Show config help",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleConfig([]string{"help"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProfileCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "profile",
|
||||||
|
Short: "Manage Komari connection profiles",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile(nil)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List configured profiles",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"list"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "current",
|
||||||
|
Short: "Print the active profile",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"current"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "use <name>",
|
||||||
|
Short: "Switch active profile",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: profileNameArgCompletions,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"use", args[0]})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newProfileAddCommand(),
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "rename <old> <new>",
|
||||||
|
Short: "Rename a profile",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: profileRenameArgCompletions,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"rename", args[0], args[1]})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "remove <name>",
|
||||||
|
Short: "Remove a profile",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: profileNameArgCompletions,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"remove", args[0]})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "help",
|
||||||
|
Short: "Show profile help",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleProfile([]string{"help"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProfileAddCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "add <name>",
|
||||||
|
Short: "Add a profile",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
next := append([]string{"add", args[0]}, changedFlagArgs(cmd)...)
|
||||||
|
return handleProfile(next)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().String("url", "", "Komari base URL")
|
||||||
|
cmd.Flags().String("api-key", "", "Komari API key")
|
||||||
|
cmd.Flags().Bool("use", false, "make this profile active")
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("api-key", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("use", cobra.NoFileCompletions))
|
||||||
|
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Check or install ktui updates",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleUpdate(nil)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
newUpdateCheckCommand(),
|
||||||
|
newUpdateInstallCommand(),
|
||||||
|
&cobra.Command{
|
||||||
|
Use: "help",
|
||||||
|
Short: "Show update help",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleUpdate([]string{"help"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateCheckCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check for a ktui update",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleUpdate(append([]string{"check"}, changedFlagArgs(cmd)...))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addUpdateFlags(cmd, false)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateInstallCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install a ktui update",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleUpdate(append([]string{"install"}, changedFlagArgs(cmd)...))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addUpdateFlags(cmd, true)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVersionCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print version information",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
printVersion()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKeysCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "keys",
|
||||||
|
Short: "Show TUI key bindings",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
printKeysHelp()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompletionCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "completion <bash|zsh|fish|powershell>",
|
||||||
|
Short: "Generate shell completion scripts",
|
||||||
|
ValidArgs: completionShells,
|
||||||
|
Args: cobra.RangeArgs(0, 1),
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return cmd.Help()
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "bash":
|
||||||
|
return cmd.Root().GenBashCompletion(os.Stdout)
|
||||||
|
case "zsh":
|
||||||
|
return cmd.Root().GenZshCompletion(os.Stdout)
|
||||||
|
case "fish":
|
||||||
|
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||||
|
case "powershell":
|
||||||
|
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown completion shell %q: use bash, zsh, fish, or powershell", args[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTUIFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
addConnectionFlags(flags, 10*time.Second)
|
||||||
|
flags.Duration("interval", 5*time.Second, "refresh interval")
|
||||||
|
flags.Duration("realtime-window", time.Minute, "realtime chart time window: 1m, 5m, or 10m")
|
||||||
|
flags.String("chart-y-axis", "absolute", "percent chart Y axis mode: absolute or relative")
|
||||||
|
flags.String("mode", "sheet", "view mode: sheet or line")
|
||||||
|
flags.Bool("ascii", false, "use ASCII-only rendering for terminals/fonts with Unicode issues")
|
||||||
|
flags.Bool("no-color", false, "disable ANSI color and inverse video")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addStatusFlags(cmd *cobra.Command) {
|
||||||
|
addConnectionFlags(cmd.Flags(), 10*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addConnectionFlags(flags *pflag.FlagSet, timeoutDefault time.Duration) {
|
||||||
|
flags.String("profile", config.DefaultProfile, "profile name")
|
||||||
|
flags.String("url", "", "Komari base URL")
|
||||||
|
flags.String("api-key", "", "Komari API key (sent as Bearer token)")
|
||||||
|
flags.Duration("timeout", timeoutDefault, "HTTP timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUpdateFlags(cmd *cobra.Command, withTag bool) {
|
||||||
|
cmd.Flags().String("api-url", update.DefaultAPIBaseURL, "Gitea repository API URL")
|
||||||
|
cmd.Flags().Duration("timeout", 60*time.Second, "update HTTP timeout")
|
||||||
|
if withTag {
|
||||||
|
cmd.Flags().String("tag", "", "install a specific release tag instead of the latest release")
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("tag", cobra.NoFileCompletions))
|
||||||
|
}
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("api-url", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addConnectionFlagCompletions(cmd *cobra.Command) {
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("profile", profileFlagCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("api-key", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTUIFlagCompletions(cmd *cobra.Command) {
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("interval", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("realtime-window", cobra.FixedCompletions([]cobra.Completion{"1m", "5m", "10m"}, cobra.ShellCompDirectiveNoFileComp)))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("chart-y-axis", cobra.FixedCompletions([]cobra.Completion{"absolute", "relative"}, cobra.ShellCompDirectiveNoFileComp)))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions([]cobra.Completion{"sheet", "line"}, cobra.ShellCompDirectiveNoFileComp)))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("ascii", cobra.NoFileCompletions))
|
||||||
|
must(cmd.RegisterFlagCompletionFunc("no-color", cobra.NoFileCompletions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func changedFlagArgs(cmd *cobra.Command) []string {
|
||||||
|
args := []string{}
|
||||||
|
cmd.Flags().Visit(func(flag *pflag.Flag) {
|
||||||
|
if flag.Name == "config" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args = append(args, "--"+flag.Name+"="+flag.Value.String())
|
||||||
|
})
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfigEnvFromCommand(cmd *cobra.Command) error {
|
||||||
|
value, err := cmd.Flags().GetString("config")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Setenv("KTUI_CONFIG", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfigPath() string {
|
||||||
|
path, err := config.Path()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileFlagCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileNameArgCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileRenameArgCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
if len(args) == 1 {
|
||||||
|
return profileCompletionsExcept(cmd, toComplete, args[0]), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
func configSetCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return filterCompletions(cobraConfigKeys, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
if len(args) == 1 {
|
||||||
|
return filterCompletions(configValueCompletionsForCobra(cmd, args[0]), toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
func configValueCompletionsForCobra(cmd *cobra.Command, key string) []string {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(key)) {
|
||||||
|
case "profile":
|
||||||
|
return completionProfileNamesForCommand(cmd)
|
||||||
|
case "mode":
|
||||||
|
return []string{"sheet", "line"}
|
||||||
|
case "realtime-window", "realtime_window":
|
||||||
|
return []string{"1m", "5m", "10m"}
|
||||||
|
case "chart-y-axis", "chart_y_axis":
|
||||||
|
return []string{"absolute", "relative"}
|
||||||
|
case "ascii", "no-color", "no_color":
|
||||||
|
return []string{"true", "false"}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileCompletions(cmd *cobra.Command, toComplete string) []cobra.Completion {
|
||||||
|
return filterCompletions(completionProfileNamesForCommand(cmd), toComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileCompletionsExcept(cmd *cobra.Command, toComplete string, exclude string) []cobra.Completion {
|
||||||
|
names := completionProfileNamesForCommand(cmd)
|
||||||
|
out := make([]string, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
if name != exclude {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filterCompletions(out, toComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func completionProfileNamesForCommand(cmd *cobra.Command) []string {
|
||||||
|
cfg, err := loadCompletionConfig(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return []string{config.DefaultProfile}
|
||||||
|
}
|
||||||
|
return cfg.ProfileNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCompletionConfig(cmd *cobra.Command) (config.Config, error) {
|
||||||
|
path, _ := cmd.Flags().GetString("config")
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
if path == "" {
|
||||||
|
cfg, _, err := config.Load()
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
old, hadOld := os.LookupEnv("KTUI_CONFIG")
|
||||||
|
if err := os.Setenv("KTUI_CONFIG", path); err != nil {
|
||||||
|
return config.Config{}, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if hadOld {
|
||||||
|
_ = os.Setenv("KTUI_CONFIG", old)
|
||||||
|
} else {
|
||||||
|
_ = os.Unsetenv("KTUI_CONFIG")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cfg, _, err := config.Load()
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterCompletions(values []string, prefix string) []cobra.Completion {
|
||||||
|
out := make([]cobra.Completion, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.HasPrefix(value, prefix) {
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"ktui/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCobraRootCommandTree(t *testing.T) {
|
||||||
|
root := newRootCommand()
|
||||||
|
for _, name := range []string{"status", "export", "config", "profile", "update", "version", "keys", "completion"} {
|
||||||
|
cmd, _, err := root.Find([]string{name})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find %q error = %v", name, err)
|
||||||
|
}
|
||||||
|
if cmd == nil || cmd.Name() != name {
|
||||||
|
t.Fatalf("find %q = %v", name, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCobraChangedFlagArgsSkipsConfig(t *testing.T) {
|
||||||
|
root := newRootCommand()
|
||||||
|
if err := root.ParseFlags([]string{"--mode", "line", "--config", "custom.json", "--ascii"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
args := changedFlagArgs(root)
|
||||||
|
if !hasString(args, "--mode=line") || !hasString(args, "--ascii=true") {
|
||||||
|
t.Fatalf("changedFlagArgs = %#v, want changed root flags", args)
|
||||||
|
}
|
||||||
|
for _, arg := range args {
|
||||||
|
if strings.HasPrefix(arg, "--config=") {
|
||||||
|
t.Fatalf("changedFlagArgs should skip config: %#v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCobraCompletionUsesConfiguredProfiles(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
|
||||||
|
cfg, err := config.AddProfile(config.Default(), "prod", "https://prod.example.com", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("KTUI_CONFIG", path)
|
||||||
|
if _, err := config.Save(cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := newRootCommand()
|
||||||
|
status, _, err := root.Find([]string{"status"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := status.ParseFlags([]string{"--config", path}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := profileCompletions(status, "pr")
|
||||||
|
if !hasCompletion(got, "prod") {
|
||||||
|
t.Fatalf("profile completions = %#v, want prod", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCobraHiddenCompletionCommand(t *testing.T) {
|
||||||
|
root := newRootCommand()
|
||||||
|
var out bytes.Buffer
|
||||||
|
root.SetOut(&out)
|
||||||
|
root.SetErr(&out)
|
||||||
|
root.SetArgs([]string{"__complete", "completion", ""})
|
||||||
|
|
||||||
|
if err := root.Execute(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
text := out.String()
|
||||||
|
for _, want := range []string{"bash", "zsh", "fish", "powershell"} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("hidden completion output missing %q:\n%s", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasString(values []string, want string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCompletion(values []string, want string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+5
-9
@@ -30,13 +30,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args, configPath := splitConfigArg(os.Args[1:])
|
if err := execute(); err != nil {
|
||||||
if configPath != "" {
|
|
||||||
if err := os.Setenv("KTUI_CONFIG", configPath); err != nil {
|
|
||||||
fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := handleCommand(args); err != nil {
|
|
||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,7 +796,7 @@ func handleProfile(args []string) error {
|
|||||||
|
|
||||||
func handleHelp(args []string) error {
|
func handleHelp(args []string) error {
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
return fmt.Errorf("usage: ktui help [status|config|profile|keys|update|export]")
|
return fmt.Errorf("usage: ktui help [status|config|profile|keys|update|export|completion]")
|
||||||
}
|
}
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printHelp()
|
printHelp()
|
||||||
@@ -880,8 +874,9 @@ Usage:
|
|||||||
ktui config <init|path|show|set|help>
|
ktui config <init|path|show|set|help>
|
||||||
ktui profile <list|current|use|add|rename|remove>
|
ktui profile <list|current|use|add|rename|remove>
|
||||||
ktui update <check|install>
|
ktui update <check|install>
|
||||||
|
ktui completion <bash|zsh|fish|powershell>
|
||||||
ktui version
|
ktui version
|
||||||
ktui help [status|config|profile|keys|update|export]
|
ktui help [status|config|profile|keys|update|export|completion]
|
||||||
|
|
||||||
Connection flags:
|
Connection flags:
|
||||||
--profile NAME profile name
|
--profile NAME profile name
|
||||||
@@ -908,6 +903,7 @@ Examples:
|
|||||||
ktui version
|
ktui version
|
||||||
ktui update check
|
ktui update check
|
||||||
ktui update install
|
ktui update install
|
||||||
|
ktui completion bash
|
||||||
ktui export markdown
|
ktui export markdown
|
||||||
ktui export csv --output nodes.csv
|
ktui export csv --output nodes.csv
|
||||||
ktui config init
|
ktui config init
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ func TestHelpTextUsesRefactoredCommands(t *testing.T) {
|
|||||||
"ktui export <markdown|csv|json> [flags]",
|
"ktui export <markdown|csv|json> [flags]",
|
||||||
"ktui profile <list|current|use|add|rename|remove>",
|
"ktui profile <list|current|use|add|rename|remove>",
|
||||||
"ktui update <check|install>",
|
"ktui update <check|install>",
|
||||||
|
"ktui completion <bash|zsh|fish|powershell>",
|
||||||
"--profile NAME",
|
"--profile NAME",
|
||||||
"--mode MODE",
|
"--mode MODE",
|
||||||
"--realtime-window DURATION",
|
"--realtime-window DURATION",
|
||||||
"ktui update check",
|
"ktui update check",
|
||||||
"ktui update install",
|
"ktui update install",
|
||||||
|
"ktui completion bash",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(helpText, want) {
|
if !strings.Contains(helpText, want) {
|
||||||
t.Fatalf("help text missing %q:\n%s", want, helpText)
|
t.Fatalf("help text missing %q:\n%s", want, helpText)
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
module ktui
|
module ktui
|
||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/spf13/pflag v1.0.9
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
Reference in new issue
Block a user