Print Markdown to a paper in your terminal

Handle wide characters better

+161 -99
+1
Cargo.lock
··· 437 437 "structopt", 438 438 "syncat-stylesheet", 439 439 "terminal_size", 440 + "unicode-width", 440 441 ] 441 442 442 443 [[package]]
+1
Cargo.toml
··· 25 25 console = "0.13" 26 26 directories-next = "2.0" 27 27 syncat-stylesheet = { version = "2.1.4", features = ["ansi_term"] } 28 + unicode-width = "0.1"
+15 -4
src/main.rs
··· 8 8 use structopt::StructOpt; 9 9 use syncat_stylesheet::Stylesheet; 10 10 use terminal_size::{terminal_size, Width}; 11 + use unicode_width::UnicodeWidthChar; 11 12 12 13 mod dirs; 13 14 mod printer; ··· 17 18 18 19 use printer::Printer; 19 20 use words::Words; 21 + 22 + fn str_width(s: &str) -> usize { 23 + strip_ansi_codes(s) 24 + .chars() 25 + .flat_map(UnicodeWidthChar::width) 26 + .sum() 27 + } 20 28 21 29 /// Prints papers in your terminal 22 30 #[derive(StructOpt, Debug)] ··· 113 121 return; 114 122 } 115 123 116 - let centering = " ".repeat((terminal_width - width) / 2); 124 + let centering = " ".repeat((terminal_width.saturating_sub(width)) / 2); 117 125 118 126 let stylesheet = Stylesheet::from_file(dirs::active_color().join("paper.syncat")) 119 127 .unwrap_or_else(|_| { ··· 153 161 let mut buffer = String::new(); 154 162 let mut indent = None; 155 163 for word in Words::preserving_whitespace(line) { 156 - if buffer.chars().count() + word.chars().count() > available_width { 164 + if str_width(&buffer) + str_width(&word) > available_width { 157 165 println!( 158 166 "{}{}{}{}{}{}", 159 167 centering, 160 168 margin, 161 169 paper_style.paint(&buffer), 162 - paper_style.paint(" ".repeat(available_width - buffer.chars().count())), 170 + paper_style.paint( 171 + " ".repeat(available_width.saturating_sub(str_width(&buffer))) 172 + ), 163 173 margin, 164 174 shadow_style.paint(" "), 165 175 ); ··· 182 192 centering, 183 193 margin, 184 194 paper_style.paint(&buffer), 185 - paper_style.paint(" ".repeat(available_width - buffer.chars().count())), 195 + paper_style 196 + .paint(" ".repeat(available_width.saturating_sub(str_width(&buffer)))), 186 197 margin, 187 198 shadow_style.paint(" "), 188 199 );
+46 -26
src/printer.rs
··· 1 + use crate::str_width; 1 2 use crate::table::Table; 2 3 use crate::termpix; 3 4 use crate::words::Words; 4 5 use ansi_term::Style; 5 - use console::{measure_text_width, AnsiCodeIterator}; 6 + use console::AnsiCodeIterator; 6 7 use image::{self, GenericImageView as _}; 7 8 use pulldown_cmark::{Alignment, CodeBlockKind, Event, Tag}; 8 9 use std::convert::{TryFrom, TryInto}; ··· 189 190 let mut all_scopes = scopes.clone(); 190 191 all_scopes.append(&mut extra_scopes.unwrap_or(&[]).to_vec()); 191 192 let style = Self::resolve_scopes(&stylesheet, &all_scopes, Some("prefix")); 192 - Some((format!("{}", style.paint(&prefix)), prefix.chars().count())) 193 + Some((format!("{}", style.paint(&prefix)), str_width(&prefix))) 193 194 }) 194 195 .fold((String::new(), 0), |(s, c), (s2, c2)| (s + &s2, c + c2)) 195 196 } ··· 208 209 let mut all_scopes = scopes.clone(); 209 210 all_scopes.append(&mut extra_scopes.unwrap_or(&[]).to_vec()); 210 211 let style = Self::resolve_scopes(&stylesheet, &all_scopes, Some("suffix")); 211 - Some((format!("{}", style.paint(&suffix)), suffix.chars().count())) 212 + Some((format!("{}", style.paint(&suffix)), str_width(&suffix))) 212 213 }) 213 214 .fold((String::new(), 0), |(s, c), (s2, c2)| (s2 + &s, c + c2)) 214 215 } ··· 272 273 self.centering, 273 274 self.margin, 274 275 prefix, 275 - self.paper_style() 276 - .paint(" ".repeat(self.width - prefix_len - suffix_len)), 276 + self.paper_style().paint( 277 + " ".repeat( 278 + self.width 279 + .saturating_sub(prefix_len) 280 + .saturating_sub(suffix_len) 281 + ) 282 + ), 277 283 suffix, 278 284 self.margin, 279 285 self.shadow(), ··· 289 295 self.centering, 290 296 self.margin, 291 297 prefix, 292 - self.style() 293 - .paint("─".repeat(self.width - prefix_len - suffix_len)), 298 + self.style().paint( 299 + "─".repeat( 300 + self.width 301 + .saturating_sub(prefix_len) 302 + .saturating_sub(suffix_len) 303 + ) 304 + ), 294 305 suffix, 295 306 self.margin, 296 307 self.shadow(), ··· 304 315 return; 305 316 }; 306 317 let (heading, rows) = std::mem::replace(&mut self.table, (vec![], vec![])); 307 - let available_width = self.width - self.prefix_len() - self.suffix_len(); 318 + let available_width = self 319 + .width 320 + .saturating_sub(self.prefix_len()) 321 + .saturating_sub(self.suffix_len()); 308 322 let table_str = 309 323 Table::new(heading, rows, available_width).print(self.paper_style(), alignments); 310 324 for line in table_str.lines() { ··· 317 331 line, 318 332 prefix, 319 333 self.paper_style() 320 - .paint(" ".repeat(available_width - measure_text_width(line))), 334 + .paint(" ".repeat(available_width.saturating_sub(str_width(line)))), 321 335 suffix, 322 336 self.margin, 323 337 self.shadow(), ··· 338 352 let mut first_prefix = Some(self.prefix2(Some(&[&language_context[..]]))); 339 353 let mut first_suffix = Some(self.suffix2(Some(&[&language_context[..]]))); 340 354 341 - let available_width = self.width 342 - - first_prefix.as_ref().unwrap().1 343 - - first_suffix.as_ref().unwrap().1; 355 + let available_width = self 356 + .width 357 + .saturating_sub(first_prefix.as_ref().unwrap().1) 358 + .saturating_sub(first_suffix.as_ref().unwrap().1); 344 359 let buffer = std::mem::replace(&mut self.buffer, String::new()); 345 360 let buffer = if self.opts.syncat { 346 361 let syncat = Command::new("syncat") ··· 368 383 .lines() 369 384 .map(|mut line| { 370 385 let mut output = String::new(); 371 - while line.chars().count() > available_width { 386 + while str_width(&line) > available_width { 372 387 let prefix = line.chars().take(available_width).collect::<String>(); 373 388 output = format!("{}{}\n", output, prefix); 374 389 line = &line[prefix.len()..]; ··· 377 392 "{}{}{}\n", 378 393 output, 379 394 line, 380 - " ".repeat(available_width - line.chars().count()) 395 + " ".repeat(available_width.saturating_sub(str_width(&line))) 381 396 ) 382 397 }) 383 398 .collect() ··· 401 416 ); 402 417 403 418 for line in buffer.lines() { 404 - let width = measure_text_width(line); 419 + let width = str_width(line); 405 420 let (prefix, _) = self.prefix2(Some(&[&language_context[..]])); 406 421 let (suffix, _) = self.suffix2(Some(&[&language_context[..]])); 407 422 print!( ··· 424 439 } 425 440 println!( 426 441 "{}{}{}{}", 427 - style.paint(" ".repeat(available_width - width)), 442 + style.paint(" ".repeat(available_width.saturating_sub(width))), 428 443 suffix, 429 444 self.margin, 430 445 self.shadow(), ··· 444 459 prefix, 445 460 format!( 446 461 "{}{}", 447 - style.paint(" ".repeat(available_width - lang.chars().count())), 462 + style.paint(" ".repeat(available_width.saturating_sub(str_width(&lang)))), 448 463 self.style3(Some(&[&language_context[..]]), Some("lang-tag")) 449 464 .paint(lang) 450 465 ), ··· 489 504 suffix, 490 505 self.paper_style().paint( 491 506 " ".repeat( 492 - self.width - measure_text_width(&self.content) - prefix_len - suffix_len 507 + self.width 508 + .saturating_sub(str_width(&self.content)) 509 + .saturating_sub(prefix_len) 510 + .saturating_sub(suffix_len) 493 511 ) 494 512 ), 495 513 self.margin, ··· 529 547 } 530 548 let style = self.style(); 531 549 for word in Words::new(s) { 532 - if measure_text_width(&self.content) 533 - + word.len() 534 - + self.prefix_len() 535 - + self.suffix_len() 550 + if str_width(&self.content) + word.len() + self.prefix_len() + self.suffix_len() 536 551 > self.width 537 552 { 538 553 self.flush(); ··· 542 557 } else { 543 558 &word 544 559 }; 545 - let available_len = self.width - self.prefix_len() - self.suffix_len(); 546 - while measure_text_width(&self.content) + word.len() > available_len { 560 + let available_len = self 561 + .width 562 + .saturating_sub(self.prefix_len()) 563 + .saturating_sub(self.suffix_len()); 564 + while str_width(&self.content) + str_width(&word) > available_len { 547 565 let part = word.chars().take(available_len).collect::<String>(); 548 566 self.target().push_str(&format!("{}", style.paint(&part))); 549 567 word = &word[part.len()..]; ··· 640 658 self.flush(); 641 659 642 660 if !self.opts.no_images { 643 - let available_width = 644 - self.width - self.prefix_len() - self.suffix_len(); 661 + let available_width = self 662 + .width 663 + .saturating_sub(self.prefix_len()) 664 + .saturating_sub(self.suffix_len()); 645 665 match image::open(destination.as_ref()) { 646 666 Ok(image) => { 647 667 let (mut width, mut height) = image.dimensions();
+87 -60
src/table.rs
··· 1 - use std::io::Write; 1 + use crate::words::Words; 2 2 use ansi_term::Style; 3 - use pulldown_cmark::Alignment; 4 3 use console::{measure_text_width, strip_ansi_codes}; 5 - use crate::words::Words; 4 + use pulldown_cmark::Alignment; 5 + use std::io::Write; 6 6 7 7 pub struct Table { 8 8 titles: Vec<String>, ··· 20 20 } 21 21 22 22 pub fn print(self, paper_style: Style, alignment: &[Alignment]) -> String { 23 - let Table { titles, rows, width } = self; 23 + let Table { 24 + titles, 25 + rows, 26 + width, 27 + } = self; 24 28 25 29 // NOTE: for now, styling is not supported within tables because that gets really hard 26 - let titles = titles.iter() 30 + let titles = titles 31 + .iter() 27 32 .map(|title| strip_ansi_codes(title).trim().to_string()) 28 33 .collect::<Vec<_>>(); 29 - let rows = rows.iter() 30 - .map(|row| row.iter() 31 - .map(|cell| strip_ansi_codes(cell).trim().to_string()) 32 - .collect() 33 - ) 34 + let rows = rows 35 + .iter() 36 + .map(|row| { 37 + row.iter() 38 + .map(|cell| strip_ansi_codes(cell).trim().to_string()) 39 + .collect() 40 + }) 34 41 .collect::<Vec<Vec<_>>>(); 35 42 36 43 let num_cols = usize::max( 37 44 titles.len(), 38 - rows.iter() 39 - .map(|row| row.len()) 40 - .max() 41 - .unwrap_or(0) 45 + rows.iter().map(|row| row.len()).max().unwrap_or(0), 42 46 ); 43 47 44 - let mut title_longest_words = titles.iter() 45 - .map(|title| Words::new(title) 46 - .map(|word| word.trim().len()) 47 - .max() 48 - .unwrap_or(0) 49 - ) 50 - .collect::<Vec<_>>(); 51 - title_longest_words.resize(num_cols, 0); 52 - let longest_words = rows.iter() 53 - .map(|row| row 54 - .iter() 55 - .map(|cell| Words::new(cell) 48 + let mut title_longest_words = titles 49 + .iter() 50 + .map(|title| { 51 + Words::new(title) 56 52 .map(|word| word.trim().len()) 57 53 .max() 58 54 .unwrap_or(0) 59 - ) 60 - .collect::<Vec<_>>() 61 - ) 55 + }) 56 + .collect::<Vec<_>>(); 57 + title_longest_words.resize(num_cols, 0); 58 + let longest_words = rows 59 + .iter() 60 + .map(|row| { 61 + row.iter() 62 + .map(|cell| { 63 + Words::new(cell) 64 + .map(|word| word.trim().len()) 65 + .max() 66 + .unwrap_or(0) 67 + }) 68 + .collect::<Vec<_>>() 69 + }) 62 70 .fold(title_longest_words.clone(), |mut chars, row| { 63 71 for i in 0..row.len() { 64 72 chars[i] = usize::max(chars[i], row[i]); ··· 66 74 chars 67 75 }); 68 76 69 - let mut title_chars = titles.iter() 70 - .map(|title| title 71 - .lines() 72 - .map(measure_text_width) 73 - .max() 74 - .unwrap_or(0) 75 - ) 77 + let mut title_chars = titles 78 + .iter() 79 + .map(|title| title.lines().map(measure_text_width).max().unwrap_or(0)) 76 80 .collect::<Vec<_>>(); 77 81 title_chars.resize(num_cols, 0); 78 - let max_chars_per_col = rows.iter() 79 - .map(|row| row 80 - .iter() 81 - .map(|cell| cell 82 - .lines() 83 - .map(measure_text_width) 84 - .max() 85 - .unwrap_or(0) 86 - ) 87 - .collect::<Vec<_>>() 88 - ) 82 + let max_chars_per_col = rows 83 + .iter() 84 + .map(|row| { 85 + row.iter() 86 + .map(|cell| cell.lines().map(measure_text_width).max().unwrap_or(0)) 87 + .collect::<Vec<_>>() 88 + }) 89 89 .fold(title_chars.clone(), |mut chars, row| { 90 90 for i in 0..row.len() { 91 91 chars[i] = usize::max(1, usize::max(chars[i], row[i])); ··· 101 101 max_chars_per_col 102 102 .into_iter() 103 103 .enumerate() 104 - .map(|(i, chars)| usize::max(longest_words[i], (max_chars_width as f64 * chars as f64 / total_chars as f64) as usize)) 104 + .map(|(i, chars)| { 105 + usize::max( 106 + longest_words[i], 107 + (max_chars_width as f64 * chars as f64 / total_chars as f64) as usize, 108 + ) 109 + }) 105 110 .collect() 106 111 }; 107 112 if col_widths.iter().sum::<usize>() > max_chars_width { ··· 127 132 } 128 133 } 129 134 130 - fn print_row<W: Write>(w: &mut W, cols: &[usize], alignment: &[Alignment], row: &[String], paper_style: Style) { 131 - let mut row_words = row 132 - .into_iter() 133 - .map(|s| Words::new(s)) 134 - .collect::<Vec<_>>(); 135 + fn print_row<W: Write>( 136 + w: &mut W, 137 + cols: &[usize], 138 + alignment: &[Alignment], 139 + row: &[String], 140 + paper_style: Style, 141 + ) { 142 + let mut row_words = row.into_iter().map(|s| Words::new(s)).collect::<Vec<_>>(); 135 143 loop { 136 144 let mut done = true; 137 145 write!(w, "{}", paper_style.paint("│")).unwrap(); ··· 139 147 let mut line = match words.next() { 140 148 Some(line) => line.trim().to_string(), 141 149 None => { 142 - write!(w, "{}", paper_style.paint(format!(" {: <width$} │", " ", width=cols[i]))).unwrap(); 150 + write!( 151 + w, 152 + "{}", 153 + paper_style.paint(format!(" {: <width$} │", " ", width = cols[i])) 154 + ) 155 + .unwrap(); 143 156 continue; 144 157 } 145 158 }; ··· 159 172 } 160 173 line = line.trim().to_string(); 161 174 let padded = if alignment[i] == Alignment::Center { 162 - format!(" {: ^width$} │", line, width=cols[i]) 175 + format!(" {: ^width$} │", line, width = cols[i]) 163 176 } else if alignment[i] == Alignment::Right { 164 - format!(" {: >width$} │", line, width=cols[i]) 177 + format!(" {: >width$} │", line, width = cols[i]) 165 178 } else { 166 - format!(" {: <width$} │", line, width=cols[i]) 179 + format!(" {: <width$} │", line, width = cols[i]) 167 180 }; 168 181 write!(w, "{}", paper_style.paint(padded)).unwrap(); 169 182 } ··· 174 187 } 175 188 } 176 189 177 - fn print_separator<W: Write>(w: &mut W, cols: &[usize], mid: char, left: char, cross: char, right: char, paper_style: Style) { 178 - let line = cols.iter() 190 + fn print_separator<W: Write>( 191 + w: &mut W, 192 + cols: &[usize], 193 + mid: char, 194 + left: char, 195 + cross: char, 196 + right: char, 197 + paper_style: Style, 198 + ) { 199 + let line = cols 200 + .iter() 179 201 .map(|width| mid.to_string().repeat(*width)) 180 202 .collect::<Vec<_>>() 181 203 .join(&format!("{}{}{}", mid, cross, mid)); 182 - write!(w, "{}\n", paper_style.paint(format!("{}{}{}{}{}", left, mid, line, mid, right))).unwrap(); 204 + write!( 205 + w, 206 + "{}\n", 207 + paper_style.paint(format!("{}{}{}{}{}", left, mid, line, mid, right)) 208 + ) 209 + .unwrap(); 183 210 }
+11 -9
src/words.rs
··· 44 44 self.position += start; 45 45 if start == chars.len() { 46 46 if chars.len() == 0 { 47 - return None 47 + return None; 48 48 } else if self.preserve_whitespace { 49 - return Some(chars[..].into_iter().collect()) 49 + return Some(chars[..].into_iter().collect()); 50 50 } else { 51 - return Some(" ".to_string()) 51 + return Some(" ".to_string()); 52 52 } 53 53 } 54 54 let mut len = 0; 55 - while start+len < chars.len() { 56 - if chars[start+len] == '-' { 55 + while start + len < chars.len() { 56 + if chars[start + len] == '-' { 57 57 len += 1; 58 58 break; 59 59 } 60 - if chars[start+len].is_whitespace() { 60 + if chars[start + len].is_whitespace() { 61 61 break; 62 62 } 63 63 len += 1; ··· 65 65 self.position += len; 66 66 if chars[0].is_whitespace() { 67 67 if self.preserve_whitespace { 68 - return Some(chars[0..start+len].into_iter().collect::<String>()) 68 + return Some(chars[0..start + len].into_iter().collect::<String>()); 69 69 } else { 70 - return Some(String::from(" ") + &chars[start..start+len].into_iter().collect::<String>()) 70 + return Some( 71 + String::from(" ") + &chars[start..start + len].into_iter().collect::<String>(), 72 + ); 71 73 } 72 74 } else { 73 - return Some(chars[start..start+len].into_iter().collect::<String>()) 75 + return Some(chars[start..start + len].into_iter().collect::<String>()); 74 76 } 75 77 } 76 78 }