Merge pull request #495 from kdheepak/update

feat: Update dependencies 
This commit is contained in:
Dheepak Krishnamurthy 2023-05-26 23:35:56 -04:00 committed by GitHub
commit dee5c0c5f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 7977 additions and 8104 deletions

View file

@ -110,6 +110,19 @@ jobs:
toolchain: stable
override: true
# - name: Build
# uses: actions-rs/cargo@v1
# with:
# command: build
# args: --release
# - name: Publish
# uses: actions-rs/cargo@v1
# with:
# command: publish
# env:
# CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
homebrew:
name: Bump Homebrew formula
runs-on: macos-latest

View file

@ -1 +1,2 @@
max_width = 120
max_width = 150
tab_spaces = 2

923
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,37 +17,37 @@ default = ["crossterm-backend"]
crossterm-backend = ["tui/crossterm", "crossterm"]
[dependencies]
anyhow = "1.0.56"
anyhow = "1.0.71"
better-panic = "0.3.0"
cassowary = "0.3.0"
chrono = "0.4.19"
clap = { version = "3.1.6", features = ["derive"] }
crossterm = { version = "0.25.0", optional = true, default-features = false, features = [
"event-stream"
chrono = "0.4.24"
clap = { version = "4.3.0", features = ["derive"] }
crossterm = { version = "0.26.1", optional = true, default-features = false, features = [
"event-stream",
] }
dirs = "4.0.0"
futures = "0.3.21"
itertools = "0.10.3"
dirs = "5.0.1"
futures = "0.3.28"
itertools = "0.10.5"
lazy_static = "1.4.0"
log = "0.4.14"
log4rs = "1.0.0"
path-clean = "0.1.0"
log = "0.4.17"
log4rs = "1.2.0"
path-clean = "1.0.1"
rand = "0.8.5"
regex = "1.5.5"
rustyline = "10.0.0"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
shellexpand = "2.1.0"
regex = "1.8.3"
rustyline = { version = "11.0.0", features = ["with-file-history", "derive"] }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
shellexpand = "3.1.0"
shlex = "1.1.0"
task-hookrs = { git = "https://github.com/kdheepak/task-hookrs" }
tokio = { version = "1.17.0", features = ["full"] }
tokio-stream = "0.1.3"
tui = { version = "0.19.0", optional = true, default-features = false }
unicode-segmentation = "1.9.0"
tokio = { version = "1.28.1", features = ["full"] }
tokio-stream = "0.1.14"
tui = { package = "ratatui", version = "0.20.1" }
unicode-segmentation = "1.10.1"
unicode-truncate = "0.2.0"
unicode-width = "0.1.9"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
versions = "4.0.0"
unicode-width = "0.1.10"
uuid = { version = "1.3.3", features = ["serde", "v4"] }
versions = "4.1.0"
[package.metadata.rpm]
package = "taskwarrior-tui"
@ -64,6 +64,6 @@ incremental = true
lto = "off"
[build-dependencies]
clap = { version = "3.1.6", features = ["derive"] }
clap_complete = "3.1.1"
clap = { version = "4.3.0", features = ["derive"] }
clap_complete = "4.3.0"
shlex = "1.1.0"

View file

@ -22,7 +22,6 @@ A Terminal User Interface (TUI) for [Taskwarrior](https://taskwarrior.org/) that
![](https://user-images.githubusercontent.com/1813121/159858280-3ca31e9a-fc38-4547-a92d-36a7758cf5dc.gif)
### Showcase
<details>
@ -125,7 +124,11 @@ uda.taskwarrior-tui.report.next.filter=(status:pending or status:waiting)
### References / Resources
If you like `taskwarrior-tui`, please consider donating to [me](https://github.com/sponsors/kdheepak), [`@GothenburgBitFactory`](https://github.com/sponsors/GothenburgBitFactory) or a charity of your choice.
If you like `taskwarrior-tui`, please consider donating to
- [`kdheepak`](https://github.com/sponsors/kdheepak)
- [`@GothenburgBitFactory`](https://github.com/sponsors/GothenburgBitFactory)
- and/or a charity of your choice.
<details>
<summary>Additional resources</summary>

View file

@ -2,32 +2,32 @@
use std::process::{Command, Output};
use clap_complete::{
generate_to,
shells::{Bash, Fish, PowerShell, Zsh},
generate_to,
shells::{Bash, Fish, PowerShell, Zsh},
};
include!("src/cli.rs");
fn run_pandoc() -> Result<Output, std::io::Error> {
let mut cmd = Command::new("pandoc");
if let Some(args) = shlex::split("--standalone --to=man docs/taskwarrior-tui.1.md -o docs/taskwarrior-tui.1") {
for arg in args {
cmd.arg(arg);
}
let mut cmd = Command::new("pandoc");
if let Some(args) = shlex::split("--standalone --to=man docs/taskwarrior-tui.1.md -o docs/taskwarrior-tui.1") {
for arg in args {
cmd.arg(arg);
}
cmd.output()
}
cmd.output()
}
fn main() {
let mut app = generate_cli_app();
let name = app.get_name().to_string();
let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/");
dbg!(&outdir);
generate_to(Bash, &mut app, &name, &outdir).unwrap();
generate_to(Zsh, &mut app, &name, &outdir).unwrap();
generate_to(Fish, &mut app, &name, &outdir).unwrap();
generate_to(PowerShell, &mut app, &name, &outdir).unwrap();
if run_pandoc().is_err() {
dbg!("Unable to run pandoc to generate man page documentation");
}
let mut app = generate_cli_app();
let name = app.get_name().to_string();
let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("completions/");
dbg!(&outdir);
generate_to(Bash, &mut app, &name, &outdir).unwrap();
generate_to(Zsh, &mut app, &name, &outdir).unwrap();
generate_to(Fish, &mut app, &name, &outdir).unwrap();
generate_to(PowerShell, &mut app, &name, &outdir).unwrap();
if run_pandoc().is_err() {
dbg!("Unable to run pandoc to generate man page documentation");
}
}

View file

@ -23,10 +23,10 @@ _taskwarrior-tui() {
'--taskrc=[Sets the .taskrc file using the TASKRC environment variable for taskwarrior]:FILE: ' \
'-r+[Sets default report]:STRING: ' \
'--report=[Sets default report]:STRING: ' \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
}
@ -36,4 +36,8 @@ _taskwarrior-tui_commands() {
_describe -t commands 'taskwarrior-tui commands' commands "$@"
}
_taskwarrior-tui "$@"
if [ "$funcstack[1]" = "_taskwarrior-tui" ]; then
_taskwarrior-tui "$@"
else
compdef _taskwarrior-tui taskwarrior-tui
fi

View file

@ -29,10 +29,10 @@ Register-ArgumentCompleter -Native -CommandName 'taskwarrior-tui' -ScriptBlock {
[CompletionResult]::new('--taskrc', 'taskrc', [CompletionResultType]::ParameterName, 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Sets default report')
[CompletionResult]::new('--report', 'report', [CompletionResultType]::ParameterName, 'Sets default report')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})

View file

@ -1,5 +1,5 @@
_taskwarrior-tui() {
local i cur prev opts cmds
local i cur prev opts cmd
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
@ -8,8 +8,8 @@ _taskwarrior-tui() {
for i in ${COMP_WORDS[@]}
do
case "${i}" in
"$1")
case "${cmd},${i}" in
",$1")
cmd="taskwarrior__tui"
;;
*)
@ -19,7 +19,7 @@ _taskwarrior-tui() {
case "${cmd}" in
taskwarrior__tui)
opts="-h -V -d -c -r --help --version --data --config --taskdata --taskrc --report"
opts="-d -c -r -h -V --data --config --taskdata --taskrc --report --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View file

@ -3,5 +3,5 @@ complete -c taskwarrior-tui -s c -l config -d 'Sets the config folder for taskwa
complete -c taskwarrior-tui -l taskdata -d 'Sets the .task folder using the TASKDATA environment variable for taskwarrior' -r
complete -c taskwarrior-tui -l taskrc -d 'Sets the .taskrc file using the TASKRC environment variable for taskwarrior' -r
complete -c taskwarrior-tui -s r -l report -d 'Sets default report' -r
complete -c taskwarrior-tui -s h -l help -d 'Print help information'
complete -c taskwarrior-tui -s V -l version -d 'Print version information'
complete -c taskwarrior-tui -s h -l help -d 'Print help'
complete -c taskwarrior-tui -s V -l version -d 'Print version'

2
justfile Normal file
View file

@ -0,0 +1,2 @@
clean:
rm -rf tests/data/.task tests/data/.config

View file

@ -1,16 +1,16 @@
#[derive(Clone, PartialEq, Eq, Debug, Copy)]
pub enum Action {
Report,
Filter,
Add,
Annotate,
Subprocess,
Log,
Modify,
HelpPopup,
ContextMenu,
Jump,
DeletePrompt,
DonePrompt,
Error,
Report,
Filter,
Add,
Annotate,
Subprocess,
Log,
Modify,
HelpPopup,
ContextMenu,
Jump,
DeletePrompt,
DonePrompt,
Error,
}

9131
src/app.rs

File diff suppressed because it is too large Load diff

View file

@ -5,275 +5,265 @@ use std::fmt;
const COL_WIDTH: usize = 21;
use chrono::{format::Fixed, Date, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
use chrono::{format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
use tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
symbols,
widgets::{Block, Widget},
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
symbols,
widgets::{Block, Widget},
};
use std::cmp::min;
#[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<(Date<FixedOffset>, Style)>,
pub today_style: Style,
pub start_on_monday: bool,
pub title_background_color: Color,
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::today().year();
let month = Local::today().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,
}
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 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 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 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 month(mut self, month: u32) -> Self {
self.month = month;
self
}
pub fn date_style(mut self, date_style: Vec<(Date<FixedOffset>, Style)>) -> Self {
self.date_style = date_style;
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 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 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
}
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);
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,
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();
if area.height < 7 {
return;
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 style = self.style;
let today = Local::today();
let year = self.year;
let month = self.month;
let months: Vec<_> = (0..12).collect();
let mut days: Vec<(Date<FixedOffset>, Date<FixedOffset>)> = months
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 = Date::from_utc(NaiveDate::from_ymd(year, i + 1, 1), *Local::now().offset());
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)))
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();
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;
.collect::<Vec<_>>(),
);
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);
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() {
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 as u16, 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::today() {
buf.set_string(x, y, s, self.today_style);
} else {
buf.set_string(x, y, s, style);
}
x += 3;
d.1 = Date::from_utc(d.1.naive_local() + Duration::days(1), *Local::now().offset());
}
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 = Date::from_utc(
NaiveDate::from_ymd(self.year + new_year as i32, i + 1, 1),
*Local::now().offset(),
);
(
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;
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
}
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
}
}

