Skip to content

Commit 0136352

Browse files
committed
feat: add filter history with arrow key navigation
Implement filter history allowing users to recall previous filter patterns using Up/Down arrow keys while in filter input mode. Features: - Store up to 50 filter patterns in history - Press Up arrow to navigate to older filters - Press Down arrow to navigate to newer filters - Down past newest clears input - Automatically saves filters when Enter is pressed - Skips duplicate consecutive entries - Skips empty patterns Implementation: - Added filter_history: Vec<String> to App state - Added history_index: Option<usize> for navigation tracking - Added add_to_history(), history_up(), history_down() methods - Enabled HistoryUp/HistoryDown events - Handle Up/Down keys in filter input mode - Save to history on FilterInputSubmit - Reset history_index on filter cancel Tests: - 12 new tests for history management and navigation - All 130 tests passing
1 parent 409d710 commit 0136352

File tree

4 files changed

+311
-8
lines changed

4 files changed

+311
-8
lines changed

generate_colored_logs.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,5 @@ while true; do
111111
echo -e "[$COUNTER] $COLORED_TIMESTAMP $COLORED_LEVEL $COLORED_MESSAGE"
112112

113113
# Sleep for 1 second
114-
sleep 1
114+
sleep 0.1
115115
done

src/app.rs

Lines changed: 289 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ pub struct App {
6464

6565
/// Skip scroll adjustment on next render (set by mouse scroll)
6666
skip_scroll_adjustment: bool,
67+
68+
/// Filter history (up to 50 entries)
69+
filter_history: Vec<String>,
70+
71+
/// Current position in filter history (None = not navigating)
72+
history_index: Option<usize>,
6773
}
6874

6975
impl App {
@@ -85,6 +91,8 @@ impl App {
8591
last_filtered_line: 0,
8692
show_help: false,
8793
skip_scroll_adjustment: false,
94+
filter_history: Vec::new(),
95+
history_index: None,
8896
}
8997
}
9098

