Compare commits
No commits in common. "a8e3a448168089d8a94e7cb80ec858a9919420c0" and "e3ce612ca375732d1af7d38f67eaa363a2897e9c" have entirely different histories.
a8e3a44816
...
e3ce612ca3
1157
Cargo.lock
generated
1157
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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" ] }
|
27
src/app.rs
27
src/app.rs
@ -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();
|
||||
}
|
||||
|
59
src/input.rs
59
src/input.rs
@ -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(())
|
||||
|
@ -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;
|
||||
|
61
src/main.rs
61
src/main.rs
@ -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()?;
|
||||
|
14
src/ui.rs
14
src/ui.rs
@ -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());
|
||||
|
Loading…
x
Reference in New Issue
Block a user