coco/core/state/
commit.rs

1use {
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 trimmed scope is not none or empty, then add it to the title
83            if !self.scope.trim().is_empty() {
84                format!("({})", self.scope.trim())
85            } else {
86                "".to_string()
87            },
88            if self.breaking { "!" } else { "" },
89            // if trimmed emoji is not none or empty, then add it to the title
90            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}