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
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ notify = "8"
# Real (Myers) line diff for `ink diff`
similar = "2"
signal-hook = "0.3"
# termios + poll for the OSC 11 background query (unix only; empty on Windows)
libc = "0.2"
ratatui-image = { version = "11.0.6", default-features = false, features = ["crossterm", "image-defaults"] }

[dev-dependencies]
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ Press `/` to search within a document. Matches highlight inline; press Enter to

ink captures the mouse for wheel-scroll, which used to mean giving up your terminal's own text
selection. Now it does the job itself, and does it better — it knows where a code block starts and
where ink's own decoration ends.
where ink's own decoration ends. If you'd rather keep your terminal's own selection and link
clicking, run `ink --no-mouse` (one session) or set `mouse_capture = false` (always); both apply to
the reader and the file browser.

- **`v`** starts a character-wise selection, **`V`** a line-wise one. Move with `h j k l`, arrows,
`w`/`b` (word), `0`/`$` (line ends), `g`/`G` (document ends), `Ctrl+d`/`Ctrl+u` (half page).
Expand Down Expand Up @@ -229,7 +231,11 @@ Press `t` to toggle a sidebar showing every heading in the document. Tracks your

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

Auto-detects your terminal background and picks dark or light mode by default.
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.

Themes adapt to what the terminal can show: 24-bit colour where it is advertised, the 256-colour palette on `*-256color` terminals, and the 16 ANSI colours on the Linux console and other 16-colour terminals (`TERM=linux`, `xterm`, `vt100`, `screen`, …) — there ink uses your terminal's own palette, so your colour scheme applies. With `--color=never` or `NO_COLOR` the reader draws without colour at all, using bold, underline and reverse video for headings, links, search hits and selections.

On the Linux console, a non-UTF-8 locale, a legacy console font or a screen reader, box-drawing and symbol characters come out as junk. `ink --ascii` (or `ascii = true` under `[behavior]`) draws every border, bullet, task box, quote bar, rule and status-bar marker in plain 7-bit ASCII instead — `+--+`, `|`, `*`, `[x]`, `#` — and turns off smart quotes and emoji shortcodes, so an ASCII document prints as pure ASCII. It switches on by itself for `TERM=linux` and non-UTF-8 locales; `ascii = false` in the config keeps Unicode regardless.

### Math and emoji

Expand Down Expand Up @@ -362,9 +368,13 @@ mouse_capture = true
# How copied text reaches the clipboard:
# auto - OSC 52 escape (crosses SSH and tmux) plus a native helper, if present
# osc52 - escape sequence only
# native - pbcopy / wl-copy / xclip / xsel / clip.exe only
# native - pbcopy / wl-copy / xclip / xsel / clip.exe / termux-clipboard-set only
# off - copying is disabled
clipboard = "auto"

# Plain-ASCII borders, bullets and markers (Linux console, legacy fonts,
# screen readers). Unset: automatic on TERM=linux and non-UTF-8 locales.
# ascii = false
```

### Custom themes
Expand Down Expand Up @@ -412,6 +422,8 @@ Options:
--no-pager Never page --plain output, even on a TTY
--frontmatter Show YAML/TOML frontmatter
--spacing <MODE> Line spacing: compact, normal, relaxed
--no-mouse Don't capture the mouse (overrides mouse_capture in config)
--ascii Draw borders, bullets and markers in plain ASCII

Subcommands:
outline Show document heading structure
Expand All @@ -422,6 +434,7 @@ Subcommands:
man Generate a man page
config Config helpers (init, path)
keybindings Print the active keybinding map
doctor Print a diagnostic report to attach to an issue
```

## How it compares
Expand Down Expand Up @@ -452,7 +465,7 @@ Terminal graphics support varies wildly. If an image is blank, garbled, or missi

