use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use super::widgets::shell_join_display;
use super::*;
pub(super) const MIN_WIDTH_FOR_DETAIL: u16 = 96;
pub(super) const DETAIL_PCT: u16 = 38;
pub(super) fn detail_fits(_app: &App, width: u16) -> bool {
width >= MIN_WIDTH_FOR_DETAIL
}
pub(super) fn render_detail(f: &mut Frame, area: Rect, app: &App) {
let lines = match app.view {
View::Connections => conn_lines(app),
View::Passwords => cred_lines(app),
View::Tunnels => tunnel_lines(app),
View::Settings => setting_lines(app),
};
let para = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Details ")
.padding(Padding::horizontal(1)),
)
.wrap(Wrap { trim: false });
f.render_widget(para, area);
}
fn conn_lines(app: &App) -> Vec<Line<'static>> {
let Some(c) = app.selected_conn() else {
return vec![Line::styled("nothing selected", dim())];
};
let mut lines = vec![
Line::styled(c.name.clone(), heading()),
Line::styled(c.engine.label().to_string(), dim()),
Line::raw(""),
];
if c.engine.networked() {
lines.push(reach_line(app.reach.get(&c.key()), sleeping_via(c)));
}
lines.push(match app.history.get(&c.key()) {
Some(e) => Line::styled(
format!(
"last {} · opened {} time{}",
history::ago(e.last, history::now()),
e.count,
if e.count == 1 { "" } else { "s" }
),
dim(),
),
None => Line::styled("never opened from here", dim()),
});
lines.push(Line::raw(""));
if c.engine == Engine::Sqlite {
lines.push(row("File", shorten(&c.database)));
let exists = crate::ini::expand_tilde(&c.database).exists();
lines.push(Line::styled(
if exists {
"● the file is there".to_string()
} else {
"● no such file yet (sqlite3 would create it)".to_string()
},
if exists {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::Yellow)
},
));
} else {
for (label, value) in [
("Host", c.host.clone()),
("Port", c.port_or_default()),
("Database", c.database.clone()),
("User", c.user.clone()),
] {
if !value.is_empty() {
lines.push(row(label, value));
}
}
for (k, v) in &c.extra {
lines.push(row(k, v.clone()));
}
lines.push(Line::raw(""));
lines.push(if !c.engine.stores_password() {
Line::styled(
format!("{} asks for the password itself", c.engine.default_client()),
dim(),
)
} else if app.has_password(c) {
Line::styled(
"password on file (the client reads it itself)",
Style::default().fg(Color::Green),
)
} else {
Line::styled("no password saved (p saves one)", dim())
});
}
let via: Vec<String> = app
.tunnels
.iter()
.filter(|t| {
t.ports()
.map(|(_, target, port)| target == c.host && port == c.port_or_default())
.unwrap_or(false)
})
.map(|t| {
format!(
"localhost:{} via {}",
t.ports().map(|(o, _, _)| o).unwrap_or(""),
t.host
)
})
.collect();
if !via.is_empty() {
lines.push(Line::raw(""));
lines.push(row("Tunnels", via.join(", ")));
}
if let Some(v) = crate::vias::get(&c.key()) {
let live = crate::tunnels::carrying(&v.local).is_some();
lines.push(Line::raw(""));
lines.push(row("Needs tunnel", format!("through {}", v.host)));
lines.push(Line::styled(
if live {
" open now".to_string()
} else {
format!(" not open: Enter reopens it, {}", v.command())
},
dim(),
));
if !crate::vias::points_at_it(c, &v) {
lines.push(Line::styled(
format!(
" but this connects to {}:{}, not localhost:{} - e to repoint it",
c.host,
c.port_or_default(),
v.local
),
Style::default().fg(Color::Yellow),
));
}
}
lines.push(Line::raw(""));
lines.push(row(
"Defined in",
shorten(&c.engine.store().to_string_lossy()),
));
lines.push(Line::raw(""));
lines.push(Line::styled(
shell_join_display(&c.connect_argv(&app.settings)),
Style::default().fg(Color::Green),
));
lines.push(Line::styled(format!("esql {}", c.name), dim()));
lines
}
fn cred_lines(app: &App) -> Vec<Line<'static>> {
let Some(c) = app.selected_cred() else {
return vec![Line::styled("nothing selected", dim())];
};
let mut lines = vec![
Line::styled(c.user.clone(), heading()),
Line::styled(c.engine.label().to_string(), dim()),
Line::raw(""),
];
lines.push(row("Host", c.host.clone()));
lines.push(row("Port", c.port.clone()));
lines.push(row("Database", c.database.clone()));
lines.push(row("User", c.user.clone()));
lines.push(Line::raw(""));
lines.push(row("Stored in", c.where_stored()));
lines.push(Line::raw(""));
let covers: Vec<String> = app
.conns
.iter()
.filter(|conn| c.covers(conn))
.map(|conn| conn.name.clone())
.collect();
lines.push(match covers.is_empty() {
true => Line::styled("covers no saved connection", dim()),
false => row("Unlocks", covers.join(", ")),
});
lines.push(Line::raw(""));
lines.push(Line::styled(
"the password itself is never read back, only written",
dim(),
));
lines
}
fn tunnel_lines(app: &App) -> Vec<Line<'static>> {
let Some(t) = app.selected_tunnel() else {
return vec![Line::styled("nothing selected", dim())];
};
let mut lines = vec![
Line::styled(format!("-{} {}", t.kind, t.spec), heading()),
Line::raw(""),
];
lines.push(Line::raw(t.explain()));
lines.push(Line::raw(""));
lines.push(row("Through", t.host.clone()));
lines.push(row("Process", format!("pid {}", t.pid)));
if let Some(started) = t.started_at() {
let secs = started
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
lines.push(row("Opened", history::ago(secs, history::now())));
}
let stderr = t.stderr();
if !stderr.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(stderr, Style::default().fg(Color::Yellow)));
}
if let Some((open, _, _)) = t.ports() {
lines.push(Line::raw(""));
lines.push(Line::styled(
format!("point a connection at localhost:{open}"),
dim(),
));
}
lines.push(Line::raw(""));
lines.push(Line::styled(t.command(), Style::default().fg(Color::Green)));
lines.push(Line::styled(format!("kill {}", t.pid), dim()));
lines
}
fn setting_lines(app: &App) -> Vec<Line<'static>> {
let Some(r) = app.selected_setting() else {
return vec![Line::styled("nothing selected", dim())];
};
let mut lines = vec![Line::styled(r.label.to_string(), heading()), Line::raw("")];
lines.push(Line::styled(r.help.to_string(), dim()));
lines.push(Line::raw(""));
lines.push(row("Now", r.value.clone()));
lines.push(row("Default", r.default.clone()));
if let Some(choices) = r.choices {
lines.push(row("Choices", choices.join(" · ")));
}
lines.push(Line::raw(""));
lines.push(Line::styled(
match r.choices {
Some(_) => "↵ cycles it · d puts it back".to_string(),
None => "↵ types it · d puts it back".to_string(),
},
dim(),
));
lines.push(Line::raw(""));
lines.push(Line::styled(
shorten(&settings::path().to_string_lossy()),
dim(),
));
lines.push(row("Key", format!("{} = {}", r.key, r.value)));
lines
}
fn row(name: &str, value: String) -> Line<'static> {
Line::from(vec![label(name), Span::raw(value)])
}
fn label(name: &str) -> Span<'static> {
const W: usize = 11;
let pad = if name.len() >= W { 1 } else { W - name.len() };
Span::styled(format!("{name}{:pad$}", "", pad = pad), dim())
}
pub(super) fn sleeping_via(c: &crate::engines::Conn) -> Option<String> {
let v = crate::vias::get(&c.key())?;
crate::tunnels::carrying(&v.local)
.is_none()
.then_some(v.host)
}
fn reach_line(reach: Option<&Reach>, sleeping: Option<String>) -> Line<'static> {
if let (Some(Reach::Down), Some(host)) = (reach, sleeping.as_ref()) {
return Line::styled(
format!("● tunnel to {host} is not open · Enter reopens it and connects"),
Style::default().fg(Color::Yellow),
);
}
match reach {
Some(Reach::Up(0)) => Line::styled(
"● up · answered instantly".to_string(),
Style::default().fg(Color::Green),
),
Some(Reach::Up(ms)) => Line::styled(
format!("● up · answered in {ms} ms"),
Style::default().fg(Color::Green),
),
Some(Reach::Down) => Line::styled(
"● down · nothing listening on that port",
Style::default().fg(Color::Red),
),
Some(Reach::Probing) => Line::styled("○ checking the port…", dim()),
None => Line::styled("○ not checked (r to check)", dim()),
}
}
fn shorten(path: &str) -> String {
crate::ini::collapse_tilde(path)
}
fn dim() -> Style {
Style::default().add_modifier(Modifier::DIM)
}
fn heading() -> Style {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
}