[redesign] - update ui

This commit is contained in:
Max Zhuravsky 2022-02-27 18:45:03 +03:00
parent 9517bc1966
commit 046d2e25c1
No known key found for this signature in database
GPG key ID: 2B6239B171404102
3 changed files with 110 additions and 166 deletions

View file

@ -1,4 +1,4 @@
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, PartialEq, Debug, Copy)]
pub enum Action {
Report,
Filter,

View file

@ -83,6 +83,7 @@ use task_hookrs::project::Project;
use versions::Versioning;
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
use tui::widgets::Tabs;
const MAX_LINE: usize = 4096;
@ -133,7 +134,7 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
.as_ref(),
)
.split(r);
@ -145,7 +146,7 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
.as_ref(),
)
.split(popup_layout[1])[1]
}
@ -329,8 +330,8 @@ impl TaskwarriorTui {
}
pub fn render<B>(&mut self, terminal: &mut Terminal<B>) -> Result<()>
where
B: Backend,
where
B: Backend,
{
terminal.draw(|f| self.draw(f))?;
Ok(())
@ -340,54 +341,65 @@ impl TaskwarriorTui {
let rect = f.size();
self.terminal_width = rect.width;
self.terminal_height = rect.height;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(f.size());
let tab_layout = chunks[0];
let main_layout = chunks[1];
self.draw_tabs(f, tab_layout);
match self.mode {
Mode::Tasks(_) => self.draw_task(f),
Mode::Calendar => self.draw_calendar(f),
Mode::Projects => self.draw_projects(f),
Mode::Tasks(action) => self.draw_task(f, main_layout, action),
Mode::Calendar => self.draw_calendar(f, main_layout),
Mode::Projects => self.draw_projects(f, main_layout),
}
}
fn draw_tabs(&self, f: &mut Frame<impl Backend>, layout: Rect) {
let titles: Vec<&str> = vec!["Tasks", "Projects", "Calendar"];
let tab_names: Vec<_> = titles.into_iter().map(|name| Spans::from(name)).collect();
let selected_tab = match self.mode {
Mode::Tasks(_) => 0,
Mode::Projects => 1,
Mode::Calendar => 2,
};
let tabs_block = Block::default().style(Style::default().add_modifier(Modifier::REVERSED));
let context = Spans::from(vec![
Span::from("["),
Span::from(if self.current_context.is_empty() { "none" } else { &self.current_context }),
Span::from("]")
]);
let tabs = Tabs::new(tab_names)
.block(tabs_block.clone())
.select(selected_tab)
.divider(" ")
.highlight_style(
Style::default().add_modifier(Modifier::BOLD)
);
let rects = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(context.width() as u16)])
.split(layout);
f.render_widget(tabs, rects[0]);
f.render_widget(Paragraph::new(Text::from(context)).block(tabs_block), rects[1]);
}
pub fn draw_debug(&mut self, f: &mut Frame<impl Backend>) {
let area = centered_rect(50, 50, f.size());
f.render_widget(Clear, area);
let t = format!("{}", self.current_selection,);
let t = format!("{}", self.current_selection);
let p = Paragraph::new(Text::from(t))
.block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded));
f.render_widget(p, area);
}
pub fn draw_projects(&mut self, f: &mut Frame<impl Backend>) {
let rects = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0)].as_ref())
.split(f.size());
let today = Local::today();
let title = vec![
Span::styled("Task", Style::default().add_modifier(Modifier::DIM)),
Span::from("|"),
Span::styled("Projects", Style::default().add_modifier(Modifier::BOLD)),
Span::from("|"),
Span::styled("Calendar", Style::default().add_modifier(Modifier::DIM)),
];
let split_task_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)].as_ref())
.split(rects[0]);
self.projects.report_height = split_task_layout[0].height;
self.draw_projects_report_window(f, split_task_layout[0], title);
}
fn draw_projects_report_window(&mut self, f: &mut Frame<impl Backend>, rect: Rect, title: Vec<Span>) {
pub fn draw_projects(&mut self, f: &mut Frame<impl Backend>, rect: Rect) {
let data = self.projects.data.clone();
let p = Paragraph::new(Text::from(&data[..])).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title),
);
let p = Paragraph::new(Text::from(&data[..]));
f.render_widget(p, rect);
}
@ -411,50 +423,32 @@ impl TaskwarriorTui {
style
}
pub fn draw_calendar(&mut self, f: &mut Frame<impl Backend>) {
let dates_with_styles = self.get_dates_with_styles();
let rects = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0)].as_ref())
.split(f.size());
let today = Local::today();
let mut title = vec![
Span::styled("Task", Style::default().add_modifier(Modifier::DIM)),
Span::from("|"),
Span::styled("Projects", Style::default().add_modifier(Modifier::DIM)),
Span::from("|"),
Span::styled("Calendar", Style::default().add_modifier(Modifier::BOLD)),
];
if !self.current_context.is_empty() {
let context_style = Style::default();
context_style.add_modifier(Modifier::ITALIC);
title.insert(title.len(), Span::from(" ("));
title.insert(title.len(), Span::styled(&self.current_context, context_style));
title.insert(title.len(), Span::from(")"));
}
pub fn draw_calendar(&mut self, f: &mut Frame<impl Backend>, layout: Rect) {
// FIXME:
// if !self.current_context.is_empty() {
// let context_style = Style::default();
// context_style.add_modifier(Modifier::ITALIC);
// title.insert(title.len(), Span::from(" ("));
// title.insert(title.len(), Span::styled(&self.current_context, context_style));
// title.insert(title.len(), Span::from(")"));
// }
let mut c = Calendar::default()
.block(
Block::default()
.title(Spans::from(title))
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.today_style(self.config.uda_style_calendar_today)
.year(self.calendar_year)
.date_style(dates_with_styles)
.date_style(self.get_dates_with_styles())
.months_per_row(self.config.uda_calendar_months_per_row);
c.title_background_color = self.config.uda_style_calendar_title.bg.unwrap_or(Color::Reset);
f.render_widget(c, rects[0]);
f.render_widget(c, layout);
}
pub fn draw_task(&mut self, f: &mut Frame<impl Backend>) {
pub fn draw_task(&mut self, f: &mut Frame<impl Backend>, layout: Rect, action: Action) {
let rects = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(3)].as_ref())
.split(f.size());
.constraints([Constraint::Min(0), Constraint::Length(2)].as_ref())
.split(layout);
// render task report and task details if required
if self.task_report_show_info {
let split_task_layout = Layout::default()
.direction(Direction::Vertical)
@ -465,14 +459,11 @@ impl TaskwarriorTui {
self.draw_task_report(f, split_task_layout[0]);
self.draw_task_details(f, split_task_layout[1]);
} else {
let full_table_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)].as_ref())
.split(rects[0]);
self.task_report_height = full_table_layout[0].height;
self.draw_task_report(f, full_table_layout[0]);
self.task_report_height = rects[0].height;
self.draw_task_report(f, rects[0]);
}
// calculate selected tasks
let selected = self.current_selection;
let task_ids = if self.tasks.is_empty() {
vec!["0".to_string()]
@ -490,12 +481,14 @@ impl TaskwarriorTui {
}
}
};
self.handle_task_mode_action(f, &rects, &task_ids);
// render task mode
self.handle_task_mode_action(f, &rects, &task_ids, action);
}
fn handle_task_mode_action(&mut self, f: &mut Frame<impl Backend>, rects: &[Rect], task_ids: &[String]) {
match self.mode {
Mode::Tasks(Action::Error) => {
fn handle_task_mode_action(&mut self, f: &mut Frame<impl Backend>, rects: &[Rect], task_ids: &[String], action: Action) {
match action {
Action::Error => {
Self::draw_command(
f,
rects[1],
@ -527,7 +520,7 @@ impl TaskwarriorTui {
.constraints([Constraint::Min(0)].as_ref())
.split(f.size());
}
Mode::Tasks(Action::Report) => {
Action::Report => {
// reset error when entering Action::Report
self.previous_mode = None;
self.error = None;
@ -545,7 +538,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Jump) => {
Action::Jump => {
let position = Self::get_position(&self.command);
Self::draw_command(
f,
@ -560,7 +553,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Filter) => {
Action::Filter => {
let position = Self::get_position(&self.filter);
if self.show_completion_pane {
self.draw_completion_pop_up(f, rects[1], position);
@ -581,7 +574,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Log) => {
Action::Log => {
if self.config.uda_auto_insert_double_quotes_on_log && self.command.is_empty() {
self.command.update(r#""""#, 1);
};
@ -605,7 +598,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Subprocess) => {
Action::Subprocess => {
let position = Self::get_position(&self.command);
Self::draw_command(
f,
@ -620,7 +613,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Modify) => {
Action::Modify => {
let position = Self::get_position(&self.modify);
if self.show_completion_pane {
self.draw_completion_pop_up(f, rects[1], position);
@ -646,7 +639,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Annotate) => {
Action::Annotate => {
if self.config.uda_auto_insert_double_quotes_on_annotate && self.command.is_empty() {
self.command.update(r#""""#, 1);
};
@ -675,7 +668,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::Add) => {
Action::Add => {
if self.config.uda_auto_insert_double_quotes_on_add && self.command.is_empty() {
self.command.update(r#""""#, 1);
};
@ -699,7 +692,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::HelpPopup) => {
Action::HelpPopup => {
Self::draw_command(
f,
rects[1],
@ -711,7 +704,7 @@ impl TaskwarriorTui {
);
self.draw_help_popup(f, 80, 90);
}
Mode::Tasks(Action::ContextMenu) => {
Action::ContextMenu => {
Self::draw_command(
f,
rects[1],
@ -723,7 +716,7 @@ impl TaskwarriorTui {
);
self.draw_context_menu(f, 80, 50);
}
Mode::Tasks(Action::DonePrompt) => {
Action::DonePrompt => {
let label = if task_ids.len() > 1 {
format!("Done Tasks {}?", task_ids.join(","))
} else {
@ -747,7 +740,7 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
Mode::Tasks(Action::DeletePrompt) => {
Action::DeletePrompt => {
let label = if task_ids.len() > 1 {
format!("Delete Tasks {}?", task_ids.join(","))
} else {
@ -771,9 +764,6 @@ impl TaskwarriorTui {
self.error.clone(),
);
}
_ => {
panic!("Reached unreachable code. Something went wrong");
}
}
}
@ -940,15 +930,11 @@ impl TaskwarriorTui {
f.render_widget(Clear, rect);
if cursor {
f.set_cursor(
std::cmp::min(rect.x + position as u16 + 1, rect.x + rect.width.saturating_sub(2)),
std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)),
rect.y + 1,
);
}
let border_style = if error.is_some() {
Style::default().fg(Color::Indexed(1)) // red color border for errors
} else {
Style::default()
};
let fg_color = if error.is_some() { Color::Red } else { Color::Reset };
let title = if let Some(subtitle) = title.1 {
let w = (title.0.width() + subtitle.width()).try_into().unwrap();
@ -961,9 +947,8 @@ impl TaskwarriorTui {
let p = Paragraph::new(Text::from(text))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(border_style)
.borders(Borders::TOP)
.style(Style::default().fg(fg_color))
.title(title),
)
.scroll((0, ((position + 3) as u16).saturating_sub(rect.width)));
@ -996,12 +981,7 @@ impl TaskwarriorTui {
self.task_details_scroll,
);
let p = Paragraph::new(Text::from(&data[..]))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(format!("Task {}", task_id)),
)
.block(Block::default().borders(Borders::TOP))
.scroll((self.task_details_scroll, 0));
f.render_widget(p, rect);
}
@ -1139,36 +1119,14 @@ impl TaskwarriorTui {
fn draw_task_report(&mut self, f: &mut Frame<impl Backend>, rect: Rect) {
let (tasks, headers) = self.get_task_report();
let mut style = Style::default();
match self.mode {
Mode::Tasks(Action::Report) => style = style.add_modifier(Modifier::BOLD),
_ => style = style.add_modifier(Modifier::DIM),
}
let mut title = vec![
Span::styled("Task", style),
Span::from("|"),
Span::styled("Projects", Style::default().add_modifier(Modifier::DIM)),
Span::from("|"),
Span::styled("Calendar", Style::default().add_modifier(Modifier::DIM)),
];
if tasks.is_empty() {
if !self.current_context.is_empty() {
let context_style = Style::default();
context_style.add_modifier(Modifier::ITALIC);
title.insert(title.len(), Span::from(" ("));
title.insert(title.len(), Span::styled(&self.current_context, context_style));
title.insert(title.len(), Span::from(")"));
}
f.render_widget(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(Spans::from(title)),
rect,
);
f.render_widget(Block::default(), rect);
return;
}
@ -1215,21 +1173,9 @@ impl TaskwarriorTui {
.map(|i| Constraint::Length((*i).try_into().unwrap_or(maximum_column_width as u16)))
.collect();
if !self.current_context.is_empty() {
let context_style = Style::default();
context_style.add_modifier(Modifier::ITALIC);
title.insert(title.len(), Span::from(" ("));
title.insert(title.len(), Span::styled(&self.current_context, context_style));
title.insert(title.len(), Span::from(")"));
}
let t = Table::new(header, rows.into_iter())
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(Spans::from(title)),
)
.header_style(
self.config
.color
@ -1253,7 +1199,7 @@ impl TaskwarriorTui {
}
}
pub fn get_all_contexts(&self) -> (Vec<Vec<String>>, Vec<String>) {
fn get_all_contexts(&self) -> (Vec<Vec<String>>, Vec<String>) {
let contexts = self
.contexts
.rows
@ -1265,7 +1211,7 @@ impl TaskwarriorTui {
(contexts, headers)
}
pub fn get_task_report(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
fn get_task_report(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
self.task_report_table.generate_table(&self.tasks);
let (tasks, headers) = self.task_report_table.simplify_table();
(tasks, headers)
@ -1663,7 +1609,7 @@ impl TaskwarriorTui {
// .arg("rc.verbose:override=false");
if let Some(args) =
shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim(),).trim())
shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim(), ).trim())
{
for arg in args {
task.arg(arg);
@ -1758,7 +1704,7 @@ impl TaskwarriorTui {
let output = command.output();
match output {
Ok(_) => Ok(()),
Err(_) => Err(format!("Shell command `{}` exited with non-zero output", shell,)),
Err(_) => Err(format!("Shell command `{}` exited with non-zero output", shell, )),
}
}
None => Err(format!("Cannot run subprocess. Unable to shlex split `{}`", shell)),
@ -1874,10 +1820,10 @@ impl TaskwarriorTui {
))
}
}
Err(s) => Err(format!("`{}` failed: {}", shell, s,)),
Err(s) => Err(format!("`{}` failed: {}", shell, s, )),
}
} else {
Err(format!("`{}` failed: {}", shell, s,))
Err(format!("`{}` failed: {}", shell, s, ))
}
}
None => Err(format!(
@ -1925,7 +1871,7 @@ impl TaskwarriorTui {
if o.status.success() {
Ok(())
} else {
Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout),))
Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout), ))
}
}
Err(_) => Err(format!(
@ -1934,7 +1880,7 @@ impl TaskwarriorTui {
)),
}
}
None => Err(format!("Cannot shlex split `{}`", shell,)),
None => Err(format!("Cannot shlex split `{}`", shell, )),
};
if task_uuids.len() == 1 {
@ -1976,7 +1922,7 @@ impl TaskwarriorTui {
if o.status.success() {
Ok(())
} else {
Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout),))
Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout), ))
}
}
Err(_) => Err(format!(
@ -2029,7 +1975,7 @@ impl TaskwarriorTui {
Err(format!("Error: {}", String::from_utf8_lossy(&output.stderr)))
}
}
Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e,)),
Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e, )),
}
}
None => Err(format!(
@ -2086,7 +2032,7 @@ impl TaskwarriorTui {
let output = Command::new("task").arg(task_uuid.to_string()).arg(command).output();
if output.is_err() {
return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid,));
return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid, ));
}
}
@ -2215,7 +2161,7 @@ impl TaskwarriorTui {
let re = Regex::new(
r"(?P<task_uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})",
)
.unwrap();
.unwrap();
if let Some(caps) = re.captures(&data) {
if let Ok(uuid) = Uuid::parse_str(&caps["task_uuid"]) {
self.current_selection_uuid = Some(uuid);
@ -4280,7 +4226,7 @@ mod tests {
let position = TaskwarriorTui::get_position(&app.modify);
f.set_cursor(
std::cmp::min(
rects[1].x + position as u16 + 1,
rects[1].x + position as u16,
rects[1].x + rects[1].width.saturating_sub(2),
),
rects[1].y + 1,
@ -4337,7 +4283,7 @@ mod tests {
let position = TaskwarriorTui::get_position(&app.modify);
f.set_cursor(
std::cmp::min(
rects[1].x + position as u16 + 1,
rects[1].x + position as u16,
rects[1].x + rects[1].width.saturating_sub(2),
),
rects[1].y + 1,

View file

@ -36,7 +36,6 @@ pub struct ProjectsState {
pub table_state: TableState,
pub current_selection: usize,
pub marked: HashSet<Project>,
pub report_height: u16,
pub columns: Vec<String>,
pub rows: Vec<ProjectDetails>,
pub data: String,
@ -57,7 +56,6 @@ impl ProjectsState {
table_state: TableState::default(),
current_selection: 0,
marked: HashSet::default(),
report_height: 0,
columns: vec![
PROJECT_HEADER.to_string(),
REMAINING_TASK_HEADER.to_string(),