Actions Status MetaCPAN Release

NAME

mdee - em·dee, Markdown Easy on the Eyes

SYNOPSIS

mdee [ options ] file ...

 -h  --help             show help
     --version          show version
 -d  --debug            debug level (repeatable)
 -x  --[no-]trace       trace mode (set -x)
 -n  --dryrun           dry-run mode
 -s  --style=#          output style (nup/pager/cat/filter/raw)
 -f  --filter           shortcut for --style=filter
 -p  --plain            shortcut for --style=pager
     --[no-]fold        line folding (default: on)
 -r  --[no-]refill      join paragraph lines (default: off)
     --[no-]table       table formatting (default: on)
     --[no-]trim        trim cell spaces (default: on)
     --[no-]nup         nup paged output (default: on)
 -V  --parallel         show files side by side
     --[no-]rule        use Unicode rules for tables (default: on)
--ts --table-style=#    table style: lattice, 3line or simple (default: lattice)
     --cat-v            visualize whitespace via cat-v
 -w  --width=#          output width (default: terminal width)
     --fold-width=#     fold width (default: pane width - 3 - run-in)
     --runin=#          run-in width for line breaking (default: 4)
     --runout=#         run-out width for line breaking (default: 4)
     --runlen=#         set both run-in and run-out
     --boundary=#       fold boundary: word, space or none (default: word)
 -t  --theme=#[,#,...]  color theme(s) (default: hashed,nomark,markcell,doubleline)
 -m  --mode=#           light or dark (default: light)
 -B  --base-color=#     override base color of theme
                        (e.g., Ivory, #780043, (120,0,67))
     --config=KEY=VALUE  set config parameter (e.g., h1=RD, FILE=...)
--cm --colormap=L=SPEC  override color for element (e.g., h1=RD)
--hm --heading-markup=# enable markup in headings (all/bold/...)
     --show=#           set field visibility (e.g., italic=1)
 -C  --pane=#           number of columns
 -R  --row=#            number of rows
 -G  --grid=#           grid layout (e.g., 2x3)
 -P  --page=#           page height in lines
 -S  --pane-width=#     pane width (default: 85)
--bs --border-style=#   border style
     --[no-]page-number page number on border (default: on)
     --[no-]filename    file name label (default: on)
     --[no-]pager[=#]   pager command

VERSION

Version 1.16

DESCRIPTION

em·dee (command: mdee) is a terminal-based multi-column Markdown text viewer with syntax highlighting, particularly useful for reading documents generated by LLMs.

It presents Markdown content in a clean, readable form: syntax highlighting distinguishes document structure, line folding wraps long list items, and tables are aligned with proper column formatting. By default, markup characters (**, `, [...], etc.) are hidden so that the text reads naturally, while color and styling preserve the structural information. Closing hashes are appended to h3-h6 headers to make heading levels visually distinct. On terminals supporting OSC 8, links remain clickable.

Use --no-theme to display the original Markdown source with markup characters intact.

Concept

em·dee began as a viewer for Markdown text, not as a Markdown renderer. The idea was to show a .md file as it is written, line for line, only made easier on the eyes with color: the Markdown is not interpreted or reformatted. Some formatting has been added since because it made the text more readable: tables are aligned, long list items are folded, the default themes hide markup characters, and closing hashes are appended to lower-level headings. With --refill, the lines that pandoc has found to be one paragraph are joined as well. Everything else is still the source text. This is why backslash escapes such as \* are shown as written, and why --no-theme gives back the original file, in color.

For full Markdown rendering, many other viewers are available, such as glow and mdcat. Combine them with nup(1) for similar paged output (e.g., nup glow README.md, nup mdcat README.md).

The pipeline combines greple(1) for colorization and nup(1) for multi-column paged output.

The following elements are highlighted. Elements marked with --show can be individually disabled via the --show option. Others are always processed for structural integrity.

--show    bold             (**bold**, __bold__)
--show    italic           (*italic*, _italic_)
--show    strike           (~~strike~~)
--show    code_inline      (`code`)
--show    header, h1-h6    (# heading)
--show    horizontal_rule  (---, ***, ___)
--show    blockquote       (> quote)
always    code_mark        (``` or ~~~)
          code_block
          code_info
always    comment          (<!-- ... -->)
always    link             ([text](url))
          image            (![text])
          image_link       (![text](url))

Tables and list item folding are controlled by --table and --fold options, not by --show.

Multi-column Layout and Pagination

By default, mdee calculates the number of display columns by dividing the output width (the terminal width unless --width is given) by the pane width (default 85 characters). This determines how the output is laid out and paginated.

When two or more columns fit, nup(1) arranges output in multi-column layout with page-by-page pagination — content is split into terminal-height pages viewed through a pager.

When only one column fits, mdee still uses nup for formatting (borders, document layout) but disables page-by-page splitting, so the content scrolls continuously in the pager. This avoids wasted space from page breaks on narrow terminals while maintaining the same visual appearance.

To force page-by-page pagination even in single-column layout, specify --nup explicitly. The --pane-width (-S) option adjusts the column width used for this calculation, and --pane (-C) sets the number of columns directly. The --width (-w) option replaces the terminal width in this calculation and limits the output to that width.

With two or more files, --parallel shows the files side by side, one pane per file, instead of one after another. Each file is folded to its pane width, and the panes scroll together in the pager. --pane (-C) still sets the number of panes when given; files that do not fit in one row continue on the next.

Use -p (--style=pager) for a simpler view without nup formatting — highlighted output is piped directly through a pager. Use -f (--style=filter) to write highlighted output to stdout without a pager, suitable for piping into other commands.

INSTALLATION

Homebrew

Use tecolicom/tap:

brew tap tecolicom/tap
brew install app-mdee

CPAN

cpanm -n App::mdee

OPTIONS

General Options

Processing Options

Theme Options

em·dee supports color themes for customizing syntax highlighting. Themes define colors for various Markdown elements (headers, code blocks, bold text, etc.).

Highlight Options

Layout Options (passed to nup)

Pager Options

CONFIGURATION

User configuration is loaded from:

${XDG_CONFIG_HOME:-~/.config}/mdee/config.sh

This is a shell script that can set defaults and override colors:

# ~/.config/mdee/config.sh
default[mode]='dark'             # set default mode
default[theme]='warm'            # set default theme(s)
default[style]='pager'           # set default style
default[width]=170               # set default output width
default[fold_width]=80           # set default fold width
default[refill]=1                # join paragraph lines
default[base_color]='DarkCyan'   # set default base color

The default associative array supports the following keys:

Overriding theme colors

Config.sh can modify theme variables directly, using the same mechanism as theme files:

# Change base color for both modes
theme_light[base]='<DarkCyan>=y25'
theme_dark[base]='<DarkCyan>=y80'

# Enable md module features
md_config+=(hashed.h3=1 hashed.h4=1 hashed.h5=1 hashed.h6=1)

# Use another character for the table cell mark
md_config+=(table_cell_mark=∙)

These settings take precedence over the default themes.

Changing the base color automatically affects all derived colors (h1, h2, bold, etc.) because the md module expands ${base} references.

Use -d to dump current theme values in sourceable format.

Color specification format

Color specifications use Term::ANSIColor::Concise format. The FG/BG notation specifies foreground and background colors (e.g., L25D/${base};E means gray foreground on base-colored background). The ${base} string is expanded to the base color value after loading.

EXAMPLES

mdee README.md              # view markdown file
mdee -C2 document.md        # 2-column view
mdee -G2x2 manual.md        # 2x2 grid (4-up)
mdee -w170 file.md          # two 85-column panes on a wide terminal
mdee -V -w170 a.md b.md     # two files in 85 columns each
mdee --fold-width=60 a.md   # narrower text within the pane
mdee --no-pager file.md     # without pager
mdee --no-nup file.md       # output to stdout without nup
mdee --no-fold file.md      # disable line folding
mdee --no-table file.md     # disable table formatting
mdee -r file.md             # join paragraph lines (needs pandoc)

# Output styles
mdee -s pager file.md       # fold + table, output to pager
mdee -s cat file.md         # fold + table, output to stdout
mdee -s filter file.md      # table only, no fold/nup
mdee -s raw file.md         # highlight only

# Style shortcuts
mdee -p file.md             # same as --style=pager
cat file.md | mdee -f       # highlight stdin (filter mode)
mdee -f file.md             # highlighted output to stdout

# Override individual settings
mdee -f --fold file.md      # filter + fold
mdee -p --no-fold file.md   # pager without fold

# Theme examples
mdee --mode=dark file.md               # use dark mode
mdee --mode=light file.md              # use light mode
mdee -B Ivory file.md                  # override base color
mdee --mode=dark -B '#780043' file.md  # dark mode with burgundy
mdee --theme=warm file.md              # warm (Coral) base color
mdee --no-theme file.md                # show all markup characters

DEPENDENCIES

This command requires the following:

--refill finds the paragraphs with Markdown::Perl (pure Perl, installed with this distribution). The CommonMark module, which needs the libcmark C library (brew install cmark, apt-get install libcmark-dev) and is only recommended, is used when it is there because it is faster; pandoc 2.11.3 or later is a third choice.

IMPLEMENTATION

em·dee is implemented as a Bash script that orchestrates multiple specialized tools into a unified pipeline. The architecture follows Unix philosophy: each tool does one thing well, and they communicate through standard streams.

The overall data flow is:

Input File
    |
    v
[greple -Mmd] --- Syntax Highlighting + Table Formatting + Text Folding
    |
    v
[nup] --- Paged Output (nup style)
    |         or
[pager] --- Pager Output (pager style)
    |
    v
Terminal

Pipeline Architecture

em·dee dynamically constructs a pipeline based on enabled options. Each stage is defined as a Bash function (e.g., run_greple, run_nup). The --dryrun option prints the command line of each stage without running them, and -d prints the stage names.

Processing Stages

The pipeline consists of configurable stages. Processing options can be enabled or disabled independently using --[no-]refill, --[no-]fold, --[no-]table, and --[no-]nup options.

Syntax Highlighting

The first stage uses greple(1) with the --filter option, which sets --all --need=0 --exit=0 to run as a filter without requiring pattern arguments. Syntax highlighting is handled entirely by the -Mmd module.

Supported Markdown elements:

Code block detection follows the CommonMark specification:

Paragraph Refill

With --refill, the App::Greple::md module joins the lines of each paragraph before anything else. It asks a Markdown parser (App::Greple::md::Blocks: the CommonMark module, Markdown::Perl, or pandoc with the sourcepos extension) which lines of the text form a paragraph, a block quote, a list item or a definition, and joins those lines in the original text. A line that starts a footnote definition ([^name]:) stays on its own line. The joined lines are folded like list items.

Text Folding

Text folding is handled within the App::Greple::md module using Greple::tee to pipe matched regions through ansifold(1). It preserves ANSI escape sequences and maintains proper indentation for nested lists. List items, definition lists and the paragraphs joined by --refill are folded; code blocks, HTML comments, and tables are excluded.

Recognized list markers include *, -, 1., 1), #., and #). The # marker is Pandoc's auto-numbered list syntax.

The folding width is controlled by --fold-width option (default: the pane width minus 3 and the run-in width, 78 for a pane of 85).

Table Formatting

Table formatting is handled within the App::Greple::md module (after syntax highlighting, before output). Markdown tables are detected by the pattern ^ {0,3}(\|.+\|\n){3,} and formatted with aligned columns using ansicolumn(1) while preserving ANSI colors.

Column alignment is parsed from the separator line: :--- (left, default), :---: (center), ---: (right). The parse_separator() function extracts alignment markers and passes --table-right and --table-center options to ansicolumn(1) (requires version 1.61 or later). Colons are stripped from the separator line before further processing.

When --rule is enabled (default), ASCII pipe characters are replaced with Unicode box-drawing characters (│, ├, ┤, ┼, ─).

Output Stage

The final stage uses nup(1) to provide multi-column paged output. Layout options (--pane, --row, --grid, --page) are passed directly to nup.

Theme System

em·dee implements a theme system where themes are transformations applied to the built-in default theme. Multiple themes can be chained via --theme=NAME1,NAME2,....

Theme Structure

Color definitions are managed by the App::Greple::md module. The theme_light and theme_dark arrays contain only the base color. Theme files can modify the base color and pass configuration to the md module via md_config[]:

# theme/warm.sh — change base color
theme_light[base]='<Coral>=y25'
theme_dark[base]='<Coral>=y80'

# theme/hashed.sh — enable closing hashes
md_config+=(hashed.h3=1 hashed.h4=1 hashed.h5=1 hashed.h6=1)

The md_config[] entries are passed as config parameters to the App::Greple::md module. Color labels (h1, bold, etc.) can also be set as config parameters:

# theme file or config.sh — override color label
md_config+=(h1='L25D/R;E' bold=RD)

Base Color Expansion

The ${base} placeholder in color values is expanded by the md module. The base color is determined by --base-color option (default: RoyalBlue) with automatic luminance adjustment based on mode (=y25 for light, =y80 for dark).

Color Specifications

Colors are specified using Term::ANSIColor::Concise format. The --colormap (--cm) option maps colors to captured groups. For example, L00D/${base};E specifies gray foreground on base-colored background.

The color specification supports modifiers:

Terminal Mode Detection

em·dee uses Getopt::EX::termcolor to detect terminal background luminance. If luminance is below 50%, dark mode is automatically selected. The environment variable TERM_LUMINANCE gives the luminance without the detection (see "Terminal Luminance").

LIMITATIONS

HTML Comments

Only HTML comments starting at the beginning of a line are highlighted. Inline comments are not matched to avoid conflicts with inline code containing comment-like text (e.g., `<!-->`).

Emphasis

Emphasis patterns (bold and italic) do not span multiple lines. Multi-line emphasis text is not supported.

***bold italic*** and ___bold italic___ are supported. Other nested forms (e.g., **bold _italic_**) are not.

* and ** start and end emphasis as in CommonMark: a delimiter with a letter on one side and punctuation on the other does not count, so L*=50, a*=68 is not italic. CommonMark counts Unicode punctuation, and this rejects some emphasis in Japanese text, such as 日本語の*「強調」*です and 日本語の**強調。**です. mdee shows them as pandoc and zenn.dev do, with the markers and without emphasis, so that such text can be found before it is published.

With cjk_friendly=1 (--config cjk_friendly=1, or md_config+=(cjk_friendly=1) in config.sh), the rules of markdown-cjk-friendly are used instead: CJK punctuation does not count, and a CJK character on the other side is as good as a space, so these are emphasis. This is a proposal, not part of CommonMark; use it for text that is processed by an implementation of it.

Block Quotes

A block quote is recognized by its > markers at the start of a line, after up to three spaces. Only the markers are colored, not the quoted text. Headings, lists and code blocks inside a quote are not recognized, so they get no color of their own, and a code block in a quote does not protect its contents from inline highlighting.

A line without a marker that continues a quoted paragraph (a lazy continuation in CommonMark) is shown as an ordinary line, unless --refill joins it to the quote.

Link patterns do not span multiple lines. The link text and URL must be on the same line.

Reference-style links ([text][ref] with [ref]: url elsewhere) are not supported.

Paragraph Refill

--refill joins lines by the rules of Markdown, as pandoc reads them. Lines of verse, addresses or lyrics that rely on the line breaks without a hard line break (two spaces or a backslash at the end of the line) are joined as well, and so are the lines of a display math block between $$ lines. Do not use --refill for such documents.

Table Alignment

Column alignment assumes that cell content in the source is not pre-padded. If the source table is already formatted with aligned spacing (e.g., | c |), the embedded spaces become part of the cell content and alignment directives in the separator line will not produce the expected result. This is by design: pre-formatted tables are assumed to be correctly aligned as-is.

Links are converted to OSC 8 terminal hyperlinks for clickable URLs:

This requires terminal support. Compatible terminals include iTerm2, Kitty, WezTerm, Ghostty, and recent versions of GNOME Terminal. Apple's default Terminal.app does not support OSC 8.

When using less as pager, version 566 or later is required with -R option.

For OSC 8 specification, see: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda

Terminal Luminance

Without --mode or default[mode], mdee asks the terminal for its background color (OSC 11) through Getopt::EX::termcolor to choose between light and dark mode. Set TERM_LUMINANCE to a value from 0 to 100 to give the answer yourself and skip the query, for example in scripts or tests, where a reply that arrives late would show up as terminal input.

Less Environment Variables

When less is used as pager (either directly via --style=pager or through nup), the following environment variables affect behavior. mdee sets defaults for these when they are not already defined:

If you already have these variables set in your environment, mdee does not override them.

SEE ALSO

AUTHOR

Kazumasa Utashiro

LICENSE

Copyright 2026 Kazumasa Utashiro.

This software is released under the MIT License. https://opensource.org/licenses/MIT