start work on tui app and rework secrets system
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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
|
||||
let hosts_str = if secret.hosts.is_empty() {
|
||||
if secret.global {
|
||||
manifest
|
||||
.hosts
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
} else {
|
||||
"none".to_string()
|
||||
}
|
||||
} else {
|
||||
secret.hosts.join(", ")
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Hosts: ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(hosts_str, Style::default().fg(Color::White)),
|
||||
]));
|
||||
}
|
||||
|
||||
// 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)),
|
||||
]));
|
||||
}
|
||||
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 host owns this key
|
||||
let host_label = manifest
|
||||
.hosts
|
||||
.iter()
|
||||
.find(|(_, info)| info.keys.contains(key))
|
||||
.map(|(name, _)| format!(" ({})", name))
|
||||
.unwrap_or_default();
|
||||
Span::styled(host_label, Style::default().fg(Color::Cyan))
|
||||
};
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(truncated, Style::default().fg(Color::Green)),
|
||||
label,
|
||||
]));
|
||||
}
|
||||
|
||||
let paragraph = 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])
|
||||
}
|
||||
Reference in New Issue
Block a user