Add 'upload' instruction to upload a file to the host using scp command
parent
6bbd9fb5f8
commit
2333d85627
|
|
@ -40,11 +40,12 @@ sshmngrs
|
|||
### Keybindings
|
||||
|
||||
| Key | Action |
|
||||
| ---------------------- | ------------------------------- |
|
||||
| ---------------------- | --------------------------------------- |
|
||||
| `Enter` | Connect to the selected command |
|
||||
| `a` | Add a new command |
|
||||
| `e` | Edit the selected command |
|
||||
| `d` | Delete the selected command |
|
||||
| `u` | Upload a file to the host through `scp` |
|
||||
| `↑` / `k` | Move selection up |
|
||||
| `↓` / `j` | Move selection down |
|
||||
| `q` / `Esc` / `Ctrl+C` | Quit |
|
||||
|
|
|
|||
120
src/main.rs
120
src/main.rs
|
|
@ -29,6 +29,17 @@ impl Command {
|
|||
format!("{} [{}]", self.alias, self.command)
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort SSH destination ("user@host") used as the scp target.
|
||||
fn destination(&self) -> Option<String> {
|
||||
let tokens: Vec<&str> = self.command.split_whitespace().collect();
|
||||
tokens
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|token| token.contains('@'))
|
||||
.or_else(|| tokens.last())
|
||||
.map(|token| token.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
|
|
@ -123,9 +134,46 @@ impl CommandForm {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum UploadField {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
struct UploadForm {
|
||||
local: Input,
|
||||
remote: Input,
|
||||
field: UploadField,
|
||||
}
|
||||
|
||||
impl UploadForm {
|
||||
fn new(destination: &str) -> Self {
|
||||
Self {
|
||||
local: Input::default(),
|
||||
remote: Input::new(format!("{destination}:")),
|
||||
field: UploadField::Local,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_field(&mut self) {
|
||||
self.field = match self.field {
|
||||
UploadField::Local => UploadField::Remote,
|
||||
UploadField::Remote => UploadField::Local,
|
||||
};
|
||||
}
|
||||
|
||||
fn current_mut(&mut self) -> &mut Input {
|
||||
match self.field {
|
||||
UploadField::Local => &mut self.local,
|
||||
UploadField::Remote => &mut self.remote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
Normal,
|
||||
Editing(CommandForm),
|
||||
Uploading(UploadForm),
|
||||
}
|
||||
|
||||
struct App {
|
||||
|
|
@ -169,6 +217,7 @@ impl App {
|
|||
match self.mode {
|
||||
Mode::Normal => self.on_key_normal(key.code),
|
||||
Mode::Editing(_) => self.on_key_editing(key),
|
||||
Mode::Uploading(_) => self.on_key_uploading(key),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +227,7 @@ impl App {
|
|||
KeyCode::Char('a') => self.mode = Mode::Editing(CommandForm::new()),
|
||||
KeyCode::Char('e') => self.start_editing(),
|
||||
KeyCode::Char('d') => self.delete_selected(),
|
||||
KeyCode::Char('u') => self.start_uploading(),
|
||||
KeyCode::Enter => self.connect_selected(),
|
||||
KeyCode::Up | KeyCode::Char('k') => self.select_previous(),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.select_next(),
|
||||
|
|
@ -207,6 +257,41 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn start_uploading(&mut self) {
|
||||
if let Some(command) = self.config.commands.get(self.selected) {
|
||||
let destination = command.destination().unwrap_or_default();
|
||||
self.mode = Mode::Uploading(UploadForm::new(&destination));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_key_uploading(&mut self, key: KeyEvent) {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.mode = Mode::Normal,
|
||||
KeyCode::Enter => self.confirm_uploading(),
|
||||
KeyCode::Tab | KeyCode::Up | KeyCode::Down => {
|
||||
if let Mode::Uploading(form) = &mut self.mode {
|
||||
form.toggle_field();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Mode::Uploading(form) = &mut self.mode {
|
||||
form.current_mut().handle_event(&Event::Key(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_uploading(&mut self) {
|
||||
if let Mode::Uploading(form) = &self.mode {
|
||||
let local = form.local.value().trim().to_string();
|
||||
let remote = form.remote.value().trim().to_string();
|
||||
if !local.is_empty() && !remote.is_empty() {
|
||||
self.pending_command = Some(format!("scp \"{local}\" \"{remote}\""));
|
||||
}
|
||||
}
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
|
||||
fn on_key_editing(&mut self, key: KeyEvent) {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.mode = Mode::Normal,
|
||||
|
|
@ -327,11 +412,12 @@ fn draw(frame: &mut Frame, app: &App) {
|
|||
match &app.mode {
|
||||
Mode::Normal => {}
|
||||
Mode::Editing(form) => draw_edit_popup(frame, form),
|
||||
Mode::Uploading(form) => draw_upload_popup(frame, form),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_main(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let instructions = " [a] Add [e] Edit [d] Delete [q/Esc] Quit ";
|
||||
let instructions = " [a] Add [e] Edit [d] Delete [u] Upload [q/Esc] Quit ";
|
||||
|
||||
let block = Block::default()
|
||||
.title(" sshmngrs ")
|
||||
|
|
@ -384,6 +470,29 @@ fn draw_edit_popup(frame: &mut Frame, form: &CommandForm) {
|
|||
);
|
||||
}
|
||||
|
||||
fn draw_upload_popup(frame: &mut Frame, form: &UploadForm) {
|
||||
let area = centered_rect(frame.area(), 60, 6);
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
let [local_area, remote_area] =
|
||||
Layout::vertical([Constraint::Length(3), Constraint::Length(3)]).areas(area);
|
||||
|
||||
draw_field(
|
||||
frame,
|
||||
local_area,
|
||||
"Local file",
|
||||
&form.local,
|
||||
matches!(form.field, UploadField::Local),
|
||||
);
|
||||
draw_field(
|
||||
frame,
|
||||
remote_area,
|
||||
"Remote destination",
|
||||
&form.remote,
|
||||
matches!(form.field, UploadField::Remote),
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused: bool) {
|
||||
let border_style = if focused {
|
||||
Style::new().fg(Color::Yellow)
|
||||
|
|
@ -391,7 +500,12 @@ fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused
|
|||
Style::new()
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(input.value()).block(
|
||||
let inner_width = area.width.saturating_sub(3) as usize;
|
||||
let scroll = input.visual_scroll(inner_width);
|
||||
|
||||
let paragraph = Paragraph::new(input.value())
|
||||
.scroll((0, scroll as u16))
|
||||
.block(
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
|
|
@ -401,7 +515,7 @@ fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused
|
|||
frame.render_widget(paragraph, area);
|
||||
|
||||
if focused {
|
||||
let cursor_x = area.x + 1 + input.visual_cursor() as u16;
|
||||
let cursor_x = area.x + 1 + (input.visual_cursor().max(scroll) - scroll) as u16;
|
||||
let cursor_y = area.y + 1;
|
||||
frame.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue