use std::io::{self, stdout, Stdout};
use ratatui::backend::{CrosstermBackend, TestBackend};
use ratatui::crossterm::execute;
use ratatui::crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap};
use ratatui::{Frame, Terminal};
use crate::app::App;
use crate::keys::Pane;
pub type CrosstermTerm = Terminal<CrosstermBackend<Stdout>>;
pub fn install() -> io::Result<CrosstermTerm> {
enable_raw_mode()?;
let mut out = stdout();
execute!(out, EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(out))
}
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
Ok(())
}
pub fn draw(frame: &mut Frame, app: &App) {
let area = frame.area();
let [tabs, body, status] = Layout::vertical([
Constraint::Length(3),
Constraint::Min(3),
Constraint::Length(1),
])
.areas(area);
draw_tabs(frame, tabs, app);
let [list, detail] =
Layout::horizontal([Constraint::Percentage(55), Constraint::Percentage(45)]).areas(body);
draw_list(frame, list, app);
draw_detail(frame, detail, app);
draw_status(frame, status, app);
if app.help {
draw_overlay(frame, area, app.help_text());
} else if let Some(line) = app.prompt_line() {
draw_prompt(frame, area, &line);
} else if let Some(line) = app.confirm_line() {
draw_prompt(frame, area, &line);
}
}
fn draw_tabs(frame: &mut Frame, area: Rect, app: &App) {
let titles: Vec<Line> = Pane::ALL.iter().map(|p| Line::from(p.title())).collect();
let tabs = Tabs::new(titles)
.select(app.pane.index())
.block(Block::bordered().title("vissue"));
frame.render_widget(tabs, area);
}
fn draw_list(frame: &mut Frame, area: Rect, app: &App) {
let mut lines = Vec::new();
if app.rows.is_empty() {
lines.push(Line::from("(empty)"));
} else {
for (i, row) in app.rows.iter().enumerate() {
let marker = if i == app.selected { ">" } else { " " };
let text = if row.extra.is_empty() {
format!(
"{marker} {} [{:>8}] [#{}] {} {}",
row.id, row.state, row.priority, row.title, row.project
)
} else {
format!(
"{marker} {} [{:>8}] [#{}] {} {} {}",
row.id, row.state, row.priority, row.title, row.project, row.extra
)
};
let style = if i == app.selected {
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default()
};
lines.push(Line::from(Span::styled(text, style)));
}
}
let block = Block::bordered().title(app.pane.title());
frame.render_widget(Paragraph::new(lines).block(block), area);
}
fn draw_detail(frame: &mut Frame, area: Rect, app: &App) {
let title = format!("detail: {}", app.detail_tab.title());
let id = app.selected_id().unwrap_or("-");
let mut text = id.to_string();
text.push('\n');
text.push_str(&app.detail_body);
let block = Block::bordered().title(title);
frame.render_widget(
Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
area,
);
}
fn draw_status(frame: &mut Frame, area: Rect, app: &App) {
frame.render_widget(Paragraph::new(app.status_line()), area);
}
fn draw_prompt(frame: &mut Frame, area: Rect, line: &str) {
let box_area = prompt_area(area, 3);
frame.render_widget(Clear, box_area);
frame.render_widget(
Paragraph::new(line).block(Block::bordered().title("input")),
box_area,
);
}
fn draw_overlay(frame: &mut Frame, area: Rect, text: &str) {
let box_area = centered(area, 70, 80);
frame.render_widget(Clear, box_area);
frame.render_widget(
Paragraph::new(text)
.block(Block::bordered().title("help").borders(Borders::ALL))
.wrap(Wrap { trim: false }),
box_area,
);
}
fn prompt_area(area: Rect, height: u16) -> Rect {
let y = area.y + area.height.saturating_sub(height + 1);
Rect {
x: area.x + 2,
y,
width: area.width.saturating_sub(4),
height,
}
}
fn centered(area: Rect, pct_x: u16, pct_y: u16) -> Rect {
let width = area.width.saturating_mul(pct_x) / 100;
let height = area.height.saturating_mul(pct_y) / 100;
Rect {
x: area.x + (area.width.saturating_sub(width)) / 2,
y: area.y + (area.height.saturating_sub(height)) / 2,
width,
height,
}
}
pub fn render_plain(app: &App, width: u16, height: u16) -> anyhow::Result<String> {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend)?;
terminal.draw(|f| draw(f, app))?;
Ok(buffer_plain(terminal.backend()))
}
pub fn buffer_plain(backend: &TestBackend) -> String {
let buf = backend.buffer();
let area = buf.area();
let mut out = String::new();
for y in area.top()..area.bottom() {
let mut line = String::new();
for x in area.left()..area.right() {
line.push_str(buf[(x, y)].symbol());
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}