Initial executable with correct motions

master
Alex Selimov 4 weeks ago
parent c6fda36b1c
commit 21ba0d9882

@ -0,0 +1,40 @@
use std::error::Error;
use crossterm::event::{self, KeyCode};
use crate::{
state::{ApplicationState, Task},
utility::Op,
};
/// Handle all key events
pub fn event_key_handler(app: &mut ApplicationState) -> Result<(), Box<dyn Error>> {
if event::poll(std::time::Duration::from_millis(100))? {
if let event::Event::Key(key_event) = event::read()? {
match key_event.code {
// Escape to exit
KeyCode::Esc => app.should_quit = true,
KeyCode::Char('a') => app.insert_task(app.selected_col, Task::new_test_task()),
KeyCode::Char('h') => app.update_selected_column(&Op::Decrement),
KeyCode::Char('l') => app.update_selected_column(&Op::Increment),
KeyCode::Char('k') => app.update_selected_item(&Op::Decrement),
KeyCode::Char('j') => app.update_selected_item(&Op::Increment),
KeyCode::Char('H') => {
app.move_task(app.selected_col, app.selected_item, &Op::Decrement, false)
}
KeyCode::Char('L') => {
app.move_task(app.selected_col, app.selected_item, &Op::Increment, false)
}
KeyCode::Char('K') => {
app.move_task(app.selected_col, app.selected_item, &Op::Decrement, true)
}
KeyCode::Char('J') => {
app.move_task(app.selected_col, app.selected_item, &Op::Increment, true)
}
// Resize the window (event handling is automatic with ratatui)
_ => {}
}
}
};
Ok(())
}

@ -0,0 +1,36 @@
pub mod event_handler;
pub mod state;
#[cfg(test)]
pub mod test;
pub mod tui;
pub mod utility;
pub mod widgets;
use crossterm::{cursor, event, event::KeyCode, terminal, ExecutableCommand};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, List, ListItem},
Terminal,
};
use state::ApplicationState;
use std::{env, error::Error, io};
use tui::run;
fn main() -> Result<(), Box<dyn Error>> {
// Get the number of columns from command-line arguments
let args: Vec<String> = env::args().collect();
let num_columns: usize = if args.len() > 1 {
args[1].parse()?
} else {
// Default to 3 columns if no argument is passed
3
};
// Initialize application state
let mut app_state = ApplicationState::new(num_columns);
run(&mut app_state)?;
// Clean up terminal state
terminal::disable_raw_mode()?;
Ok(())
}

