feat: Use color_eyre instead of anyhow

This commit is contained in:
Dheepak Krishnamurthy 2023-09-03 06:54:37 -04:00
parent fe171ea35a
commit 2bb15f7cf2
18 changed files with 1180 additions and 412 deletions

976
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,38 +7,45 @@ repository = "https://github.com/kdheepak/taskwarrior-tui/"
homepage = "https://kdheepak.com/taskwarrior-tui" homepage = "https://kdheepak.com/taskwarrior-tui"
readme = "README.md" readme = "README.md"
authors = ["Dheepak Krishnamurthy <me@kdheepak.com>"] authors = ["Dheepak Krishnamurthy <me@kdheepak.com>"]
edition = "2018" edition = "2021"
keywords = ["taskwarrior", "tui"] keywords = ["taskwarrior", "tui"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
[dependencies] [dependencies]
anyhow = "1.0.75"
better-panic = "0.3.0"
cassowary = "0.3.0" cassowary = "0.3.0"
chrono = "0.4.26" chrono = "0.4.28"
clap = { version = "4.4.1", features = ["derive"] } clap = { version = "4.4.2", features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] }
crossterm = { version = "0.27.0", features = [ color-eyre = "0.6.2"
"event-stream", config = "0.13.3"
] } crossterm = { version = "0.27.0", features = ["event-stream", "serde"] }
dirs = "5.0.1" directories = "5.0.1"
futures = "0.3.28" futures = "0.3.28"
human-panic = "1.2.0"
itertools = "0.11.0" itertools = "0.11.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2.147"
log = "0.4.20" log = "0.4.20"
log4rs = "1.2.0"
path-clean = "1.0.1" path-clean = "1.0.1"
pretty_assertions = "1.4.0"
rand = "0.8.5" rand = "0.8.5"
regex = "1.9.4" ratatui = "0.23.0"
regex = "1.9.5"
rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] } rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] }
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_derive = "1.0.188"
serde_json = "1.0.105" serde_json = "1.0.105"
shellexpand = "3.1.0" shellexpand = "3.1.0"
shlex = "1.1.0" shlex = "1.1.0"
signal-hook = "0.3.17"
strip-ansi-escapes = "0.2.0"
task-hookrs = "0.9.0" task-hookrs = "0.9.0"
tokio = { version = "1.32.0", features = ["full"] } tokio = { version = "1.32.0", features = ["full"] }
tokio-stream = "0.1.14" tokio-util = "0.7.8"
ratatui = "0.23.0" tracing = "0.1.37"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tui-logger = { version = "0.9.5", features = ["ratatui-support", "tracing-support"] }
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
unicode-truncate = "0.2.0" unicode-truncate = "0.2.0"
unicode-width = "0.1.10" unicode-width = "0.1.10"
@ -55,11 +62,10 @@ buildflags = ["--release"]
taskwarrior-tui = { path = "/usr/bin/taskwarrior-tui" } taskwarrior-tui = { path = "/usr/bin/taskwarrior-tui" }
[profile.release] [profile.release]
debug = 1
incremental = true incremental = true
lto = "off" lto = true
[build-dependencies] [build-dependencies]
clap = { version = "4.4.1", features = ["derive"] } clap = { version = "4.4.2", features = ["derive"] }
clap_complete = "4.4.0" clap_complete = "4.4.0"
shlex = "1.1.0" shlex = "1.1.0"

View file

