Files
sshx/src/ui.rs
2025-12-11 12:37:35 +08:00

169 lines
5.8 KiB
Rust

use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, Paragraph, Clear},
Frame,
};
use crate::app::{App, InputMode};
pub fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(3)].as_ref())
.split(size);
let items: Vec<ListItem> = app
.servers
.iter()
.map(|s| {
let content = format!("{} ({}) - {}:{}", s.name, s.user, s.host, s.port);
ListItem::new(content).style(Style::default())
})
.collect();
let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(" SSHX - Servers "))
.highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow))
.highlight_symbol("> ");
f.render_stateful_widget(list, chunks[0], &mut app.state);
// Help text
let help_text = match app.input_mode {
InputMode::Normal => "Enter: SSH Connect | m: Mosh Connect | n: New | Shift+n: Copy ID | i: Edit | d: Delete | q: Quit",
InputMode::Adding(_) => "Enter: Save | Esc: Cancel | Tab: Next Field",
InputMode::Editing(_) => "Enter: Save | Esc: Cancel | Tab: Next Field",
};
let help = Paragraph::new(help_text)
.style(Style::default().fg(Color::Gray))
.block(Block::default().borders(Borders::ALL).title(" Help "));
f.render_widget(help, chunks[1]);
// Popup for Adding Server
if let InputMode::Adding(state) = &app.input_mode {
let block = Block::default().borders(Borders::ALL).title(" Add New Connection ");
let area = centered_rect(60, 40, size);
f.render_widget(Clear, area); // Clear the background
f.render_widget(block, area);
let input_layout = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3), // Name
Constraint::Length(3), // User
Constraint::Length(3), // Host
Constraint::Length(3), // Port
Constraint::Min(1),
]
.as_ref(),
)
.split(area);
let fields = [
("Name", &state.name),
("User (default: root)", &state.user),
("Host/IP", &state.host),
("Port (default: 22)", &state.port),
];
for (i, (label, value)) in fields.iter().enumerate() {
let mut style = Style::default();
if state.field_idx == i {
style = style.fg(Color::Yellow);
}
let input = Paragraph::new(value.as_str())
.style(style)
.block(Block::default().borders(Borders::ALL).title(*label));
f.render_widget(input, input_layout[i]);
// Show cursor in the active input field
if state.field_idx == i {
f.set_cursor(
input_layout[i].x + value.len() as u16 + 1, // Position after the text
input_layout[i].y + 1, // Middle of the input area
);
}
}
}
// Popup for Editing Server
if let InputMode::Editing(state) = &app.input_mode {
let block = Block::default().borders(Borders::ALL).title(" Edit Connection ");
let area = centered_rect(60, 40, size);
f.render_widget(Clear, area); // Clear the background
f.render_widget(block, area);
let input_layout = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3), // Name
Constraint::Length(3), // User
Constraint::Length(3), // Host
Constraint::Length(3), // Port
Constraint::Min(1),
]
.as_ref(),
)
.split(area);
let fields = [
("Name", &state.name),
("User (default: root)", &state.user),
("Host/IP", &state.host),
("Port (default: 22)", &state.port),
];
for (i, (label, value)) in fields.iter().enumerate() {
let mut style = Style::default();
if state.field_idx == i {
style = style.fg(Color::Yellow);
}
let input = Paragraph::new(value.as_str())
.style(style)
.block(Block::default().borders(Borders::ALL).title(*label));
f.render_widget(input, input_layout[i]);
// Show cursor in the active input field
if state.field_idx == i {
f.set_cursor(
input_layout[i].x + value.len() as u16 + 1, // Position after the text
input_layout[i].y + 1, // Middle of the input area
);
}
}
}
}
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.split(popup_layout[1])[1]
}