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