Skip to content

Commit

Permalink
Merge pull request #7 from Julien-R44/feat/search
Browse files Browse the repository at this point in the history
Added fuzzy search mode + some refacto
  • Loading branch information
Julien-R44 authored Nov 19, 2021
2 parents fa66469 + 32d90eb commit 590b818
Show file tree
Hide file tree
Showing 19 changed files with 562 additions and 363 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ tokio = { version = "1", features = ["full"] }
rustbreak = { version = "2.0.0", features = ["ron_enc"] }
dirs = "4.0.0"
serde = "1.0.130"
chrono = "0.4.19"
chrono = "0.4.19"
sublime_fuzzy = "0.7.0"
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ Now all you have to do is launch Fast-SSH, select your service and press enter t
A file database is stored at ~/.fastssh/db.ron. This file is automatically created when you launch Fast-SSH.
This database is used to store the number of connections to a service and the date of last connection.

## Search Mode
Fast-SSH implements a search mode ( fuzzy ) that allows you to type to find one of your hosts. To use it, press `s`, start typing, finish your selection with the arrow keys then press enter once the host is selected to make the SSH connection. Press ESC if you wish to leave the search mode and return to the "groups" mode.

## Shortcuts
| Key | Action |
| ------------- | ------------- |
| h | Display Shortcuts Panel |
| Enter | Validate selection : Execute SSH cmd |
| Tab | Switch group |
| Up/Down | Navigate through your hosts |
| c | Switch Config display mode |
| PageUp/Down | Scroll Configuration |
| s | Enable Search Mode |
| Esc | Exit Search Mode |
| q | Exit Fast-SSH |



