use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
use unicode_width::UnicodeWidthStr;
use crate::app::{AlertKind, App, Screen};
use crate::config::Colors;
fn parse_cc(message: &str) -> Option<(&str, Option<&str>, bool, &str)> {
let colon = message.find(':')?;
let prefix = message[..colon].trim_end();
let desc = message[colon + 1..].trim_start();
let breaking = prefix.ends_with('!');
let clean = if breaking {
&prefix[..prefix.len() - 1]
} else {
prefix
};
if let Some(open) = clean.find('(') {
let close = clean.rfind(')')?;
if close > open {
let r#type = clean[..open].trim();
let scope = clean[open + 1..close].trim();
if !r#type.is_empty() {
return Some((r#type, Some(scope), breaking, desc));
}
}
}
let r#type = clean.trim();
if r#type.is_empty() {
None
} else {
Some((r#type, None, breaking, desc))
}
}
fn cc_type_color<'a>(colors: &'a Colors, type_name: &str) -> &'a str {
match type_name {
"feat" => &colors.cc_type_feat,
"fix" => &colors.cc_type_fix,
"docs" => &colors.cc_type_docs,
"style" => &colors.cc_type_style,
"refactor" => &colors.cc_type_refactor,
"perf" => &colors.cc_type_perf,
"test" => &colors.cc_type_test,
"chore" => &colors.cc_type_chore,
_ => &colors.cc_type_chore,
}
}
fn hex_color(hex: &str) -> Color {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Color::Reset;
}
match u32::from_str_radix(hex, 16) {
Ok(rgb) => Color::Rgb(
((rgb >> 16) & 0xFF) as u8,
((rgb >> 8) & 0xFF) as u8,
(rgb & 0xFF) as u8,
),
Err(_) => Color::Reset,
}
}
fn fg(hex: &str) -> Style {
Style::default().fg(hex_color(hex))
}
const RANGE_BG: &str = "#45475a";
pub fn render(frame: &mut Frame, app: &App) {
let area = frame.area();
let footer_area = Rect {
x: 0,
y: area.height.saturating_sub(1),
width: area.width,
height: 1,
};
match &app.screen {
Screen::Loading => render_loading(frame),
Screen::List => render_commit_list(frame, app, false),
Screen::Detail => {
render_commit_list(frame, app, true);
render_detail_overlay(frame, app);
render_footer(frame, app, footer_area, true);
}
Screen::Error(msg) => render_error(frame, msg),
Screen::Alert(kind) => {
render_commit_list(frame, app, true);
render_alert_overlay(frame, app, kind);
render_footer(frame, app, footer_area, true);
}
}
if app.notification.is_some() {
render_notification(frame, app);
}
if app.file_selection.is_some() {
render_file_select_overlay(frame, app);
}
if app.show_help {
render_help_overlay(frame, app);
}
}
fn render_loading(frame: &mut Frame) {
let area = frame.area();
let text = Paragraph::new("Loading commits...")
.alignment(Alignment::Center)
.block(Block::default().title(" tuit ").borders(Borders::ALL));
frame.render_widget(text, area);
}
pub fn render_commit_list(frame: &mut Frame, app: &App, grayed_out: bool) {
let colors = &app.colors;
let area = frame.area();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ])
.split(area);
let branch_text = app.current_branch.clone();
let branch_display = format!(" branch: {}", branch_text);
let version_text = format!("tuit {} ", env!("CARGO_PKG_VERSION"));
let header_width = layout[0].width as usize;
let branch_display_width = branch_display.width();
let version_width = version_text.width();
let padding_width = header_width.saturating_sub(branch_display_width + version_width);
let header_line = Line::from(vec![
Span::styled(branch_display, Style::default().bold()),
Span::styled(" ".repeat(padding_width), Style::default()),
Span::styled(version_text, Style::default().fg(Color::DarkGray)),
]);
let header = Paragraph::new(header_line);
frame.render_widget(header, layout[0]);
let list_area = layout[1];
let list_bg = hex_color(&colors.list_bg);
let muted_fg = Style::default().fg(Color::DarkGray);
let viewport_height = list_area.height as usize;
let commits_len = app.commits.len();
let mut scroll = app.list_scroll.get();
if app.selected_index < scroll {
scroll = app.selected_index;
}
if viewport_height > 0 && app.selected_index >= scroll + viewport_height {
scroll = app
.selected_index
.saturating_add(1)
.saturating_sub(viewport_height);
}
let max_scroll = commits_len.saturating_sub(viewport_height);
if scroll > max_scroll {
scroll = max_scroll;
}
app.list_scroll.set(scroll);
app.list_content_height.set(viewport_height);
let (range_lo, range_hi) = app.range_start.map_or((0, 0), |start| {
(start.min(app.selected_index), start.max(app.selected_index))
});
let has_range = app.range_start.is_some();
let rows: Vec<Line> = app
.commits
.iter()
.enumerate()
.map(|(i, commit)| {
let is_selected = i == app.selected_index;
let in_range = has_range && i >= range_lo && i <= range_hi;
let cursor = if is_selected {
"❯"
} else {
" "
};
let w = list_area.width as usize;
let show_hash = w >= 140;
let date_str = if commit.date.len() > 10 {
let mut s: String = commit.date.chars().take(9).collect();
s.push('.');
s
} else {
commit.date.clone()
};
let cursor_part = 2usize;
let hash_part = if show_hash { 9usize } else { 0 };
let before_msg = 1usize;
let after_msg = 1usize;
let date_part = 1usize + date_str.width(); let reserved = cursor_part + hash_part + before_msg + after_msg + date_part;
let max_msg_width = w.saturating_sub(reserved).max(5);
let msg = if commit.message.len() > max_msg_width {
let mut s: String = commit
.message
.chars()
.take(max_msg_width.saturating_sub(1))
.collect();
s.push('.');
s
} else {
commit.message.clone()
};
if is_selected {
let bg = if grayed_out {
Color::Gray
} else {
hex_color(&colors.cursor_bg)
};
let fg_color = if grayed_out {
Color::Black
} else {
hex_color(&colors.cursor_fg)
};
let base = Style::default().bg(bg).fg(fg_color);
let mut spans = vec![Span::styled(format!(" {}", cursor), base)];
if show_hash {
spans.push(Span::styled(format!(" {} ", commit.hash), base));
}
spans.push(Span::styled(" ", base));
if let Some((cc_type, cc_scope, cc_breaking, cc_desc)) = parse_cc(&msg) {
let type_color = if grayed_out {
Color::Black
} else {
hex_color(cc_type_color(&colors, cc_type))
};
spans.push(Span::styled(
cc_type.to_string(),
Style::default()
.bg(bg)
.fg(type_color)
.add_modifier(Modifier::BOLD),
));
if let Some(scope) = cc_scope {
spans.push(Span::styled(format!("({})", scope), base));
}
if cc_breaking {
spans.push(Span::styled("!", base.fg(Color::Red)));
}
let rest = if cc_desc.is_empty() {
String::new()
} else {
format!(": {}", cc_desc)
};
spans.push(Span::styled(rest, base));
} else {
spans.push(Span::styled(msg.clone(), base));
}
spans.push(Span::styled(" ", base));
spans.push(Span::styled(format!(" {}", date_str), base));
Line::from(spans)
} else if in_range && !grayed_out {
let range_bg = hex_color(RANGE_BG);
let base = Style::default().bg(range_bg);
let mut spans = vec![Span::styled(format!(" {}", cursor), base)];
if show_hash {
spans.push(Span::styled(
format!(" {} ", commit.hash),
base.fg(hex_color(&colors.hash)),
));
}
let msg_style = base.fg(hex_color(&colors.message));
spans.push(Span::styled(" ", msg_style));
if let Some((cc_type, cc_scope, cc_breaking, cc_desc)) = parse_cc(&msg) {
let type_style = base
.fg(hex_color(cc_type_color(&colors, cc_type)))
.add_modifier(Modifier::BOLD);
spans.push(Span::styled(cc_type.to_string(), type_style));
if let Some(scope) = cc_scope {
spans.push(Span::styled(format!("({})", scope), base.fg(Color::DarkGray)));
}
if cc_breaking {
spans.push(Span::styled("!", base.fg(hex_color(&colors.diff_del))));
}
let rest = if cc_desc.is_empty() {
String::new()
} else {
format!(": {}", cc_desc)
};
spans.push(Span::styled(rest, msg_style));
} else {
spans.push(Span::styled(msg.clone(), msg_style));
}
spans.push(Span::styled(" ", msg_style));
spans.push(Span::styled(
format!(" {}", date_str),
base.fg(hex_color(&colors.date)),
));
Line::from(spans)
} else {
let mut spans = vec![Span::styled(format!(" {}", cursor), muted_fg)];
if show_hash {
spans.push(Span::styled(
format!(" {} ", commit.hash),
if grayed_out {
muted_fg
} else {
fg(&colors.hash)
},
));
}
let msg_style = if grayed_out {
muted_fg
} else {
fg(&colors.message)
};
spans.push(Span::styled(" ", msg_style));
if let Some((cc_type, cc_scope, cc_breaking, cc_desc)) = parse_cc(&msg) {
let type_style = if grayed_out {
muted_fg
} else {
fg(cc_type_color(&colors, cc_type)).add_modifier(Modifier::BOLD)
};
spans.push(Span::styled(cc_type.to_string(), type_style));
if let Some(scope) = cc_scope {
let scope_style = if grayed_out {
muted_fg
} else {
Style::default().fg(Color::DarkGray)
};
spans.push(Span::styled(format!("({})", scope), scope_style));
}
if cc_breaking {
let br_style = if grayed_out {
muted_fg
} else {
fg(&colors.diff_del)
};
spans.push(Span::styled("!", br_style));
}
let rest = if cc_desc.is_empty() {
String::new()
} else {
format!(": {}", cc_desc)
};
spans.push(Span::styled(rest, msg_style));
} else {
spans.push(Span::styled(msg.clone(), msg_style));
}
spans.push(Span::styled(" ", msg_style));
spans.push(Span::styled(
format!(" {}", date_str),
if grayed_out {
muted_fg
} else {
fg(&colors.date)
},
));
Line::from(spans)
}
})
.collect();
let list_block = Block::default().style(Style::default().bg(list_bg));
let list = Paragraph::new(Text::from(rows))
.block(list_block)
.scroll((scroll as u16, 0))
.wrap(Wrap { trim: false });
frame.render_widget(list, layout[1]);
render_footer(frame, app, layout[2], grayed_out);
}
fn render_footer(frame: &mut Frame, app: &App, area: Rect, grayed_out: bool) {
let dim = Style::default().fg(Color::DarkGray);
let cursor_fg = hex_color(&app.colors.cursor_fg);
let (mode_label, mode_bg_hex, mode_fg) = if grayed_out {
(" NORM ", "#6c7086", Color::Black)
} else {
let (label, bg) = match &app.screen {
Screen::List => {
if app.range_start.is_some() {
(" SEL ", "#fab387")
} else {
(" NORM ", "#89b4fa")
}
}
Screen::Detail => (" DETAIL ", "#a6e3a1"),
Screen::Alert(_) => (" ALERT ", "#f38ba8"),
Screen::Error(_) => (" ERROR ", "#f38ba8"),
Screen::Loading => (" LOAD ", "#6c7086"),
};
(label, bg, cursor_fg)
};
let mut left_spans: Vec<Span> = vec![
Span::styled(
mode_label,
Style::default()
.bg(hex_color(mode_bg_hex))
.fg(mode_fg),
),
];
if let Some(n) = app.pending_count {
left_spans.push(Span::styled(format!(" [{}]", n), dim));
}
let left = Paragraph::new(Line::from(left_spans))
.alignment(Alignment::Left);
frame.render_widget(left, area);
let right = Paragraph::new(Line::from(Span::styled(
"q: quit | ?: help",
dim,
)))
.alignment(Alignment::Right);
frame.render_widget(right, area);
}
pub fn render_detail_overlay(frame: &mut Frame, app: &App) {
let colors = &app.colors;
let area = frame.area();
let commit = match &app.selected_commit {
Some(c) => c,
None => return,
};
let overlay_w = (area.width as f64 * 0.9) as u16;
let overlay_h = (area.height as f64 * 0.8) as u16;
let overlay_x = (area.width - overlay_w) / 2;
let overlay_y = (area.height - overlay_h) / 3;
let overlay_area = Rect {
x: overlay_x,
y: overlay_y,
width: overlay_w,
height: overlay_h,
};
let dim_block = Block::default().style(Style::default().bg(Color::Black));
frame.render_widget(dim_block, area);
frame.render_widget(Clear, overlay_area);
let border_color = hex_color(&colors.detail_border);
let bg_color = hex_color(&colors.detail_bg);
let overlay = Block::default()
.title(format!(
" {} | {} | {} ",
commit.hash, commit.author, commit.date,
))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(bg_color));
let inner = overlay.inner(overlay_area);
frame.render_widget(overlay, overlay_area);
let body_lines: Vec<&str> = commit.body.lines().collect();
let body_count = body_lines.len();
let showed_all_body = body_count <= 8;
let wrap_width = inner.width.max(1) as usize;
let wrap_rows = |text: &str| -> u16 {
let w = wrap_width.max(1);
let vis = text.width();
if vis == 0 { 1 } else { vis.div_ceil(w) as u16 }
};
let mut msg_height = wrap_rows(&commit.message); if body_count > 0 {
msg_height += 1; for line in body_lines.iter().take(8) {
msg_height += wrap_rows(line);
}
if !showed_all_body {
msg_height += 1; }
}
let max_msg = (inner.height.saturating_sub(3) * 2 / 3).max(1);
msg_height = msg_height.min(max_msg);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(msg_height),
Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), ])
.split(inner);
let value_style = Style::default().fg(hex_color(&colors.detail_value));
let mut msg_lines: Vec<Line> = Vec::new();
msg_lines.push(Line::from(Span::styled(
commit.message.clone(),
value_style.add_modifier(Modifier::BOLD),
)));
if body_count > 0 {
msg_lines.push(Line::from(Span::styled(String::new(), value_style)));
for line in body_lines.iter().take(8) {
msg_lines.push(Line::from(Span::styled(line.to_string(), value_style)));
}
if !showed_all_body {
let dim_style = Style::default().fg(Color::DarkGray);
msg_lines.push(Line::from(Span::styled(
format!(" (... and {} more lines)", body_count - 8),
dim_style,
)));
}
}
let msg = Paragraph::new(Text::from(msg_lines))
.wrap(Wrap { trim: false })
.style(Style::default().bg(bg_color));
frame.render_widget(msg, layout[0]);
let sep_style = Style::default().fg(Color::DarkGray);
let sep = Paragraph::new(Line::from(Span::styled(
"-".repeat(layout[1].width.max(1) as usize),
sep_style,
)))
.style(Style::default().bg(bg_color));
frame.render_widget(sep, layout[1]);
let diff_area = layout[2];
let diff_width = diff_area.width.saturating_sub(2) as usize;
let mut diff_lines: Vec<Line> = Vec::new();
for line in commit.diff.lines() {
let style = if line.starts_with('+') {
fg(&colors.diff_add)
} else if line.starts_with('-') {
fg(&colors.diff_del)
} else {
fg(&colors.diff_normal)
};
let display = if line.len() > diff_width {
let mut s: String = line.chars().take(diff_width.saturating_sub(3)).collect();
s.push_str(" ...");
s
} else {
line.to_string()
};
diff_lines.push(Line::from(Span::styled(display, style)));
}
let diff_height = diff_area.height as usize;
app.detail_content_height.set(diff_height);
let total_diff_lines = diff_lines.len();
let max_scroll = total_diff_lines.saturating_sub(diff_height);
let scroll = app.detail_scroll.get().min(max_scroll);
app.detail_scroll.set(scroll);
let diff_para = Paragraph::new(Text::from(diff_lines))
.style(Style::default().bg(bg_color))
.scroll((scroll as u16, 0));
frame.render_widget(diff_para, diff_area);
let hint_text = if total_diff_lines > diff_height {
let pct = if max_scroll > 0 {
(scroll as f64 / max_scroll as f64 * 100.0) as u8
} else {
0
};
format!(" [Esc] close | c copy | ^/v scroll ({pct}%) ")
} else {
" [Esc] close | c copy ".to_string()
};
let hint = Paragraph::new(Line::from(Span::styled(
hint_text,
Style::default().fg(Color::DarkGray),
)))
.alignment(Alignment::Center)
.style(Style::default().bg(bg_color));
frame.render_widget(hint, layout[3]);
}
fn render_file_select_overlay(frame: &mut Frame, app: &App) {
let colors = &app.colors;
let area = frame.area();
let fs = match &app.file_selection {
Some(fs) => fs,
None => return,
};
let dim_block = Block::default().style(Style::default().bg(Color::Black));
frame.render_widget(dim_block, area);
let popup_w = (area.width as f64 * 0.7).min(60.0).max(40.0) as u16;
let popup_h = (area.height as f64 * 0.85).min(30.0).max(10.0) as u16;
let popup_x = (area.width - popup_w) / 2;
let popup_y = (area.height - popup_h) / 2;
let popup_area = Rect {
x: popup_x,
y: popup_y,
width: popup_w,
height: popup_h,
};
frame.render_widget(Clear, popup_area);
let short_oid = fs.commit_oid.chars().take(7).collect::<String>();
let border_color = hex_color(&colors.detail_border);
let bg_color = hex_color(&colors.detail_bg);
let overlay = Block::default()
.title(format!(" Select files — {} ", short_oid))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(bg_color));
let inner = overlay.inner(popup_area);
frame.render_widget(overlay, popup_area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(inner);
let cursor_bg = hex_color(&colors.cursor_bg);
let cursor_fg = hex_color(&colors.cursor_fg);
let value_style = Style::default().fg(hex_color(&colors.detail_value));
let total = fs.files.len();
let visible = layout[0].height as usize;
let mut scroll = fs.scroll.get();
if fs.cursor < scroll {
scroll = fs.cursor;
}
if visible > 0 && fs.cursor >= scroll + visible {
scroll = fs.cursor.saturating_add(1).saturating_sub(visible);
}
let max_scroll = total.saturating_sub(visible);
scroll = scroll.min(max_scroll);
fs.scroll.set(scroll);
let mut lines: Vec<Line> = Vec::new();
let display_range = scroll..(scroll + visible).min(total);
for i in display_range {
let is_cursor = i == fs.cursor;
let is_sel = fs.selected.contains(&i);
let prefix = if is_sel { "[x]" } else { "[ ]" };
let text = format!(" {} {}", prefix, fs.files[i]);
if is_cursor {
lines.push(Line::from(Span::styled(
text,
Style::default().bg(cursor_bg).fg(cursor_fg),
)));
} else {
lines.push(Line::from(Span::styled(text, value_style)));
}
}
let file_list = Paragraph::new(Text::from(lines)).style(Style::default().bg(bg_color));
frame.render_widget(file_list, layout[0]);
let sel = fs.selected.len();
let hint = if sel == total {
" Space: toggle a: all n: none Enter: hunk (all) Esc: cancel "
} else if sel == 0 {
" Space: toggle a: all n: none (select at least one file) "
} else {
" Space: toggle a: all n: none Enter: hunk (filtered) Esc: cancel "
};
let hint_para = Paragraph::new(Line::from(Span::styled(
hint,
Style::default().fg(Color::DarkGray),
)))
.alignment(Alignment::Center)
.style(Style::default().bg(bg_color));
frame.render_widget(hint_para, layout[1]);
}
fn render_help_overlay(frame: &mut Frame, app: &App) {
let colors = &app.colors;
let area = frame.area();
let dim_block = Block::default().style(Style::default().bg(Color::Black));
frame.render_widget(dim_block, area);
let popup_w = (area.width as f64 * 0.7).min(52.0).max(40.0) as u16;
let popup_h = (area.height as f64 * 0.85).min(28.0) as u16;
let popup_x = (area.width - popup_w) / 2;
let popup_y = (area.height - popup_h) / 2;
let popup_area = Rect {
x: popup_x,
y: popup_y,
width: popup_w,
height: popup_h,
};
frame.render_widget(Clear, popup_area);
let border_color = hex_color(&colors.detail_border);
let bg_color = hex_color(&colors.detail_bg);
let overlay = Block::default()
.title(" Keybindings ")
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(bg_color));
let inner = overlay.inner(popup_area);
frame.render_widget(overlay, popup_area);
let heading_style = Style::default()
.fg(hex_color(&colors.detail_heading))
.add_modifier(Modifier::BOLD);
let key_style = Style::default().fg(hex_color(&colors.detail_border));
let desc_style = Style::default().fg(hex_color(&colors.detail_value));
let dim_style = Style::default().fg(Color::DarkGray);
let mut lines: Vec<Line> = Vec::new();
let (section_title, section_bindings): (&str, &[(&str, &str)]) = match &app.screen {
Screen::List => (
"Commit List",
&[
("^ / k", "Move cursor up"),
("v / j", "Move cursor down"),
("Ctrl+f", "Page down"),
("Ctrl+b", "Page up"),
("Enter", "Show commit details"),
("c", "Copy commit hash to clipboard"),
("r", "Reload from current branch"),
("v", "Mark/unmark range start"),
("f", "Select files for hunk"),
("h", "Open commit/range in hunk"),
("q", "Quit tuit"),
],
),
Screen::Detail => (
"Commit Detail",
&[
("^ / k", "Scroll up"),
("v / j", "Scroll down"),
("Ctrl+f", "Scroll down one page"),
("Ctrl+b", "Scroll up one page"),
("c", "Copy commit hash to clipboard"),
("f", "Select files for hunk"),
("h", "Open commit in hunk for review"),
("r", "Reload from current branch"),
("Esc", "Close detail view"),
],
),
Screen::Error(_) => ("Error", &[("Enter", "Quit")]),
Screen::Alert(_) => ("Alert", &[("Enter / Esc", "Dismiss")]),
Screen::Loading => return, };
lines.push(Line::from(Span::styled(
format!(" {} ", section_title),
heading_style,
)));
lines.push(Line::from(Span::styled(String::new(), dim_style)));
for (key_str, desc) in section_bindings {
lines.push(Line::from(vec![
Span::styled(format!(" {:<12}", key_str), key_style),
Span::styled(desc.to_string(), desc_style),
]));
}
lines.push(Line::from(Span::styled(String::new(), dim_style)));
lines.push(Line::from(Span::styled(" [Esc/?] close ", dim_style)));
let content = Paragraph::new(Text::from(lines)).style(Style::default().bg(bg_color));
frame.render_widget(content, inner);
}
pub fn render_alert_overlay(frame: &mut Frame, app: &App, kind: &AlertKind) {
let colors = &app.colors;
let area = frame.area();
let dim_block = Block::default().style(Style::default().bg(Color::Black));
frame.render_widget(dim_block, area);
let popup_w = (area.width as f64 * 0.6).min(50.0).max(36.0) as u16;
let popup_h = 8;
let popup_x = (area.width - popup_w) / 2;
let popup_y = (area.height - popup_h) / 2;
let popup_area = Rect {
x: popup_x,
y: popup_y,
width: popup_w,
height: popup_h,
};
frame.render_widget(Clear, popup_area);
let border_color = hex_color(&colors.detail_border);
let bg_color = hex_color(&colors.detail_bg);
let overlay = Block::default()
.title(" ⚠ Commit Deleted ")
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(bg_color));
let inner = overlay.inner(popup_area);
frame.render_widget(overlay, popup_area);
let value_style = Style::default().fg(hex_color(&colors.detail_value));
let dim_style = Style::default().fg(Color::DarkGray);
let msg = match kind {
AlertKind::CommitDeleted { oid } => {
let short = oid.chars().take(7).collect::<String>();
format!("Commit {} no longer exists in the repository.", short)
}
};
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(inner);
let msg_para =
Paragraph::new(Line::from(Span::styled(msg, value_style))).alignment(Alignment::Center);
frame.render_widget(msg_para, layout[0]);
let hint = Paragraph::new(Line::from(Span::styled(
" [Enter / Esc] back to list ",
dim_style,
)))
.alignment(Alignment::Center);
frame.render_widget(hint, layout[1]);
}
pub fn render_notification(frame: &mut Frame, app: &App) {
let colors = &app.colors;
let area = frame.area();
let notif = match &app.notification {
Some(n) => n,
None => return,
};
let bar = Rect {
x: 0,
y: 0,
width: area.width,
height: 1,
};
let bg_color = hex_color(&colors.detail_border);
let notice = Paragraph::new(Line::from(Span::styled(
format!(" {} ", notif.message),
Style::default().fg(Color::Black).bg(bg_color),
)))
.alignment(Alignment::Right);
frame.render_widget(notice, bar);
}
pub fn render_error(frame: &mut Frame, msg: &str) {
let area = frame.area();
let block = Block::default().title(" tuit ").borders(Borders::ALL);
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
.split(inner);
let icon = Paragraph::new(Line::from(Span::styled(
" ⚠ Error ",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)))
.alignment(Alignment::Center);
frame.render_widget(icon, layout[0]);
let msg_para = Paragraph::new(Line::from(Span::styled(
msg,
Style::default().fg(Color::White),
)))
.alignment(Alignment::Center);
frame.render_widget(msg_para, layout[1]);
let hint = Paragraph::new(Line::from(Span::styled(
" [Enter] exit ",
Style::default().fg(Color::DarkGray),
)))
.alignment(Alignment::Center);
frame.render_widget(hint, layout[2]);
}