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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
use clap::builder::styling::{Ansi256Color, Color, Style, Styles};
use crate::color::ColorMode;
/// Help styling: bold headers, the brand pink for literals. clap only
/// applies it on a tty and honors NO_COLOR.
const HELP_STYLES: Styles = Styles::styled()
.header(Style::new().bold())
.usage(Style::new().bold())
.literal(Style::new().fg_color(Some(Color::Ansi256(Ansi256Color(212)))))
.placeholder(Style::new().dimmed());
#[derive(clap::Parser)]
#[command(
name = "rat",
version,
about = "Ratatui-powered primitives for shell dashboards",
styles = HELP_STYLES
)]
pub struct Cli {
#[arg(long, value_enum, global = true, default_value_t = ColorMode::Auto)]
pub color: ColorMode,
#[command(subcommand)]
pub command: Command,
}
#[derive(clap::Subcommand)]
pub enum Command {
/// Apply colors and text attributes to text
Style(StyleArgs),
/// Render one-shot progress bars
Bar(BarArgs),
/// Format and parse durations
Duration(DurationArgs),
/// Parse, format, and diff timestamps portably
Date(DateArgs),
/// Render a sparkline from numbers
Spark(SparkArgs),
/// Print a styled log line to stderr
Log(LogArgs),
/// Repaint a frame of output in place, flicker-free
Frame(FrameArgs),
/// Run a command on an interval and repaint its output flicker-free
Watch(WatchArgs),
/// Diagnose terminal capabilities
Doctor(DoctorArgs),
/// Pick one or more items from a list
Choose(ChooseArgs),
/// Ask a yes/no question
Confirm(ConfirmArgs),
/// Prompt for a line of input
Input(InputArgs),
/// Fuzzy-filter lines from stdin
Filter(FilterArgs),
/// Show a spinner while a command runs
Spin(SpinArgs),
/// Generate shell completions
Completion(CompletionArgs),
/// Test harness: exit with the given code through AppError mapping.
#[cfg(debug_assertions)]
#[command(name = "__exitcode", hide = true)]
ExitCode(ExitCodeArgs),
}
#[cfg(debug_assertions)]
#[derive(clap::Args)]
pub struct ExitCodeArgs {
pub code: i32,
}
#[derive(clap::Args)]
pub struct StyleArgs {
/// Text to style; reads stdin when omitted. Multiple args join with newlines.
pub text: Vec<String>,
#[arg(long)]
pub bold: bool,
#[arg(long)]
pub faint: bool,
#[arg(long)]
pub italic: bool,
#[arg(long)]
pub underline: bool,
#[arg(long)]
pub strikethrough: bool,
/// Foreground color: name, 256 index, or #rrggbb
#[arg(long, env = "FOREGROUND")]
pub foreground: Option<String>,
/// Background color: name, 256 index, or #rrggbb
#[arg(long, env = "BACKGROUND")]
pub background: Option<String>,
/// Trim whitespace from each line
#[arg(long)]
pub trim: bool,
/// Strip ANSI escapes from input before styling (default)
#[arg(long, overrides_with = "no_strip_ansi")]
pub strip_ansi: bool,
/// Keep ANSI escapes present in the input
#[arg(long)]
pub no_strip_ansi: bool,
}
#[derive(clap::Args)]
pub struct BarArgs {
/// Current value
#[arg(long, allow_negative_numbers = true)]
pub value: Option<f64>,
#[arg(long, default_value_t = 100.0, allow_negative_numbers = true)]
pub total: f64,
/// Bar width in cells
#[arg(long, default_value_t = 32)]
pub width: u16,
#[arg(long)]
pub label: Option<String>,
/// Label column width (display cells)
#[arg(long, default_value_t = 34)]
pub label_width: u16,
#[arg(long, value_enum, default_value_t = crate::core::bar::BarPreset::Blocks)]
pub preset: crate::core::bar::BarPreset,
/// Override the fill character
#[arg(long)]
pub fill: Option<char>,
/// Override the empty character
#[arg(long)]
pub empty: Option<char>,
/// Fill color; wins over --thresholds when given (default 212)
#[arg(long)]
pub fill_color: Option<String>,
#[arg(long, default_value = "240")]
pub empty_color: String,
/// State word appended after the annotations
#[arg(long)]
pub state: Option<String>,
#[arg(long, value_enum, default_value_t = crate::core::bar::Annotation::Both)]
pub annotation: crate::core::bar::Annotation,
/// Color the fill by percentage band, e.g. "33:196,66:214,100:42"
#[arg(long)]
pub thresholds: Option<String>,
/// Field delimiter for stdin batch rows
#[arg(long, default_value_t = '\t')]
pub delimiter: char,
/// Render a moving block instead of progress (unknown total)
#[arg(long)]
pub indeterminate: bool,
/// Animation step for --indeterminate
#[arg(long, default_value_t = 0)]
pub tick: u64,
}
#[derive(clap::Args)]
pub struct DurationArgs {
/// Seconds to format, or a duration string with --seconds
pub value: String,
/// Parse a duration string ("1h33m") and print integer seconds
#[arg(long, conflicts_with_all = ["ms", "format"])]
pub seconds: bool,
/// Treat the value as milliseconds
#[arg(long)]
pub ms: bool,
#[arg(long, value_enum, default_value_t = crate::core::duration::DurationFormat::Compact)]
pub format: crate::core::duration::DurationFormat,
}
#[derive(clap::Args)]
pub struct DateArgs {
/// "now" (default), epoch seconds, or an RFC3339 timestamp
pub value: Option<String>,
/// Print epoch seconds
#[arg(long, conflicts_with_all = ["format", "relative", "since", "until"])]
pub epoch: bool,
/// strftime output format
#[arg(long)]
pub format: Option<String>,
/// Use UTC instead of the local zone
#[arg(long)]
pub utc: bool,
/// Phrase the timestamp relative to now
#[arg(long, conflicts_with_all = ["format", "since", "until"])]
pub relative: bool,
/// Seconds elapsed from this timestamp to the value
#[arg(long, conflicts_with = "until")]
pub since: Option<String>,
/// Seconds remaining from the value to this timestamp
#[arg(long)]
pub until: Option<String>,
}
#[derive(clap::Args)]
pub struct SparkArgs {
/// Numbers to plot; reads stdin when omitted
#[arg(allow_negative_numbers = true)]
pub values: Vec<String>,
/// Clamp the lower bound
#[arg(long, allow_negative_numbers = true)]
pub min: Option<f64>,
/// Clamp the upper bound
#[arg(long, allow_negative_numbers = true)]
pub max: Option<f64>,
/// Color for the sparkline
#[arg(long = "spark-color", visible_alias = "color-fg")]
pub spark_color: Option<String>,
}
#[derive(clap::Args)]
pub struct LogArgs {
/// Message text; joined with single spaces
pub text: Vec<String>,
/// Log level tag
#[arg(short = 'l', long, value_enum)]
pub level: Option<LogLevel>,
/// Minimum level to emit (lower levels are dropped)
#[arg(long, value_enum, env = "RAT_LOG_LEVEL")]
pub min_level: Option<LogLevel>,
/// Prefix with the current time in this strftime format
#[arg(short = 't', long)]
pub time: Option<String>,
/// Append plain text to this file instead of stderr
#[arg(short = 'o', long)]
pub file: Option<std::path::PathBuf>,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, clap::ValueEnum)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
Fatal,
}
#[derive(clap::Args)]
pub struct FrameArgs {
#[command(subcommand)]
pub action: Option<FrameAction>,
/// State file path (defaults to a per-terminal temp file)
#[arg(long)]
pub state: Option<std::path::PathBuf>,
/// Override the detected terminal width
#[arg(long)]
pub width: Option<u16>,
/// Skip synchronized-output escapes
#[arg(long)]
pub no_sync: bool,
/// Leave the cursor visible while painting
#[arg(long)]
pub no_hide_cursor: bool,
/// Forget the previous frame without painting
#[arg(long, conflicts_with_all = ["finish", "clear"])]
pub reset: bool,
/// Show the cursor, close any open frame, and forget state
#[arg(long, conflicts_with = "clear")]
pub finish: bool,
/// Erase the painted frame, show the cursor, and forget state
#[arg(long)]
pub clear: bool,
}
#[derive(clap::Subcommand)]
pub enum FrameAction {
/// Emit the begin-synchronized-update escape
Begin,
/// Emit the end-synchronized-update escape
End,
}
#[derive(clap::Args)]
pub struct WatchArgs {
/// Refresh interval, e.g. "2s", "500ms", "1m"
#[arg(short = 'n', long, default_value = "2s")]
pub interval: String,
/// Run one tick and exit
#[arg(long)]
pub once: bool,
/// Clear the screen before the first frame (atomically, inside it)
#[arg(long)]
pub clear: bool,
/// Leave the cursor visible
#[arg(long)]
pub no_hide_cursor: bool,
/// Skip synchronized-output escapes
#[arg(long)]
pub no_sync: bool,
/// Run the command through `sh -c`
#[arg(long)]
pub shell: bool,
/// Bold title line above the output
#[arg(long)]
pub title: Option<String>,
/// Cap the painted height (defaults to terminal height minus two)
#[arg(long)]
pub max_height: Option<u16>,
/// Command to run each tick (after --)
#[arg(last = true, required = true)]
pub command: Vec<String>,
}
#[derive(clap::Args)]
pub struct DoctorArgs {
/// Machine-readable output
#[arg(long)]
pub json: bool,
}
#[derive(clap::Args)]
pub struct ChooseArgs {
/// Options to pick from; reads stdin when omitted
pub options: Vec<String>,
/// Maximum selections (single-select by default)
#[arg(long, default_value_t = 1, conflicts_with = "no_limit")]
pub limit: usize,
/// Allow any number of selections
#[arg(long)]
pub no_limit: bool,
/// Return results in list order instead of selection order
#[arg(long)]
pub ordered: bool,
/// Visible list height
#[arg(long, default_value_t = 10)]
pub height: u16,
/// Cursor marker
#[arg(long, default_value = "> ")]
pub cursor: String,
/// Header shown above the list
#[arg(long, default_value = "Choose:")]
pub header: String,
/// Prefix for selected items (multi-select)
#[arg(long, default_value = "✓ ")]
pub selected_prefix: String,
/// Prefix for unselected items (multi-select)
#[arg(long, default_value = "• ")]
pub unselected_prefix: String,
/// Preselect these options
#[arg(long)]
pub selected: Vec<String>,
/// Auto-select when only one option exists
#[arg(long)]
pub select_if_one: bool,
/// Delimiter for stdin options
#[arg(long, default_value = "\n")]
pub input_delimiter: String,
/// Delimiter joining printed results
#[arg(long, default_value = "\n")]
pub output_delimiter: String,
/// Show the key help footer
#[arg(long, overrides_with = "no_show_help")]
pub show_help: bool,
#[arg(long)]
pub no_show_help: bool,
/// Give up after this long (exit 124)
#[arg(long)]
pub timeout: Option<String>,
}
#[derive(clap::Args)]
pub struct ConfirmArgs {
/// The question to ask
#[arg(default_value = "Are you sure?")]
pub prompt: String,
/// Initially selected answer
#[arg(long = "default", default_value_t = true, action = clap::ArgAction::Set)]
pub default_yes: bool,
/// Label for the affirmative answer
#[arg(long, default_value = "Yes")]
pub affirmative: String,
/// Label for the negative answer
#[arg(long, default_value = "No")]
pub negative: String,
/// Print the chosen label to stdout
#[arg(long)]
pub show_output: bool,
/// Give up after this long (exit 124)
#[arg(long)]
pub timeout: Option<String>,
}
#[derive(clap::Args)]
pub struct InputArgs {
/// Placeholder shown while empty
#[arg(long, default_value = "Type something...")]
pub placeholder: String,
/// Prompt prefix
#[arg(long, default_value = "> ")]
pub prompt: String,
/// Initial value
#[arg(long, default_value = "")]
pub value: String,
/// Mask the input
#[arg(long)]
pub password: bool,
/// Maximum input length
#[arg(long, default_value_t = 400)]
pub char_limit: usize,
/// Header line above the prompt
#[arg(long)]
pub header: Option<String>,
/// Give up after this long (exit 124)
#[arg(long)]
pub timeout: Option<String>,
}
#[derive(clap::Args)]
pub struct FilterArgs {
/// Maximum selections (single-select by default)
#[arg(long, default_value_t = 1, conflicts_with = "no_limit")]
pub limit: usize,
/// Allow any number of selections
#[arg(long)]
pub no_limit: bool,
/// Placeholder shown while the query is empty
#[arg(long, default_value = "Filter...")]
pub placeholder: String,
/// Prompt prefix
#[arg(long, default_value = "> ")]
pub prompt: String,
/// Initial query
#[arg(long, default_value = "")]
pub value: String,
/// Visible list height
#[arg(long, default_value_t = 10)]
pub height: u16,
/// With no matches, print nothing instead of the query
#[arg(long, overrides_with = "no_strict", default_value_t = true)]
pub strict: bool,
#[arg(long)]
pub no_strict: bool,
/// Fuzzy matching (subsequences) instead of substring
#[arg(long, overrides_with = "no_fuzzy", default_value_t = true)]
pub fuzzy: bool,
#[arg(long)]
pub no_fuzzy: bool,
/// Rank matches by score
#[arg(long, overrides_with = "no_fuzzy_sort", default_value_t = true)]
pub fuzzy_sort: bool,
#[arg(long)]
pub no_fuzzy_sort: bool,
/// Cursor indicator
#[arg(long, default_value = "• ")]
pub indicator: String,
/// Prefix for selected items (multi-select)
#[arg(long, default_value = "◉ ")]
pub selected_prefix: String,
/// Prefix for unselected items (multi-select)
#[arg(long, default_value = "○ ")]
pub unselected_prefix: String,
/// Header line above the prompt
#[arg(long)]
pub header: Option<String>,
/// Auto-select when only one candidate exists
#[arg(long)]
pub select_if_one: bool,
/// Delimiter for stdin candidates
#[arg(long, default_value = "\n")]
pub input_delimiter: String,
/// Delimiter joining printed results
#[arg(long, default_value = "\n")]
pub output_delimiter: String,
/// Give up after this long (exit 124)
#[arg(long)]
pub timeout: Option<String>,
}
#[derive(clap::Args)]
pub struct SpinArgs {
/// Spinner charset
#[arg(short = 's', long, value_enum, default_value_t = crate::ui::spin::Spinner::Dot)]
pub spinner: crate::ui::spin::Spinner,
/// Text shown beside the spinner
#[arg(long, default_value = "Loading...")]
pub title: String,
/// Show the child's stdout and stderr when it finishes
#[arg(long)]
pub show_output: bool,
/// Show only the child's stdout
#[arg(long)]
pub show_stdout: bool,
/// Show only the child's stderr
#[arg(long)]
pub show_stderr: bool,
/// Show the child's output only when it fails
#[arg(long)]
pub show_error: bool,
/// Kill the child after this long (exit 124)
#[arg(long)]
pub timeout: Option<String>,
/// Command to run (after --)
#[arg(last = true, required = true)]
pub command: Vec<String>,
}
#[derive(clap::Args)]
pub struct CompletionArgs {
/// Shell to generate completions for
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
#[test]
fn cli_is_well_formed() {
super::Cli::command().debug_assert();
}
}