Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,13 @@ mdterm README.md | less -R
| `f` | Link picker (open URLs / follow local links) |
| `t` | Toggle dark/light theme |
| `l` | Toggle line numbers in code blocks |
| Drag | Select text; copies to clipboard on release |
| Click heading | Copy heading section to clipboard |
| Click list | Copy entire list to clipboard |
| Click code block | Copy code block to clipboard |
| `Y` | Copy entire document to clipboard |
| `c` | Copy nearest code block to clipboard |
| `m` | Toggle mouse capture (hand the mouse back to the terminal) |
| `Tab` / `Shift+Tab` | Switch between files |
| `h` / `?` / `F1` | Help screen |
| `q` / `Ctrl+C` | Quit |
Expand Down
2 changes: 1 addition & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3103,7 +3103,7 @@ mod tests {
assert!(
buf.windows(header_prefix.len()).any(|w| w == header_prefix),
"header not found in output.\nGot (hex): {:?}",
&buf
buf
);

// ── Check placeholder rows ────────────────────────────────────────
Expand Down
78 changes: 56 additions & 22 deletions src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ impl<'a> Renderer<'a> {
text: BLOCKQUOTE_PREFIX.to_string(),
style: Style {
fg: Some(self.theme.blockquote_bar),
..Default::default()
..Style::frame()
},
});
}
Expand All @@ -215,7 +215,7 @@ impl<'a> Renderer<'a> {
text: BLOCKQUOTE_PREFIX_TRIMMED.to_string(),
style: Style {
fg: Some(self.theme.blockquote_bar),
..Default::default()
..Style::frame()
},
}],
meta: LineMeta::None,
Expand Down Expand Up @@ -302,23 +302,23 @@ impl<'a> Renderer<'a> {
text: " ╭─".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
}];
if !label.is_empty() {
top_spans.push(StyledSpan {
text: label,
style: Style {
fg: Some(label_fg),
..Default::default()
..Style::frame()
},
});
}
top_spans.push(StyledSpan {
text: format!("{}╮", "─".repeat(dashes_after)),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
});
self.lines.push(Line {
Expand All @@ -333,14 +333,14 @@ impl<'a> Renderer<'a> {
text: " │".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
},
StyledSpan {
text: " ".to_string(),
style: Style {
bg: Some(code_bg),
..Default::default()
..Style::frame()
},
},
];
Expand All @@ -355,11 +355,12 @@ impl<'a> Renderer<'a> {
style: Style {
fg: Some(self.theme.line_number),
bg: Some(code_bg),
..Default::default()
..Style::frame()
},
});
}

let mut has_content = false;
if let Ok(ranges) = highlighter.highlight_line(line_str, self.syntax_set) {
for (syn_style, text) in ranges {
let trimmed = text.trim_end_matches('\n').trim_end_matches('\r');
Expand All @@ -371,6 +372,7 @@ impl<'a> Renderer<'a> {
text: trimmed.to_string(),
style,
});
has_content = true;
}
}
} else {
Expand All @@ -383,21 +385,25 @@ impl<'a> Renderer<'a> {
..Default::default()
},
});
has_content = true;
}
if !has_content {
spans.push(empty_code_content(code_bg));
}

let padding = content_width.saturating_sub(char_count) + 1;
spans.push(StyledSpan {
text: " ".repeat(padding),
style: Style {
bg: Some(code_bg),
..Default::default()
..Style::frame()
},
});
spans.push(StyledSpan {
text: "│".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
});

Expand All @@ -413,7 +419,7 @@ impl<'a> Renderer<'a> {
text: format!(" ╰{}╯", "─".repeat(inner_width)),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
}],
meta: LineMeta::CodeContent { block_id },
Expand Down Expand Up @@ -443,21 +449,21 @@ impl<'a> Renderer<'a> {
text: " ╭─".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
},
StyledSpan {
text: label.to_string(),
style: Style {
fg: Some(label_fg),
..Default::default()
..Style::frame()
},
},
StyledSpan {
text: format!("{}╮", "─".repeat(dashes_after)),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
},
],
Expand All @@ -471,14 +477,14 @@ impl<'a> Renderer<'a> {
text: " │".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
},
StyledSpan {
text: " ".to_string(),
style: Style {
bg: Some(code_bg),
..Default::default()
..Style::frame()
},
},
];
Expand All @@ -487,21 +493,25 @@ impl<'a> Renderer<'a> {
.iter()
.map(|s| UnicodeWidthStr::width(s.text.as_str()))
.sum();
spans.extend(row_spans.iter().cloned());
if row_spans.is_empty() {
spans.push(empty_code_content(code_bg));
} else {
spans.extend(row_spans.iter().cloned());
}

let padding = content_width.saturating_sub(row_width) + 1;
spans.push(StyledSpan {
text: " ".repeat(padding),
style: Style {
bg: Some(code_bg),
..Default::default()
..Style::frame()
},
});
spans.push(StyledSpan {
text: "│".to_string(),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
});

Expand All @@ -517,7 +527,7 @@ impl<'a> Renderer<'a> {
text: format!(" ╰{}╯", "─".repeat(inner_width)),
style: Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
},
}],
meta: LineMeta::CodeContent { block_id },
Expand Down Expand Up @@ -587,9 +597,12 @@ impl<'a> Renderer<'a> {
*w = (*w).max(3);
}

// Frame, not content: the rules and column separators are stripped from
// copied text. Cell padding deliberately stays content so a pasted table
// keeps its column alignment instead of collapsing to "ab".
let border_style = Style {
fg: Some(border_fg),
..Default::default()
..Style::frame()
};

let make_rule = |left: &str, mid: &str, right: &str, widths: &[usize]| -> Line {
Expand Down Expand Up @@ -717,10 +730,14 @@ impl<'a> Renderer<'a> {
self.lines.push(Line {
spans: vec![StyledSpan {
text: "─".repeat(self.width.min(60)),
// Invented by the renderer — there is no rule in
// the source, so it must not reach the clipboard.
// `Event::Rule` below is the opposite case: that
// one is a real `---` and stays content.
style: Style {
fg: Some(self.theme.heading_separator),
dim: true,
..Default::default()
..Style::frame()
},
}],
meta: LineMeta::None,
Expand Down Expand Up @@ -1289,6 +1306,23 @@ fn wrap_cell(spans: &[StyledSpan], width: usize) -> Vec<Vec<StyledSpan>> {
lines
}

/// A zero-width content span for a code-block row that has no text of its own.
///
/// A blank line inside a fenced block produces no highlighted spans, which would
/// leave the row built entirely from `Style::frame()` — indistinguishable from
/// the block's top and bottom borders, so a copied selection would drop the
/// blank line rather than preserve it. This marks the row as content without
/// drawing anything or contributing a character to the copy.
fn empty_code_content(code_bg: Color) -> StyledSpan {
StyledSpan {
text: String::new(),
style: Style {
bg: Some(code_bg),
..Default::default()
},
}
}

fn syntect_to_style(syn: SynStyle) -> Style {
Style {
fg: Some(Color::Rgb {
Expand Down
Loading
Loading