Skip to content

Commit b726f66

Browse files
authored
Refactor & robust terminal restore (#1)
1 parent d1cbcb7 commit b726f66

File tree

6 files changed

+201
-146
lines changed

6 files changed

+201
-146
lines changed

src/event.rs

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,56 @@
11
use crate::model::{Message, Model};
2+
use anyhow::Context;
23
use crossterm::event;
34
use crossterm::event::{Event, KeyCode, KeyModifiers};
45
use ratatui::prelude::Size;
56
use std::time::Duration;
67

78
pub fn handle_event(_: &Model) -> anyhow::Result<Option<Message>> {
8-
if event::poll(Duration::from_millis(250))? {
9-
match event::read()? {
10-
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
11-
return Ok(handle_key(key));
12-
}
13-
Event::Resize(cols, rows) => {
14-
return Ok(handle_resize(cols, rows));
15-
}
16-
_ => (),
17-
}
9+
let event_available = event::poll(Duration::from_millis(250)).context("failed to poll event")?;
10+
11+
if !event_available {
12+
return Ok(None);
1813
}
19-
Ok(None)
14+
15+
let event = event::read().context("failed to read event")?;
16+
let message = match event {
17+
Event::Key(key) if key.kind == event::KeyEventKind::Press => handle_key(key),
18+
Event::Resize(cols, rows) => handle_resize(cols, rows),
19+
_ => None,
20+
};
21+
22+
Ok(message)
2023
}
2124

2225
fn handle_key(key: event::KeyEvent) -> Option<Message> {
23-
match key.modifiers {
26+
Some(match key.modifiers {
2427
KeyModifiers::NONE => match key.code {
25-
KeyCode::Home => Some(Message::First),
26-
KeyCode::End => Some(Message::Last),
27-
KeyCode::Up => Some(Message::ScrollUp),
28-
KeyCode::Down => Some(Message::ScrollDown),
29-
KeyCode::PageUp => Some(Message::PageUp),
30-
KeyCode::PageDown => Some(Message::PageDown),
31-
KeyCode::Left => Some(Message::ScrollLeft),
32-
KeyCode::Right => Some(Message::ScrollRight),
33-
KeyCode::Enter => Some(Message::Enter),
34-
KeyCode::Esc => Some(Message::Exit),
35-
KeyCode::Char('/') => Some(Message::OpenFindTask),
36-
KeyCode::Backspace => Some(Message::Backspace),
37-
KeyCode::Char(c) => Some(Message::CharacterInput(c)),
38-
_ => None,
28+
KeyCode::Home => Message::First,
29+
KeyCode::End => Message::Last,
30+
KeyCode::Up => Message::ScrollUp,
31+
KeyCode::Down => Message::ScrollDown,
32+
KeyCode::PageUp => Message::PageUp,
33+
KeyCode::PageDown => Message::PageDown,
34+
KeyCode::Left => Message::ScrollLeft,
35+
KeyCode::Right => Message::ScrollRight,
36+
KeyCode::Enter => Message::Enter,
37+
KeyCode::Esc => Message::Exit,
38+
KeyCode::Char('/') => Message::OpenFindTask,
39+
KeyCode::Backspace => Message::Backspace,
40+
KeyCode::Char(c) => Message::CharacterInput(c),
41+
_ => return None,
3942
},
4043
KeyModifiers::SHIFT => match key.code {
41-
KeyCode::Char(c) => Some(Message::CharacterInput(c)),
42-
_ => None
43-
}
44+
KeyCode::Char(c) => Message::CharacterInput(c),
45+
_ => return None,
46+
},
4447
KeyModifiers::CONTROL => match key.code {
45-
KeyCode::Char('s') => Some(Message::SaveSettings),
46-
KeyCode::Char('f') => Some(Message::OpenFindTask),
47-
_ => None,
48+
KeyCode::Char('s') => Message::SaveSettings,
49+
KeyCode::Char('f') => Message::OpenFindTask,
50+
_ => return None,
4851
},
49-
_ => None,
50-
}
52+
_ => return None,
53+
})
5154
}
5255

