term_transcript/svg/data.rs
1//! Data provided to Handlebars templates.
2
3use std::{collections::HashMap, fmt};
4
5use serde::Serialize;
6
7use super::write::StyledLine;
8use crate::{
9 svg::{EmbeddedFont, TemplateOptions},
10 UserInput,
11};
12
13/// Root data structure sent to the Handlebars template.
14///
15/// # Examples
16///
17/// Here's example of JSON serialization of this type:
18///
19/// ```
20/// # use term_transcript::{svg::{TemplateOptions, NamedPalette}, Transcript, UserInput};
21/// let mut transcript = Transcript::new();
22/// let input = UserInput::command("rainbow");
23/// transcript.add_interaction(input, "Hello, \u{1b}[32mworld\u{1b}[0m!");
24/// let template_options = TemplateOptions {
25/// palette: NamedPalette::Dracula.into(),
26/// font_family: "Consolas, Menlo, monospace".to_owned(),
27/// ..TemplateOptions::default()
28/// }
29/// .validated()?;
30/// let data = template_options.render_data(&transcript).unwrap();
31///
32/// let expected_json = serde_json::json!({
33/// "creator": {
34/// "name": "term-transcript",
35/// "version": "0.4.0",
36/// "repo": "https://github.com/slowli/term-transcript",
37/// },
38/// "width": 720,
39/// "line_height": null,
40/// "advance_width": null,
41/// "palette": {
42/// "colors": {
43/// "black": "#282936",
44/// "red": "#ea51b2",
45/// "green": "#ebff87",
46/// "yellow": "#00f769",
47/// "blue": "#62d6e8",
48/// "magenta": "#b45bcf",
49/// "cyan": "#a1efe4",
50/// "white": "#e9e9f4",
51/// },
52/// "intense_colors": {
53/// "black": "#626483",
54/// "red": "#b45bcf",
55/// "green": "#3a3c4e",
56/// "yellow": "#4d4f68",
57/// "blue": "#62d6e8",
58/// "magenta": "#f1f2f8",
59/// "cyan": "#00f769",
60/// "white": "#f7f7fb",
61/// },
62/// },
63/// "font_family": "Consolas, Menlo, monospace",
64/// "window": null,
65/// "wrap": {
66/// "hard_break_at": {
67/// "chars": 80,
68/// "mark": "ยป",
69/// },
70/// },
71/// "line_numbers": null,
72/// "dim_opacity": 0.7,
73/// "blink": {
74/// "opacity": 0.7,
75/// "interval": 1.0,
76/// },
77/// "has_failures": false,
78/// "interactions": [{
79/// "input": {
80/// "text": "rainbow",
81/// "prompt": "$",
82/// "hidden": false,
83/// },
84/// "output": [{
85/// "spans": [
86/// { "text": "Hello, " },
87/// { "text": "world", "fg": 2 },
88/// { "text": "!" },
89/// ],
90/// }],
91/// "failure": false,
92/// "exit_status": null,
93/// }]
94/// });
95/// assert_eq!(serde_json::to_value(data).unwrap(), expected_json);
96/// # anyhow::Ok(())
97/// ```
98#[derive(Debug, Serialize)]
99#[non_exhaustive]
100pub struct HandlebarsData<'r> {
101 /// Information about the rendering software.
102 pub creator: CreatorData,
103 /// Template options used for rendering. These options are flattened into the parent
104 /// during serialization.
105 #[serde(flatten)]
106 pub options: &'r TemplateOptions,
107 /// Recorded terminal interactions.
108 pub interactions: Vec<SerializedInteraction<'r>>,
109 /// Has any of terminal interactions failed?
110 pub has_failures: bool,
111 /// A font (usually subset) to be embedded into the generated transcript.
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub embedded_font: Option<EmbeddedFont>,
114}
115
116// 1. font-subset -> term-transcript -> font-subset-cli, ..
117// Problem: different workspaces / repos; meaning that font-subset-cli will depend on 2 `font-subset`s (????)
118// Patching the font-subset dep sort of works, unless term-transcript code needs to be modified
119//
120// 2. same, but font-subset is an optional dep in term-transcript, not used in font-subset-cli
121// (replaced with a local module)
122
123/// Information about software used for rendering (i.e., this crate).
124///
125/// It can make sense to include this info as a comment in the rendered template
126/// for debugging purposes.
127#[derive(Debug, Serialize)]
128#[non_exhaustive]
129pub struct CreatorData {
130 /// Name of software rendering the template.
131 pub name: &'static str,
132 /// Version of the rendering software.
133 pub version: &'static str,
134 /// Link to the git repository with the rendering software.
135 pub repo: &'static str,
136}
137
138impl Default for CreatorData {
139 fn default() -> Self {
140 Self {
141 name: env!("CARGO_PKG_NAME"),
142 version: env!("CARGO_PKG_VERSION"),
143 repo: env!("CARGO_PKG_REPOSITORY"),
144 }
145 }
146}
147
148/// Serializable version of [`Interaction`](crate::Interaction).
149#[derive(Serialize)]
150#[non_exhaustive]
151pub struct SerializedInteraction<'a> {
152 /// User's input.
153 pub input: &'a UserInput,
154 /// Terminal output in the [HTML format](#html-output).
155 pub(crate) output: Vec<StyledLine>,
156 /// Exit status of the latest executed program, or `None` if it cannot be determined.
157 pub exit_status: Option<i32>,
158 /// Was execution unsuccessful judging by the [`ExitStatus`](crate::ExitStatus)?
159 pub failure: bool,
160}
161
162impl fmt::Debug for SerializedInteraction<'_> {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 formatter
165 .debug_struct("SerializedInteraction")
166 .field("input", &self.input)
167 .field("output.line_count", &self.output.len())
168 .field("exit_status", &self.exit_status)
169 .finish_non_exhaustive()
170 }
171}
172
173#[derive(Debug, Serialize)]
174pub(super) struct CompleteHandlebarsData<'r> {
175 #[serde(flatten)]
176 pub inner: HandlebarsData<'r>,
177 #[serde(rename = "const")]
178 pub constants: &'r HashMap<&'static str, u32>,
179}