Redesign UI, project stats panel, show project name, fixes and refactorings
This commit is contained in:
parent
a98b82f3e4
commit
d23f458444
11
src/app.rs
11
src/app.rs
@ -1,9 +1,9 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use int_enum::IntEnum;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
use int_enum::IntEnum;
|
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
|
||||||
@ -55,6 +55,7 @@ impl Default for TaskState<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct State<'a> {
|
pub struct State<'a> {
|
||||||
|
pub project_name: String,
|
||||||
pub selected_column_idx: usize,
|
pub selected_column_idx: usize,
|
||||||
pub columns: Vec<Column>,
|
pub columns: Vec<Column>,
|
||||||
pub db_conn: db::DBConn,
|
pub db_conn: db::DBConn,
|
||||||
@ -72,7 +73,15 @@ impl<'a> State<'a> {
|
|||||||
let db_conn = db::DBConn::new(conn);
|
let db_conn = db::DBConn::new(conn);
|
||||||
let columns = db_conn.get_all_columns()?;
|
let columns = db_conn.get_all_columns()?;
|
||||||
let selected_column = db_conn.get_selected_column()?;
|
let selected_column = db_conn.get_selected_column()?;
|
||||||
|
|
||||||
|
let project_name = std::env::current_dir()?
|
||||||
|
.file_name()
|
||||||
|
.and_then(std::ffi::OsStr::to_str)
|
||||||
|
.unwrap_or("KANBAN PROJECT")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
Ok(State {
|
Ok(State {
|
||||||
|
project_name,
|
||||||
columns,
|
columns,
|
||||||
selected_column_idx: selected_column,
|
selected_column_idx: selected_column,
|
||||||
quit: false,
|
quit: false,
|
||||||
|
14
src/input.rs
14
src/input.rs
@ -1,10 +1,10 @@
|
|||||||
use crate::app::{State, TaskEditFocus, TaskState, EDIT_WINDOW_FOCUS_STATES};
|
use crate::app::{State, TaskEditFocus, TaskState, EDIT_WINDOW_FOCUS_STATES};
|
||||||
|
use anyhow::Error;
|
||||||
use crossterm::event;
|
use crossterm::event;
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use int_enum::IntEnum;
|
use int_enum::IntEnum;
|
||||||
use anyhow::Error;
|
|
||||||
|
|
||||||
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error>{
|
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> {
|
||||||
let cycle;
|
let cycle;
|
||||||
if forward {
|
if forward {
|
||||||
cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES;
|
cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES;
|
||||||
@ -24,11 +24,11 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<(
|
|||||||
(KeyCode::Tab, _) => {
|
(KeyCode::Tab, _) => {
|
||||||
cycle_focus(&mut task, true)?;
|
cycle_focus(&mut task, true)?;
|
||||||
Some(task)
|
Some(task)
|
||||||
},
|
}
|
||||||
(KeyCode::BackTab, _) => {
|
(KeyCode::BackTab, _) => {
|
||||||
cycle_focus(&mut task, false)?;
|
cycle_focus(&mut task, false)?;
|
||||||
Some(task)
|
Some(task)
|
||||||
},
|
}
|
||||||
(KeyCode::Enter, TaskEditFocus::ConfirmBtn) => {
|
(KeyCode::Enter, TaskEditFocus::ConfirmBtn) => {
|
||||||
// The structure of this function is so we avoid an
|
// The structure of this function is so we avoid an
|
||||||
// unncessary clone() here. We can just transfer
|
// unncessary clone() here. We can just transfer
|
||||||
@ -49,12 +49,12 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<(
|
|||||||
(_, TaskEditFocus::Title) => {
|
(_, TaskEditFocus::Title) => {
|
||||||
task.title.input(key);
|
task.title.input(key);
|
||||||
Some(task)
|
Some(task)
|
||||||
},
|
}
|
||||||
(_, TaskEditFocus::Description) => {
|
(_, TaskEditFocus::Description) => {
|
||||||
task.description.input(key);
|
task.description.input(key);
|
||||||
Some(task)
|
Some(task)
|
||||||
}
|
}
|
||||||
_ => Some(task)
|
_ => Some(task),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -79,7 +79,7 @@ pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Er
|
|||||||
KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())),
|
KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())),
|
||||||
KeyCode::Char('e') => Ok(state.task_edit_state = state.get_task_state_from_current()),
|
KeyCode::Char('e') => Ok(state.task_edit_state = state.get_task_state_from_current()),
|
||||||
KeyCode::Char('D') => state.delete_task(),
|
KeyCode::Char('D') => state.delete_task(),
|
||||||
_ => Ok(())
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
src/ui.rs
104
src/ui.rs
@ -35,9 +35,12 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
|
|||||||
span = Span::raw(&task.title);
|
span = Span::raw(&task.title);
|
||||||
}
|
}
|
||||||
span.style = style;
|
span.style = style;
|
||||||
|
// TODO: This is unoptimized, we can actually construct the list
|
||||||
|
// inside a single ListItem it seems
|
||||||
ListItem::new(vec![Spans::from(span)])
|
ListItem::new(vec![Spans::from(span)])
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
if i == state.selected_column_idx {
|
if i == state.selected_column_idx {
|
||||||
style = style.add_modifier(Modifier::REVERSED);
|
style = style.add_modifier(Modifier::REVERSED);
|
||||||
@ -49,8 +52,10 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
|
|||||||
let inner_area = block.inner(columns[i]);
|
let inner_area = block.inner(columns[i]);
|
||||||
let inner_block = Block::default().style(style);
|
let inner_block = Block::default().style(style);
|
||||||
let list = List::new(items).block(inner_block);
|
let list = List::new(items).block(inner_block);
|
||||||
|
|
||||||
let mut list_state = ListState::default();
|
let mut list_state = ListState::default();
|
||||||
list_state.select(Some(column.selected_task_idx + 1));
|
list_state.select(Some(column.selected_task_idx + 1));
|
||||||
|
|
||||||
f.render_widget(block, columns[i]);
|
f.render_widget(block, columns[i]);
|
||||||
f.render_stateful_widget(list, inner_area, &mut list_state);
|
f.render_stateful_widget(list, inner_area, &mut list_state);
|
||||||
}
|
}
|
||||||
@ -95,7 +100,7 @@ fn centered_rect_for_popup(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
|||||||
.split(popup_layout[1])[1]
|
.split(popup_layout[1])[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) {
|
fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>, popup_title: &str) {
|
||||||
let area = centered_rect_for_popup(45, 60, f.size());
|
let area = centered_rect_for_popup(45, 60, f.size());
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(popup_title)
|
.title(popup_title)
|
||||||
@ -130,30 +135,22 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
|
|||||||
)
|
)
|
||||||
.split(layout[2]);
|
.split(layout[2]);
|
||||||
|
|
||||||
let create_style;
|
let (create_style, cancel_style, create_txt, cancel_txt) = match task.focus {
|
||||||
let create_txt;
|
TaskEditFocus::ConfirmBtn => (
|
||||||
let cancel_style;
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
let cancel_txt;
|
Style::default(),
|
||||||
match task.focus {
|
"[Confirm]",
|
||||||
TaskEditFocus::ConfirmBtn => {
|
" Cancel ",
|
||||||
create_style = Style::default().add_modifier(Modifier::BOLD);
|
),
|
||||||
cancel_style = Style::default();
|
TaskEditFocus::CancelBtn => (
|
||||||
create_txt = "[Confirm]";
|
Style::default(),
|
||||||
cancel_txt = " Cancel ";
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
}
|
" Confirm ",
|
||||||
TaskEditFocus::CancelBtn => {
|
"[Cancel]",
|
||||||
create_style = Style::default();
|
),
|
||||||
cancel_style = Style::default().add_modifier(Modifier::BOLD);
|
_ => (Style::default(), Style::default(), " Confirm ", " Cancel "),
|
||||||
create_txt = " Confirm ";
|
};
|
||||||
cancel_txt = "[Cancel]";
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
create_style = Style::default();
|
|
||||||
cancel_style = Style::default();
|
|
||||||
create_txt = " Confirm ";
|
|
||||||
cancel_txt = " Cancel ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let create_btn = Paragraph::new(create_txt).style(create_style);
|
let create_btn = Paragraph::new(create_txt).style(create_style);
|
||||||
let cancel_btn = Paragraph::new(cancel_txt).style(cancel_style);
|
let cancel_btn = Paragraph::new(cancel_txt).style(cancel_style);
|
||||||
f.render_widget(create_btn, buttons[1]);
|
f.render_widget(create_btn, buttons[1]);
|
||||||
@ -194,6 +191,40 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_project_stats<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &mut State<'_>) {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("PROJECT STATS")
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let c1_len = state.columns[0].tasks.len();
|
||||||
|
let c2_len = state.columns[1].tasks.len();
|
||||||
|
let c3_len = state.columns[2].tasks.len();
|
||||||
|
let c4_len = state.columns[3].tasks.len();
|
||||||
|
let tocomplete_total = c1_len + c2_len + c3_len;
|
||||||
|
let percentage = (c3_len as f32 / tocomplete_total as f32 * 100.0) as u8;
|
||||||
|
let list = List::new(
|
||||||
|
vec![
|
||||||
|
ListItem::new(vec![
|
||||||
|
Spans::from("Tasks per Column:"),
|
||||||
|
Spans::from(format!(" Todo ({})", c1_len)),
|
||||||
|
Spans::from(format!(" In Progress ({})", c2_len)),
|
||||||
|
Spans::from(format!(" Done ({})", c3_len)),
|
||||||
|
Spans::from(format!(" Ideas ({})", c4_len)),
|
||||||
|
Spans::from(
|
||||||
|
format!(
|
||||||
|
"Progress: {} / {} - {}%",
|
||||||
|
c3_len,
|
||||||
|
tocomplete_total,
|
||||||
|
percentage
|
||||||
|
))
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
.block(block);
|
||||||
|
|
||||||
|
f.render_widget(list, area);
|
||||||
|
}
|
||||||
|
|
||||||
/// Macro to generate keybindings string at compile time
|
/// Macro to generate keybindings string at compile time
|
||||||
macro_rules! unroll {
|
macro_rules! unroll {
|
||||||
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
|
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
|
||||||
@ -206,26 +237,35 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>) {
|
|||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Percentage(10),
|
Constraint::Length(2),
|
||||||
Constraint::Percentage(65),
|
Constraint::Min(10),
|
||||||
Constraint::Percentage(20),
|
Constraint::Max(10),
|
||||||
Constraint::Length(3),
|
Constraint::Length(2),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
|
|
||||||
let block = Block::default().title("KANBAN BOARD").borders(Borders::ALL);
|
let block = Block::default()
|
||||||
|
.title(format!("⎸ {} ⎹", state.project_name))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::TOP);
|
||||||
f.render_widget(block, main_layout[0]);
|
f.render_widget(block, main_layout[0]);
|
||||||
|
|
||||||
draw_tasks(f, main_layout[1], state);
|
draw_tasks(f, main_layout[1], state);
|
||||||
|
|
||||||
draw_task_info(f, main_layout[2], state);
|
let info_area = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![Constraint::Min(60), Constraint::Max(60)].as_ref())
|
||||||
|
.split(main_layout[2]);
|
||||||
|
|
||||||
|
draw_task_info(f, info_area[0], state);
|
||||||
|
draw_project_stats(f, info_area[1], state);
|
||||||
|
|
||||||
let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP);
|
let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP);
|
||||||
|
|
||||||
let foot_txt = unroll![
|
let foot_txt = unroll![
|
||||||
("quit", "c"),
|
("quit", "q"),
|
||||||
("navigation", "hjkl"),
|
("navigation", "hjkl"),
|
||||||
("move task", "HJKL"),
|
("move task", "HJKL"),
|
||||||
("new task", "n"),
|
("new task", "n"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user