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