Skip to content

Commit 8f84cf1

Browse files
authored
Merge pull request #4 from yassinebridi/1-configurable-project-root
1 configurable project root
2 parents bfba48d + 78cd2a2 commit 8f84cf1

14 files changed

+157
-49
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "serpl"
3-
version = "0.1.2"
3+
version = "0.1.4"
44
edition = "2021"
55
description = "VS Code like search and replace TUI"
66
repository = "https://github.com/yassinebridi/serpl"

src/app.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::Arc;
1+
use std::{path::PathBuf, sync::Arc};
22

33
use color_eyre::eyre::Result;
44
use crossterm::event::KeyEvent;
@@ -46,10 +46,11 @@ pub struct App {
4646
pub should_suspend: bool,
4747
pub mode: Mode,
4848
pub last_tick_key_events: Vec<KeyEvent>,
49+
pub project_root: PathBuf,
4950
}
5051

5152
impl App {
52-
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
53+
pub fn new(project_root: PathBuf) -> Result<Self> {
5354
let config = Config::new()?;
5455
let mode = Mode::Normal;
5556

@@ -81,17 +82,22 @@ impl App {
8182
config,
8283
mode,
8384
last_tick_key_events: Vec::new(),
85+
project_root,
8486
})
8587
}
8688

8789
pub async fn run(&mut self) -> Result<()> {
88-
let initial_state = State::new();
90+
log::info!("Starting app..");
91+
// log project root
92+
log::info!("Project root: {:?}", self.project_root);
93+
let initial_state = State::new(self.project_root.clone());
94+
log::info!("Initial state: {:?}", initial_state);
8995
let store = Store::new_with_state(reducer, initial_state).wrap(ThunkMiddleware).await;
9096

9197
let (action_tx, mut action_rx) = mpsc::unbounded_channel();
9298
let (redux_action_tx, mut redux_action_rx) = mpsc::unbounded_channel::<AppAction>();
9399

94-
let mut tui = tui::Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate);
100+
let mut tui = tui::Tui::new()?;
95101
// tui.mouse(true);
96102
tui.enter()?;
97103

src/cli.rs

+2-11
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@ use crate::utils::version;
77
#[derive(Parser, Debug)]
88
#[command(author, version = version(), about)]
99
pub struct Cli {
10-
#[arg(short, long, value_name = "FLOAT", help = "Tick rate, i.e. number of ticks per second", default_value_t = 1.0)]
11-
pub tick_rate: f64,
12-
13-
#[arg(
14-
short,
15-
long,
16-
value_name = "FLOAT",
17-
help = "Frame rate, i.e. number of frames per second",
18-
default_value_t = 4.0
19-
)]
20-
pub frame_rate: f64,
10+
#[arg(short, long, value_name = "PATH", help = "Path to the project root", default_value = ".")]
11+
pub project_root: PathBuf,
2112
}

src/components/preview.rs

+26-5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ impl Preview {
6161
}
6262
}
6363

