Skip to main content

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}