use super::*;
pub fn render_theme_panel(frame: &mut Frame, area: Rect, app: &App) {
let theme = app.theme();
let v = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(4),
])
.split(area);
let controls = Paragraph::new(Line::from(vec![
Span::styled(" ↑ [ ", Style::default().fg(theme.dim)),
Span::styled("prev", Style::default().fg(theme.accent)),
Span::styled(" ", Style::default().fg(theme.dim)),
Span::styled("↓ t ", Style::default().fg(theme.dim)),
Span::styled("next", Style::default().fg(theme.accent)),
]))
.block(
Block::default()
.title(Span::styled(
" \u{1F3A8} Themes ",
Style::default()
.fg(theme.brand)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent)),
);
frame.render_widget(controls, v[0]);
let visible = v[1].height.saturating_sub(2) as usize;
let scroll = if app.theme_idx >= visible {
app.theme_idx - visible + 1
} else {
0
};
let items: Vec<ListItem> = app
.themes
.iter()
.enumerate()
.skip(scroll)
.take(visible)
.map(|(i, (name, _))| {
let is_active = i == app.theme_idx;
let marker = if is_active { "\u{25BA} " } else { " " };
let line = Line::from(vec![
Span::styled(
format!("{marker}{:>2}. ", i + 1),
Style::default().fg(if is_active { theme.brand } else { theme.dim }),
),
Span::styled(
name.to_string(),
if is_active {
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.fg)
},
),
]);
if is_active {
ListItem::new(line).style(Style::default().bg(theme.sel_bg))
} else {
ListItem::new(line)
}
})
.collect();
let mut list_state = ListState::default();
list_state.select(Some(app.theme_idx.saturating_sub(scroll)));
let list = List::new(items).block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT)
.border_style(Style::default().fg(theme.accent)),
);
frame.render_stateful_widget(list, v[1], &mut list_state);
let desc_text = format!(
"{}\n{}/{}",
app.theme_name(),
app.theme_idx + 1,
app.themes.len()
);
let desc = Paragraph::new(desc_text)
.style(Style::default().fg(theme.success))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent)),
);
frame.render_widget(desc, v[2]);
}
pub fn render_editor_panel(frame: &mut Frame, area: Rect, app: &App) {
use crate::app::{App as TfeApp, Editor};
let theme = app.theme();
let on_style = Style::default()
.fg(theme.success)
.add_modifier(Modifier::BOLD);
let key_style = Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD);
let label_style = Style::default().fg(theme.fg);
let subtitle_style = Style::default().fg(theme.dim);
let title_style = Style::default()
.fg(theme.brand)
.add_modifier(Modifier::BOLD);
let editors = TfeApp::all_editors();
let first_ide = TfeApp::first_ide_idx();
let terminal_editors = &editors[..first_ide]; let ide_editors = &editors[first_ide..];
let terminal_cell_h = crate::to_u16(terminal_editors.len()) + 2;
let ide_cell_h = crate::to_u16(ide_editors.len()) + 2;
let slots = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), Constraint::Length(terminal_cell_h), Constraint::Length(1), Constraint::Length(1), Constraint::Length(ide_cell_h), Constraint::Length(1), Constraint::Length(3), Constraint::Min(0), ])
.split(area);
let section_title = |frame: &mut Frame, slot: Rect, label: &str| {
let dashes = "─".repeat((slot.width as usize).saturating_sub(label.len() + 2));
let para = Paragraph::new(Line::from(vec![
Span::styled(format!(" {label} "), subtitle_style),
Span::styled(dashes, subtitle_style),
]));
frame.render_widget(para, slot);
};
let editor_row = |editor: &Editor, idx: usize| -> Line {
let is_highlighted = idx == app.editor_panel_idx;
let is_selected = editor == &app.editor;
let marker = if is_highlighted { "\u{25BA} " } else { " " };
let check = if is_selected { "\u{2713} " } else { " " };
Line::from(vec![
Span::styled(
marker,
Style::default().fg(if is_highlighted {
theme.brand
} else {
theme.dim
}),
),
Span::styled(
check,
if is_selected {
on_style
} else {
subtitle_style
},
),
Span::styled(
format!("{:<width$}", editor.label(), width = 16),
if is_highlighted {
key_style
} else {
label_style
},
),
])
};
let header = Block::default()
.title(Span::styled(" \u{1F4DD} Editor ", title_style))
.title_bottom(Line::from(vec![
Span::styled(" Shift + E ", key_style),
Span::styled("close", subtitle_style),
]))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent));
frame.render_widget(header, slots[0]);
section_title(frame, slots[2], "Terminal Editors");
let terminal_rows: Vec<Line> = terminal_editors
.iter()
.enumerate()
.map(|(i, ed)| editor_row(ed, i))
.collect();
let terminal_cell = Paragraph::new(terminal_rows).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.dim)),
);
frame.render_widget(terminal_cell, slots[3]);
section_title(frame, slots[5], "IDEs & GUI Editors");
let ide_rows: Vec<Line> = ide_editors
.iter()
.enumerate()
.map(|(i, ed)| editor_row(ed, first_ide + i))
.collect();
let ide_cell = Paragraph::new(ide_rows).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.dim)),
);
frame.render_widget(ide_cell, slots[6]);
let highlighted_editor = &editors[app.editor_panel_idx];
let footer_text = if *highlighted_editor == Editor::None {
"none — no editor".to_string()
} else {
format!(
"{} → {}",
highlighted_editor.label(),
highlighted_editor.binary().unwrap_or_default()
)
};
let footer = Paragraph::new(footer_text)
.style(Style::default().fg(theme.success))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent)),
);
frame.render_widget(footer, slots[8]);
}
pub fn render_options_panel(frame: &mut Frame, area: Rect, app: &App) {
let theme = app.theme();
let on_style = Style::default()
.fg(theme.success)
.add_modifier(Modifier::BOLD);
let off_style = Style::default().fg(theme.dim);
let key_style = Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD);
let label_style = Style::default().fg(theme.fg);
let subtitle_style = Style::default().fg(theme.dim);
let title_style = Style::default()
.fg(theme.brand)
.add_modifier(Modifier::BOLD);
let slots = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), Constraint::Length(8), Constraint::Length(1), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(1), Constraint::Length(9), Constraint::Min(0), ])
.split(area);
let section_title = |frame: &mut Frame, slot: Rect, label: &str| {
let dashes = "─".repeat((slot.width as usize).saturating_sub(label.len() + 2));
let para = Paragraph::new(Line::from(vec![
Span::styled(format!(" {label} "), subtitle_style),
Span::styled(dashes, subtitle_style),
]));
frame.render_widget(para, slot);
};
let option_row = |key: &str, label: &str, value: Span<'static>| -> Line {
Line::from(vec![
Span::raw(" "),
Span::styled(format!("{key:<12}"), key_style),
Span::styled(format!("{label:<14}"), label_style),
value,
])
};
let bool_span = |enabled: bool| -> Span {
if enabled {
Span::styled("● on ", on_style)
} else {
Span::styled("○ off", off_style)
}
};
let header = Block::default()
.title(Span::styled(" ⚙ Options ", title_style))
.title_bottom(Line::from(vec![
Span::styled(" Shift + O ", key_style),
Span::styled("close", subtitle_style),
]))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent));
frame.render_widget(header, slots[0]);
section_title(frame, slots[2], "Toggles");
let toggles_rows = vec![
option_row("Shift + C", "cd on exit", bool_span(app.cd_on_exit)),
option_row("w", "single pane", bool_span(app.single_pane)),
option_row("Shift + T", "theme panel", bool_span(app.show_theme_panel)),
option_row("z", "show sizes", bool_span(app.active_pane().show_sizes)),
option_row(
"Shift + Z",
"folder sizes",
bool_span(app.active_pane().show_dir_sizes),
),
option_row("v", "git markers", bool_span(app.active_pane().show_git)),
];
let toggles_cell = Paragraph::new(toggles_rows).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.dim)),
);
frame.render_widget(toggles_cell, slots[3]);
section_title(frame, slots[5], "Editor");
let editor_label = app.editor.label().to_string();
let editor_val_style = if app.editor == crate::app::Editor::None {
off_style
} else {
Style::default()
.fg(theme.success)
.add_modifier(Modifier::BOLD)
};
let editor_rows = vec![option_row(
"Shift + E",
"editor",
Span::styled(editor_label, editor_val_style),
)];
let editor_cell = Paragraph::new(editor_rows).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.dim)),
);
frame.render_widget(editor_cell, slots[6]);
section_title(frame, slots[8], "File Ops");
let fileops_rows = vec![
option_row(
"Space",
"mark",
Span::styled("multi-select", Style::default().fg(theme.accent)),
),
option_row(
"x",
"cut",
Span::styled("cut", Style::default().fg(theme.accent)),
),
option_row(
"p",
"paste",
Span::styled("paste", Style::default().fg(theme.accent)),
),
option_row(
"d",
"delete",
Span::styled("delete", Style::default().fg(theme.accent)),
),
option_row(
"n",
"new folder",
Span::styled("mkdir", Style::default().fg(theme.accent)),
),
option_row(
"N",
"new file",
Span::styled("touch", Style::default().fg(theme.accent)),
),
option_row(
"r",
"rename",
Span::styled("rename", Style::default().fg(theme.accent)),
),
];
let fileops_cell = Paragraph::new(fileops_rows).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.dim)),
);
frame.render_widget(fileops_cell, slots[9]);
}