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…
Reference in new issue