@@ -254,6 +262,86 @@ impl App {
254262
pub fn cancel_filter_input(&mut self) {
255263
self.input_mode = InputMode::Normal;
256264
self.input_buffer.clear();
265+
self.history_index = None;
266+
}
267+
268+
/// Add filter pattern to history (called on filter submit)
269+
pub fn add_to_history(&mut self, pattern: String) {
270+
if pattern.is_empty() {
271+
return;
272+
}
273+
274+
// Don't add if it's the same as the last entry
275+
if let Some(last) = self.filter_history.last() {
276+
if last == &pattern {
277+
return;
278+
}
279+
}
280+
281+
// Add to history
282+
self.filter_history.push(pattern);
283+
284+
// Limit history to 50 entries
285+
if self.filter_history.len() > 50 {
286+
self.filter_history.remove(0);
287+
}
288+
289+
// Reset history navigation
290+
self.history_index = None;
291+
}
292+
293+
/// Navigate up in filter history (older entries)
294+
pub fn history_up(&mut self) {
295+
if self.filter_history.is_empty() {
296+
return;
297+
}
298+
299+
let new_index = match self.history_index {
300+
None => {
301+
// First time navigating - save current input and go to most recent
302+
Some(self.filter_history.len() - 1)
303+
}
304+
Some(idx) => {
305+
// Already navigating - go to older entry
306+
if idx > 0 {
307+
Some(idx - 1)
308+
} else {
309+
Some(idx) // At oldest, stay there
310+
}
311+
}
312+
};
313+
314+
self.history_index = new_index;
315+
if let Some(idx) = new_index {
316+
self.input_buffer = self.filter_history[idx].clone();
317+
}
318+
}
319+
320+
/// Navigate down in filter history (newer entries)
321+
pub fn history_down(&mut self) {
322+
if self.filter_history.is_empty() {
323+
return;
324+
}
325+
326+
let new_index = match self.history_index {
327+
None => None, // Not navigating, do nothing
328+
Some(idx) => {
329+
if idx < self.filter_history.len() - 1 {
330+
Some(idx + 1)
331+
} else {
332+
// At newest entry, go back to empty input
333+
None
334+
}
335+
}
336+
};
337+
338+
self.history_index = new_index;
339+
if let Some(idx) = new_index {
340+
self.input_buffer = self.filter_history[idx].clone();
341+
} else {
342+
// Back to empty
343+
self.input_buffer.clear();
344+
}
257345
}
258346

259347
/// Add a character to the input buffer
@@ -366,7 +454,12 @@ impl App {
366454
AppEvent::StartFilterInput => self.start_filter_input(),
367455
AppEvent::FilterInputChar(c) => self.input_char(c),
368456
AppEvent::FilterInputBackspace => self.input_backspace(),
369-
AppEvent::FilterInputSubmit => self.cancel_filter_input(),
457+
AppEvent::FilterInputSubmit => {
458+
// Save current filter to history before closing
459+
let pattern = self.input_buffer.clone();
460+
self.add_to_history(pattern);
461+
self.cancel_filter_input();
462+
}
370463
AppEvent::FilterInputCancel => self.cancel_filter_input(),
371464
AppEvent::ClearFilter => self.clear_filter(),
372465

@@ -466,13 +559,14 @@ impl App {
466559
}
467560
AppEvent::LineJumpInputCancel => self.cancel_line_jump_input(),
468561

562+
// Filter history navigation
563+
AppEvent::HistoryUp => self.history_up(),
564+
AppEvent::HistoryDown => self.history_down(),
565+
469566
// Future events - not yet implemented
470567
AppEvent::StartFilter { .. } => {
471568
// Will be handled in main loop to trigger background filter
472569
}
473-
AppEvent::HistoryUp | AppEvent::HistoryDown => {
474-
// Not yet implemented - placeholders for future features
475-
}
476570
}
477571
}
478572
}
@@ -1110,4 +1204,195 @@ mod tests {
11101204
// Should apply padding adjustment (selection is at top, should add padding)
11111205
assert_eq!(app.scroll_position, 2); // 5 - 3 (padding)
11121206
}
1207+
1208+
#[test]
1209+
fn test_add_to_history() {
1210+
let mut app = App::new(10);
1211+
1212+
// Add patterns to history
1213+
app.add_to_history("ERROR".to_string());
1214+
app.add_to_history("WARN".to_string());
1215+
app.add_to_history("INFO".to_string());
1216+
1217+
assert_eq!(app.filter_history.len(), 3);
1218+
assert_eq!(app.filter_history[0], "ERROR");
1219+
assert_eq!(app.filter_history[1], "WARN");
1220+
assert_eq!(app.filter_history[2], "INFO");
1221+
}
1222+
1223+
#[test]
1224+
fn test_add_to_history_skips_duplicates() {
1225+
let mut app = App::new(10);
1226+
1227+
app.add_to_history("ERROR".to_string());
1228+
app.add_to_history("ERROR".to_string()); // Duplicate - should not add
1229+
1230+
assert_eq!(app.filter_history.len(), 1);
1231+
}
1232+
1233+
#[test]
1234+
fn test_add_to_history_skips_empty() {
1235+
let mut app = App::new(10);
1236+
1237+
app.add_to_history("".to_string());
1238+
1239+
assert_eq!(app.filter_history.len(), 0);
1240+
}
1241+
1242+
#[test]
1243+
fn test_history_limit() {
1244+
let mut app = App::new(10);
1245+
1246+
// Add 52 entries to exceed limit of 50
1247+
for i in 0..52 {
1248+
app.add_to_history(format!("pattern{}", i));
1249+
}
1250+
1251+
// Should only keep 50 most recent
1252+
assert_eq!(app.filter_history.len(), 50);
1253+
// Oldest should be removed
1254+
assert_eq!(app.filter_history[0], "pattern2");
1255+
assert_eq!(app.filter_history[49], "pattern51");
1256+
}
1257+
1258+
#[test]
1259+
fn test_history_up_navigation() {
1260+
use crate::event::AppEvent;
1261+
1262+
let mut app = App::new(10);
1263+
1264+
app.add_to_history("ERROR".to_string());
1265+
app.add_to_history("WARN".to_string());
1266+
app.add_to_history("INFO".to_string());
1267+
1268+
// Start filter input
1269+
app.start_filter_input();
1270+
1271+
// Navigate up (most recent)
1272+
app.apply_event(AppEvent::HistoryUp);
1273+
assert_eq!(app.input_buffer, "INFO");
1274+
assert_eq!(app.history_index, Some(2));
1275+
1276+
// Navigate up again (older)
1277+
app.apply_event(AppEvent::HistoryUp);
1278+
assert_eq!(app.input_buffer, "WARN");
1279+
assert_eq!(app.history_index, Some(1));
1280+
1281+
// Navigate up again
1282+
app.apply_event(AppEvent::HistoryUp);
1283+
assert_eq!(app.input_buffer, "ERROR");
1284+
assert_eq!(app.history_index, Some(0));
1285+
1286+
// Try to go up past oldest (should stay)
1287+
app.apply_event(AppEvent::HistoryUp);
1288+
assert_eq!(app.input_buffer, "ERROR");
1289+
assert_eq!(app.history_index, Some(0));
1290+
}
1291+
1292+
#[test]
1293+
fn test_history_down_navigation() {
1294+
use crate::event::AppEvent;
1295+
1296+
let mut app = App::new(10);
1297+
1298+
app.add_to_history("ERROR".to_string());
1299+
app.add_to_history("WARN".to_string());
1300+
app.add_to_history("INFO".to_string());
1301+
1302+
app.start_filter_input();
1303+
1304+
// Navigate up to oldest
1305+
app.apply_event(AppEvent::HistoryUp);
1306+
app.apply_event(AppEvent::HistoryUp);
1307+
app.apply_event(AppEvent::HistoryUp);
1308+
assert_eq!(app.input_buffer, "ERROR");
1309+
1310+
// Navigate down (newer)
1311+
app.apply_event(AppEvent::HistoryDown);
1312+
assert_eq!(app.input_buffer, "WARN");
1313+
assert_eq!(app.history_index, Some(1));
1314+
1315+
// Navigate down again
1316+
app.apply_event(AppEvent::HistoryDown);
1317+
assert_eq!(app.input_buffer, "INFO");
1318+
assert_eq!(app.history_index, Some(2));
1319+
1320+
// Navigate down past newest (should clear)
1321+
app.apply_event(AppEvent::HistoryDown);
1322+
assert_eq!(app.input_buffer, "");
1323+
assert_eq!(app.history_index, None);
1324+
}
1325+
1326+
#[test]
1327+
fn test_history_down_when_not_navigating() {
1328+
use crate::event::AppEvent;
1329+
1330+
let mut app = App::new(10);
1331+
1332+
app.add_to_history("ERROR".to_string());
1333+
app.start_filter_input();
1334+
1335+
// Down arrow when not navigating should do nothing
1336+
app.apply_event(AppEvent::HistoryDown);
1337+
assert_eq!(app.input_buffer, "");
1338+
assert_eq!(app.history_index, None);
1339+
}
1340+
1341+
#[test]
1342+
fn test_filter_submit_saves_to_history() {
1343+
use crate::event::AppEvent;
1344+
1345+
let mut app = App::new(10);
1346+
1347+
// Start filter and type
1348+
app.start_filter_input();
1349+
app.input_char('E');
1350+
app.input_char('R');
1351+
app.input_char('R');
1352+
1353+
// Submit filter
1354+
app.apply_event(AppEvent::FilterInputSubmit);
1355+
1356+
// Should be saved to history
1357+
assert_eq!(app.filter_history.len(), 1);
1358+
assert_eq!(app.filter_history[0], "ERR");
1359+
}
1360+
1361+
#[test]
1362+
fn test_cancel_filter_resets_history_index() {
1363+
use crate::event::AppEvent;
1364+
1365+
let mut app = App::new(10);
1366+
1367+
app.add_to_history("ERROR".to_string());
1368+
app.start_filter_input();
1369+
1370+
// Navigate history
1371+
app.apply_event(AppEvent::HistoryUp);
1372+
assert_eq!(app.history_index, Some(0));
1373+
1374+
// Cancel filter
1375+
app.cancel_filter_input();
1376+
1377+
// History index should be reset
1378+
assert_eq!(app.history_index, None);
1379+
}
1380+
1381+
#[test]
1382+
fn test_history_empty() {
1383+
use crate::event::AppEvent;
1384+
1385+
let mut app = App::new(10);
1386+
1387+
app.start_filter_input();
1388+
1389+
// Try to navigate empty history
1390+
app.apply_event(AppEvent::HistoryUp);
1391+
assert_eq!(app.input_buffer, "");
1392+
assert_eq!(app.history_index, None);
1393+
1394+
app.apply_event(AppEvent::HistoryDown);
1395+
assert_eq!(app.input_buffer, "");
1396+
assert_eq!(app.history_index, None);
1397+
}
11131398
}

