mirror of
https://github.com/wez/wezterm.git
synced 2024-09-19 02:37:51 +03:00
wezterm: render release notes as markdown in the update UI
This adds a markdown -> termwiz render helper for the update UI to make the release notes look a bit more intelligible.
This commit is contained in:
parent
8204a1a6b6
commit
d660f53f86
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -1144,6 +1144,15 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.14"
|
||||
@ -2432,6 +2441,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e142c3b8f49d2200605ee6ba0b1d757310e9e7a72afe78c36ee2ef67300ee00"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@ -3829,6 +3850,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"pretty_env_logger 0.4.0",
|
||||
"promise",
|
||||
"pulldown-cmark",
|
||||
"rangeset",
|
||||
"ratelimit_meter",
|
||||
"rcgen",
|
||||
|
@ -47,6 +47,7 @@ libc = "0.2"
|
||||
log = "0.4"
|
||||
lru = "0.4"
|
||||
open = "1.2"
|
||||
pulldown-cmark = "0.7"
|
||||
metrics = { version="0.12", features=["std"]}
|
||||
mlua = {version="0.2", features=["vendored"]}
|
||||
hdrhistogram = "6.3"
|
||||
|
@ -26,6 +26,7 @@ mod connui;
|
||||
mod frontend;
|
||||
mod keyassignment;
|
||||
mod localtab;
|
||||
mod markdown;
|
||||
mod mux;
|
||||
mod ratelim;
|
||||
mod server;
|
||||
|
277
src/markdown.rs
Normal file
277
src/markdown.rs
Normal file
@ -0,0 +1,277 @@
|
||||
use pulldown_cmark::{Event, Options, Parser, Tag};
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use termwiz::cell::*;
|
||||
use termwiz::color::AnsiColor;
|
||||
use termwiz::surface::Change;
|
||||
use termwiz::terminal::ScreenSize;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub struct RenderState {
|
||||
screen_size: ScreenSize,
|
||||
changes: Vec<Change>,
|
||||
current_list_item: Option<u64>,
|
||||
current_indent: Option<usize>,
|
||||
x_pos: usize,
|
||||
wrap_width: usize,
|
||||
}
|
||||
|
||||
fn is_whitespace_word(word: &str) -> bool {
|
||||
word.chars().any(|c| c.is_whitespace())
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub fn into_changes(self) -> Vec<Change> {
|
||||
self.changes
|
||||
}
|
||||
|
||||
pub fn new(wrap_width: usize, screen_size: ScreenSize) -> Self {
|
||||
Self {
|
||||
changes: vec![],
|
||||
current_list_item: None,
|
||||
current_indent: None,
|
||||
x_pos: 0,
|
||||
wrap_width,
|
||||
screen_size,
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_indent(&mut self) {
|
||||
if let Some(indent) = self.current_indent {
|
||||
let mut s = String::new();
|
||||
for _ in 0..indent {
|
||||
s.push(' ');
|
||||
}
|
||||
self.changes.push(s.into());
|
||||
self.x_pos += indent;
|
||||
}
|
||||
}
|
||||
|
||||
fn newline(&mut self) {
|
||||
self.changes.push("\r\n".into());
|
||||
self.x_pos = 0;
|
||||
}
|
||||
|
||||
fn wrap_text(&mut self, text: &str) {
|
||||
for word in text.split_word_bounds() {
|
||||
let len = unicode_column_width(word);
|
||||
if self.x_pos + len < self.wrap_width {
|
||||
if !(self.x_pos == 0 && is_whitespace_word(word)) {
|
||||
self.changes.push(word.into());
|
||||
self.x_pos += len;
|
||||
}
|
||||
} else if len < self.wrap_width {
|
||||
self.newline();
|
||||
self.emit_indent();
|
||||
if !is_whitespace_word(word) {
|
||||
self.changes.push(word.into());
|
||||
self.x_pos += len;
|
||||
}
|
||||
} else {
|
||||
self.newline();
|
||||
self.emit_indent();
|
||||
self.changes.push(word.into());
|
||||
self.newline();
|
||||
self.emit_indent();
|
||||
self.x_pos = len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_event(&mut self, event: Event) {
|
||||
match event {
|
||||
Event::Start(Tag::Paragraph) => {}
|
||||
Event::End(Tag::Paragraph) => {
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::Start(Tag::BlockQuote) => {}
|
||||
Event::End(Tag::BlockQuote) => {
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::Start(Tag::CodeBlock(_)) => {}
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::Start(Tag::List(first_idx)) => {
|
||||
self.current_list_item = first_idx;
|
||||
self.newline();
|
||||
}
|
||||
Event::End(Tag::List(_)) => {
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::Start(Tag::Item) => {
|
||||
let list_item_prefix = if let Some(idx) = self.current_list_item.take() {
|
||||
self.current_list_item.replace(idx + 1);
|
||||
format!(" {}. ", idx)
|
||||
} else {
|
||||
" * ".to_owned()
|
||||
};
|
||||
let indent_width = unicode_column_width(&list_item_prefix);
|
||||
self.current_indent.replace(indent_width);
|
||||
self.changes.push(list_item_prefix.into());
|
||||
self.x_pos += indent_width;
|
||||
}
|
||||
Event::End(Tag::Item) => {
|
||||
self.newline();
|
||||
self.current_indent.take();
|
||||
}
|
||||
|
||||
Event::Start(Tag::Heading(_)) => {
|
||||
self.newline();
|
||||
self.changes
|
||||
.push(AttributeChange::Intensity(Intensity::Bold).into());
|
||||
}
|
||||
Event::End(Tag::Heading(_)) => {
|
||||
self.changes
|
||||
.push(AttributeChange::Intensity(Intensity::Normal).into());
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::Start(Tag::Strikethrough) => {
|
||||
self.changes
|
||||
.push(AttributeChange::StrikeThrough(true).into());
|
||||
}
|
||||
Event::End(Tag::Strikethrough) => {
|
||||
self.changes
|
||||
.push(AttributeChange::StrikeThrough(false).into());
|
||||
}
|
||||
|
||||
Event::Start(Tag::Emphasis) => {
|
||||
self.changes.push(AttributeChange::Italic(true).into());
|
||||
}
|
||||
Event::End(Tag::Emphasis) => {
|
||||
self.changes.push(AttributeChange::Italic(false).into());
|
||||
}
|
||||
|
||||
Event::Start(Tag::Link(_linktype, url, _title)) => {
|
||||
self.changes.push(
|
||||
AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(url.into_string()))))
|
||||
.into(),
|
||||
);
|
||||
self.changes
|
||||
.push(AttributeChange::Underline(Underline::Single).into());
|
||||
}
|
||||
Event::End(Tag::Link(..)) => {
|
||||
self.changes.push(AttributeChange::Hyperlink(None).into());
|
||||
self.changes
|
||||
.push(AttributeChange::Underline(Underline::None).into());
|
||||
}
|
||||
|
||||
Event::Start(Tag::Image(_linktype, img_url, _title)) => {
|
||||
use image::GenericImageView;
|
||||
use termwiz::image::TextureCoordinate;
|
||||
|
||||
let url: &str = img_url.as_ref();
|
||||
if let Ok(mut f) = std::fs::File::open(url) {
|
||||
let mut data = vec![];
|
||||
if let Ok(_len) = f.read_to_end(&mut data) {
|
||||
if let Ok(decoded_image) = image::load_from_memory(&data) {
|
||||
let image = Arc::new(termwiz::image::ImageData::with_raw_data(data));
|
||||
|
||||
let scale = self.wrap_width as f32 / decoded_image.width() as f32;
|
||||
|
||||
let aspect_ratio =
|
||||
if self.screen_size.xpixel == 0 || self.screen_size.ypixel == 0 {
|
||||
// Guess: most monospace fonts are twice as tall as they are wide
|
||||
2.0
|
||||
} else {
|
||||
let cell_height = self.screen_size.ypixel as f32
|
||||
/ self.screen_size.rows as f32;
|
||||
let cell_width = self.screen_size.xpixel as f32
|
||||
/ self.screen_size.cols as f32;
|
||||
cell_height / cell_width
|
||||
};
|
||||
|
||||
let height = decoded_image.height() as f32 * scale / aspect_ratio;
|
||||
|
||||
self.newline();
|
||||
self.changes.push(termwiz::surface::Change::Image(
|
||||
termwiz::surface::Image {
|
||||
width: self.wrap_width,
|
||||
height: height as usize,
|
||||
top_left: TextureCoordinate::new_f32(0., 0.),
|
||||
bottom_right: TextureCoordinate::new_f32(1., 1.),
|
||||
image,
|
||||
},
|
||||
));
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::End(Tag::Image(_linktype, _img_url, _title)) => {}
|
||||
|
||||
Event::Start(Tag::Strong) => {
|
||||
self.changes
|
||||
.push(AttributeChange::Intensity(Intensity::Bold).into());
|
||||
}
|
||||
Event::End(Tag::Strong) => {
|
||||
self.changes
|
||||
.push(AttributeChange::Intensity(Intensity::Normal).into());
|
||||
}
|
||||
|
||||
Event::Start(Tag::FootnoteDefinition(_label)) => {}
|
||||
Event::End(Tag::FootnoteDefinition(_)) => {}
|
||||
|
||||
Event::Start(Tag::Table(_alignment)) => {}
|
||||
Event::End(Tag::Table(_)) => {}
|
||||
|
||||
Event::Start(Tag::TableHead) => {}
|
||||
Event::End(Tag::TableHead) => {}
|
||||
|
||||
Event::Start(Tag::TableRow) => {}
|
||||
Event::End(Tag::TableRow) => {}
|
||||
|
||||
Event::Start(Tag::TableCell) => {}
|
||||
Event::End(Tag::TableCell) => {}
|
||||
|
||||
Event::FootnoteReference(s) | Event::Text(s) | Event::Html(s) => {
|
||||
self.wrap_text(&s);
|
||||
}
|
||||
|
||||
Event::Code(s) => {
|
||||
self.changes
|
||||
.push(AttributeChange::Foreground(AnsiColor::Fuschia.into()).into());
|
||||
self.wrap_text(&s);
|
||||
self.changes
|
||||
.push(AttributeChange::Foreground(Default::default()).into());
|
||||
}
|
||||
|
||||
Event::SoftBreak => {
|
||||
self.wrap_text(" ");
|
||||
}
|
||||
|
||||
Event::HardBreak => {
|
||||
self.newline();
|
||||
self.emit_indent();
|
||||
}
|
||||
|
||||
Event::Rule => {
|
||||
self.changes.push("---".into());
|
||||
self.newline();
|
||||
}
|
||||
|
||||
Event::TaskListMarker(true) => {
|
||||
self.changes.push("[x]".into());
|
||||
}
|
||||
|
||||
Event::TaskListMarker(false) => {
|
||||
self.changes.push("[ ]".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_str(&mut self, s: &str) {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
let parser = Parser::new_ext(s, options);
|
||||
|
||||
for event in parser {
|
||||
self.apply_event(event);
|
||||
}
|
||||
}
|
||||
}
|
@ -162,11 +162,20 @@ fn show_update_available(release: Release) {
|
||||
.trim_end()
|
||||
// Normalize any dos line endings that might have wound
|
||||
// up in the body field...
|
||||
.replace("\r\n", "\n")
|
||||
// ... and then canonicalize the line endings for the terminal
|
||||
.replace("\n", "\r\n");
|
||||
.replace("\r\n", "\n");
|
||||
|
||||
ui.output(vec![
|
||||
let mut render = crate::markdown::RenderState::new(
|
||||
78,
|
||||
termwiz::terminal::ScreenSize {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
xpixel: 0,
|
||||
ypixel: 0,
|
||||
},
|
||||
);
|
||||
render.parse_str(&brief_blurb);
|
||||
|
||||
let mut output = vec![
|
||||
Change::CursorShape(CursorShape::Hidden),
|
||||
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
|
||||
install,
|
||||
@ -174,7 +183,10 @@ fn show_update_available(release: Release) {
|
||||
format!("Version {} is now available!\r\n", release.tag_name).into(),
|
||||
Change::Attribute(AttributeChange::Hyperlink(None)),
|
||||
format!("(this is version {})\r\n", wezterm_version()).into(),
|
||||
format!("{}\r\n", brief_blurb).into(),
|
||||
];
|
||||
output.append(&mut render.into_changes());
|
||||
output.extend_from_slice(&[
|
||||
"\r\n".into(),
|
||||
Change::Attribute(AttributeChange::Hyperlink(Some(Arc::new(Hyperlink::new(
|
||||
change_log,
|
||||
))))),
|
||||
@ -182,6 +194,7 @@ fn show_update_available(release: Release) {
|
||||
"View Change Log\r\n".into(),
|
||||
Change::Attribute(AttributeChange::Hyperlink(None)),
|
||||
]);
|
||||
ui.output(output);
|
||||
|
||||
let assets = release.classify_assets();
|
||||
let appimage = assets.get(&AssetKind::AppImage);
|
||||
|
Loading…
Reference in New Issue
Block a user