mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-23 20:16:41 +02:00
WIP
This commit is contained in:
parent
6f7d3e5f0b
commit
c01c9f6de1
5 changed files with 188 additions and 126 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1998,6 +1998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"serde",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ toml = "0.8.0"
|
|||
tracing = "0.1.37"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tui-input = "0.8.0"
|
||||
tui-input = { version = "0.8.0", features = ["serde"] }
|
||||
unicode-segmentation = "1.10.1"
|
||||
unicode-truncate = "0.2.0"
|
||||
unicode-width = "0.1.10"
|
||||
|
|
|
@ -35,7 +35,7 @@ impl App {
|
|||
pub fn new(tick_rate: f64, frame_rate: f64, report: &str) -> Result<Self> {
|
||||
let app = TaskReport::new().report(report.into());
|
||||
let mut config = Config::new()?;
|
||||
config.taskwarrior_config()?;
|
||||
config.taskwarrior.taskwarrior_config(report)?;
|
||||
let mode = Mode::TaskReport;
|
||||
Ok(Self {
|
||||
tick_rate,
|
||||
|
|
|
@ -8,7 +8,7 @@ use ratatui::{prelude::*, widgets::*};
|
|||
use serde_derive::{Deserialize, Serialize};
|
||||
use task_hookrs::{import::import, status::TaskStatus, task::Task, uda::UDAValue};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
use tui_input::{backend::crossterm::EventHandler, Input};
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use uuid::Uuid;
|
||||
|
@ -55,26 +55,49 @@ const VIRTUAL_TAGS: [&str; 34] = [
|
|||
"TEMPLATE",
|
||||
];
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Report,
|
||||
Filter,
|
||||
Add,
|
||||
Annotate,
|
||||
Subprocess,
|
||||
Log,
|
||||
Modify,
|
||||
HelpPopup,
|
||||
ContextMenu,
|
||||
Jump,
|
||||
DeletePrompt,
|
||||
UndoPrompt,
|
||||
DonePrompt,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TaskReport {
|
||||
pub config: Config,
|
||||
pub command_tx: Option<UnboundedSender<Action>>,
|
||||
pub last_export: Option<std::time::SystemTime>,
|
||||
pub report: String,
|
||||
pub filter: String,
|
||||
pub current_context_filter: String,
|
||||
pub tasks: Vec<Task>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
pub row_heights: Vec<u16>,
|
||||
pub state: TableState,
|
||||
pub columns: Vec<String>,
|
||||
pub labels: Vec<String>,
|
||||
pub date_time_vague_precise: bool,
|
||||
pub virtual_tags: Vec<String>,
|
||||
pub description_width: usize,
|
||||
pub command_tx: Option<UnboundedSender<Action>>,
|
||||
pub config: Config,
|
||||
pub current_context: String,
|
||||
pub current_context_filter: String,
|
||||
pub current_filter: String,
|
||||
pub current_selection: usize,
|
||||
pub current_selection_id: Option<u64>,
|
||||
pub current_selection_uuid: Option<Uuid>,
|
||||
pub date_time_vague_precise: bool,
|
||||
pub description_width: usize,
|
||||
pub input: Input,
|
||||
pub labels: Vec<String>,
|
||||
pub last_export: Option<std::time::SystemTime>,
|
||||
pub report: String,
|
||||
pub row_heights: Vec<u16>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
pub state: TableState,
|
||||
pub task_details: HashMap<Uuid, String>,
|
||||
pub tasks: Vec<Task>,
|
||||
pub virtual_tags: Vec<String>,
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
impl TaskReport {
|
||||
|
@ -432,6 +455,29 @@ impl TaskReport {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_context(&mut self) -> Result<()> {
|
||||
let output = std::process::Command::new("task").arg("_get").arg("rc.context").output()?;
|
||||
self.current_context = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
self.current_context = self.current_context.strip_suffix('\n').unwrap_or("").to_string();
|
||||
|
||||
// support new format for context
|
||||
let output = std::process::Command::new("task")
|
||||
.arg("_get")
|
||||
.arg(format!("rc.context.{}.read", self.current_context))
|
||||
.output()?;
|
||||
self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string();
|
||||
|
||||
// If new format is not used, check if old format is used
|
||||
if self.current_context_filter.is_empty() {
|
||||
let output =
|
||||
std::process::Command::new("task").arg("_get").arg(format!("rc.context.{}", self.current_context)).output()?;
|
||||
self.current_context_filter = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
self.current_context_filter = self.current_context_filter.strip_suffix('\n').unwrap_or("").to_string();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn task_export(&mut self) -> Result<()> {
|
||||
let mut task = std::process::Command::new("task");
|
||||
|
||||
|
@ -443,7 +489,9 @@ impl TaskReport {
|
|||
.arg("rc._forcecolor=off");
|
||||
// .arg("rc.verbose:override=false");
|
||||
|
||||
if let Some(args) = shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.trim()).trim()) {
|
||||
if let Some(args) =
|
||||
shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.current_filter.trim()).trim())
|
||||
{
|
||||
for arg in args {
|
||||
task.arg(arg);
|
||||
}
|
||||
|
|
229
src/config.rs
229
src/config.rs
|
@ -15,18 +15,133 @@ const CONFIG: &str = include_str!("../.config/config.json5");
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct TaskwarriorConfig {
|
||||
#[serde(default)]
|
||||
pub rule_precedence_color: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub uda_priority_values: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub weekstart: bool,
|
||||
#[serde(default)]
|
||||
pub due: usize,
|
||||
#[serde(default)]
|
||||
pub color: HashMap<String, Style>,
|
||||
#[serde(default)]
|
||||
pub data_location: String,
|
||||
pub filter: String,
|
||||
}
|
||||
|
||||
impl TaskwarriorConfig {
|
||||
pub fn taskwarrior_config(&mut self, report: &str) -> Result<()> {
|
||||
let output = std::process::Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg("rc._forcecolor=off")
|
||||
.arg("rc.defaultwidth=0")
|
||||
.arg("show")
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let output = std::process::Command::new("task").arg("diagnostics").output()?;
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
self.rule_precedence_color(&data);
|
||||
self.uda_priority_values(&data);
|
||||
self.weekstart(&data);
|
||||
self.due(&data);
|
||||
self.data_location(&data);
|
||||
self.color(&data);
|
||||
self.filter(&data, &report);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn color(&mut self, data: &str) {
|
||||
let mut color = HashMap::new();
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with("color.") {
|
||||
let mut i = line.split(' ');
|
||||
let attribute = i.next();
|
||||
let line = i.collect::<Vec<_>>().join(" ");
|
||||
let line = line.trim_start_matches(' ');
|
||||
let style = parse_style(line);
|
||||
if let Some(attr) = attribute {
|
||||
color.insert(attr.to_string(), style);
|
||||
};
|
||||
}
|
||||
}
|
||||
log::info!("{color:?}");
|
||||
self.color = color;
|
||||
}
|
||||
|
||||
fn filter(&mut self, data: &str, report: &str) {
|
||||
let filter = if report == "all" {
|
||||
"".into()
|
||||
} else if let Some(s) =
|
||||
// TODO: move this to configuration struct
|
||||
Self::try_get_config(format!("uda.taskwarrior-tui.task-report.{}.filter", report).as_str(), data)
|
||||
{
|
||||
s
|
||||
} else {
|
||||
Self::get_config(format!("report.{}.filter", report).as_str(), data).unwrap_or_default()
|
||||
};
|
||||
let filter = if filter.trim_start().trim_end().is_empty() { filter } else { format!("{} ", filter) };
|
||||
self.filter = filter;
|
||||
}
|
||||
|
||||
fn data_location(&mut self, data: &str) {
|
||||
self.data_location = Self::get_config("data.location", data).unwrap();
|
||||
}
|
||||
|
||||
fn rule_precedence_color(&mut self, data: &str) {
|
||||
let data = Self::get_config("rule.precedence.color", data).unwrap();
|
||||
self.rule_precedence_color = data.split(',').map(ToString::to_string).collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
fn weekstart(&mut self, data: &str) {
|
||||
let data = Self::try_get_config("weekstart", data).unwrap_or_default();
|
||||
self.weekstart = data.eq_ignore_ascii_case("Monday");
|
||||
}
|
||||
|
||||
fn due(&mut self, data: &str) {
|
||||
self.due = Self::try_get_config("due", data).unwrap_or_default().parse::<usize>().unwrap_or(7)
|
||||
}
|
||||
|
||||
fn uda_priority_values(&mut self, data: &str) {
|
||||
let data = Self::get_config("uda.priority.values", data).unwrap();
|
||||
self.uda_priority_values = data.split(',').map(ToString::to_string).collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
fn get_config(config: &str, data: &str) -> Result<String> {
|
||||
Self::try_get_config(config, data).ok_or(color_eyre::eyre::eyre!("Unable to parse `task show {config}`"))
|
||||
}
|
||||
|
||||
fn try_get_config(config: &str, data: &str) -> Option<String> {
|
||||
let mut config_lines = Vec::new();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if config_lines.is_empty() {
|
||||
if line.starts_with(config) {
|
||||
config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string());
|
||||
} else {
|
||||
let config = &config.replace('-', "_");
|
||||
if line.starts_with(config) {
|
||||
config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !line.starts_with(" ") {
|
||||
return Some(config_lines.join(" "));
|
||||
}
|
||||
|
||||
config_lines.push(line.trim_start().trim_end().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !config_lines.is_empty() {
|
||||
return Some(config_lines.join(" "));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
|
@ -131,108 +246,6 @@ impl Config {
|
|||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn taskwarrior_config(&mut self) -> Result<()> {
|
||||
let output = std::process::Command::new("task")
|
||||
.arg("rc.color=off")
|
||||
.arg("rc._forcecolor=off")
|
||||
.arg("rc.defaultwidth=0")
|
||||
.arg("show")
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let output = std::process::Command::new("task").arg("diagnostics").output()?;
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
self.rule_precedence_color(&data);
|
||||
self.uda_priority_values(&data);
|
||||
self.weekstart(&data);
|
||||
self.due(&data);
|
||||
self.data_location(&data);
|
||||
self.color(&data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn color(&mut self, data: &str) {
|
||||
let mut color = HashMap::new();
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with("color.") {
|
||||
let mut i = line.split(' ');
|
||||
let attribute = i.next();
|
||||
let line = i.collect::<Vec<_>>().join(" ");
|
||||
let line = line.trim_start_matches(' ');
|
||||
let style = parse_style(line);
|
||||
if let Some(attr) = attribute {
|
||||
color.insert(attr.to_string(), style);
|
||||
};
|
||||
}
|
||||
}
|
||||
log::info!("{color:?}");
|
||||
self.taskwarrior.color = color;
|
||||
}
|
||||
|
||||
fn data_location(&mut self, data: &str) {
|
||||
self.taskwarrior.data_location = get_config("data.location", data).unwrap();
|
||||
}
|
||||
|
||||
fn rule_precedence_color(&mut self, data: &str) {
|
||||
let data = get_config("rule.precedence.color", data).unwrap();
|
||||
self.taskwarrior.rule_precedence_color = data.split(',').map(ToString::to_string).collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
fn weekstart(&mut self, data: &str) {
|
||||
let data = try_get_config("weekstart", data).unwrap_or_default();
|
||||
self.taskwarrior.weekstart = data.eq_ignore_ascii_case("Monday");
|
||||
}
|
||||
|
||||
fn due(&mut self, data: &str) {
|
||||
self.taskwarrior.due = try_get_config("due", data).unwrap_or_default().parse::<usize>().unwrap_or(7)
|
||||
}
|
||||
|
||||
fn uda_priority_values(&mut self, data: &str) {
|
||||
let data = get_config("uda.priority.values", data).unwrap();
|
||||
self.taskwarrior.uda_priority_values = data.split(',').map(ToString::to_string).collect::<Vec<_>>();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config(config: &str, data: &str) -> Result<String> {
|
||||
try_get_config(config, data).ok_or(color_eyre::eyre::eyre!("Unable to parse `task show {config}`"))
|
||||
}
|
||||
|
||||
fn try_get_config(config: &str, data: &str) -> Option<String> {
|
||||
let mut config_lines = Vec::new();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if config_lines.is_empty() {
|
||||
if line.starts_with(config) {
|
||||
config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string());
|
||||
} else {
|
||||
let config = &config.replace('-', "_");
|
||||
if line.starts_with(config) {
|
||||
config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !line.starts_with(" ") {
|
||||
return Some(config_lines.join(" "));
|
||||
}
|
||||
|
||||
config_lines.push(line.trim_start().trim_end().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !config_lines.is_empty() {
|
||||
return Some(config_lines.join(" "));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue