mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-23 11:07:45 +02:00
Merge pull request #495 from kdheepak/update
feat: Update dependencies ✨
This commit is contained in:
commit
dee5c0c5f6
30 changed files with 7977 additions and 8104 deletions
13
.github/workflows/cd.yml
vendored
13
.github/workflows/cd.yml
vendored
|
@ -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
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
max_width = 120
|
||||
max_width = 150
|
||||
tab_spaces = 2
|
||||
|
|
923
Cargo.lock
generated
923
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
50
Cargo.toml
50
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -22,7 +22,6 @@ A Terminal User Interface (TUI) for [Taskwarrior](https://taskwarrior.org/) that
|
|||
|
||||

|
||||
|
||||
|
||||
### 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>
|
||||
|
|
38
build.rs
38
build.rs
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
2
justfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
clean:
|
||||
rm -rf tests/data/.task tests/data/.config
|
|
@ -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
9131
src/app.rs
File diff suppressed because it is too large
Load diff
450
src/calendar.rs
450
src/calendar.rs
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
90
src/cli.rs
90
src/cli.rs
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
1508
src/config.rs
1508
src/config.rs
File diff suppressed because it is too large
Load diff
212
src/event.rs
212
src/event.rs
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
60
src/help.rs
60
src/help.rs
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
218
src/history.rs
218
src/history.rs
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
388
src/keyconfig.rs
388
src/keyconfig.rs
|
@ -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::*;
|
||||
}
|
||||
|
|
221
src/main.rs
221
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
100
src/scrollbar.rs
100
src/scrollbar.rs
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
856
src/table.rs
856
src/table.rs
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
48
src/ui.rs
48
src/ui.rs
|
@ -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
19
src/utils.rs
Normal 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) {}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue