Skip to content

Commit 590b818

Browse files
authored
Merge pull request #7 from Julien-R44/feat/search
Added fuzzy search mode + some refacto
2 parents fa66469 + 32d90eb commit 590b818

19 files changed

+562
-363
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ tokio = { version = "1", features = ["full"] }
2020
rustbreak = { version = "2.0.0", features = ["ron_enc"] }
2121
dirs = "4.0.0"
2222
serde = "1.0.130"
23-
chrono = "0.4.19"
23+
chrono = "0.4.19"
24+
sublime_fuzzy = "0.7.0"

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ Now all you have to do is launch Fast-SSH, select your service and press enter t
3434
A file database is stored at ~/.fastssh/db.ron. This file is automatically created when you launch Fast-SSH.
3535
This database is used to store the number of connections to a service and the date of last connection.
3636

37+
## Search Mode
38+
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.
39+
40+
## Shortcuts
41+
| Key | Action |
42+
| ------------- | ------------- |
43+
| h | Display Shortcuts Panel |
44+
| Enter | Validate selection : Execute SSH cmd |
45+
| Tab | Switch group |
46+
| Up/Down | Navigate through your hosts |
47+
| c | Switch Config display mode |
48+
| PageUp/Down | Scroll Configuration |
49+
| s | Enable Search Mode |
50+
| Esc | Exit Search Mode |
51+
| q | Exit Fast-SSH |
52+
53+
54+
3755
# Installation
3856
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 )
3957

src/app.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use tui::widgets::TableState;
33

44
use crate::{
55
database::FileDatabase,
6+
searcher::Searcher,
67
ssh_config_store::{SshConfigStore, SshGroup, SshGroupItem},
78
};
89

@@ -11,7 +12,14 @@ pub enum ConfigDisplayMode {
1112
Selected,
1213
}
1314

15+
pub enum AppState {
16+
Searching,
17+
Normal,
18+
}
19+
1420
pub struct App {
21+
pub state: AppState,
22+
pub searcher: Searcher,
1523
pub selected_group: usize,
1624
pub host_state: TableState,
1725
pub scs: SshConfigStore,
@@ -29,6 +37,7 @@ impl App {
2937
let scs = SshConfigStore::new(&db).await?;
3038

3139
Ok(App {
40+
state: AppState::Normal,
3241
selected_group: 0,
3342
config_paragraph_offset: 0,
3443
scs,
@@ -37,6 +46,7 @@ impl App {
3746
should_spawn_ssh: false,
3847
config_display_mode: ConfigDisplayMode::Selected,
3948
db,
49+
searcher: Searcher::new(),
4050
show_help: false,
4151
})
4252
}
@@ -69,4 +79,56 @@ impl App {
6979
None
7080
}
7181
}
82+
83+
pub fn get_all_items(&self) -> Vec<&SshGroupItem> {
84+
self.scs
85+
.groups
86+
.iter()
87+
.map(|group| &group.items)
88+
.flatten()
89+
.collect::<Vec<&SshGroupItem>>()
90+
}
91+
92+
pub fn get_items_based_on_mode(&self) -> Vec<&SshGroupItem> {
93+
let items: Vec<&SshGroupItem> = match self.state {
94+
AppState::Normal => self
95+
.get_selected_group()
96+
.items
97+
.iter()
98+
.collect::<Vec<&SshGroupItem>>(),
99+
AppState::Searching => self.searcher.get_filtered_items(self),
100+
};
101+
102+
items
103+
}
104+
105+
pub fn change_selected_group(&mut self) {
106+
self.selected_group = (self.selected_group + 1) % self.scs.groups.len();
107+
}
108+
109+
pub fn change_selected_item(&mut self, rot_right: bool) {
110+
let items_len = self.get_items_based_on_mode().len();
111+
let i = match self.host_state.selected() {
112+
Some(i) => {
113+
if rot_right {
114+
(i + 1) % items_len
115+
} else {
116+
(i + items_len - 1) % items_len
117+
}
118+
}
119+
None => 0,
120+
};
121+
self.host_state.select(Some(i));
122+
}
123+
124+
pub fn scroll_config_paragraph(&mut self, offset: i64) {
125+
self.config_paragraph_offset = (self.config_paragraph_offset as i64 + offset).max(0) as u16;
126+
}
127+
128+
pub fn toggle_config_display_mode(&mut self) {
129+
self.config_display_mode = match self.config_display_mode {
130+
ConfigDisplayMode::Global => ConfigDisplayMode::Selected,
131+
ConfigDisplayMode::Selected => ConfigDisplayMode::Global,
132+
};
133+
}
72134
}

src/input_handler.rs

Lines changed: 31 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,42 @@
11
use crossterm::event::{self, Event, KeyCode};
22

3-
use crate::app::{App, ConfigDisplayMode};
3+
use crate::app::{App, AppState};
44