View file

@ -3,50 +3,50 @@ use clap::Arg;
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
const APP_NAME: &str = env!("CARGO_PKG_NAME");
pub fn generate_cli_app() -> clap::Command<'static> {
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")
.takes_value(true),
)
.arg(
Arg::new("config")
.short('c')
.long("config")
.value_name("FOLDER")
.help("Sets the config folder for taskwarrior-tui (currently not used)")
.takes_value(true),
)
.arg(
Arg::new("taskdata")
.long("taskdata")
.value_name("FOLDER")
.help("Sets the .task folder using the TASKDATA environment variable for taskwarrior")
.takes_value(true),
)
.arg(
Arg::new("taskrc")
.long("taskrc")
.value_name("FILE")
.help("Sets the .taskrc file using the TASKRC environment variable for taskwarrior")
.takes_value(true),
)
.arg(
Arg::new("report")
.short('r')
.long("report")
.value_name("STRING")
.help("Sets default report")
.takes_value(true),
);
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 (currently not used)")
.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),
);
app.set_bin_name(APP_NAME);
app
app.set_bin_name(APP_NAME);
app
}

View file

@ -1,207 +1,201 @@
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
use std::{error::Error, io};
use tui::{
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::Hinter;
use rustyline::line_buffer::LineBuffer;
use rustyline::Context;
use rustyline::{error::ReadlineError, history::FileHistory};
use unicode_segmentation::Graphemes;
use unicode_segmentation::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();
let mut chars = line[..cursor_pos].chars();
let mut res = cursor_pos;
while let Some(c) = chars.next_back() {
if c == ' ' || c == '(' || c == ')' {
break;
}
// if iter == None, res == 0.
res
res -= c.len_utf8();
}
// if iter == None, res == 0.
res
}
pub struct TaskwarriorTuiCompletionHelper {
pub candidates: Vec<(String, String)>,
pub context: String,
pub input: String,
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))
}
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,
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 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 with_items(items: Vec<(String, String)>) -> CompletionList {
let mut candidates = vec![];
for i in items {
if !candidates.contains(&i) {
candidates.push(i);
}
}
pub fn insert(&mut self, item: (String, String)) {
if !self.helper.candidates.contains(&item) {
self.helper.candidates.push(item);
}
let context = String::new();
let input = String::new();
CompletionList {
state: ListState::default(),
current: String::new(),
pos: 0,
helper: TaskwarriorTuiCompletionHelper { candidates, context, input },
}
}
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 insert(&mut self, item: (String, String)) {
if !self.helper.candidates.contains(&item) {
self.helper.candidates.push(item);
}
}
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())
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.candidates().len() - 1 {
0
} else {
None
i + 1
}
}
}
None => 0,
};
self.state.select(Some(i));
}
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 = rustyline::history::History::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();
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.candidates().len() - 1
} else {
self.current = current;
self.helper.context = "attribute".to_string();
i - 1
}
self.pos = self.current.len();
}
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();
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,133 +5,125 @@ use serde::{Deserialize, Serialize};
use tokio::{sync::mpsc, sync::oneshot, task::JoinHandle};
use crossterm::event::{
KeyCode::{
BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, Home, Insert, Left, Null, PageDown, PageUp, Right,
Tab, Up, F,
},
KeyModifiers,
KeyCode::{BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, Home, Insert, Left, Null, PageDown, PageUp, Right, Tab, Up, F},
KeyModifiers,
};
#[derive(Debug, Clone, Copy)]
pub enum Event<I> {
Input(I),
Tick,
Closed,
Input(I),
Tick,
Closed,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq)]
pub enum KeyCode {
CtrlBackspace,
CtrlDelete,
AltBackspace,
AltDelete,
Backspace,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
BackTab,
Delete,
Insert,
F(u8),
Char(char),
Alt(char),
Ctrl(char),
Null,
Esc,
Tab,
CtrlBackspace,
CtrlDelete,
AltBackspace,
AltDelete,
Backspace,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
BackTab,
Delete,
Insert,
F(u8),
Char(char),
Alt(char),
Ctrl(char),
Null,
Esc,
Tab,
}
pub struct EventLoop {
pub rx: mpsc::UnboundedReceiver<Event<KeyCode>>,
pub tx: mpsc::UnboundedSender<Event<KeyCode>>,
pub abort: mpsc::UnboundedSender<()>,
pub tick_rate: std::time::Duration,
pub rx: mpsc::UnboundedReceiver<Event<KeyCode>>,
pub tx: mpsc::UnboundedSender<Event<KeyCode>>,
pub abort: mpsc::UnboundedSender<()>,
pub tick_rate: std::time::Duration,
}
impl EventLoop {
pub fn new(tick_rate: Option<std::time::Duration>, init: bool) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let _tx = tx.clone();
let should_tick = tick_rate.is_some();
let tick_rate = tick_rate.unwrap_or(std::time::Duration::from_millis(250));
pub fn new(tick_rate: Option<std::time::Duration>, init: bool) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let _tx = tx.clone();
let should_tick = tick_rate.is_some();
let tick_rate = tick_rate.unwrap_or(std::time::Duration::from_millis(250));
let (abort, mut abort_recv) = mpsc::unbounded_channel();
let (abort, mut abort_recv) = mpsc::unbounded_channel();
if init {
let mut reader = crossterm::event::EventStream::new();
tokio::spawn(async move {
loop {
let delay = tokio::time::sleep(tick_rate);
let event = reader.next();
if init {
let mut reader = crossterm::event::EventStream::new();
tokio::spawn(async move {
loop {
let delay = tokio::time::sleep(tick_rate);
let event = reader.next();
tokio::select! {
_ = abort_recv.recv() => {
_tx.send(Event::Closed).unwrap_or_else(|_| warn!("Unable to send Closed event"));
_tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
break;
},
_ = delay, if should_tick => {
_tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
},
_ = _tx.closed() => break,
maybe_event = event => {
if let Some(Ok(crossterm::event::Event::Key(key))) = maybe_event {
let key = match key.code {
Backspace => {
match key.modifiers {
KeyModifiers::CONTROL => KeyCode::CtrlBackspace,
KeyModifiers::ALT => KeyCode::AltBackspace,
_ => KeyCode::Backspace,
}
},
Delete => {
match key.modifiers {
KeyModifiers::CONTROL => KeyCode::CtrlDelete,
KeyModifiers::ALT => KeyCode::AltDelete,
_ => KeyCode::Delete,
}
},
Enter => KeyCode::Char('\n'),
Left => KeyCode::Left,
Right => KeyCode::Right,
Up => KeyCode::Up,
Down => KeyCode::Down,
Home => KeyCode::Home,
End => KeyCode::End,
PageUp => KeyCode::PageUp,
PageDown => KeyCode::PageDown,
Tab => KeyCode::Tab,
BackTab => KeyCode::BackTab,
Insert => KeyCode::Insert,
F(k) => KeyCode::F(k),
Null => KeyCode::Null,
Esc => KeyCode::Esc,
Char(c) => match key.modifiers {
KeyModifiers::NONE | KeyModifiers::SHIFT => KeyCode::Char(c),
KeyModifiers::CONTROL => KeyCode::Ctrl(c),
KeyModifiers::ALT => KeyCode::Alt(c),
_ => KeyCode::Null,
},
_ => KeyCode::Null,
};
_tx.send(Event::Input(key)).unwrap_or_else(|_| warn!("Unable to send {:?} event", key));
}
}
}
}
});
}
Self {
tx,
rx,
tick_rate,
abort,
tokio::select! {
_ = abort_recv.recv() => {
_tx.send(Event::Closed).unwrap_or_else(|_| warn!("Unable to send Closed event"));
_tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
break;
},
_ = delay, if should_tick => {
_tx.send(Event::Tick).unwrap_or_else(|_| warn!("Unable to send Tick event"));
},
_ = _tx.closed() => break,
maybe_event = event => {
if let Some(Ok(crossterm::event::Event::Key(key))) = maybe_event {
let key = match key.code {
Backspace => {
match key.modifiers {
KeyModifiers::CONTROL => KeyCode::CtrlBackspace,
KeyModifiers::ALT => KeyCode::AltBackspace,
_ => KeyCode::Backspace,
}
},
Delete => {
match key.modifiers {
KeyModifiers::CONTROL => KeyCode::CtrlDelete,
KeyModifiers::ALT => KeyCode::AltDelete,
_ => KeyCode::Delete,
}
},
Enter => KeyCode::Char('\n'),
Left => KeyCode::Left,
Right => KeyCode::Right,
Up => KeyCode::Up,
Down => KeyCode::Down,
Home => KeyCode::Home,
End => KeyCode::End,
PageUp => KeyCode::PageUp,
PageDown => KeyCode::PageDown,
Tab => KeyCode::Tab,
BackTab => KeyCode::BackTab,
Insert => KeyCode::Insert,
F(k) => KeyCode::F(k),
Null => KeyCode::Null,
Esc => KeyCode::Esc,
Char(c) => match key.modifiers {
KeyModifiers::NONE | KeyModifiers::SHIFT => KeyCode::Char(c),
KeyModifiers::CONTROL => KeyCode::Ctrl(c),
KeyModifiers::ALT => KeyCode::Alt(c),
_ => KeyCode::Null,
},
_ => KeyCode::Null,
};
_tx.send(Event::Input(key)).unwrap_or_else(|_| warn!("Unable to send {:?} event", key));
}
}
}
}
});
}
Self { tx, rx, tick_rate, abort }
}
}

