227 lines
7.4 KiB
Rust
227 lines
7.4 KiB
Rust
use ratatui::{
|
|
buffer::Buffer,
|
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
style::{Color, Modifier, Style, Stylize},
|
|
text::{Line, Span, Text},
|
|
widgets::{Block, BorderType, Borders, List, Paragraph, StatefulWidget, Widget},
|
|
};
|
|
|
|
use crate::app::App;
|
|
|
|
impl Widget for &mut App {
|
|
/// Renders the user interface widgets.
|
|
///
|
|
// This is where you add new widgets.
|
|
// See the following resources:
|
|
// - https://docs.rs/ratatui/latest/ratatui/widgets/index.html
|
|
// - https://github.com/ratatui/ratatui/tree/master/examples
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
let items = if let Some(manifest) = &self.manifest {
|
|
manifest
|
|
.secrets
|
|
.keys()
|
|
.map(|v| Text::from(truncate_with_ellipsis(v, 50)))
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let layout = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
|
.split(area);
|
|
let left = layout[0];
|
|
let right = layout[1];
|
|
let left_block = Block::default().title_bottom("meow").borders(Borders::ALL);
|
|
|
|
let list = List::default()
|
|
.block(left_block)
|
|
.highlight_style(Style::default().bg(Color::Blue).fg(Color::White))
|
|
.highlight_symbol("\u{25b6} ")
|
|
.items(items);
|
|
StatefulWidget::render(list, left, buf, &mut self.list_state);
|
|
|
|
render_secret_detail(self, right, buf);
|
|
}
|
|
}
|
|
|
|
fn render_secret_detail(state: &App, area: Rect, buf: &mut Buffer) {
|
|
let manifest = match &state.manifest {
|
|
Some(m) => m,
|
|
None => {
|
|
render_placeholder(area, buf, " Detail ", "No manifest loaded");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let selected_name = match state.selected_secret_name() {
|
|
Some(name) => name.to_string(),
|
|
None => {
|
|
render_placeholder(area, buf, " Detail ", "No secret selected");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let secret = match manifest.secrets.get(&selected_name) {
|
|
Some(s) => s,
|
|
None => return,
|
|
};
|
|
|
|
let title = format!(" {} ", selected_name);
|
|
let block = Block::default().title(title).borders(Borders::ALL);
|
|
// Build detail lines
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
// Separator
|
|
lines.push(Line::from(""));
|
|
|
|
// Format
|
|
lines.push(Line::from(vec![
|
|
Span::styled(" Format: ", Style::default().fg(Color::DarkGray)),
|
|
Span::styled(secret.format.to_string(), Style::default().fg(Color::White)),
|
|
]));
|
|
|
|
// Scope
|
|
let scope = if secret.global {
|
|
"global".to_string()
|
|
} else {
|
|
"host-specific".to_string()
|
|
};
|
|
lines.push(Line::from(vec![
|
|
Span::styled(" Scope: ", Style::default().fg(Color::DarkGray)),
|
|
Span::styled(scope, Style::default().fg(Color::White)),
|
|
]));
|
|
if !secret.global {
|
|
// Hosts / identities. Home-manager identities are coloured distinctly
|
|
// so it is obvious which targets are user (home) rather than system.
|
|
let mut host_spans: Vec<Span> = vec![Span::styled(
|
|
" Hosts: ",
|
|
Style::default().fg(Color::DarkGray),
|
|
)];
|
|
if secret.hosts.is_empty() {
|
|
host_spans.push(Span::styled("none", Style::default().fg(Color::White)));
|
|
} else {
|
|
for (i, identity) in secret.hosts.iter().enumerate() {
|
|
if i > 0 {
|
|
host_spans.push(Span::styled(", ", Style::default().fg(Color::DarkGray)));
|
|
}
|
|
let is_home = manifest.home_identities.contains(identity);
|
|
let style = if is_home {
|
|
Style::default().fg(Color::Magenta)
|
|
} else {
|
|
Style::default().fg(Color::White)
|
|
};
|
|
host_spans.push(Span::styled(identity.clone(), style));
|
|
}
|
|
}
|
|
lines.push(Line::from(host_spans));
|
|
}
|
|
|
|
// Recipients
|
|
lines.push(Line::from(vec![
|
|
Span::styled(" Recipients: ", Style::default().fg(Color::DarkGray)),
|
|
Span::styled(
|
|
format!("{} age keys", secret.keys.len()),
|
|
Style::default().fg(Color::White),
|
|
),
|
|
]));
|
|
|
|
// Needed for users
|
|
if secret.needed_for_users {
|
|
lines.push(Line::from(vec![
|
|
Span::styled(" Users: ", Style::default().fg(Color::DarkGray)),
|
|
Span::styled("yes", Style::default().fg(Color::Yellow)),
|
|
]));
|
|
}
|
|
|
|
// Home-manager: shown when this secret is consumed by a home-manager user.
|
|
if secret.home {
|
|
lines.push(Line::from(vec![
|
|
Span::styled(" Home-mgr: ", Style::default().fg(Color::DarkGray)),
|
|
Span::styled("yes", Style::default().fg(Color::Magenta)),
|
|
]));
|
|
}
|
|
while lines.len() < 6 {
|
|
lines.push(Line::from(""));
|
|
}
|
|
|
|
// Key details section
|
|
lines.push(Line::from(""));
|
|
lines.push(Line::from(Span::styled(
|
|
" Keys:",
|
|
Style::default()
|
|
.fg(Color::DarkGray)
|
|
.add_modifier(Modifier::BOLD),
|
|
)));
|
|
for key in &secret.keys {
|
|
let truncated = if key.len() > 20 {
|
|
format!("{}...", &key[..20])
|
|
} else {
|
|
key.clone()
|
|
};
|
|
|
|
// Check if this is a master key
|
|
let label = if manifest.master_keys.contains(key) {
|
|
Span::styled(" (master)", Style::default().fg(Color::Magenta))
|
|
} else {
|
|
// Try to find which identity owns this key. Home identities are
|
|
// tagged and coloured distinctly from system identities.
|
|
match manifest
|
|
.hosts
|
|
.iter()
|
|
.find(|(_, info)| info.keys.contains(key))
|
|
{
|
|
Some((name, _)) if manifest.home_identities.contains(name) => Span::styled(
|
|
format!(" ({name}, home)"),
|
|
Style::default().fg(Color::Magenta),
|
|
),
|
|
Some((name, _)) => {
|
|
Span::styled(format!(" ({name})"), Style::default().fg(Color::Cyan))
|
|
}
|
|
None => Span::raw(""),
|
|
}
|
|
};
|
|
|
|
lines.push(Line::from(vec![
|
|
Span::raw(" "),
|
|
Span::styled(truncated, Style::default().fg(Color::Green)),
|
|
label,
|
|
]));
|
|
}
|
|
|
|
Paragraph::new(lines).block(block).render(area, buf);
|
|
}
|
|
|
|
fn render_placeholder(area: Rect, buf: &mut Buffer, title: &str, message: &str) {
|
|
let block = Block::default()
|
|
.title(title.to_string())
|
|
.borders(Borders::ALL);
|
|
Paragraph::new(message.to_string())
|
|
.style(Style::default().fg(Color::DarkGray))
|
|
.block(block)
|
|
.render(area, buf);
|
|
}
|
|
|
|
/// Truncate a name with middle ellipsis, preserving the file extension
|
|
fn truncate_with_ellipsis(name: &str, max_width: usize) -> String {
|
|
if name.len() <= max_width {
|
|
return name.to_string();
|
|
}
|
|
|
|
if max_width < 5 {
|
|
return name[..max_width].to_string();
|
|
}
|
|
|
|
// Try to preserve the extension
|
|
if let Some(dot_pos) = name.rfind('.') {
|
|
let ext = &name[dot_pos..];
|
|
if ext.len() < max_width - 3 {
|
|
let prefix_len = max_width - 3 - ext.len();
|
|
return format!("{}...{}", &name[..prefix_len], ext);
|
|
}
|
|
}
|
|
|
|
// Fallback: just truncate with ellipsis at end
|
|
let prefix_len = max_width - 3;
|
|
format!("{}...", &name[..prefix_len])
|
|
}
|