@ -18,7 +18,61 @@ fn run_pandoc() -> Result<Output, std::io::Error> {
cmd.output() cmd.output()
} }
fn get_commit_hash() {
let git_output = std::process::Command::new("git").args(["rev-parse", "--git-dir"]).output().ok();
let git_dir = git_output.as_ref().and_then(|output| {
std::str::from_utf8(&output.stdout)
.ok()
.and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n")))
});
// Tell cargo to rebuild if the head or any relevant refs change.
if let Some(git_dir) = git_dir {
let git_path = std::path::Path::new(git_dir);
let refs_path = git_path.join("refs");
if git_path.join("HEAD").exists() {
println!("cargo:rerun-if-changed={}/HEAD", git_dir);
}
if git_path.join("packed-refs").exists() {
println!("cargo:rerun-if-changed={}/packed-refs", git_dir);
}
if refs_path.join("heads").exists() {
println!("cargo:rerun-if-changed={}/refs/heads", git_dir);
}
if refs_path.join("tags").exists() {
println!("cargo:rerun-if-changed={}/refs/tags", git_dir);
}
}
let git_output = std::process::Command::new("git")
.args(["describe", "--always", "--tags", "--long", "--dirty"])
.output()
.ok();
let git_info = git_output
.as_ref()
.and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim));
let cargo_pkg_version = env!("CARGO_PKG_VERSION");
// Default git_describe to cargo_pkg_version
let mut git_describe = String::from(cargo_pkg_version);
if let Some(git_info) = git_info {
// If the `git_info` contains `CARGO_PKG_VERSION`, we simply use `git_info` as it is.
// Otherwise, prepend `CARGO_PKG_VERSION` to `git_info`.
if git_info.contains(cargo_pkg_version) {
// Remove the 'g' before the commit sha
let git_info = &git_info.replace('g', "");
git_describe = git_info.to_string();
} else {
git_describe = format!("v{}-{}", cargo_pkg_version, git_info);
}
}
println!("cargo:rustc-env=TASKWARRIOR_TUI_GIT_INFO={}", git_describe);
}
fn main() { fn main() {
get_commit_hash();
let mut app = generate_cli_app(); let mut app = generate_cli_app();
let name = app.get_name().to_string(); let name = app.get_name().to_string();
let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/"); let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/");

View file

@ -17,8 +17,8 @@ _taskwarrior-tui() {
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-d+[Sets the data folder for taskwarrior-tui]:FOLDER: ' \ '-d+[Sets the data folder for taskwarrior-tui]:FOLDER: ' \
'--data=[Sets the data folder for taskwarrior-tui]:FOLDER: ' \ '--data=[Sets the data folder for taskwarrior-tui]:FOLDER: ' \
'-c+[Sets the config folder for taskwarrior-tui (currently not used)]:FOLDER: ' \ '-c+[Sets the config folder for taskwarrior-tui]:FOLDER: ' \
'--config=[Sets the config folder for taskwarrior-tui (currently not used)]:FOLDER: ' \ '--config=[Sets the config folder for taskwarrior-tui]:FOLDER: ' \
'--taskdata=[Sets the .task folder using the TASKDATA environment variable for taskwarrior]:FOLDER: ' \ '--taskdata=[Sets the .task folder using the TASKDATA environment variable for taskwarrior]:FOLDER: ' \
'--taskrc=[Sets the .taskrc file using the TASKRC environment variable for taskwarrior]:FILE: ' \ '--taskrc=[Sets the .taskrc file using the TASKRC environment variable for taskwarrior]:FILE: ' \
'-r+[Sets default report]:STRING: ' \ '-r+[Sets default report]:STRING: ' \

View file

@ -23,8 +23,8 @@ Register-ArgumentCompleter -Native -CommandName 'taskwarrior-tui' -ScriptBlock {
'taskwarrior-tui' { 'taskwarrior-tui' {
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui') [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui')
[CompletionResult]::new('--data', 'data', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui') [CompletionResult]::new('--data', 'data', [CompletionResultType]::ParameterName, 'Sets the data folder for taskwarrior-tui')
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui (currently not used)') [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui')
[CompletionResult]::new('--config', 'config', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui (currently not used)') [CompletionResult]::new('--config', 'config', [CompletionResultType]::ParameterName, 'Sets the config folder for taskwarrior-tui')
[CompletionResult]::new('--taskdata', 'taskdata', [CompletionResultType]::ParameterName, 'Sets the .task folder using the TASKDATA environment variable for taskwarrior') [CompletionResult]::new('--taskdata', 'taskdata', [CompletionResultType]::ParameterName, 'Sets the .task folder using the TASKDATA environment variable for taskwarrior')
[CompletionResult]::new('--taskrc', 'taskrc', [CompletionResultType]::ParameterName, 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior') [CompletionResult]::new('--taskrc', 'taskrc', [CompletionResultType]::ParameterName, 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Sets default report') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Sets default report')

View file

@ -1,5 +1,5 @@
complete -c taskwarrior-tui -s d -l data -d 'Sets the data folder for taskwarrior-tui' -r complete -c taskwarrior-tui -s d -l data -d 'Sets the data folder for taskwarrior-tui' -r
complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwarrior-tui (currently not used)' -r complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwarrior-tui' -r
complete -c taskwarrior-tui -l taskdata -d 'Sets the .task folder using the TASKDATA environment variable for taskwarrior' -r complete -c taskwarrior-tui -l taskdata -d 'Sets the .task folder using the TASKDATA environment variable for taskwarrior' -r
complete -c taskwarrior-tui -l taskrc -d 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior' -r complete -c taskwarrior-tui -l taskrc -d 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior' -r
complete -c taskwarrior-tui -s r -l report -d 'Sets default report' -r complete -c taskwarrior-tui -s r -l report -d 'Sets default report' -r

View file

@ -5,13 +5,13 @@ use std::{
convert::TryInto, convert::TryInto,
fs, io, fs, io,
io::{Read, Write}, io::{Read, Write},
path::Path, path::{Path, PathBuf},
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use anyhow::{anyhow, Context as AnyhowContext, Result};
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike}; use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Timelike};
use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture}, event::{DisableMouseCapture, EnableMouseCapture},
execute, execute,
@ -57,7 +57,8 @@ use crate::{
scrollbar::Scrollbar, scrollbar::Scrollbar,
table::{Row, Table, TableMode, TableState}, table::{Row, Table, TableMode, TableState},
task_report::TaskReportTable, task_report::TaskReportTable,
ui, utils, ui,
utils::{self, get_data_dir},
}; };
const MAX_LINE: usize = 4096; const MAX_LINE: usize = 4096;
@ -177,7 +178,6 @@ pub struct TaskwarriorTui {
pub all_tasks: Vec<Task>, pub all_tasks: Vec<Task>,
pub task_details: HashMap<Uuid, String>, pub task_details: HashMap<Uuid, String>,
pub marked: HashSet<Uuid>, pub marked: HashSet<Uuid>,
// stores index of current task that is highlighted
pub current_selection: usize, pub current_selection: usize,
pub current_selection_uuid: Option<Uuid>, pub current_selection_uuid: Option<Uuid>,
pub current_selection_id: Option<u64>, pub current_selection_id: Option<u64>,
@ -240,7 +240,7 @@ impl TaskwarriorTui {
.output() .output()
.context("Unable to run `task --version`")?; .context("Unable to run `task --version`")?;
let task_version = Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).context("Unable to get version string")?; let task_version = Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).ok_or(anyhow!("Unable to get version string"))?;
let (w, h) = crossterm::terminal::size().unwrap_or((50, 15)); let (w, h) = crossterm::terminal::size().unwrap_or((50, 15));
@ -251,6 +251,8 @@ impl TaskwarriorTui {
}; };
let event_loop = crate::event::EventLoop::new(tick_rate, init_event_loop); let event_loop = crate::event::EventLoop::new(tick_rate, init_event_loop);
let data_dir = get_data_dir();
let mut app = Self { let mut app = Self {
should_quit: false, should_quit: false,
dirty: true, dirty: true,
@ -280,8 +282,8 @@ impl TaskwarriorTui {
keyconfig: kc, keyconfig: kc,
terminal_width: w, terminal_width: w,
terminal_height: h, terminal_height: h,
filter_history: HistoryContext::new("filter.history"), filter_history: HistoryContext::new("filter.history", data_dir.clone()),
command_history: HistoryContext::new("command.history"), command_history: HistoryContext::new("command.history", data_dir.clone()),
history_status: None, history_status: None,
completion_list: CompletionList::with_items(vec![]), completion_list: CompletionList::with_items(vec![]),
show_completion_pane: false, show_completion_pane: false,

View file

@ -21,7 +21,7 @@ pub fn generate_cli_app() -> clap::Command {
.short('c') .short('c')
.long("config") .long("config")
.value_name("FOLDER") .value_name("FOLDER")
.help("Sets the config folder for taskwarrior-tui (currently not used)") .help("Sets the config folder for taskwarrior-tui")
.action(clap::ArgAction::Set), .action(clap::ArgAction::Set),
) )
.arg( .arg(

View file

@ -1,6 +1,6 @@
use std::{collections::HashMap, error::Error, str}; use std::{collections::HashMap, error::Error, str};
use anyhow::{Context, Result}; use color_eyre::eyre::{eyre, Context, Result};
use ratatui::{ use ratatui::{
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
symbols::{bar::FULL, line::DOUBLE_VERTICAL}, symbols::{bar::FULL, line::DOUBLE_VERTICAL},
@ -465,14 +465,14 @@ impl Config {
fn get_rule_precedence_color(data: &str) -> Vec<String> { fn get_rule_precedence_color(data: &str) -> Vec<String> {
let data = Self::get_config("rule.precedence.color", data) let data = Self::get_config("rule.precedence.color", data)
.context("Unable to parse `task show rule.precedence.color`.") .ok_or_else(|| eyre!("Unable to parse `task show rule.precedence.color`."))
.unwrap(); .unwrap();
data.split(',').map(ToString::to_string).collect::<Vec<_>>() data.split(',').map(ToString::to_string).collect::<Vec<_>>()
} }
fn get_uda_priority_values(data: &str) -> Vec<String> { fn get_uda_priority_values(data: &str) -> Vec<String> {
let data = Self::get_config("uda.priority.values", data) let data = Self::get_config("uda.priority.values", data)
.context("Unable to parse `task show uda.priority.values`.") .ok_or_else(|| eyre!("Unable to parse `task show uda.priority.values`."))
.unwrap(); .unwrap();
data.split(',').map(ToString::to_string).collect::<Vec<_>>() data.split(',').map(ToString::to_string).collect::<Vec<_>>()
} }
@ -489,7 +489,7 @@ impl Config {
fn get_data_location(data: &str) -> String { fn get_data_location(data: &str) -> String {
Self::get_config("data.location", data) Self::get_config("data.location", data)
.context("Unable to parse `task show data.location`.") .ok_or_else(|| eyre!("Unable to parse `task show data.location`."))
.unwrap() .unwrap()
} }

View file

@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Result}; use color_eyre::eyre::{anyhow, Result};
use rustyline::{ use rustyline::{
error::ReadlineError, error::ReadlineError,
history::{DefaultHistory, History, SearchDirection}, history::{DefaultHistory, History, SearchDirection},
@ -16,17 +16,9 @@ pub struct HistoryContext {
} }
impl HistoryContext { impl HistoryContext {
pub fn new(filename: &str) -> Self { pub fn new(filename: &str, data_path: PathBuf) -> Self {
let history = DefaultHistory::new(); let history = DefaultHistory::new();
let data_path = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.map(|d| d.join("taskwarrior-tui"))
.expect("Unable to create configuration directory for taskwarrior-tui")
};
std::fs::create_dir_all(&data_path).unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path)); std::fs::create_dir_all(&data_path).unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path));
let data_path = data_path.join(filename); let data_path = data_path.join(filename);

View file

@ -1,6 +1,6 @@
use std::{collections::HashSet, error::Error, hash::Hash}; use std::{collections::HashSet, error::Error, hash::Hash};
use anyhow::{anyhow, Result}; use color_eyre::eyre::{anyhow, Result};
use log::{error, info, warn}; use log::{error, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -17,6 +17,7 @@ mod pane;
mod scrollbar; mod scrollbar;
mod table; mod table;
mod task_report; mod task_report;
mod tui;
mod ui; mod ui;
mod utils; mod utils;
@ -29,8 +30,8 @@ use std::{
time::Duration, time::Duration,
}; };
use anyhow::Result;
use app::{Mode, TaskwarriorTui}; use app::{Mode, TaskwarriorTui};
use color_eyre::eyre::Result;
use crossterm::{ use crossterm::{
cursor, cursor,
event::{DisableMouseCapture, EnableMouseCapture, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, EventStream},
@ -39,91 +40,20 @@ use crossterm::{
}; };
use futures::stream::{FuturesUnordered, StreamExt}; use futures::stream::{FuturesUnordered, StreamExt};
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter}; use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
use log4rs::{
append::file::FileAppender,
config::{Appender, Config, Logger, Root},
encode::pattern::PatternEncoder,
};
use path_clean::PathClean;
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use utils::{get_config_dir, get_data_dir};
use crate::{action::Action, event::Event, keyconfig::KeyConfig}; use crate::{
action::Action,
event::Event,
keyconfig::KeyConfig,
utils::{initialize_logging, initialize_panic_handler},
};
const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}"; const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
pub fn destruct_terminal() { #[tokio::main]
disable_raw_mode().unwrap(); async fn main() -> Result<()> {
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
execute!(io::stdout(), cursor::Show).unwrap();
}
pub fn initialize_logging() {
let data_local_dir = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.expect("Unable to find data directory for taskwarrior-tui")
.join("taskwarrior-tui")
};
std::fs::create_dir_all(&data_local_dir).unwrap_or_else(|_| panic!("Unable to create {:?}", data_local_dir));
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(LOG_PATTERN)))
.append(false)
.build(data_local_dir.join("taskwarrior-tui.log"))
.expect("Failed to build log file appender.");
let levelfilter = match std::env::var("TASKWARRIOR_TUI_LOG_LEVEL").unwrap_or_else(|_| "info".to_string()).as_str() {
"off" => LevelFilter::Off,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.logger(Logger::builder().build("taskwarrior_tui", levelfilter))
.build(Root::builder().appender("logfile").build(LevelFilter::Info))
.expect("Failed to build logging config.");
log4rs::init_config(config).expect("Failed to initialize logging.");
}
pub fn absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = path.as_ref();
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
env::current_dir()?.join(path)
}
.clean();
Ok(absolute_path)
}
async fn tui_main(report: &str) -> Result<()> {
panic::set_hook(Box::new(|panic_info| {
destruct_terminal();
better_panic::Settings::auto().create_panic_handler()(panic_info);
}));
let mut app = app::TaskwarriorTui::new(report, true).await?;
let mut terminal = app.start_tui()?;
let r = app.run(&mut terminal).await;
app.pause_tui().await?;
r
}
fn main() -> Result<()> {
better_panic::install();
let matches = cli::generate_cli_app().get_matches(); let matches = cli::generate_cli_app().get_matches();
let config = matches.get_one::<String>("config"); let config = matches.get_one::<String>("config");
@ -133,58 +63,42 @@ fn main() -> Result<()> {
let binding = String::from("next"); let binding = String::from("next");
let report = matches.get_one::<String>("report").unwrap_or(&binding); let report = matches.get_one::<String>("report").unwrap_or(&binding);
if let Some(e) = config { let config_dir = config.map(PathBuf::from).unwrap_or_else(get_config_dir);
if env::var("TASKWARRIOR_TUI_CONFIG").is_err() { let data_dir = data.map(PathBuf::from).unwrap_or_else(get_data_dir);
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKWARRIOR_TUI_CONFIG",
absolute_path(PathBuf::from(e)).expect("Unable to get path for config"),
)
} else {
warn!("TASKWARRIOR_TUI_CONFIG environment variable cannot be set.")
}
}
if let Some(e) = data { // if let Some(e) = taskrc {
if env::var("TASKWARRIOR_TUI_DATA").is_err() { // if env::var("TASKRC").is_err() {
// if environment variable is not set, this env::var returns an error // // if environment variable is not set, this env::var returns an error
env::set_var( // env::set_var("TASKRC", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"))
"TASKWARRIOR_TUI_DATA", // } else {
absolute_path(PathBuf::from(e)).expect("Unable to get path for data"), // warn!("TASKRC environment variable cannot be set.")
) // }
} else { // }
warn!("TASKWARRIOR_TUI_DATA environment variable cannot be set.") //
} // if let Some(e) = taskdata {
} // if env::var("TASKDATA").is_err() {
// // if environment variable is not set, this env::var returns an error
// env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"))
// } else {
// warn!("TASKDATA environment variable cannot be set.")
// }
// }
if let Some(e) = taskrc { initialize_logging(data_dir)?;
if env::var("TASKRC").is_err() { initialize_panic_handler()?;
// if environment variable is not set, this env::var returns an error
env::set_var("TASKRC", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"))
} else {
warn!("TASKRC environment variable cannot be set.")
}
}
if let Some(e) = taskdata {
if env::var("TASKDATA").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"))
} else {
warn!("TASKDATA environment variable cannot be set.")
}
}
initialize_logging();
debug!("getting matches from clap..."); debug!("getting matches from clap...");
debug!("report = {:?}", &report); debug!("report = {:?}", &report);
debug!("config = {:?}", &config); debug!("config = {:?}", &config);
let r = tokio::runtime::Builder::new_multi_thread() let mut app = app::TaskwarriorTui::new(report, true).await?;
.enable_all()
.build()? let mut terminal = app.start_tui()?;
.block_on(async { tui_main(report).await });
let r = app.run(&mut terminal).await;
app.pause_tui().await?;
if let Err(err) = r { if let Err(err) = r {
eprintln!("\x1b[0;31m[taskwarrior-tui error]\x1b[0m: {}\n\nIf you need additional help, please report as a github issue on https://github.com/kdheepak/taskwarrior-tui", err); eprintln!("\x1b[0;31m[taskwarrior-tui error]\x1b[0m: {}\n\nIf you need additional help, please report as a github issue on https://github.com/kdheepak/taskwarrior-tui", err);
std::process::exit(1); std::process::exit(1);

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use anyhow::{anyhow, Context as AnyhowContext, Result}; use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
const NAME: &str = "Name"; const NAME: &str = "Name";
const TYPE: &str = "Remaining"; const TYPE: &str = "Remaining";

View file

@ -1,6 +1,6 @@
use std::ops::Index; use std::ops::Index;
use anyhow::Result; use color_eyre::eyre::Result;
use crate::{ use crate::{
action::Action, action::Action,

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use anyhow::{anyhow, Context as AnyhowContext, Result}; use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
const COL_WIDTH: usize = 21; const COL_WIDTH: usize = 21;
const PROJECT_HEADER: &str = "Name"; const PROJECT_HEADER: &str = "Name";

View file

@ -1,7 +1,7 @@
use std::{error::Error, process::Command}; use std::{error::Error, process::Command};
use anyhow::Result;
use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, TimeZone}; use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, TimeZone};
use color_eyre::eyre::Result;
use itertools::join; use itertools::join;
use task_hookrs::{task::Task, uda::UDAValue}; use task_hookrs::{task::Task, uda::UDAValue};
use unicode_truncate::UnicodeTruncateStr; use unicode_truncate::UnicodeTruncateStr;
@ -14,7 +14,7 @@ pub fn format_date_time(dt: NaiveDateTime) -> String {
pub fn format_date(dt: NaiveDateTime) -> String { pub fn format_date(dt: NaiveDateTime) -> String {
let offset = Local.offset_from_utc_datetime(&dt); let offset = Local.offset_from_utc_datetime(&dt);
let dt = DateTime::<Local>::from_utc(dt, offset); let dt = DateTime::<Local>::from_naive_utc_and_offset(dt, offset);
dt.format("%Y-%m-%d").to_string() dt.format("%Y-%m-%d").to_string()
} }

150
src/tui.rs Normal file
View file

@ -0,0 +1,150 @@
use std::ops::{Deref, DerefMut};
use color_eyre::eyre::Result;
use crossterm::{
cursor,
event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::{FutureExt, StreamExt};
use ratatui::backend::CrosstermBackend as Backend;
use serde_derive::{Deserialize, Serialize};
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
pub type CrosstermFrame<'a> = ratatui::Frame<'a, Backend<std::io::Stderr>>;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Event {
Quit,
Error,
Closed,
Tick,
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
}
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub tick_rate: usize,
}
impl Tui {
pub fn new(tick_rate: usize) -> Result<Self> {
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
Ok(Self {
terminal,
task,
cancellation_token,
event_rx,
event_tx,
tick_rate,
})
}
pub fn start(&mut self) {
let tick_rate = std::time::Duration::from_millis(self.tick_rate as u64);
self.cancellation_token.cancel();
self.cancellation_token = CancellationToken::new();
let _cancellation_token = self.cancellation_token.clone();
let _event_tx = self.event_tx.clone();
self.task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut interval = tokio::time::interval(tick_rate);
loop {
let delay = interval.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
_ = _cancellation_token.cancelled() => {
break;
}
maybe_event = crossterm_event => {
match maybe_event {
Some(Ok(evt)) => {
match evt {
CrosstermEvent::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Event::Key(key)).unwrap();
}
},
CrosstermEvent::Resize(x, y) => {
_event_tx.send(Event::Resize(x, y)).unwrap();
},
_ => {},
}
}
Some(Err(_)) => {
_event_tx.send(Event::Error).unwrap();
}
None => {},
}
},
_ = delay => {
_event_tx.send(Event::Tick).unwrap();
},
}
}
});
}
pub fn enter(&mut self) -> Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
self.start();
Ok(())
}
pub fn exit(&self) -> Result<()> {
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
self.cancellation_token.cancel();
Ok(())
}
pub fn suspend(&self) -> Result<()> {
self.exit()?;
#[cfg(not(windows))]
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
self.enter()?;
Ok(())
}
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
}
impl Deref for Tui {
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Tui {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.exit().unwrap();
}
}

View file

@ -15,3 +15,151 @@ impl ChangeListener for Changeset {
fn replace(&mut self, idx: usize, old: &str, new: &str) {} fn replace(&mut self, idx: usize, old: &str, new: &str) {}
} }
use std::path::PathBuf;
use color_eyre::eyre::{anyhow, Context, Result};
use directories::ProjectDirs;
use lazy_static::lazy_static;
use tracing::error;
use tracing_error::ErrorLayer;
use tracing_subscriber::{self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer};
use crate::tui::Tui;
lazy_static! {
pub static ref CRATE_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
pub static ref DATA_FOLDER: Option<PathBuf> = std::env::var(format!("{}_DATA", CRATE_NAME.clone())).ok().map(PathBuf::from);
pub static ref CONFIG_FOLDER: Option<PathBuf> = std::env::var(format!("{}_CONFIG", CRATE_NAME.clone())).ok().map(PathBuf::from);
pub static ref GIT_COMMIT_HASH: String = std::env::var(format!("{}_GIT_INFO", CRATE_NAME.clone())).unwrap_or_else(|_| String::from("Unknown"));
pub static ref LOG_FILE: String = format!("{}.log", CRATE_NAME.to_lowercase());
}
fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("com", "kdheepak", CRATE_NAME.clone().to_lowercase().as_str())
}
pub fn initialize_panic_handler() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default().into_hooks();
eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| {
if let Ok(t) = Tui::new(0) {
if let Err(r) = t.exit() {
error!("Unable to exit Terminal: {:?}", r);
}
}
let msg = format!("{}", panic_hook.panic_report(panic_info));
tracing::error!("{}", strip_ansi_escapes::strip_str(&msg));
use human_panic::{handle_dump, print_msg, Metadata};
let meta = Metadata {
version: env!("CARGO_PKG_VERSION").into(),
name: env!("CARGO_PKG_NAME").into(),
authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(),
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
};
let file_path = handle_dump(&meta, panic_info);
print_msg(file_path, &meta).expect("human-panic: printing error message to console failed");
eprintln!("{}", msg);
std::process::exit(libc::EXIT_FAILURE);
}));
Ok(())
}
pub fn get_data_dir() -> PathBuf {
let directory = if let Some(s) = DATA_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.data_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".data")
};
directory
}
pub fn get_config_dir() -> PathBuf {
let directory = if let Some(s) = CONFIG_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.config_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".config")
};
directory
}
pub fn initialize_logging(directory: PathBuf) -> Result<()> {
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
let file_subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_target(false)
.with_ansi(false)
.with_filter(EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(file_subscriber)
.with(tui_logger::tracing_subscriber_layer())
.with(ErrorLayer::default())
.init();
let default_level = std::env::var("RUST_LOG").map_or(log::LevelFilter::Info, |val| match val.to_lowercase().as_str() {
"off" => log::LevelFilter::Off,
"error" => log::LevelFilter::Error,
"warn" => log::LevelFilter::Warn,
"info" => log::LevelFilter::Info,
"debug" => log::LevelFilter::Debug,
"trace" => log::LevelFilter::Trace,
_ => log::LevelFilter::Info,
});
tui_logger::set_default_level(default_level);
Ok(())
}
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
/// than printing to stdout.
///
/// By default, the verbosity level for the generated events is `DEBUG`, but
/// this can be customized.
#[macro_export]
macro_rules! trace_dbg {
(target: $target:expr, level: $level:expr, $ex:expr) => {{
match $ex {
value => {
tracing::event!(target: $target, $level, ?value, stringify!($ex));
value
}
}
}};
(level: $level:expr, $ex:expr) => {
trace_dbg!(target: module_path!(), level: $level, $ex)
};
(target: $target:expr, $ex:expr) => {
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
};
($ex:expr) => {
trace_dbg!(level: tracing::Level::DEBUG, $ex)
};
}
pub fn version() -> String {
let author = clap::crate_authors!();
let commit_hash = GIT_COMMIT_HASH.clone();
// let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string();
let config_dir_path = get_config_dir().display().to_string();
let data_dir_path = get_data_dir().display().to_string();
format!(
"\
{commit_hash}
Authors: {author}
Config directory: {config_dir_path}
Data directory: {data_dir_path}"
)
}