1use std::io::{self, stdout, Stdout};
4
5use ratatui::backend::{CrosstermBackend, TestBackend};
6use ratatui::crossterm::execute;
7use ratatui::crossterm::terminal::{
8 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
9};
10use ratatui::layout::{Constraint, Layout, Rect};
11use ratatui::style::{Modifier, Style};
12use ratatui::text::{Line, Span};
13use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap};
14use ratatui::{Frame, Terminal};
15
16use crate::app::App;
17use crate::keys::Pane;
18
19pub type CrosstermTerm = Terminal<CrosstermBackend<Stdout>>;
20
21pub fn install() -> io::Result<CrosstermTerm> {
22 enable_raw_mode()?;
23 let mut out = stdout();
24 execute!(out, EnterAlternateScreen)?;
25 Terminal::new(CrosstermBackend::new(out))
26}
27
28pub fn restore() -> io::Result<()> {
29 disable_raw_mode()?;
30 execute!(stdout(), LeaveAlternateScreen)?;
31 Ok(())
32}
33
34pub fn draw(frame: &mut Frame, app: &App) {
35 let area = frame.area();
36 let [tabs, body, status] = Layout::vertical([
37 Constraint::Length(3),
38 Constraint::Min(3),
39 Constraint::Length(1),
40 ])
41 .areas(area);
42
43 draw_tabs(frame, tabs, app);
44 let [list, detail] =
45 Layout::horizontal([Constraint::Percentage(55), Constraint::Percentage(45)]).areas(body);
46 draw_list(frame, list, app);
47 draw_detail(frame, detail, app);
48 draw_status(frame, status, app);
49
50 if app.help {
51 draw_overlay(frame, area, app.help_text());
52 } else if let Some(line) = app.prompt_line() {
53 draw_prompt(frame, area, &line);
54 } else if let Some(line) = app.confirm_line() {
55 draw_prompt(frame, area, &line);
56 }
57}
58
59fn draw_tabs(frame: &mut Frame, area: Rect, app: &App) {
60 let titles: Vec<Line> = Pane::ALL.iter().map(|p| Line::from(p.title())).collect();
61 let tabs = Tabs::new(titles)
62 .select(app.pane.index())
63 .block(Block::bordered().title("vissue"));
64 frame.render_widget(tabs, area);
65}
66
67fn draw_list(frame: &mut Frame, area: Rect, app: &App) {
68 let mut lines = Vec::new();
69 if app.rows.is_empty() {
70 lines.push(Line::from("(empty)"));
71 } else {
72 for (i, row) in app.rows.iter().enumerate() {
73 let marker = if i == app.selected { ">" } else { " " };
74 let text = if row.extra.is_empty() {
75 format!(
76 "{marker} {} [{:>8}] [#{}] {} {}",
77 row.id, row.state, row.priority, row.title, row.project
78 )
79 } else {
80 format!(
81 "{marker} {} [{:>8}] [#{}] {} {} {}",
82 row.id, row.state, row.priority, row.title, row.project, row.extra
83 )
84 };
85 let style = if i == app.selected {
86 Style::default().add_modifier(Modifier::REVERSED)
87 } else {
88 Style::default()
89 };
90 lines.push(Line::from(Span::styled(text, style)));
91 }
92 }
93 let block = Block::bordered().title(app.pane.title());
94 frame.render_widget(Paragraph::new(lines).block(block), area);
95}
96
97fn draw_detail(frame: &mut Frame, area: Rect, app: &App) {
98 let title = format!("detail: {}", app.detail_tab.title());
99 let id = app.selected_id().unwrap_or("-");
100 let mut text = id.to_string();
101 text.push('\n');
102 text.push_str(&app.detail_body);
103 let block = Block::bordered().title(title);
104 frame.render_widget(
105 Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
106 area,
107 );
108}
109
110fn draw_status(frame: &mut Frame, area: Rect, app: &App) {
111 frame.render_widget(Paragraph::new(app.status_line()), area);
112}
113
114fn draw_prompt(frame: &mut Frame, area: Rect, line: &str) {
115 let box_area = prompt_area(area, 3);
116 frame.render_widget(Clear, box_area);
117 frame.render_widget(
118 Paragraph::new(line).block(Block::bordered().title("input")),
119 box_area,
120 );
121}
122
123fn draw_overlay(frame: &mut Frame, area: Rect, text: &str) {
124 let box_area = centered(area, 70, 80);
125 frame.render_widget(Clear, box_area);
126 frame.render_widget(
127 Paragraph::new(text)
128 .block(Block::bordered().title("help").borders(Borders::ALL))
129 .wrap(Wrap { trim: false }),
130 box_area,
131 );
132}
133
134fn prompt_area(area: Rect, height: u16) -> Rect {
135 let y = area.y + area.height.saturating_sub(height + 1);
136 Rect {
137 x: area.x + 2,
138 y,
139 width: area.width.saturating_sub(4),
140 height,
141 }
142}
143
144fn centered(area: Rect, pct_x: u16, pct_y: u16) -> Rect {
145 let width = area.width.saturating_mul(pct_x) / 100;
146 let height = area.height.saturating_mul(pct_y) / 100;
147 Rect {
148 x: area.x + (area.width.saturating_sub(width)) / 2,
149 y: area.y + (area.height.saturating_sub(height)) / 2,
150 width,
151 height,
152 }
153}
154
155pub fn render_plain(app: &App, width: u16, height: u16) -> anyhow::Result<String> {
157 let backend = TestBackend::new(width, height);
158 let mut terminal = Terminal::new(backend)?;
159 terminal.draw(|f| draw(f, app))?;
160 Ok(buffer_plain(terminal.backend()))
161}
162
163pub fn buffer_plain(backend: &TestBackend) -> String {
164 let buf = backend.buffer();
165 let area = buf.area();
166 let mut out = String::new();
167 for y in area.top()..area.bottom() {
168 let mut line = String::new();
169 for x in area.left()..area.right() {
170 line.push_str(buf[(x, y)].symbol());
171 }
172 out.push_str(line.trim_end());
173 out.push('\n');
174 }
175 out
176}