use crate::core::RequestExecutor;
use crate::tui::{AppEvent, AppMode, AppState, EventHandler};
use crate::ui::{render_app, restore_terminal, setup_terminal};
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use std::path::Path;
pub struct TuiApp {
state: AppState,
event_handler: EventHandler,
executor: RequestExecutor,
}
impl TuiApp {
pub async fn new(collection_path: impl AsRef<Path>) -> Result<Self> {
let state = AppState::new(collection_path.as_ref().to_path_buf()).await?;
let event_handler = EventHandler::new();
let executor = RequestExecutor::new();
Ok(Self {
state,
event_handler,
executor,
})
}
pub async fn run(mut self) -> Result<()> {
let mut terminal = setup_terminal()?;
let result = self.run_app(&mut terminal).await;
restore_terminal(&mut terminal)?;
result
}
async fn run_app(
&mut self,
terminal: &mut ratatui::Terminal<impl ratatui::backend::Backend>,
) -> Result<()> {
loop {
terminal.draw(|frame| render_app(frame, &self.state))?;
if let Some(event) = self.event_handler.next().await {
match event {
AppEvent::Key(key) => {
if self.handle_key_event(key).await? {
break;
}
}
AppEvent::ExecutionStarted => {
self.state.is_executing = true;
self.state.status_message = "Executing request...".to_string();
}
AppEvent::ExecutionCompleted(response) => {
self.state.is_executing = false;
self.state.status_message = format!(
"Request completed - Status: {} ({}ms)",
response.status, response.timing.total_ms
);
if let Some(request) = self.state.get_current_request() {
let _ = self
.state
.save_response_to_history(request.name.clone(), response.clone())
.await;
}
self.state.current_response = Some(response);
}
AppEvent::ExecutionFailed(error) => {
self.state.is_executing = false;
self.state.status_message = format!("Request failed: {}", error);
self.state.current_response = None;
}
AppEvent::Quit => break,
}
}
if self.state.should_quit {
break;
}
}
Ok(())
}
async fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
match self.state.mode {
AppMode::Normal => self.handle_normal_mode_keys(key).await,
AppMode::Filter => self.handle_filter_mode_keys(key),
AppMode::Variables => self.handle_variables_mode_keys(key).await,
AppMode::History => self.handle_history_mode_keys(key),
AppMode::Command => self.handle_command_mode_keys(key).await,
}
}
async fn handle_normal_mode_keys(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Char('q') => {
self.state.should_quit = true;
return Ok(true);
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.state.should_quit = true;
return Ok(true);
}
KeyCode::Up | KeyCode::Char('k') => {
self.state.move_selection_up();
}
KeyCode::Down | KeyCode::Char('j') => {
self.state.move_selection_down();
}
KeyCode::Enter => {
self.execute_current_request().await?;
}
KeyCode::Char('/') => {
self.state.mode = AppMode::Filter;
self.state.filter_text.clear();
}
KeyCode::Char('v') => {
self.state.mode = AppMode::Variables;
}
KeyCode::Char('h') => {
self.state.mode = AppMode::History;
}
KeyCode::Char(':') => {
self.state.mode = AppMode::Command;
}
KeyCode::Char('e') => {
self.edit_current_request().await?;
}
KeyCode::Tab => {
self.state.next_response_tab();
}
KeyCode::BackTab => {
self.state.previous_response_tab();
}
KeyCode::Char('s') if self.state.current_response.is_some() => {
self.state.status_message = "Response variable saved (simplified)".to_string();
}
_ => {}
}
Ok(false)
}
fn handle_filter_mode_keys(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Enter => {
self.state.mode = AppMode::Normal;
}
KeyCode::Esc => {
self.state.mode = AppMode::Normal;
self.state.update_filter("".to_string());
}
KeyCode::Backspace => {
self.state.filter_text.pop();
self.state.update_filter(self.state.filter_text.clone());
}
KeyCode::Char(c) => {
self.state.filter_text.push(c);
self.state.update_filter(self.state.filter_text.clone());
}
_ => {}
}
Ok(false)
}
async fn handle_variables_mode_keys(
&mut self,
key: crossterm::event::KeyEvent,
) -> Result<bool> {
match key.code {
KeyCode::Esc | KeyCode::Char('v') => {
self.state.mode = AppMode::Normal;
}
_ => {}
}
Ok(false)
}
fn handle_history_mode_keys(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Esc | KeyCode::Char('h') => {
self.state.mode = AppMode::Normal;
}
KeyCode::Up | KeyCode::Char('k') => {
if self.state.history_selected_index > 0 {
self.state.history_selected_index -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if self.state.history_selected_index + 1 < self.state.history.entries.len() {
self.state.history_selected_index += 1;
}
}
KeyCode::Enter => {
if let Some(entry) = self
.state
.history
.entries
.get(self.state.history_selected_index)
{
self.state.current_response = Some(entry.response.clone());
self.state.mode = AppMode::Normal;
}
}
_ => {}
}
Ok(false)
}
async fn handle_command_mode_keys(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Esc => {
self.state.mode = AppMode::Normal;
}
KeyCode::Char('e') => {
self.edit_current_request().await?;
self.state.mode = AppMode::Normal;
}
_ => {}
}
Ok(false)
}
async fn execute_current_request(&mut self) -> Result<()> {
if let Some(request) = self.state.get_current_request() {
if self.state.is_executing {
return Ok(());
}
let request = request.clone();
let interpolator = self.state.interpolator.clone();
let executor = self.executor.clone();
let tx = self.event_handler.get_sender();
tokio::spawn(async move {
let _ = tx.send(AppEvent::ExecutionStarted);
match executor
.execute_with_interpolator(&request, &interpolator)
.await
{
Ok(response) => {
let _ = tx.send(AppEvent::ExecutionCompleted(response));
}
Err(e) => {
let _ = tx.send(AppEvent::ExecutionFailed(e.to_string()));
}
}
});
self.state.is_executing = true;
self.state.status_message = "Starting request execution...".to_string();
}
Ok(())
}
async fn edit_current_request(&mut self) -> Result<()> {
use std::process::Command;
if let Some(request) = self.state.get_current_request() {
let request_name = request.name.clone();
let mut terminal = crate::ui::setup_terminal()?;
crate::ui::restore_terminal(&mut terminal)?;
let temp_dir = std::env::temp_dir();
let temp_file =
temp_dir.join(format!("netbook_{}.json", request_name.replace(' ', "_")));
let content = serde_json::to_string_pretty(request)?;
std::fs::write(&temp_file, &content)?;
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vi".to_string());
let status = Command::new(&editor).arg(&temp_file).status()?;
if status.success() {
if let Ok(edited_content) = std::fs::read_to_string(&temp_file) {
if let Ok(edited_request) =
serde_json::from_str::<crate::core::Request>(&edited_content)
{
if let Some(req) = self
.state
.collection
.iter_mut()
.find(|r| r.name == request_name)
{
*req = edited_request;
}
if let Err(e) = crate::io::save_collection(
&self.state.collection,
&self.state.collection_path,
) {
self.state.status_message = format!("Failed to save: {}", e);
} else {
self.state.status_message =
format!("✓ Updated request '{}'", request_name);
}
} else {
self.state.status_message =
"Error: Invalid JSON in edited file".to_string();
}
} else {
self.state.status_message = "Error: Could not read edited file".to_string();
}
} else {
self.state.status_message = "Editor exited with error".to_string();
}
let _ = std::fs::remove_file(&temp_file);
let _terminal = crate::ui::setup_terminal()?;
} else {
self.state.status_message = "No request selected".to_string();
}
Ok(())
}
}
impl Clone for RequestExecutor {
fn clone(&self) -> Self {
RequestExecutor::new()
}
}