64+
fn top(&mut self, state: &State) {
65+
if !self.non_divider_lines.is_empty() {
66+
self.lines_state.select(Some(self.non_divider_lines[0]));
67+
}
68+
}
69+
70+
fn bottom(&mut self, state: &State) {
71+
if !self.non_divider_lines.is_empty() {
72+
self.lines_state.select(Some(self.non_divider_lines[self.non_divider_lines.len() - 1]));
73+
}
74+
}
75+
6476
fn delete_line(&mut self, selected_result_state: &SearchResultState) {
6577
if let Some(selected_index) = self.lines_state.selected() {
6678
let line_index = self.non_divider_lines.iter().position(|&index| index == selected_index).unwrap_or(0);
@@ -84,20 +96,29 @@ impl Component for Preview {
8496

8597
fn handle_key_events(&mut self, key: KeyEvent, state: &State) -> Result<Option<AppAction>> {
8698
if state.active_tab == Tab::Preview {
87-
match key.code {
88-
KeyCode::Char('d') => {
99+
match (key.code, key.modifiers) {
100+
(KeyCode::Char('d'), _) => {
89101
self.delete_line(&state.selected_result);
90102
Ok(None)
91103
},
92-
KeyCode::Char('j') | KeyCode::Down => {
104+
(KeyCode::Char('g'), _) => {
105+
self.top(state);
106+
Ok(None)
107+
},
108+
(KeyCode::Char('G'), _) => {
109+
self.bottom(state);
110+
Ok(None)
111+
},
112+
113+
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => {
93114
self.next();
94115
Ok(None)
95116
},
96-
KeyCode::Char('k') | KeyCode::Up => {
117+
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => {
97118
self.previous();
98119
Ok(None)
99120
},
100-
KeyCode::Enter | KeyCode::Esc => {
121+
(KeyCode::Enter, _) | (KeyCode::Esc, _) => {
101122
let action = AppAction::Action(Action::SetActiveTab { tab: Tab::SearchResult });
102123
self.command_tx.as_ref().unwrap().send(action).unwrap();
103124
Ok(None)

src/components/replace.rs

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{collections::HashMap, time::Duration};
22

33
use color_eyre::eyre::Result;
4-
use crossterm::event::{KeyCode, KeyEvent};
4+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
55
use ratatui::{
66
prelude::*,
77
widgets::{block::Title, *},
@@ -20,7 +20,7 @@ use crate::{
2020
state::{ReplaceTextKind, State},
2121
thunk::ThunkAction,
2222
},
23-
tabs::Tab,
23+
tabs::Tab, utils::is_git_repo,
2424
};
2525

2626
#[derive(Default)]
@@ -58,9 +58,15 @@ impl Component for Replace {
5858

5959
fn handle_key_events(&mut self, key: KeyEvent, state: &State) -> Result<Option<AppAction>> {
6060
if state.active_tab == Tab::Replace {
61-
match key.code {
62-
KeyCode::Tab | KeyCode::BackTab => Ok(None),
63-
KeyCode::Enter => {
61+
match (key.code, key.modifiers) {
62+
(KeyCode::Char('p'), KeyModifiers::CONTROL) => {
63+
let replace_text_kind = match state.replace_text.kind {
64+
ReplaceTextKind::Simple => ReplaceTextKind::PreserveCase,
65+
ReplaceTextKind::PreserveCase => ReplaceTextKind::Simple,
66+
};
67+
Ok(None)
68+
},
69+
(KeyCode::Char('n'), KeyModifiers::CONTROL) => {
6470
let replace_text_kind = match state.replace_text.kind {
6571
ReplaceTextKind::Simple => ReplaceTextKind::PreserveCase,
6672
ReplaceTextKind::PreserveCase => ReplaceTextKind::Simple,
@@ -69,7 +75,10 @@ impl Component for Replace {
6975
Ok(None)
7076
},
7177
_ => {
72-
self.handle_input(key);
78+
let is_git_folder = is_git_repo(state.project_root.clone());
79+
if is_git_folder {
80+
self.handle_input(key);
81+
}
7382
Ok(None)
7483
},
7584
}

src/components/search.rs

+24-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
};
66

77
use color_eyre::eyre::Result;
8-
use crossterm::event::{KeyCode, KeyEvent};
8+
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
99
use ratatui::{
1010
layout::Position,
1111
prelude::*,
@@ -29,6 +29,7 @@ use crate::{
2929
},
3030
ripgrep::RipgrepOutput,
3131
tabs::Tab,
32+
utils::is_git_repo,
3233
};
3334

3435
#[derive(Default)]
@@ -68,9 +69,9 @@ impl Component for Search {
6869

6970
fn handle_key_events(&mut self, key: KeyEvent, state: &State) -> Result<Option<AppAction>> {
7071
if state.active_tab == Tab::Search {
71-
match key.code {
72-
KeyCode::Tab | KeyCode::BackTab => Ok(None),
73-
KeyCode::Enter => {
72+
match (key.code, key.modifiers) {
73+
(KeyCode::Tab, _) | (KeyCode::BackTab, _) => Ok(None),
74+
(KeyCode::Char('p'), KeyModifiers::CONTROL) => {
7475
let search_text_kind = match state.search_text.kind {
7576
SearchTextKind::Simple => SearchTextKind::MatchCase,
7677
SearchTextKind::MatchCase => SearchTextKind::MatchWholeWord,
@@ -81,10 +82,28 @@ impl Component for Search {
8182
self.change_kind(search_text_kind);
8283
Ok(None)
8384
},
84-
_ => {
85+
(KeyCode::Char('n'), KeyModifiers::CONTROL) => {
86+
let search_text_kind = match state.search_text.kind {
87+
SearchTextKind::Simple => SearchTextKind::Regex,
88+
SearchTextKind::MatchCase => SearchTextKind::Simple,
89+
SearchTextKind::MatchWholeWord => SearchTextKind::MatchCase,
90+
SearchTextKind::MatchCaseWholeWord => SearchTextKind::MatchWholeWord,
91+
SearchTextKind::Regex => SearchTextKind::MatchCaseWholeWord,
92+
};
93+
self.change_kind(search_text_kind);
94+
Ok(None)
95+
},
96+
(KeyCode::Enter, _) => {
8597
self.handle_input(key);
8698
Ok(None)
8799
},
100+
_ => {
101+
let is_git_folder = is_git_repo(state.project_root.clone());
102+
if is_git_folder {
103+
self.handle_input(key);
104+
}
105+
Ok(None)
106+
},
88107
}
89108
} else {
90109
Ok(None)

src/components/search_result.rs

+53-6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,43 @@ impl SearchResult {
9090
self.command_tx.as_ref().unwrap().send(action).unwrap();
9191
}
9292

93+
fn top(&mut self, state: &State) {
94+
if state.search_result.list.is_empty() {
95+
return;
96+
}
97+
98+
self.state.select(Some(0));
99+
let selected_result = state.search_result.list.first().unwrap();
100+
let action = AppAction::Action(Action::SetSelectedResult {
101+
result: SearchResultState {
102+
index: selected_result.index,
103+
path: selected_result.path.clone(),
104+
matches: selected_result.matches.clone(),
105+
total_matches: selected_result.total_matches,
106+
},
107+
});
108+
self.command_tx.as_ref().unwrap().send(action).unwrap();
109+
}
110+
111+
fn bottom(&mut self, state: &State) {
112+
if state.search_result.list.is_empty() {
113+
return;
114+
}
115+
116+
let i = state.search_result.list.len() - 1;
117+
self.state.select(Some(i));
118+
let selected_result = state.search_result.list.get(i).unwrap();
119+
let action = AppAction::Action(Action::SetSelectedResult {
120+
result: SearchResultState {
121+
index: selected_result.index,
122+
path: selected_result.path.clone(),
123+
matches: selected_result.matches.clone(),
124+
total_matches: selected_result.total_matches,
125+
},
126+
});
127+
self.command_tx.as_ref().unwrap().send(action).unwrap();
128+
}
129+
93130
fn calculate_total_matches(&mut self, search_result_state: &SearchResultState) -> &str {
94131
let total_matches: usize = search_result_state.matches.iter().map(|m| m.submatches.len()).sum();
95132
let total_matches_str = total_matches.to_string();
@@ -119,20 +156,28 @@ impl Component for SearchResult {
119156

120157
fn handle_key_events(&mut self, key: KeyEvent, state: &State) -> Result<Option<AppAction>> {
121158
if state.active_tab == Tab::SearchResult {
122-
match key.code {
123-
KeyCode::Char('d') => {
159+
match (key.code, key.modifiers) {
160+
(KeyCode::Char('d'), _) => {
124161
self.delete_file(&state.selected_result);
125162
Ok(None)
126163
},
127-
KeyCode::Char('j') => {
164+
(KeyCode::Char('g'), _) => {
165+
self.top(state);
166+
Ok(None)
167+
},
168+
(KeyCode::Char('G'), _) => {
169+
self.bottom(state);
170+
Ok(None)
171+
},
172+
(KeyCode::Char('j'), _) => {
128173
self.next(state);
129174
Ok(None)
130175
},
131-
KeyCode::Char('k') => {
176+
(KeyCode::Char('k'), _) => {
132177
self.previous(state);
133178
Ok(None)
134179
},
135-
KeyCode::Enter => {
180+
(KeyCode::Enter, _) => {
136181
let action = AppAction::Action(Action::SetActiveTab { tab: Tab::Preview });
137182
self.command_tx.as_ref().unwrap().send(action).unwrap();
138183
Ok(None)
@@ -155,13 +200,15 @@ impl Component for SearchResult {
155200
block
156201
};
157202

203+
let project_root = state.project_root.to_string_lossy();
158204
let list_items: Vec<ListItem> = state
159205
.search_result
160206
.list
161207
.iter()
162208
.map(|s| {
163209
let text = Line::from(vec![
164-
Span::raw(&s.path),
210+
// Display the relative path
211+
Span::raw(s.path.strip_prefix(format!("{}/", project_root).as_str()).unwrap_or(&s.path)),
165212
Span::raw(" ("),
166213
Span::styled(s.total_matches.to_string(), Style::default().fg(Color::Yellow)),
167214
Span::raw(")"),

src/components/small_help.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@ impl Component for SmallHelp {
5151
fn draw(&mut self, f: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
5252
let layout = get_layout(area);
5353
let content = if state.active_tab == Tab::Search {
54-
"Search mode: [Ctrl + s] to search, [Ctrl + r] to replace, [Ctrl + g] to switch to normal mode"
54+
"Search: <Enter> search, <Tab> switch replace, <Ctrl-n> toggle search mode"
5555
} else if state.active_tab == Tab::Replace {
56-
"Replace mode: [Ctrl + s] to search, [Ctrl + r] to replace, [Ctrl + g] to switch to normal mode"
56+
"Replace: <C-o> replace, <Tab> switch search list, <Ctrl-n> toggle search mode"
5757
} else if state.active_tab == Tab::SearchResult {
58-
"Search result mode: [Enter] to open file, [Ctrl + s] to search, [Ctrl + r] to replace, [Ctrl + g] to switch to normal mode"
58+
"Search List: <Enter> open file, <Tab> switch search, <j> go next, <k> go previous, <g> go top, <G> go bottom, <d> delete file"
59+
} else if state.active_tab == Tab::Preview {
60+
"Preview: <Enter> go back to list, <Tab> switch search, <j> go next, <k> go previous, <g> go top, <G> go bottom, <d> delete line"
5961
} else {
60-
"Preview mode: [Ctrl + s] to search, [Ctrl + r] to replace, [Ctrl + g] to switch to normal mode"
62+
"<Ctrl-C> exit"
6163
};
6264

6365
let small_help = SmallHelpWidget::new(content.to_string(), Color::Blue, Alignment::Left);

src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async fn tokio_main() -> Result<()> {
3434
initialize_panic_handler()?;
3535

3636
let args = Cli::parse();
37-
let mut app = App::new(args.tick_rate, args.frame_rate)?;
37+
let mut app = App::new(args.project_root)?;
3838
app.run().await?;
3939

4040
Ok(())

src/redux/reducer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub fn reducer(state: State, action: Action) -> State {
143143
},
144144
Action::ChangeMode { mode } => State { mode, ..state },
145145
Action::SetGlobalLoading { global_loading } => State { global_loading, ..state },
146-
Action::ResetState => State::new(),
146+
Action::ResetState => State::new(state.project_root.clone()),
147147
Action::SetNotification { message, show, ttl, color } => {
148148
State { notification: NotificationState { message, show, ttl, color }, ..state }
149149
},

src/redux/state.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct State {
2020
pub global_loading: bool,
2121
pub notification: NotificationState,
2222
pub dialog: Option<Dialog>,
23+
pub project_root: PathBuf,
2324
}
2425

2526
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq)]
@@ -119,7 +120,10 @@ pub struct SubMatch {
119120
}
120121

121122
impl State {
122-
pub fn new() -> Self {
123-
Self::default()
123+
pub fn new(project_root: PathBuf) -> Self {
124+
Self {
125+
project_root,
126+
..Default::default()
127+
}
124128
}
125129
}

0 commit comments

Comments
 (0)