View file

@ -1,49 +1,49 @@
use std::cmp;
use tui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget},
buffer::Buffer,
layout::{Alignment, Rect},
style::{Modifier, Style},
text::{Span, Spans, 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,
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(),
}
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()
}
fn default() -> Self {
Self::new()
}
}
impl Widget for &Help {
fn render(self, area: Rect, buf: &mut Buffer) {
let text: Vec<Spans> = TEXT.lines().map(|line| Spans::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);
}
fn render(self, area: Rect, buf: &mut Buffer) {
let text: Vec<Spans> = TEXT.lines().map(|line| Spans::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);
}
}

View file

@ -1,126 +1,134 @@
use anyhow::{anyhow, Result};
use rustyline::error::ReadlineError;
use rustyline::history::DefaultHistory;
use rustyline::history::History;
use rustyline::history::SearchDirection;
use std::fs::File;
use std::path::{Path, PathBuf};
pub struct HistoryContext {
history: History,
history_index: Option<usize>,
data_path: PathBuf,
history: DefaultHistory,
history_index: Option<usize>,
data_path: PathBuf,
}
impl HistoryContext {
pub fn new(filename: &str) -> Self {
let history = History::new();
pub fn new(filename: &str) -> Self {
let history = DefaultHistory::new();
let data_path = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.map(|d| d.join("taskwarrior-tui"))
.expect("Unable to create configuration directory for taskwarrior-tui")
};
let data_path = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.map(|d| d.join("taskwarrior-tui"))
.expect("Unable to create configuration directory for taskwarrior-tui")
};
std::fs::create_dir_all(&data_path)
.unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path));
std::fs::create_dir_all(&data_path).unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path));
let data_path = data_path.join(filename);
let data_path = data_path.join(filename);
Self {
history,
history_index: None,
data_path,
}
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;
}
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(())
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 write(&mut self) -> Result<()> {
self.history.save(&self.data_path)?;
Ok(())
}
pub fn reset(&mut self) {
self.history_index = None
}
pub fn history(&self) -> &History {
&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) {
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);
self.history.get(history_index).cloned()
} else {
log::debug!("History index = {}. Found no match.", history_index);
None
};
}
pub fn add(&mut self, buf: &str) {
if self.history.add(buf) {
self.reset();
}
}
pub fn reset(&mut self) {
self.history_index = None
}
pub fn history_len(&self) -> usize {
self.history.len()
}
pub fn history_len(&self) -> usize {
self.history.len()
}
}

