<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Frank Chan</title>
    <description>The latest articles on Forem by Frank Chan (@dunkinfrunkin).</description>
    <link>https://forem.com/dunkinfrunkin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3808691%2Fd93228ed-3b40-4c84-8ef6-49a3fedd93b7.png</url>
      <title>Forem: Frank Chan</title>
      <link>https://forem.com/dunkinfrunkin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dunkinfrunkin"/>
    <language>en</language>
    <item>
      <title>I built a markdown pager for the terminal because I live in the CLI and nothing else worked</title>
      <dc:creator>Frank Chan</dc:creator>
      <pubDate>Fri, 06 Mar 2026 18:48:19 +0000</pubDate>
      <link>https://forem.com/dunkinfrunkin/i-built-a-markdown-pager-for-the-terminal-because-i-live-in-the-cli-and-nothing-else-worked-h95</link>
      <guid>https://forem.com/dunkinfrunkin/i-built-a-markdown-pager-for-the-terminal-because-i-live-in-the-cli-and-nothing-else-worked-h95</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5engx6bo9203kwbbz9bh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5engx6bo9203kwbbz9bh.gif" alt="Demo" width="760" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First post here (new user as well). Figured I'd share something I built instead of lurking forever.&lt;/p&gt;

&lt;p&gt;I've been leaning hard into AI for my dev workflow this year. Claude, Cursor, agentic coding, the whole thing. And the more I use AI the more I'm in the terminal. Running agents, reading output, checking docs, piping things around. My entire day is basically a terminal now.&lt;/p&gt;

&lt;p&gt;And you know what sucks in the terminal? Reading markdown.&lt;/p&gt;

&lt;p&gt;I cat a README and get a wall of ### and ** and raw link syntax. I've done this hundreds of times. Sometimes I'd open a browser tab just to read one file, which felt ridiculous when I'm already staring at a terminal.&lt;/p&gt;

&lt;p&gt;I tried the obvious stuff. bat is great for source code but it doesn't render markdown, it just syntax-highlights the raw source. glow actually renders it, but it's not a pager. No search, no page-up/page-down, no mouse. less works but then you're reading plain text.&lt;/p&gt;

&lt;p&gt;So I built my own thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  mdcat
&lt;/h2&gt;

&lt;p&gt;It's a TUI pager. You give it a markdown file, it renders it in your terminal with actual colors and formatting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @dunkinfrunkin/mdcat
mdcat README.md &lt;span class="c"&gt;# for terminal&lt;/span&gt;
mdcat &lt;span class="nt"&gt;--web&lt;/span&gt; README.md &lt;span class="c"&gt;# for web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Dark color scheme across headings, code blocks, blockquotes, tables, links&lt;/li&gt;
&lt;li&gt;Syntax highlighting inside fenced code blocks (not just the markdown source, the actual language inside)&lt;/li&gt;
&lt;li&gt;Incremental search: &lt;code&gt;/&lt;/code&gt; to open, &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;N&lt;/code&gt; to cycle, gutter markers show all matches on screen. Stole this straight from vim&lt;/li&gt;
&lt;li&gt;Mouse scroll. Toggle it off with &lt;code&gt;M&lt;/code&gt; when you need to select text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;y&lt;/code&gt; copies the visible page to clipboard&lt;/li&gt;
&lt;li&gt;Clickable links if your terminal supports OSC 8 (iTerm2, kitty, WezTerm, Ghostty)&lt;/li&gt;
&lt;li&gt;Pipes work: &lt;code&gt;curl -s url | mdcat&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--web&lt;/code&gt; flag if you actually do want a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;This is the part I'm honestly most proud of. The whole thing is raw ANSI escape codes. No blessed, no ink, no terminal UI framework. Nothing.&lt;/p&gt;

&lt;p&gt;The TUI (alternate screen, draw loop, keyboard input, mouse events via SGR mode) is about 400 lines of vanilla Node. The renderer walks markdown tokens and maps them to ANSI sequences against a hardcoded One Dark palette.&lt;/p&gt;

&lt;p&gt;Some of the gnarlier bits if you're into this kind of thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mouse&lt;/strong&gt;: SGR extended mouse mode (&lt;code&gt;\x1B[?1000h\x1B[?1006h&lt;/code&gt;). Scroll events come in as &lt;code&gt;\x1B[&amp;lt;64;x;yM&lt;/code&gt; (down) and &lt;code&gt;\x1B[&amp;lt;65;x;yM&lt;/code&gt; (up). I spent way too long debugging this before realizing the regex needs to match uppercase &lt;code&gt;M&lt;/code&gt; (press) not lowercase &lt;code&gt;m&lt;/code&gt; (release). Felt dumb.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clipboard&lt;/strong&gt;: Tries OSC 52 first (&lt;code&gt;\x1B]52;c;base64\x1B\\&lt;/code&gt;) which works over SSH and in most modern terminals. Falls back to &lt;code&gt;pbcopy&lt;/code&gt; on macOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt;: ANSI-strips each line before matching, stores hit line numbers in a Set for O(1) lookup during draw. Gutter is 2 chars: &lt;code&gt;▶&lt;/code&gt; for current match, &lt;code&gt;›&lt;/code&gt; for other matches on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;Terminals are way weirder than I expected.&lt;/p&gt;

&lt;p&gt;The ANSI-aware string truncation function (&lt;code&gt;vtrunc&lt;/code&gt;) was a nightmare. It has to handle multi-byte characters, nested escape sequences, and still calculate visible width correctly or the whole layout breaks. The first version had off-by-one errors everywhere in the table renderer. I'm talking columns shifting by one character depending on whether a cell had bold text or not.&lt;/p&gt;

&lt;p&gt;Also learned the hard way: the default npm bundled with Node 20 doesn't support OIDC for scoped packages. Had to use npm 11+ to publish. Not a terminal issue but it cost me an hour of confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @dunkinfrunkin/mdcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Site: &lt;a href="https://mdcat.frankchan.dev/" rel="noopener noreferrer"&gt;https://mdcat.frankchan.dev/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/dunkinfrunkin/mdcat" rel="noopener noreferrer"&gt;https://github.com/dunkinfrunkin/mdcat&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my first open source release. It is a blessing now, especially when I'm deep in an agentic coding session and need to quickly check a README or pipe some markdown output through something readable.&lt;/p&gt;

&lt;p&gt;What's your terminal workflow look like these days? Curious if anyone else has gone full CLI-brain from using AI tools.&lt;/p&gt;




&lt;p&gt;If you want to follow along with what I'm building, more cool stuff to come:&lt;br&gt;
&lt;a href="https://github.com/dunkinfrunkin" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://x.com/dunkinfrunkin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/dunkinfrunkin/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>productivity</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
