1use super::input_handler::{InputResult, handle_input};
2use super::spinner::SpinnerState;
3use super::state::{Mode, TuiState};
4use super::ui::draw_ui;
5use crate::debug;
6use crate::features::commit::{CommitService, format_commit_result, types::GeneratedMessage};
7use anyhow::{Error, Result};
8use crossterm::event::KeyEventKind;
9use ratatui::{
10 Terminal,
11 backend::CrosstermBackend,
12 crossterm::{
13 event::{self, Event},
14 execute,
15 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
16 },
17};
18
19use std::io;
20use std::sync::Arc;
21use std::time::Duration;
22
23pub struct TuiCommit {
24 pub state: TuiState,
25 service: Arc<CommitService>,
26}
27
28impl TuiCommit {
29 pub fn new(
30 initial_messages: Vec<GeneratedMessage>,
31 custom_instructions: String,
32 preset: String,
33 user_name: String,
34 user_email: String,
35 service: Arc<CommitService>,
36 ) -> Self {
37 let state = TuiState::new(
38 initial_messages,
39 custom_instructions,
40 preset,
41 user_name,
42 user_email,
43 );
44
45 Self { state, service }
46 }
47
48 #[allow(clippy::unused_async)]
49 pub async fn run(
50 initial_messages: Vec<GeneratedMessage>,
51 custom_instructions: String,
52 selected_preset: String,
53 user_name: String,
54 user_email: String,
55 service: Arc<CommitService>,
56 ) -> Result<()> {
57 let mut app = Self::new(
58 initial_messages,
59 custom_instructions,
60 selected_preset,
61 user_name,
62 user_email,
63 service,
64 );
65
66 app.run_app().map_err(Error::from)
67 }
68
69 pub fn run_app(&mut self) -> io::Result<()> {
70 enable_raw_mode()?;
72 let mut stdout = io::stdout();
73 execute!(stdout, EnterAlternateScreen)?;
74 let backend = CrosstermBackend::new(stdout);
75 let mut terminal = Terminal::new(backend)?;
76
77 let result = self.main_loop(&mut terminal);
79
80 disable_raw_mode()?;
82 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
83 terminal.show_cursor()?;
84
85 match result {
87 Ok(exit_status) => match exit_status {
88 ExitStatus::Committed(message) => {
89 println!("{message}");
90 }
91 ExitStatus::Cancelled => {
92 println!("Commit operation cancelled. Your changes remain staged.");
93 }
94 ExitStatus::Error(error_message) => {
95 eprintln!("An error occurred: {error_message}");
96 }
97 },
98 Err(e) => {
99 eprintln!("An unexpected error occurred: {e}");
100 return Err(io::Error::other(e.to_string()));
101 }
102 }
103
104 Ok(())
105 }
106
107 fn main_loop(
108 &mut self,
109 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
110 ) -> anyhow::Result<ExitStatus> {
111 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<GeneratedMessage, anyhow::Error>>(1);
112 let mut task_spawned = false;
113
114 loop {
115 if self.state.dirty {
117 terminal.draw(|f| draw_ui(f, &mut self.state))?;
118 self.state.dirty = false; }
120
121 if self.state.mode == Mode::Generating && !task_spawned {
123 let service = self.service.clone();
124 let preset = self.state.selected_preset.clone();
125 let instructions = self.state.custom_instructions.clone();
126 let tx = tx.clone();
127
128 tokio::spawn(async move {
129 debug!("Generating message...");
130 let result = service.generate_message(&preset, &instructions).await;
131 let _ = tx.send(result).await;
132 });
133
134 task_spawned = true; }
136
137 match rx.try_recv() {
139 Ok(result) => match result {
140 Ok(new_message) => {
141 self.state.messages.push(new_message);
142 self.state.current_index = self.state.messages.len() - 1;
143
144 self.state.update_message_textarea();
145 self.state.mode = Mode::Normal; self.state.spinner = None; self.state
148 .set_status(String::from("New message generated successfully!"));
149 task_spawned = false; }
151 Err(e) => {
152 self.state.mode = Mode::Normal; self.state.spinner = None; self.state
155 .set_status(format!("Failed to generate new message: {e}. Press 'r' to retry or 'Esc' to exit."));
156 task_spawned = false; }
158 },
159 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
160 }
162 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
163 break;
165 }
166 }
167
168 if event::poll(Duration::from_millis(20))?
170 && let Event::Key(key) = event::read()?
171 && key.kind == KeyEventKind::Press
172 {
173 match handle_input(self, key) {
174 InputResult::Exit => return Ok(ExitStatus::Cancelled),
175 InputResult::Commit(message) => match self.perform_commit(&message) {
176 Ok(status) => return Ok(status),
177 Err(e) => {
178 self.state.set_status(format!("Commit failed: {e}"));
179 self.state.dirty = true;
180 }
181 },
182 InputResult::Continue => self.state.dirty = true,
183 }
184 }
185
186 if self.state.mode == Mode::Generating
188 && self.state.last_spinner_update.elapsed() >= Duration::from_millis(100)
189 {
190 if let Some(spinner) = &mut self.state.spinner {
191 spinner.tick();
192 self.state.dirty = true; }
194 self.state.last_spinner_update = std::time::Instant::now(); }
196 }
197
198 Ok(ExitStatus::Cancelled)
199 }
200
201 pub fn handle_regenerate(&mut self) {
202 self.state.mode = Mode::Generating;
203 self.state.spinner = Some(SpinnerState::new());
204 }
205
206 pub fn perform_commit(&self, message: &str) -> Result<ExitStatus, Error> {
207 match self.service.perform_commit(message) {
208 Ok(result) => {
209 let output = format_commit_result(&result, message);
210 Ok(ExitStatus::Committed(output))
211 }
212 Err(e) => Ok(ExitStatus::Error(e.to_string())),
213 }
214 }
215}
216
217#[allow(clippy::unused_async)]
218pub async fn run_tui_commit(
219 initial_messages: Vec<GeneratedMessage>,
220 custom_instructions: String,
221 selected_preset: String,
222 user_name: String,
223 user_email: String,
224 service: Arc<CommitService>,
225) -> Result<()> {
226 TuiCommit::run(
227 initial_messages,
228 custom_instructions,
229 selected_preset,
230 user_name,
231 user_email,
232 service,
233 )
234 .await
235}
236
237pub enum ExitStatus {
238 Committed(String),
239 Cancelled,
240 Error(String),
241}