Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.11.0 — 2026-10-03

### Added
- **`terminal` theme: use your terminal's colours.** A new built-in theme drawn only with the terminal's 16 palette colours and its default foreground, with no background of its own and code highlighted from the palette as well — so ink matches your terminal scheme on light and dark backgrounds without switching themes. Custom themes can do the same: any color may be `ansi:<name>` (`ansi:blue`, `ansi:bright-black`, `ansi:0`–`ansi:15`) or `default`, and `code_theme = "ansi"` highlights code with the palette. (#29)

### Fixed
- **Pie chart labels longer than 12 columns no longer push their bar out of line.** The label column now fits the widest label, measured in display columns, and long labels are cut with `…` only when the chart is too narrow. (#30)

## 0.10.0 — 2026-10-01

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ink-md"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
# Highest rust-version declared by any resolved dependency (quantette, via
# ratatui-image). CI's msrv job builds with exactly this toolchain.
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | sh
The installer picks the right binary for your OS, CPU and libc (static musl build on Alpine, Raspberry Pi and older-glibc systems), checks it against `SHA256SUMS` and installs to `/usr/local/bin` (using `sudo` only if it has to). Pin a version with `INK_VERSION=v0.8.0` or change the target with `INK_INSTALL_DIR="$HOME/.local/bin"`:

```bash
curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | INK_VERSION=v0.10.0 INK_INSTALL_DIR="$HOME/.local/bin" sh
curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | INK_VERSION=v0.11.0 INK_INSTALL_DIR="$HOME/.local/bin" sh
```

### Quick install (Windows PowerShell)
Expand All @@ -38,7 +38,7 @@ curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | INK_V
irm https://raw.githubusercontent.com/borghei/ink/main/install.ps1 | iex
```

Installs `ink.exe` (x64 or ARM64) to `%LOCALAPPDATA%\Programs\ink` and adds it to your user `PATH`. Set `$env:INK_VERSION = "v0.10.0"` first to pin a version.
Installs `ink.exe` (x64 or ARM64) to `%LOCALAPPDATA%\Programs\ink` and adds it to your user `PATH`. Set `$env:INK_VERSION = "v0.11.0"` first to pin a version.

### Homebrew (macOS / Linux)

Expand Down Expand Up @@ -237,9 +237,11 @@ Press `t` to toggle a sidebar showing every heading in the document. Tracks your

Press `o` to move into it (it opens if it was closed). `j`/`k` or the arrows move, `g`/`G` go to the first and last heading, `Ctrl+d`/`Ctrl+u` page, and `Enter` jumps there — `[` brings you back. `h`/`l` fold and unfold a section's subheadings in the sidebar, and `/` filters the list as you type (every word you type must appear, case doesn't matter). `Esc` clears the filter; `Esc` again, or `o`, goes back to the document without moving. With the mouse, click a heading to jump to it; the wheel over the sidebar scrolls the sidebar, not the page.

### 8 built-in themes
### 9 built-in themes

Dark, Light, Dracula, Catppuccin, Nord, Tokyo Night, Gruvbox, and Solarized. Press `T` to open the theme picker and preview each one live.
Dark, Light, Dracula, Catppuccin, Nord, Tokyo Night, Gruvbox, Solarized, and Terminal. Press `T` to open the theme picker and preview each one live.

`terminal` uses your terminal's own colour scheme instead of fixed colours — the 16 palette colours and the default foreground, no background of its own, and code highlighted with the palette too (like `bat --theme=ansi`). It looks right on light and dark terminals without switching themes.

With no theme set, ink asks the terminal for its background colour (an OSC 11 query, answered by nearly every modern terminal and passed through by tmux) and picks Light or Dark to match, falling back to `COLORFGBG` and then Dark. An explicit `--theme` or `theme =` in the config always wins, and never triggers the query.

Expand Down Expand Up @@ -357,7 +359,7 @@ ink reads `$XDG_CONFIG_HOME/ink/config.toml` when `XDG_CONFIG_HOME` is set, othe
Create the file:

```toml
# Default theme (dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized)
# Default theme (dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized, terminal)
theme = "catppuccin"

# Max rendering width in columns
Expand Down Expand Up @@ -403,6 +405,8 @@ ink --theme mytheme README.md

Every color is customizable — headings, code, links, blockquotes, admonitions, status bar, and more. Check any built-in theme in `src/theme/builtin.rs` for the full list of color keys.

A color is a hex value (`"#7aa2f7"`), one of your terminal's palette colours (`"ansi:blue"`, `"ansi:bright-black"`, or `"ansi:0"` through `"ansi:15"`), or `"default"` for the terminal's own foreground/background. Set `code_theme = "ansi"` to highlight code with the palette.

## Shell integration

```bash
Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,8 @@ <h3>Mermaid diagrams</h3>
<p>Flowcharts, state, class and ER diagrams laid out as boxes and arrows, plus sequence, pie, Gantt and mindmaps.</p>
</div>
<div class="feature">
<h3>8 themes</h3>
<p>Dark, Light, Dracula, Catppuccin, Nord, Tokyo Night, Gruvbox, Solarized. Or make your own.</p>
<h3>9 themes</h3>
<p>Dark, Light, Dracula, Catppuccin, Nord, Tokyo Night, Gruvbox, Solarized, and Terminal, which follows your terminal's own palette. Or make your own.</p>
</div>
<div class="feature">
<h3>File browser</h3>
Expand Down
4 changes: 2 additions & 2 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ ink is a feature-rich terminal markdown reader built in Rust. It renders markdow
- Browser opens a file then exits by default; press Shift+B to reopen, or set behavior.browser_loop = true to return automatically

### Themes
- 8 built-in themes: dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized
- 9 built-in themes: dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized, terminal (follows the terminal's own 16-colour palette)
- Auto-detection of terminal background (dark/light) by querying the terminal (OSC 11), falling back to COLORFGBG
- 24-bit, 256-colour and 16-colour output chosen from the terminal; --ascii (or [behavior] ascii = true) draws borders, bullets and markers in plain ASCII, automatic on the Linux console and non-UTF-8 locales
- --no-mouse leaves the mouse to the terminal for one session
- Live theme preview with T key; the chosen theme is saved to config
- ink --list-themes lists all available themes (built-in + custom)
- 30+ customizable color properties per theme
- 30+ customizable color properties per theme; a color is hex, an ANSI palette slot (ansi:blue, ansi:bright-black, ansi:0–15) or default
- Custom themes via TOML files in the themes/ folder of the ink config directory ($XDG_CONFIG_HOME/ink/themes/ when set)
- Honors NO_COLOR and quantizes 24-bit colors to the 256-color palette on non-truecolor terminals (truecolor detected via COLORTERM, WT_SESSION, or TERM_PROGRAM vscode/ghostty/WezTerm/iTerm.app)

Expand Down
4 changes: 2 additions & 2 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ink is a command-line tool that renders markdown files in the terminal with rich
- Interactive TUI with multi-tab support, in-document search (n/N to cycle matches), keyboard link opening (press f), a help overlay (?), and a table of contents sidebar
- Text selection and clipboard copy: v/V for character- and line-wise visual selection (y copies), mouse drag with double-click word and triple-click line, c to copy a code block's raw source, Y to copy the current section's markdown; delivered over OSC 52 (works across SSH/tmux) and a native helper (pbcopy/wl-copy/xclip/xsel/clip.exe)
- File browser mode for exploring directories of markdown files
- 8 built-in color themes with auto dark/light detection, live theme picker that saves to config, plus custom TOML themes
- 9 built-in color themes (including terminal, which follows your terminal's palette) with auto dark/light detection, live theme picker that saves to config, plus custom TOML themes
- Presentation mode (split on --- separators), navigate with arrows/space
- Watch mode for live-reloading on file changes
- Pager mode: `ink --plain` on a TTY pages long output through $PAGER (like bat); --line-range START:END renders only those source lines
Expand All @@ -39,7 +39,7 @@ ink is a command-line tool that renders markdown files in the terminal with rich
curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | sh # macOS / Linux (picks glibc or static musl build)
irm https://raw.githubusercontent.com/borghei/ink/main/install.ps1 | iex # Windows PowerShell (x64 / ARM64)
cargo install ink-md # from source (Rust 1.90+)
INK_VERSION=v0.10.0 sh install.sh # pin a version
INK_VERSION=v0.11.0 sh install.sh # pin a version

Prebuilt: Linux amd64/arm64 (glibc 2.35+), static musl Linux amd64/arm64/armv7 (any distro), macOS amd64/arm64, Windows amd64/arm64; .deb/.rpm and tar.gz/zip archives include the man page and completions. Verify with SHA256SUMS or `gh attestation verify <file> --repo borghei/ink`.

Expand Down
2 changes: 1 addition & 1 deletion install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# irm https://raw.githubusercontent.com/borghei/ink/main/install.ps1 | iex
#
# Environment:
# $env:INK_VERSION = "v0.10.0" install a specific release (default: latest)
# $env:INK_VERSION = "v0.11.0" install a specific release (default: latest)
# $env:INK_INSTALL_DIR = "C:\tools" install somewhere else
# (default: %LOCALAPPDATA%\Programs\ink)
#
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | sh
#
# Environment:
# INK_VERSION=v0.10.0 install a specific release (default: latest)
# INK_VERSION=v0.11.0 install a specific release (default: latest)
# INK_INSTALL_DIR="$HOME/.local/bin" install somewhere else (default: /usr/local/bin)
#
# Options:
Expand Down
1 change: 1 addition & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const THEME_LIST: &[&str] = &[
"tokyo-night",
"gruvbox",
"solarized",
"terminal",
];

/// How the document viewer exited.
Expand Down
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ fn config_init(force: bool) -> Result<()> {
pub(crate) const STARTER_CONFIG: &str = r#"# ink configuration
# https://github.com/borghei/ink

# Color theme: dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized
# Color theme: dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized, terminal
# theme = "catppuccin"

# Max rendering width in columns (or use --width on the CLI)
Expand Down
101 changes: 99 additions & 2 deletions src/highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,113 @@
//! every startup, resize, theme-picker keystroke, and watch reload. Loading
//! them once behind a `OnceLock` removes that cost from every rebuild.

use std::str::FromStr;
use std::sync::OnceLock;
use syntect::highlighting::ThemeSet;
use syntect::highlighting::{
Color, FontStyle, ScopeSelectors, StyleModifier, Theme, ThemeItem, ThemeSet, ThemeSettings,
};
use syntect::parsing::SyntaxSet;

/// Name of the code theme drawn in the terminal's own 16-colour palette.
pub const ANSI_CODE_THEME: &str = "ansi";

pub fn syntax_set() -> &'static SyntaxSet {
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
}

pub fn theme_set() -> &'static ThemeSet {
static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
THEME_SET.get_or_init(ThemeSet::load_defaults)
THEME_SET.get_or_init(|| {
let mut set = ThemeSet::load_defaults();
set.themes.insert(ANSI_CODE_THEME.to_string(), ansi_theme());
set
})
}

/// A syntect colour that stands for ANSI palette slot `idx` rather than an
/// RGB value (the encoding bat uses): alpha 0, the index in `r`.
const fn ansi(idx: u8) -> Color {
Color {
r: idx,
g: 0,
b: 0,
a: 0,
}
}

/// Alpha 1 marks "the terminal's default colour".
const DEFAULT: Color = Color {
r: 0,
g: 0,
b: 0,
a: 1,
};

/// The theme-colour string for a highlighted token: `ansi:N`, `None` for the
/// default colour (the caller falls back to the theme's text colour), or hex.
pub fn token_color(c: Color) -> Option<String> {
match c.a {
0 => Some(format!("ansi:{}", c.r & 15)),
1 => None,
_ => Some(format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)),
}
}

fn ansi_theme() -> Theme {
// (scopes, palette slot, italic)
let rules: &[(&str, Color, bool)] = &[
("comment, punctuation.definition.comment", ansi(8), true),
("string, punctuation.definition.string", ansi(2), false),
("constant.character.escape", ansi(6), false),
(
"constant.numeric, constant.language, constant.character",
ansi(3),
false,
),
(
"keyword, storage.modifier, storage.type.function",
ansi(5),
false,
),
("keyword.operator", DEFAULT, false),
(
"storage.type, support.type, entity.name.type, entity.name.class",
ansi(6),
false,
),
(
"entity.name.function, support.function, meta.function-call",
ansi(4),
false,
),
("entity.name.tag", ansi(4), false),
("entity.other.attribute-name", ansi(3), false),
("variable.language, support.constant", ansi(1), false),
("markup.heading", ansi(4), false),
("markup.inserted", ansi(2), false),
("markup.deleted", ansi(1), false),
("invalid", ansi(1), false),
];
Theme {
name: Some(ANSI_CODE_THEME.to_string()),
author: None,
settings: ThemeSettings {
foreground: Some(DEFAULT),
..ThemeSettings::default()
},
scopes: rules
.iter()
.filter_map(|&(scope, color, italic)| {
Some(ThemeItem {
scope: ScopeSelectors::from_str(scope).ok()?,
style: StyleModifier {
foreground: Some(color),
background: None,
font_style: italic.then_some(FontStyle::ITALIC),
},
})
})
.collect(),
}
}
23 changes: 20 additions & 3 deletions src/layout/mermaid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -807,9 +807,22 @@ fn render_pie(source: &str, theme: &Theme, margin: &str, width: usize) -> Vec<St
width,
));

// Render as horizontal bar chart
// Render as horizontal bar chart. The label column is as wide as the
// widest label (at least 12), so every bar starts in the same column; a
// fixed `{:>12}` let longer labels push their bar right. Labels are cut
// only when the chart is too narrow to hold them beside a 10-cell bar.
// "│ " + label + " " + bar + " 100.0%"
const FIXED: usize = 3 + 1 + 7;
let widest = slices
.iter()
.map(|(l, _)| text::width(l))
.max()
.unwrap_or(0);
let label_w = widest.max(12).min(width.saturating_sub(FIXED + 10)).max(1);
// 30 cells at the usual 56-column chart width; less when narrower.
let max_bar = 30usize.min(width.saturating_sub(26)).max(1);
let max_bar = 30usize
.min(width.saturating_sub(FIXED + label_w + 1))
.max(1);
for (i, (label, value)) in slices.iter().enumerate() {
let pct = if total > 0.0 {
value / total * 100.0
Expand All @@ -829,7 +842,11 @@ fn render_pie(source: &str, theme: &Theme, margin: &str, width: usize) -> Vec<St
},
});
line.push(StyledSpan {
text: format!("{:>12} ", label),
text: {
let label = text::truncate(label, label_w);
let pad = label_w.saturating_sub(text::width(&label));
format!("{}{label} ", " ".repeat(pad))
},
style: SpanStyle {
fg: Some(color.clone()),
bold: true,
Expand Down
23 changes: 23 additions & 0 deletions src/layout/mermaid/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,29 @@ fn sequence_and_pie_wrap_when_narrow_and_keep_their_form_when_not() {
);
}

// Issue #30: a label wider than 12 columns ("Documentation") pushed its bar
// one column right of the others.
#[test]
fn pie_bars_share_a_start_column_whatever_the_label_length() {
let pie = "pie\n title Weekly Time Allocation\n \"Coding\" : 40\n \"Code Review\" : 15\n \"Meetings\" : 15\n \"Planning\" : 10\n \"Documentation\" : 10\n \"Learning\" : 10";
for w in [40, 60, 150] {
let out = render(pie, w);
let starts: Vec<usize> = out
.lines()
.filter(|l| l.contains('█'))
.map(|l| text::width(&l[..l.find('█').unwrap()]))
.collect();
assert_eq!(starts.len(), 6, "{out}");
assert!(starts.iter().all(|&s| s == starts[0]), "width {w}:\n{out}");
assert!(out.contains("Documentation"), "width {w}:\n{out}");
}
// Too narrow for the full label: cut with an ellipsis, still aligned.
let out = render(pie, 24);
for line in out.lines() {
assert!(text::width(line) <= 24, "{line}");
}
}

#[test]
fn corpus_is_big_enough() {
assert!(corpus().len() >= 20, "{} diagrams", corpus().len());
Expand Down
17 changes: 13 additions & 4 deletions src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,15 @@ fn image_backdrop(theme: &Theme) -> (u8, u8, u8) {
if let Some(bg) = &theme.colors.bg {
return crate::theme::hex_to_rgb(bg);
}
// A `default` foreground says nothing about the theme's darkness; ask
// the terminal instead.
if crate::theme::parse_color(&theme.colors.fg) == crate::theme::ColorSpec::Default {
return if crate::theme::detect::is_dark_background() {
(0, 0, 0)
} else {
(255, 255, 255)
};
}
let (r, g, b) = crate::theme::hex_to_rgb(&theme.colors.fg);
let luminance = 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32;
// Light foreground → dark theme → composite over black; else white.
Expand Down Expand Up @@ -1107,10 +1116,10 @@ fn layout_code_block(info: &str, literal: &str, ctx: &LayoutContext, lines: &mut
content.push(StyledSpan {
text: trimmed.to_string(),
style: SpanStyle {
fg: Some(format!(
"#{:02x}{:02x}{:02x}",
style.foreground.r, style.foreground.g, style.foreground.b
)),
fg: Some(
crate::highlight::token_color(style.foreground)
.unwrap_or_else(|| ctx.theme.colors.fg.clone()),
),
..Default::default()
},
});
Expand Down
Loading
Loading