# Installation
Download the latest release for your platform [here](https://github.com/Julien-R44/fast-ssh/releases) and put in directory in your PATH. ( Packages managers coming soon )

Expand Down
62 changes: 62 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use tui::widgets::TableState;

use crate::{
database::FileDatabase,
searcher::Searcher,
ssh_config_store::{SshConfigStore, SshGroup, SshGroupItem},
};

Expand All @@ -11,7 +12,14 @@ pub enum ConfigDisplayMode {
Selected,
}

pub enum AppState {
Searching,
Normal,
}

pub struct App {
pub state: AppState,
pub searcher: Searcher,
pub selected_group: usize,
pub host_state: TableState,
pub scs: SshConfigStore,
Expand All @@ -29,6 +37,7 @@ impl App {
let scs = SshConfigStore::new(&db).await?;

Ok(App {
state: AppState::Normal,
selected_group: 0,
config_paragraph_offset: 0,
scs,
Expand All @@ -37,6 +46,7 @@ impl App {
should_spawn_ssh: false,
config_display_mode: ConfigDisplayMode::Selected,
db,
searcher: Searcher::new(),
show_help: false,
})
}
Expand Down Expand Up @@ -69,4 +79,56 @@ impl App {
None
}
}

pub fn get_all_items(&self) -> Vec<&SshGroupItem> {
self.scs
.groups
.iter()
.map(|group| &group.items)
.flatten()
.collect::<Vec<&SshGroupItem>>()
}

pub fn get_items_based_on_mode(&self) -> Vec<&SshGroupItem> {
let items: Vec<&SshGroupItem> = match self.state {
AppState::Normal => self
.get_selected_group()
.items
.iter()
.collect::<Vec<&SshGroupItem>>(),
AppState::Searching => self.searcher.get_filtered_items(self),
};

items
}

pub fn change_selected_group(&mut self) {
self.selected_group = (self.selected_group + 1) % self.scs.groups.len();
}

pub fn change_selected_item(&mut self, rot_right: bool) {
let items_len = self.get_items_based_on_mode().len();
let i = match self.host_state.selected() {
Some(i) => {
if rot_right {
(i + 1) % items_len
} else {
(i + items_len - 1) % items_len
}
}
None => 0,
};
self.host_state.select(Some(i));
}

pub fn scroll_config_paragraph(&mut self, offset: i64) {
self.config_paragraph_offset = (self.config_paragraph_offset as i64 + offset).max(0) as u16;
}

pub fn toggle_config_display_mode(&mut self) {
self.config_display_mode = match self.config_display_mode {
ConfigDisplayMode::Global => ConfigDisplayMode::Selected,
ConfigDisplayMode::Selected => ConfigDisplayMode::Global,
};
}
}
81 changes: 31 additions & 50 deletions src/input_handler.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,42 @@
use crossterm::event::{self, Event, KeyCode};

use crate::app::{App, ConfigDisplayMode};
use crate::app::{App, AppState};

pub fn handle_inputs(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
if let Event::Key(key) = event::read()? {
match app.state {
AppState::Normal => handle_input_normal_mode(app, key.code),
AppState::Searching => handle_input_search_mode(app, key.code),
};

match key.code {
KeyCode::Tab => {
app.selected_group = (app.selected_group + 1) % app.scs.groups.len();
}
KeyCode::Down => {
let items = &app.scs.groups[app.selected_group].items;
let i = match app.host_state.selected() {
Some(i) => {
if i >= items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
app.host_state.select(Some(i));
}
KeyCode::Up => {
let items = &app.scs.groups[app.selected_group].items;
let i = match app.host_state.selected() {
Some(i) => {
if i == 0 {
items.len() - 1
} else {
i - 1
}
}
None => 0,
};
app.host_state.select(Some(i));
}
KeyCode::PageDown => {
app.config_paragraph_offset += 1;
}
KeyCode::PageUp => {
app.config_paragraph_offset =
(app.config_paragraph_offset as i64 - 1).max(0) as u16;
}
KeyCode::Char('c') => {
app.config_display_mode = match app.config_display_mode {
ConfigDisplayMode::Global => ConfigDisplayMode::Selected,
ConfigDisplayMode::Selected => ConfigDisplayMode::Global,
};
}
KeyCode::Char('h') => {
app.show_help = !app.show_help;
}
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Tab => app.change_selected_group(),
KeyCode::Down => app.change_selected_item(true),
KeyCode::Up => app.change_selected_item(false),
KeyCode::PageDown => app.scroll_config_paragraph(1),
KeyCode::PageUp => app.scroll_config_paragraph(-1),
KeyCode::Enter => app.should_spawn_ssh = true,
_ => {}
}
};
}
Ok(())
}

fn handle_input_search_mode(app: &mut App, key: KeyCode) {
match key {
KeyCode::Esc => app.state = AppState::Normal,
KeyCode::Char(c) => app.searcher.add_char(c),
KeyCode::Backspace => app.searcher.del_char(),
_ => {}
}
}

fn handle_input_normal_mode(app: &mut App, key: KeyCode) {
match key {
KeyCode::Char('c') => app.toggle_config_display_mode(),
KeyCode::Char('h') => app.show_help = !app.show_help,
KeyCode::Char('s') => app.state = AppState::Searching,
KeyCode::Char('q') => app.should_quit = true,
_ => {}
}
}
67 changes: 67 additions & 0 deletions src/layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::io::Stdout;

use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
Frame,
};

use crate::app::App;

pub struct AppLayout {
pub chunks_top: Vec<Rect>,
pub chunks_bot: Vec<Rect>,
}

pub fn create_layout(app: &App, frame: &mut Frame<CrosstermBackend<Stdout>>) -> AppLayout {
let base_chunk = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.horizontal_margin(4)
.constraints([Constraint::Length(3), Constraint::Percentage(90)].as_ref())
.split(frame.size());

let chunks_top = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints(
[
Constraint::Percentage(80),
Constraint::Length(2),
Constraint::Length(10),
]
.as_ref(),
)
.split(base_chunk[0]);

let constraints = match app.show_help {
false => {
vec![
Constraint::Percentage(50),
Constraint::Length(2),
Constraint::Percentage(50),
]
}
true => {
vec![
Constraint::Percentage(40),
Constraint::Length(2),
Constraint::Percentage(30),
Constraint::Length(2),
Constraint::Percentage(30),
]
}
};

let chunks_bot = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.horizontal_margin(0)
.constraints(constraints.as_ref())
.split(base_chunk[1]);

AppLayout {
chunks_bot,
chunks_top,
}
}
Loading

0 comments on commit 590b818

Please sign in to comment.