mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-25 08:47:18 +02:00
Refactor using async-std
This commit is contained in:
parent
6df2aba770
commit
979e886a29
7 changed files with 919 additions and 162 deletions
150
src/app.rs
150
src/app.rs
|
@ -30,7 +30,16 @@ use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, TimeZone};
|
|||
|
||||
use anyhow::Result;
|
||||
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
use async_std::prelude::*;
|
||||
use async_std::stream::StreamExt;
|
||||
use async_std::task;
|
||||
use futures::future::join_all;
|
||||
use futures::join;
|
||||
use futures::stream::FuturesOrdered;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use std::time::Duration;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
|
@ -140,6 +149,7 @@ pub enum AppMode {
|
|||
|
||||
pub struct TaskwarriorTuiApp {
|
||||
pub should_quit: bool,
|
||||
pub dirty: bool,
|
||||
pub task_table_state: TableState,
|
||||
pub context_table_state: TableState,
|
||||
pub current_context_filter: String,
|
||||
|
@ -173,8 +183,10 @@ impl TaskwarriorTuiApp {
|
|||
let c = Config::default()?;
|
||||
let mut kc = KeyConfig::default();
|
||||
kc.update()?;
|
||||
let (w, h) = crossterm::terminal::size()?;
|
||||
let mut app = Self {
|
||||
should_quit: false,
|
||||
dirty: true,
|
||||
task_table_state: TableState::default(),
|
||||
context_table_state: TableState::default(),
|
||||
tasks: vec![],
|
||||
|
@ -198,8 +210,8 @@ impl TaskwarriorTuiApp {
|
|||
contexts: vec![],
|
||||
last_export: None,
|
||||
keyconfig: kc,
|
||||
terminal_width: 0,
|
||||
terminal_height: 0,
|
||||
terminal_width: w,
|
||||
terminal_height: h,
|
||||
};
|
||||
for c in app.config.filter.chars() {
|
||||
app.filter.insert(c, 1);
|
||||
|
@ -223,6 +235,14 @@ impl TaskwarriorTuiApp {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render<B>(&mut self, terminal: &mut Terminal<B>) -> Result<()>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal.draw(|f| self.draw(f))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, f: &mut Frame<impl Backend>) {
|
||||
let rect = f.size();
|
||||
self.terminal_width = rect.width;
|
||||
|
@ -280,7 +300,6 @@ impl TaskwarriorTuiApp {
|
|||
let mut tasks_with_styles = vec![];
|
||||
|
||||
let tasks_is_empty = self.tasks.is_empty();
|
||||
let tasks_len = self.tasks.len();
|
||||
|
||||
if !tasks_is_empty {
|
||||
let tasks = &self.tasks;
|
||||
|
@ -305,8 +324,7 @@ impl TaskwarriorTuiApp {
|
|||
|
||||
pub fn draw_task(&mut self, f: &mut Frame<impl Backend>) {
|
||||
let tasks_is_empty = self.tasks.is_empty();
|
||||
let tasks_len = self.tasks.len();
|
||||
while !tasks_is_empty && self.current_selection >= tasks_len {
|
||||
while !tasks_is_empty && self.current_selection >= self.tasks.len() {
|
||||
self.task_report_previous();
|
||||
}
|
||||
let rects = Layout::default()
|
||||
|
@ -333,7 +351,7 @@ impl TaskwarriorTuiApp {
|
|||
self.draw_task_details(f, split_task_layout[1]);
|
||||
}
|
||||
let selected = self.current_selection;
|
||||
let task_ids = if tasks_len == 0 {
|
||||
let task_ids = if self.tasks.is_empty() {
|
||||
vec!["0".to_string()]
|
||||
} else {
|
||||
match self.task_table_state.mode() {
|
||||
|
@ -593,24 +611,14 @@ impl TaskwarriorTuiApp {
|
|||
let task_id = self.tasks[selected].id().unwrap_or_default();
|
||||
let task_uuid = *self.tasks[selected].uuid();
|
||||
|
||||
if !self.task_details.contains_key(&task_uuid) {
|
||||
let output = Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg(format!("rc.defaultwidth={}", self.terminal_width - 2))
|
||||
.arg(format!("{}", task_uuid))
|
||||
.output();
|
||||
let data = if let Ok(output) = output {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let entry = self.task_details.entry(task_uuid).or_insert_with(|| "".to_string());
|
||||
*entry = data;
|
||||
}
|
||||
let data = self.task_details[&task_uuid].clone();
|
||||
|
||||
let data = match self.task_details.get(&task_uuid) {
|
||||
Some(s) => s.clone(),
|
||||
None => "Loading task details ...".to_string(),
|
||||
};
|
||||
self.task_details_scroll = std::cmp::min(
|
||||
(data.lines().count() as u16).saturating_sub(rect.height),
|
||||
(data.lines().count() as u16)
|
||||
.saturating_sub(rect.height)
|
||||
.saturating_add(2),
|
||||
self.task_details_scroll,
|
||||
);
|
||||
let p = Paragraph::new(Text::from(&data[..]))
|
||||
|
@ -624,11 +632,11 @@ impl TaskwarriorTuiApp {
|
|||
f.render_widget(p, rect);
|
||||
}
|
||||
|
||||
fn task_details_scroll_down(&mut self) {
|
||||
fn task_details_scroll_up(&mut self) {
|
||||
self.task_details_scroll = self.task_details_scroll.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn task_details_scroll_up(&mut self) {
|
||||
fn task_details_scroll_down(&mut self) {
|
||||
self.task_details_scroll = self.task_details_scroll.saturating_add(1);
|
||||
}
|
||||
|
||||
|
@ -856,13 +864,83 @@ impl TaskwarriorTuiApp {
|
|||
}
|
||||
|
||||
pub fn update(&mut self, force: bool) -> Result<()> {
|
||||
if force || self.tasks_changed_since(self.last_export)? {
|
||||
if force || self.dirty || self.tasks_changed_since(self.last_export)? {
|
||||
self.last_export = Some(std::time::SystemTime::now());
|
||||
self.task_report_table.export_headers()?;
|
||||
let _ = self.export_tasks();
|
||||
self.export_contexts()?;
|
||||
self.update_tags();
|
||||
self.task_details.clear();
|
||||
self.dirty = false;
|
||||
}
|
||||
if self.task_report_show_info {
|
||||
task::block_on(self.update_task_details())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_task_details(&mut self) -> Result<()> {
|
||||
if self.tasks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// remove task_details of tasks not in task report
|
||||
let mut to_delete = vec![];
|
||||
for k in self.task_details.keys() {
|
||||
if !self.tasks.iter().map(|t| t.uuid()).any(|x| x == k) {
|
||||
to_delete.push(*k);
|
||||
}
|
||||
}
|
||||
for k in to_delete {
|
||||
self.task_details.remove(&k);
|
||||
}
|
||||
|
||||
let selected = self.current_selection;
|
||||
if selected >= self.tasks.len() {
|
||||
return Ok(());
|
||||
}
|
||||
let current_task_uuid = *self.tasks[selected].uuid();
|
||||
|
||||
let mut l = vec![selected];
|
||||
|
||||
for s in 1..=self.config.uda_task_detail_prefetch {
|
||||
l.insert(0, std::cmp::min(selected.saturating_sub(s), self.tasks.len() - 1));
|
||||
l.push(std::cmp::min(selected + s, self.tasks.len() - 1))
|
||||
}
|
||||
|
||||
l.dedup();
|
||||
|
||||
let mut output_futs = FuturesOrdered::new();
|
||||
for s in l.iter() {
|
||||
if self.tasks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if s >= &self.tasks.len() {
|
||||
break;
|
||||
}
|
||||
let task_uuid = *self.tasks[*s].uuid();
|
||||
if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid {
|
||||
let output_fut = async_std::process::Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg(format!("rc.defaultwidth={}", self.terminal_width - 2))
|
||||
.arg(format!("{}", task_uuid))
|
||||
.output();
|
||||
output_futs.push(output_fut);
|
||||
}
|
||||
}
|
||||
|
||||
for s in l.iter() {
|
||||
if s >= &self.tasks.len() {
|
||||
break;
|
||||
}
|
||||
let task_id = self.tasks[*s].id().unwrap_or_default();
|
||||
let task_uuid = *self.tasks[*s].uuid();
|
||||
if !self.task_details.contains_key(&task_uuid) || task_uuid == current_task_uuid {
|
||||
if let Some(Ok(output)) = output_futs.next().await {
|
||||
let data = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
self.task_details.insert(task_uuid, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1708,9 +1786,9 @@ impl TaskwarriorTuiApp {
|
|||
} else if input == Key::PageUp || input == self.keyconfig.page_up {
|
||||
self.task_report_previous_page();
|
||||
} else if input == Key::Ctrl('e') {
|
||||
self.task_details_scroll_up();
|
||||
} else if input == Key::Ctrl('y') {
|
||||
self.task_details_scroll_down();
|
||||
} else if input == Key::Ctrl('y') {
|
||||
self.task_details_scroll_up();
|
||||
} else if input == self.keyconfig.done {
|
||||
match self.task_done() {
|
||||
Ok(_) => self.update(true)?,
|
||||
|
@ -1983,7 +2061,7 @@ impl TaskwarriorTuiApp {
|
|||
}
|
||||
_ => {
|
||||
handle_movement(&mut self.filter, input);
|
||||
// TODO: call self.update(true) here for instant filter updates
|
||||
self.dirty = true;
|
||||
}
|
||||
},
|
||||
AppMode::TaskError => self.mode = AppMode::TaskReport,
|
||||
|
@ -2123,6 +2201,9 @@ mod tests {
|
|||
|
||||
test_draw_empty_task_report();
|
||||
|
||||
test_draw_calendar();
|
||||
test_draw_help_popup();
|
||||
|
||||
setup();
|
||||
|
||||
let app = TaskwarriorTuiApp::new().unwrap();
|
||||
|
@ -2141,6 +2222,7 @@ mod tests {
|
|||
test_task_tomorrow();
|
||||
test_task_earlier_today();
|
||||
test_task_later_today();
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
|
@ -2766,9 +2848,9 @@ mod tests {
|
|||
"╰────────────────────────────────────────────────╯",
|
||||
"╭Task 27─────────────────────────────────────────╮",
|
||||
"│ │",
|
||||
"│Name Value │",
|
||||
"│----------- ------------------------------------│",
|
||||
"│ID 27 │",
|
||||
"│Name Value │",
|
||||
"│------------- ----------------------------------│",
|
||||
"│ID 27 │",
|
||||
"╰────────────────────────────────────────────────╯",
|
||||
"╭Filter Tasks────────────────────────────────────╮",
|
||||
"│status:pending -private │",
|
||||
|
@ -2824,7 +2906,6 @@ mod tests {
|
|||
test_case(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_calendar() {
|
||||
let test_case = |expected: &Buffer| {
|
||||
let mut app = TaskwarriorTuiApp::new().unwrap();
|
||||
|
@ -2909,7 +2990,6 @@ mod tests {
|
|||
test_case(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_help_popup() {
|
||||
let test_case = |expected: &Buffer| {
|
||||
let mut app = TaskwarriorTuiApp::new().unwrap();
|
||||
|
|
198
src/config.rs
198
src/config.rs
|
@ -1,4 +1,6 @@
|
|||
use anyhow::{Context, Result};
|
||||
use async_std::task;
|
||||
use futures::join;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
@ -43,6 +45,7 @@ pub struct Config {
|
|||
pub print_empty_columns: bool,
|
||||
pub due: usize,
|
||||
pub rule_precedence_color: Vec<String>,
|
||||
pub uda_task_detail_prefetch: usize,
|
||||
pub uda_task_report_show_info: bool,
|
||||
pub uda_task_report_looping: bool,
|
||||
pub uda_selection_indicator: String,
|
||||
|
@ -61,28 +64,102 @@ pub struct Config {
|
|||
impl Config {
|
||||
pub fn default() -> Result<Self> {
|
||||
let bool_collection = Self::get_bool_collection();
|
||||
|
||||
let enabled = true;
|
||||
let obfuscate = bool_collection.get("obfuscate").cloned().unwrap_or(false);
|
||||
let print_empty_columns = bool_collection.get("print_empty_columns").cloned().unwrap_or(false);
|
||||
|
||||
let color = Self::get_color_collection();
|
||||
let filter = Self::get_filter();
|
||||
let data_location = Self::get_data_location();
|
||||
let due = Self::get_due();
|
||||
let rule_precedence_color = Self::get_rule_precedence_color();
|
||||
let uda_task_detail_prefetch = Self::get_uda_task_detail_prefetch();
|
||||
let uda_task_report_show_info = Self::get_uda_task_report_show_info();
|
||||
let uda_task_report_looping = Self::get_uda_task_report_looping();
|
||||
let uda_selection_indicator = Self::get_uda_selection_indicator();
|
||||
let uda_mark_indicator = Self::get_uda_mark_indicator();
|
||||
let uda_unmark_indicator = Self::get_uda_unmark_indicator();
|
||||
let uda_selection_bold = Self::get_uda_selection_bold();
|
||||
let uda_selection_italic = Self::get_uda_selection_italic();
|
||||
let uda_selection_dim = Self::get_uda_selection_dim();
|
||||
let uda_selection_blink = Self::get_uda_selection_blink();
|
||||
let uda_calendar_months_per_row = Self::get_uda_months_per_row();
|
||||
let uda_style_calendar_title = Self::get_uda_style("calendar.title");
|
||||
let uda_style_context_active = Self::get_uda_style("context.active");
|
||||
let uda_shortcuts = Self::get_uda_shortcuts();
|
||||
|
||||
let (
|
||||
color,
|
||||
filter,
|
||||
data_location,
|
||||
due,
|
||||
rule_precedence_color,
|
||||
uda_task_detail_prefetch,
|
||||
uda_task_report_show_info,
|
||||
uda_task_report_looping,
|
||||
uda_selection_indicator,
|
||||
uda_mark_indicator,
|
||||
uda_unmark_indicator,
|
||||
uda_selection_bold,
|
||||
uda_selection_italic,
|
||||
uda_selection_dim,
|
||||
uda_selection_blink,
|
||||
uda_calendar_months_per_row,
|
||||
uda_style_calendar_title,
|
||||
uda_style_context_active,
|
||||
uda_shortcuts,
|
||||
) = task::block_on(async {
|
||||
join!(
|
||||
color,
|
||||
filter,
|
||||
data_location,
|
||||
due,
|
||||
rule_precedence_color,
|
||||
uda_task_detail_prefetch,
|
||||
uda_task_report_show_info,
|
||||
uda_task_report_looping,
|
||||
uda_selection_indicator,
|
||||
uda_mark_indicator,
|
||||
uda_unmark_indicator,
|
||||
uda_selection_bold,
|
||||
uda_selection_italic,
|
||||
uda_selection_dim,
|
||||
uda_selection_blink,
|
||||
uda_calendar_months_per_row,
|
||||
uda_style_calendar_title,
|
||||
uda_style_context_active,
|
||||
uda_shortcuts,
|
||||
)
|
||||
});
|
||||
|
||||
let color = color?;
|
||||
let uda_style_calendar_title = uda_style_calendar_title.unwrap_or_default();
|
||||
let uda_style_context_active = uda_style_context_active.unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
enabled: true,
|
||||
obfuscate: bool_collection.get("obfuscate").cloned().unwrap_or(false),
|
||||
print_empty_columns: bool_collection.get("print_empty_columns").cloned().unwrap_or(false),
|
||||
color: Self::get_color_collection()?,
|
||||
filter: Self::get_filter(),
|
||||
data_location: Self::get_data_location(),
|
||||
due: Self::get_due(),
|
||||
rule_precedence_color: Self::get_rule_precedence_color(),
|
||||
uda_task_report_show_info: Self::get_uda_task_report_show_info(),
|
||||
uda_task_report_looping: Self::get_uda_task_report_looping(),
|
||||
uda_selection_indicator: Self::get_uda_selection_indicator(),
|
||||
uda_mark_indicator: Self::get_uda_mark_indicator(),
|
||||
uda_unmark_indicator: Self::get_uda_unmark_indicator(),
|
||||
uda_selection_bold: Self::get_uda_selection_bold(),
|
||||
uda_selection_italic: Self::get_uda_selection_italic(),
|
||||
uda_selection_dim: Self::get_uda_selection_dim(),
|
||||
uda_selection_blink: Self::get_uda_selection_blink(),
|
||||
uda_calendar_months_per_row: Self::get_uda_months_per_row(),
|
||||
uda_style_calendar_title: Self::get_uda_style("calendar.title").unwrap_or_default(),
|
||||
uda_style_context_active: Self::get_uda_style("context.active").unwrap_or_default(),
|
||||
uda_shortcuts: Self::get_uda_shortcuts(),
|
||||
enabled,
|
||||
color,
|
||||
filter,
|
||||
data_location,
|
||||
obfuscate,
|
||||
print_empty_columns,
|
||||
due,
|
||||
rule_precedence_color,
|
||||
uda_task_detail_prefetch,
|
||||
uda_task_report_show_info,
|
||||
uda_task_report_looping,
|
||||
uda_selection_indicator,
|
||||
uda_mark_indicator,
|
||||
uda_unmark_indicator,
|
||||
uda_selection_bold,
|
||||
uda_selection_italic,
|
||||
uda_selection_dim,
|
||||
uda_selection_blink,
|
||||
uda_calendar_months_per_row,
|
||||
uda_style_context_active,
|
||||
uda_style_calendar_title,
|
||||
uda_shortcuts,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -90,25 +167,29 @@ impl Config {
|
|||
HashMap::new()
|
||||
}
|
||||
|
||||
fn get_uda_shortcuts() -> Vec<String> {
|
||||
async fn get_uda_shortcuts() -> Vec<String> {
|
||||
let mut v = vec![];
|
||||
for s in 0..=9 {
|
||||
let c = format!("uda.taskwarrior-tui.shortcuts.{}", s);
|
||||
let s = Self::get_config(&c).unwrap_or_default();
|
||||
let s = Self::get_config(&c).await.unwrap_or_default();
|
||||
v.push(s);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn get_uda_style(config: &str) -> Option<Style> {
|
||||
async fn get_uda_style(config: &str) -> Option<Style> {
|
||||
let c = format!("uda.taskwarrior-tui.style.{}", config);
|
||||
let s = Self::get_config(&c)?;
|
||||
let s = Self::get_config(&c).await?;
|
||||
Some(Self::get_tcolor(&s))
|
||||
}
|
||||
|
||||
fn get_color_collection() -> Result<HashMap<String, Style>> {
|
||||
async fn get_color_collection() -> Result<HashMap<String, Style>> {
|
||||
let mut color_collection = HashMap::new();
|
||||
let output = Command::new("task").arg("rc.color=off").arg("show").output()?;
|
||||
let output = async_std::process::Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg("show")
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
for line in data.split('\n') {
|
||||
|
@ -274,12 +355,13 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_config(config: &str) -> Option<String> {
|
||||
let output = Command::new("task")
|
||||
async fn get_config(config: &str) -> Option<String> {
|
||||
let output = async_std::process::Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg("show")
|
||||
.arg(config)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("Unable to run `task show {}`.", config))
|
||||
.unwrap();
|
||||
|
||||
|
@ -311,100 +393,119 @@ impl Config {
|
|||
None
|
||||
}
|
||||
|
||||
fn get_due() -> usize {
|
||||
async fn get_due() -> usize {
|
||||
Self::get_config("due")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(7)
|
||||
}
|
||||
|
||||
fn get_rule_precedence_color() -> Vec<String> {
|
||||
async fn get_rule_precedence_color() -> Vec<String> {
|
||||
let data = Self::get_config("rule.precedence.color")
|
||||
.await
|
||||
.context("Unable to parse `task show rule.precedence.color`.")
|
||||
.unwrap();
|
||||
data.split(',').map(|s| s.to_string()).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn get_filter() -> String {
|
||||
async fn get_filter() -> String {
|
||||
Self::get_config("report.next.filter")
|
||||
.await
|
||||
.context("Unable to parse `task show report.next.filter`.")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_data_location() -> String {
|
||||
async fn get_data_location() -> String {
|
||||
Self::get_config("data.location")
|
||||
.await
|
||||
.context("Unable to parse `task show data.location`.")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_uda_task_report_show_info() -> bool {
|
||||
async fn get_uda_task_detail_prefetch() -> usize {
|
||||
Self::get_config("uda.taskwarrior-tui.task-report.task-detail-prefetch")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(10)
|
||||
}
|
||||
|
||||
async fn get_uda_task_report_show_info() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.task-report.show-info")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn get_uda_task_report_looping() -> bool {
|
||||
async fn get_uda_task_report_looping() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.task-report.looping")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn get_uda_selection_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.selection.indicator");
|
||||
async fn get_uda_selection_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.selection.indicator").await;
|
||||
match indicator {
|
||||
None => "• ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_uda_mark_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.mark.indicator");
|
||||
async fn get_uda_mark_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.mark.indicator").await;
|
||||
match indicator {
|
||||
None => "✔ ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_uda_unmark_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.unmark.indicator");
|
||||
async fn get_uda_unmark_indicator() -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.unmark.indicator").await;
|
||||
match indicator {
|
||||
None => " ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_uda_selection_bold() -> bool {
|
||||
async fn get_uda_selection_bold() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.selection.bold")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn get_uda_selection_italic() -> bool {
|
||||
async fn get_uda_selection_italic() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.selection.italic")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_uda_selection_dim() -> bool {
|
||||
async fn get_uda_selection_dim() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.selection.dim")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_uda_selection_blink() -> bool {
|
||||
async fn get_uda_selection_blink() -> bool {
|
||||
Self::get_config("uda.taskwarrior-tui.selection.blink")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get_bool()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_uda_months_per_row() -> usize {
|
||||
async fn get_uda_months_per_row() -> usize {
|
||||
Self::get_config("uda.taskwarrior-tui.calendar.months-per-row")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(4)
|
||||
|
@ -416,7 +517,10 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_uda_configuration() {
|
||||
assert_eq!(None, Config::get_config("uda.taskwarrior-tui.unmark.indicator"));
|
||||
assert_eq!(
|
||||
None,
|
||||
task::block_on(Config::get_config("uda.taskwarrior-tui.unmark.indicator"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
50
src/main.rs
50
src/main.rs
|
@ -21,6 +21,11 @@ use std::io::Write;
|
|||
use std::panic;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use async_std::task;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
use crate::util::Key;
|
||||
use app::{AppMode, TaskwarriorTuiApp};
|
||||
|
||||
|
@ -44,28 +49,12 @@ fn main() -> Result<()> {
|
|||
.get_matches();
|
||||
|
||||
let config = matches.value_of("config").unwrap_or("~/.taskrc");
|
||||
let r = tui_main(config);
|
||||
match r {
|
||||
Ok(_) => std::process::exit(0),
|
||||
Err(error) => {
|
||||
if error.to_string().to_lowercase().contains("no such file or directory") {
|
||||
eprintln!(
|
||||
"[taskwarrior-tui error]: Unable to find executable `task`: {}. Check that taskwarrior is installed correctly and try again.", error
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"[taskwarrior-tui error]: {}. Please report as a github issue on https://github.com/kdheepak/taskwarrior-tui",
|
||||
error
|
||||
);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
task::block_on(tui_main(config))
|
||||
}
|
||||
|
||||
fn tui_main(_config: &str) -> Result<()> {
|
||||
async fn tui_main(_config: &str) -> Result<()> {
|
||||
// Terminal initialization
|
||||
let mut terminal = setup_terminal();
|
||||
let terminal = setup_terminal();
|
||||
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
destruct_terminal();
|
||||
|
@ -79,21 +68,30 @@ fn tui_main(_config: &str) -> Result<()> {
|
|||
|
||||
let maybeapp = TaskwarriorTuiApp::new();
|
||||
match maybeapp {
|
||||
Ok(mut app) => {
|
||||
Ok(app) => {
|
||||
let app = Arc::new(Mutex::new(app));
|
||||
let terminal = Arc::new(Mutex::new(terminal));
|
||||
loop {
|
||||
terminal.draw(|mut frame| app.draw(&mut frame)).unwrap();
|
||||
|
||||
let handle = {
|
||||
let app = app.clone();
|
||||
let terminal = terminal.clone();
|
||||
task::spawn_local(async move {
|
||||
let mut t = terminal.lock().await;
|
||||
app.lock().await.render(&mut t).unwrap();
|
||||
})
|
||||
};
|
||||
// Handle input
|
||||
match events.next()? {
|
||||
match events.next().await? {
|
||||
Event::Input(input) => {
|
||||
let r = app.handle_input(input, &mut terminal, &events);
|
||||
let mut t = terminal.lock().await;
|
||||
let r = app.lock().await.handle_input(input, &mut t, &events);
|
||||
if r.is_err() {
|
||||
destruct_terminal();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
let r = app.update(false);
|
||||
let r = app.lock().await.update(false);
|
||||
if r.is_err() {
|
||||
destruct_terminal();
|
||||
return r;
|
||||
|
@ -101,7 +99,7 @@ fn tui_main(_config: &str) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
if app.should_quit {
|
||||
if app.lock().await.should_quit {
|
||||
destruct_terminal();
|
||||
break;
|
||||
}
|
||||
|
|
91
src/util.rs
91
src/util.rs
|
@ -1,15 +1,20 @@
|
|||
use crossterm::{
|
||||
cursor,
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, EventStream},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use async_std::channel::unbounded;
|
||||
use async_std::sync::Arc;
|
||||
use async_std::task;
|
||||
use futures::prelude::*;
|
||||
use futures::{future::FutureExt, select, StreamExt};
|
||||
use futures_timer::Delay;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{sync::mpsc, thread, time::Duration, time::Instant};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -62,38 +67,36 @@ pub fn destruct_terminal() {
|
|||
}
|
||||
|
||||
pub struct Events {
|
||||
pub rx: mpsc::Receiver<Event<Key>>,
|
||||
pub tx: mpsc::Sender<Event<Key>>,
|
||||
pub rx: async_std::channel::Receiver<Event<Key>>,
|
||||
pub pause_stdin: Arc<AtomicBool>,
|
||||
pub handle: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Events {
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub fn with_config(config: EventConfig) -> Events {
|
||||
use crossterm::event::{KeyCode::*, KeyModifiers};
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let pause_stdin = Arc::new(AtomicBool::new(false));
|
||||
let tick_rate = config.tick_rate;
|
||||
let handle = Some({
|
||||
let tx = tx.clone();
|
||||
let pause_stdin = pause_stdin.clone();
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
if pause_stdin.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
thread::park();
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
let (tx, rx) = unbounded::<Event<Key>>();
|
||||
let ps = pause_stdin.clone();
|
||||
task::spawn_local(async move {
|
||||
let mut reader = EventStream::new();
|
||||
|
||||
let timeout = Duration::from_millis(10)
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(5));
|
||||
loop {
|
||||
if ps.load(Ordering::SeqCst) {
|
||||
task::sleep(Duration::from_millis(250)).await;
|
||||
task::yield_now().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event::poll(timeout).unwrap() {
|
||||
if let event::Event::Key(key) = event::read().unwrap() {
|
||||
let mut delay = Delay::new(Duration::from_millis(250)).fuse();
|
||||
let mut event = reader.next().fuse();
|
||||
|
||||
select! {
|
||||
_ = delay => {
|
||||
tx.send(Event::Tick).await.ok();
|
||||
},
|
||||
maybe_event = event => {
|
||||
if let Some(Ok(event::Event::Key(key))) = maybe_event {
|
||||
let key = match key.code {
|
||||
Backspace => Key::Backspace,
|
||||
Enter => Key::Char('\n'),
|
||||
|
@ -119,49 +122,39 @@ impl Events {
|
|||
_ => Key::Null,
|
||||
},
|
||||
};
|
||||
tx.send(Event::Input(key)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate && tx.send(Event::Tick).is_ok() {
|
||||
last_tick = Instant::now();
|
||||
tx.send(Event::Input(key)).await.unwrap();
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
Events {
|
||||
rx,
|
||||
tx,
|
||||
pause_stdin,
|
||||
handle,
|
||||
}
|
||||
Events { rx, pause_stdin }
|
||||
}
|
||||
|
||||
/// Attempts to read an event.
|
||||
/// This function will block the current thread.
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
pub async fn next(&self) -> Result<Event<Key>, async_std::channel::RecvError> {
|
||||
self.rx.recv().await
|
||||
}
|
||||
|
||||
pub fn pause_event_loop(&self) {
|
||||
pub async fn pause_event_loop(&self) {
|
||||
self.pause_stdin.store(true, Ordering::SeqCst);
|
||||
thread::yield_now();
|
||||
task::yield_now().await;
|
||||
while !self.pause_stdin.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
task::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resume_event_loop(&self) {
|
||||
pub async fn resume_event_loop(&self) {
|
||||
self.pause_stdin.store(false, Ordering::SeqCst);
|
||||
thread::yield_now();
|
||||
task::yield_now().await;
|
||||
while self.pause_stdin.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
task::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
self.handle.as_ref().unwrap().thread().unpark();
|
||||
}
|
||||
|
||||
pub fn pause_key_capture(&self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
self.pause_event_loop();
|
||||
task::block_on(self.pause_event_loop());
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
|
||||
terminal.show_cursor().unwrap();
|
||||
|
@ -170,7 +163,7 @@ impl Events {
|
|||
pub fn resume_key_capture(&self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap();
|
||||
enable_raw_mode().unwrap();
|
||||
self.resume_event_loop();
|
||||
task::block_on(self.resume_event_loop());
|
||||
terminal.resize(terminal.size().unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue