tangled
alpha
login
or
join now
eldridge.cam
/
paper-terminal
0
fork
atom
Print Markdown to a paper in your terminal
0
fork
atom
overview
issues
pulls
pipelines
Handle wide characters better
eldridge.cam
5 years ago
40d6a707
a6186503
+161
-99
6 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
main.rs
printer.rs
table.rs
words.rs
+1
Cargo.lock
···
437
437
"structopt",
438
438
"syncat-stylesheet",
439
439
"terminal_size",
440
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
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
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
21
+
22
22
+
fn str_width(s: &str) -> usize {
23
23
+
strip_ansi_codes(s)
24
24
+
.chars()
25
25
+
.flat_map(UnicodeWidthChar::width)
26
26
+
.sum()
27
27
+
}
20
28
21
29
/// Prints papers in your terminal
22
30
#[derive(StructOpt, Debug)]
···
113
121
return;
114
122
}
115
123
116
116
-
let centering = " ".repeat((terminal_width - width) / 2);
124
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
156
-
if buffer.chars().count() + word.chars().count() > available_width {
164
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
162
-
paper_style.paint(" ".repeat(available_width - buffer.chars().count())),
170
170
+
paper_style.paint(
171
171
+
" ".repeat(available_width.saturating_sub(str_width(&buffer)))
172
172
+
),
163
173
margin,
164
174
shadow_style.paint(" "),
165
175
);
···
182
192
centering,
183
193
margin,
184
194
paper_style.paint(&buffer),
185
185
-
paper_style.paint(" ".repeat(available_width - buffer.chars().count())),
195
195
+
paper_style
196
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
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
5
-
use console::{measure_text_width, AnsiCodeIterator};
6
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
192
-
Some((format!("{}", style.paint(&prefix)), prefix.chars().count()))
193
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
211
-
Some((format!("{}", style.paint(&suffix)), suffix.chars().count()))
212
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
275
-
self.paper_style()
276
276
-
.paint(" ".repeat(self.width - prefix_len - suffix_len)),
276
276
+
self.paper_style().paint(
277
277
+
" ".repeat(
278
278
+
self.width
279
279
+
.saturating_sub(prefix_len)
280
280
+
.saturating_sub(suffix_len)
281
281
+
)
282
282
+
),
277
283
suffix,
278
284
self.margin,
279
285
self.shadow(),
···
289
295
self.centering,
290
296
self.margin,
291
297
prefix,
292
292
-
self.style()
293
293
-
.paint("─".repeat(self.width - prefix_len - suffix_len)),
298
298
+
self.style().paint(
299
299
+
"─".repeat(
300
300
+
self.width
301
301
+
.saturating_sub(prefix_len)
302
302
+
.saturating_sub(suffix_len)
303
303
+
)
304
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
307
-
let available_width = self.width - self.prefix_len() - self.suffix_len();
318
318
+
let available_width = self
319
319
+
.width
320
320
+
.saturating_sub(self.prefix_len())
321
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
320
-
.paint(" ".repeat(available_width - measure_text_width(line))),
334
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
341
-
let available_width = self.width
342
342
-
- first_prefix.as_ref().unwrap().1
343
343
-
- first_suffix.as_ref().unwrap().1;
355
355
+
let available_width = self
356
356
+
.width
357
357
+
.saturating_sub(first_prefix.as_ref().unwrap().1)
358
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
371
-
while line.chars().count() > available_width {
386
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
380
-
" ".repeat(available_width - line.chars().count())
395
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
404
-
let width = measure_text_width(line);
419
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
427
-
style.paint(" ".repeat(available_width - width)),
442
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
447
-
style.paint(" ".repeat(available_width - lang.chars().count())),
462
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
492
-
self.width - measure_text_width(&self.content) - prefix_len - suffix_len
507
507
+
self.width
508
508
+
.saturating_sub(str_width(&self.content))
509
509
+
.saturating_sub(prefix_len)
510
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
532
-
if measure_text_width(&self.content)
533
533
-
+ word.len()
534
534
-
+ self.prefix_len()
535
535
-
+ self.suffix_len()
550
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
545
-
let available_len = self.width - self.prefix_len() - self.suffix_len();
546
546
-
while measure_text_width(&self.content) + word.len() > available_len {
560
560
+
let available_len = self
561
561
+
.width
562
562
+
.saturating_sub(self.prefix_len())
563
563
+
.saturating_sub(self.suffix_len());
564
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
643
-
let available_width =
644
644
-
self.width - self.prefix_len() - self.suffix_len();
661
661
+
let available_width = self
662
662
+
.width
663
663
+
.saturating_sub(self.prefix_len())
664
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
1
-
use std::io::Write;
1
1
+
use crate::words::Words;
2
2
use ansi_term::Style;
3
3
-
use pulldown_cmark::Alignment;
4
3
use console::{measure_text_width, strip_ansi_codes};
5
5
-
use crate::words::Words;
4
4
+
use pulldown_cmark::Alignment;
5
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
23
-
let Table { titles, rows, width } = self;
23
23
+
let Table {
24
24
+
titles,
25
25
+
rows,
26
26
+
width,
27
27
+
} = self;
24
28
25
29
// NOTE: for now, styling is not supported within tables because that gets really hard
26
26
-
let titles = titles.iter()
30
30
+
let titles = titles
31
31
+
.iter()
27
32
.map(|title| strip_ansi_codes(title).trim().to_string())
28
33
.collect::<Vec<_>>();
29
29
-
let rows = rows.iter()
30
30
-
.map(|row| row.iter()
31
31
-
.map(|cell| strip_ansi_codes(cell).trim().to_string())
32
32
-
.collect()
33
33
-
)
34
34
+
let rows = rows
35
35
+
.iter()
36
36
+
.map(|row| {
37
37
+
row.iter()
38
38
+
.map(|cell| strip_ansi_codes(cell).trim().to_string())
39
39
+
.collect()
40
40
+
})
34
41
.collect::<Vec<Vec<_>>>();
35
42
36
43
let num_cols = usize::max(
37
44
titles.len(),
38
38
-
rows.iter()
39
39
-
.map(|row| row.len())
40
40
-
.max()
41
41
-
.unwrap_or(0)
45
45
+
rows.iter().map(|row| row.len()).max().unwrap_or(0),
42
46
);
43
47
44
44
-
let mut title_longest_words = titles.iter()
45
45
-
.map(|title| Words::new(title)
46
46
-
.map(|word| word.trim().len())
47
47
-
.max()
48
48
-
.unwrap_or(0)
49
49
-
)
50
50
-
.collect::<Vec<_>>();
51
51
-
title_longest_words.resize(num_cols, 0);
52
52
-
let longest_words = rows.iter()
53
53
-
.map(|row| row
54
54
-
.iter()
55
55
-
.map(|cell| Words::new(cell)
48
48
+
let mut title_longest_words = titles
49
49
+
.iter()
50
50
+
.map(|title| {
51
51
+
Words::new(title)
56
52
.map(|word| word.trim().len())
57
53
.max()
58
54
.unwrap_or(0)
59
59
-
)
60
60
-
.collect::<Vec<_>>()
61
61
-
)
55
55
+
})
56
56
+
.collect::<Vec<_>>();
57
57
+
title_longest_words.resize(num_cols, 0);
58
58
+
let longest_words = rows
59
59
+
.iter()
60
60
+
.map(|row| {
61
61
+
row.iter()
62
62
+
.map(|cell| {
63
63
+
Words::new(cell)
64
64
+
.map(|word| word.trim().len())
65
65
+
.max()
66
66
+
.unwrap_or(0)
67
67
+
})
68
68
+
.collect::<Vec<_>>()
69
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
69
-
let mut title_chars = titles.iter()
70
70
-
.map(|title| title
71
71
-
.lines()
72
72
-
.map(measure_text_width)
73
73
-
.max()
74
74
-
.unwrap_or(0)
75
75
-
)
77
77
+
let mut title_chars = titles
78
78
+
.iter()
79
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
78
-
let max_chars_per_col = rows.iter()
79
79
-
.map(|row| row
80
80
-
.iter()
81
81
-
.map(|cell| cell
82
82
-
.lines()
83
83
-
.map(measure_text_width)
84
84
-
.max()
85
85
-
.unwrap_or(0)
86
86
-
)
87
87
-
.collect::<Vec<_>>()
88
88
-
)
82
82
+
let max_chars_per_col = rows
83
83
+
.iter()
84
84
+
.map(|row| {
85
85
+
row.iter()
86
86
+
.map(|cell| cell.lines().map(measure_text_width).max().unwrap_or(0))
87
87
+
.collect::<Vec<_>>()
88
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
104
-
.map(|(i, chars)| usize::max(longest_words[i], (max_chars_width as f64 * chars as f64 / total_chars as f64) as usize))
104
104
+
.map(|(i, chars)| {
105
105
+
usize::max(
106
106
+
longest_words[i],
107
107
+
(max_chars_width as f64 * chars as f64 / total_chars as f64) as usize,
108
108
+
)
109
109
+
})
105
110
.collect()
106
111
};
107
112
if col_widths.iter().sum::<usize>() > max_chars_width {
···
127
132
}
128
133
}
129
134
130
130
-
fn print_row<W: Write>(w: &mut W, cols: &[usize], alignment: &[Alignment], row: &[String], paper_style: Style) {
131
131
-
let mut row_words = row
132
132
-
.into_iter()
133
133
-
.map(|s| Words::new(s))
134
134
-
.collect::<Vec<_>>();
135
135
+
fn print_row<W: Write>(
136
136
+
w: &mut W,
137
137
+
cols: &[usize],
138
138
+
alignment: &[Alignment],
139
139
+
row: &[String],
140
140
+
paper_style: Style,
141
141
+
) {
142
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
142
-
write!(w, "{}", paper_style.paint(format!(" {: <width$} │", " ", width=cols[i]))).unwrap();
150
150
+
write!(
151
151
+
w,
152
152
+
"{}",
153
153
+
paper_style.paint(format!(" {: <width$} │", " ", width = cols[i]))
154
154
+
)
155
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
162
-
format!(" {: ^width$} │", line, width=cols[i])
175
175
+
format!(" {: ^width$} │", line, width = cols[i])
163
176
} else if alignment[i] == Alignment::Right {
164
164
-
format!(" {: >width$} │", line, width=cols[i])
177
177
+
format!(" {: >width$} │", line, width = cols[i])
165
178
} else {
166
166
-
format!(" {: <width$} │", line, width=cols[i])
179
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
177
-
fn print_separator<W: Write>(w: &mut W, cols: &[usize], mid: char, left: char, cross: char, right: char, paper_style: Style) {
178
178
-
let line = cols.iter()
190
190
+
fn print_separator<W: Write>(
191
191
+
w: &mut W,
192
192
+
cols: &[usize],
193
193
+
mid: char,
194
194
+
left: char,
195
195
+
cross: char,
196
196
+
right: char,
197
197
+
paper_style: Style,
198
198
+
) {
199
199
+
let line = cols
200
200
+
.iter()
179
201
.map(|width| mid.to_string().repeat(*width))
180
202
.collect::<Vec<_>>()
181
203
.join(&format!("{}{}{}", mid, cross, mid));
182
182
-
write!(w, "{}\n", paper_style.paint(format!("{}{}{}{}{}", left, mid, line, mid, right))).unwrap();
204
204
+
write!(
205
205
+
w,
206
206
+
"{}\n",
207
207
+
paper_style.paint(format!("{}{}{}{}{}", left, mid, line, mid, right))
208
208
+
)
209
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
47
-
return None
47
47
+
return None;
48
48
} else if self.preserve_whitespace {
49
49
-
return Some(chars[..].into_iter().collect())
49
49
+
return Some(chars[..].into_iter().collect());
50
50
} else {
51
51
-
return Some(" ".to_string())
51
51
+
return Some(" ".to_string());
52
52
}
53
53
}
54
54
let mut len = 0;
55
55
-
while start+len < chars.len() {
56
56
-
if chars[start+len] == '-' {
55
55
+
while start + len < chars.len() {
56
56
+
if chars[start + len] == '-' {
57
57
len += 1;
58
58
break;
59
59
}
60
60
-
if chars[start+len].is_whitespace() {
60
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
68
-
return Some(chars[0..start+len].into_iter().collect::<String>())
68
68
+
return Some(chars[0..start + len].into_iter().collect::<String>());
69
69
} else {
70
70
-
return Some(String::from(" ") + &chars[start..start+len].into_iter().collect::<String>())
70
70
+
return Some(
71
71
+
String::from(" ") + &chars[start..start + len].into_iter().collect::<String>(),
72
72
+
);
71
73
}
72
74
} else {
73
73
-
return Some(chars[start..start+len].into_iter().collect::<String>())
75
75
+
return Some(chars[start..start + len].into_iter().collect::<String>());
74
76
}
75
77
}
76
78
}