View file

@ -7,216 +7,212 @@ use std::hash::Hash;
#[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,
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'),
}
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 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"))
}
}
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 line.len() == 1 {
return Some(KeyCode::Char(line.chars().next().unwrap()));
}
}
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 line.len() == 1 {
return Some(KeyCode::Char(line.chars().next().unwrap()));
}
} else if line.starts_with(&config.replace('-', "_")) {
let line = line
.trim_start_matches(&config.replace('-', "_"))
.trim_start()
.trim_end()
.to_string();
if line.len() == 1 {
return Some(KeyCode::Char(line.chars().next().unwrap()));
}
}
} else if line.starts_with(&config.replace('-', "_")) {
let line = line.trim_start_matches(&config.replace('-', "_")).trim_start().trim_end().to_string();
if line.len() == 1 {
return Some(KeyCode::Char(line.chars().next().unwrap()));
}
None
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
}

View file

@ -18,6 +18,7 @@ mod scrollbar;
mod table;
mod task_report;
mod ui;
mod utils;
use log::{debug, error, info, log_enabled, trace, warn, Level, LevelFilter};
use log4rs::append::file::FileAppender;
@ -32,10 +33,10 @@ use std::time::Duration;
use anyhow::Result;
use crossterm::{
cursor,
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
execute,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
cursor,
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
execute,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::stream::{FuturesUnordered, StreamExt};
use tui::{backend::CrosstermBackend, Terminal};
@ -51,150 +52,142 @@ use crate::keyconfig::KeyConfig;
const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S)} | {l} | {f}:{L} | {m}{n}";
pub fn destruct_terminal() {
disable_raw_mode().unwrap();
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
execute!(io::stdout(), cursor::Show).unwrap();
disable_raw_mode().unwrap();
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
execute!(io::stdout(), cursor::Show).unwrap();
}
pub fn initialize_logging() {
let data_local_dir = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.expect("Unable to find data directory for taskwarrior-tui")
.join("taskwarrior-tui")
};
let data_local_dir = if let Ok(s) = std::env::var("TASKWARRIOR_TUI_DATA") {
PathBuf::from(s)
} else {
dirs::data_local_dir()
.expect("Unable to find data directory for taskwarrior-tui")
.join("taskwarrior-tui")
};
std::fs::create_dir_all(&data_local_dir).unwrap_or_else(|_| panic!("Unable to create {:?}", data_local_dir));
std::fs::create_dir_all(&data_local_dir).unwrap_or_else(|_| panic!("Unable to create {:?}", data_local_dir));
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(LOG_PATTERN)))
.append(false)
.build(data_local_dir.join("taskwarrior-tui.log"))
.expect("Failed to build log file appender.");
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(LOG_PATTERN)))
.append(false)
.build(data_local_dir.join("taskwarrior-tui.log"))
.expect("Failed to build log file appender.");
let levelfilter = match std::env::var("TASKWARRIOR_TUI_LOG_LEVEL")
.unwrap_or_else(|_| "info".to_string())
.as_str()
{
"off" => LevelFilter::Off,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.logger(Logger::builder().build("taskwarrior_tui", levelfilter))
.build(Root::builder().appender("logfile").build(LevelFilter::Info))
.expect("Failed to build logging config.");
let levelfilter = match std::env::var("TASKWARRIOR_TUI_LOG_LEVEL").unwrap_or_else(|_| "info".to_string()).as_str() {
"off" => LevelFilter::Off,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.logger(Logger::builder().build("taskwarrior_tui", levelfilter))
.build(Root::builder().appender("logfile").build(LevelFilter::Info))
.expect("Failed to build logging config.");
log4rs::init_config(config).expect("Failed to initialize logging.");
log4rs::init_config(config).expect("Failed to initialize logging.");
}
pub fn absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = path.as_ref();
let path = path.as_ref();
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
env::current_dir()?.join(path)
}
.clean();
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
env::current_dir()?.join(path)
}
.clean();
Ok(absolute_path)
Ok(absolute_path)
}
async fn tui_main(report: &str) -> Result<()> {
panic::set_hook(Box::new(|panic_info| {
destruct_terminal();
better_panic::Settings::auto().create_panic_handler()(panic_info);
}));
panic::set_hook(Box::new(|panic_info| {
destruct_terminal();
better_panic::Settings::auto().create_panic_handler()(panic_info);
}));
let mut app = app::TaskwarriorTui::new(report, true).await?;
let mut app = app::TaskwarriorTui::new(report, true).await?;
let mut terminal = app.start_tui()?;
let mut terminal = app.start_tui()?;
let r = app.run(&mut terminal).await;
let r = app.run(&mut terminal).await;
app.pause_tui().await?;
app.pause_tui().await?;
r
r
}
fn main() -> Result<()> {
better_panic::install();
better_panic::install();
let matches = cli::generate_cli_app().get_matches();
let matches = cli::generate_cli_app().get_matches();
let config = matches.value_of("config");
let data = matches.value_of("data");
let taskrc = matches.value_of("taskrc");
let taskdata = matches.value_of("taskdata");
let report = matches.value_of("report").unwrap_or("next");
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);
if let Some(e) = config {
if env::var("TASKWARRIOR_TUI_CONFIG").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKWARRIOR_TUI_CONFIG",
absolute_path(PathBuf::from(e)).expect("Unable to get path for config"),
)
} else {
warn!("TASKWARRIOR_TUI_CONFIG environment variable cannot be set.")
}
if let Some(e) = config {
if env::var("TASKWARRIOR_TUI_CONFIG").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKWARRIOR_TUI_CONFIG",
absolute_path(PathBuf::from(e)).expect("Unable to get path for config"),
)
} else {
warn!("TASKWARRIOR_TUI_CONFIG environment variable cannot be set.")
}
}
if let Some(e) = data {
if env::var("TASKWARRIOR_TUI_DATA").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKWARRIOR_TUI_DATA",
absolute_path(PathBuf::from(e)).expect("Unable to get path for data"),
)
} else {
warn!("TASKWARRIOR_TUI_DATA environment variable cannot be set.")
}
if let Some(e) = data {
if env::var("TASKWARRIOR_TUI_DATA").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKWARRIOR_TUI_DATA",
absolute_path(PathBuf::from(e)).expect("Unable to get path for data"),
)
} else {
warn!("TASKWARRIOR_TUI_DATA environment variable cannot be set.")
}
}
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 {
warn!("TASKRC environment variable cannot be set.")
}
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 {
warn!("TASKRC environment variable cannot be set.")
}
}
if let Some(e) = taskdata {
if env::var("TASKDATA").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var(
"TASKDATA",
absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"),
)
} else {
warn!("TASKDATA environment variable cannot be set.")
}
if let Some(e) = taskdata {
if env::var("TASKDATA").is_err() {
// if environment variable is not set, this env::var returns an error
env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"))
} else {
warn!("TASKDATA environment variable cannot be set.")
}
}
initialize_logging();
initialize_logging();
debug!("getting matches from clap...");
debug!("report = {:?}", &report);
debug!("config = {:?}", &config);
debug!("getting matches from clap...");
debug!("report = {:?}", &report);
debug!("config = {:?}", &config);
let r = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async { tui_main(report).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);
}
Ok(())
let r = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async { tui_main(report).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);
}
Ok(())
}

