mod commands;
mod input;
mod loading;
mod navigation;
#[cfg(test)]
mod tests;
use tui_input::Input;
use crate::error::XorcistError;
use crate::jj::{GraphLog, JjRunner, ShowOutput, fetch_show};
use crate::text::truncate_str;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum View {
#[default]
Log,
Detail,
Diff,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputMode {
Describe,
BookmarkSet,
NewWithMessage,
RebaseDestination,
}
impl InputMode {
pub fn placeholder(&self) -> &'static str {
match self {
InputMode::Describe => "Enter commit message...",
InputMode::BookmarkSet => "Enter bookmark name...",
InputMode::NewWithMessage => "Enter message (empty for no message)...",
InputMode::RebaseDestination => "Enter destination (e.g., @-, main, abc123)...",
}
}
}
#[derive(Debug, Clone)]
pub struct DetailState {
pub show_output: ShowOutput,
pub scroll: usize,
pub content_height: usize,
}
#[derive(Debug, Clone, Default)]
pub struct DiffState {
pub change_id: String,
pub files: Vec<crate::jj::DiffEntry>,
pub selected: usize,
pub file_scroll: usize,
pub diff_lines: Vec<String>,
pub diff_scroll: usize,
pub diff_h_scroll: usize,
}
impl DiffState {
pub fn new(change_id: String, files: Vec<crate::jj::DiffEntry>) -> Self {
Self {
change_id,
files,
selected: 0,
file_scroll: 0,
diff_lines: Vec::new(),
diff_scroll: 0,
diff_h_scroll: 0,
}
}
pub fn selected_file(&self) -> Option<&crate::jj::DiffEntry> {
self.files.get(self.selected)
}
}
#[derive(Debug, Clone)]
pub enum PendingAction {
Abandon {
change_id: String,
description: String,
},
Squash {
change_id: String,
description: String,
},
GitPush,
Undo,
}
impl PendingAction {
pub fn confirm_message(&self) -> String {
match self {
PendingAction::Abandon { description, .. } => {
format!("Abandon change: \"{}\"?", truncate_str(description, 40))
}
PendingAction::Squash { description, .. } => {
format!(
"Squash change: \"{}\" into parent?",
truncate_str(description, 40)
)
}
PendingAction::GitPush => "Push to remote?".to_string(),
PendingAction::Undo => "Undo last operation?".to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub enum ModalState {
#[default]
None,
Confirm(PendingAction),
}
#[derive(Debug, Clone)]
pub struct CommandResult {
pub success: bool,
pub message: String,
}
const DEFAULT_BATCH_SIZE: usize = 500;
const LOAD_MORE_THRESHOLD: usize = 50;
pub struct App {
pub graph_log: GraphLog,
pub selected: usize,
pub scroll_offset: usize,
pub should_quit: bool,
pub repo_root: String,
pub view: View,
pub detail_state: Option<DetailState>,
pub diff_state: DiffState,
pub show_help: bool,
runner: JjRunner,
pub modal: ModalState,
pub last_command_result: Option<CommandResult>,
pub input_mode: Option<InputMode>,
pub input: Input,
log_limit: Option<usize>,
pub has_more_entries: bool,
pub is_loading_more: bool,
pending_load_more: bool,
}
impl App {
pub fn new(graph_log: GraphLog, repo_root: String, runner: JjRunner) -> Self {
Self {
graph_log,
selected: 0,
scroll_offset: 0,
should_quit: false,
repo_root,
view: View::default(),
detail_state: None,
diff_state: DiffState::default(),
show_help: false,
runner,
modal: ModalState::default(),
last_command_result: None,
input_mode: None,
input: Input::default(),
log_limit: Some(DEFAULT_BATCH_SIZE),
has_more_entries: false, is_loading_more: false,
pending_load_more: false,
}
}
pub fn quit(&mut self) {
self.should_quit = true;
}
pub fn toggle_help(&mut self) {
self.show_help = !self.show_help;
}
pub fn close_help(&mut self) {
self.show_help = false;
}
pub fn is_modal_open(&self) -> bool {
!matches!(self.modal, ModalState::None)
}
pub fn close_modal(&mut self) {
self.modal = ModalState::None;
}
pub fn open_detail(&mut self) -> Result<(), XorcistError> {
if let Some(change_id) = self.selected_change_id() {
let show_output = fetch_show(&self.runner, change_id)?;
self.detail_state = Some(DetailState {
show_output,
scroll: 0,
content_height: 0, });
self.view = View::Detail;
}
Ok(())
}
pub fn close_detail(&mut self) {
self.view = View::Log;
self.detail_state = None;
}
pub fn close_diff(&mut self) {
self.view = View::Detail;
}
pub fn detail_scroll_down(&mut self, amount: usize) {
if let Some(state) = &mut self.detail_state {
state.scroll = state.scroll.saturating_add(amount);
}
}
pub fn detail_scroll_up(&mut self, amount: usize) {
if let Some(state) = &mut self.detail_state {
state.scroll = state.scroll.saturating_sub(amount);
}
}
pub fn set_detail_content_height(&mut self, height: usize) {
if let Some(state) = &mut self.detail_state {
state.content_height = height;
if height > 0 && state.scroll >= height {
state.scroll = height.saturating_sub(1);
}
}
}
}