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 service: Arc<CommitService>,
33 ) -> Self {
34 let state = TuiState::new(initial_messages, custom_instructions);
35
36 Self { state, service }
37 }
38
39 #[allow(clippy::unused_async)]
40 pub async fn run(
41 initial_messages: Vec<GeneratedMessage>,
42 custom_instructions: String,
43 service: Arc<CommitService>,
44 ) -> Result<()> {
45 let mut app = Self::new(initial_messages, custom_instructions, service);
46
47 app.run_app().map_err(Error::from)
48 }
49
50 pub fn run_app(&mut self) -> io::Result<()> {
51 enable_raw_mode()?;
53 let mut stdout = io::stdout();
54 execute!(stdout, EnterAlternateScreen)?;
55 let backend = CrosstermBackend::new(stdout);
56 let mut terminal = Terminal::new(backend)?;
57
58 let result = self.main_loop(&mut terminal);
60
61 disable_raw_mode()?;
63 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
64 terminal.show_cursor()?;
65
66 match result {
68 Ok(exit_status) => match exit_status {
69 ExitStatus::Committed(message) => {
70 println!("{message}");
71 }
72 ExitStatus::Cancelled => {
73 println!("Commit operation cancelled. Your changes remain staged.");
74 }
75 ExitStatus::Error(error_message) => {
76 eprintln!("An error occurred: {error_message}");
77 }
78 },
79 Err(e) => {
80 eprintln!("An unexpected error occurred: {e}");
81 return Err(io::Error::other(e.to_string()));
82 }
83 }
84
85 Ok(())
86 }
87
88 fn main_loop(
89 &mut self,
90 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
91 ) -> anyhow::Result<ExitStatus> {
92 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<GeneratedMessage, anyhow::Error>>(1);
93 let mut task_spawned = false;
94
95 loop {
96 if self.state.dirty {
98 terminal.draw(|f| draw_ui(f, &mut self.state))?;
99 self.state.dirty = false; }
101
102 if self.state.mode == Mode::Generating && !task_spawned {
104 let service = self.service.clone();
105 let instructions = self.state.custom_instructions.clone();
106 let tx = tx.clone();
107
108 tokio::spawn(async move {
109 debug!("Generating message...");
110 let result = service.generate_message(&instructions).await;
111 let _ = tx.send(result).await;
112 });
113
114 task_spawned = true; }
116
117 match rx.try_recv() {
119 Ok(result) => match result {
120 Ok(new_message) => {
121 self.state.messages.push(new_message);
122 self.state.current_index = self.state.messages.len() - 1;
123
124 self.state.update_message_textarea();
125 self.state.mode = Mode::Normal; self.state.spinner = None; self.state
128 .set_status(String::from("New message generated successfully!"));
129 task_spawned = false; }
131 Err(e) => {
132 self.state.mode = Mode::Normal; self.state.spinner = None; self.state
135 .set_status(format!("Failed to generate new message: {e}. Press 'r' to retry or 'Esc' to exit."));
136 task_spawned = false; }
138 },
139 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
140 }
142 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
143 break;
145 }
146 }
147
148 if event::poll(Duration::from_millis(20))?
150 && let Event::Key(key) = event::read()?
151 && key.kind == KeyEventKind::Press
152 {
153 match handle_input(self, key) {
154 InputResult::Exit => return Ok(ExitStatus::Cancelled),
155 InputResult::Commit(message) => match self.perform_commit(&message) {
156 Ok(status) => return Ok(status),
157 Err(e) => {
158 self.state.set_status(format!("Commit failed: {e}"));
159 self.state.dirty = true;
160 }
161 },
162 InputResult::Continue => self.state.dirty = true,
163 }
164 }
165
166 if self.state.mode == Mode::Generating
168 && self.state.last_spinner_update.elapsed() >= Duration::from_millis(100)
169 {
170 if let Some(spinner) = &mut self.state.spinner {
171 spinner.tick();
172 self.state.dirty = true; }
174 self.state.last_spinner_update = std::time::Instant::now(); }
176 }
177
178 Ok(ExitStatus::Cancelled)
179 }
180
181 pub fn handle_regenerate(&mut self) {
182 self.state.mode = Mode::Generating;
183 self.state.spinner = Some(SpinnerState::new());
184 }
185
186 pub fn perform_commit(&self, message: &str) -> Result<ExitStatus, Error> {
187 match self.service.perform_commit(message) {
188 Ok(result) => {
189 let output = format_commit_result(&result, message);
190 Ok(ExitStatus::Committed(output))
191 }
192 Err(e) => Ok(ExitStatus::Error(e.to_string())),
193 }
194 }
195}
196
197#[allow(clippy::unused_async)]
198pub async fn run_tui_commit(
199 initial_messages: Vec<GeneratedMessage>,
200 custom_instructions: String,
201 service: Arc<CommitService>,
202) -> Result<()> {
203 TuiCommit::run(initial_messages, custom_instructions, service).await
204}
205
206pub enum ExitStatus {
207 Committed(String),
208 Cancelled,
209 Error(String),
210}