@ -0,0 +1,161 @@
use crate::utility::Op;
#[derive(Debug)]
pub struct ApplicationState {
pub columns: usize,
col_idx: Vec<usize>,
pub tasks: Vec<Task>,
pub new_task_popup: bool,
pub should_quit: bool,
pub selected_col: usize,
pub selected_item: usize,
}
#[derive(Debug, Clone)]
pub struct Task {
pub title: String,
pub notes: String,
pub tags: Vec<String>,
}
impl Task {
pub fn new_test_task() -> Self {
Task {
title: "This is a test".to_string(),
notes: "".to_string(),
tags: vec![],
}
}
}
impl ApplicationState {
pub fn new(columns: usize) -> Self {
ApplicationState {
columns,
col_idx: (0..columns + 1).map(|_| 0).collect(),
tasks: Vec::new(),
new_task_popup: false,
should_quit: false,
selected_col: 0,
selected_item: 0,
}
}
/// Get a slice containing all of the tasks associated with that column
/// and the index of the highlighted column in that
pub fn get_col_slice(&self, col: usize) -> Vec<(&Task, bool)> {
(self.col_idx[col]..self.col_idx[col + 1])
.map(|idx| (&self.tasks[idx], idx == self.selected_item))
.collect()
}
/// Get number of items in a column
pub fn col_n_items(&self, i: usize) -> usize {
self.col_idx[i + 1] - self.col_idx[i]
}
/// Determine whether a column is empty
pub fn col_is_empty(&self, col: usize) -> bool {
self.col_idx[col] == self.col_idx[col + 1]
}
/// Placeholder testing function to insert a new task in a column
pub fn insert_task(&mut self, col: usize, task: Task) {
// Compute the index of the new item
let idx = self.col_idx[col + 1];
// Update the other columns
self.col_idx[col + 1..]
.iter_mut()
.for_each(|idx: &mut usize| *idx += 1);
// Now insert the new task into the vector
self.tasks.insert(idx, task);
self.selected_item = idx;
}
pub fn remove_task(&mut self, col: usize, item: usize) {
self.tasks.remove(item);
for start_idx in self.col_idx[col + 1..].iter_mut() {
*start_idx -= 1;
}
}
/// Update the selected column
pub fn update_selected_column(&mut self, op: &Op) {
self.selected_col = op.apply(self.selected_col).min(self.columns - 1);
self.selected_item = self.col_idx[self.selected_col];
}
/// Update the selected column
pub fn update_selected_item(&mut self, op: &Op) {
if self.col_n_items(self.selected_col) > 0 {
self.selected_item = op
.apply(self.selected_item)
.max(self.col_idx[self.selected_col])
.min(self.col_idx[self.selected_col + 1] - 1)
};
}
/// Move a task either between columns or within a column
pub fn move_task(&mut self, col: usize, item: usize, op: &Op, in_col: bool) {
if in_col {
let new_idx = op
.apply(item)
.max(self.col_idx[col])
.min(self.col_idx[col + 1] - 1);
self.tasks.swap(item, new_idx);
self.selected_item = new_idx;
} else {
match op {
Op::Decrement => {
if self.selected_col > 0 {
let task_copy = self.tasks[item].clone();
self.remove_task(col, item);
self.insert_task(col - 1, task_copy);
self.selected_col -= 1;
}
}
Op::Increment => {
if self.selected_col < self.columns - 1 {
let task_copy = self.tasks[item].clone();
self.remove_task(col, item);
self.insert_task(col + 1, task_copy);
self.selected_col += 1;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_highlighted_item() {
let mut app = ApplicationState::new(3);
app.insert_task(app.selected_col, Task::new_test_task());
app.insert_task(app.selected_col, Task::new_test_task());
app.insert_task(app.selected_col, Task::new_test_task());
app.update_selected_column(&Op::Increment);
app.insert_task(app.selected_col, Task::new_test_task());
assert_eq!(app.selected_item, 3);
let sol = app.get_col_slice(1);
assert!(sol[0].1)
}
#[test]
pub fn test_move_task() {
let mut app = ApplicationState::new(3);
app.insert_task(app.selected_col, Task::new_test_task());
app.insert_task(app.selected_col, Task::new_test_task());
app.insert_task(app.selected_col, Task::new_test_task());
app.move_task(app.selected_col, app.selected_item, &Op::Decrement, true);
assert_eq!(app.selected_item, 1);
app.move_task(app.selected_col, app.selected_item, &Op::Increment, true);
assert_eq!(app.selected_item, 2);
}
}

@ -0,0 +1 @@
use crate::{state::ApplicationState, utility::Op};

@ -0,0 +1,88 @@
use std::{error::Error, io, iter::zip};
use crossterm::{terminal, ExecutableCommand};
use ratatui::{
layout::{Constraint, Direction, Layout},
prelude::CrosstermBackend,
style::{Color, Style},
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
Terminal,
};
use crate::{
event_handler::event_key_handler,
state::ApplicationState,
widgets::blocks::{basic_block, highlighted_border_block, highlighted_item_block},
};
pub fn run(app_state: &mut ApplicationState) -> Result<(), Box<dyn Error>> {
// Set up terminal
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
stdout.execute(terminal::Clear(terminal::ClearType::All))?;
stdout.execute(terminal::EnterAlternateScreen)?;
// Initialize the terminal with the Crossterm backend
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Start the main loop for rendering
loop {
terminal.draw(|f| {
// Define the layout with the specified number of columns
let size = f.area();
let constraints: Vec<Constraint> =
vec![Constraint::Percentage(100 / app_state.columns as u16); app_state.columns]
.into_iter()
.collect();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints::<&Vec<Constraint>>(constraints.as_ref())
.split(size);
// Render a item blocks in each column
for (i, chunk) in chunks.iter().enumerate() {
let block = if i == app_state.selected_col {
highlighted_border_block(i)
} else {
basic_block(Some(i))
};
f.render_widget(block, *chunk);
let nitems = app_state.col_n_items(i);
if nitems == 0 {
continue;
}
let constraints: Vec<Constraint> =
vec![Constraint::Max(5); nitems].into_iter().collect();
let sub_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints::<&Vec<Constraint>>(constraints.as_ref())
.margin(1)
.split(*chunk);
for ((task, highlight), sub_chunk) in
zip(app_state.get_col_slice(i), sub_chunks.iter())
{
let style = if highlight {
Style::default().bg(Color::Blue).fg(Color::DarkGray)
} else {
Style::default()
};
let paragraph = Paragraph::new(task.title.clone())
.block(basic_block(None))
.style(style);
f.render_widget(paragraph, *sub_chunk);
}
}
})?;
// Handle events such as key presses
event_key_handler(app_state)?;
if app_state.should_quit {
break;
};
}
Ok(())
}

@ -0,0 +1,14 @@
/// Enum used to determine whether we increment or decrement a value
pub enum Op {
Decrement,
Increment,
}
impl Op {
pub fn apply(&self, val: usize) -> usize {
match &self {
Op::Decrement => val.saturating_sub(1),
Op::Increment => val + 1,
}
}
}

@ -0,0 +1,29 @@
use ratatui::{
style::{Color, Style},
widgets::{Block, Borders},
};
/// Return the basic block which uses normal foreground coloring
pub fn basic_block(i: Option<usize>) -> Block<'static> {
let block = Block::new().borders(Borders::ALL);
if let Some(i) = i {
block.title(format!("Column {}", i + 1))
} else {
block
}
}
/// Return the block used for highlighted borders
pub fn highlighted_border_block(i: usize) -> Block<'static> {
Block::new()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue))
.title(format!("Column {}", i + 1))
}
pub fn highlighted_item_block() -> Block<'static> {
Block::new()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue).bg(Color::Blue))
}

@ -0,0 +1 @@
pub mod blocks;
Loading…
Cancel
Save