mod app;
mod diff_parser;
mod ui;
use anyhow::Result;
use app::{HunkSplitApp, HunkSplitMode};
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::time::Duration;
pub fn run(no_verify: bool) -> Result<()> {
let mut app = HunkSplitApp::new(no_verify)?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_app(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
match result {
Ok(true) => match app.finalize() {
Ok(()) => {
println!("Split complete! Created {} branches.", app.round);
println!("Use `stax ls` to see the new stack structure.");
Ok(())
}
Err(e) => {
app.rollback();
Err(e)
}
},
Ok(false) => {
app.rollback();
println!("Split aborted.");
Ok(())
}
Err(e) => {
app.rollback();
Err(e)
}
}
}
fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut HunkSplitApp,
) -> Result<bool> {
loop {
terminal.draw(|f| ui::render(f, app))?;
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('c'))
{
return Ok(false);
}
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
match &app.mode {
HunkSplitMode::List => handle_list_key(app, key.code, shift),
HunkSplitMode::Sequential => handle_sequential_key(app, key.code, shift),
HunkSplitMode::Naming => handle_naming_key(app, key.code),
HunkSplitMode::ConfirmAbort => handle_confirm_abort_key(app, key.code),
HunkSplitMode::Help => handle_help_key(app, key.code),
}
}
}
if app.should_quit {
return Ok(false);
}
if app.round_complete {
app.round_complete = false;
let branch_name = app.input_buffer.trim().to_string();
app.input_buffer.clear();
app.input_cursor = 0;
app.status_message = Some(format!("Committing '{}'...", branch_name));
terminal.draw(|f| ui::render(f, app))?;
let has_remaining = app.commit_round(&branch_name)?;
if has_remaining {
app.mode = HunkSplitMode::List;
app.status_message = Some(format!(
"Committed '{}'. Select hunks for round {}.",
branch_name, app.round
));
} else {
app.all_done = true;
}
}
if app.all_done {
return Ok(true);
}
}
}
fn try_finish_round(app: &mut HunkSplitApp) {
if app.selected_count() > 0 {
app.mode = HunkSplitMode::Naming;
app.input_buffer = app.suggest_branch_name();
app.input_cursor = app.input_buffer.len();
} else {
app.status_message = Some("No hunks selected".to_string());
}
}
fn handle_list_key(app: &mut HunkSplitApp, code: KeyCode, shift: bool) {
match code {
KeyCode::Down | KeyCode::Char('j' | 'J') if shift => app.scroll_diff_down(),
KeyCode::Up | KeyCode::Char('k' | 'K') if shift => app.scroll_diff_up(),
KeyCode::Down | KeyCode::Char('j') => app.move_cursor_down(),
KeyCode::Up | KeyCode::Char('k') => app.move_cursor_up(),
KeyCode::PageDown => app.scroll_diff_down(),
KeyCode::PageUp => app.scroll_diff_up(),
KeyCode::Char(' ') => app.toggle_current(),
KeyCode::Char('a') => app.toggle_file(),
KeyCode::Char('u') => app.undo(),
KeyCode::Tab => {
app.mode = HunkSplitMode::Sequential;
app.status_message = Some("Sequential mode: y/n to accept/skip hunks".to_string());
}
KeyCode::Enter => try_finish_round(app),
KeyCode::Char('q') | KeyCode::Esc => app.mode = HunkSplitMode::ConfirmAbort,
KeyCode::Char('?') => app.mode = HunkSplitMode::Help,
_ => {}
}
}
fn handle_sequential_key(app: &mut HunkSplitApp, code: KeyCode, shift: bool) {
match code {
KeyCode::Down | KeyCode::Char('j' | 'J') if shift => app.scroll_diff_down(),
KeyCode::Up | KeyCode::Char('k' | 'K') if shift => app.scroll_diff_up(),
KeyCode::PageDown => app.scroll_diff_down(),
KeyCode::PageUp => app.scroll_diff_up(),
KeyCode::Char('y') => app.accept_and_advance(),
KeyCode::Char('n') => app.skip_and_advance(),
KeyCode::Char('a') => {
app.toggle_file();
app.advance_past_current_file();
}
KeyCode::Char('u') => app.undo(),
KeyCode::Tab => {
app.mode = HunkSplitMode::List;
app.status_message = Some("List mode".to_string());
}
KeyCode::Enter => try_finish_round(app),
KeyCode::Char('q') | KeyCode::Esc => app.mode = HunkSplitMode::ConfirmAbort,
KeyCode::Char('?') => app.mode = HunkSplitMode::Help,
_ => {}
}
}
fn handle_naming_key(app: &mut HunkSplitApp, code: KeyCode) {
match code {
KeyCode::Enter => {
let name = app.input_buffer.trim().to_string();
match app.validate_branch_name(&name) {
Ok(()) => {
app.round_complete = true;
}
Err(msg) => {
app.status_message = Some(msg);
}
}
}
KeyCode::Esc => {
app.mode = HunkSplitMode::List;
app.input_buffer.clear();
app.input_cursor = 0;
}
KeyCode::Char(c) => {
app.input_buffer.insert(app.input_cursor, c);
app.input_cursor += 1;
}
KeyCode::Backspace => {
if app.input_cursor > 0 {
app.input_cursor -= 1;
app.input_buffer.remove(app.input_cursor);
}
}
KeyCode::Left => {
if app.input_cursor > 0 {
app.input_cursor -= 1;
}
}
KeyCode::Right => {
if app.input_cursor < app.input_buffer.len() {
app.input_cursor += 1;
}
}
_ => {}
}
}
fn handle_confirm_abort_key(app: &mut HunkSplitApp, code: KeyCode) {
match code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
app.should_quit = true;
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.mode = HunkSplitMode::List;
}
_ => {}
}
}
fn handle_help_key(app: &mut HunkSplitApp, _code: KeyCode) {
app.mode = HunkSplitMode::List;
}