Skip to main content

gitkit_cli/tui/
page.rs

1use crossterm::event::{KeyCode, KeyEvent};
2use ratatui::{
3    Frame,
4    layout::{Alignment, Constraint, HorizontalAlignment::Center, Layout, Rect},
5    style::{Stylize, palette::material::WHITE},
6    text::{Line, Text},
7    widgets::{Block, Padding, Paragraph, Wrap},
8};
9
10use crate::{
11    git::{kit::KitRepo, model::KitCommit, status::KitStatus, util},
12    tui::{ACCENT, ACCENT_TEXT, GITKIT_ASCII, Renderable},
13};
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
16pub enum Page {
17    #[default]
18    Home = 0,
19    Cadence = 1,
20    Silo = 2,
21}
22
23impl Page {
24    pub const ALL: [Page; 3] = [Page::Home, Page::Cadence, Page::Silo];
25
26    pub fn to_str(&self) -> &'static str {
27        match self {
28            Page::Home => "Home",
29            Page::Cadence => "Cadence",
30            Page::Silo => "Silo",
31        }
32    }
33
34    pub fn size() -> usize {
35        Self::ALL.len()
36    }
37
38    pub fn next(&self) -> Page {
39        match &self {
40            Page::Home => Page::Cadence,
41            Page::Cadence => Page::Silo,
42            Page::Silo => Page::Home,
43        }
44    }
45}
46
47pub struct HomeData {
48    pub repo_name: String, // directory name
49    pub current_branch: String,
50    pub total_commits: u32,
51    pub status: KitStatus,
52    pub first_commit: Option<KitCommit>,
53    pub last_commit: Option<KitCommit>,
54}
55
56impl HomeData {
57    pub fn new(repo: &KitRepo) -> Self {
58        let workdir = repo.inner.workdir().unwrap_or_else(|| repo.inner.path());
59
60        let repo_name = workdir
61            .file_name()
62            .map(|n| n.to_string_lossy().to_string())
63            .unwrap_or_else(|| "unkown workdir".to_string());
64        let current_branch = repo
65            .current_branch()
66            .unwrap_or_else(|_| "not found".to_owned());
67
68        let total_commits: u32 = repo
69            .iter_commits()
70            .map_or(0, |iter| iter.count())
71            .try_into()
72            .unwrap_or(u32::MAX);
73
74        let status = repo.get_status();
75
76        // commit iter is reversed
77        let first_commit: Option<KitCommit> =
78            repo.iter_commits().map_or(None, |commits| commits.last());
79
80        let last_commit = repo
81            .iter_commits()
82            .map_or(None, |mut commits| commits.next());
83
84        HomeData {
85            repo_name,
86            current_branch,
87            total_commits,
88            status,
89            first_commit,
90            last_commit,
91        }
92    }
93}
94
95pub struct HomePage {
96    data: HomeData,
97}
98
99impl HomePage {
100    pub fn new(data: HomeData) -> Self {
101        HomePage { data }
102    }
103
104    pub fn handle_key(&mut self, key_event: KeyEvent, _repo: &KitRepo, refresh: &mut bool) {
105        match key_event.code {
106            KeyCode::Char('r') => *refresh = true,
107            _ => {}
108        }
109    }
110
111    // TODO make this scrollable
112    fn info_box(&self, frame: &mut Frame, area: Rect) {
113        let title = Line::from(vec![
114            "|<".into(),
115            "Repository: ".fg(ACCENT_TEXT).bold(),
116            format!("{}", self.data.repo_name).fg(ACCENT),
117            ">|".into(),
118        ]);
119        let general = Block::bordered()
120            .title(title)
121            .title_alignment(Center)
122            .padding(Padding {
123                left: 2,
124                right: 0,
125                top: 1,
126                bottom: 0,
127            });
128
129        let active_since = util::active_since(&self.data.first_commit);
130        let last_activity = util::last_activity(&self.data.last_commit);
131
132        let mut info = vec![
133            Line::from(vec![
134                "branch: ".fg(ACCENT).bold(),
135                format!("{}", self.data.current_branch).fg(WHITE),
136            ]),
137            Line::from(vec![
138                "total commits: ".fg(ACCENT).bold(),
139                format!("{}", self.data.total_commits).fg(WHITE),
140            ]),
141            active_since,
142            last_activity,
143        ];
144
145        let status = &self.data.status;
146
147        info.push(Line::from("status: ".fg(ACCENT).bold()));
148        info.extend(status.tui_print());
149
150        // +2 borders + 1 padding (top) TODO make this better
151        // paragraph wrap seems to break the last info lines
152        let min_height = info.len() as u16 + (info.len() as u16) / 2 + 3;
153
154        let vertical_chunks =
155            Layout::vertical([Constraint::Length(min_height), Constraint::Min(0)]).split(area);
156
157        let info_area = vertical_chunks[0].centered_horizontally(Constraint::Percentage(50));
158
159        let paragraph = Paragraph::new(info)
160            .block(general)
161            .alignment(Alignment::Left)
162            .wrap(Wrap { trim: true });
163
164        frame.render_widget(paragraph, info_area);
165    }
166}
167
168impl<'repo> Renderable for HomePage {
169    fn render(&mut self, frame: &mut ratatui::prelude::Frame, area: ratatui::prelude::Rect) {
170        let header_height = GITKIT_ASCII.lines().count() as u16;
171
172        let subtext_height = 3;
173        let chunks = Layout::vertical([
174            Constraint::Length(header_height),
175            Constraint::Length(1), // padding
176            Constraint::Length(subtext_height),
177            Constraint::Length(2), // padding
178            Constraint::Min(0),    // rest of page
179        ])
180        .split(area);
181
182        let header = Text::from(GITKIT_ASCII).alignment(Center).style(WHITE);
183        let sub_text = Text::from(format!(
184            "gitkit version {}\n made by {} \nrepo: {}",
185            env!("CARGO_PKG_VERSION"),
186            env!("CARGO_PKG_AUTHORS"),
187            env!("CARGO_PKG_REPOSITORY")
188        ))
189        .style(ACCENT_TEXT)
190        .italic()
191        .alignment(Center);
192
193        frame.render_widget(header, chunks[0]);
194        frame.render_widget(sub_text, chunks[2]);
195        self.info_box(frame, chunks[4]);
196    }
197}