View file

@ -10,12 +10,12 @@ const ACTIVE: &str = "Complete";
use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
use tui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Clear, Paragraph, StatefulWidget, Widget},
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Clear, Paragraph, StatefulWidget, Widget},
};
use crate::action::Action;
@ -33,113 +33,98 @@ use uuid::Uuid;
#[derive(Debug, Clone, Default)]
pub struct ContextDetails {
pub name: String,
pub definition: String,
pub active: String,
pub type_: String,
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 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>,
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(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);
}
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(())
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(())
}
}

View file

@ -9,29 +9,29 @@ pub mod context;
pub mod project;
pub trait Pane {
fn handle_input(app: &mut TaskwarriorTui, input: KeyCode) -> Result<()>;
fn change_focus_to_left_pane(app: &mut TaskwarriorTui) {
match app.mode {
Mode::Tasks(_) => {
if app.config.uda_change_focus_rotate {
app.mode = Mode::Calendar;
}
}
Mode::Projects => app.mode = Mode::Tasks(Action::Report),
Mode::Calendar => {
app.mode = Mode::Projects;
}
fn handle_input(app: &mut TaskwarriorTui, input: KeyCode) -> Result<()>;
fn change_focus_to_left_pane(app: &mut TaskwarriorTui) {
match app.mode {
Mode::Tasks(_) => {
if app.config.uda_change_focus_rotate {
app.mode = Mode::Calendar;
}
}
Mode::Projects => app.mode = Mode::Tasks(Action::Report),
Mode::Calendar => {
app.mode = Mode::Projects;
}
}
fn change_focus_to_right_pane(app: &mut TaskwarriorTui) {
match app.mode {
Mode::Tasks(_) => app.mode = Mode::Projects,
Mode::Projects => app.mode = Mode::Calendar,
Mode::Calendar => {
if app.config.uda_change_focus_rotate {
app.mode = Mode::Tasks(Action::Report);
}
}
}
fn change_focus_to_right_pane(app: &mut TaskwarriorTui) {
match app.mode {
Mode::Tasks(_) => app.mode = Mode::Projects,
Mode::Projects => app.mode = Mode::Calendar,
Mode::Calendar => {
if app.config.uda_change_focus_rotate {
app.mode = Mode::Tasks(Action::Report);
}
}
}
}
}

View file

@ -11,11 +11,11 @@ const COMPLETE_HEADER: &str = "Complete";
use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
use tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
symbols,
widgets::{Block, Widget},
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
symbols,
widgets::{Block, Widget},
};
use crate::action::Action;
@ -23,6 +23,7 @@ use crate::app::{Mode, TaskwarriorTui};
use crate::event::KeyCode;
use crate::pane::Pane;
use crate::table::TableState;
use crate::utils::Changeset;
use itertools::Itertools;
use std::cmp::min;
use std::collections::{HashMap, HashSet};
@ -32,165 +33,156 @@ use task_hookrs::project::Project;
use uuid::Uuid;
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,
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,
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![],
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();
}
}
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();
if idx == 0 {
project_pattern = format!("\'(project:{}", input);
} 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);
}
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: KeyCode) -> Result<()> {
if input == app.keyconfig.quit || input == KeyCode::Ctrl('c') {
app.should_quit = true;
} else if input == app.keyconfig.next_tab {
Self::change_focus_to_right_pane(app);
} else if input == app.keyconfig.previous_tab {
Self::change_focus_to_left_pane(app);
} else if input == KeyCode::Down || input == app.keyconfig.down {
self::focus_on_next_project(app);
} else if input == KeyCode::Up || input == app.keyconfig.up {
self::focus_on_previous_project(app);
} else if input == app.keyconfig.select {
self::update_task_filter_by_selection(app)?;
}
app.projects.update_table_state();
Ok(())
fn handle_input(app: &mut TaskwarriorTui, input: KeyCode) -> Result<()> {
if input == app.keyconfig.quit || input == KeyCode::Ctrl('c') {
app.should_quit = true;
} else if input == app.keyconfig.next_tab {
Self::change_focus_to_right_pane(app);
} else if input == app.keyconfig.previous_tab {
Self::change_focus_to_left_pane(app);
} else if input == KeyCode::Down || input == app.keyconfig.down {
self::focus_on_next_project(app);
} else if input == KeyCode::Up || input == app.keyconfig.up {
self::focus_on_previous_project(app);
} else if input == 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));
}
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));
}
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.as_str();
app.filter_history.add(current_filter);
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.as_str();
app.filter_history.add(current_filter);
let mut filter = current_filter.replace(&last_project_pattern, "");
filter = format!("{}{}", filter, new_project_pattern);
app.filter.update(filter.as_str(), filter.len());
Ok(())
let mut filter = current_filter.replace(&last_project_pattern, "");
filter = format!("{}{}", filter, new_project_pattern);
app.filter.update(filter.as_str(), filter.len(), &mut Changeset::default());
Ok(())
}

