From 800701df2e677ba3049d6c94c0a7926b0657a683 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sat, 7 Jan 2023 09:49:23 +0100 Subject: [PATCH 1/3] Refactor terminal detection Introduce a separate TerminalApp struct to abstract terminal detection separately from terminal capabilities. Check $TERM before all other environment variables; terminal emulators always set this variable and if it's something other than xterm-256colors it's definitely accurate. Closes GH-230 --- CHANGELOG.md | 7 + Cargo.lock | 10 + Cargo.toml | 1 + mdcat.1.adoc | 38 ++- src/bin/mdcat/main.rs | 22 +- src/lib.rs | 11 +- src/render.rs | 3 +- src/render/state.rs | 3 +- src/render/write.rs | 8 +- src/terminal.rs | 154 +----------- src/terminal/capabilities.rs | 84 +++++++ src/terminal/{ => capabilities}/iterm2.rs | 18 +- src/terminal/{ => capabilities}/kitty.rs | 13 +- .../{ => capabilities}/terminology.rs | 6 +- src/terminal/detect.rs | 225 ++++++++++++++++++ tests/render.rs | 9 +- 16 files changed, 402 insertions(+), 210 deletions(-) create mode 100644 src/terminal/capabilities.rs rename src/terminal/{ => capabilities}/iterm2.rs (82%) rename src/terminal/{ => capabilities}/kitty.rs (95%) rename src/terminal/{ => capabilities}/terminology.rs (95%) create mode 100644 src/terminal/detect.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f00614a0..8d5a535d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,15 @@ To publish a new release run `scripts/release` from the project directory. - Replace `ureq` with `reqwest` (see [GH-229]). This implies that the default build now creates a binary linked against the system standard SSL library, i.e. openssl under Linux. A fully static build now requires `--no-default-features --features static` for `cargo build`. +- Terminal detection always checks `$TERM` first and trusts its value if it denotes a specific terminal emulator (see [GH-232]). + +### Fixed + +- Correctly detect kitty started from iTerm (see [GH-230] and [GH-232]). [GH-229]: https://github.com/swsnr/mdcat/pull/229 +[GH-230]: https://github.com/swsnr/mdcat/pull/230 +[GH-232]: https://github.com/swsnr/mdcat/pull/232 ## [0.30.3] – 2022-12-01 diff --git a/Cargo.lock b/Cargo.lock index beb956d0..afb935d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,7 @@ dependencies = [ "reqwest", "shell-words", "syntect", + "temp-env", "terminal_size", "tracing", "tracing-subscriber", @@ -1534,6 +1535,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "temp-env" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30d48359f77fbb6af3d7b928cc2d092e1dc90b44f397e979ef08ae15733ed65" +dependencies = [ + "once_cell", +] + [[package]] name = "tempfile" version = "3.3.0" diff --git a/Cargo.toml b/Cargo.toml index d9ae7c8c..af421d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ terminal_size = "0.2.3" pretty_assertions = "1.3.0" lazy_static = "1.4.0" glob = "0.3.0" +temp-env = "0.3.1" [build-dependencies] # To generate completions during build diff --git a/mdcat.1.adoc b/mdcat.1.adoc index dc165d4d..47806df9 100644 --- a/mdcat.1.adoc +++ b/mdcat.1.adoc @@ -25,9 +25,21 @@ If invoked as `mdless` automatically use a pager to display the output, see belo === CommonMark and terminal support -mdcat supports all basic CommonMark syntax plus a few extensions, highlights syntax in code blocks, and shows inline links and even inline images in some terminal emulators. +mdcat supports all basic CommonMark syntax plus a few extensions, highlights syntax in code blocks, and shows inline links and even inline images in some terminal programs. In iTerm2 it also adds jump marks for section headings. +See <> below for a list of supported terminal programs and their features. + +=== Terminal detection + +To enable formatting extensions such sa inline images, `mdcat` needs to detect the terminal program, by checking the following environment variables in the gven order: + +1. `$TERM` +2. `$TERM_PROGRAM` +3. `$TERMINOLOGY` + +See <> below for a detailed description of each environment variable. + === Pagination mdcat can render output in a pager; this is the default when run as `mdless`. @@ -109,15 +121,29 @@ If run as `mdless` or if `--paginate` is given and the pager fails to start mdca == Environment TERM:: - If this variable is `wezterm`, mdcat assumes that the terminal is WezTerm. By default WezTerm does not set TERM to wezterm but mdcat can still detect it trough TERM_PROGRAM. - If this variable is `xterm-kitty`, mdcat assumes that the terminal is Kitty. + + `mdcat` first checks this variable to identify the terminal program (see <>). +It understands the following values. ++ + * `wezterm`: WezTerm. Note that WezTerm sets `$TERM` to `xterm-256color` by default, and only uses `wezterm` for `$TERM` if explicitly configured to do so. + * `xterm-kitty`: Kitty ++ +For all other values `mdcat` proceeds to check `$TERM_PROGRAM`. TERM_PROGRAM:: - If this variable is `iTerm.app`, mdcat assumes that the terminal is iTerm2. - If this variable is `WezTerm`, mdcat assumes that the terminal is WezTerm. + + If `$TERM` does not conclusively identify the terminal program `mdcat` checks this variable next. It understands the following values: ++ + * `iTerm.app`: iTerm2 + * `WezTerm`: WezTerm ++ +For all other values `mdcat` proceeds to check `$TERMINOLOGY`. TERMINOLOGY:: + If this variable is `1`, mdcat assumes that the terminal is Terminology. ++ +Otherwise `mdcat` ends terminal detection and assumes that the terminal is only capable of standard ANSI formatting. COLUMNS:: The number of character columns on screen. @@ -183,7 +209,7 @@ Unless `--no-colour` is given, mdcat translates CommonMark text into ANSI format It uses bold (SGR 1), italic (SGR 3) and strikethrough (SGR 9) formatting, and the standard 4-bit color sequences, as well as https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda[OSC 8] for hyperlinks. It does not use 8-bit or 24-bit color sequences, though this may change in future releases. -Additionally, it uses proprietary escape code if it detects specific terminal emulators: +Additionally, it uses proprietary escape code if it detects one of the following terminal emulators (see <> and <> for details): * https://iterm2.com/[iTerm2]: Inline images (https://iterm2.com/documentation-images.html[iTerm2 protocol]) and https://iterm2.com/documentation-escape-codes.html[Marks]. diff --git a/src/bin/mdcat/main.rs b/src/bin/mdcat/main.rs index 1d1e636f..dceee40c 100644 --- a/src/bin/mdcat/main.rs +++ b/src/bin/mdcat/main.rs @@ -21,8 +21,9 @@ use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::EnvFilter; use crate::output::Output; +use mdcat::terminal::{TerminalProgram, TerminalSize}; +use mdcat::ResourceAccess; use mdcat::{Environment, Settings}; -use mdcat::{ResourceAccess, TerminalCapabilities, TerminalSize}; mod args; mod output; @@ -102,21 +103,17 @@ fn main() { let args = Args::parse().command; event!(target: "mdcat::main", Level::TRACE, ?args, "mdcat arguments"); - let terminal_capabilities = if args.no_colour { - // If the user disabled colours assume a dumb terminal - TerminalCapabilities::none() + let terminal = if args.no_colour { + TerminalProgram::Dumb } else if args.paginate() || args.ansi_only { // A pager won't support any terminal-specific features - TerminalCapabilities::ansi() + TerminalProgram::Ansi } else { - TerminalCapabilities::detect() + TerminalProgram::detect() }; - let size = TerminalSize::detect().unwrap_or_default(); - let columns = args.columns.unwrap_or(size.columns); - if args.detect_only { - println!("Terminal: {}", terminal_capabilities.name); + println!("Terminal: {terminal}"); } else { // On Windows 10 we need to enable ANSI term explicitly. #[cfg(windows)] @@ -125,10 +122,13 @@ fn main() { ansi_term::enable_ansi_support().ok(); } + let size = TerminalSize::detect().unwrap_or_default(); + let columns = args.columns.unwrap_or(size.columns); + let exit_code = match Output::new(args.paginate()) { Ok(mut output) => { let settings = Settings { - terminal_capabilities, + terminal_capabilities: terminal.capabilities(), terminal_size: TerminalSize { columns, ..size }, resource_access: if args.local_only { ResourceAccess::LocalOnly diff --git a/src/lib.rs b/src/lib.rs index 3e5faa56..4df8b794 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,14 +18,15 @@ use tracing::instrument; // Expose some select things for use in main pub use crate::resources::ResourceAccess; -pub use crate::terminal::*; +use crate::terminal::capabilities::TerminalCapabilities; +use crate::terminal::TerminalSize; use url::Url; mod magic; mod references; mod resources; mod svg; -mod terminal; +pub mod terminal; mod render; @@ -143,6 +144,7 @@ mod tests { use pretty_assertions::assert_eq; use syntect::parsing::SyntaxSet; + use crate::terminal::TerminalProgram; use crate::*; use super::render_string; @@ -153,7 +155,7 @@ mod tests { &Settings { resource_access: ResourceAccess::LocalOnly, syntax_set: SyntaxSet::default(), - terminal_capabilities: TerminalCapabilities::none(), + terminal_capabilities: TerminalProgram::Dumb.capabilities(), terminal_size: TerminalSize::default(), }, ) @@ -286,6 +288,7 @@ Hello Donald[2] use pretty_assertions::assert_eq; use syntect::parsing::SyntaxSet; + use crate::terminal::TerminalProgram; use crate::*; use super::render_string; @@ -296,7 +299,7 @@ Hello Donald[2] &Settings { resource_access: ResourceAccess::LocalOnly, syntax_set: SyntaxSet::default(), - terminal_capabilities: TerminalCapabilities::none(), + terminal_capabilities: TerminalProgram::Dumb.capabilities(), terminal_size: TerminalSize::default(), }, ) diff --git a/src/render.rs b/src/render.rs index 4061d30a..924c57c6 100644 --- a/src/render.rs +++ b/src/render.rs @@ -31,6 +31,7 @@ use state::*; use write::*; use crate::render::state::MarginControl::{Margin, NoMargin}; +use crate::terminal::capabilities::LinkCapability; pub use data::StateData; pub use state::State; pub use state::StateAndData; @@ -607,7 +608,7 @@ pub fn write_event<'a, W: Write>( // Images (Stacked(stack, Inline(state, attrs)), Start(Image(_, link, _))) => { let InlineAttrs { style, indent } = attrs; - use ImageCapability::*; + use crate::terminal::capabilities::ImageCapability::*; let resolved_link = environment.resolve_reference(&link); let image_state = match (settings.terminal_capabilities.image, resolved_link) { (Some(Terminology(terminology)), Some(ref url)) => { diff --git a/src/render/state.rs b/src/render/state.rs index 47db4ef0..2e1056c1 100644 --- a/src/render/state.rs +++ b/src/render/state.rs @@ -4,7 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use crate::{AnsiStyle, LinkCapability}; +use crate::terminal::capabilities::LinkCapability; +use crate::terminal::AnsiStyle; use ansi_term::Style; use std::borrow::Borrow; use syntect::highlighting::HighlightState; diff --git a/src/render/write.rs b/src/render/write.rs index 32af3abb..dfd5baee 100644 --- a/src/render/write.rs +++ b/src/render/write.rs @@ -14,9 +14,9 @@ use syntect::parsing::{ParseState, ScopeStack}; use crate::references::*; use crate::render::data::LinkReferenceDefinition; use crate::render::state::*; -use crate::{ - Environment, MarkCapability, Settings, StyleCapability, TerminalCapabilities, TerminalSize, -}; +use crate::terminal::capabilities::{MarkCapability, StyleCapability, TerminalCapabilities}; +use crate::terminal::TerminalSize; +use crate::{Environment, Settings}; #[inline] pub fn write_indent(writer: &mut W, level: u16) -> Result<()> { @@ -86,7 +86,7 @@ pub fn write_link_refs( // clickable. This mostly helps images inside inline links which we had to write as // reference links because we can't nest inline links. if let Some(url) = environment.resolve_reference(&link.target) { - use crate::LinkCapability::*; + use crate::terminal::capabilities::LinkCapability::*; match &capabilities.links { Some(Osc8(links)) => { links.set_link_url(writer, url, &environment.hostname)?; diff --git a/src/terminal.rs b/src/terminal.rs index d9332759..0a433674 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -10,158 +10,12 @@ mod ansi; pub mod highlighting; +mod osc; mod size; -mod iterm2; -mod kitty; -mod osc; -mod terminology; +pub mod capabilities; +mod detect; pub use self::ansi::AnsiStyle; +pub use self::detect::TerminalProgram; pub use self::size::TerminalSize; - -/// The capability of basic styling. -#[derive(Debug, Copy, Clone)] -pub enum StyleCapability { - /// The terminal supports ANSI styles. - Ansi(AnsiStyle), -} - -/// How the terminal supports inline links. -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum LinkCapability { - /// The terminal supports [OSC 8] inline links. - /// - /// [OSC 8]: https://git.io/vd4ee - Osc8(self::osc::Osc8Links), -} - -/// The capability of the terminal to set marks. -#[derive(Debug, Copy, Clone)] -pub enum MarkCapability { - /// The terminal supports iTerm2 jump marks. - ITerm2(self::iterm2::ITerm2Marks), -} - -/// The capability of the terminal to write images inline. -#[derive(Debug, Copy, Clone)] -pub enum ImageCapability { - /// The terminal understands the terminology way of inline images. - Terminology(self::terminology::TerminologyImages), - /// The terminal understands the iterm2 way of inline images. - ITerm2(self::iterm2::ITerm2Images), - /// The terminal understands the Kitty way of inline images. - Kitty(self::kitty::KittyImages), -} - -/// The capabilities of a terminal. -#[derive(Debug)] -pub struct TerminalCapabilities { - /// How do we call this terminal? - pub name: String, - /// How the terminal supports basic styling. - pub style: Option, - /// How the terminal supports links. - pub links: Option, - /// How the terminal supports images. - pub image: Option, - /// How the terminal supports marks. - pub marks: Option, -} - -/// Checks if the current terminal is WezTerm. -fn is_wezterm() -> bool { - std::env::var("TERM_PROGRAM").map_or(false, |value| value == "WezTerm") - || std::env::var("TERM").map_or(false, |value| value == "wezterm") -} - -impl TerminalCapabilities { - /// A terminal which supports nothing. - pub fn none() -> TerminalCapabilities { - TerminalCapabilities { - name: "dumb".to_string(), - style: None, - links: None, - image: None, - marks: None, - } - } - - /// A terminal with basic ANSI formatting only. - pub fn ansi() -> TerminalCapabilities { - TerminalCapabilities { - name: "Ansi".to_string(), - style: Some(StyleCapability::Ansi(AnsiStyle)), - links: Some(LinkCapability::Osc8(self::osc::Osc8Links)), - image: None, - marks: None, - } - } - - /// Terminal capabilities of iTerm2. - pub fn iterm2() -> TerminalCapabilities { - TerminalCapabilities { - name: "iTerm2".to_string(), - style: Some(StyleCapability::Ansi(AnsiStyle)), - links: Some(LinkCapability::Osc8(self::osc::Osc8Links)), - image: Some(ImageCapability::ITerm2(self::iterm2::ITerm2Images)), - marks: Some(MarkCapability::ITerm2(self::iterm2::ITerm2Marks)), - } - } - - /// Terminal capabilities of Terminology. - pub fn terminology() -> TerminalCapabilities { - TerminalCapabilities { - name: "Terminology".to_string(), - style: Some(StyleCapability::Ansi(AnsiStyle)), - links: Some(LinkCapability::Osc8(self::osc::Osc8Links)), - image: Some(ImageCapability::Terminology( - self::terminology::TerminologyImages, - )), - marks: None, - } - } - - /// Terminal capabilities of Kitty. - pub fn kitty() -> TerminalCapabilities { - TerminalCapabilities { - name: "Kitty".to_string(), - style: Some(StyleCapability::Ansi(AnsiStyle)), - links: Some(LinkCapability::Osc8(self::osc::Osc8Links)), - image: Some(ImageCapability::Kitty(self::kitty::KittyImages)), - marks: None, - } - } - - /// Terminal capabilities of WezTerm (Wez's Terminal Emulator). - /// - /// WezTerm is a GPU-accelerated cross-platform - /// terminal emulator and multiplexer written by @wez - /// and implemented in Rust. - /// - /// See for more details. - pub fn wezterm() -> TerminalCapabilities { - TerminalCapabilities { - name: "WezTerm".to_string(), - style: Some(StyleCapability::Ansi(AnsiStyle)), - links: Some(LinkCapability::Osc8(self::osc::Osc8Links)), - image: Some(ImageCapability::ITerm2(self::iterm2::ITerm2Images)), - marks: None, - } - } - - /// Detect the capabilities of the current terminal. - pub fn detect() -> TerminalCapabilities { - if self::iterm2::is_iterm2() { - Self::iterm2() - } else if self::terminology::is_terminology() { - Self::terminology() - } else if self::kitty::is_kitty() { - Self::kitty() - } else if is_wezterm() { - Self::wezterm() - } else { - Self::ansi() - } - } -} diff --git a/src/terminal/capabilities.rs b/src/terminal/capabilities.rs new file mode 100644 index 00000000..bdf8f11a --- /dev/null +++ b/src/terminal/capabilities.rs @@ -0,0 +1,84 @@ +// Copyright 2018-2020 Sebastian Wiesner + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Capabilities of terminal emulators. + +use crate::terminal::AnsiStyle; + +pub mod iterm2; +pub mod kitty; +pub mod terminology; + +/// The capability of basic styling. +#[derive(Debug, Copy, Clone)] +pub enum StyleCapability { + /// The terminal supports ANSI styles. + Ansi(AnsiStyle), +} + +/// How the terminal supports inline links. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum LinkCapability { + /// The terminal supports [OSC 8] inline links. + /// + /// [OSC 8]: https://git.io/vd4ee + Osc8(crate::terminal::osc::Osc8Links), +} + +/// The capability of the terminal to set marks. +#[derive(Debug, Copy, Clone)] +pub enum MarkCapability { + /// The terminal supports iTerm2 jump marks. + ITerm2(self::iterm2::ITerm2Marks), +} + +/// The capability of the terminal to write images inline. +#[derive(Debug, Copy, Clone)] +pub enum ImageCapability { + /// The terminal understands the terminology way of inline images. + Terminology(self::terminology::TerminologyImages), + /// The terminal understands the iterm2 way of inline images. + ITerm2(self::iterm2::ITerm2Images), + /// The terminal understands the Kitty way of inline images. + Kitty(self::kitty::KittyImages), +} + +/// The capabilities of a terminal. +#[derive(Debug)] +pub struct TerminalCapabilities { + /// How the terminal supports basic styling. + pub style: Option, + /// How the terminal supports links. + pub links: Option, + /// How the terminal supports images. + pub image: Option, + /// How the terminal supports marks. + pub marks: Option, +} + +impl Default for TerminalCapabilities { + /// A terminal which supports nothing. + fn default() -> Self { + TerminalCapabilities { + style: None, + links: None, + image: None, + marks: None, + } + } +} + +impl TerminalCapabilities { + pub(crate) fn with_image_capability(mut self, cap: ImageCapability) -> Self { + self.image = Some(cap); + self + } + + pub(crate) fn with_mark_capability(mut self, cap: MarkCapability) -> Self { + self.marks = Some(cap); + self + } +} diff --git a/src/terminal/iterm2.rs b/src/terminal/capabilities/iterm2.rs similarity index 82% rename from src/terminal/iterm2.rs rename to src/terminal/capabilities/iterm2.rs index 83c4a69c..749322ef 100644 --- a/src/terminal/iterm2.rs +++ b/src/terminal/capabilities/iterm2.rs @@ -4,28 +4,18 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -//! The iTerm2 terminal. +//! Support for specific iTerm2 features. //! -//! iTerm2 is a powerful macOS terminal emulator with many formatting -//! features, including images and inline links. -//! -//! See for more information. +//! This module provides the iTerm2 marks and the iTerm2 image protocol. -use super::osc::write_osc; use crate::resources::read_url; +use crate::terminal::osc::write_osc; use crate::{magic, ResourceAccess}; use anyhow::{Context, Result}; use std::io::{self, Write}; use url::Url; -use super::super::svg; - -/// Whether we run inside iTerm2 or not. -pub fn is_iterm2() -> bool { - std::env::var("TERM_PROGRAM") - .map(|value| value.contains("iTerm.app")) - .unwrap_or(false) -} +use crate::svg; /// Iterm2 marks. #[derive(Debug, Copy, Clone)] diff --git a/src/terminal/kitty.rs b/src/terminal/capabilities/kitty.rs similarity index 95% rename from src/terminal/kitty.rs rename to src/terminal/capabilities/kitty.rs index c8683603..fb4b3df3 100644 --- a/src/terminal/kitty.rs +++ b/src/terminal/capabilities/kitty.rs @@ -13,11 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The kitty terminal. -//! -//! kitty is a fast, featureful, GPU based terminal emulator. -//! -//! See for more information. +//! Kitty terminal extensions. use crate::resources::read_url; use crate::svg::render_svg; @@ -31,13 +27,6 @@ use std::io::Write; use std::str; use url::Url; -/// Whether we run in Kitty or not. -pub fn is_kitty() -> bool { - std::env::var("TERM") - .map(|value| value == "xterm-kitty") - .unwrap_or(false) -} - /// Provides access to printing images for kitty. #[derive(Debug, Copy, Clone)] pub struct KittyImages; diff --git a/src/terminal/terminology.rs b/src/terminal/capabilities/terminology.rs similarity index 95% rename from src/terminal/terminology.rs rename to src/terminal/capabilities/terminology.rs index 026cf952..4545565f 100644 --- a/src/terminal/terminology.rs +++ b/src/terminal/capabilities/terminology.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! [Terminology][] specific functions. +//! Support for terminology extensions. //! -//! [Terminology]: http://terminolo.gy +//! This module implements the terminology image protocol. -use super::TerminalSize; +use crate::terminal::TerminalSize; use std::io::{Result, Write}; use url::Url; diff --git a/src/terminal/detect.rs b/src/terminal/detect.rs new file mode 100644 index 00000000..046b8fee --- /dev/null +++ b/src/terminal/detect.rs @@ -0,0 +1,225 @@ +// Copyright 2018-2020 Sebastian Wiesner + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Detect the terminal application mdcat is running on. + +use crate::terminal::capabilities::iterm2::{ITerm2Images, ITerm2Marks}; +use crate::terminal::capabilities::*; +use crate::terminal::AnsiStyle; +use std::fmt::{Display, Formatter}; + +/// A terminal application. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum TerminalProgram { + /// A dumb terminal which does not support any formatting. + Dumb, + /// A plain ANSI terminal which supports only standard ANSI formatting. + Ansi, + /// iTerm2. + /// + /// iTerm2 is a powerful macOS terminal emulator with many formatting features, including images + /// and inline links. + /// + /// See for more information. + ITerm2, + /// Terminology. + /// + /// See for more information. + Terminology, + /// Kitty. + /// + /// kitty is a fast, featureful, GPU-based terminal emulator with a lot of extensions to the + /// terminal protocol. + /// + /// See for more information. + Kitty, + /// WezTerm + /// + /// WezTerm is a GPU-accelerated cross-platform terminal emulator and multiplexer. It's highly + /// customizable and supports some terminal extensions. + /// + /// See for more information. + WezTerm, +} + +impl Display for TerminalProgram { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let name = match *self { + TerminalProgram::Dumb => "dumb", + TerminalProgram::Ansi => "ansi", + TerminalProgram::ITerm2 => "iTerm2", + TerminalProgram::Terminology => "Terminology", + TerminalProgram::Kitty => "kitty", + TerminalProgram::WezTerm => "WezTerm", + }; + write!(f, "{name}") + } +} + +impl TerminalProgram { + fn detect_term() -> Option { + match std::env::var("TERM").ok().as_deref() { + Some("wezterm") => Some(Self::WezTerm), + Some("xterm-kitty") => Some(Self::Kitty), + _ => None, + } + } + + fn detect_term_program() -> Option { + match std::env::var("TERM_PROGRAM").ok().as_deref() { + Some("WezTerm") => Some(Self::WezTerm), + Some("iTerm.app") => Some(Self::ITerm2), + _ => None, + } + } + + /// Attempt to detect the terminal program mdcat is running on. + /// + /// This function looks at various environment variables to identify the terminal program. + /// + /// It first looks at `$TERM` to determine the terminal program, then at `$TERM_PROGRAM`, and + /// finally at `$TERMINOLOGY`. + /// + /// If `$TERM` is set to anything other than `xterm-256colors` it's definitely accurate, since + /// it points to the terminfo entry to use. `$TERM` also propagates across most boundaries + /// (e.g. `sudo`, `ssh`), and thus the most reliable place to check. + /// + /// However, `$TERM` only works if the terminal has a dedicated entry in terminfo database. Many + /// terminal programs avoid this complexity (even WezTerm only sets `$TERM` if explicitly + /// configured to do so), so `mdcat` proceeds to look at other variables. However, these are + /// generally not as reliable as `$TERM`, because they often do not propagate across SSH or + /// sudo, and may leak if one terminal program is started from another one. + /// + /// # Returns + /// + /// - [`TerminalProgram::Kitty`] if `$TERM` is `xterm-kitty`. + /// - [`TerminalProgram::WezTerm`] if `$TERM` is `wezterm`. + /// - [`TerminalProgram::WezTerm`] if `$TERM_PROGRAM` is `WezTerm`. + /// - [`TerminalProgram::ITerm2`] if `$TERM_PROGRAM` is `iTerm.app`. + /// - [`TerminalProgram::Terminology`] if `$TERMINOLOGY` is `1`. + /// - [`TerminalProgram::Ansi`] otherwise. + pub fn detect() -> Self { + Self::detect_term() + .or_else(Self::detect_term_program) + .or_else(|| match std::env::var("TERMINOLOGY").ok().as_deref() { + Some("1") => Some(Self::Terminology), + _ => None, + }) + .unwrap_or(Self::Ansi) + } + + /// Get the capabilities of this terminal emulator. + pub fn capabilities(self) -> TerminalCapabilities { + let ansi = TerminalCapabilities { + style: Some(StyleCapability::Ansi(AnsiStyle)), + links: Some(LinkCapability::Osc8(crate::terminal::osc::Osc8Links)), + image: None, + marks: None, + }; + match self { + TerminalProgram::Dumb => TerminalCapabilities::default(), + TerminalProgram::Ansi => ansi, + TerminalProgram::ITerm2 => ansi + .with_mark_capability(MarkCapability::ITerm2(ITerm2Marks)) + .with_image_capability(ImageCapability::ITerm2(ITerm2Images)), + TerminalProgram::Terminology => ansi.with_image_capability( + ImageCapability::Terminology(terminology::TerminologyImages), + ), + TerminalProgram::Kitty => { + ansi.with_image_capability(ImageCapability::Kitty(self::kitty::KittyImages)) + } + TerminalProgram::WezTerm => { + ansi.with_image_capability(ImageCapability::ITerm2(ITerm2Images)) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::terminal::TerminalProgram; + + #[test] + pub fn detect_term_kitty() { + temp_env::with_var("TERM", Some("xterm-kitty"), || { + assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty) + }) + } + + #[test] + pub fn detect_term_wezterm() { + temp_env::with_var("TERM", Some("wezterm"), || { + assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm) + }) + } + + #[test] + pub fn detect_term_program_wezterm() { + temp_env::with_vars( + vec![ + ("TERM", Some("xterm-256color")), + ("TERM_PROGRAM", Some("WezTerm")), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm), + ) + } + + #[test] + pub fn detect_term_program_iterm2() { + temp_env::with_vars( + vec![ + ("TERM", Some("xterm-256color")), + ("TERM_PROGRAM", Some("iTerm.app")), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::ITerm2), + ) + } + + #[test] + pub fn detect_terminology() { + temp_env::with_vars( + vec![ + ("TERM", Some("xterm-256color")), + ("TERM_PROGRAM", None), + ("TERMINOLOGY", Some("1")), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::Terminology), + ); + temp_env::with_vars( + vec![ + ("TERM", Some("xterm-256color")), + ("TERM_PROGRAM", None), + ("TERMINOLOGY", Some("0")), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi), + ); + } + + #[test] + pub fn detect_ansi() { + temp_env::with_vars( + vec![ + ("TERM", Some("xterm-256color")), + ("TERM_PROGRAM", None), + ("TERMINOLOGY", None), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi), + ) + } + + /// Regression test for + #[test] + #[allow(non_snake_case)] + pub fn GH_230_detect_nested_kitty_from_iterm2() { + temp_env::with_vars( + vec![ + ("TERM_PROGRAM", Some("iTerm.app")), + ("TERM", Some("xterm-kitty")), + ], + || assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty), + ) + } +} diff --git a/tests/render.rs b/tests/render.rs index 50f33ac4..2ecefca3 100644 --- a/tests/render.rs +++ b/tests/render.rs @@ -21,19 +21,20 @@ use pulldown_cmark::{Options, Parser}; use syntect::parsing::SyntaxSet; use url::Url; +use mdcat::terminal::TerminalProgram; use mdcat::Environment; lazy_static! { static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines(); static ref SETTINGS_ANSI_ONLY: mdcat::Settings = mdcat::Settings { - terminal_capabilities: mdcat::TerminalCapabilities::ansi(), - terminal_size: mdcat::TerminalSize::default(), + terminal_capabilities: TerminalProgram::Ansi.capabilities(), + terminal_size: mdcat::terminal::TerminalSize::default(), resource_access: mdcat::ResourceAccess::LocalOnly, syntax_set: (*SYNTAX_SET).clone(), }; static ref SETTINGS_ITERM2: mdcat::Settings = mdcat::Settings { - terminal_capabilities: mdcat::TerminalCapabilities::iterm2(), - terminal_size: mdcat::TerminalSize::default(), + terminal_capabilities: TerminalProgram::ITerm2.capabilities(), + terminal_size: mdcat::terminal::TerminalSize::default(), resource_access: mdcat::ResourceAccess::LocalOnly, syntax_set: (*SYNTAX_SET).clone(), }; From 48f77b33cfa876557577c2e9264c03330a1f07a5 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sat, 7 Jan 2023 11:33:36 +0100 Subject: [PATCH 2/3] Add --detect-terminal option --- CHANGELOG.md | 3 +++ mdcat.1.adoc | 5 ++++- src/bin/mdcat/args.rs | 6 +++--- src/bin/mdcat/main.rs | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5a535d..e0d23dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ To publish a new release run `scripts/release` from the project directory. ## [Unreleased] +### Added +- Add `--detect-terminal` to print the name of the detected terminal program (see [GH-232]). + ### Changed - Replace `ureq` with `reqwest` (see [GH-229]). diff --git a/mdcat.1.adoc b/mdcat.1.adoc index 47806df9..0c804db3 100644 --- a/mdcat.1.adoc +++ b/mdcat.1.adoc @@ -100,7 +100,10 @@ This is the default when run as `mdcat`. --fail:: Fail immediately at the first FILE which fails to read. - By default mdcat continues with the next file. + By default, mdcat continues with the next file. + +--detect-terminal:: + Detect the terminal program, print its name, and exit. -h:: --help:: diff --git a/src/bin/mdcat/args.rs b/src/bin/mdcat/args.rs index 2783a2c4..24836b99 100644 --- a/src/bin/mdcat/args.rs +++ b/src/bin/mdcat/args.rs @@ -98,9 +98,9 @@ pub struct CommonArgs { /// Exit immediately if any error occurs processing an input file. #[arg(long = "fail")] pub fail_fast: bool, - /// Only detect the terminal type and exit. - #[arg(long, hide = true)] - pub detect_only: bool, + /// Print detected terminal name and exit. + #[arg(long = "detect-terminal")] + pub detect_and_exit: bool, /// Limit to standard ANSI formatting. #[arg(long, conflicts_with = "no_colour", hide = true)] pub ansi_only: bool, diff --git a/src/bin/mdcat/main.rs b/src/bin/mdcat/main.rs index dceee40c..d1039007 100644 --- a/src/bin/mdcat/main.rs +++ b/src/bin/mdcat/main.rs @@ -112,7 +112,7 @@ fn main() { TerminalProgram::detect() }; - if args.detect_only { + if args.detect_and_exit { println!("Terminal: {terminal}"); } else { // On Windows 10 we need to enable ANSI term explicitly. From eedb1f736bb0835841d447dd444f09da1bd1bf3a Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sat, 7 Jan 2023 11:35:50 +0100 Subject: [PATCH 3/3] Expose --ansi flag --- CHANGELOG.md | 1 + mdcat.1.adoc | 3 +++ src/bin/mdcat/args.rs | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d23dc1..5643f0ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ To publish a new release run `scripts/release` from the project directory. ### Added - Add `--detect-terminal` to print the name of the detected terminal program (see [GH-232]). +- Add `--ansi` to skip terminal detection and use ANSI-formatting only (see [GH-232]). ### Changed diff --git a/mdcat.1.adoc b/mdcat.1.adoc index 0c804db3..c4f55b0a 100644 --- a/mdcat.1.adoc +++ b/mdcat.1.adoc @@ -90,6 +90,9 @@ This is the default when run as `mdcat`. --no-colour:: Disable all colours and other styles. +--ansi:: + Skip terminal detection and only use ANSI formatting. + --columns:: Maximum number of columns to use for text output. Defaults to the size of the underlying terminal. diff --git a/src/bin/mdcat/args.rs b/src/bin/mdcat/args.rs index 24836b99..f8a3f5b1 100644 --- a/src/bin/mdcat/args.rs +++ b/src/bin/mdcat/args.rs @@ -101,7 +101,7 @@ pub struct CommonArgs { /// Print detected terminal name and exit. #[arg(long = "detect-terminal")] pub detect_and_exit: bool, - /// Limit to standard ANSI formatting. - #[arg(long, conflicts_with = "no_colour", hide = true)] + /// Skip terminal detection and only use ANSI formatting. + #[arg(long = "ansi", conflicts_with = "no_colour")] pub ansi_only: bool, }