1use std::fmt::Display;
8
9use cursive::theme::{ConcreteEffects, Effect, Style};
10use cursive::utils::markup::StyledString;
11use cursive::utils::span::Span;
12
13pub struct Pluralize<'a> {
32 pub determiner: Option<(&'a str, &'a str)>,
34
35 pub amount: usize,
37
38 pub unit: (&'a str, &'a str),
40}
41
42impl Display for Pluralize<'_> {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self {
46 amount: 1,
47 unit: (unit, _),
48 determiner: None,
49 } => write!(f, "{} {}", 1, unit),
50
51 Self {
52 amount,
53 unit: (_, unit),
54 determiner: None,
55 } => write!(f, "{amount} {unit}"),
56
57 Self {
58 amount: 1,
59 unit: (unit, _),
60 determiner: Some((determiner, _)),
61 } => write!(f, "{} {} {}", determiner, 1, unit),
62
63 Self {
64 amount,
65 unit: (_, unit),
66 determiner: Some((_, determiner)),
67 } => write!(f, "{determiner} {amount} {unit}"),
68 }
69 }
70}
71
72#[derive(Clone)]
74pub struct Glyphs {
75 pub should_write_ansi_escape_codes: bool,
78
79 pub line: &'static str,
81
82 pub line_with_offshoot: &'static str,
84
85 pub vertical_ellipsis: &'static str,
87
88 pub split: &'static str,
90
91 pub merge: &'static str,
93
94 pub commit_visible: &'static str,
96
97 pub commit_visible_head: &'static str,
99
100 pub commit_obsolete: &'static str,
102
103 pub commit_obsolete_head: &'static str,
105
106 pub commit_main: &'static str,
109
110 pub commit_main_head: &'static str,
113
114 pub commit_main_obsolete: &'static str,
117
118 pub commit_main_obsolete_head: &'static str,
121
122 pub commit_omitted: &'static str,
125
126 pub commit_merge: &'static str,
129
130 pub commit_merge_rev: &'static str,
133
134 pub branch_arrow: &'static str,
136
137 pub bullet_point: &'static str,
139
140 pub cycle_arrow: &'static str,
142
143 pub cycle_horizontal_line: &'static str,
145
146 pub cycle_vertical_line: &'static str,
148
149 pub cycle_upper_left_corner: &'static str,
151
152 pub cycle_lower_left_corner: &'static str,
154}
155
156impl Glyphs {
157 pub fn detect() -> Self {
159 let color_support = concolor::get(concolor::Stream::Stdout);
160 if color_support.color() {
161 Glyphs::pretty()
162 } else {
163 Glyphs::text()
164 }
165 }
166
167 pub fn text() -> Self {
169 Glyphs {
170 should_write_ansi_escape_codes: false,
171 line: "|",
172 line_with_offshoot: "|",
173 vertical_ellipsis: ":",
174 split: "\\",
175 merge: "/",
176 commit_visible: "o",
177 commit_visible_head: "@",
178 commit_obsolete: "x",
179 commit_obsolete_head: "%",
180 commit_main: "O",
181 commit_main_head: "@",
182 commit_main_obsolete: "X",
183 commit_main_obsolete_head: "%",
184 commit_omitted: "#",
185 commit_merge: "&",
186 commit_merge_rev: "&",
187 branch_arrow: ">",
188 bullet_point: "-",
189 cycle_arrow: ">",
190 cycle_horizontal_line: "-",
191 cycle_vertical_line: "|",
192 cycle_upper_left_corner: ",",
193 cycle_lower_left_corner: "`",
194 }
195 }
196
197 pub fn pretty() -> Self {
199 Glyphs {
200 should_write_ansi_escape_codes: true,
201 line: "│",
202 line_with_offshoot: "├",
203 vertical_ellipsis: "⋮",
204 split: "─╮",
205 merge: "─╯",
206 commit_visible: "○",
207 commit_visible_head: "●",
208 commit_obsolete: "✕",
209 commit_obsolete_head: "⦻",
210 commit_omitted: "◌",
211 commit_merge: "↓",
212 commit_merge_rev: "↑",
213 commit_main: "◇",
214 commit_main_head: "◆",
215 commit_main_obsolete: "✕",
216 commit_main_obsolete_head: "❖",
217 branch_arrow: "ᐅ",
218 bullet_point: "•",
219 cycle_arrow: "ᐅ",
220 cycle_horizontal_line: "─",
221 cycle_vertical_line: "│",
222 cycle_upper_left_corner: "┌",
223 cycle_lower_left_corner: "└",
224 }
225 }
226
227 pub fn reverse_order(mut self, reverse: bool) -> Self {
230 if reverse {
231 std::mem::swap(&mut self.split, &mut self.merge);
232 std::mem::swap(&mut self.commit_merge, &mut self.commit_merge_rev);
233 }
234 self
235 }
236
237 pub fn render(&self, string: StyledString) -> eyre::Result<String> {
242 let result = string
243 .spans()
244 .map(|span| {
245 let Span {
246 content,
247 attr,
248 width: _,
249 } = span;
250 if self.should_write_ansi_escape_codes {
251 Ok(render_style_as_ansi(content, *attr)?)
252 } else {
253 Ok(content.to_string())
254 }
255 })
256 .collect::<eyre::Result<String>>()?;
257 Ok(result)
258 }
259}
260
261impl std::fmt::Debug for Glyphs {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(
264 f,
265 "<Glyphs pretty={:?}>",
266 self.should_write_ansi_escape_codes
267 )
268 }
269}
270
271pub struct StyledStringBuilder {
274 elements: Vec<StyledString>,
275}
276
277impl Default for StyledStringBuilder {
278 fn default() -> Self {
279 StyledStringBuilder::new()
280 }
281}
282
283impl StyledStringBuilder {
284 pub fn new() -> Self {
286 Self {
287 elements: Vec::new(),
288 }
289 }
290
291 fn append_plain_inner(mut self, text: &str) -> Self {
292 self.elements.push(StyledString::plain(text));
293 self
294 }
295
296 pub fn append_plain(self, text: impl AsRef<str>) -> Self {
298 self.append_plain_inner(text.as_ref())
299 }
300
301 fn append_styled_inner(mut self, text: &str, style: Style) -> Self {
302 self.elements.push(StyledString::styled(text, style));
303 self
304 }
305
306 pub fn append_styled(self, text: impl AsRef<str>, style: impl Into<Style>) -> Self {
309 self.append_styled_inner(text.as_ref(), style.into())
310 }
311
312 fn append_inner(mut self, text: StyledString) -> Self {
313 self.elements.push(text);
314 self
315 }
316
317 pub fn append(self, text: impl Into<StyledString>) -> Self {
319 self.append_inner(text.into())
320 }
321
322 pub fn build(self) -> StyledString {
325 let mut result = StyledString::new();
326 for element in self.elements {
327 result.append(element);
328 }
329 result
330 }
331
332 pub fn join(delimiter: &str, strings: Vec<StyledString>) -> StyledString {
335 let mut result = Self::new();
336 let mut is_first = true;
337 for string in strings {
338 if is_first {
339 is_first = false;
340 } else {
341 result = result.append_plain(delimiter);
342 }
343 result = result.append(string);
344 }
345 result.into()
346 }
347
348 pub fn from_lines(lines: Vec<StyledString>) -> StyledString {
352 let mut result = Self::new();
353 for line in lines {
354 result = result.append(line);
355 result = result.append_plain("\n");
356 }
357 result.into()
358 }
359}
360
361pub fn set_effect(mut string: StyledString, effect: Effect) -> StyledString {
363 string.spans_raw_attr_mut().for_each(|span| {
364 span.attr.effects.insert(effect);
365 });
366 string
367}
368
369impl From<StyledStringBuilder> for StyledString {
370 fn from(builder: StyledStringBuilder) -> Self {
371 builder.build()
372 }
373}
374
375fn render_style_as_ansi(content: &str, style: Style) -> eyre::Result<String> {
376 let Style { effects, color } = style;
377 let output = {
378 use console::style;
379 use cursive::theme::{BaseColor, Color, ColorType};
380 let output = content.to_string();
381 match color.front {
382 ColorType::Palette(_) => {
383 eyre::bail!("Not implemented: using cursive palette colors")
384 }
385 ColorType::Color(Color::Rgb(..)) | ColorType::Color(Color::RgbLowRes(..)) => {
386 eyre::bail!("Not implemented: using raw RGB colors")
387 }
388 ColorType::InheritParent | ColorType::Color(Color::TerminalDefault) => style(output),
389 ColorType::Color(Color::Light(color)) => match color {
390 BaseColor::Black => style(output).black().bright(),
391 BaseColor::Red => style(output).red().bright(),
392 BaseColor::Green => style(output).green().bright(),
393 BaseColor::Yellow => style(output).yellow().bright(),
394 BaseColor::Blue => style(output).blue().bright(),
395 BaseColor::Magenta => style(output).magenta().bright(),
396 BaseColor::Cyan => style(output).cyan().bright(),
397 BaseColor::White => style(output).white().bright(),
398 },
399 ColorType::Color(Color::Dark(color)) => match color {
400 BaseColor::Black => style(output).black(),
401 BaseColor::Red => style(output).red(),
402 BaseColor::Green => style(output).green(),
403 BaseColor::Yellow => style(output).yellow(),
404 BaseColor::Blue => style(output).blue(),
405 BaseColor::Magenta => style(output).magenta(),
406 BaseColor::Cyan => style(output).cyan(),
407 BaseColor::White => style(output).white(),
408 },
409 }
410 };
411
412 let output = {
413 let mut output = output;
414 for effect in effects.resolve(ConcreteEffects::empty()) {
415 output = match effect {
416 Effect::Simple => output,
417 Effect::Dim => output.dim(),
418 Effect::Reverse => output.reverse(),
419 Effect::Bold => output.bold(),
420 Effect::Italic => output.italic(),
421 Effect::Strikethrough => eyre::bail!("Not implemented: Effect::Strikethrough"),
422 Effect::Underline => output.underlined(),
423 Effect::Blink => output.blink(),
424 };
425 }
426 output
427 };
428
429 let output = output.force_styling(true);
435
436 Ok(output.to_string())
437}