Add 'upload' instruction to upload a file to the host using scp command
parent
6bbd9fb5f8
commit
2333d85627
|
|
@ -40,11 +40,12 @@ sshmngrs
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
| ---------------------- | ------------------------------- |
|
| ---------------------- | --------------------------------------- |
|
||||||
| `Enter` | Connect to the selected command |
|
| `Enter` | Connect to the selected command |
|
||||||
| `a` | Add a new command |
|
| `a` | Add a new command |
|
||||||
| `e` | Edit the selected command |
|
| `e` | Edit the selected command |
|
||||||
| `d` | Delete the selected command |
|
| `d` | Delete the selected command |
|
||||||
|
| `u` | Upload a file to the host through `scp` |
|
||||||
| `↑` / `k` | Move selection up |
|
| `↑` / `k` | Move selection up |
|
||||||
| `↓` / `j` | Move selection down |
|
| `↓` / `j` | Move selection down |
|
||||||
| `q` / `Esc` / `Ctrl+C` | Quit |
|
| `q` / `Esc` / `Ctrl+C` | Quit |
|
||||||
|
|
|
||||||
120
src/main.rs
120
src/main.rs
|
|
@ -29,6 +29,17 @@ impl Command {
|
||||||
format!("{} [{}]", self.alias, self.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)]
|
#[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 {
|
enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
Editing(CommandForm),
|
Editing(CommandForm),
|
||||||
|
Uploading(UploadForm),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
|
|
@ -169,6 +217,7 @@ impl App {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => self.on_key_normal(key.code),
|
Mode::Normal => self.on_key_normal(key.code),
|
||||||
Mode::Editing(_) => self.on_key_editing(key),
|
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('a') => self.mode = Mode::Editing(CommandForm::new()),
|
||||||
KeyCode::Char('e') => self.start_editing(),
|
KeyCode::Char('e') => self.start_editing(),
|
||||||
KeyCode::Char('d') => self.delete_selected(),
|
KeyCode::Char('d') => self.delete_selected(),
|
||||||
|
KeyCode::Char('u') => self.start_uploading(),
|
||||||
KeyCode::Enter => self.connect_selected(),
|
KeyCode::Enter => self.connect_selected(),
|
||||||
KeyCode::Up | KeyCode::Char('k') => self.select_previous(),
|
KeyCode::Up | KeyCode::Char('k') => self.select_previous(),
|
||||||
KeyCode::Down | KeyCode::Char('j') => self.select_next(),
|
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) {
|
fn on_key_editing(&mut self, key: KeyEvent) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => self.mode = Mode::Normal,
|
KeyCode::Esc => self.mode = Mode::Normal,
|
||||||
|
|
@ -327,11 +412,12 @@ fn draw(frame: &mut Frame, app: &App) {
|
||||||
match &app.mode {
|
match &app.mode {
|
||||||
Mode::Normal => {}
|
Mode::Normal => {}
|
||||||
Mode::Editing(form) => draw_edit_popup(frame, form),
|
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) {
|
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()
|
let block = Block::default()
|
||||||
.title(" sshmngrs ")
|
.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) {
|
fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused: bool) {
|
||||||
let border_style = if focused {
|
let border_style = if focused {
|
||||||
Style::new().fg(Color::Yellow)
|
Style::new().fg(Color::Yellow)
|
||||||
|
|
@ -391,7 +500,12 @@ fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused
|
||||||
Style::new()
|
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()
|
Block::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
.borders(Borders::ALL)
|
.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);
|
frame.render_widget(paragraph, area);
|
||||||
|
|
||||||
if focused {
|
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;
|
let cursor_y = area.y + 1;
|
||||||
frame.set_cursor_position((cursor_x, cursor_y));
|
frame.set_cursor_position((cursor_x, cursor_y));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue