279 lines
9.7 KiB
Rust
279 lines
9.7 KiB
Rust
use crate::app::App;
|
|
use crate::ui::component::{panel, panel_with_subtitle};
|
|
use crate::ui::{BLUE, GREEN, MUTED, SELECTED_BG, TEXT};
|
|
|
|
use super::View;
|
|
|
|
use anyhow::Result;
|
|
use crossterm::event::{KeyCode, KeyEvent};
|
|
use ratatui::{
|
|
Frame,
|
|
layout::{Alignment, Constraint, Rect},
|
|
style::{Modifier, Style, Stylize},
|
|
widgets::{Cell, Paragraph, Row, Table, Widget},
|
|
};
|
|
|
|
pub struct ImportView;
|
|
|
|
impl View for ImportView {
|
|
fn title(&self) -> &'static str { "Import" }
|
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
|
vec![
|
|
("j/k", "move"),
|
|
("Space", "toggle"),
|
|
("a/A", "all/none"),
|
|
("Enter", "import"),
|
|
("Esc", "cancel"),
|
|
]
|
|
}
|
|
|
|
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|
draw_import(frame, app, area);
|
|
}
|
|
|
|
fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> {
|
|
handle_import(app, key)
|
|
}
|
|
}
|
|
|
|
// ── Rendering ──────────────────────────────────────────────────
|
|
|
|
fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|
let shell_len = app.session.import.shell_candidates.len();
|
|
let ssh_len = app.session.import.candidates.len();
|
|
let total = shell_len + ssh_len;
|
|
|
|
if total == 0 {
|
|
Paragraph::new("\n No importable hosts or shells found")
|
|
.fg(MUTED)
|
|
.alignment(Alignment::Center)
|
|
.block(panel("Import"))
|
|
.render(area, frame.buffer_mut());
|
|
return;
|
|
}
|
|
|
|
let mut rows: Vec<Row<'_>> = Vec::new();
|
|
let mut entry_row: Vec<usize> = Vec::new(); // item_idx -> visual row
|
|
|
|
if shell_len > 0 {
|
|
rows.push(section_row("Shell", shell_len));
|
|
}
|
|
for (idx, item) in app.session.import.shell_candidates.iter().enumerate() {
|
|
entry_row.push(rows.len());
|
|
let selected_row = idx == app.session.import.cursor;
|
|
let checked = app.session.import.shell_selected.get(idx).copied().unwrap_or(false);
|
|
let has_conflict = item.conflict.is_some();
|
|
let style = if selected_row {
|
|
Style::default().bg(SELECTED_BG).fg(TEXT).add_modifier(Modifier::BOLD)
|
|
} else if has_conflict {
|
|
Style::default().fg(MUTED)
|
|
} else {
|
|
Style::default().fg(TEXT)
|
|
};
|
|
let check_style = if checked {
|
|
Style::default().fg(GREEN).add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default().fg(MUTED)
|
|
};
|
|
let status = item
|
|
.conflict
|
|
.as_ref()
|
|
.map(|c| format!("conflict: {}", c.name))
|
|
.unwrap_or_else(|| "-".to_string());
|
|
let check_cell_style = if selected_row {
|
|
Style::default().bg(SELECTED_BG)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
rows.push(Row::new([
|
|
Cell::from(if checked { " [x]" } else { " [ ]" }).style(check_style.patch(check_cell_style)),
|
|
Cell::from(item.name.clone()).style(style),
|
|
Cell::from(item.path.display().to_string()).style(style),
|
|
Cell::from(status).style(style),
|
|
]));
|
|
}
|
|
|
|
if shell_len > 0 && ssh_len > 0 {
|
|
rows.push(Row::new(["", "", "", ""]).height(1));
|
|
}
|
|
|
|
if ssh_len > 0 {
|
|
rows.push(section_row("SSH", ssh_len));
|
|
}
|
|
for (idx, item) in app.session.import.candidates.iter().enumerate() {
|
|
entry_row.push(rows.len());
|
|
let selected_row = (shell_len + idx) == app.session.import.cursor;
|
|
let checked = app.session.import.selected.get(idx).copied().unwrap_or(false);
|
|
let style = if selected_row {
|
|
Style::default().bg(SELECTED_BG).fg(TEXT).add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default().fg(TEXT)
|
|
};
|
|
let check_style = if checked {
|
|
Style::default().fg(GREEN).add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default().fg(MUTED)
|
|
};
|
|
let check_cell_style = if selected_row {
|
|
Style::default().bg(SELECTED_BG)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
rows.push(Row::new([
|
|
Cell::from(if checked { " [x]" } else { " [ ]" }).style(check_style.patch(check_cell_style)),
|
|
Cell::from(item.name.clone()).style(style),
|
|
Cell::from(format!("{}@{}:{}", item.user, item.host, item.port)).style(style),
|
|
Cell::from(
|
|
item.identity_file
|
|
.as_ref()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| "-".to_string()),
|
|
)
|
|
.style(style),
|
|
]));
|
|
}
|
|
|
|
// Scroll to keep selected entry visible
|
|
let visible = area.height.saturating_sub(3) as usize;
|
|
if visible > 0 && !entry_row.is_empty() {
|
|
let sel_row = entry_row[app.session.import.cursor.min(entry_row.len() - 1)];
|
|
let total_rows = rows.len();
|
|
if total_rows > visible {
|
|
let scroll = if sel_row < visible / 2 {
|
|
0
|
|
} else if sel_row + visible / 2 >= total_rows {
|
|
total_rows.saturating_sub(visible)
|
|
} else {
|
|
sel_row - visible / 2
|
|
};
|
|
rows = rows.into_iter().skip(scroll).take(visible).collect();
|
|
}
|
|
}
|
|
|
|
let table = Table::new(
|
|
rows,
|
|
[
|
|
Constraint::Length(14),
|
|
Constraint::Length(22),
|
|
Constraint::Percentage(40),
|
|
Constraint::Percentage(30),
|
|
],
|
|
)
|
|
.header(
|
|
Row::new([" Use", "Name", "Target", "Identity / Status"])
|
|
.style(Style::default().fg(BLUE).add_modifier(Modifier::BOLD)),
|
|
)
|
|
.block(panel_with_subtitle(
|
|
"Import",
|
|
"Space toggle a all A none Enter import Esc cancel",
|
|
))
|
|
.column_spacing(0)
|
|
.row_highlight_style(Style::default().bg(SELECTED_BG));
|
|
frame.render_widget(table, area);
|
|
}
|
|
|
|
fn section_row(label: &str, count: usize) -> Row<'static> {
|
|
Row::new([
|
|
Cell::from(format!(" {label} ({count})"))
|
|
.style(Style::default().fg(BLUE).add_modifier(Modifier::BOLD)),
|
|
Cell::from(""),
|
|
Cell::from(""),
|
|
Cell::from(""),
|
|
])
|
|
.height(1)
|
|
}
|
|
|
|
// ── Key handling ───────────────────────────────────────────────
|
|
|
|
fn handle_import(app: &mut App, key: KeyEvent) -> Result<()> {
|
|
let shell_len = app.session.import.shell_candidates.len();
|
|
let total = shell_len + app.session.import.candidates.len();
|
|
|
|
match key.code {
|
|
KeyCode::Esc => app.session.mode = crate::app::Mode::Home,
|
|
KeyCode::Down | KeyCode::Char('j') if total > 0 => {
|
|
app.session.import.cursor = (app.session.import.cursor + 1) % total;
|
|
}
|
|
KeyCode::Up | KeyCode::Char('k') if total > 0 => {
|
|
app.session.import.cursor = (app.session.import.cursor + total - 1) % total;
|
|
}
|
|
KeyCode::Char(' ') => {
|
|
if app.session.import.cursor < shell_len {
|
|
let can_toggle = app
|
|
.session
|
|
.import
|
|
.shell_candidates
|
|
.get(app.session.import.cursor)
|
|
.is_some_and(|c| c.conflict.is_none());
|
|
if can_toggle
|
|
&& let Some(v) = app.session.import.shell_selected.get_mut(app.session.import.cursor)
|
|
{
|
|
*v = !*v;
|
|
}
|
|
} else {
|
|
let ssh_idx = app.session.import.cursor - shell_len;
|
|
if let Some(v) = app.session.import.selected.get_mut(ssh_idx) {
|
|
*v = !*v;
|
|
}
|
|
}
|
|
}
|
|
KeyCode::Char('a') => {
|
|
app.session
|
|
.import
|
|
.shell_selected
|
|
.iter_mut()
|
|
.zip(&app.session.import.shell_candidates)
|
|
.for_each(|(sel, c)| *sel = c.conflict.is_none());
|
|
app.session.import.selected.fill(true);
|
|
}
|
|
KeyCode::Char('A') => {
|
|
app.session.import.shell_selected.fill(false);
|
|
app.session.import.selected.fill(false);
|
|
}
|
|
KeyCode::Enter => {
|
|
let mut imported = 0;
|
|
let picked_shells: Vec<_> = app
|
|
.session
|
|
.import
|
|
.shell_candidates
|
|
.iter()
|
|
.zip(&app.session.import.shell_selected)
|
|
.filter_map(|(item, &selected)| selected.then_some(item.clone()))
|
|
.collect();
|
|
for candidate in &picked_shells {
|
|
if candidate.conflict.is_none() {
|
|
app.config.add_local_shell(candidate)?;
|
|
imported += 1;
|
|
}
|
|
}
|
|
let picked_ssh: Vec<_> = app
|
|
.session
|
|
.import
|
|
.candidates
|
|
.iter()
|
|
.zip(&app.session.import.selected)
|
|
.filter_map(|(item, &selected)| selected.then_some(item.clone()))
|
|
.collect();
|
|
if !picked_ssh.is_empty() {
|
|
match crate::import::import_candidates(&mut app.config, &picked_ssh) {
|
|
Ok(count) => imported += count,
|
|
Err(err) => {
|
|
app.toast(err.to_string(), false);
|
|
app.session.mode = crate::app::Mode::Home;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
if imported > 0 {
|
|
app.config.save()?;
|
|
app.toast(format!("imported {imported} items"), true);
|
|
} else {
|
|
app.toast("nothing selected", false);
|
|
}
|
|
app.session.mode = crate::app::Mode::Home;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|