mod ai_rally;
mod comment_list;
mod common;
pub mod diff_view;
mod file_list;
mod footer;
mod help;
mod pr_description;
mod pr_list;
mod split_view;
pub mod text_area;
use anyhow::Result;
use crossterm::{
event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem},
Frame, Terminal,
};
use std::io::{self, Stdout};
use std::sync::atomic::{AtomicBool, Ordering};
use crate::app::{App, AppState, DataState};
static KITTY_ENABLED: AtomicBool = AtomicBool::new(false);
pub fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
if execute!(
stdout,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
)
.is_ok()
{
KITTY_ENABLED.store(true, Ordering::SeqCst);
}
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
pub fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
cleanup_keyboard_enhancement();
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
pub fn cleanup_keyboard_enhancement() {
if KITTY_ENABLED
.compare_exchange(true, false, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
}
}
pub fn render(frame: &mut Frame, app: &mut App) {
if app.state != AppState::PullRequestList
&& app.state != AppState::Help
&& app.state != AppState::PrDescription
{
if matches!(app.data_state, DataState::Loading) {
file_list::render_loading(frame, app);
return;
}
if let DataState::Error(ref msg) = app.data_state {
file_list::render_error(frame, app, msg);
return;
}
}
match app.state {
AppState::PullRequestList => pr_list::render(frame, app),
AppState::FileList => file_list::render(frame, app),
AppState::DiffView => diff_view::render(frame, app),
AppState::TextInput => diff_view::render_text_input(frame, app),
AppState::CommentList => comment_list::render(frame, app),
AppState::Help => help::render(frame, app),
AppState::AiRally => ai_rally::render(frame, app),
AppState::SplitViewFileList | AppState::SplitViewDiff => split_view::render(frame, app),
AppState::PrDescription => pr_description::render(frame, app),
}
if let Some(ref popup) = app.symbol_popup {
render_symbol_popup(frame, popup);
}
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect::new(x, y, width.min(area.width), height.min(area.height))
}
fn render_symbol_popup(frame: &mut Frame, popup: &crate::app::SymbolPopupState) {
let area = frame.area();
let max_width = popup
.symbols
.iter()
.map(|(name, _, _)| name.len())
.max()
.unwrap_or(10) as u16
+ 6; let height = (popup.symbols.len() as u16 + 2).min(area.height.saturating_sub(4)); let width = max_width.max(20).min(area.width.saturating_sub(4));
let popup_area = centered_rect(width, height, area);
frame.render_widget(Clear, popup_area);
let items: Vec<ListItem> = popup
.symbols
.iter()
.enumerate()
.map(|(i, (name, _, _))| {
let style = if i == popup.selected {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
ListItem::new(Line::from(Span::styled(format!(" {} ", name), style)))
})
.collect();
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title("Select symbol (j/k/↑↓: move, Enter: jump, Esc: cancel)")
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(list, popup_area);
}