-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroot.go
More file actions
151 lines (129 loc) · 3.69 KB
/
Copy pathroot.go
File metadata and controls
151 lines (129 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Package cli builds the comick command tree on top of the comick library.
package cli
import (
"fmt"
"os"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/tamnd/comick-cli/comick"
)
// Build metadata, set via -ldflags at release time.
var (
Version = "dev"
Commit = "none"
Date = "unknown"
)
// exit codes
const (
exitError = 1
exitUsage = 2
exitNoData = 3
)
// ExitError carries a process exit code up to main.
type ExitError struct {
Code int
Err error
}
func (e *ExitError) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return fmt.Sprintf("exit %d", e.Code)
}
func (e *ExitError) Unwrap() error { return e.Err }
func codeError(code int, err error) error { return &ExitError{Code: code, Err: err} }
// App holds shared state threaded through every command.
type App struct {
client *comick.Client
cfg comick.Config
output string
fields []string
noHeader bool
template string
limit int
quiet bool
}
// Root builds the root command and its subtree.
func Root() *cobra.Command {
app := &App{cfg: comick.DefaultConfig()}
root := &cobra.Command{
Use: "comick",
Short: "Browse manga and comics on comick.io",
Long: `comick reads manga and comic data from comick.io through its public API.
No API key is required. It returns records as table, JSON, JSONL, CSV, TSV, or URLs.
comick is an independent tool and is not affiliated with comick.io.`,
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return app.setup()
},
}
pf := root.PersistentFlags()
pf.StringVarP(&app.output, "output", "o", "auto", "output: table|json|jsonl|csv|tsv|url|raw (auto=table on TTY, jsonl piped)")
pf.StringSliceVar(&app.fields, "fields", nil, "comma-separated columns to include")
pf.BoolVar(&app.noHeader, "no-header", false, "omit the header row in table/csv/tsv")
pf.StringVar(&app.template, "template", "", "Go text/template applied per record")
pf.IntVarP(&app.limit, "limit", "n", 0, "limit number of records (0 = command default)")
pf.BoolVarP(&app.quiet, "quiet", "q", false, "suppress progress on stderr")
pf.DurationVar(&app.cfg.Rate, "delay", app.cfg.Rate, "minimum spacing between requests")
pf.DurationVar(&app.cfg.Timeout, "timeout", app.cfg.Timeout, "per-request timeout")
pf.IntVar(&app.cfg.Retries, "retries", app.cfg.Retries, "retry attempts on 429/5xx")
pf.StringVar(&app.cfg.UserAgent, "user-agent", app.cfg.UserAgent, "User-Agent sent with each request")
root.AddCommand(
app.searchCmd(),
app.trendingCmd(),
app.newCmd(),
app.comicCmd(),
app.chaptersCmd(),
newVersionCmd(),
)
return root
}
func (a *App) setup() error {
if a.output == "" || a.output == "auto" {
if isatty.IsTerminal(os.Stdout.Fd()) {
a.output = string(FormatTable)
} else {
a.output = string(FormatJSONL)
}
}
if !Format(a.output).Valid() {
return codeError(exitUsage, fmt.Errorf("unknown output format %q", a.output))
}
a.client = comick.NewClient(a.cfg)
return nil
}
func (a *App) render(records any) error {
r := NewRenderer(os.Stdout, Format(a.output), a.fields, a.noHeader, a.template)
return r.Render(records)
}
func (a *App) renderOrEmpty(records any, n int) error {
if err := a.render(records); err != nil {
return err
}
if n == 0 {
return codeError(exitNoData, nil)
}
return nil
}
func (a *App) progressf(format string, args ...any) {
if a.quiet {
return
}
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args...)
}
func mapFetchErr(err error) error {
if err == nil {
return nil
}
if isNotFound(err) {
return codeError(exitNoData, err)
}
return codeError(exitError, err)
}
func (a *App) effectiveLimit(def int) int {
if a.limit > 0 {
return a.limit
}
return def
}