Skip to main content

mdcat/
cli.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! Shared CLI entry point for both binaries.
6//!
7//! `src/main.rs` and `src/bin/mdless.rs` are shims into `run`.
8//! clap's multicall mode picks the subcommand from `argv[0]`.
9
10use clap::{CommandFactory, Parser};
11use clap_complete::generate;
12
13use crate::args::{Args, PagingMode};
14use crate::output::Output;
15use crate::{
16    create_resource_handler, process_file, Multiplexer, Settings, TerminalProgram, TerminalSize,
17    Theme,
18};
19use syntect::parsing::SyntaxSet;
20use tracing::{event, Level};
21use tracing_subscriber::filter::LevelFilter;
22use tracing_subscriber::EnvFilter;
23
24/// Parse arguments, detect the terminal, dispatch. Exits the process.
25pub fn run() -> ! {
26    let filter = EnvFilter::builder()
27        .with_default_directive(LevelFilter::OFF.into())
28        .with_env_var("MDCAT_LOG")
29        .from_env_lossy();
30    tracing_subscriber::fmt::Subscriber::builder()
31        .pretty()
32        .with_env_filter(filter)
33        .with_writer(std::io::stderr)
34        .init();
35
36    let args = Args::parse().command;
37    event!(target: "mdcat::main", Level::TRACE, ?args, "mdcat arguments");
38
39    if let Some(shell) = args.completions {
40        let binary = match args {
41            crate::args::Command::Mdcat { .. } => "mdcat",
42            crate::args::Command::Mdless { .. } => "mdless",
43        };
44        let mut command = Args::command();
45        let subcommand = command.find_subcommand_mut(binary).unwrap();
46        generate(shell, subcommand, binary, &mut std::io::stdout());
47        std::process::exit(0);
48    }
49
50    let stdout_is_tty = std::io::IsTerminal::is_terminal(&std::io::stdout());
51    let paging_mode = args.paging_mode();
52
53    let terminal = if args.no_colour {
54        TerminalProgram::Dumb
55    } else if paging_mode.is_paginated() || args.ansi_only {
56        TerminalProgram::Ansi
57    } else if !stdout_is_tty {
58        TerminalProgram::Dumb
59    } else {
60        TerminalProgram::detect()
61    };
62
63    let multiplexer = Multiplexer::detect();
64
65    let probed = if !args.no_probe_terminal
66        && terminal == TerminalProgram::Ansi
67        && stdout_is_tty
68        && !paging_mode.is_paginated()
69    {
70        crate::terminal::probe_da1(std::time::Duration::from_millis(args.probe_timeout_ms))
71    } else {
72        None
73    };
74
75    if args.detect_and_exit {
76        println!("Terminal: {terminal}");
77        if multiplexer != Multiplexer::None {
78            println!("Multiplexer: {multiplexer:?}");
79        }
80        if let Some(attrs) = probed {
81            println!("Probed: sixel={}", attrs.sixel);
82        }
83        std::process::exit(0);
84    }
85
86    if paging_mode == PagingMode::Interactive {
87        std::process::exit(run_interactive_mdless(&args));
88    }
89
90    #[cfg(windows)]
91    anstyle_query::windows::enable_ansi_colors();
92
93    // Leave ~2 columns of breathing room on the right edge, except on
94    // pathologically narrow terminals where every column counts.
95    let base = TerminalSize::detect().unwrap_or_default();
96    let max_columns = args.columns.unwrap_or(if base.columns > 20 {
97        base.columns - 2
98    } else {
99        base.columns
100    });
101    let terminal_size = base.with_max_columns(max_columns);
102
103    let exit_code = match Output::new(paging_mode == PagingMode::ExternalLess) {
104        Ok(mut output) => {
105            #[cfg_attr(not(feature = "sixel"), allow(unused_mut))]
106            let mut capabilities = terminal.capabilities();
107            #[cfg(feature = "sixel")]
108            if probed.is_some_and(|attrs| attrs.sixel) && capabilities.image.is_none() {
109                use crate::terminal::capabilities::{sixel::SixelProtocol, ImageCapability};
110                capabilities.image = Some(ImageCapability::Sixel(SixelProtocol));
111            }
112            let settings = Settings {
113                terminal_capabilities: capabilities,
114                terminal_size,
115                multiplexer,
116                syntax_set: &SyntaxSet::load_defaults_newlines(),
117                theme: Theme::default(),
118                wrap_code: args.wrap_code,
119            };
120            event!(
121                target: "mdcat::main",
122                Level::TRACE,
123                ?settings.terminal_size,
124                ?settings.terminal_capabilities,
125                "settings"
126            );
127            let resource_handler = create_resource_handler(args.resource_access()).unwrap();
128            args.filenames
129                .iter()
130                .try_fold(0, |code, filename| {
131                    process_file(
132                        filename,
133                        &settings,
134                        args.resource_access(),
135                        &resource_handler,
136                        &mut output,
137                    )
138                    .map(|()| code)
139                    .or_else(|error| {
140                        eprintln!("Error: {filename}: {error}");
141                        if args.fail_fast {
142                            Err(error)
143                        } else {
144                            Ok(1)
145                        }
146                    })
147                })
148                .unwrap_or(1)
149        }
150        Err(error) => {
151            eprintln!("Error: {error:#}");
152            128
153        }
154    };
155    event!(target: "mdcat::main", Level::TRACE, "Exiting with final exit code {}", exit_code);
156    std::process::exit(exit_code);
157}
158
159/// Run the interactive `mdless` pager for the first filename.
160///
161/// Extra filenames are ignored with a warning since the interactive
162/// pager only buffers one document at a time.
163fn run_interactive_mdless(args: &crate::args::Command) -> i32 {
164    let resource_handler =
165        create_resource_handler(args.resource_access()).expect("resource handler");
166    let filename = args.filenames.first().map_or("-", String::as_str);
167    if args.filenames.len() > 1 {
168        eprintln!(
169            "mdless: only the first file is shown interactively; {} more ignored",
170            args.filenames.len() - 1,
171        );
172    }
173    let opts = match args {
174        crate::args::Command::Mdless {
175            search,
176            case_sensitive,
177            regex,
178            line_numbers,
179            ..
180        } => crate::mdless::MdlessOptions {
181            initial: search.clone(),
182            case_sensitive: *case_sensitive,
183            regex: *regex,
184            line_numbers: *line_numbers,
185        },
186        crate::args::Command::Mdcat { .. } => crate::mdless::MdlessOptions::default(),
187    };
188    match crate::mdless::run(filename, args, opts, &resource_handler) {
189        Ok(code) => code,
190        Err(error) => {
191            eprintln!("Error: {filename}: {error:#}");
192            1
193        }
194    }
195}