Compare commits

..

No commits in common. "a8e3a448168089d8a94e7cb80ec858a9919420c0" and "e3ce612ca375732d1af7d38f67eaa363a2897e9c" have entirely different histories.

7 changed files with 146 additions and 1306 deletions

1157
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,4 @@ serde_json = "1.0.89"
int-enum = "0.5.0"
thiserror = "1"
anyhow = "1"
sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "sqlite" ] }
async-std = { version = "1", features = [ "attributes" ] }
clap = { version = "4.3.2" , features = [ "derive" ] }

View File

@ -2,7 +2,7 @@
// use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use std::fs::File;
use std::fs::{File};
use std::io::Read;
use tui_textarea::TextArea;
@ -29,7 +29,7 @@ pub struct Project {
pub name: String,
pub filepath: String,
pub selected_column_idx: usize,
pub columns: Vec<Column>,
pub columns: Vec<Column>
}
#[derive(Debug, thiserror::Error)]
@ -52,7 +52,7 @@ pub struct TaskState<'a> {
pub title: TextArea<'a>,
pub description: TextArea<'a>,
pub focus: TaskEditFocus,
pub is_edit: bool,
pub is_edit: bool
}
impl Default for TaskState<'_> {
@ -61,7 +61,7 @@ impl Default for TaskState<'_> {
title: TextArea::default(),
description: TextArea::default(),
focus: TaskEditFocus::Title,
is_edit: false,
is_edit: false
}
}
}
@ -135,11 +135,13 @@ impl<'a> Column {
#[must_use]
pub fn get_task_state_from_curr_selected_task(&self) -> Option<TaskState<'a>> {
self.get_selected_task().map(|t| TaskState {
self.get_selected_task().map(|t| {
TaskState {
title: TextArea::from(t.title.lines()),
description: TextArea::from(t.description.lines()),
focus: TaskEditFocus::Title,
is_edit: true,
is_edit: true
}
})
}
}
@ -201,7 +203,10 @@ impl Project {
}
pub fn select_next_column(&mut self) -> &Column {
self.selected_column_idx = min(self.selected_column_idx + 1, self.columns.len() - 1);
self.selected_column_idx = min(
self.selected_column_idx + 1,
self.columns.len() - 1,
);
&self.columns[self.selected_column_idx]
}
@ -240,9 +245,7 @@ impl Project {
pub fn move_task_up(&mut self) {
let column = self.get_selected_column_mut();
if column.selected_task_idx > 0 {
column
.tasks
.swap(column.selected_task_idx, column.selected_task_idx - 1);
column.tasks.swap(column.selected_task_idx, column.selected_task_idx - 1);
column.selected_task_idx -= 1;
self.save();
}
@ -251,9 +254,7 @@ impl Project {
pub fn move_task_down(&mut self) {
let column = self.get_selected_column_mut();
if column.selected_task_idx < column.tasks.len() - 1 {
column
.tasks
.swap(column.selected_task_idx, column.selected_task_idx + 1);
column.tasks.swap(column.selected_task_idx, column.selected_task_idx + 1);
column.selected_task_idx += 1;
self.save();
}

View File

@ -1,6 +1,6 @@
use crate::app::{State, TaskEditFocus, TaskState};
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use crate::app::{TaskState, State, TaskEditFocus};
/// # Errors
///
@ -14,22 +14,23 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
// TODO: Extract this code to a separate function to avoid nesting
match task.focus {
// TODO: Handle wrapping around the enum rather than doing it manually
TaskEditFocus::Title => match key.code {
TaskEditFocus::Title => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Description,
KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::Enter => (),
_ => {
task.title.input(key);
_ => { task.title.input(key); }
}
},
TaskEditFocus::Description => match key.code {
}
TaskEditFocus::Description => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Title,
_ => {
task.description.input(key);
_ => { task.description.input(key); }
}
},
TaskEditFocus::ConfirmBtn => match key.code {
}
TaskEditFocus::ConfirmBtn => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Description,
KeyCode::Enter => {
@ -47,27 +48,31 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
project.save();
}
_ => (),
},
TaskEditFocus::CancelBtn => match key.code {
}
}
TaskEditFocus::CancelBtn => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Title,
KeyCode::BackTab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::Enter => {
state.task_edit_state = None;
}
_ => (),
},
}
}
};
}
None => match key.code {
None => {
match key.code {
KeyCode::Char('q') => state.quit = true,
KeyCode::Char('h') | KeyCode::Left => {
project.select_previous_column();
}
KeyCode::Char('j') | KeyCode::Down => column.select_next_task(),
KeyCode::Char('k') | KeyCode::Up => column.select_previous_task(),
KeyCode::Char('l') | KeyCode::Right => {
project.select_next_column();
}
KeyCode::Char('h') |
KeyCode::Left => { project.select_previous_column(); },
KeyCode::Char('j') |
KeyCode::Down => column.select_next_task(),
KeyCode::Char('k') |
KeyCode::Up => column.select_previous_task(),
KeyCode::Char('l') |
KeyCode::Right => { project.select_next_column(); },
KeyCode::Char('g') => column.select_first_task(),
KeyCode::Char('G') => column.select_last_task(),
KeyCode::Char('H') => project.move_task_previous_column(),
@ -75,15 +80,15 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
KeyCode::Char('J') => project.move_task_down(),
KeyCode::Char('K') => project.move_task_up(),
KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()),
KeyCode::Char('e') => {
state.task_edit_state = column.get_task_state_from_curr_selected_task()
}
KeyCode::Char('e') =>
state.task_edit_state = column.get_task_state_from_curr_selected_task(),
KeyCode::Char('D') => {
column.remove_task();
project.save();
}
_ => {}
},
_ => {}
}
}
}
}
Ok(())