55
pub fn handle_inputs(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
66
if let Event::Key(key) = event::read()? {
7+
match app.state {
8+
AppState::Normal => handle_input_normal_mode(app, key.code),
9+
AppState::Searching => handle_input_search_mode(app, key.code),
10+
};
11+
712
match key.code {
8-
KeyCode::Tab => {
9-
app.selected_group = (app.selected_group + 1) % app.scs.groups.len();
10-
}
11-
KeyCode::Down => {
12-
let items = &app.scs.groups[app.selected_group].items;
13-
let i = match app.host_state.selected() {
14-
Some(i) => {
15-
if i >= items.len() - 1 {
16-
0
17-
} else {
18-
i + 1
19-
}
20-
}
21-
None => 0,
22-
};
23-
app.host_state.select(Some(i));
24-
}
25-
KeyCode::Up => {
26-
let items = &app.scs.groups[app.selected_group].items;
27-
let i = match app.host_state.selected() {
28-
Some(i) => {
29-
if i == 0 {
30-
items.len() - 1
31-
} else {
32-
i - 1
33-
}
34-
}
35-
None => 0,
36-
};
37-
app.host_state.select(Some(i));
38-
}
39-
KeyCode::PageDown => {
40-
app.config_paragraph_offset += 1;
41-
}
42-
KeyCode::PageUp => {
43-
app.config_paragraph_offset =
44-
(app.config_paragraph_offset as i64 - 1).max(0) as u16;
45-
}
46-
KeyCode::Char('c') => {
47-
app.config_display_mode = match app.config_display_mode {
48-
ConfigDisplayMode::Global => ConfigDisplayMode::Selected,
49-
ConfigDisplayMode::Selected => ConfigDisplayMode::Global,
50-
};
51-
}
52-
KeyCode::Char('h') => {
53-
app.show_help = !app.show_help;
54-
}
55-
KeyCode::Char('q') => app.should_quit = true,
13+
KeyCode::Tab => app.change_selected_group(),
14+
KeyCode::Down => app.change_selected_item(true),
15+
KeyCode::Up => app.change_selected_item(false),
16+
KeyCode::PageDown => app.scroll_config_paragraph(1),
17+
KeyCode::PageUp => app.scroll_config_paragraph(-1),
5618
KeyCode::Enter => app.should_spawn_ssh = true,
5719
_ => {}
58-
}
20+
};
5921
}
6022
Ok(())
6123
}
24+
25+
fn handle_input_search_mode(app: &mut App, key: KeyCode) {
26+
match key {
27+
KeyCode::Esc => app.state = AppState::Normal,
28+
KeyCode::Char(c) => app.searcher.add_char(c),
29+
KeyCode::Backspace => app.searcher.del_char(),
30+
_ => {}
31+
}
32+
}
33+
34+
fn handle_input_normal_mode(app: &mut App, key: KeyCode) {
35+
match key {
36+
KeyCode::Char('c') => app.toggle_config_display_mode(),
37+
KeyCode::Char('h') => app.show_help = !app.show_help,
38+
KeyCode::Char('s') => app.state = AppState::Searching,
39+
KeyCode::Char('q') => app.should_quit = true,
40+
_ => {}
41+
}
42+
}

src/layout.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use std::io::Stdout;
2+
3+
use tui::{
4+
backend::CrosstermBackend,
5+
layout::{Constraint, Direction, Layout, Rect},
6+
Frame,
7+
};
8+
9+
use crate::app::App;
10+
11+
pub struct AppLayout {
12+
pub chunks_top: Vec<Rect>,
13+
pub chunks_bot: Vec<Rect>,
14+
}
15+
16+
pub fn create_layout(app: &App, frame: &mut Frame<CrosstermBackend<Stdout>>) -> AppLayout {
17+
let base_chunk = Layout::default()
18+
.direction(Direction::Vertical)
19+
.margin(1)
20+
.horizontal_margin(4)
21+
.constraints([Constraint::Length(3), Constraint::Percentage(90)].as_ref())
22+
.split(frame.size());
23+
24+
let chunks_top = Layout::default()
25+
.direction(Direction::Horizontal)
26+
.margin(0)
27+
.constraints(
28+
[
29+
Constraint::Percentage(80),
30+
Constraint::Length(2),
31+
Constraint::Length(10),
32+
]
33+
.as_ref(),
34+
)
35+
.split(base_chunk[0]);
36+
37+
let constraints = match app.show_help {
38+
false => {
39+
vec![
40+
Constraint::Percentage(50),
41+
Constraint::Length(2),
42+
Constraint::Percentage(50),
43+
]
44+
}
45+
true => {
46+
vec![
47+
Constraint::Percentage(40),
48+
Constraint::Length(2),
49+
Constraint::Percentage(30),
50+
Constraint::Length(2),
51+
Constraint::Percentage(30),
52+
]
53+
}
54+
};
55+
56+
let chunks_bot = Layout::default()
57+
.direction(Direction::Horizontal)
58+
.margin(1)
59+
.horizontal_margin(0)
60+
.constraints(constraints.as_ref())
61+
.split(base_chunk[1]);
62+
63+
AppLayout {
64+
chunks_bot,
65+
chunks_top,
66+
}
67+
}

0 commit comments

Comments
 (0)