src/event.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@ pub enum AppEvent {
5656
LineJumpInputSubmit,
5757
LineJumpInputCancel,
5858

59-
// Future events (placeholders for roadmap features)
60-
#[allow(dead_code)]
59+
// Filter history navigation
6160
HistoryUp,
62-
#[allow(dead_code)]
6361
HistoryDown,
6462

6563
// System events

src/handlers/input.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ fn handle_filter_input_mode(key: KeyEvent) -> Vec<AppEvent> {
3636
KeyCode::Backspace => vec![AppEvent::FilterInputBackspace],
3737
KeyCode::Enter => vec![AppEvent::FilterInputSubmit],
3838
KeyCode::Esc => vec![AppEvent::FilterInputCancel, AppEvent::ClearFilter],
39+
KeyCode::Up => vec![AppEvent::HistoryUp],
40+
KeyCode::Down => vec![AppEvent::HistoryDown],
3941
_ => vec![],
4042
}
4143
}
@@ -272,4 +274,22 @@ mod tests {
272274
let events = handle_input_event(key, &app);
273275
assert_eq!(events, vec![AppEvent::LineJumpInputCancel]);
274276
}
277+
278+
#[test]
279+
fn test_filter_input_history_up() {
280+
let mut app = App::new(10);
281+
app.start_filter_input();
282+
let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
283+
let events = handle_input_event(key, &app);
284+
assert_eq!(events, vec![AppEvent::HistoryUp]);
285+
}
286+
287+
#[test]
288+
fn test_filter_input_history_down() {
289+
let mut app = App::new(10);
290+
app.start_filter_input();
291+
let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
292+
let events = handle_input_event(key, &app);
293+
assert_eq!(events, vec![AppEvent::HistoryDown]);
294+
}
275295
}

0 commit comments

Comments
 (0)