1use std::fmt::Display;
8
9use cursive::theme::{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 branch_arrow: &'static str,
132
133 pub bullet_point: &'static str,
135
136 pub cycle_arrow: &'static str,
138
139 pub cycle_horizontal_line: &'static str,
141
142 pub cycle_vertical_line: &'static str,
144
145 pub cycle_upper_left_corner: &'static str,
147
148 pub cycle_lower_left_corner: &'static str,
150}
151
152impl Glyphs {
153 pub fn detect() -> Self {
155 let color_support = concolor::get(concolor::Stream::Stdout);
156 if color_support.color() {
157 Glyphs::pretty()
158 } else {
159 Glyphs::text()
160 }
161 }
162
163 pub fn text() -> Self {
165 Glyphs {
166 should_write_ansi_escape_codes: false,
167 line: "|",
168 line_with_offshoot: "|",
169 vertical_ellipsis: ":",
170 split: "\\",
171 merge: "/",
172 commit_visible: "o",
173 commit_visible_head: "@",
174 commit_obsolete: "x",
175 commit_obsolete_head: "%",
176 commit_main: "O",
177 commit_main_head: "@",
178 commit_main_obsolete: "X",
179 commit_main_obsolete_head: "%",
180 commit_omitted: "#",
181 commit_merge: "&",
182 branch_arrow: ">",
183 bullet_point: "-",
184 cycle_arrow: ">",
185 cycle_horizontal_line: "-",
186 cycle_vertical_line: "|",
187 cycle_upper_left_corner: ",",
188 cycle_lower_left_corner: "`",
189 }
190 }
191
192 pub fn pretty() -> Self {
194 Glyphs {
195 should_write_ansi_escape_codes: true,
196 line: "┃",
197 line_with_offshoot: "┣",
198 vertical_ellipsis: "⋮",
199 split: "━┓",
200 merge: "━┛",
201 commit_visible: "◯",
202 commit_visible_head: "●",
203 commit_obsolete: "✕",
204 commit_obsolete_head: "⦻",
205 commit_omitted: "◌",
206 commit_merge: "↓",
207 commit_main: "◇",
208 commit_main_head: "◆",
209 commit_main_obsolete: "✕",
210 commit_main_obsolete_head: "❖",
211 branch_arrow: "ᐅ",
212 bullet_point: "•",
213 cycle_arrow: "ᐅ",
214 cycle_horizontal_line: "─",
215 cycle_vertical_line: "│",
216 cycle_upper_left_corner: "┌",
217 cycle_lower_left_corner: "└",
218 }
219 }
220
221 pub fn reverse_order(mut self, reverse: bool) -> Self {
224 if reverse {
225 std::mem::swap(&mut self.split, &mut self.merge);
226 }
227 self
228 }
229
230 pub fn render(&self, string: StyledString) -> eyre::Result<String> {
235 let result = string
236 .spans()
237 .map(|span| {
238 let Span {
239 content,
240 attr,
241 width: _,
242 } = span;
243 if self.should_write_ansi_escape_codes {
244 Ok(render_style_as_ansi(content, *attr)?)
245 } else {
246 Ok(content.to_string())
247 }
248 })
249 .collect::<eyre::Result<String>>()?;
250 Ok(result)
251 }
252}
253
254impl std::fmt::Debug for Glyphs {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(
257 f,
258 "<Glyphs pretty={:?}>",
259 self.should_write_ansi_escape_codes
260 )
261 }
262}
263
264pub struct StyledStringBuilder {
267 elements: Vec<StyledString>,
268}
269
270impl Default for StyledStringBuilder {
271 fn default() -> Self {
272 StyledStringBuilder::new()
273 }
274}
275
276impl StyledStringBuilder {
277 pub fn new() -> Self {
279 Self {
280 elements: Vec::new(),
281 }
282 }
283
284 fn append_plain_inner(mut self, text: &str) -> Self {
285 self.elements.push(StyledString::plain(text));
286 self
287 }
288
289 pub fn append_plain(self, text: impl AsRef<str>) -> Self {
291 self.append_plain_inner(text.as_ref())
292 }
293
294 fn append_styled_inner(mut self, text: &str, style: Style) -> Self {
295 self.elements.push(StyledString::styled(text, style));
296 self
297 }
298
299 pub fn append_styled(self, text: impl AsRef<str>, style: impl Into<Style>) -> Self {
302 self.append_styled_inner(text.as_ref(), style.into())
303 }
304
305 fn append_inner(mut self, text: StyledString) -> Self {
306 self.elements.push(text);
307 self
308 }
309
310 pub fn append(self, text: impl Into<StyledString>) -> Self {
312 self.append_inner(text.into())
313 }
314
315 pub fn build(self) -> StyledString {
318 let mut result = StyledString::new();
319 for element in self.elements {
320 result.append(element);
321 }
322 result
323 }
324
325 pub fn join(delimiter: &str, strings: Vec<StyledString>) -> StyledString {
328 let mut result = Self::new();
329 let mut is_first = true;
330 for string in strings {
331 if is_first {
332 is_first = false;
333 } else {
334 result = result.append_plain(delimiter);
335 }
336 result = result.append(string);
337 }
338 result.into()
339 }
340
341 pub fn from_lines(lines: Vec<StyledString>) -> StyledString {
345 let mut result = Self::new();
346 for line in lines {
347 result = result.append(line);
348 result = result.append_plain("\n");
349 }
350 result.into()
351 }
352}
353
354pub fn set_effect(mut string: StyledString, effect: Effect) -> StyledString {
356 string.spans_raw_attr_mut().for_each(|span| {
357 span.attr.effects.insert(effect);
358 });
359 string
360}
361
362impl From<StyledStringBuilder> for StyledString {
363 fn from(builder: StyledStringBuilder) -> Self {
364 builder.build()
365 }
366}
367
368fn render_style_as_ansi(content: &str, style: Style) -> eyre::Result<String> {
369 let Style { effects, color } = style;
370 let output = {
371 use console::style;
372 use cursive::theme::{BaseColor, Color, ColorType};
373 let output = content.to_string();
374 match color.front {
375 ColorType::Palette(_) => {
376 eyre::bail!("Not implemented: using cursive palette colors")
377 }
378 ColorType::Color(Color::Rgb(..)) | ColorType::Color(Color::RgbLowRes(..)) => {
379 eyre::bail!("Not implemented: using raw RGB colors")
380 }
381 ColorType::InheritParent | ColorType::Color(Color::TerminalDefault) => style(output),
382 ColorType::Color(Color::Light(color)) => match color {
383 BaseColor::Black => style(output).black().bright(),
384 BaseColor::Red => style(output).red().bright(),
385 BaseColor::Green => style(output).green().bright(),
386 BaseColor::Yellow => style(output).yellow().bright(),
387 BaseColor::Blue => style(output).blue().bright(),
388 BaseColor::Magenta => style(output).magenta().bright(),
389 BaseColor::Cyan => style(output).cyan().bright(),
390 BaseColor::White => style(output).white().bright(),
391 },
392 ColorType::Color(Color::Dark(color)) => match color {
393 BaseColor::Black => style(output).black(),
394 BaseColor::Red => style(output).red(),
395 BaseColor::Green => style(output).green(),
396 BaseColor::Yellow => style(output).yellow(),
397 BaseColor::Blue => style(output).blue(),
398 BaseColor::Magenta => style(output).magenta(),
399 BaseColor::Cyan => style(output).cyan(),
400 BaseColor::White => style(output).white(),
401 },
402 }
403 };
404
405 let output = {
406 let mut output = output;
407 for effect in effects.iter() {
408 output = match effect {
409 Effect::Simple => output,
410 Effect::Dim => output.dim(),
411 Effect::Reverse => output.reverse(),
412 Effect::Bold => output.bold(),
413 Effect::Italic => output.italic(),
414 Effect::Strikethrough => eyre::bail!("Not implemented: Effect::Strikethrough"),
415 Effect::Underline => output.underlined(),
416 Effect::Blink => output.blink(),
417 };
418 }
419 output
420 };
421
422 let output = output.force_styling(true);
428
429 Ok(output.to_string())
430}