coco/core/state/
commit.rs1use {
2 matetui::ratatui::text::{Line, Text},
3 rust_i18n::t,
4 std::{
5 borrow::Cow,
6 fmt::{Display, Formatter},
7 },
8 unicode_width::UnicodeWidthStr,
9};
10
11#[derive(Debug, Clone)]
12pub struct CommitInfo {
13 pub hash: String,
14 pub author: String,
15 pub author_email: String,
16 pub date: String,
17}
18
19#[derive(Debug, Clone)]
20pub struct Commit {
21 pub info: Option<CommitInfo>,
22 pub message: Option<ConventionalCommitMessage>,
23}
24
25#[derive(Debug, Clone)]
26pub struct ConventionalCommitMessage {
27 pub kind: String,
28 pub emoji: String,
29 pub scope: String,
30 pub summary: String,
31 pub body: Vec<String>,
32 pub footer: Vec<String>,
33 pub breaking: bool,
34}
35
36impl ConventionalCommitMessage {
37 fn title_width(&self) -> u16 {
38 UnicodeWidthStr::width(self.raw_title().as_str()) as u16
39 }
40
41 fn body_width(&self) -> u16 {
42 self.body.iter().map(|s| UnicodeWidthStr::width(s.as_str())).max().unwrap_or(0) as u16
43 }
44
45 fn footer_width(&self) -> u16 {
46 self.footer.iter().map(|s| UnicodeWidthStr::width(s.as_str())).max().unwrap_or(0) as u16
47 }
48
49 pub fn width(&self) -> u16 {
50 self.title_width().max(self.body_width()).max(self.footer_width())
51 }
52
53 pub fn height(&self) -> u16 {
54 let mut height = 1;
55
56 let body = self.body.join("\n");
57 if !body.is_empty() {
58 height += (self.body.len() + 1) as u16;
59 }
60
61 let footer = self.footer.join("\n");
62
63 if !footer.is_empty() {
64 height += (self.footer.len() + 1) as u16;
65 }
66
67 height
68 }
69
70 pub fn raw_body(&self) -> String {
71 self.body.join("\n").trim().to_string()
72 }
73
74 pub fn raw_footer(&self) -> String {
75 self.footer.join("\n").trim().to_string()
76 }
77
78 pub fn raw_title(&self) -> String {
79 format!(
80 "{}{}{}: {}{}",
81 self.kind,
82 if !self.scope.trim().is_empty() {
84 format!("({})", self.scope.trim())
85 } else {
86 "".to_string()
87 },
88 if self.breaking { "!" } else { "" },
89 if !self.emoji.trim().is_empty() {
91 format!("{} ", self.emoji.trim())
92 } else {
93 "".to_string()
94 },
95 self.summary
96 )
97 .trim()
98 .to_string()
99 }
100
101 pub fn raw_commit(&self) -> String {
102 let mut commit = self.raw_title();
103
104 let raw_body = self.raw_body();
105 let raw_footer = self.raw_footer();
106
107 if !raw_body.is_empty() {
108 commit.push_str("\n\n");
109 commit.push_str(raw_body.as_str());
110 }
111
112 if !raw_footer.is_empty() {
113 commit.push_str("\n\n");
114 commit.push_str(raw_footer.as_str());
115 }
116
117 commit
118 }
119
120 pub fn raw_full_body(&self) -> String {
121 let mut result = "".to_string();
122
123 let raw_body = self.raw_body();
124 let raw_footer = self.raw_footer();
125
126 if !raw_body.is_empty() {
127 result.push_str(raw_body.as_str());
128 }
129
130 if !raw_footer.is_empty() {
131 if !raw_body.is_empty() {
132 result.push_str("\n\n");
133 }
134 result.push_str(raw_footer.as_str());
135 }
136
137 result
138 }
139
140 pub fn size(&self) -> (u16, u16) {
141 (self.width(), self.height())
142 }
143}
144
145impl Display for Commit {
146 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147 use matetui::ratatui::crossterm::style::Stylize;
148
149 if let Some(info) = &self.info {
150 writeln!(f, "{} {}", t!("Commit").yellow().bold(), info.hash.as_str().yellow())?;
151 writeln!(f, "{} {} <{}>", t!("Author").bold(), info.author, info.author_email)?;
152 writeln!(f, "{} {}", t!("Date").bold(), info.date)?;
153 writeln!(f)?;
154 }
155
156 if let Some(message) = &self.message {
157 write!(f, "{}", message.raw_title().blue())?;
158
159 let body = message.raw_full_body();
160 if !body.is_empty() {
161 write!(f, "\n\n{}", body)?;
162 }
163 }
164
165 Ok(())
166 }
167}
168
169impl Commit {
170 pub fn as_text(&self) -> Text<'static> {
171 use matetui::ratatui::style::Stylize;
172
173 #[inline]
174 fn normalize_labels(
175 author: Cow<str>,
176 date: Cow<str>,
177 commit: Cow<str>,
178 ) -> (String, String, String) {
179 let max_len = author.len().max(date.len()).max(commit.len());
180 (
181 format!("{:<width$} ", author, width = max_len),
182 format!("{:<width$} ", date, width = max_len),
183 format!("{:<width$} ", commit, width = max_len),
184 )
185 }
186
187 if self.info.is_none() || self.message.is_none() {
188 panic!("Commit info or message missing, something went wrong");
189 }
190
191 let message = self.message.as_ref().unwrap();
192 let (hash, author, date) = {
193 let info = self.info.as_ref().unwrap();
194 (
195 info.hash.to_string(),
196 format!("{} <{}>", info.author, info.author_email),
197 info.date.to_string(),
198 )
199 };
200
201 let (author_lbl, date_lbl, commit_lbl) =
202 normalize_labels(t!("Author"), t!("Date"), t!("Commit"));
203
204 let mut text = Text::from(vec![
205 Line::from(vec![commit_lbl.bold().yellow(), hash.yellow()]),
206 Line::from(vec![author_lbl.bold(), author.into()]),
207 Line::from(vec![date_lbl.bold(), date.into()]),
208 "".into(),
209 message.raw_title().to_string().blue().into(),
210 ]);
211
212 let body = message.raw_full_body();
213 if !body.is_empty() {
214 text.push_line(Line::from(""));
215 let body_lines = body.lines().map(|l| l.to_string()).collect::<Vec<_>>();
216 for line in body_lines {
217 text.push_line(Line::from(line));
218 }
219 }
220
221 text
222 }
223}