View File

@ -1,8 +1,8 @@
#![deny(rust_2018_idioms)]
mod app;
mod input;
mod ui;
mod input;
pub use app::*;
pub use input::handle;
pub use ui::draw;
pub use input::handle;

View File

@ -1,19 +1,23 @@
#![deny(rust_2018_idioms)]
use clap::{Parser, ValueHint::FilePath};
use kanban_tui::{Project, State};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
terminal::{
disable_raw_mode,
enable_raw_mode,
EnterAlternateScreen,
LeaveAlternateScreen
},
};
use kanban_tui::{Project, State};
use sqlx::sqlite::SqlitePool;
use std::{
error::Error,
fs::{File, OpenOptions},
io::{self, Write},
path::PathBuf,
fs::{File, OpenOptions},
error::Error
};
use tui::backend::CrosstermBackend;
use tui::Terminal;
use clap::{Parser, ValueHint::FilePath};
const DEFAULT_DATABASE_NAME: &str = "kanban.json";
@ -23,7 +27,7 @@ const DEFAULT_DATABASE_NAME: &str = "kanban.json";
pub struct CliArgs {
#[arg(value_name="DATABASE", value_hint=FilePath, index=1)]
/// Path to the
pub filepath: Option<PathBuf>,
pub filepath: Option<PathBuf>
}
// TODO: This should just return a struct beacuse we should add a
@ -38,33 +42,33 @@ fn prompt_project_init(default_name: &str) -> (String, io::Result<File>) {
let result = io::stdin().read_line(&mut input);
let input = input.trim();
let filename = match result {
let filename =
match result {
Ok(b) if b == 0 => std::process::exit(0),
Ok(b) if b > 0 && !input.is_empty() => input,
_ => default_name,
_ => default_name
};
// TODO: This might be a good time to prompt the user if they want
// to change the default column names
(
filename.to_string(),
(filename.to_string(),
OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(filename),
)
.open(filename))
}
#[async_std::main]
async fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (filepath, file) = match CliArgs::parse() {
CliArgs {
filepath: Some(filepath),
} => {
fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (filepath, file) =
match CliArgs::parse() {
CliArgs { filepath: Some(filepath) } => {
let fpath = filepath.into_os_string().into_string().unwrap();
let file = OpenOptions::new().write(true).read(true).open(&fpath);
let file = OpenOptions::new()
.write(true)
.read(true)
.open(&fpath);
if let Ok(f) = file {
(fpath, f)
@ -72,7 +76,7 @@ async fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (fp, fname) = prompt_project_init(&fpath);
(fp, fname.unwrap())
}
}
},
CliArgs { filepath: None } => {
let file = OpenOptions::new()
.write(true)
@ -86,21 +90,6 @@ async fn main() -> anyhow::Result<(), Box<dyn Error>> {
}
}
};
let pool = SqlitePool::connect("sqlite:db.sqlite").await?;
let stuff = sqlx::query!(
r#"
select * from kanban
"#
)
.fetch_all(&pool)
.await?;
for item in stuff {
println!("{} - {} - {}", item.id, item.name, item.description);
}
let mut state = State::new(Project::load(filepath, &file)?);
enable_raw_mode()?;

View File

@ -19,8 +19,7 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
.split(area);
for (i, column) in state.project.columns.iter().enumerate() {
let items: Vec<ListItem<'_>> = column
.tasks
let items: Vec<ListItem<'_>> = column.tasks
.iter()
.enumerate()
.map(|(j, task)| {
@ -124,7 +123,7 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
[
Constraint::Percentage(80),
Constraint::Min(10),
Constraint::Min(10),
Constraint::Min(10)
]
.as_ref(),
)
@ -168,8 +167,7 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
task.title.set_block(b1);
if let TaskEditFocus::Title = task.focus {
task.title.set_style(Style::default().fg(Color::Yellow));
task.title
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
task.title.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else {
task.title.set_style(Style::default());
task.title.set_cursor_style(Style::default());
@ -178,10 +176,8 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
task.description.set_block(b2);
if let TaskEditFocus::Description = task.focus {
task.description
.set_style(Style::default().fg(Color::Yellow));
task.description
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
task.description.set_style(Style::default().fg(Color::Yellow));
task.description.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else {
task.description.set_style(Style::default());
task.description.set_cursor_style(Style::default());