View file

@ -1,63 +1,63 @@
use tui::{
backend::Backend,
buffer::Buffer,
layout::{Margin, Rect},
style::{Color, Style},
symbols::{block::FULL, line::DOUBLE_VERTICAL},
widgets::Widget,
Frame,
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,
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(),
}
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);
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);
}
}

View file

@ -1,19 +1,19 @@
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
WeightedRelation::{EQ, GE, LE},
{Expression, Solver},
strength::{MEDIUM, REQUIRED, WEAK},
WeightedRelation::{EQ, GE, LE},
{Expression, Solver},
};
use std::collections::HashSet;
use std::{
collections::HashMap,
fmt::Display,
iter::{self, Iterator},
collections::HashMap,
fmt::Display,
iter::{self, Iterator},
};
use tui::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
widgets::{Block, StatefulWidget, Widget},
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
widgets::{Block, StatefulWidget, Widget},
};
use unicode_segmentation::Graphemes;
use unicode_segmentation::UnicodeSegmentation;
@ -21,91 +21,91 @@ use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
pub enum TableMode {
SingleSelection,
MultipleSelection,
SingleSelection,
MultipleSelection,
}
#[derive(Clone)]
pub struct TableState {
offset: usize,
current_selection: Option<usize>,
marked: HashSet<usize>,
mode: TableMode,
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,
}
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 mode(&self) -> TableMode {
self.mode.clone()
}
pub fn multiple_selection(&mut self) {
self.mode = TableMode::MultipleSelection;
}
pub fn multiple_selection(&mut self) {
self.mode = TableMode::MultipleSelection;
}
pub fn single_selection(&mut self) {
self.mode = TableMode::SingleSelection;
}
pub fn single_selection(&mut self) {
self.mode = TableMode::SingleSelection;
}
pub fn current_selection(&self) -> Option<usize> {
self.current_selection
}
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 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 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 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 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 marked(&self) -> std::collections::hash_set::Iter<usize> {
self.marked.iter()
}
pub fn clear(&mut self) {
self.marked.drain().for_each(drop);
}
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,
D: Iterator,
D::Item: Display,
{
Data(D),
StyledData(D, Style),
Data(D),
StyledData(D, Style),
}
/// A widget to display data in formatted columns
@ -134,409 +134,403 @@ where
/// ```
#[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,
/// 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,
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(),
}
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>>,
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 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<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 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 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 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 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 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 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 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 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_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 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 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
}
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>>,
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
type State = TableState;
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style);
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,
};
// 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;
}
}
}
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>>,
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);
}
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}

View file

@ -9,474 +9,439 @@ 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()
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_utc(dt, offset);
dt.format("%Y-%m-%d").to_string()
let offset = Local.offset_from_utc_datetime(&dt);
let dt = DateTime::<Local>::from_utc(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;
"-"
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)
};
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)
} 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,
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 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());
}
}
}
pub fn export_headers(&mut self, data: Option<&str>, report: &str) -> Result<()> {
self.columns = vec![];
self.labels = vec![];
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);
let data = if let Some(s) = data {
s.to_string()
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 {
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()
Default::default()
};
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());
}
}
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 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());
}
}
let (d, _) = d.unicode_truncate(available_width);
let mut d = d.to_string();
if d != *task.description() {
d = format!("{}\u{2026}", d);
}
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(),
}
}
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(),
}
}
}
}
}

View file

@ -1,37 +1,37 @@
use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::{Block, BorderType, Borders, Cell, LineGauge, Paragraph, Row, Table},
Frame,
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
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,
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 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]);
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),
)
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),
)
}

19
src/utils.rs Normal file
View file

@ -0,0 +1,19 @@
use rustyline::line_buffer::ChangeListener;
use rustyline::line_buffer::DeleteListener;
use rustyline::line_buffer::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) {}
}