5356
fn handle_resize(

src/main.rs

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ mod terminal;
88
use crate::model::{Model, Screen};
99
use crate::props::Props;
1010
use crate::raw_json_lines::{RawJsonLines, SourceName};
11-
use anyhow::anyhow;
11+
use anyhow::{Context, anyhow};
1212
use clap::Parser;
13+
use ratatui::prelude::Backend;
14+
use ratatui::Terminal;
1315
use std::fs::File;
1416
use std::io;
15-
use std::io::{BufRead, Write};
17+
use std::io::BufRead;
1618
use std::path::{Path, PathBuf};
1719

1820
#[derive(Parser, Debug)]
@@ -36,21 +38,35 @@ struct Args {
3638

3739
fn main() -> anyhow::Result<()> {
3840
let args = Args::parse();
39-
let props: Props = init_props(&args)?;
41+
let props: Props = init_props(&args).context("failed to init props")?;
4042

41-
let lines = load_files(&args.files)?;
43+
let lines = load_files(&args.files).context("failed to load files")?;
4244

4345
terminal::install_panic_hook();
44-
let mut terminal = terminal::init_terminal()?;
46+
let terminal = terminal::init_terminal().context("failed to initialize terminal")?;
4547

46-
let mut model = Model::new(props, terminal.size().map_err(|e| anyhow!("{e}"))?, &lines);
48+
if let Err(err) = run_app(terminal, props, lines) {
49+
eprintln!("{err:?}");
50+
}
51+
52+
terminal::restore_terminal().context("failed to restore terminal state")?;
53+
54+
Ok(())
55+
}
56+
57+
fn run_app(mut terminal: Terminal<impl Backend>, props: Props, lines: RawJsonLines) -> Result<(), anyhow::Error> {
58+
let terminal_size = terminal.size().map_err(|e| anyhow!("{e}")).context("failed to get terminal size")?;
59+
let mut model = Model::new(props, terminal_size, &lines);
4760

4861
while model.active_screen != Screen::Done {
4962
// Render the current view
50-
terminal.draw(|f| terminal::view(&mut model, f)).map_err(|e| anyhow!("{e}"))?;
63+
terminal
64+
.draw(|f| terminal::view(&mut model, f))
65+
.map_err(|e| anyhow!("{e}"))
66+
.context("failed to draw to terminal")?;
5167

5268
// Handle events and map to a Message
53-
let mut current_msg = event::handle_event(&model)?;
69+
let mut current_msg = event::handle_event(&model).context("failed to handle event")?;
5470

5571
// Process updates as long as they return a non-None message
5672
while let Some(msg) = current_msg {
@@ -60,29 +76,32 @@ fn main() -> anyhow::Result<()> {
6076
}
6177
}
6278

63-
terminal::restore_terminal()?;
6479
Ok(())
6580
}
6681

82+
6783
fn init_props(args: &Args) -> anyhow::Result<Props> {
68-
let mut props = Props::init()?;
84+
let mut props = Props::init().context("failed to load props")?;
85+
6986
if let Some(e) = &args.field_order {
7087
props.fields_order = e.clone();
7188
}
89+
7290
if let Some(e) = &args.suppressed_fields {
7391
props.fields_suppressed = e.clone();
7492
}
93+
7594
Ok(props)
7695
}
7796

7897
fn load_files(files: &[PathBuf]) -> anyhow::Result<RawJsonLines> {
7998
let mut raw_lines = RawJsonLines::default();
80-
for f in files {
81-
let path = PathBuf::from(f);
82-
match path.extension().map(|e| e.to_str()) {
83-
Some(Some("json")) => load_lines_from_json(&mut raw_lines, &path)?,
84-
Some(Some("zip")) => load_lines_from_zip(&mut raw_lines, &path)?,
85-
_ => writeln!(&mut io::stderr(), "unknown file extension: '{}'", path.to_string_lossy()).expect("failed to write to stderr"),
99+
100+
for path in files {
101+
match path.extension().and_then(|e| e.to_str()) {
102+
Some("json") => load_lines_from_json(&mut raw_lines, path).with_context(|| format!("failed to load lines from {path:?}"))?,
103+
Some("zip") => load_lines_from_zip(&mut raw_lines, path).with_context(|| format!("failed to load lines from {path:?}"))?,
104+
_ => eprintln!("unknown file extension: '{}'", path.to_string_lossy()),
86105
}
87106
}
88107

@@ -93,34 +112,56 @@ fn load_lines_from_json(
93112
raw_lines: &mut RawJsonLines,
94113
path: &Path,
95114
) -> anyhow::Result<()> {
96-
for (line_nr, line) in io::BufReader::new(File::open(path)?).lines().enumerate() {
97-
raw_lines.push(SourceName::JsonFile(path.file_name().unwrap().to_string_lossy().into()), line_nr + 1, line?);
115+
let json_file = File::open(path).context("failed to open json")?;
116+
let json_file = io::BufReader::new(json_file);
117+
118+
for (line_nr, line) in json_file.lines().enumerate() {
119+
let line = line.context("failed to read json line")?;
120+
let file_name = path
121+
.file_name()
122+
.context("BUG: json path is missing filename")?
123+
.to_string_lossy()
124+
.into();
125+
let source_name = SourceName::JsonFile(file_name);
126+
127+
raw_lines.push(source_name, line_nr + 1, line);
98128
}
129+
99130
Ok(())
100131
}
101132

102133
fn load_lines_from_zip(
103134
raw_lines: &mut RawJsonLines,
104135
path: &Path,
105136
) -> anyhow::Result<()> {
106-
let zip_file = File::open(path)?;
107-
let mut archive = zip::ZipArchive::new(zip_file)?;
137+
let zip_file = File::open(path).context("failed to open zip")?;
138+
let mut archive = zip::ZipArchive::new(zip_file).context("failed to parse zip")?;
108139

109140
for i in 0..archive.len() {
110-
let f = archive.by_index(i)?;
111-
if f.is_file() && f.name().ends_with(".json") {
112-
let json_file = f.name().to_string();
113-
for (line_nr, line) in io::BufReader::new(f).lines().enumerate() {
114-
raw_lines.push(
115-
SourceName::JsonInZip {
116-
zip_file: path.file_name().unwrap().to_string_lossy().into(),
117-
json_file: json_file.clone(),
118-
},
119-
line_nr + 1,
120-
line?,
121-
);
122-
}
141+
let f = archive
142+
.by_index(i)
143+
.with_context(|| format!("failed to get file with index {i} from zip"))?;
144+
145+
if !f.is_file() || !f.name().ends_with(".json") {
146+
continue;
147+
}
148+
149+
let json_file = f.name().to_string();
150+
let f = io::BufReader::new(f);
151+
152+
for (line_nr, line) in f.lines().enumerate() {
153+
let line = line.context("failed to read line from file in zip")?;
154+
let zip_file = path
155+
.file_name()
156+
.context("BUG: zip path is missing filename")?
157+
.to_string_lossy()
158+
.into();
159+
let json_file = json_file.clone();
160+
let source_name = SourceName::JsonInZip { zip_file, json_file };
161+
162+
raw_lines.push(source_name, line_nr + 1, line);
123163
}
124164
}
165+
125166
Ok(())
126167
}

src/model.rs

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use std::cell::Cell;
88
use std::cmp;
99
use std::num::NonZero;
1010
use std::ops::Add;
11-
use std::rc::Rc;
1211

1312
#[derive(Clone)]
1413
pub struct Model<'a> {
@@ -372,47 +371,57 @@ impl<'a> Model<'a> {
372371
}
373372

374373
pub fn render_status_line_left(&self) -> String {
375-
match self.view_state.main_window_list_state.selected() {
376-
Some(line_nr) if self.raw_json_lines.lines.len() > line_nr => {
377-
let raw_line = &self.raw_json_lines.lines[line_nr];
378-
let source_name = self.raw_json_lines.source_name(raw_line.source_id).expect("invalid source id");
379-
format!("{}:{}", source_name, raw_line.line_nr)
380-
}
381-
_ => String::new(),
382-
}
374+
let Some(line_nr) = self.view_state.main_window_list_state.selected() else {
375+
return "".into();
376+
};
377+
378+
let Some(raw_line) = self.raw_json_lines.lines.get(line_nr) else {
379+
return "".into();
380+
};
381+
382+
let source_name = self.raw_json_lines.source_name(raw_line.source_id).expect("invalid source id");
383+
384+
format!("{}:{}", source_name, raw_line.line_nr)
383385
}
386+
384387
pub fn render_status_line_right(&self) -> String {
385388
self.last_action_result.clone()
386389
}
387390

388391
pub fn render_find_task_line_left(&self) -> Line {
389-
if let Some(task) = self.find_task.as_ref() {
390-
let color = match task.found {
391-
None => Color::default(),
392-
Some(false) => Color::Red,
393-
Some(true) => Color::Green
394-
};
395-
" [".to_span().set_style(color)
396-
.add("Find ".to_span())
397-
.add("🔍".to_span())
398-
.add(": ".bold())
399-
.add(task.search_string.to_span().bold())
400-
.add(" ] ".to_span().set_style(color)).to_owned()
401-
} else {
402-
Line::raw("").to_owned()
403-
}
392+
let Some(task) = &self.find_task else {
393+
return "".into();
394+
};
395+
396+
let color = match task.found {
397+
None => Color::default(),
398+
Some(false) => Color::Red,
399+
Some(true) => Color::Green,
400+
};
401+
402+
" [".to_span()
403+
.set_style(color)
404+
.add("Find ".to_span())
405+
.add("🔍".to_span())
406+
.add(": ".bold())
407+
.add(task.search_string.to_span().bold())
408+
.add(" ] ".to_span().set_style(color))
409+
.to_owned()
404410
}
405411

406412
pub fn render_find_task_line_right(&self) -> Line {
407-
if let Some(t) = self.find_task.as_ref() {
408-
if let Some(state) = t.found {
409-
return match state {
410-
true => "found".to_owned().into(),
411-
false => "NOT found".to_owned().into(),
412-
}
413-
}
413+
let Some(task) = &self.find_task else {
414+
return "".into();
415+
};
416+
417+
let Some(found) = task.found else {
418+
return "".into();
419+
};
420+
421+
match found {
422+
true => "found".into(),
423+
false => "NOT found".into(),
414424
}
415-
"".into()
416425
}
417426

418427
pub fn page_len(&self) -> u16 {
@@ -512,7 +521,7 @@ pub struct ModelIntoIter<'a> {
512521
index: usize,
513522
}
514523

515-
impl<'a> ModelIntoIter<'a> {
524+
impl ModelIntoIter<'_> {
516525
// light version of Self::next() that simply skips the item.
517526
// returns true if the item was skipped, false if there are no more items
518527
fn skip_item(&mut self) -> bool {
@@ -538,19 +547,15 @@ impl<'a> Iterator for ModelIntoIter<'a> {
538547
type Item = ListItem<'a>;
539548

540549
fn next(&mut self) -> Option<Self::Item> {
541-
if self.index >= self.model.raw_json_lines.lines.len() {
542-
None
543-
} else {
544-
let raw_line = &self.model.raw_json_lines.lines[self.index];
545-
let json: Rc<serde_json::Value> = Rc::new(serde_json::from_str(&raw_line.content).expect("invalid json"));
546-
let line = match json.as_ref() {
547-
serde_json::Value::Object(o) => self.model.render_json_line(o),
548-
e => Line::from(format!("{e}")),
549-
};
550+
let raw_line = self.model.raw_json_lines.lines.get(self.index)?;
551+
let json = serde_json::from_str::<serde_json::Value>(&raw_line.content).expect("invalid json");
552+
let line = match json {
553+
serde_json::Value::Object(o) => self.model.render_json_line(&o),
554+
e => Line::from(format!("{e}")),
555+
};
550556

551-
self.index += 1;
552-
Some(ListItem::new(line))
553-
}
557+
self.index += 1;
558+
Some(ListItem::new(line))
554559
}
555560

556561
fn size_hint(&self) -> (usize, Option<usize>) {

0 commit comments

Comments
 (0)