Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 62 additions & 74 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
env,
io::{self, ErrorKind},
path::PathBuf,
io::{self},
path::{Path, PathBuf},
process,
rc::Rc,
};
Expand All @@ -27,14 +27,12 @@ use crate::{
};

/// TUI application which displays the UI and handles events
#[allow(dead_code)]
pub struct App {
exit: bool,
backend: Pike,
ui_state: UIState,
}

#[allow(dead_code, unused_variables, unused_mut)]
impl App {
pub fn build(args: Args) -> App {
let cwd = env::current_dir().map_err(|_| "Failed to get current working directory");
Expand All @@ -45,15 +43,17 @@ impl App {

let config_path = args.config.map(PathBuf::from);
let file_path = args.file.map(PathBuf::from);
let no_file_open = file_path.is_none();

let backend: Result<Pike, String> =
Pike::build(cwd.expect("Error case was handled"), file_path, config_path);
let backend: Result<Pike, String> = Pike::build(
&cwd.expect("Error case was handled"),
file_path,
config_path,
);

match backend {
Ok(backend) => App::new(backend),
Err(err) => {
eprintln!("{}", err);
eprintln!("{err}");
process::exit(1);
}
}
Expand All @@ -78,6 +78,7 @@ impl App {
}

/// Builds an app with the default configuration and no open file
#[cfg(test)]
fn build_default() -> Self {
App::build(Args {
config: None,
Expand All @@ -104,7 +105,7 @@ impl App {

let cursor_pos = self.backend.cursor_position();

let mut render_cursor_position;
let render_cursor_position;

self.render_buffer_contents(main_area, frame.buffer_mut());

Expand All @@ -114,27 +115,27 @@ impl App {
if let Some(ref input_state) = file_input_value {
self.render_file_input(status_bar_area, frame.buffer_mut());
render_cursor_position = self.ui_state.calculate_cursor_position(
CursorCalculationMode::FileInput(&input_state.input),
&CursorCalculationMode::FileInput(&input_state.input),
&layout,
cursor_pos,
);
} else if let Some(ref search_input) = search_input_value {
self.render_search_input(status_bar_area, frame.buffer_mut());
render_cursor_position = self.ui_state.calculate_cursor_position(
CursorCalculationMode::FileInput(search_input),
&CursorCalculationMode::FileInput(search_input),
&layout,
cursor_pos,
);
} else {
render_cursor_position = self.ui_state.calculate_cursor_position(
CursorCalculationMode::Buffer,
&CursorCalculationMode::Buffer,
&layout,
cursor_pos,
);
self.render_status_bar(status_bar_area, frame.buffer_mut());
}

self.render_cursor(frame, render_cursor_position);
Self::render_cursor(frame, render_cursor_position);
}

/// Splits an area using the main app layout and returns the
Expand All @@ -161,7 +162,7 @@ impl App {
fn render_buffer_contents(&mut self, area: Rect, buf: &mut ratatui::prelude::Buffer) {
// Display a welcome message if no buffer is open
if self.backend.current_buffer().is_none() {
self.render_welcome_banner(area, buf);
Self::render_welcome_banner(area, buf);
}

let contents = self.backend.current_buffer_contents();
Expand All @@ -171,7 +172,7 @@ impl App {
widget.render(area, buf, &mut self.ui_state.buffer_state);
}

fn render_welcome_banner(&self, area: Rect, buf: &mut ratatui::prelude::Buffer) {
fn render_welcome_banner(area: Rect, buf: &mut ratatui::prelude::Buffer) {
let banner = WELCOME_MESSAGE;
let paragraph = Paragraph::new(banner).block(Block::default().borders(Borders::NONE));
paragraph.render(area, buf);
Expand All @@ -183,7 +184,7 @@ impl App {
let is_modified = self.backend.is_current_buffer_modified();

let indicator = if is_modified { "*" } else { "" };
let text_widget = Text::from(format!("{}{}", filename, indicator));
let text_widget = Text::from(format!("{filename}{indicator}"));

let paragraph_widget = Paragraph::new(text_widget).wrap(Wrap { trim: false });
let block_widget = paragraph_widget.block(Block::default().borders(Borders::TOP));
Expand All @@ -192,7 +193,7 @@ impl App {
}

/// Render the cursor in a given position
fn render_cursor(&self, frame: &mut ratatui::prelude::Frame, position: TerminalPosition) {
fn render_cursor(frame: &mut ratatui::prelude::Frame, position: TerminalPosition) {
frame.set_cursor_position(position);
}

Expand Down Expand Up @@ -228,7 +229,7 @@ impl App {
self.ui_state.search_input = None;
}

/// Open a file input with the given contents and store it in UIState
/// Open a file input with the given contents and store it in `UIState`
fn open_file_input(&mut self, contents: &str, role: FileInputRole) {
self.ui_state.file_input = Some((contents, role).into());
}
Expand Down Expand Up @@ -256,16 +257,15 @@ impl App {
/// indicating whether the event has been handled or not.
fn try_handle_key_press_with_file_input(&mut self, key: KeyEvent) -> bool {
// No input means the event can't be handled
let input = match self.ui_state.file_input.as_mut() {
Some(input) => input,
None => return false,
let Some(input) = self.ui_state.file_input.as_mut() else {
return false;
};

// Perform the corresponding operation and close the input
if (key.code, key.modifiers) == (KeyCode::Enter, KeyModifiers::NONE) {
let path = input.to_path();
match input.role {
FileInputRole::GetOpenPath => self.open_file_from_path(path),
FileInputRole::GetOpenPath => self.open_file_from_path(&path),
FileInputRole::GetSavePath => {
self.backend.bind_current_buffer_to_path(path);
self.handle_save_operation();
Expand Down Expand Up @@ -297,9 +297,8 @@ impl App {
/// Returns a boolean indicating whether the event has been handled or not.
fn try_handle_key_press_with_search_input(&mut self, key: KeyEvent) -> bool {
// No input means the event can't be handled
let input = match self.ui_state.search_input.as_mut() {
Some(input) => input,
None => return false,
let Some(input) = self.ui_state.search_input.as_mut() else {
return false;
};

// Perform the corresponding operation and close the input
Expand All @@ -309,7 +308,7 @@ impl App {
.backend
.search_in_current_buffer(&query)
.unwrap_or_else(|err| {
eprintln!("Error searching in buffer: {}", err);
eprintln!("Error searching in buffer: {err}");
vec![]
});

Expand Down Expand Up @@ -368,21 +367,18 @@ impl App {
}

/// Open a file from a given path
fn open_file_from_path(&mut self, path: PathBuf) {
fn open_file_from_path(&mut self, path: &Path) {
self.backend
.create_and_open_file(&path)
.create_and_open_file(path)
// TODO: display message in the UI
.expect("Error opening file!");
}

/// Try to convert a given key event to an InputRequest to be sent to a tui_input::Input
/// Try to convert a given key event to an `InputRequest` to be sent to a `tui_input::Input`
/// instance.
fn key_event_to_input_request(key: KeyEvent) -> Option<tui_input::InputRequest> {
match (key.code, key.modifiers) {
(KeyCode::Char(chr), KeyModifiers::NONE) => {
Some(tui_input::InputRequest::InsertChar(chr))
}
(KeyCode::Char(chr), KeyModifiers::SHIFT) => {
(KeyCode::Char(chr), KeyModifiers::SHIFT | KeyModifiers::NONE) => {
Some(tui_input::InputRequest::InsertChar(chr))
}
(KeyCode::Backspace, KeyModifiers::NONE) => {
Expand All @@ -408,7 +404,11 @@ impl App {
return Ok(());
}

if !key.modifiers.contains(KeyModifiers::CONTROL) && self.try_handle_input_key(key)? {
if self.try_write_char(key)? {
return Ok(());
}

if self.try_handle_special_keys(key)? {
return Ok(());
}

Expand Down Expand Up @@ -470,28 +470,37 @@ impl App {
}
}

fn try_handle_input_key(&mut self, key: KeyEvent) -> Result<bool, io::Error> {
if self.backend.current_buffer().is_none() {
fn try_write_char(&mut self, key: KeyEvent) -> Result<bool, io::Error> {
if self.backend.current_buffer().is_none() || key.modifiers.contains(KeyModifiers::CONTROL)
{
return Ok(false);
}
if let KeyCode::Char(ch) = key.code {
self.backend
.write_to_current_buffer(&ch.to_string())
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
.map_err(|e| io::Error::other(e.to_string()))?;

Ok(true)
} else {
Ok(false)
}
}

return Ok(true);
fn try_handle_special_keys(&mut self, key: KeyEvent) -> Result<bool, io::Error> {
if self.backend.current_buffer().is_none() {
return Ok(false);
}
match key.code {
KeyCode::Enter => {
self.backend
.write_to_current_buffer("\n")
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(true)
}
KeyCode::Tab => {
self.backend
.write_to_current_buffer(" ")
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(true)
}
KeyCode::Backspace => {
Expand Down Expand Up @@ -519,9 +528,9 @@ impl App {
}

fn handle_save_operation(&mut self) {
if let Some(path) = self.backend.current_buffer_path() {
if let Some(_path) = self.backend.current_buffer_path() {
if let Err(err) = self.backend.save_current_buffer() {
eprintln!("Failed to save buffer: {}", err);
eprintln!("Failed to save buffer: {err}");
}
} else {
// Ask for filepath if the buffer is not bound to one
Expand Down Expand Up @@ -558,7 +567,7 @@ mod tests {
temp_file_with_contents,
ui::{n_spaces, solid_border},
},
ui::FileInputRole,
ui::{self, FileInputRole},
};

use super::App;
Expand Down Expand Up @@ -591,6 +600,7 @@ mod tests {
/// Used in unit tests to provide the UI element, based on which the cursor
/// position should be calculated, so that a testing buffer can be created only
/// to accommodate this element instead of the whole UI.
#[derive(Copy, Clone)]
enum CursorRenderingWidget {
CurrentBuffer,
FileInput,
Expand Down Expand Up @@ -628,15 +638,14 @@ mod tests {
.file_input
.as_ref()
.expect("A file input should be open when testing cursor in file input");
app.ui_state
.calculate_cursor_for_file_input(&input.input, buf.area)
ui::UIState::calculate_cursor_for_file_input(&input.input, buf.area)
}
};

assert_eq!(pos, expected.into());
}

/// Shorthand for defining the renderer in unit tests and calling assert_cursor_render_pos
/// Shorthand for defining the renderer in unit tests and calling `assert_cursor_render_pos`
fn acrp_based_on_current_buffer(
app: &mut App,
buf: &ratatui::buffer::Buffer,
Expand Down Expand Up @@ -704,28 +713,9 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, width, 2));
let expected = Buffer::with_lines(vec![solid_border(width.into()), filename.to_string()]);
app.render_status_bar(buf.area, &mut buf);
assert_eq!(buf, expected)
assert_eq!(buf, expected);
}

#[allow(dead_code)]
/// Helper function to assert the position to render the cursor at in the visible
/// buffer after syncing the buffer contents and cursor position from the backend.
fn assert_cursor_render_pos_no_input(app: &mut App, buf: &Buffer, expected: (u16, u16)) {
let cursor_position = app.backend.cursor_position();

if let Some(cp) = cursor_position {
app.ui_state
.buffer_state
.update_x_offset(buf.area, cp.offset);
app.ui_state.buffer_state.update_y_offset(buf.area, cp.line);
}

let pos = app
.ui_state
.calculate_cursor_for_buffer(buf.area, cursor_position);

assert_eq!(pos, expected.into());
}
/// The cursor should not move past the bounds of the buffer
#[test]
fn test_cant_move_cursor_too_far_right() {
Expand Down Expand Up @@ -886,7 +876,7 @@ mod tests {
let buf = Buffer::empty(Rect::new(0, 0, 4, 1));
app.open_file_input("hello, world!", FileInputRole::GetOpenPath);
// Does not reach (3, 1) because of the border
acrp_based_on_file_input(&mut app, &buf, (2, 1))
acrp_based_on_file_input(&mut app, &buf, (2, 1));
}

#[test]
Expand Down Expand Up @@ -915,7 +905,7 @@ mod tests {

app.handle_key_event(close_event)
.expect("Failed to handle key event");
assert!(app.exit)
assert!(app.exit);
}

#[test]
Expand Down Expand Up @@ -994,8 +984,7 @@ mod tests {
for (event, expected_pos) in navigation_cases {
assert!(
app.try_handle_navigation(event),
"Navigation event {:?} was not handled",
event
"Navigation event {event:?} was not handled",
);
acrp_based_on_current_buffer(&mut app, &buf, expected_pos);
}
Expand All @@ -1009,8 +998,7 @@ mod tests {
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert!(
!app.try_handle_navigation(event),
"Navigation event {:?} was handled",
event
"Navigation event {event:?} was handled",
);
acrp_based_on_current_buffer(&mut app, &buf, (0, 0));
}
Expand Down Expand Up @@ -1096,7 +1084,7 @@ mod tests {
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
];

for event in wor_query_key_events.iter() {
for event in &wor_query_key_events {
app.handle_key_event(*event)
.expect("Failed to handle key event");
}
Expand Down Expand Up @@ -1159,7 +1147,7 @@ mod tests {
KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE),
];

for event in hello.iter() {
for event in &hello {
app.handle_key_event(*event)
.expect("Failed to handle key event");
}
Expand Down
Loading
Loading