mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-24 14:36:42 +02:00
WIP
This commit is contained in:
parent
732c5b6f84
commit
1ec93c0913
46 changed files with 1233 additions and 7570 deletions
|
@ -1,39 +0,0 @@
|
|||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Action {
|
||||
Tick,
|
||||
Error(String),
|
||||
Quit,
|
||||
Refresh,
|
||||
GotoBottom,
|
||||
GotoTop,
|
||||
GotoPageBottom,
|
||||
GotoPageTop,
|
||||
Down,
|
||||
Up,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Delete,
|
||||
Done,
|
||||
ToggleStartStop,
|
||||
ToggleMark,
|
||||
ToggleMarkAll,
|
||||
QuickTag,
|
||||
Select,
|
||||
SelectAll,
|
||||
Undo,
|
||||
Edit,
|
||||
Shell,
|
||||
Help,
|
||||
ToggleZoom,
|
||||
Context,
|
||||
Next,
|
||||
Previous,
|
||||
Shortcut(usize),
|
||||
Modify,
|
||||
Log,
|
||||
Annotate,
|
||||
Filter,
|
||||
Add,
|
||||
}
|
2752
src/app.rs
2752
src/app.rs
File diff suppressed because it is too large
Load diff
277
src/calendar.rs
277
src/calendar.rs
|
@ -1,277 +0,0 @@
|
|||
// Based on https://gist.github.com/diwic/5c20a283ca3a03752e1a27b0f3ebfa30
|
||||
// See https://old.reddit.com/r/rust/comments/4xneq5/the_calendar_example_challenge_ii_why_eddyb_all/
|
||||
|
||||
use std::fmt;
|
||||
|
||||
const COL_WIDTH: usize = 21;
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use chrono::{
|
||||
format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calendar<'a> {
|
||||
pub block: Option<Block<'a>>,
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub style: Style,
|
||||
pub months_per_row: usize,
|
||||
pub date_style: Vec<(NaiveDate, Style)>,
|
||||
pub today_style: Style,
|
||||
pub start_on_monday: bool,
|
||||
pub title_background_color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Default for Calendar<'a> {
|
||||
fn default() -> Calendar<'a> {
|
||||
let year = Local::now().year();
|
||||
let month = Local::now().month();
|
||||
Calendar {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
months_per_row: 0,
|
||||
year,
|
||||
month,
|
||||
date_style: vec![],
|
||||
today_style: Style::default(),
|
||||
start_on_monday: false,
|
||||
title_background_color: Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Calendar<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn year(mut self, year: i32) -> Self {
|
||||
self.year = year;
|
||||
if self.year < 0 {
|
||||
self.year = 0;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn month(mut self, month: u32) -> Self {
|
||||
self.month = month;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn date_style(mut self, date_style: Vec<(NaiveDate, Style)>) -> Self {
|
||||
self.date_style = date_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn today_style(mut self, today_style: Style) -> Self {
|
||||
self.today_style = today_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn months_per_row(mut self, months_per_row: usize) -> Self {
|
||||
self.months_per_row = months_per_row;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_on_monday(mut self, start_on_monday: bool) -> Self {
|
||||
self.start_on_monday = start_on_monday;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Calendar<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let month_names = Self::generate_month_names();
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if area.height < 7 {
|
||||
return;
|
||||
}
|
||||
|
||||
let style = self.style;
|
||||
let today = Local::now();
|
||||
|
||||
let year = self.year;
|
||||
let month = self.month;
|
||||
|
||||
let months: Vec<_> = (0..12).collect();
|
||||
|
||||
let mut days: Vec<(NaiveDate, NaiveDate)> = months
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let first = NaiveDate::from_ymd_opt(year, i + 1, 1).unwrap();
|
||||
let num_days = if self.start_on_monday {
|
||||
first.weekday().num_days_from_monday()
|
||||
} else {
|
||||
first.weekday().num_days_from_sunday()
|
||||
};
|
||||
(first, first - Duration::days(i64::from(num_days)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut start_m = 0_usize;
|
||||
if self.months_per_row > area.width as usize / 8 / 3 || self.months_per_row == 0 {
|
||||
self.months_per_row = area.width as usize / 8 / 3;
|
||||
}
|
||||
let mut y = area.y;
|
||||
y += 1;
|
||||
|
||||
let x = area.x;
|
||||
let s = format!("{year:^width$}", year = year, width = area.width as usize);
|
||||
|
||||
let mut new_year = 0;
|
||||
let style = Style::default().add_modifier(Modifier::UNDERLINED);
|
||||
if self.year + new_year as i32 == today.year() {
|
||||
buf.set_string(x, y, &s, self.today_style.add_modifier(Modifier::UNDERLINED));
|
||||
} else {
|
||||
buf.set_string(x, y, &s, style);
|
||||
}
|
||||
|
||||
let start_x = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2;
|
||||
y += 2;
|
||||
loop {
|
||||
let endm = std::cmp::min(start_m + self.months_per_row, 12);
|
||||
let mut x = area.x + start_x;
|
||||
for (c, d) in days.iter_mut().enumerate().take(endm).skip(start_m) {
|
||||
if c > start_m {
|
||||
x += 1;
|
||||
}
|
||||
let m = d.0.month() as usize;
|
||||
let s = format!("{:^20}", month_names[m - 1]);
|
||||
let style = Style::default().bg(self.title_background_color);
|
||||
if m == today.month() as usize && self.year + new_year as i32 == today.year() {
|
||||
buf.set_string(x, y, &s, self.today_style);
|
||||
} else {
|
||||
buf.set_string(x, y, &s, style);
|
||||
}
|
||||
x += s.len() as u16 + 1;
|
||||
}
|
||||
y += 1;
|
||||
let mut x = area.x + start_x;
|
||||
for d in days.iter_mut().take(endm).skip(start_m) {
|
||||
let m = d.0.month() as usize;
|
||||
let style = Style::default().bg(self.title_background_color);
|
||||
let days_string = if self.start_on_monday {
|
||||
"Mo Tu We Th Fr Sa Su"
|
||||
} else {
|
||||
"Su Mo Tu We Th Fr Sa"
|
||||
};
|
||||
buf.set_string(x, y, days_string, style.add_modifier(Modifier::UNDERLINED));
|
||||
x += 21 + 1;
|
||||
}
|
||||
y += 1;
|
||||
loop {
|
||||
let mut moredays = false;
|
||||
let mut x = area.x + start_x;
|
||||
for c in start_m..endm {
|
||||
if c > start_m {
|
||||
x += 1;
|
||||
}
|
||||
let d = &mut days[c + new_year * 12];
|
||||
for _ in 0..7 {
|
||||
let s = if d.0.month() == d.1.month() {
|
||||
format!("{:>2}", d.1.day())
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
let mut style = Style::default();
|
||||
let index = self.date_style.iter().position(|(date, style)| d.1 == *date);
|
||||
if let Some(i) = index {
|
||||
style = self.date_style[i].1;
|
||||
}
|
||||
if d.1 == Local::now().date_naive() {
|
||||
buf.set_string(x, y, s, self.today_style);
|
||||
} else {
|
||||
buf.set_string(x, y, s, style);
|
||||
}
|
||||
x += 3;
|
||||
d.1 += Duration::days(1);
|
||||
}
|
||||
moredays |= d.0.month() == d.1.month() || d.1 < d.0;
|
||||
}
|
||||
y += 1;
|
||||
if !moredays {
|
||||
break;
|
||||
}
|
||||
}
|
||||
start_m += self.months_per_row;
|
||||
y += 2;
|
||||
if y + 8 > area.height {
|
||||
break;
|
||||
} else if start_m >= 12 {
|
||||
start_m = 0;
|
||||
new_year += 1;
|
||||
days.append(
|
||||
&mut months
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let first = NaiveDate::from_ymd_opt(self.year + new_year as i32, i + 1, 1).unwrap();
|
||||
(
|
||||
first,
|
||||
first - Duration::days(i64::from(first.weekday().num_days_from_sunday())),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let x = area.x;
|
||||
let s = format!(
|
||||
"{year:^width$}",
|
||||
year = self.year as usize + new_year,
|
||||
width = area.width as usize
|
||||
);
|
||||
let mut style = Style::default().add_modifier(Modifier::UNDERLINED);
|
||||
if self.year + new_year as i32 == today.year() {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
buf.set_string(x, y, &s, style);
|
||||
y += 1;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Calendar<'a> {
|
||||
fn generate_month_names() -> [&'a str; 12] {
|
||||
let month_names = [
|
||||
Month::January.name(),
|
||||
Month::February.name(),
|
||||
Month::March.name(),
|
||||
Month::April.name(),
|
||||
Month::May.name(),
|
||||
Month::June.name(),
|
||||
Month::July.name(),
|
||||
Month::August.name(),
|
||||
Month::September.name(),
|
||||
Month::October.name(),
|
||||
Month::November.name(),
|
||||
Month::December.name(),
|
||||
];
|
||||
month_names
|
||||
}
|
||||
}
|
80
src/cli.rs
80
src/cli.rs
|
@ -1,52 +1,36 @@
|
|||
use clap::Arg;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
use clap::Parser;
|
||||
|
||||
pub fn generate_cli_app() -> clap::Command {
|
||||
let mut app = clap::Command::new(APP_NAME)
|
||||
.version(APP_VERSION)
|
||||
.author("Dheepak Krishnamurthy <@kdheepak>")
|
||||
.about("A taskwarrior terminal user interface")
|
||||
.arg(
|
||||
Arg::new("data")
|
||||
.short('d')
|
||||
.long("data")
|
||||
.value_name("FOLDER")
|
||||
.help("Sets the data folder for taskwarrior-tui")
|
||||
.action(clap::ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("config")
|
||||
.short('c')
|
||||
.long("config")
|
||||
.value_name("FOLDER")
|
||||
.help("Sets the config folder for taskwarrior-tui")
|
||||
.action(clap::ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("taskdata")
|
||||
.long("taskdata")
|
||||
.value_name("FOLDER")
|
||||
.help("Sets the .task folder using the TASKDATA environment variable for taskwarrior")
|
||||
.action(clap::ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("taskrc")
|
||||
.long("taskrc")
|
||||
.value_name("FILE")
|
||||
.help("Sets the .taskrc file using the TASKRC environment variable for taskwarrior")
|
||||
.action(clap::ArgAction::Set),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("report")
|
||||
.short('r')
|
||||
.long("report")
|
||||
.value_name("STRING")
|
||||
.help("Sets default report")
|
||||
.action(clap::ArgAction::Set),
|
||||
);
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
pub struct Cli {
|
||||
#[arg(short, long, value_name = "FOLDER", help = "Sets the data folder for taskwarrior-tui")]
|
||||
pub data: Option<String>,
|
||||
|
||||
app.set_bin_name(APP_NAME);
|
||||
app
|
||||
#[arg(short, long, value_name = "FOLDER", help = "Sets the config folder for taskwarrior-tui")]
|
||||
pub config: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FOLDER",
|
||||
help = "Sets the .task folder using the TASKDATA environment variable for taskwarrior"
|
||||
)]
|
||||
pub taskdata: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
help = "Sets the .taskrc file using the TASKRC environment variable for taskwarrior"
|
||||
)]
|
||||
pub taskrc: Option<PathBuf>,
|
||||
|
||||
#[arg(value_name = "FLOAT", help = "Tick rate", default_value_t = 4.0)]
|
||||
pub tick_rate: f64,
|
||||
|
||||
#[arg(value_name = "FLOAT", help = "Frame rate", default_value_t = 60.0)]
|
||||
pub frame_rate: f64,
|
||||
|
||||
#[arg(short, long, value_name = "STRING", help = "Sets default report")]
|
||||
pub report: Option<String>,
|
||||
}
|
||||
|
|
46
src/command.rs
Normal file
46
src/command.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Command {
|
||||
Tick,
|
||||
Render,
|
||||
Resize(u16, u16),
|
||||
Suspend,
|
||||
Resume,
|
||||
Quit,
|
||||
Refresh,
|
||||
Error(String),
|
||||
Help,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
MoveBottom,
|
||||
MoveTop,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveHome,
|
||||
MoveEnd,
|
||||
ToggleMark,
|
||||
ToggleMarkAll,
|
||||
Select,
|
||||
SelectAll,
|
||||
ToggleZoom,
|
||||
Context,
|
||||
RunShortcut(usize),
|
||||
RunShell,
|
||||
Task,
|
||||
ShowTaskReport,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Task {
|
||||
Undo,
|
||||
Edit,
|
||||
Tag,
|
||||
Start,
|
||||
Stop,
|
||||
Modify,
|
||||
Log,
|
||||
Annotate,
|
||||
Filter,
|
||||
Add,
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
use std::{error::Error, io};
|
||||
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
use rustyline::{
|
||||
error::ReadlineError,
|
||||
highlight::{Highlighter, MatchingBracketHighlighter},
|
||||
hint::Hinter,
|
||||
history::FileHistory,
|
||||
line_buffer::LineBuffer,
|
||||
Context,
|
||||
};
|
||||
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub fn get_start_word_under_cursor(line: &str, cursor_pos: usize) -> usize {
|
||||
let mut chars = line[..cursor_pos].chars();
|
||||
let mut res = cursor_pos;
|
||||
while let Some(c) = chars.next_back() {
|
||||
if c == ' ' || c == '(' || c == ')' {
|
||||
break;
|
||||
}
|
||||
res -= c.len_utf8();
|
||||
}
|
||||
// if iter == None, res == 0.
|
||||
res
|
||||
}
|
||||
|
||||
pub struct TaskwarriorTuiCompletionHelper {
|
||||
pub candidates: Vec<(String, String)>,
|
||||
pub context: String,
|
||||
pub input: String,
|
||||
}
|
||||
|
||||
type Completion = (String, String, String, String, String);
|
||||
|
||||
impl TaskwarriorTuiCompletionHelper {
|
||||
fn complete(&self, word: &str, pos: usize, _ctx: &Context) -> rustyline::Result<(usize, Vec<Completion>)> {
|
||||
let candidates: Vec<Completion> = self
|
||||
.candidates
|
||||
.iter()
|
||||
.filter_map(|(context, candidate)| {
|
||||
if context == &self.context
|
||||
&& (candidate.starts_with(&word[..pos]) || candidate.to_lowercase().starts_with(&word[..pos].to_lowercase()))
|
||||
&& (!self.input.contains(candidate) || !self.input.to_lowercase().contains(&candidate.to_lowercase()))
|
||||
{
|
||||
Some((
|
||||
candidate.clone(), // display
|
||||
candidate.to_string(), // replacement
|
||||
word[..pos].to_string(), // original
|
||||
candidate[..pos].to_string(),
|
||||
candidate[pos..].to_string(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok((pos, candidates))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompletionList {
|
||||
pub state: ListState,
|
||||
pub current: String,
|
||||
pub pos: usize,
|
||||
pub helper: TaskwarriorTuiCompletionHelper,
|
||||
}
|
||||
|
||||
impl CompletionList {
|
||||
pub fn new() -> CompletionList {
|
||||
CompletionList {
|
||||
state: ListState::default(),
|
||||
current: String::new(),
|
||||
pos: 0,
|
||||
helper: TaskwarriorTuiCompletionHelper {
|
||||
candidates: vec![],
|
||||
context: String::new(),
|
||||
input: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<(String, String)>) -> CompletionList {
|
||||
let mut candidates = vec![];
|
||||
for i in items {
|
||||
if !candidates.contains(&i) {
|
||||
candidates.push(i);
|
||||
}
|
||||
}
|
||||
let context = String::new();
|
||||
let input = String::new();
|
||||
CompletionList {
|
||||
state: ListState::default(),
|
||||
current: String::new(),
|
||||
pos: 0,
|
||||
helper: TaskwarriorTuiCompletionHelper {
|
||||
candidates,
|
||||
context,
|
||||
input,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, item: (String, String)) {
|
||||
if !self.helper.candidates.contains(&item) {
|
||||
self.helper.candidates.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.candidates().len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.candidates().len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.helper.candidates.clear();
|
||||
self.state.select(None);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.candidates().len()
|
||||
}
|
||||
|
||||
pub fn max_width(&self) -> Option<usize> {
|
||||
self.candidates().iter().map(|p| p.1.width() + 4).max()
|
||||
}
|
||||
|
||||
pub fn get(&self, i: usize) -> Option<Completion> {
|
||||
let candidates = self.candidates();
|
||||
if i < candidates.len() {
|
||||
Some(candidates[i].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<(usize, Completion)> {
|
||||
self.state.selected().and_then(|i| self.get(i)).map(|s| (self.pos, s))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.candidates().is_empty()
|
||||
}
|
||||
|
||||
pub fn candidates(&self) -> Vec<Completion> {
|
||||
let hist = FileHistory::new();
|
||||
let ctx = rustyline::Context::new(&hist);
|
||||
let (pos, candidates) = self.helper.complete(&self.current, self.pos, &ctx).unwrap();
|
||||
candidates
|
||||
}
|
||||
|
||||
pub fn input(&mut self, current: String, i: String) {
|
||||
self.helper.input = i;
|
||||
if current.contains('.') && current.contains(':') {
|
||||
self.current = current.split_once(':').unwrap().1.to_string();
|
||||
self.helper.context = current.split_once('.').unwrap().0.to_string();
|
||||
} else if current.contains('.') {
|
||||
self.current = format!(".{}", current.split_once('.').unwrap().1);
|
||||
self.helper.context = "modifier".to_string();
|
||||
} else if current.contains(':') {
|
||||
self.current = current.split_once(':').unwrap().1.to_string();
|
||||
self.helper.context = current.split_once(':').unwrap().0.to_string();
|
||||
} else if current.contains('+') {
|
||||
self.current = format!("+{}", current.split_once('+').unwrap().1);
|
||||
self.helper.context = "+".to_string();
|
||||
} else {
|
||||
self.current = current;
|
||||
self.helper.context = "attribute".to_string();
|
||||
}
|
||||
self.pos = self.current.len();
|
||||
}
|
||||
}
|
42
src/components.rs
Normal file
42
src/components.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
command::Command,
|
||||
tui::{Event, Frame},
|
||||
};
|
||||
|
||||
pub mod app;
|
||||
|
||||
pub trait Component {
|
||||
#[allow(unused_variables)]
|
||||
fn register_command_handler(&mut self, tx: UnboundedSender<Command>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn init(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Command>> {
|
||||
let r = match event {
|
||||
Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
|
||||
Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?,
|
||||
_ => None,
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Command>> {
|
||||
Ok(None)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Option<Command>> {
|
||||
Ok(None)
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn update(&mut self, command: Command) -> Result<Option<Command>> {
|
||||
Ok(None)
|
||||
}
|
||||
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()>;
|
||||
}
|
141
src/components/app.rs
Normal file
141
src/components/app.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use task_hookrs::{import::import, task::Task};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{Component, Frame};
|
||||
use crate::{command::Command, config::KeyBindings};
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
TaskReport,
|
||||
TaskContext,
|
||||
Calendar,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
pub mode: Mode,
|
||||
pub command_tx: Option<UnboundedSender<Command>>,
|
||||
pub keybindings: KeyBindings,
|
||||
pub last_export: Option<std::time::SystemTime>,
|
||||
pub report: String,
|
||||
pub filter: String,
|
||||
pub current_context_filter: String,
|
||||
pub tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn keybindings(mut self, keybindings: KeyBindings) -> Self {
|
||||
self.keybindings = keybindings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
self.last_export = Some(std::time::SystemTime::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_command(&self, command: Command) -> Result<()> {
|
||||
if let Some(ref tx) = self.command_tx {
|
||||
tx.send(command)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn task_export(&mut self) -> Result<()> {
|
||||
let mut task = std::process::Command::new("task");
|
||||
|
||||
task
|
||||
.arg("rc.json.array=on")
|
||||
.arg("rc.confirmation=off")
|
||||
.arg("rc.json.depends.array=on")
|
||||
.arg("rc.color=off")
|
||||
.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()) {
|
||||
for arg in args {
|
||||
task.arg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.current_context_filter.trim().is_empty() {
|
||||
if let Some(args) = shlex::split(&self.current_context_filter) {
|
||||
for arg in args {
|
||||
task.arg(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.arg("export");
|
||||
|
||||
task.arg(&self.report);
|
||||
|
||||
log::info!("Running `{:?}`", task);
|
||||
let output = task.output()?;
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if output.status.success() {
|
||||
if let Ok(imported) = import(data.as_bytes()) {
|
||||
self.tasks = imported;
|
||||
log::info!("Imported {} tasks", self.tasks.len());
|
||||
if self.mode == Mode::Error {
|
||||
self.send_command(Command::ShowTaskReport)?;
|
||||
};
|
||||
// } else {
|
||||
// self.error = Some(format!("Unable to parse output of `{:?}`:\n`{:?}`", task, data));
|
||||
// self.mode = Mode::Tasks(Action::Error);
|
||||
// debug!("Unable to parse output: {:?}", data);
|
||||
}
|
||||
} else {
|
||||
// self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
fn register_command_handler(&mut self, tx: UnboundedSender<Command>) -> Result<()> {
|
||||
self.command_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Command>> {
|
||||
let command = if let Some(keymap) = self.keybindings.get(&self.mode) {
|
||||
if let Some(command) = keymap.get(&vec![key]) {
|
||||
command
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(command.clone()))
|
||||
}
|
||||
|
||||
fn update(&mut self, command: Command) -> Result<Option<Command>> {
|
||||
match command {
|
||||
_ => (),
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
828
src/config.rs
828
src/config.rs
|
@ -1,102 +1,322 @@
|
|||
use std::{collections::HashMap, error::Error, path::PathBuf, str};
|
||||
use std::{collections::HashMap, fmt, path::PathBuf};
|
||||
|
||||
use color_eyre::eyre::{eyre, Context, Result};
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use figment::{
|
||||
providers::{Env, Format, Serialized, Toml},
|
||||
Figment,
|
||||
};
|
||||
use ratatui::{
|
||||
style::{Color, Modifier, Style},
|
||||
symbols::line::DOUBLE_VERTICAL,
|
||||
};
|
||||
use serde::{
|
||||
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
||||
ser::{self, Serialize, Serializer},
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::{action::Action, keyevent::parse_key_sequence, keymap::KeyMap, utils::get_config_dir};
|
||||
use crate::{command::Command, components::app::Mode};
|
||||
|
||||
#[derive(Default, Clone, Debug, Copy)]
|
||||
pub struct SerdeStyle(pub Style);
|
||||
const CONFIG: &'static str = include_str!("../.config/config.json5");
|
||||
|
||||
impl std::ops::Deref for SerdeStyle {
|
||||
type Target = Style;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub _data_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
pub _config_dir: PathBuf,
|
||||
}
|
||||
impl std::ops::DerefMut for SerdeStyle {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default, flatten)]
|
||||
pub config: AppConfig,
|
||||
#[serde(default)]
|
||||
pub keybindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
pub styles: Styles,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, config::ConfigError> {
|
||||
let default_config: Config = json5::from_str(&CONFIG).unwrap();
|
||||
let data_dir = crate::utils::get_data_dir();
|
||||
let config_dir = crate::utils::get_config_dir();
|
||||
let mut builder = config::Config::builder()
|
||||
.set_default("_data_dir", data_dir.to_str().unwrap())?
|
||||
.set_default("_config_dir", config_dir.to_str().unwrap())?;
|
||||
|
||||
builder = builder
|
||||
.add_source(config::File::from(config_dir.join("config.json5")).format(config::FileFormat::Json5).required(false))
|
||||
.add_source(config::File::from(config_dir.join("config.json")).format(config::FileFormat::Json).required(false))
|
||||
.add_source(config::File::from(config_dir.join("config.yaml")).format(config::FileFormat::Yaml).required(false))
|
||||
.add_source(config::File::from(config_dir.join("config.toml")).format(config::FileFormat::Toml).required(false))
|
||||
.add_source(config::File::from(config_dir.join("config.ini")).format(config::FileFormat::Ini).required(false));
|
||||
|
||||
let mut cfg: Self = builder.build()?.try_deserialize()?;
|
||||
|
||||
for (mode, default_bindings) in default_config.keybindings.iter() {
|
||||
let user_bindings = cfg.keybindings.entry(*mode).or_default();
|
||||
for (key, cmd) in default_bindings.iter() {
|
||||
user_bindings.entry(key.clone()).or_insert_with(|| cmd.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerdeStyle {
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Command>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyBindings {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct StyleVisitor;
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, Command>>::deserialize(deserializer)?;
|
||||
|
||||
impl<'de> Visitor<'de> for StyleVisitor {
|
||||
type Value = SerdeStyle;
|
||||
let keybindings = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map =
|
||||
inner_map.into_iter().map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)).collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string representation of tui::style::Style")
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, v: &str) -> Result<SerdeStyle, E> {
|
||||
Ok(SerdeStyle(get_tcolor(v)))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(StyleVisitor)
|
||||
Ok(KeyBindings(keybindings))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tcolor(line: &str) -> Style {
|
||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
},
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
},
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
},
|
||||
_ => break, // break out of the loop if no known prefix is detected
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
},
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"hyphen" => KeyCode::Char('-'),
|
||||
"minus" => KeyCode::Char('-'),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
},
|
||||
_ => return Err(format!("Unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
let char;
|
||||
let key_code = match key_event.code {
|
||||
KeyCode::Backspace => "backspace",
|
||||
KeyCode::Enter => "enter",
|
||||
KeyCode::Left => "left",
|
||||
KeyCode::Right => "right",
|
||||
KeyCode::Up => "up",
|
||||
KeyCode::Down => "down",
|
||||
KeyCode::Home => "home",
|
||||
KeyCode::End => "end",
|
||||
KeyCode::PageUp => "pageup",
|
||||
KeyCode::PageDown => "pagedown",
|
||||
KeyCode::Tab => "tab",
|
||||
KeyCode::BackTab => "backtab",
|
||||
KeyCode::Delete => "delete",
|
||||
KeyCode::Insert => "insert",
|
||||
KeyCode::F(c) => {
|
||||
char = format!("f({})", c.to_string());
|
||||
&char
|
||||
},
|
||||
KeyCode::Char(c) if c == ' ' => "space",
|
||||
KeyCode::Char(c) => {
|
||||
char = c.to_string();
|
||||
&char
|
||||
},
|
||||
KeyCode::Esc => "esc",
|
||||
KeyCode::Null => "",
|
||||
KeyCode::CapsLock => "",
|
||||
KeyCode::Menu => "",
|
||||
KeyCode::ScrollLock => "",
|
||||
KeyCode::Media(_) => "",
|
||||
KeyCode::NumLock => "",
|
||||
KeyCode::PrintScreen => "",
|
||||
KeyCode::Pause => "",
|
||||
KeyCode::KeypadBegin => "",
|
||||
KeyCode::Modifier(_) => "",
|
||||
};
|
||||
|
||||
let mut modifiers = Vec::with_capacity(3);
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
modifiers.push("ctrl");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
let mut key = modifiers.join("-");
|
||||
|
||||
if !key.is_empty() {
|
||||
key.push('-');
|
||||
}
|
||||
key.push_str(key_code);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
|
||||
return Err(format!("Unable to parse `{}`", raw));
|
||||
}
|
||||
let raw = if !raw.contains("><") {
|
||||
let raw = raw.strip_prefix("<").unwrap_or(raw);
|
||||
let raw = raw.strip_prefix(">").unwrap_or(raw);
|
||||
raw
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let sequences = raw
|
||||
.split("><")
|
||||
.map(|seq| {
|
||||
if seq.starts_with('<') {
|
||||
&seq[1..]
|
||||
} else if seq.ends_with('>') {
|
||||
&seq[..seq.len() - 1]
|
||||
} else {
|
||||
seq
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sequences.into_iter().map(parse_key_event).collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Styles {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?;
|
||||
|
||||
let styles = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map.into_iter().map(|(str, style)| (str, parse_style(&style))).collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Styles(styles))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_style(line: &str) -> Style {
|
||||
let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
|
||||
let (mut foreground, mut background) = (String::from(foreground), String::from(background));
|
||||
background = background.replace("on ", "");
|
||||
let mut modifiers = Modifier::empty();
|
||||
if foreground.contains("bright") {
|
||||
foreground = foreground.replace("bright ", "");
|
||||
background = background.replace("bright ", "");
|
||||
background.insert_str(0, "bright ");
|
||||
}
|
||||
foreground = foreground.replace("grey", "gray");
|
||||
background = background.replace("grey", "gray");
|
||||
if foreground.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
let foreground = foreground.replace("underline ", "");
|
||||
// TODO: use bold, bright boolean flags
|
||||
if foreground.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
// let foreground = foreground.replace("bold ", "");
|
||||
if foreground.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
let foreground = foreground.replace("inverse ", "");
|
||||
let foreground = process_color_string(foreground);
|
||||
let background = process_color_string(&background.replace("on ", ""));
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Some(fg) = get_color_foreground(foreground.as_str()) {
|
||||
if let Some(fg) = parse_color(&foreground.0) {
|
||||
style = style.fg(fg);
|
||||
}
|
||||
if let Some(bg) = get_color_background(background.as_str()) {
|
||||
if let Some(bg) = parse_color(&background.0) {
|
||||
style = style.bg(bg);
|
||||
}
|
||||
style = style.add_modifier(modifiers);
|
||||
style = style.add_modifier(foreground.1 | background.1);
|
||||
style
|
||||
}
|
||||
|
||||
fn get_color_foreground(s: &str) -> Option<Color> {
|
||||
fn process_color_string(color_str: &str) -> (String, Modifier) {
|
||||
let color = color_str
|
||||
.replace("grey", "gray")
|
||||
.replace("bright ", "")
|
||||
.replace("bold ", "")
|
||||
.replace("underline ", "")
|
||||
.replace("inverse ", "");
|
||||
|
||||
let mut modifiers = Modifier::empty();
|
||||
if color_str.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
if color_str.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
if color_str.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
(color, modifiers)
|
||||
}
|
||||
|
||||
fn parse_color(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
if s.contains("color") {
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
|
@ -145,372 +365,118 @@ fn get_color_foreground(s: &str) -> Option<Color> {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_color_background(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = 232 + s.trim_start_matches("gray").parse::<u8>().unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("rgb") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let c = 16 + red * 36 + green * 6 + blue;
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s == "bright black" {
|
||||
Some(Color::Indexed(8))
|
||||
} else if s == "bright red" {
|
||||
Some(Color::Indexed(9))
|
||||
} else if s == "bright green" {
|
||||
Some(Color::Indexed(10))
|
||||
} else if s == "bright yellow" {
|
||||
Some(Color::Indexed(11))
|
||||
} else if s == "bright blue" {
|
||||
Some(Color::Indexed(12))
|
||||
} else if s == "bright magenta" {
|
||||
Some(Color::Indexed(13))
|
||||
} else if s == "bright cyan" {
|
||||
Some(Color::Indexed(14))
|
||||
} else if s == "bright white" {
|
||||
Some(Color::Indexed(15))
|
||||
} else if s == "black" {
|
||||
Some(Color::Indexed(0))
|
||||
} else if s == "red" {
|
||||
Some(Color::Indexed(1))
|
||||
} else if s == "green" {
|
||||
Some(Color::Indexed(2))
|
||||
} else if s == "yellow" {
|
||||
Some(Color::Indexed(3))
|
||||
} else if s == "blue" {
|
||||
Some(Color::Indexed(4))
|
||||
} else if s == "magenta" {
|
||||
Some(Color::Indexed(5))
|
||||
} else if s == "cyan" {
|
||||
Some(Color::Indexed(6))
|
||||
} else if s == "white" {
|
||||
Some(Color::Indexed(7))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SerdeStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Getting the foreground color string
|
||||
let fg_str = color_to_string(self.0.fg.unwrap_or_default());
|
||||
|
||||
// Getting the background color string
|
||||
let mut bg_str = color_to_string(self.0.bg.unwrap_or_default());
|
||||
|
||||
// If the background is not default, prepend with "on "
|
||||
if bg_str != "" {
|
||||
bg_str.insert_str(0, "on ");
|
||||
}
|
||||
|
||||
// Building the modifier string
|
||||
let mut mod_str = String::new();
|
||||
let mod_val = self.0.add_modifier;
|
||||
if mod_val.contains(Modifier::BOLD) {
|
||||
mod_str.push_str("bold ");
|
||||
}
|
||||
if mod_val.contains(Modifier::UNDERLINED) {
|
||||
mod_str.push_str("underline ");
|
||||
}
|
||||
if mod_val.contains(Modifier::REVERSED) {
|
||||
mod_str.push_str("inverse ");
|
||||
}
|
||||
|
||||
// Constructing the final style string
|
||||
let style_str = format!("{}{} {}", mod_str, fg_str, bg_str).trim().to_string();
|
||||
|
||||
serializer.serialize_str(&style_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn color_to_string(color: Color) -> String {
|
||||
match color {
|
||||
Color::Black => "black".to_string(),
|
||||
Color::Red => "red".to_string(),
|
||||
Color::Green => "green".to_string(),
|
||||
Color::Reset => "reset".to_string(),
|
||||
Color::Yellow => "yellow".to_string(),
|
||||
Color::Blue => "blue".to_string(),
|
||||
Color::Magenta => "magenta".to_string(),
|
||||
Color::Cyan => "cyan".to_string(),
|
||||
Color::Gray => "gray".to_string(),
|
||||
Color::DarkGray => "darkgray".to_string(),
|
||||
Color::LightRed => "lightred".to_string(),
|
||||
Color::LightGreen => "lightgreen".to_string(),
|
||||
Color::LightYellow => "lightyellow".to_string(),
|
||||
Color::LightBlue => "lightblue".to_string(),
|
||||
Color::LightMagenta => "lightmagenta".to_string(),
|
||||
Color::LightCyan => "lightcyan".to_string(),
|
||||
Color::White => "white".to_string(),
|
||||
Color::Rgb(r, g, b) => format!("#{}{}{}", r, g, b),
|
||||
Color::Indexed(u) => format!("#{}", u),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub tick_rate: usize,
|
||||
pub keymap: HashMap<String, KeyMap>,
|
||||
pub enabled: bool,
|
||||
pub color: HashMap<String, SerdeStyle>,
|
||||
pub filter: String,
|
||||
pub data_location: String,
|
||||
pub obfuscate: bool,
|
||||
pub print_empty_columns: bool,
|
||||
pub due: usize,
|
||||
pub weekstart: bool,
|
||||
pub rule_precedence_color: Vec<String>,
|
||||
pub uda_priority_values: Vec<String>,
|
||||
pub uda_tick_rate: u64,
|
||||
pub uda_auto_insert_double_quotes_on_add: bool,
|
||||
pub uda_auto_insert_double_quotes_on_annotate: bool,
|
||||
pub uda_auto_insert_double_quotes_on_log: bool,
|
||||
pub uda_prefill_task_metadata: bool,
|
||||
pub uda_reset_filter_on_esc: bool,
|
||||
pub uda_task_detail_prefetch: usize,
|
||||
pub uda_task_report_use_all_tasks_for_completion: bool,
|
||||
pub uda_task_report_show_info: bool,
|
||||
pub uda_task_report_looping: bool,
|
||||
pub uda_task_report_jump_to_task_on_add: bool,
|
||||
pub uda_selection_indicator: String,
|
||||
pub uda_mark_indicator: String,
|
||||
pub uda_unmark_indicator: String,
|
||||
pub uda_scrollbar_indicator: String,
|
||||
pub uda_scrollbar_area: String,
|
||||
pub uda_style_report_scrollbar: SerdeStyle,
|
||||
pub uda_style_report_scrollbar_area: SerdeStyle,
|
||||
pub uda_selection_bold: bool,
|
||||
pub uda_selection_italic: bool,
|
||||
pub uda_selection_dim: bool,
|
||||
pub uda_selection_blink: bool,
|
||||
pub uda_selection_reverse: bool,
|
||||
pub uda_calendar_months_per_row: usize,
|
||||
pub uda_style_context_active: SerdeStyle,
|
||||
pub uda_style_report_selection: SerdeStyle,
|
||||
pub uda_style_calendar_title: SerdeStyle,
|
||||
pub uda_style_calendar_today: SerdeStyle,
|
||||
pub uda_style_navbar: SerdeStyle,
|
||||
pub uda_style_command: SerdeStyle,
|
||||
pub uda_style_report_completion_pane: SerdeStyle,
|
||||
pub uda_style_report_completion_pane_highlight: SerdeStyle,
|
||||
pub uda_shortcuts: Vec<String>,
|
||||
pub uda_change_focus_rotate: bool,
|
||||
pub uda_background_process: String,
|
||||
pub uda_background_process_period: usize,
|
||||
pub uda_quick_tag_name: String,
|
||||
pub uda_task_report_prompt_on_undo: bool,
|
||||
pub uda_task_report_prompt_on_delete: bool,
|
||||
pub uda_task_report_prompt_on_done: bool,
|
||||
pub uda_task_report_date_time_vague_more_precise: bool,
|
||||
pub uda_context_menu_select_on_move: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let tick_rate = 250;
|
||||
|
||||
let mut task_report_keymap: KeyMap = Default::default();
|
||||
task_report_keymap.insert(parse_key_sequence("q").unwrap(), Action::Quit);
|
||||
task_report_keymap.insert(parse_key_sequence("r").unwrap(), Action::Refresh);
|
||||
task_report_keymap.insert(parse_key_sequence("G").unwrap(), Action::GotoBottom);
|
||||
task_report_keymap.insert(parse_key_sequence("<g><g>").unwrap(), Action::GotoTop);
|
||||
task_report_keymap.insert(parse_key_sequence("<g><j>").unwrap(), Action::GotoPageBottom);
|
||||
task_report_keymap.insert(parse_key_sequence("<g><k>").unwrap(), Action::GotoPageTop);
|
||||
task_report_keymap.insert(parse_key_sequence("<g><G>").unwrap(), Action::GotoBottom);
|
||||
task_report_keymap.insert(parse_key_sequence("j").unwrap(), Action::Down);
|
||||
task_report_keymap.insert(parse_key_sequence("k").unwrap(), Action::Up);
|
||||
task_report_keymap.insert(parse_key_sequence("J").unwrap(), Action::PageDown);
|
||||
task_report_keymap.insert(parse_key_sequence("K").unwrap(), Action::PageUp);
|
||||
task_report_keymap.insert(parse_key_sequence("<d><d>").unwrap(), Action::Delete);
|
||||
task_report_keymap.insert(parse_key_sequence("<x><x>").unwrap(), Action::Done);
|
||||
task_report_keymap.insert(parse_key_sequence("s").unwrap(), Action::ToggleStartStop);
|
||||
task_report_keymap.insert(parse_key_sequence("t").unwrap(), Action::QuickTag);
|
||||
task_report_keymap.insert(parse_key_sequence("v").unwrap(), Action::Select);
|
||||
task_report_keymap.insert(parse_key_sequence("V").unwrap(), Action::SelectAll);
|
||||
task_report_keymap.insert(parse_key_sequence("u").unwrap(), Action::Undo);
|
||||
task_report_keymap.insert(parse_key_sequence("e").unwrap(), Action::Edit);
|
||||
task_report_keymap.insert(parse_key_sequence("m").unwrap(), Action::Modify);
|
||||
task_report_keymap.insert(parse_key_sequence("!").unwrap(), Action::Shell);
|
||||
task_report_keymap.insert(parse_key_sequence("l").unwrap(), Action::Log);
|
||||
task_report_keymap.insert(parse_key_sequence("a").unwrap(), Action::Add);
|
||||
task_report_keymap.insert(parse_key_sequence("A").unwrap(), Action::Annotate);
|
||||
task_report_keymap.insert(parse_key_sequence("?").unwrap(), Action::Help);
|
||||
task_report_keymap.insert(parse_key_sequence("/").unwrap(), Action::Filter);
|
||||
task_report_keymap.insert(parse_key_sequence("z").unwrap(), Action::ToggleZoom);
|
||||
task_report_keymap.insert(parse_key_sequence("c").unwrap(), Action::Context);
|
||||
task_report_keymap.insert(parse_key_sequence("]").unwrap(), Action::Next);
|
||||
task_report_keymap.insert(parse_key_sequence("[").unwrap(), Action::Previous);
|
||||
task_report_keymap.insert(parse_key_sequence("1").unwrap(), Action::Shortcut(1));
|
||||
task_report_keymap.insert(parse_key_sequence("2").unwrap(), Action::Shortcut(2));
|
||||
task_report_keymap.insert(parse_key_sequence("3").unwrap(), Action::Shortcut(3));
|
||||
task_report_keymap.insert(parse_key_sequence("4").unwrap(), Action::Shortcut(4));
|
||||
task_report_keymap.insert(parse_key_sequence("5").unwrap(), Action::Shortcut(5));
|
||||
task_report_keymap.insert(parse_key_sequence("6").unwrap(), Action::Shortcut(6));
|
||||
task_report_keymap.insert(parse_key_sequence("7").unwrap(), Action::Shortcut(7));
|
||||
task_report_keymap.insert(parse_key_sequence("8").unwrap(), Action::Shortcut(8));
|
||||
task_report_keymap.insert(parse_key_sequence("9").unwrap(), Action::Shortcut(9));
|
||||
|
||||
let mut keymap: HashMap<String, KeyMap> = Default::default();
|
||||
keymap.insert("task-report".into(), task_report_keymap);
|
||||
|
||||
let enabled = true;
|
||||
let color = Default::default();
|
||||
let filter = Default::default();
|
||||
let data_location = Default::default();
|
||||
let obfuscate = false;
|
||||
let print_empty_columns = false;
|
||||
let due = 7; // due 7 days
|
||||
let weekstart = true; // starts on monday
|
||||
let rule_precedence_color = Default::default();
|
||||
let uda_priority_values = Default::default();
|
||||
let uda_tick_rate = 250;
|
||||
let uda_auto_insert_double_quotes_on_add = true;
|
||||
let uda_auto_insert_double_quotes_on_annotate = true;
|
||||
let uda_auto_insert_double_quotes_on_log = true;
|
||||
let uda_prefill_task_metadata = Default::default();
|
||||
let uda_reset_filter_on_esc = true;
|
||||
let uda_task_detail_prefetch = 10;
|
||||
let uda_task_report_use_all_tasks_for_completion = Default::default();
|
||||
let uda_task_report_show_info = true;
|
||||
let uda_task_report_looping = true;
|
||||
let uda_task_report_jump_to_task_on_add = true;
|
||||
let uda_selection_indicator = "\u{2022} ".to_string();
|
||||
let uda_mark_indicator = "\u{2714} ".to_string();
|
||||
let uda_unmark_indicator = " ".to_string();
|
||||
let uda_scrollbar_indicator = "█".to_string();
|
||||
let uda_scrollbar_area = "║".to_string();
|
||||
let uda_style_report_scrollbar = Default::default();
|
||||
let uda_style_report_scrollbar_area = Default::default();
|
||||
let uda_selection_bold = true;
|
||||
let uda_selection_italic = Default::default();
|
||||
let uda_selection_dim = Default::default();
|
||||
let uda_selection_blink = Default::default();
|
||||
let uda_selection_reverse = Default::default();
|
||||
let uda_calendar_months_per_row = 4;
|
||||
let uda_style_context_active = Default::default();
|
||||
let uda_style_report_selection = Default::default();
|
||||
let uda_style_calendar_title = Default::default();
|
||||
let uda_style_calendar_today = Default::default();
|
||||
let uda_style_navbar = Default::default();
|
||||
let uda_style_command = Default::default();
|
||||
let uda_style_report_completion_pane = Default::default();
|
||||
let uda_style_report_completion_pane_highlight = Default::default();
|
||||
let uda_shortcuts = Default::default();
|
||||
let uda_change_focus_rotate = Default::default();
|
||||
let uda_background_process = Default::default();
|
||||
let uda_background_process_period = Default::default();
|
||||
let uda_quick_tag_name = Default::default();
|
||||
let uda_task_report_prompt_on_undo = Default::default();
|
||||
let uda_task_report_prompt_on_delete = Default::default();
|
||||
let uda_task_report_prompt_on_done = Default::default();
|
||||
let uda_task_report_date_time_vague_more_precise = Default::default();
|
||||
let uda_context_menu_select_on_move = Default::default();
|
||||
|
||||
Self {
|
||||
tick_rate,
|
||||
keymap,
|
||||
enabled,
|
||||
color,
|
||||
filter,
|
||||
data_location,
|
||||
obfuscate,
|
||||
print_empty_columns,
|
||||
due,
|
||||
weekstart,
|
||||
rule_precedence_color,
|
||||
uda_priority_values,
|
||||
uda_tick_rate,
|
||||
uda_auto_insert_double_quotes_on_add,
|
||||
uda_auto_insert_double_quotes_on_annotate,
|
||||
uda_auto_insert_double_quotes_on_log,
|
||||
uda_prefill_task_metadata,
|
||||
uda_reset_filter_on_esc,
|
||||
uda_task_detail_prefetch,
|
||||
uda_task_report_use_all_tasks_for_completion,
|
||||
uda_task_report_show_info,
|
||||
uda_task_report_looping,
|
||||
uda_task_report_jump_to_task_on_add,
|
||||
uda_selection_indicator,
|
||||
uda_mark_indicator,
|
||||
uda_unmark_indicator,
|
||||
uda_scrollbar_indicator,
|
||||
uda_scrollbar_area,
|
||||
uda_style_report_scrollbar,
|
||||
uda_style_report_scrollbar_area,
|
||||
uda_selection_bold,
|
||||
uda_selection_italic,
|
||||
uda_selection_dim,
|
||||
uda_selection_blink,
|
||||
uda_selection_reverse,
|
||||
uda_calendar_months_per_row,
|
||||
uda_style_context_active,
|
||||
uda_style_report_selection,
|
||||
uda_style_calendar_title,
|
||||
uda_style_calendar_today,
|
||||
uda_style_navbar,
|
||||
uda_style_command,
|
||||
uda_style_report_completion_pane,
|
||||
uda_style_report_completion_pane_highlight,
|
||||
uda_shortcuts,
|
||||
uda_change_focus_rotate,
|
||||
uda_background_process,
|
||||
uda_background_process_period,
|
||||
uda_quick_tag_name,
|
||||
uda_task_report_prompt_on_undo,
|
||||
uda_task_report_prompt_on_delete,
|
||||
uda_task_report_prompt_on_done,
|
||||
uda_task_report_date_time_vague_more_precise,
|
||||
uda_context_menu_select_on_move,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self> {
|
||||
let config: Self = Figment::from(Serialized::defaults(Config::default()))
|
||||
.merge(Toml::file(get_config_dir().join("config.toml")))
|
||||
.merge(Env::prefixed("TASKWARRIOR_TUI_"))
|
||||
.extract()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn write(&self, path: PathBuf) -> Result<()> {
|
||||
let content = toml::to_string(&self)?;
|
||||
std::fs::write(&path, content)?;
|
||||
std::fs::File::open(&path)?.sync_data()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_read_config() {
|
||||
let config = Config::new().unwrap();
|
||||
dbg!(config);
|
||||
fn test_parse_style_default() {
|
||||
let style = parse_style("");
|
||||
assert_eq!(style, Style::default());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_write_config() {
|
||||
// let config: Config = Default::default();
|
||||
// config.write("tests/data/test.toml".into()).unwrap();
|
||||
// }
|
||||
#[test]
|
||||
fn test_parse_style_foreground() {
|
||||
let style = parse_style("red");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_background() {
|
||||
let style = parse_style("on blue");
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_modifiers() {
|
||||
let style = parse_style("underline red on blue");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_color_string() {
|
||||
let (color, modifiers) = process_color_string("underline bold inverse gray");
|
||||
assert_eq!(color, "gray");
|
||||
assert!(modifiers.contains(Modifier::UNDERLINED));
|
||||
assert!(modifiers.contains(Modifier::BOLD));
|
||||
assert!(modifiers.contains(Modifier::REVERSED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_rgb() {
|
||||
let color = parse_color("rgb123");
|
||||
let expected = 16 + 1 * 36 + 2 * 6 + 3;
|
||||
assert_eq!(color, Some(Color::Indexed(expected)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_unknown() {
|
||||
let color = parse_color("unknown");
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config() -> Result<()> {
|
||||
let c = Config::new()?;
|
||||
assert_eq!(
|
||||
c.keybindings.get(&Mode::TaskReport).unwrap().get(&parse_key_sequence("<q>").unwrap_or_default()).unwrap(),
|
||||
&Command::Quit
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_keys() {
|
||||
assert_eq!(parse_key_event("a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(parse_key_event("enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_modifiers() {
|
||||
assert_eq!(parse_key_event("ctrl-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL));
|
||||
|
||||
assert_eq!(parse_key_event("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
|
||||
|
||||
assert_eq!(parse_key_event("shift-esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-alt-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-shift-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
key_event_to_string(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)),
|
||||
"ctrl-alt-a".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_keys() {
|
||||
assert!(parse_key_event("invalid-key").is_err());
|
||||
assert!(parse_key_event("ctrl-invalid-key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitivity() {
|
||||
assert_eq!(parse_key_event("CTRL-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL));
|
||||
|
||||
assert_eq!(parse_key_event("AlT-eNtEr").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
|
49
src/help.rs
49
src/help.rs
|
@ -1,49 +0,0 @@
|
|||
use std::cmp;
|
||||
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const TEXT: &str = include_str!("../docs/keybindings.md");
|
||||
|
||||
pub struct Help {
|
||||
pub title: String,
|
||||
pub scroll: u16,
|
||||
pub text_height: usize,
|
||||
}
|
||||
|
||||
impl Help {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: "Help".to_string(),
|
||||
scroll: 0,
|
||||
text_height: TEXT.lines().count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Help {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Help {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let text: Vec<Line> = TEXT.lines().map(|line| Line::from(format!("{}\n", line))).collect();
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(&self.title, Style::default().add_modifier(Modifier::BOLD)))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded),
|
||||
)
|
||||
.alignment(Alignment::Left)
|
||||
.scroll((self.scroll, 0))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
132
src/history.rs
132
src/history.rs
|
@ -1,132 +0,0 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use color_eyre::eyre::{anyhow, Result};
|
||||
use rustyline::{
|
||||
error::ReadlineError,
|
||||
history::{DefaultHistory, History, SearchDirection},
|
||||
};
|
||||
|
||||
pub struct HistoryContext {
|
||||
history: DefaultHistory,
|
||||
history_index: Option<usize>,
|
||||
data_path: PathBuf,
|
||||
}
|
||||
|
||||
impl HistoryContext {
|
||||
pub fn new(filename: &str, data_path: PathBuf) -> Self {
|
||||
let history = DefaultHistory::new();
|
||||
|
||||
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);
|
||||
|
||||
Self {
|
||||
history,
|
||||
history_index: None,
|
||||
data_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<()> {
|
||||
if self.data_path.exists() {
|
||||
self.history.load(&self.data_path)?;
|
||||
} else {
|
||||
self.history.save(&self.data_path)?;
|
||||
}
|
||||
self.history_index = None;
|
||||
log::debug!("Loading history of length {}", self.history.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(&mut self) -> Result<()> {
|
||||
self.history.save(&self.data_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn history(&self) -> &DefaultHistory {
|
||||
&self.history
|
||||
}
|
||||
|
||||
pub fn history_index(&self) -> Option<usize> {
|
||||
self.history_index
|
||||
}
|
||||
|
||||
pub fn history_search(&mut self, buf: &str, dir: SearchDirection) -> Option<String> {
|
||||
log::debug!(
|
||||
"Searching history for {:?} in direction {:?} with history index = {:?} and history len = {:?}",
|
||||
buf,
|
||||
dir,
|
||||
self.history_index(),
|
||||
self.history.len(),
|
||||
);
|
||||
|
||||
if self.history.is_empty() {
|
||||
log::debug!("History is empty");
|
||||
return None;
|
||||
}
|
||||
|
||||
let history_index = if self.history_index().is_none() {
|
||||
log::debug!("History index is none");
|
||||
match dir {
|
||||
SearchDirection::Forward => return None,
|
||||
SearchDirection::Reverse => self.history_index = Some(self.history_len().saturating_sub(1)),
|
||||
}
|
||||
self.history_index.unwrap()
|
||||
} else {
|
||||
let hi = self.history_index().unwrap();
|
||||
|
||||
if hi == self.history.len().saturating_sub(1) && dir == SearchDirection::Forward
|
||||
|| hi == 0 && dir == SearchDirection::Reverse
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match dir {
|
||||
SearchDirection::Reverse => hi.saturating_sub(1),
|
||||
SearchDirection::Forward => hi.saturating_add(1).min(self.history_len().saturating_sub(1)),
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("Using history index = {} for searching", history_index);
|
||||
return if let Some(history_index) = self.history.starts_with(buf, history_index, dir).unwrap() {
|
||||
log::debug!("Found index {:?}", history_index);
|
||||
log::debug!("Previous index {:?}", self.history_index);
|
||||
self.history_index = Some(history_index.idx);
|
||||
Some(history_index.entry.to_string())
|
||||
} else if buf.is_empty() {
|
||||
self.history_index = Some(history_index);
|
||||
Some(
|
||||
self
|
||||
.history
|
||||
.get(history_index, SearchDirection::Forward)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.entry
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
log::debug!("History index = {}. Found no match.", history_index);
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add(&mut self, buf: &str) {
|
||||
if let Ok(x) = self.history.add(buf) {
|
||||
if x {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.history_index = None
|
||||
}
|
||||
|
||||
pub fn history_len(&self) -> usize {
|
||||
self.history.len()
|
||||
}
|
||||
}
|
231
src/keyconfig.rs
231
src/keyconfig.rs
|
@ -1,231 +0,0 @@
|
|||
use std::{collections::HashSet, error::Error, hash::Hash};
|
||||
|
||||
use color_eyre::eyre::{anyhow, Result};
|
||||
use crossterm::event::KeyCode;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct KeyConfig {
|
||||
pub quit: KeyCode,
|
||||
pub refresh: KeyCode,
|
||||
pub go_to_bottom: KeyCode,
|
||||
pub go_to_top: KeyCode,
|
||||
pub down: KeyCode,
|
||||
pub up: KeyCode,
|
||||
pub page_down: KeyCode,
|
||||
pub page_up: KeyCode,
|
||||
pub delete: KeyCode,
|
||||
pub done: KeyCode,
|
||||
pub start_stop: KeyCode,
|
||||
pub quick_tag: KeyCode,
|
||||
pub select: KeyCode,
|
||||
pub select_all: KeyCode,
|
||||
pub undo: KeyCode,
|
||||
pub edit: KeyCode,
|
||||
pub modify: KeyCode,
|
||||
pub shell: KeyCode,
|
||||
pub log: KeyCode,
|
||||
pub add: KeyCode,
|
||||
pub annotate: KeyCode,
|
||||
pub help: KeyCode,
|
||||
pub filter: KeyCode,
|
||||
pub zoom: KeyCode,
|
||||
pub context_menu: KeyCode,
|
||||
pub next_tab: KeyCode,
|
||||
pub previous_tab: KeyCode,
|
||||
pub shortcut0: KeyCode,
|
||||
pub shortcut1: KeyCode,
|
||||
pub shortcut2: KeyCode,
|
||||
pub shortcut3: KeyCode,
|
||||
pub shortcut4: KeyCode,
|
||||
pub shortcut5: KeyCode,
|
||||
pub shortcut6: KeyCode,
|
||||
pub shortcut7: KeyCode,
|
||||
pub shortcut8: KeyCode,
|
||||
pub shortcut9: KeyCode,
|
||||
}
|
||||
|
||||
impl Default for KeyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
quit: KeyCode::Char('q'),
|
||||
refresh: KeyCode::Char('r'),
|
||||
go_to_bottom: KeyCode::Char('G'),
|
||||
go_to_top: KeyCode::Char('g'),
|
||||
down: KeyCode::Char('j'),
|
||||
up: KeyCode::Char('k'),
|
||||
page_down: KeyCode::Char('J'),
|
||||
page_up: KeyCode::Char('K'),
|
||||
delete: KeyCode::Char('x'),
|
||||
done: KeyCode::Char('d'),
|
||||
start_stop: KeyCode::Char('s'),
|
||||
quick_tag: KeyCode::Char('t'),
|
||||
select: KeyCode::Char('v'),
|
||||
select_all: KeyCode::Char('V'),
|
||||
undo: KeyCode::Char('u'),
|
||||
edit: KeyCode::Char('e'),
|
||||
modify: KeyCode::Char('m'),
|
||||
shell: KeyCode::Char('!'),
|
||||
log: KeyCode::Char('l'),
|
||||
add: KeyCode::Char('a'),
|
||||
annotate: KeyCode::Char('A'),
|
||||
help: KeyCode::Char('?'),
|
||||
filter: KeyCode::Char('/'),
|
||||
zoom: KeyCode::Char('z'),
|
||||
context_menu: KeyCode::Char('c'),
|
||||
next_tab: KeyCode::Char(']'),
|
||||
previous_tab: KeyCode::Char('['),
|
||||
shortcut0: KeyCode::Char('0'),
|
||||
shortcut1: KeyCode::Char('1'),
|
||||
shortcut2: KeyCode::Char('2'),
|
||||
shortcut3: KeyCode::Char('3'),
|
||||
shortcut4: KeyCode::Char('4'),
|
||||
shortcut5: KeyCode::Char('5'),
|
||||
shortcut6: KeyCode::Char('6'),
|
||||
shortcut7: KeyCode::Char('7'),
|
||||
shortcut8: KeyCode::Char('8'),
|
||||
shortcut9: KeyCode::Char('9'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyConfig {
|
||||
pub fn new(data: &str) -> Result<Self> {
|
||||
let mut kc = Self::default();
|
||||
kc.update(data)?;
|
||||
Ok(kc)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, data: &str) -> Result<()> {
|
||||
let quit = Self::get_config("uda.taskwarrior-tui.keyconfig.quit", data);
|
||||
let refresh = Self::get_config("uda.taskwarrior-tui.keyconfig.refresh", data);
|
||||
let go_to_bottom = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-bottom", data);
|
||||
let go_to_top = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-top", data);
|
||||
let down = Self::get_config("uda.taskwarrior-tui.keyconfig.down", data);
|
||||
let up = Self::get_config("uda.taskwarrior-tui.keyconfig.up", data);
|
||||
let page_down = Self::get_config("uda.taskwarrior-tui.keyconfig.page-down", data);
|
||||
let page_up = Self::get_config("uda.taskwarrior-tui.keyconfig.page-up", data);
|
||||
let delete = Self::get_config("uda.taskwarrior-tui.keyconfig.delete", data);
|
||||
let done = Self::get_config("uda.taskwarrior-tui.keyconfig.done", data);
|
||||
let start_stop = Self::get_config("uda.taskwarrior-tui.keyconfig.start-stop", data);
|
||||
let quick_tag = Self::get_config("uda.taskwarrior-tui.keyconfig.quick-tag", data);
|
||||
let select = Self::get_config("uda.taskwarrior-tui.keyconfig.select", data);
|
||||
let select_all = Self::get_config("uda.taskwarrior-tui.keyconfig.select-all", data);
|
||||
let undo = Self::get_config("uda.taskwarrior-tui.keyconfig.undo", data);
|
||||
let edit = Self::get_config("uda.taskwarrior-tui.keyconfig.edit", data);
|
||||
let modify = Self::get_config("uda.taskwarrior-tui.keyconfig.modify", data);
|
||||
let shell = Self::get_config("uda.taskwarrior-tui.keyconfig.shell", data);
|
||||
let log = Self::get_config("uda.taskwarrior-tui.keyconfig.log", data);
|
||||
let add = Self::get_config("uda.taskwarrior-tui.keyconfig.add", data);
|
||||
let annotate = Self::get_config("uda.taskwarrior-tui.keyconfig.annotate", data);
|
||||
let filter = Self::get_config("uda.taskwarrior-tui.keyconfig.filter", data);
|
||||
let zoom = Self::get_config("uda.taskwarrior-tui.keyconfig.zoom", data);
|
||||
let context_menu = Self::get_config("uda.taskwarrior-tui.keyconfig.context-menu", data);
|
||||
let next_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.next-tab", data);
|
||||
let previous_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.previous-tab", data);
|
||||
|
||||
self.quit = quit.unwrap_or(self.quit);
|
||||
self.refresh = refresh.unwrap_or(self.refresh);
|
||||
self.go_to_bottom = go_to_bottom.unwrap_or(self.go_to_bottom);
|
||||
self.go_to_top = go_to_top.unwrap_or(self.go_to_top);
|
||||
self.down = down.unwrap_or(self.down);
|
||||
self.up = up.unwrap_or(self.up);
|
||||
self.page_down = page_down.unwrap_or(self.page_down);
|
||||
self.page_up = page_up.unwrap_or(self.page_up);
|
||||
self.delete = delete.unwrap_or(self.delete);
|
||||
self.done = done.unwrap_or(self.done);
|
||||
self.start_stop = start_stop.unwrap_or(self.start_stop);
|
||||
self.quick_tag = quick_tag.unwrap_or(self.quick_tag);
|
||||
self.select = select.unwrap_or(self.select);
|
||||
self.select_all = select_all.unwrap_or(self.select_all);
|
||||
self.undo = undo.unwrap_or(self.undo);
|
||||
self.edit = edit.unwrap_or(self.edit);
|
||||
self.modify = modify.unwrap_or(self.modify);
|
||||
self.shell = shell.unwrap_or(self.shell);
|
||||
self.log = log.unwrap_or(self.log);
|
||||
self.add = add.unwrap_or(self.add);
|
||||
self.annotate = annotate.unwrap_or(self.annotate);
|
||||
self.filter = filter.unwrap_or(self.filter);
|
||||
self.zoom = zoom.unwrap_or(self.zoom);
|
||||
self.context_menu = context_menu.unwrap_or(self.context_menu);
|
||||
self.next_tab = next_tab.unwrap_or(self.next_tab);
|
||||
self.previous_tab = previous_tab.unwrap_or(self.previous_tab);
|
||||
|
||||
self.check()
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Result<()> {
|
||||
let mut elements = vec![
|
||||
&self.quit,
|
||||
&self.refresh,
|
||||
&self.go_to_bottom,
|
||||
&self.go_to_top,
|
||||
&self.down,
|
||||
&self.up,
|
||||
&self.page_down,
|
||||
&self.page_up,
|
||||
&self.delete,
|
||||
&self.done,
|
||||
&self.select,
|
||||
&self.select_all,
|
||||
&self.start_stop,
|
||||
&self.quick_tag,
|
||||
&self.undo,
|
||||
&self.edit,
|
||||
&self.modify,
|
||||
&self.shell,
|
||||
&self.log,
|
||||
&self.add,
|
||||
&self.annotate,
|
||||
&self.help,
|
||||
&self.filter,
|
||||
&self.zoom,
|
||||
&self.context_menu,
|
||||
&self.next_tab,
|
||||
&self.previous_tab,
|
||||
];
|
||||
let l = elements.len();
|
||||
elements.dedup();
|
||||
if l == elements.len() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Duplicate keys found in key config"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config(config: &str, data: &str) -> Option<KeyCode> {
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with(config) {
|
||||
let line = line.trim_start_matches(config).trim_start().trim_end().to_string();
|
||||
if has_just_one_char(&line) {
|
||||
return Some(KeyCode::Char(line.chars().next().unwrap()));
|
||||
} else {
|
||||
error!("Found multiple characters in {} for {}", line, config);
|
||||
}
|
||||
} else if line.starts_with(&config.replace('-', "_")) {
|
||||
let line = line
|
||||
.trim_start_matches(&config.replace('-', "_"))
|
||||
.trim_start()
|
||||
.trim_end()
|
||||
.to_string();
|
||||
if has_just_one_char(&line) {
|
||||
return Some(KeyCode::Char(line.chars().next().unwrap()));
|
||||
} else {
|
||||
error!("Found multiple characters in {} for {}", line, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn has_just_one_char(s: &str) -> bool {
|
||||
let mut chars = s.chars();
|
||||
chars.next().is_some() && chars.next().is_none()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
}
|
307
src/keyevent.rs
307
src/keyevent.rs
|
@ -1,307 +0,0 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode, ModifierKeyCode};
|
||||
|
||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
}
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
}
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
}
|
||||
_ => break, // break out of the loop if no known prefix is detected
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
}
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
}
|
||||
_ => return Err(format!("Unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
|
||||
return Err(format!("Unable to parse `{}`", raw));
|
||||
}
|
||||
let raw = if !raw.contains("><") {
|
||||
let raw = raw.strip_prefix("<").unwrap_or(raw);
|
||||
let raw = raw.strip_prefix(">").unwrap_or(raw);
|
||||
raw
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let sequences = raw
|
||||
.split("><")
|
||||
.map(|seq| {
|
||||
if seq.starts_with('<') {
|
||||
&seq[1..]
|
||||
} else if seq.ends_with('>') {
|
||||
&seq[..seq.len() - 1]
|
||||
} else {
|
||||
seq
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sequences.into_iter().map(parse_key_event).collect()
|
||||
}
|
||||
|
||||
pub fn key_event_to_string(event: KeyEvent) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
result.push('<');
|
||||
|
||||
// Add modifiers
|
||||
if event.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if event.modifiers.contains(KeyModifiers::ALT) {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if event.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
|
||||
match event.code {
|
||||
KeyCode::Char(' ') => result.push_str("space"),
|
||||
KeyCode::Char(c) => result.push(c),
|
||||
KeyCode::Enter => result.push_str("enter"),
|
||||
KeyCode::Esc => result.push_str("esc"),
|
||||
KeyCode::Left => result.push_str("left"),
|
||||
KeyCode::Right => result.push_str("right"),
|
||||
KeyCode::Up => result.push_str("up"),
|
||||
KeyCode::Down => result.push_str("down"),
|
||||
KeyCode::Home => result.push_str("home"),
|
||||
KeyCode::End => result.push_str("end"),
|
||||
KeyCode::PageUp => result.push_str("pageup"),
|
||||
KeyCode::PageDown => result.push_str("pagedown"),
|
||||
KeyCode::BackTab => result.push_str("backtab"),
|
||||
KeyCode::Delete => result.push_str("delete"),
|
||||
KeyCode::Insert => result.push_str("insert"),
|
||||
KeyCode::F(n) => result.push_str(&format!("f{}", n)),
|
||||
KeyCode::Backspace => result.push_str("backspace"),
|
||||
KeyCode::Tab => result.push_str("tab"),
|
||||
KeyCode::Null => result.push_str("null"),
|
||||
KeyCode::CapsLock => result.push_str("capslock"),
|
||||
KeyCode::ScrollLock => result.push_str("scrolllock"),
|
||||
KeyCode::NumLock => result.push_str("numlock"),
|
||||
KeyCode::PrintScreen => result.push_str("printscreen"),
|
||||
KeyCode::Pause => result.push_str("pause"),
|
||||
KeyCode::Menu => result.push_str("menu"),
|
||||
KeyCode::KeypadBegin => result.push_str("keypadbegin"),
|
||||
KeyCode::Media(media) => match media {
|
||||
MediaKeyCode::Play => result.push_str("play"),
|
||||
MediaKeyCode::Pause => result.push_str("pause"),
|
||||
MediaKeyCode::PlayPause => result.push_str("playpause"),
|
||||
MediaKeyCode::Reverse => result.push_str("reverse"),
|
||||
MediaKeyCode::Stop => result.push_str("stop"),
|
||||
MediaKeyCode::FastForward => result.push_str("fastforward"),
|
||||
MediaKeyCode::Rewind => result.push_str("rewind"),
|
||||
MediaKeyCode::TrackNext => result.push_str("tracknext"),
|
||||
MediaKeyCode::TrackPrevious => result.push_str("trackprevious"),
|
||||
MediaKeyCode::Record => result.push_str("record"),
|
||||
MediaKeyCode::LowerVolume => result.push_str("lowervolume"),
|
||||
MediaKeyCode::RaiseVolume => result.push_str("raisevolume"),
|
||||
MediaKeyCode::MuteVolume => result.push_str("mutevolume"),
|
||||
},
|
||||
KeyCode::Modifier(keycode) => match keycode {
|
||||
ModifierKeyCode::LeftShift => result.push_str("leftshift"),
|
||||
ModifierKeyCode::LeftControl => result.push_str("leftcontrol"),
|
||||
ModifierKeyCode::LeftAlt => result.push_str("leftalt"),
|
||||
ModifierKeyCode::LeftSuper => result.push_str("leftsuper"),
|
||||
ModifierKeyCode::LeftHyper => result.push_str("lefthyper"),
|
||||
ModifierKeyCode::LeftMeta => result.push_str("leftmeta"),
|
||||
ModifierKeyCode::RightShift => result.push_str("rightshift"),
|
||||
ModifierKeyCode::RightControl => result.push_str("rightcontrol"),
|
||||
ModifierKeyCode::RightAlt => result.push_str("rightalt"),
|
||||
ModifierKeyCode::RightSuper => result.push_str("rightsuper"),
|
||||
ModifierKeyCode::RightHyper => result.push_str("righthyper"),
|
||||
ModifierKeyCode::RightMeta => result.push_str("rightmeta"),
|
||||
ModifierKeyCode::IsoLevel3Shift => result.push_str("isolevel3shift"),
|
||||
ModifierKeyCode::IsoLevel5Shift => result.push_str("isolevel5shift"),
|
||||
},
|
||||
}
|
||||
|
||||
result.push('>');
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_event_to_string() {
|
||||
let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
|
||||
println!("{}", key_event_to_string(event)); // Outputs: ctrl-a
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_key_sequence() {
|
||||
let result = parse_key_sequence("a");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())]
|
||||
);
|
||||
|
||||
let result = parse_key_sequence("<a><b>");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())
|
||||
]
|
||||
);
|
||||
|
||||
let result = parse_key_sequence("<Ctrl-a><Alt-b>");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT)
|
||||
]
|
||||
);
|
||||
let result = parse_key_sequence("<Ctrl-a>");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),]
|
||||
);
|
||||
let result = parse_key_sequence("<Ctrl-Alt-a>");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
vec![KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
),]
|
||||
);
|
||||
assert!(parse_key_sequence("Ctrl-a>").is_err());
|
||||
assert!(parse_key_sequence("<Ctrl-a").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_keys() {
|
||||
assert_eq!(
|
||||
parse_key_event("a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("alt-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("shift-esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-alt-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-shift-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_keys() {
|
||||
assert!(parse_key_event("invalid-key").is_err());
|
||||
assert!(parse_key_event("ctrl-invalid-key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitivity() {
|
||||
assert_eq!(
|
||||
parse_key_event("CTRL-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("AlT-eNtEr").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
}
|
||||
}
|
220
src/keymap.rs
220
src/keymap.rs
|
@ -1,220 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
ops::{Deref, DerefMut},
|
||||
str,
|
||||
};
|
||||
|
||||
use color_eyre::eyre::{eyre, Context, Result};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::{
|
||||
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
||||
ser::{self, Serialize, SerializeMap, Serializer},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
keyevent::{key_event_to_string, parse_key_sequence},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KeyMap(pub std::collections::HashMap<Vec<KeyEvent>, Action>);
|
||||
|
||||
impl Deref for KeyMap {
|
||||
type Target = std::collections::HashMap<Vec<KeyEvent>, Action>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for KeyMap {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMap {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
let mut sorted_sequences: Vec<_> = self.keys().collect();
|
||||
sorted_sequences.sort_by_key(|seq| seq.len());
|
||||
|
||||
for i in 0..sorted_sequences.len() {
|
||||
for j in i + 1..sorted_sequences.len() {
|
||||
if sorted_sequences[j].starts_with(sorted_sequences[i]) {
|
||||
return Err(format!(
|
||||
"Conflict detected: Sequence {:?} is a prefix of sequence {:?}",
|
||||
sorted_sequences[i], sorted_sequences[j]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for KeyMap {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Begin serializing a map.
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
|
||||
for (key_sequence, action) in &self.0 {
|
||||
let key_string = key_sequence
|
||||
.iter()
|
||||
.map(|key_event| key_event_to_string(*key_event))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
map.serialize_entry(&key_string, action)?;
|
||||
}
|
||||
|
||||
// End serialization.
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyMap {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct KeyMapVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for KeyMapVisitor {
|
||||
type Value = KeyMap;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a keymap with string representation of KeyEvent as key and Action as value")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut access: M) -> Result<KeyMap, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut keymap = std::collections::HashMap::new();
|
||||
|
||||
// While there are entries in the map, read them
|
||||
while let Some((key_sequence_str, action)) = access.next_entry::<String, Action>()? {
|
||||
let key_sequence = parse_key_sequence(&key_sequence_str).map_err(de::Error::custom)?;
|
||||
|
||||
if let Some(old_action) = keymap.insert(key_sequence, action.clone()) {
|
||||
if old_action != action {
|
||||
return Err(format!(
|
||||
"Found a {:?} for both {:?} and {:?}",
|
||||
key_sequence_str, old_action, action
|
||||
))
|
||||
.map_err(de::Error::custom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(KeyMap(keymap))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(KeyMapVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod validate_tests {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_no_conflict() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(
|
||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())],
|
||||
Action::Quit,
|
||||
);
|
||||
map.insert(
|
||||
vec![KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())],
|
||||
Action::Quit,
|
||||
);
|
||||
let keymap = KeyMap(map);
|
||||
|
||||
assert!(keymap.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflict_prefix() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(
|
||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())],
|
||||
Action::Quit,
|
||||
);
|
||||
map.insert(
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
|
||||
],
|
||||
Action::Quit,
|
||||
);
|
||||
let keymap = KeyMap(map);
|
||||
|
||||
assert!(keymap.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_conflict_different_modifiers() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(
|
||||
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)],
|
||||
Action::Quit,
|
||||
);
|
||||
map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)], Action::Quit);
|
||||
let keymap = KeyMap(map);
|
||||
|
||||
assert!(keymap.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_conflict_multiple_keys() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
|
||||
],
|
||||
Action::Quit,
|
||||
);
|
||||
map.insert(
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
],
|
||||
Action::Quit,
|
||||
);
|
||||
let keymap = KeyMap(map);
|
||||
|
||||
assert!(keymap.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflict_three_keys() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()),
|
||||
],
|
||||
Action::Quit,
|
||||
);
|
||||
map.insert(
|
||||
vec![
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
|
||||
],
|
||||
Action::Quit,
|
||||
);
|
||||
let keymap = KeyMap(map);
|
||||
|
||||
assert!(keymap.validate().is_err());
|
||||
}
|
||||
}
|
123
src/main.rs
123
src/main.rs
|
@ -1,111 +1,38 @@
|
|||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
mod action;
|
||||
mod app;
|
||||
mod calendar;
|
||||
mod cli;
|
||||
mod completion;
|
||||
mod config;
|
||||
mod help;
|
||||
mod history;
|
||||
mod keyconfig;
|
||||
mod keyevent;
|
||||
mod keymap;
|
||||
mod pane;
|
||||
mod scrollbar;
|
||||
mod table;
|
||||
mod task_report;
|
||||
mod traits;
|
||||
mod tui;
|
||||
mod ui;
|
||||
mod utils;
|
||||
pub mod cli;
|
||||
pub mod command;
|
||||
pub mod components;
|
||||
pub mod config;
|
||||
pub mod runner;
|
||||
pub mod tui;
|
||||
pub mod utils;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
io::{self, Write},
|
||||
panic,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use color_eyre::eyre::Result;
|
||||
|
||||
use crate::{
|
||||
runner::Runner,
|
||||
utils::{initialize_logging, initialize_panic_handler, version},
|
||||
};
|
||||
|
||||
// use app::{Mode, TaskwarriorTui};
|
||||
use color_eyre::eyre::Result;
|
||||
use utils::{absolute_path, get_config_dir, get_data_dir, initialize_logging, initialize_panic_handler};
|
||||
// use crossterm::{
|
||||
// cursor,
|
||||
// event::{DisableMouseCapture, EnableMouseCapture, EventStream},
|
||||
// execute,
|
||||
// terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
// };
|
||||
// use futures::stream::{FuturesUnordered, StreamExt};
|
||||
// use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
|
||||
// use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
// use utils::{get_config_dir, get_data_dir};
|
||||
async fn tokio_main() -> Result<()> {
|
||||
initialize_logging()?;
|
||||
|
||||
// use crate::{
|
||||
// action::Action,
|
||||
// keyconfig::KeyConfig,
|
||||
// utils::{initialize_logging, initialize_panic_handler},
|
||||
// };
|
||||
//
|
||||
// const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
|
||||
initialize_panic_handler()?;
|
||||
|
||||
let args = Cli::parse();
|
||||
let mut runner = Runner::new(args.tick_rate, args.frame_rate)?;
|
||||
runner.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let matches = cli::generate_cli_app().get_matches();
|
||||
|
||||
let config = matches.get_one::<String>("config");
|
||||
let data = matches.get_one::<String>("data");
|
||||
let taskrc = matches.get_one::<String>("taskrc");
|
||||
let taskdata = matches.get_one::<String>("taskdata");
|
||||
let binding = String::from("next");
|
||||
let report = matches.get_one::<String>("report").unwrap_or(&binding);
|
||||
|
||||
let config_dir = config.map(PathBuf::from).unwrap_or_else(get_config_dir);
|
||||
let data_dir = data.map(PathBuf::from).unwrap_or_else(get_data_dir);
|
||||
|
||||
if let Some(e) = taskrc {
|
||||
if env::var("TASKRC").is_err() {
|
||||
// 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 {
|
||||
log::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 {
|
||||
log::warn!("TASKDATA environment variable cannot be set.")
|
||||
}
|
||||
}
|
||||
|
||||
initialize_logging()?;
|
||||
initialize_panic_handler()?;
|
||||
|
||||
log::info!("getting matches from clap...");
|
||||
log::debug!("report = {:?}", &report);
|
||||
log::debug!("config = {:?}", &config);
|
||||
|
||||
let mut app = app::TaskwarriorTui::new(report)?;
|
||||
|
||||
let r = app.run().await;
|
||||
|
||||
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);
|
||||
std::process::exit(1);
|
||||
}
|
||||
tokio_main().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
|
||||
|
||||
const NAME: &str = "Name";
|
||||
const TYPE: &str = "Remaining";
|
||||
const DEFINITION: &str = "Avg age";
|
||||
const ACTIVE: &str = "Complete";
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
cmp::min,
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph, StatefulWidget, Widget},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
app::{Mode, TaskwarriorTui},
|
||||
pane::Pane,
|
||||
table::TableState,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextDetails {
|
||||
pub name: String,
|
||||
pub definition: String,
|
||||
pub active: String,
|
||||
pub type_: String,
|
||||
}
|
||||
|
||||
impl ContextDetails {
|
||||
pub fn new(name: String, definition: String, active: String, type_: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
definition,
|
||||
active,
|
||||
type_,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextsState {
|
||||
pub table_state: TableState,
|
||||
pub report_height: u16,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<ContextDetails>,
|
||||
}
|
||||
|
||||
impl ContextsState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
table_state: TableState::default(),
|
||||
report_height: 0,
|
||||
columns: vec![
|
||||
NAME.to_string(),
|
||||
TYPE.to_string(),
|
||||
DEFINITION.to_string(),
|
||||
ACTIVE.to_string(),
|
||||
],
|
||||
rows: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simplified_view(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
|
||||
let rows = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|c| vec![c.name.clone(), c.type_.clone(), c.definition.clone(), c.active.clone()])
|
||||
.collect();
|
||||
let headers = self.columns.clone();
|
||||
(rows, headers)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rows.len()
|
||||
}
|
||||
|
||||
pub fn update_data(&mut self) -> Result<()> {
|
||||
let output = Command::new("task").arg("context").output()?;
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
self.rows = vec![];
|
||||
for (i, line) in data.trim().split('\n').enumerate() {
|
||||
if line.starts_with(" ") && line.trim().starts_with("write") {
|
||||
continue;
|
||||
}
|
||||
if line.starts_with(" ") && !(line.trim().ends_with("yes") || line.trim().ends_with("no")) {
|
||||
let definition = line.trim();
|
||||
if let Some(c) = self.rows.last_mut() {
|
||||
c.definition = format!("{} {}", c.definition, definition);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line == "Use 'task context none' to unset the current context." {
|
||||
continue;
|
||||
}
|
||||
if i == 0 || i == 1 {
|
||||
continue;
|
||||
}
|
||||
let mut s = line.split_whitespace();
|
||||
let name = s.next().unwrap_or_default();
|
||||
let typ = s.next().unwrap_or_default();
|
||||
let active = s.last().unwrap_or_default();
|
||||
let definition = line.replacen(name, "", 1);
|
||||
let definition = definition.replacen(typ, "", 1);
|
||||
let definition = definition.strip_suffix(active).unwrap_or_default();
|
||||
let context = ContextDetails::new(
|
||||
name.to_string(),
|
||||
definition.trim().to_string(),
|
||||
active.to_string(),
|
||||
typ.to_string(),
|
||||
);
|
||||
self.rows.push(context);
|
||||
}
|
||||
if self.rows.iter().any(|r| r.active != "no") {
|
||||
self.rows.insert(
|
||||
0,
|
||||
ContextDetails::new("none".to_string(), "".to_string(), "no".to_string(), "read".to_string()),
|
||||
);
|
||||
} else {
|
||||
self.rows.insert(
|
||||
0,
|
||||
ContextDetails::new(
|
||||
"none".to_string(),
|
||||
"".to_string(),
|
||||
"yes".to_string(),
|
||||
"read".to_string(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use std::ops::Index;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
app::{Mode, TaskwarriorTui},
|
||||
tui::Event,
|
||||
};
|
||||
|
||||
pub mod context;
|
||||
pub mod project;
|
||||
|
||||
pub trait Pane {
|
||||
fn handle_input(app: &mut TaskwarriorTui, input: KeyEvent) -> Result<()>;
|
||||
fn change_focus_to_left_pane(app: &mut TaskwarriorTui) {
|
||||
match app.mode {
|
||||
Mode::Projects => app.mode = Mode::TaskReport,
|
||||
Mode::Calendar => {
|
||||
app.mode = Mode::Projects;
|
||||
}
|
||||
_ => {
|
||||
if app.config.uda_change_focus_rotate {
|
||||
app.mode = Mode::Calendar;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn change_focus_to_right_pane(app: &mut TaskwarriorTui) {
|
||||
match app.mode {
|
||||
Mode::Projects => app.mode = Mode::Calendar,
|
||||
Mode::Calendar => {
|
||||
if app.config.uda_change_focus_rotate {
|
||||
app.mode = Mode::TaskReport;
|
||||
}
|
||||
}
|
||||
_ => app.mode = Mode::Projects,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
use color_eyre::eyre::{anyhow, Context as AnyhowContext, Result};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
const COL_WIDTH: usize = 21;
|
||||
const PROJECT_HEADER: &str = "Name";
|
||||
const REMAINING_TASK_HEADER: &str = "Remaining";
|
||||
const AVG_AGE_HEADER: &str = "Avg age";
|
||||
const COMPLETE_HEADER: &str = "Complete";
|
||||
|
||||
use std::{
|
||||
cmp::min,
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use task_hookrs::project::Project;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
app::{Mode, TaskwarriorTui},
|
||||
pane::Pane,
|
||||
table::TableState,
|
||||
utils::Changeset,
|
||||
};
|
||||
|
||||
pub struct ProjectsState {
|
||||
pub(crate) list: Vec<Project>,
|
||||
pub table_state: TableState,
|
||||
pub current_selection: usize,
|
||||
pub marked: HashSet<Project>,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<ProjectDetails>,
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProjectDetails {
|
||||
name: Project,
|
||||
remaining: usize,
|
||||
avg_age: String,
|
||||
complete: String,
|
||||
}
|
||||
|
||||
impl ProjectsState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
list: Vec::default(),
|
||||
table_state: TableState::default(),
|
||||
current_selection: 0,
|
||||
marked: HashSet::default(),
|
||||
columns: vec![
|
||||
PROJECT_HEADER.to_string(),
|
||||
REMAINING_TASK_HEADER.to_string(),
|
||||
AVG_AGE_HEADER.to_string(),
|
||||
COMPLETE_HEADER.to_string(),
|
||||
],
|
||||
data: Default::default(),
|
||||
rows: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn pattern_by_marked(app: &mut TaskwarriorTui) -> String {
|
||||
let mut project_pattern = String::new();
|
||||
if !app.projects.marked.is_empty() {
|
||||
for (idx, project) in app.projects.marked.clone().iter().enumerate() {
|
||||
let mut input: String = String::from(project);
|
||||
if input.as_str() == "(none)" {
|
||||
input = " ".to_string();
|
||||
}
|
||||
if idx == 0 {
|
||||
project_pattern = format!("\'(project:{}", input);
|
||||
} else {
|
||||
project_pattern = format!("{} or project:{}", project_pattern, input);
|
||||
}
|
||||
}
|
||||
project_pattern = format!("{})\'", project_pattern);
|
||||
}
|
||||
project_pattern
|
||||
}
|
||||
|
||||
pub fn toggle_mark(&mut self) {
|
||||
if !self.list.is_empty() {
|
||||
let selected = self.current_selection;
|
||||
if !self.marked.insert(self.list[selected].clone()) {
|
||||
self.marked.remove(self.list[selected].as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simplified_view(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
|
||||
let rows = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|c| {
|
||||
vec![
|
||||
c.name.clone(),
|
||||
c.remaining.to_string(),
|
||||
c.avg_age.to_string(),
|
||||
c.complete.clone(),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let headers = self.columns.clone();
|
||||
(rows, headers)
|
||||
}
|
||||
|
||||
pub fn last_line(&self, line: &str) -> bool {
|
||||
let words = line.trim().split(' ').map(|s| s.trim()).collect::<Vec<&str>>();
|
||||
return words.len() == 2
|
||||
&& words[0].chars().map(|c| c.is_numeric()).all(|b| b)
|
||||
&& (words[1] == "project" || words[1] == "projects");
|
||||
}
|
||||
|
||||
pub fn update_data(&mut self) -> Result<()> {
|
||||
self.list.clear();
|
||||
self.rows.clear();
|
||||
let output = Command::new("task")
|
||||
.arg("summary")
|
||||
.output()
|
||||
.context("Unable to run `task summary`")
|
||||
.unwrap();
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
self.data = data.into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_table_state(&mut self) {
|
||||
self.table_state.select(Some(self.current_selection));
|
||||
if self.marked.is_empty() {
|
||||
self.table_state.single_selection();
|
||||
} else {
|
||||
self.table_state.multiple_selection();
|
||||
self.table_state.clear();
|
||||
for project in &self.marked {
|
||||
let index = self.list.iter().position(|x| x == project);
|
||||
self.table_state.mark(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pane for ProjectsState {
|
||||
fn handle_input(app: &mut TaskwarriorTui, input: KeyEvent) -> Result<()> {
|
||||
if input.code == app.keyconfig.quit {
|
||||
// || input == KeyCode::Ctrl('c') {
|
||||
app.should_quit = true;
|
||||
} else if input.code == app.keyconfig.next_tab {
|
||||
Self::change_focus_to_right_pane(app);
|
||||
} else if input.code == app.keyconfig.previous_tab {
|
||||
Self::change_focus_to_left_pane(app);
|
||||
} else if input.code == KeyCode::Down || input.code == app.keyconfig.down {
|
||||
self::focus_on_next_project(app);
|
||||
} else if input.code == KeyCode::Up || input.code == app.keyconfig.up {
|
||||
self::focus_on_previous_project(app);
|
||||
} else if input.code == app.keyconfig.select {
|
||||
self::update_task_filter_by_selection(app)?;
|
||||
}
|
||||
app.projects.update_table_state();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_on_next_project(app: &mut TaskwarriorTui) {
|
||||
if app.projects.current_selection < app.projects.list.len().saturating_sub(1) {
|
||||
app.projects.current_selection += 1;
|
||||
app.projects.table_state.select(Some(app.projects.current_selection));
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_on_previous_project(app: &mut TaskwarriorTui) {
|
||||
if app.projects.current_selection >= 1 {
|
||||
app.projects.current_selection -= 1;
|
||||
app.projects.table_state.select(Some(app.projects.current_selection));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_task_filter_by_selection(app: &mut TaskwarriorTui) -> Result<()> {
|
||||
app.projects.table_state.multiple_selection();
|
||||
let last_project_pattern = ProjectsState::pattern_by_marked(app);
|
||||
app.projects.toggle_mark();
|
||||
let new_project_pattern = ProjectsState::pattern_by_marked(app);
|
||||
let current_filter = app.filter.value();
|
||||
app.filter_history.add(current_filter);
|
||||
|
||||
let mut filter = current_filter.replace(&last_project_pattern, "");
|
||||
filter = format!("{}{}", filter, new_project_pattern);
|
||||
app.filter = app.filter.clone().with_value(filter);
|
||||
Ok(())
|
||||
}
|
109
src/runner.rs
Normal file
109
src/runner.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use color_eyre::eyre::Result;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
command::Command,
|
||||
components::{app::App, Component},
|
||||
config::Config,
|
||||
tui,
|
||||
};
|
||||
|
||||
pub struct Runner {
|
||||
pub config: Config,
|
||||
pub tick_rate: f64,
|
||||
pub frame_rate: f64,
|
||||
pub components: Vec<Box<dyn Component>>,
|
||||
pub should_quit: bool,
|
||||
pub should_suspend: bool,
|
||||
}
|
||||
|
||||
impl Runner {
|
||||
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
|
||||
let app = App::new();
|
||||
let config = Config::new()?;
|
||||
let app = app.keybindings(config.keybindings.clone());
|
||||
Ok(Self {
|
||||
tick_rate,
|
||||
frame_rate,
|
||||
components: vec![Box::new(app)],
|
||||
should_quit: false,
|
||||
should_suspend: false,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut tui = tui::Tui::new()?;
|
||||
tui.tick_rate(self.tick_rate);
|
||||
tui.frame_rate(self.frame_rate);
|
||||
tui.enter()?;
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.register_command_handler(command_tx.clone())?;
|
||||
}
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.init()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(e) = tui.next().await {
|
||||
match e {
|
||||
tui::Event::Quit => command_tx.send(Command::Quit)?,
|
||||
tui::Event::Tick => command_tx.send(Command::Tick)?,
|
||||
tui::Event::Render => command_tx.send(Command::Render)?,
|
||||
tui::Event::Resize(x, y) => command_tx.send(Command::Resize(x, y))?,
|
||||
e => {
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(command) = component.handle_events(Some(e.clone()))? {
|
||||
command_tx.send(command)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(command) = command_rx.try_recv() {
|
||||
if command != Command::Tick && command != Command::Render {
|
||||
log::debug!("{command:?}");
|
||||
}
|
||||
match command {
|
||||
Command::Quit => self.should_quit = true,
|
||||
Command::Suspend => self.should_suspend = true,
|
||||
Command::Resume => self.should_suspend = false,
|
||||
Command::Render => {
|
||||
tui.draw(|f| {
|
||||
for component in self.components.iter_mut() {
|
||||
let r = component.draw(f, f.size());
|
||||
if let Err(e) = r {
|
||||
command_tx.send(Command::Error(format!("Failed to draw: {:?}", e))).unwrap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(command) = component.update(command.clone())? {
|
||||
command_tx.send(command)?
|
||||
};
|
||||
}
|
||||
}
|
||||
if self.should_suspend {
|
||||
tui.suspend()?;
|
||||
command_tx.send(Command::Resume)?;
|
||||
tui = tui::Tui::new()?;
|
||||
tui.tick_rate(self.tick_rate);
|
||||
tui.frame_rate(self.frame_rate);
|
||||
tui.enter()?;
|
||||
} else if self.should_quit {
|
||||
tui.stop()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tui.exit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
use ratatui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
layout::{Margin, Rect},
|
||||
style::{Color, Style},
|
||||
symbols::{block::FULL, line::DOUBLE_VERTICAL},
|
||||
widgets::Widget,
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct Scrollbar {
|
||||
pub pos: u16,
|
||||
pub len: u16,
|
||||
pub pos_style: Style,
|
||||
pub pos_symbol: String,
|
||||
pub area_style: Style,
|
||||
pub area_symbol: String,
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
pub fn new(pos: usize, len: usize) -> Self {
|
||||
Self {
|
||||
pos: pos as u16,
|
||||
len: len as u16,
|
||||
pos_style: Style::default(),
|
||||
pos_symbol: FULL.to_string(),
|
||||
area_style: Style::default(),
|
||||
area_symbol: DOUBLE_VERTICAL.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scrollbar {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.height <= 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.len == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let right = area.right().saturating_sub(1);
|
||||
|
||||
if right <= area.left() {
|
||||
return;
|
||||
};
|
||||
|
||||
let (top, height) = { (area.top() + 3, area.height.saturating_sub(4)) };
|
||||
|
||||
for y in top..(top + height) {
|
||||
buf.set_string(right, y, self.area_symbol.clone(), self.area_style);
|
||||
}
|
||||
|
||||
let progress = self.pos as f64 / self.len as f64;
|
||||
let progress = if progress > 1.0 { 1.0 } else { progress };
|
||||
let pos = height as f64 * progress;
|
||||
|
||||
let pos = pos as i64 as u16;
|
||||
|
||||
buf.set_string(right, top + pos, self.pos_symbol, self.pos_style);
|
||||
}
|
||||
}
|
537
src/table.rs
537
src/table.rs
|
@ -1,537 +0,0 @@
|
|||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
iter::{self, Iterator},
|
||||
};
|
||||
|
||||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
Expression, Solver,
|
||||
WeightedRelation::{EQ, GE, LE},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TableMode {
|
||||
SingleSelection,
|
||||
MultipleSelection,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
current_selection: Option<usize>,
|
||||
marked: HashSet<usize>,
|
||||
mode: TableMode,
|
||||
}
|
||||
|
||||
impl Default for TableState {
|
||||
fn default() -> TableState {
|
||||
TableState {
|
||||
offset: 0,
|
||||
current_selection: Some(0),
|
||||
marked: HashSet::new(),
|
||||
mode: TableMode::SingleSelection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn mode(&self) -> TableMode {
|
||||
self.mode.clone()
|
||||
}
|
||||
|
||||
pub fn multiple_selection(&mut self) {
|
||||
self.mode = TableMode::MultipleSelection;
|
||||
}
|
||||
|
||||
pub fn single_selection(&mut self) {
|
||||
self.mode = TableMode::SingleSelection;
|
||||
}
|
||||
|
||||
pub fn current_selection(&self) -> Option<usize> {
|
||||
self.current_selection
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.current_selection = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark(&mut self, index: Option<usize>) {
|
||||
if let Some(i) = index {
|
||||
self.marked.insert(i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmark(&mut self, index: Option<usize>) {
|
||||
if let Some(i) = index {
|
||||
self.marked.remove(&i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_mark(&mut self, index: Option<usize>) {
|
||||
if let Some(i) = index {
|
||||
if !self.marked.insert(i) {
|
||||
self.marked.remove(&i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn marked(&self) -> std::collections::hash_set::Iter<usize> {
|
||||
self.marked.iter()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.marked.drain().for_each(drop);
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Row<D>
|
||||
where
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Block, Borders, Table, Row};
|
||||
/// # use ratatui::layout::Constraint;
|
||||
/// # use ratatui::style::{Style, Color};
|
||||
/// let row_style = Style::default().fg(Color::White);
|
||||
/// Table::new(
|
||||
/// ["Col1", "Col2", "Col3"].into_iter(),
|
||||
/// vec![
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
|
||||
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
|
||||
/// ].into_iter()
|
||||
/// )
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// .header_style(Style::default().fg(Color::Yellow))
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .column_spacing(1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table<'a, H, R> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Header row for all columns
|
||||
header: H,
|
||||
/// Style for the header
|
||||
header_style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Space between the header and the rows
|
||||
header_gap: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected row
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Symbol in front of the marked row
|
||||
mark_symbol: Option<&'a str>,
|
||||
/// Symbol in front of the unmarked row
|
||||
unmark_symbol: Option<&'a str>,
|
||||
/// Symbol in front of the marked and selected row
|
||||
mark_highlight_symbol: Option<&'a str>,
|
||||
/// Symbol in front of the unmarked and selected row
|
||||
unmark_highlight_symbol: Option<&'a str>,
|
||||
/// Data to display in each row
|
||||
rows: R,
|
||||
}
|
||||
|
||||
impl<'a, H, R> Default for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator + Default,
|
||||
R: Iterator + Default,
|
||||
{
|
||||
fn default() -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header: H::default(),
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
mark_symbol: None,
|
||||
unmark_symbol: None,
|
||||
mark_highlight_symbol: None,
|
||||
unmark_highlight_symbol: None,
|
||||
rows: R::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, H, D, R> Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
header,
|
||||
header_style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
header_gap: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
mark_symbol: None,
|
||||
unmark_symbol: None,
|
||||
mark_highlight_symbol: None,
|
||||
unmark_highlight_symbol: None,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = H::Item, IntoIter = H>,
|
||||
{
|
||||
self.header = header.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
|
||||
where
|
||||
II: IntoIterator<Item = Row<D>, IntoIter = R>,
|
||||
{
|
||||
self.rows = rows.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mark_symbol(mut self, mark_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.mark_symbol = Some(mark_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unmark_symbol(mut self, unmark_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.unmark_symbol = Some(unmark_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mark_highlight_symbol(mut self, mark_highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.mark_highlight_symbol = Some(mark_highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unmark_highlight_symbol(mut self, unmark_highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.unmark_highlight_symbol = Some(unmark_highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
|
||||
self.header_gap = gap;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
// Render block if necessary and get the drawing area
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
let mut solver = Solver::new();
|
||||
let mut var_indices = HashMap::new();
|
||||
let mut ccs = Vec::new();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
variables.push(var);
|
||||
var_indices.insert(var, i);
|
||||
}
|
||||
for (i, constraint) in self.widths.iter().enumerate() {
|
||||
ccs.push(variables[i] | GE(WEAK) | 0.);
|
||||
ccs.push(match *constraint {
|
||||
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0),
|
||||
Constraint::Ratio(n, d) => variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d)),
|
||||
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
|
||||
});
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables.iter().fold(Expression::from_constant(0.), |acc, v| acc + *v)
|
||||
| LE(REQUIRED)
|
||||
| f64::from(area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1))),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
let mut solved_widths = vec![0; variables.len()];
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() { 0 } else { value as u16 };
|
||||
solved_widths[index] = value;
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
let mut x = table_area.left();
|
||||
|
||||
// Draw header
|
||||
let mut header_index = usize::MAX;
|
||||
let mut index = 0;
|
||||
if y < table_area.bottom() {
|
||||
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
format!("{symbol:>width$}", symbol = " ", width = *w as usize),
|
||||
*w as usize,
|
||||
self.header_style,
|
||||
);
|
||||
if t.to_string() == "ID" {
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
format!("{symbol:>width$}", symbol = t, width = *w as usize),
|
||||
*w as usize,
|
||||
self.header_style,
|
||||
);
|
||||
header_index = index;
|
||||
} else {
|
||||
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
|
||||
}
|
||||
x += *w + self.column_spacing;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
y += 1 + self.header_gap;
|
||||
|
||||
// Use highlight_style only if something is selected
|
||||
let (selected, highlight_style) = if state.current_selection().is_some() {
|
||||
(state.current_selection(), self.highlight_style)
|
||||
} else {
|
||||
(None, self.style)
|
||||
};
|
||||
|
||||
let highlight_symbol = match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
let s = self.highlight_symbol.unwrap_or("\u{2022}").trim_end();
|
||||
format!("{} ", s)
|
||||
}
|
||||
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||
};
|
||||
|
||||
let mark_symbol = match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
let s = self.mark_symbol.unwrap_or("\u{2714}").trim_end();
|
||||
format!("{} ", s)
|
||||
}
|
||||
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||
};
|
||||
|
||||
let blank_symbol = match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
let s = self.unmark_symbol.unwrap_or(" ").trim_end();
|
||||
format!("{} ", s)
|
||||
}
|
||||
TableMode::SingleSelection => " ".repeat(highlight_symbol.width()),
|
||||
};
|
||||
|
||||
let mark_highlight_symbol = {
|
||||
let s = self.mark_highlight_symbol.unwrap_or("\u{29bf}").trim_end();
|
||||
format!("{} ", s)
|
||||
};
|
||||
|
||||
let unmark_highlight_symbol = {
|
||||
let s = self.unmark_highlight_symbol.unwrap_or("\u{29be}").trim_end();
|
||||
format!("{} ", s)
|
||||
};
|
||||
|
||||
// Draw rows
|
||||
let default_style = Style::default();
|
||||
if y < table_area.bottom() {
|
||||
let remaining = (table_area.bottom() - y) as usize;
|
||||
|
||||
// Make sure the table shows the selected item
|
||||
state.offset = selected.map_or(0, |s| {
|
||||
if s >= remaining + state.offset - 1 {
|
||||
s + 1 - remaining
|
||||
} else if s < state.offset {
|
||||
s
|
||||
} else {
|
||||
state.offset
|
||||
}
|
||||
});
|
||||
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
||||
let (data, style, symbol) = match row {
|
||||
Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.current_selection().map(|s| s - state.offset) => {
|
||||
match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
if state.marked.contains(&(i + state.offset)) {
|
||||
(d, highlight_style, mark_highlight_symbol.to_string())
|
||||
} else {
|
||||
(d, highlight_style, unmark_highlight_symbol.to_string())
|
||||
}
|
||||
}
|
||||
TableMode::SingleSelection => (d, highlight_style, highlight_symbol.to_string()),
|
||||
}
|
||||
}
|
||||
Row::Data(d) => {
|
||||
if state.marked.contains(&(i + state.offset)) {
|
||||
(d, default_style, mark_symbol.to_string())
|
||||
} else {
|
||||
(d, default_style, blank_symbol.to_string())
|
||||
}
|
||||
}
|
||||
Row::StyledData(d, s) => {
|
||||
if state.marked.contains(&(i + state.offset)) {
|
||||
(d, s, mark_symbol.to_string())
|
||||
} else {
|
||||
(d, s, blank_symbol.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
x = table_area.left();
|
||||
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
|
||||
let s = if c == 0 {
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y + i as u16,
|
||||
format!("{symbol:^width$}", symbol = "", width = area.width as usize),
|
||||
*w as usize,
|
||||
style,
|
||||
);
|
||||
if c == header_index {
|
||||
let symbol = match state.mode {
|
||||
TableMode::SingleSelection | TableMode::MultipleSelection => &symbol,
|
||||
};
|
||||
format!(
|
||||
"{symbol}{elt:>width$}",
|
||||
symbol = symbol,
|
||||
elt = elt,
|
||||
width = (*w as usize).saturating_sub(symbol.to_string().width())
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{symbol}{elt:<width$}",
|
||||
symbol = symbol,
|
||||
elt = elt,
|
||||
width = (*w as usize).saturating_sub(symbol.to_string().width())
|
||||
)
|
||||
}
|
||||
} else {
|
||||
buf.set_stringn(
|
||||
x - 1,
|
||||
y + i as u16,
|
||||
format!("{symbol:^width$}", symbol = "", width = area.width as usize),
|
||||
*w as usize + 1,
|
||||
style,
|
||||
);
|
||||
if c == header_index {
|
||||
format!("{elt:>width$}", elt = elt, width = *w as usize)
|
||||
} else {
|
||||
format!("{elt:<width$}", elt = elt, width = *w as usize)
|
||||
}
|
||||
};
|
||||
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, D, R> Widget for Table<'a, H, R>
|
||||
where
|
||||
H: Iterator,
|
||||
H::Item: Display,
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
R: Iterator<Item = Row<D>>,
|
||||
{
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
|
@ -1,481 +0,0 @@
|
|||
use std::{error::Error, process::Command};
|
||||
|
||||
use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, TimeZone};
|
||||
use color_eyre::eyre::Result;
|
||||
use itertools::join;
|
||||
use task_hookrs::{task::Task, uda::UDAValue};
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub fn format_date_time(dt: NaiveDateTime) -> String {
|
||||
let dt = Local.from_local_datetime(&dt).unwrap();
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
pub fn format_date(dt: NaiveDateTime) -> String {
|
||||
let offset = Local.offset_from_utc_datetime(&dt);
|
||||
let dt = DateTime::<Local>::from_naive_utc_and_offset(dt, offset);
|
||||
dt.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
pub fn vague_format_date_time(from_dt: NaiveDateTime, to_dt: NaiveDateTime, with_remainder: bool) -> String {
|
||||
let to_dt = Local.from_local_datetime(&to_dt).unwrap();
|
||||
let from_dt = Local.from_local_datetime(&from_dt).unwrap();
|
||||
let mut seconds = (to_dt - from_dt).num_seconds();
|
||||
let minus = if seconds < 0 {
|
||||
seconds *= -1;
|
||||
"-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let year = 60 * 60 * 24 * 365;
|
||||
let month = 60 * 60 * 24 * 30;
|
||||
let week = 60 * 60 * 24 * 7;
|
||||
let day = 60 * 60 * 24;
|
||||
let hour = 60 * 60;
|
||||
let minute = 60;
|
||||
|
||||
if seconds >= 60 * 60 * 24 * 365 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}y{}mo",
|
||||
minus,
|
||||
seconds / year,
|
||||
(seconds - year * (seconds / year)) / month
|
||||
)
|
||||
} else {
|
||||
format!("{}{}y", minus, seconds / year)
|
||||
};
|
||||
} else if seconds >= 60 * 60 * 24 * 90 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}mo{}w",
|
||||
minus,
|
||||
seconds / month,
|
||||
(seconds - month * (seconds / month)) / week
|
||||
)
|
||||
} else {
|
||||
format!("{}{}mo", minus, seconds / month)
|
||||
};
|
||||
} else if seconds >= 60 * 60 * 24 * 14 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}w{}d",
|
||||
minus,
|
||||
seconds / week,
|
||||
(seconds - week * (seconds / week)) / day
|
||||
)
|
||||
} else {
|
||||
format!("{}{}w", minus, seconds / week)
|
||||
};
|
||||
} else if seconds >= 60 * 60 * 24 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}d{}h",
|
||||
minus,
|
||||
seconds / day,
|
||||
(seconds - day * (seconds / day)) / hour
|
||||
)
|
||||
} else {
|
||||
format!("{}{}d", minus, seconds / day)
|
||||
};
|
||||
} else if seconds >= 60 * 60 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}h{}min",
|
||||
minus,
|
||||
seconds / hour,
|
||||
(seconds - hour * (seconds / hour)) / minute
|
||||
)
|
||||
} else {
|
||||
format!("{}{}h", minus, seconds / hour)
|
||||
};
|
||||
} else if seconds >= 60 {
|
||||
return if with_remainder {
|
||||
format!(
|
||||
"{}{}min{}s",
|
||||
minus,
|
||||
seconds / minute,
|
||||
(seconds - minute * (seconds / minute))
|
||||
)
|
||||
} else {
|
||||
format!("{}{}min", minus, seconds / minute)
|
||||
};
|
||||
}
|
||||
format!("{}{}s", minus, seconds)
|
||||
}
|
||||
|
||||
pub struct TaskReportTable {
|
||||
pub labels: Vec<String>,
|
||||
pub columns: Vec<String>,
|
||||
pub tasks: Vec<Vec<String>>,
|
||||
pub virtual_tags: Vec<String>,
|
||||
pub description_width: usize,
|
||||
pub date_time_vague_precise: bool,
|
||||
}
|
||||
|
||||
impl TaskReportTable {
|
||||
pub fn new(data: &str, report: &str) -> Result<Self> {
|
||||
let virtual_tags = vec![
|
||||
"PROJECT",
|
||||
"BLOCKED",
|
||||
"UNBLOCKED",
|
||||
"BLOCKING",
|
||||
"DUE",
|
||||
"DUETODAY",
|
||||
"TODAY",
|
||||
"OVERDUE",
|
||||
"WEEK",
|
||||
"MONTH",
|
||||
"QUARTER",
|
||||
"YEAR",
|
||||
"ACTIVE",
|
||||
"SCHEDULED",
|
||||
"PARENT",
|
||||
"CHILD",
|
||||
"UNTIL",
|
||||
"WAITING",
|
||||
"ANNOTATED",
|
||||
"READY",
|
||||
"YESTERDAY",
|
||||
"TOMORROW",
|
||||
"TAGGED",
|
||||
"PENDING",
|
||||
"COMPLETED",
|
||||
"DELETED",
|
||||
"UDA",
|
||||
"ORPHAN",
|
||||
"PRIORITY",
|
||||
"PROJECT",
|
||||
"LATEST",
|
||||
"RECURRING",
|
||||
"INSTANCE",
|
||||
"TEMPLATE",
|
||||
];
|
||||
let mut task_report_table = Self {
|
||||
labels: vec![],
|
||||
columns: vec![],
|
||||
tasks: vec![vec![]],
|
||||
virtual_tags: virtual_tags.iter().map(ToString::to_string).collect::<Vec<_>>(),
|
||||
description_width: 100,
|
||||
date_time_vague_precise: false,
|
||||
};
|
||||
task_report_table.export_headers(Some(data), report)?;
|
||||
Ok(task_report_table)
|
||||
}
|
||||
|
||||
pub fn export_headers(&mut self, data: Option<&str>, report: &str) -> Result<()> {
|
||||
self.columns = vec![];
|
||||
self.labels = vec![];
|
||||
|
||||
let data = if let Some(s) = data {
|
||||
s.to_string()
|
||||
} else {
|
||||
let output = Command::new("task")
|
||||
.arg("show")
|
||||
.arg("rc.defaultwidth=0")
|
||||
.arg(format!("report.{}.columns", report))
|
||||
.output()?;
|
||||
String::from_utf8_lossy(&output.stdout).into_owned()
|
||||
};
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with(format!("report.{}.columns", report).as_str()) {
|
||||
let column_names = line.split_once(' ').unwrap().1;
|
||||
for column in column_names.split(',') {
|
||||
self.columns.push(column.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = Command::new("task")
|
||||
.arg("show")
|
||||
.arg("rc.defaultwidth=0")
|
||||
.arg(format!("report.{}.labels", report))
|
||||
.output()?;
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with(format!("report.{}.labels", report).as_str()) {
|
||||
let label_names = line.split_once(' ').unwrap().1;
|
||||
for label in label_names.split(',') {
|
||||
self.labels.push(label.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.labels.is_empty() {
|
||||
for label in &self.columns {
|
||||
let label = label.split('.').collect::<Vec<&str>>()[0];
|
||||
let label = if label == "id" { "ID" } else { label };
|
||||
let mut c = label.chars();
|
||||
let label = match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
};
|
||||
if !label.is_empty() {
|
||||
self.labels.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
let num_labels = self.labels.len();
|
||||
let num_columns = self.columns.len();
|
||||
assert!(num_labels == num_columns, "Must have the same number of labels (currently {}) and columns (currently {}). Compare their values as shown by \"task show report.{}.\" and fix your taskwarrior config.", num_labels, num_columns, report);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_table(&mut self, tasks: &[Task]) {
|
||||
self.tasks = vec![];
|
||||
|
||||
// get all tasks as their string representation
|
||||
for task in tasks {
|
||||
if self.columns.is_empty() {
|
||||
break;
|
||||
}
|
||||
let mut item = vec![];
|
||||
for name in &self.columns {
|
||||
let s = self.get_string_attribute(name, task, tasks);
|
||||
item.push(s);
|
||||
}
|
||||
self.tasks.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simplify_table(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
|
||||
// find which columns are empty
|
||||
if self.tasks.is_empty() {
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
|
||||
let mut null_columns = vec![0; self.tasks[0].len()];
|
||||
|
||||
for task in &self.tasks {
|
||||
for (i, s) in task.iter().enumerate() {
|
||||
null_columns[i] += s.len();
|
||||
}
|
||||
}
|
||||
|
||||
// filter out columns where everything is empty
|
||||
let mut tasks = vec![];
|
||||
for task in &self.tasks {
|
||||
let t = task.clone();
|
||||
let t: Vec<String> = t
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| null_columns[i] != 0)
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
tasks.push(t);
|
||||
}
|
||||
|
||||
// filter out header where all columns are empty
|
||||
let headers: Vec<String> = self
|
||||
.labels
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| null_columns[i] != 0)
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
|
||||
(tasks, headers)
|
||||
}
|
||||
|
||||
pub fn get_string_attribute(&self, attribute: &str, task: &Task, tasks: &[Task]) -> String {
|
||||
match attribute {
|
||||
"id" => task.id().unwrap_or_default().to_string(),
|
||||
"scheduled.relative" => match task.scheduled() {
|
||||
Some(v) => vague_format_date_time(
|
||||
Local::now().naive_utc(),
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"due.relative" => match task.due() {
|
||||
Some(v) => vague_format_date_time(
|
||||
Local::now().naive_utc(),
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"due" => match task.due() {
|
||||
Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"until.remaining" => match task.until() {
|
||||
Some(v) => vague_format_date_time(
|
||||
Local::now().naive_utc(),
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"until" => match task.until() {
|
||||
Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"entry.age" => vague_format_date_time(
|
||||
NaiveDateTime::new(task.entry().date(), task.entry().time()),
|
||||
Local::now().naive_utc(),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
"entry" => format_date(NaiveDateTime::new(task.entry().date(), task.entry().time())),
|
||||
"start.age" => match task.start() {
|
||||
Some(v) => vague_format_date_time(
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
Local::now().naive_utc(),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"start" => match task.start() {
|
||||
Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"end.age" => match task.end() {
|
||||
Some(v) => vague_format_date_time(
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
Local::now().naive_utc(),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"end" => match task.end() {
|
||||
Some(v) => format_date(NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"status.short" => task.status().to_string().chars().next().unwrap().to_string(),
|
||||
"status" => task.status().to_string(),
|
||||
"priority" => match task.priority() {
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"project" => match task.project() {
|
||||
Some(p) => p.to_string(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"depends.count" => match task.depends() {
|
||||
Some(v) => {
|
||||
if v.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("{}", v.len())
|
||||
}
|
||||
}
|
||||
None => "".to_string(),
|
||||
},
|
||||
"depends" => match task.depends() {
|
||||
Some(v) => {
|
||||
if v.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
let mut dt = vec![];
|
||||
for u in v {
|
||||
if let Some(t) = tasks.iter().find(|t| t.uuid() == u) {
|
||||
dt.push(t.id().unwrap());
|
||||
}
|
||||
}
|
||||
join(dt.iter().map(ToString::to_string), " ")
|
||||
}
|
||||
}
|
||||
None => "".to_string(),
|
||||
},
|
||||
"tags.count" => match task.tags() {
|
||||
Some(v) => {
|
||||
let t = v.iter().filter(|t| !self.virtual_tags.contains(t)).count();
|
||||
if t == 0 {
|
||||
"".to_string()
|
||||
} else {
|
||||
t.to_string()
|
||||
}
|
||||
}
|
||||
None => "".to_string(),
|
||||
},
|
||||
"tags" => match task.tags() {
|
||||
Some(v) => v
|
||||
.iter()
|
||||
.filter(|t| !self.virtual_tags.contains(t))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"recur" => match task.recur() {
|
||||
Some(v) => v.clone(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"wait" => match task.wait() {
|
||||
Some(v) => vague_format_date_time(
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
Local::now().naive_utc(),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"wait.remaining" => match task.wait() {
|
||||
Some(v) => vague_format_date_time(
|
||||
Local::now().naive_utc(),
|
||||
NaiveDateTime::new(v.date(), v.time()),
|
||||
self.date_time_vague_precise,
|
||||
),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"description.count" => {
|
||||
let c = if let Some(a) = task.annotations() {
|
||||
format!("[{}]", a.len())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
format!("{} {}", task.description(), c)
|
||||
}
|
||||
"description.truncated_count" => {
|
||||
let c = if let Some(a) = task.annotations() {
|
||||
format!("[{}]", a.len())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let d = task.description().to_string();
|
||||
let mut available_width = self.description_width;
|
||||
if self.description_width >= c.len() {
|
||||
available_width = self.description_width - c.len();
|
||||
}
|
||||
let (d, _) = d.unicode_truncate(available_width);
|
||||
let mut d = d.to_string();
|
||||
if d != *task.description() {
|
||||
d = format!("{}\u{2026}", d);
|
||||
}
|
||||
format!("{}{}", d, c)
|
||||
}
|
||||
"description.truncated" => {
|
||||
let d = task.description().to_string();
|
||||
let available_width = self.description_width;
|
||||
let (d, _) = d.unicode_truncate(available_width);
|
||||
let mut d = d.to_string();
|
||||
if d != *task.description() {
|
||||
d = format!("{}\u{2026}", d);
|
||||
}
|
||||
d
|
||||
}
|
||||
"description.desc" | "description" => task.description().to_string(),
|
||||
"urgency" => match &task.urgency() {
|
||||
Some(f) => format!("{:.2}", *f),
|
||||
None => "0.00".to_string(),
|
||||
},
|
||||
s => {
|
||||
let u = &task.uda();
|
||||
let v = u.get(s);
|
||||
if v.is_none() {
|
||||
return "".to_string();
|
||||
}
|
||||
match v.unwrap() {
|
||||
UDAValue::Str(s) => s.to_string(),
|
||||
UDAValue::F64(f) => f.to_string(),
|
||||
UDAValue::U64(u) => u.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
use task_hookrs::task::Task;
|
||||
|
||||
pub trait TaskwarriorTuiTask {
|
||||
fn add_tag(&mut self, tag: String);
|
||||
|
||||
fn remove_tag(&mut self, tag: &str);
|
||||
}
|
||||
|
||||
impl TaskwarriorTuiTask for Task {
|
||||
fn add_tag(&mut self, tag: String) {
|
||||
match self.tags_mut() {
|
||||
Some(t) => t.push(tag),
|
||||
None => self.set_tags(Some(vec![tag])),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tag(&mut self, tag: &str) {
|
||||
if let Some(t) = self.tags_mut() {
|
||||
if let Some(index) = t.iter().position(|x| *x == tag) {
|
||||
t.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
src/tui.rs
42
src/tui.rs
|
@ -1,4 +1,3 @@
|
|||
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
|
@ -23,6 +22,7 @@ pub type Frame<'a> = ratatui::Frame<'a, Backend<std::io::Stderr>>;
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Init,
|
||||
Quit,
|
||||
Error,
|
||||
Closed,
|
||||
|
@ -42,36 +42,43 @@ pub struct Tui {
|
|||
pub cancellation_token: CancellationToken,
|
||||
pub event_rx: UnboundedReceiver<Event>,
|
||||
pub event_tx: UnboundedSender<Event>,
|
||||
pub tick_rate: (usize, usize),
|
||||
pub frame_rate: f64,
|
||||
pub tick_rate: f64,
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new() -> Result<Self> {
|
||||
let tick_rate = (1000, 100);
|
||||
let tick_rate = 4.0;
|
||||
let frame_rate = 60.0;
|
||||
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 })
|
||||
Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate })
|
||||
}
|
||||
|
||||
pub fn tick_rate(&mut self, tick_rate: (usize, usize)) {
|
||||
pub fn tick_rate(&mut self, tick_rate: f64) {
|
||||
self.tick_rate = tick_rate;
|
||||
}
|
||||
|
||||
pub fn frame_rate(&mut self, frame_rate: f64) {
|
||||
self.frame_rate = frame_rate;
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
let tick_rate = std::time::Duration::from_millis(self.tick_rate.0 as u64);
|
||||
let render_tick_rate = std::time::Duration::from_millis(self.tick_rate.1 as u64);
|
||||
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
|
||||
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
|
||||
self.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);
|
||||
let mut render_interval = tokio::time::interval(render_tick_rate);
|
||||
let mut tick_interval = tokio::time::interval(tick_delay);
|
||||
let mut render_interval = tokio::time::interval(render_delay);
|
||||
_event_tx.send(Event::Init).unwrap();
|
||||
loop {
|
||||
let delay = interval.tick();
|
||||
let tick_delay = tick_interval.tick();
|
||||
let render_delay = render_interval.tick();
|
||||
let crossterm_event = reader.next().fuse();
|
||||
tokio::select! {
|
||||
|
@ -110,7 +117,7 @@ impl Tui {
|
|||
None => {},
|
||||
}
|
||||
},
|
||||
_ = delay => {
|
||||
_ = tick_delay => {
|
||||
_event_tx.send(Event::Tick).unwrap();
|
||||
},
|
||||
_ = render_delay => {
|
||||
|
@ -132,7 +139,7 @@ impl Tui {
|
|||
}
|
||||
if counter > 100 {
|
||||
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
|
||||
return Err(color_eyre::eyre::eyre!("Unable to abort task"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -145,10 +152,13 @@ impl Tui {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exit(&self) -> Result<()> {
|
||||
pub fn exit(&mut self) -> Result<()> {
|
||||
self.stop()?;
|
||||
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||
self.flush()?;
|
||||
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -156,7 +166,7 @@ impl Tui {
|
|||
self.cancellation_token.cancel();
|
||||
}
|
||||
|
||||
pub fn suspend(&self) -> Result<()> {
|
||||
pub fn suspend(&mut self) -> Result<()> {
|
||||
self.exit()?;
|
||||
#[cfg(not(windows))]
|
||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
|
||||
|
|
37
src/ui.rs
37
src/ui.rs
|
@ -1,37 +0,0 @@
|
|||
use ratatui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Cell, LineGauge, Paragraph, Row, Table},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::TaskwarriorTui;
|
||||
|
||||
pub fn draw<B>(rect: &mut Frame<B>, app: &TaskwarriorTui)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let size = rect.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(10), Constraint::Length(3)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let title = draw_title();
|
||||
rect.render_widget(title, chunks[0]);
|
||||
}
|
||||
|
||||
fn draw_title<'a>() -> Paragraph<'a> {
|
||||
Paragraph::new("Taskwarrior TUI")
|
||||
.style(Style::default().fg(Color::LightCyan))
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.border_type(BorderType::Plain),
|
||||
)
|
||||
}
|
129
src/utils.rs
129
src/utils.rs
|
@ -1,92 +1,64 @@
|
|||
use path_clean::PathClean;
|
||||
use rustyline::line_buffer::{ChangeListener, DeleteListener, Direction};
|
||||
|
||||
/// Undo manager
|
||||
#[derive(Default)]
|
||||
pub struct Changeset {}
|
||||
|
||||
impl DeleteListener for Changeset {
|
||||
fn delete(&mut self, idx: usize, string: &str, _: Direction) {}
|
||||
}
|
||||
|
||||
impl ChangeListener for Changeset {
|
||||
fn insert_char(&mut self, idx: usize, c: char) {}
|
||||
|
||||
fn insert_str(&mut self, idx: usize, string: &str) {}
|
||||
|
||||
fn replace(&mut self, idx: usize, old: &str, new: &str) {}
|
||||
}
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::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;
|
||||
use tracing_subscriber::{self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> = std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref CONFIG_FOLDER: Option<PathBuf> = std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||
std::env::var(format!("{}_DATA", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||
pub static ref CONFIG_FOLDER: Option<PathBuf> =
|
||||
std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||
pub static ref GIT_COMMIT_HASH: String =
|
||||
std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())).unwrap_or_else(|_| String::from("Unknown"));
|
||||
pub static ref LOG_LEVEL: String = std::env::var(format!("{}_LOG_LEVEL", PROJECT_NAME.clone())).unwrap_or_default();
|
||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME").to_lowercase());
|
||||
std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())).unwrap_or_else(|_| String::from("UNKNOWN"));
|
||||
pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
|
||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
|
||||
fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "kdheepak", PROJECT_NAME.clone().to_lowercase().as_str())
|
||||
ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
pub fn initialize_panic_handler() -> Result<()> {
|
||||
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||
.panic_section(format!(
|
||||
"This is a bug. Consider reporting it at {}",
|
||||
env!("CARGO_PKG_REPOSITORY")
|
||||
))
|
||||
.display_location_section(true)
|
||||
.display_env_section(true)
|
||||
.issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
|
||||
.add_issue_metadata("version", env!("CARGO_PKG_VERSION"))
|
||||
.add_issue_metadata("os", std::env::consts::OS)
|
||||
.add_issue_metadata("arch", std::env::consts::ARCH)
|
||||
.panic_section(format!("This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY")))
|
||||
.capture_span_trace_by_default(false)
|
||||
.display_location_section(false)
|
||||
.display_env_section(false)
|
||||
.into_hooks();
|
||||
eyre_hook.install()?;
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Ok(t) = Tui::new() {
|
||||
if let Ok(mut t) = crate::tui::Tui::new() {
|
||||
if let Err(r) = t.exit() {
|
||||
error!("Unable to exit Terminal: {:?}", r);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
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);
|
||||
// prints human-panic message
|
||||
print_msg(file_path, &meta).expect("human-panic: printing error message to console failed");
|
||||
eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr
|
||||
}
|
||||
let msg = format!("{}", panic_hook.panic_report(panic_info));
|
||||
eprintln!("{}", msg);
|
||||
log::error!("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");
|
||||
|
||||
// Better Panic. Only enabled *when* debugging.
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Better Panic stacktrace that is only enabled when debugging.
|
||||
better_panic::Settings::auto()
|
||||
.most_recent_first(false)
|
||||
.lineno_suffix(true)
|
||||
|
@ -126,30 +98,20 @@ pub fn initialize_logging() -> 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)?;
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
std::env::var("RUST_LOG")
|
||||
.or_else(|_| std::env::var(LOG_ENV.clone()))
|
||||
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
|
||||
);
|
||||
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 = match LOG_LEVEL.clone().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);
|
||||
|
||||
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
|
||||
tracing_subscriber::registry().with(file_subscriber).with(ErrorLayer::default()).init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -198,16 +160,3 @@ Config directory: {config_dir_path}
|
|||
Data directory: {data_dir_path}"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn absolute_path(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let absolute_path = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path)
|
||||
}
|
||||
.clean();
|
||||
|
||||
Ok(absolute_path)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue