mdcat/args.rs
1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Command-line argument definitions for the `mdcat` multicall binary.
8//!
9//! The binary dispatches on its `argv[0]` basename: invoking it as
10//! `mdcat` selects the `Command::Mdcat` variant, `mdless` selects
11//! `Command::Mdless`. Flags common to both subcommands live on
12//! `CommonArgs`; mode-specific flags hang off each enum variant.
13//! `Command::paging_mode` maps the final flag state to a
14//! `PagingMode` that drives the output layer in [`crate::cli`].
15
16use clap::ValueHint;
17use clap_complete::Shell;
18
19/// `-h`/`--help` footer.
20fn after_help() -> &'static str {
21 "See 'man 1 mdcat' for more information.
22
23Two binaries ship: mdcat prints to stdout, mdless opens the
24interactive pager. Report issues at
25<https://github.com/pawelb0/mdcat-ng>."
26}
27
28fn long_version() -> &'static str {
29 concat!(
30 env!("CARGO_PKG_VERSION"),
31 "
32Licensed under the Mozilla Public License, v. 2.0.
33See <http://mozilla.org/MPL/2.0/>."
34 )
35}
36
37/// Top-level clap parser. Wraps the multicall subcommand dispatch.
38#[derive(Debug, clap::Parser)]
39#[command(multicall = true)]
40pub struct Args {
41 /// Subcommand selected from `argv[0]` (`mdcat` or `mdless`).
42 #[command(subcommand)]
43 pub command: Command,
44}
45
46/// Subcommand selected by `argv[0]`.
47#[derive(Debug, clap::Subcommand)]
48pub enum Command {
49 /// `mdcat`: render markdown to the terminal.
50 #[command(version, about, after_help = after_help(), long_version = long_version())]
51 Mdcat {
52 /// Flags common to both subcommands.
53 #[command(flatten)]
54 args: CommonArgs,
55 /// Pipe the rendered output through `$PAGER` / `less -r`.
56 ///
57 /// Disables image protocols for the duration of the pager
58 /// session since most pagers mangle position-sensitive
59 /// escapes.
60 #[arg(short, long, overrides_with = "no_pager")]
61 paginate: bool,
62 /// Do not paginate output (default). Overrides a preceding `--paginate`.
63 #[arg(short = 'P', long)]
64 no_pager: bool,
65 },
66 /// `mdless`: open the interactive markdown-aware pager.
67 #[command(version, about, after_help = after_help(), long_version = long_version())]
68 Mdless {
69 /// Flags common to both subcommands.
70 #[command(flatten)]
71 args: CommonArgs,
72 /// Skip the pager and print to stdout, like `mdcat FILE`.
73 #[arg(short = 'P', long, overrides_with_all = ["external_pager"])]
74 no_pager: bool,
75 /// Shell out to `$PAGER` / `less -r` instead of the built-in pager.
76 ///
77 /// Preserves the 2.x `mdless` behaviour for users who prefer
78 /// their existing pager over the built-in interactive one.
79 #[arg(long)]
80 external_pager: bool,
81 /// Pattern to jump to and highlight on startup (like typing `/PATTERN`).
82 #[arg(long = "search", value_name = "PATTERN")]
83 search: Option<String>,
84 /// Force case-sensitive search (default is smart-case).
85 #[arg(long)]
86 case_sensitive: bool,
87 /// Interpret the search pattern as a regex instead of a literal.
88 #[arg(long)]
89 regex: bool,
90 /// Render to stdout without entering the pager; for the test harness.
91 #[arg(long, hide = true)]
92 render_only: bool,
93 /// Show rendered-line numbers in a left gutter. Toggle live with `#`.
94 #[arg(short = 'n', long = "line-numbers")]
95 line_numbers: bool,
96 },
97}
98
99/// How `mdcat` should deliver its rendered output to the user.
100#[derive(Debug, Copy, Clone, Eq, PartialEq)]
101pub enum PagingMode {
102 /// Print to stdout and exit.
103 None,
104 /// Pipe through the external `$PAGER` / `less -r` child process.
105 ExternalLess,
106 /// Run the built-in interactive markdown-aware pager.
107 Interactive,
108}
109
110impl Command {
111 /// Resolve the active paging mode from the parsed flags. `--no-pager`
112 /// always wins via clap's `overrides_with`, so by this point the flag
113 /// combinations below are already mutually consistent.
114 pub fn paging_mode(&self) -> PagingMode {
115 match *self {
116 Command::Mdcat { paginate: true, .. } => PagingMode::ExternalLess,
117 Command::Mdcat { .. } => PagingMode::None,
118 Command::Mdless { no_pager: true, .. }
119 | Command::Mdless {
120 render_only: true, ..
121 } => PagingMode::None,
122 Command::Mdless {
123 external_pager: true,
124 ..
125 } => PagingMode::ExternalLess,
126 Command::Mdless { .. } => PagingMode::Interactive,
127 }
128 }
129}
130
131impl PagingMode {
132 /// `true` if *any* pager owns the terminal (external `less` or the
133 /// built-in interactive pager). Used to decide whether we should emit
134 /// image-protocol escapes, run active TTY probes, etc.
135 pub fn is_paginated(self) -> bool {
136 !matches!(self, PagingMode::None)
137 }
138}
139
140impl std::ops::Deref for Command {
141 type Target = CommonArgs;
142
143 fn deref(&self) -> &Self::Target {
144 match self {
145 Command::Mdcat { args, .. } => args,
146 Command::Mdless { args, .. } => args,
147 }
148 }
149}
150
151/// Flags shared by both `mdcat` and `mdless`.
152#[derive(Debug, clap::Args)]
153pub struct CommonArgs {
154 /// Files to read. If - read from standard input instead.
155 #[arg(default_value="-", value_hint = ValueHint::FilePath)]
156 pub filenames: Vec<String>,
157 /// Disable all colours and other styles.
158 #[arg(short = 'c', long, aliases=["nocolour", "no-color", "nocolor"])]
159 pub no_colour: bool,
160 /// Maximum number of columns to use for output.
161 #[arg(long)]
162 pub columns: Option<u16>,
163 /// Deprecated: kept for 2.x compatibility. Local-only is now the default.
164 #[arg(short = 'l', long = "local", hide = true)]
165 pub local_only: bool,
166 /// Fetch remote (HTTP/HTTPS) images for inline display.
167 ///
168 /// Off by default: remote fetches can be slow, and the tracking /
169 /// SSRF surface they open isn't something a Markdown viewer should
170 /// pay for silently. Pass this when you want images from URLs to
171 /// render inline on a capable terminal.
172 #[arg(long = "remote-images")]
173 pub remote_images: bool,
174 /// Exit immediately if any error occurs processing an input file.
175 #[arg(long = "fail")]
176 pub fail_fast: bool,
177 /// Print detected terminal name and exit.
178 #[arg(long = "detect-terminal")]
179 pub detect_and_exit: bool,
180 /// Skip terminal detection and only use ANSI formatting.
181 #[arg(long = "ansi", conflicts_with = "no_colour")]
182 pub ansi_only: bool,
183 /// Skip active DA1 capability probing (which is on by default for interactive TTY output).
184 #[arg(long = "no-probe-terminal")]
185 pub no_probe_terminal: bool,
186 /// Milliseconds to wait for a Primary Device Attributes (DA1) probe reply.
187 ///
188 /// Real terminals answer in 1-5 ms; the default gives a generous window
189 /// for slow SSH sessions without noticeably delaying startup. Bump this
190 /// if the probe silently falls through on a responsive-but-slow terminal.
191 #[arg(long = "probe-timeout-ms", default_value_t = 50)]
192 pub probe_timeout_ms: u64,
193 /// Generate completions for a shell to standard output and exit.
194 #[arg(long)]
195 pub completions: Option<Shell>,
196 /// Wrap code-block lines that exceed the terminal width instead of overflowing.
197 #[arg(long = "wrap-code")]
198 pub wrap_code: bool,
199}
200
201/// What resources mdcat may access.
202#[derive(Debug, Copy, Clone)]
203pub enum ResourceAccess {
204 /// Only allow local resources.
205 LocalOnly,
206 /// Allow remote resources
207 Remote,
208}
209
210impl CommonArgs {
211 /// Whether remote resource access is permitted.
212 ///
213 /// Local-only is the default. `--remote-images` opts in. `--local`
214 /// is kept as a no-op alias so 2.x command lines keep parsing.
215 pub fn resource_access(&self) -> ResourceAccess {
216 if self.remote_images && !self.local_only {
217 ResourceAccess::Remote
218 } else {
219 ResourceAccess::LocalOnly
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::Args;
227 use clap::CommandFactory;
228
229 #[test]
230 fn verify_app() {
231 Args::command().debug_assert();
232 }
233}