1. **Try the universal renderer:** `ink --image-protocol halfblocks file.md`. If the image appears as coarse colored blocks, decoding is fine — the problem is your terminal's pixel graphics protocol.
2. **Try a specific protocol:** `--image-protocol iterm2` (iTerm2, WezTerm, VS Code, Warp and friends) or `--image-protocol sixel`. Some terminals advertise protocols they only partially implement — auto-detection already works around the known cases (e.g. iTerm2 claiming kitty support), but new terminal versions ship new quirks.
3. **Collect a diagnostic report:** run `ink doctor` in the affected terminal — it prints your terminal identity, what the graphics negotiation chose, and decoder self-tests. `ink doctor --save ink-doctor.txt` writes it to a file.
3. **Collect a diagnostic report:** run `ink doctor` in the affected terminal — it prints your platform and terminal identity, the colour depth and theme ink picked (and which signal decided each), the clipboard helper and OSC 52 wrapping it would use, mouse capture, config warnings, what the graphics negotiation chose, and decoder self-tests. It leaves out anything that identifies you (no hostnames, user names, IPs or session IDs; your home directory shows as `~`), so it is safe to paste into an issue. `ink doctor --save ink-doctor.txt` writes it to a file. The same report helps with colour, theme and clipboard problems.
4. **Send it to us:** open an [image rendering issue](https://github.com/borghei/ink/issues/new?template=image-rendering.yml) with the report attached. That output usually pinpoints the problem immediately.

## Security model
Expand Down
20 changes: 14 additions & 6 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,9 @@ fn run_inner(
Style::default().fg(bar_bg)
};
let sep_line = Line::from(Span::styled(
"▁".repeat(separator_area.width as usize),
crate::glyphs::current()
.separator
.repeat(separator_area.width as usize),
sep_style,
));
frame.render_widget(Paragraph::new(vec![sep_line]), separator_area);
Expand Down Expand Up @@ -641,7 +643,10 @@ fn run_inner(
frame.buffer_mut().set_string(
doc_area.x + img.col_offset,
doc_area.y + y as u16,
"🖼 (image could not be encoded for this terminal)",
format!(
"{} (image could not be encoded for this terminal)",
crate::glyphs::current().image
),
Style::default()
.fg(theme::hex_to_color(&t.colors.link_url))
.add_modifier(Modifier::ITALIC),
Expand Down Expand Up @@ -669,13 +674,16 @@ fn run_inner(
.iter()
.map(|h| (h.label, h.caption.clone(), h.url.clone()))
.collect();
let dash = crate::glyphs::current().dash;
let title = match hint_kind {
HintKind::Open => {
"Follow link — press a letter, Y to copy instead, Esc to cancel"
HintKind::Open => format!(
"Follow link {dash} press a letter, Y to copy instead, Esc to cancel"
),
HintKind::CopyUrl => {
format!("Copy link URL {dash} press a letter, Esc to cancel")
}
HintKind::CopyUrl => "Copy link URL — press a letter, Esc to cancel",
};
render::render_link_hints(frame, main_area, &hints, title, &t);
render::render_link_hints(frame, main_area, &hints, &title, &t);
}

// Code-block hint labels, painted on the blocks themselves.
Expand Down
40 changes: 30 additions & 10 deletions src/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ struct FileEntry {
///
/// Returns `Some(path)` if the user selected a file, or `None` if they quit.
/// Gracefully handles empty directories, permission errors, etc.
pub fn browse(dir: &Path, theme_name: &str) -> Result<Option<PathBuf>> {
///
/// `mouse_capture` follows the same setting as the reader (`--no-mouse`,
/// `[behavior] mouse_capture`); the browser has no mouse actions of its own,
/// but switching capture on here and off in the reader would flip the
/// terminal's selection behaviour between screens.
pub fn browse(dir: &Path, theme_name: &str, mouse_capture: bool) -> Result<Option<PathBuf>> {
let files = find_markdown_files(dir);
if files.is_empty() {
eprintln!("ink: no markdown files found in {}", dir.display());
Expand All @@ -29,7 +34,10 @@ pub fn browse(dir: &Path, theme_name: &str) -> Result<Option<PathBuf>> {
crate::app::install_panic_hook();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
execute!(stdout, EnterAlternateScreen)?;
if mouse_capture {
execute!(stdout, EnableMouseCapture)?;
}
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;

Expand Down Expand Up @@ -73,6 +81,8 @@ fn browse_inner(
terminal.draw(|frame| {
let size = frame.area();
let t = theme::resolve_theme(theme_name);
let g = crate::glyphs::current();
let dot = format!(" {} ", g.dot);

// Fill background
if let Some(ref bg_hex) = t.colors.bg {
Expand Down Expand Up @@ -110,7 +120,10 @@ fn browse_inner(
Line::from(""),
Line::from(vec![
Span::styled(" ink ", Style::default().fg(accent).bold()),
Span::styled(format!("— {dir_display}"), Style::default().fg(dim)),
Span::styled(
format!("{} {dir_display}", g.dash),
Style::default().fg(dim),
),
]),
];
frame.render_widget(
Expand All @@ -136,7 +149,11 @@ fn browse_inner(
.map(|(i, &file_idx)| {
let entry = &files[file_idx];
let is_sel = i == selected;
let marker = if is_sel { " ▸ " } else { " " };
let marker = if is_sel {
format!(" {} ", g.pointer)
} else {
" ".to_string()
};
let size_str = format_size(entry.size);

let mut name_style = Style::default().fg(if is_sel { accent } else { fg });
Expand All @@ -158,7 +175,7 @@ fn browse_inner(
}

Line::from(vec![
Span::styled(marker.to_string(), marker_style),
Span::styled(marker, marker_style),
Span::styled(entry.relative_path.clone(), name_style),
Span::styled(format!(" {size_str}"), dim_style),
])
Expand All @@ -180,7 +197,7 @@ fn browse_inner(
Line::from(vec![
Span::styled(" / ", Style::default().fg(accent).bg(bar_bg).bold()),
Span::styled(filter.clone(), Style::default().fg(bar_fg).bg(bar_bg)),
Span::styled("█", Style::default().fg(accent).bg(bar_bg)),
Span::styled(g.block, Style::default().fg(accent).bg(bar_bg)),
Span::styled(
format!(" {} matches", filtered.len()),
Style::default().fg(bar_dim).bg(bar_bg),
Expand All @@ -189,15 +206,18 @@ fn browse_inner(
])
} else {
Line::from(vec![
Span::styled(" ↑↓/jk ", Style::default().fg(bar_fg).bg(bar_bg).bold()),
Span::styled(
format!(" {} ", g.scroll_keys),
Style::default().fg(bar_fg).bg(bar_bg).bold(),
),
Span::styled("navigate", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" · ", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(dot.clone(), Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" Enter ", Style::default().fg(bar_fg).bg(bar_bg).bold()),
Span::styled("open", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" · ", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(dot.clone(), Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" / ", Style::default().fg(bar_fg).bg(bar_bg).bold()),
Span::styled("filter", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" · ", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(dot.clone(), Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" q ", Style::default().fg(bar_fg).bg(bar_bg).bold()),
Span::styled("quit", Style::default().fg(bar_dim).bg(bar_bg)),
Span::styled(" ".repeat(size.width as usize), Style::default().bg(bar_bg)),
Expand Down
Loading
Loading