alle Funktionen implementiert
This commit is contained in:
@@ -14,4 +14,5 @@ directories = "6.0.0"
|
|||||||
rusqlite = { version = "0.35.0", features = ["chrono", "bundled"] }
|
rusqlite = { version = "0.35.0", features = ["chrono", "bundled"] }
|
||||||
langtime = "0.2.1"
|
langtime = "0.2.1"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
tabled = "0.19.0"
|
tabled = "0.19.0"
|
||||||
|
serde_json = "1.0.140"
|
||||||
205
src/commands/display.rs
Normal file
205
src/commands/display.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Duration, Local};
|
||||||
|
use serde_json::to_string_pretty;
|
||||||
|
use tabled::builder::Builder;
|
||||||
|
use tabled::settings::object::Rows;
|
||||||
|
use tabled::settings::themes::Colorization;
|
||||||
|
use tabled::settings::{Border, Color, Padding, Style};
|
||||||
|
|
||||||
|
|
||||||
|
use crate::database::get_sheet_entries;
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::State;
|
||||||
|
use crate::Entry;
|
||||||
|
use crate::utils::{day_begin, day_end, format_duration, is_same_day};
|
||||||
|
|
||||||
|
|
||||||
|
pub struct ReadableOptions {
|
||||||
|
pub show_ids: bool,
|
||||||
|
pub show_timesheet: bool,
|
||||||
|
pub show_partial_sum: bool,
|
||||||
|
pub show_total: bool,
|
||||||
|
pub show_headings: bool,
|
||||||
|
//pub padding: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadableOptions {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
show_ids: false,
|
||||||
|
show_timesheet: false,
|
||||||
|
show_partial_sum : false,
|
||||||
|
show_total : false,
|
||||||
|
show_headings : false,
|
||||||
|
// padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete() -> Self {
|
||||||
|
Self {
|
||||||
|
show_ids: true,
|
||||||
|
show_timesheet: true,
|
||||||
|
show_partial_sum: true,
|
||||||
|
show_total: true,
|
||||||
|
show_headings: true,
|
||||||
|
// padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_tasks(
|
||||||
|
print_json: &bool,
|
||||||
|
sheet: Option<&String>,
|
||||||
|
start: Option<DateTime<Local>>,
|
||||||
|
end: Option<DateTime<Local>>,
|
||||||
|
filter_by_date: &bool,
|
||||||
|
ids: &bool,
|
||||||
|
state: &State,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Getting the data
|
||||||
|
let sheet = sheet.unwrap_or(&state.current_sheet);
|
||||||
|
let mut entries = get_sheet_entries(sheet, &state.database)?;
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("No sheet found with name:", Styles::Message),
|
||||||
|
sheet
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
entries.sort_by(|a, b| a.start.cmp(&b.start));
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
let mut start = start;
|
||||||
|
let mut end = end;
|
||||||
|
|
||||||
|
if *filter_by_date {
|
||||||
|
start = start.map(day_begin);
|
||||||
|
end = end.map(day_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.retain(|e| {
|
||||||
|
if start.is_some() && e.start < start.unwrap() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.is_some() && e.start > end.unwrap() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Displaying
|
||||||
|
match print_json {
|
||||||
|
true => print_all_tasks_json(&entries)?,
|
||||||
|
false => {
|
||||||
|
let mut options = ReadableOptions::complete();
|
||||||
|
options.show_ids = *ids;
|
||||||
|
|
||||||
|
print_all_tasks_readable(sheet, &entries, &options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_all_tasks_readable(sheet: &str, entries: &Vec<Entry>, options: &ReadableOptions) {
|
||||||
|
if options.show_timesheet {
|
||||||
|
println!("{} {}", style_string("Timesheet:", Styles::Title), sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = Builder::new();
|
||||||
|
|
||||||
|
if options.show_headings {
|
||||||
|
let h_id = match options.show_ids {
|
||||||
|
true => "ID",
|
||||||
|
false => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let headings = vec![h_id, "Date", "Start", "End", "Duration", "Task"];
|
||||||
|
builder.push_record(headings);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prev_date = None;
|
||||||
|
let mut day_sum = Duration::zero();
|
||||||
|
for entry in entries {
|
||||||
|
let mut print_date = true;
|
||||||
|
let mut print_partial = false;
|
||||||
|
let is_same = prev_date.is_some() && is_same_day(prev_date.unwrap(), &entry.start);
|
||||||
|
|
||||||
|
if is_same {
|
||||||
|
print_date = false;
|
||||||
|
} else if prev_date.is_some() {
|
||||||
|
print_partial = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_date = Some(&entry.start);
|
||||||
|
|
||||||
|
if print_partial && options.show_partial_sum {
|
||||||
|
builder.push_record(vec!["", "", "", "", &format_duration(&day_sum)]);
|
||||||
|
day_sum = Duration::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
day_sum = day_sum + entry.get_duration();
|
||||||
|
|
||||||
|
let id = match options.show_ids {
|
||||||
|
true => entry.id.unwrap().to_string(),
|
||||||
|
false => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let date = match print_date {
|
||||||
|
true => entry.start.format("%a %b %d, %Y").to_string(),
|
||||||
|
false => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = entry.start.format("%H:%M:%S").to_string();
|
||||||
|
|
||||||
|
let end = match entry.end {
|
||||||
|
Some(d) => d.format("%H:%M:%S").to_string(),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.push_record(vec![
|
||||||
|
&id,
|
||||||
|
&date,
|
||||||
|
&start,
|
||||||
|
&end,
|
||||||
|
&format_duration(&entry.get_duration()),
|
||||||
|
&entry.name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.show_partial_sum {
|
||||||
|
builder.push_record(vec!["", "", "", "", &format_duration(&day_sum)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = entries.iter().map(|e| e.get_duration()).sum();
|
||||||
|
if options.show_total {
|
||||||
|
builder.push_record(vec!["Total", "", "", "", &format_duration(&total)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = builder.build();
|
||||||
|
table.with(Style::empty());
|
||||||
|
table.with(Padding::new(2, 2, 0, 0));
|
||||||
|
|
||||||
|
if options.show_headings {
|
||||||
|
table.with(Colorization::exact([Color::BOLD], Rows::first()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.show_total {
|
||||||
|
table.with(Colorization::exact([Color::BOLD], Rows::last()));
|
||||||
|
table.modify(Rows::last(), Border::new().top('-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_all_tasks_json(entries: &Vec<Entry>) -> Result<()> {
|
||||||
|
println!("{}", to_string_pretty(entries)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::database::{get_entry_by_id, running_entry};
|
|
||||||
use crate::{Entry, State};
|
use langtime::parse;
|
||||||
|
|
||||||
|
use crate::commands::display::{print_all_tasks_readable, ReadableOptions};
|
||||||
|
use crate::database::{get_entry_by_id, running_entry, update_entry};
|
||||||
use crate::style::{style_string, Styles};
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
pub fn edit_task(
|
pub fn edit_task(
|
||||||
id: &Option<usize>,
|
id: &Option<usize>,
|
||||||
@@ -28,8 +32,33 @@ pub fn edit_task(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut entry = entry.unwrap_or_else(|| running_entry.unwrap());
|
let mut entry = entry.unwrap_or_else(|| running_entry.unwrap());
|
||||||
|
|
||||||
|
if let Some(start) = start {
|
||||||
|
entry.start = parse(start)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = end {
|
||||||
|
entry.end = Some(parse(end)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(move_to) = move_to {
|
||||||
|
entry.sheet = move_to.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(notes) = notes {
|
||||||
|
entry.name = notes.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
update_entry(&entry, &state.database)?;
|
||||||
|
|
||||||
|
// Display output
|
||||||
|
println!("{}", style_string("Entry updated:", Styles::Message));
|
||||||
|
|
||||||
|
let mut options = ReadableOptions::new();
|
||||||
|
options.show_headings = true;
|
||||||
|
options.show_ids = true;
|
||||||
|
|
||||||
|
print_all_tasks_readable("", &vec![entry], &options);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
99
src/commands/kill.rs
Normal file
99
src/commands/kill.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::database::{
|
||||||
|
get_all_sheets, get_entry_by_id, remove_entries_by_sheet, remove_entry_by_id,
|
||||||
|
};
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn kill_task(id: &usize, state: &mut State) -> Result<()> {
|
||||||
|
let entry = get_entry_by_id(id, &state.database)?;
|
||||||
|
|
||||||
|
// Guard for non-existent entries
|
||||||
|
if entry.is_none() {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Entry not found. Id:", Styles::Message),
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
|
||||||
|
if !confirm_action(&format!(
|
||||||
|
"Are you sure you want to remove entry {}?",
|
||||||
|
entry.name
|
||||||
|
)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_entry_by_id(id, &state.database)?;
|
||||||
|
|
||||||
|
println!("{} {}", style_string("Removed entry:", Styles::Message), id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill_sheet(sheet: &str, state: &mut State) -> Result<()> {
|
||||||
|
let sheets = get_all_sheets(&state.database)?;
|
||||||
|
|
||||||
|
// Guard for non-existent sheets
|
||||||
|
if !sheets.iter().any(|s| s == sheet) {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Sheet not found:", Styles::Message),
|
||||||
|
sheet
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !confirm_action(&format!("Are you sure you want to remove sheet {}?", sheet)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_entries_by_sheet(sheet, &state.database)?;
|
||||||
|
|
||||||
|
// Check edge cases for sheets that are in use
|
||||||
|
if state.current_sheet == sheet {
|
||||||
|
move_to_last_sheet(state)?;
|
||||||
|
} else if state.last_sheet == sheet {
|
||||||
|
state.last_sheet = "default".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Removed sheet:", Styles::Message),
|
||||||
|
sheet
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to_last_sheet(state: &mut State) -> Result<()> {
|
||||||
|
// If there is a last sheet, move to it, otherwise move
|
||||||
|
// to the first sheet available, or "default" if none exist.
|
||||||
|
let sheets = get_all_sheets(&state.database)?;
|
||||||
|
let last_sheet_exists = sheets.iter().any(|s| s == &state.last_sheet);
|
||||||
|
|
||||||
|
match last_sheet_exists {
|
||||||
|
true => state.change_sheet(&state.last_sheet.clone())?,
|
||||||
|
false => {
|
||||||
|
match !sheets.is_empty() {
|
||||||
|
true => state.change_sheet(&sheets[0])?,
|
||||||
|
false => state.change_sheet("default")?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn confirm_action(msg: &str) -> bool {
|
||||||
|
println!("{} ", msg);
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
input.trim().to_lowercase() == "y"
|
||||||
|
}
|
||||||
@@ -3,9 +3,19 @@ mod in_cmd;
|
|||||||
mod out;
|
mod out;
|
||||||
mod lists;
|
mod lists;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
mod display;
|
||||||
|
mod sheet;
|
||||||
|
mod kill;
|
||||||
|
mod month;
|
||||||
|
|
||||||
pub use current::current_task;
|
pub use current::current_task;
|
||||||
pub use in_cmd::start_task;
|
pub use in_cmd::start_task;
|
||||||
pub use out::stop_task;
|
pub use out::stop_task;
|
||||||
pub use lists::list_sheets;
|
pub use lists::list_sheets;
|
||||||
pub use edit::edit_task;
|
pub use edit::edit_task;
|
||||||
|
pub use display::display_tasks;
|
||||||
|
pub use sheet::checkout_sheet;
|
||||||
|
pub use sheet::rename_sheet;
|
||||||
|
pub use kill::kill_sheet;
|
||||||
|
pub use kill::kill_task;
|
||||||
|
pub use month::display_month;
|
||||||
20
src/commands/month.rs
Normal file
20
src/commands/month.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
use crate::commands::display_tasks;
|
||||||
|
use crate::utils::get_month_boundaries;
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
pub fn display_month(
|
||||||
|
json: &bool,
|
||||||
|
ids: &bool,
|
||||||
|
month: Option<&String>,
|
||||||
|
sheet: Option<&String>,
|
||||||
|
state: &mut State,
|
||||||
|
) -> Result<()> {
|
||||||
|
let now = Local::now().format("%Y-%m").to_string();
|
||||||
|
let month = month.unwrap_or(&now);
|
||||||
|
let (start, end) = get_month_boundaries(month)?;
|
||||||
|
|
||||||
|
display_tasks(json, sheet, Some(start), Some(end), &true, ids, state)
|
||||||
|
}
|
||||||
46
src/commands/sheet.rs
Normal file
46
src/commands/sheet.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate:: {
|
||||||
|
database::update_sheet_name,
|
||||||
|
style::{style_string, Styles},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn checkout_sheet(name: &str, state: &mut State) -> Result<()> {
|
||||||
|
if state.current_sheet == name {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Already on sheet:", Styles::Message),
|
||||||
|
name
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = if name == "-" {
|
||||||
|
state.last_sheet.clone()
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
state.change_sheet(&name)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Switched to sheet:", Styles::Message),
|
||||||
|
name
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rename_sheet(name: &str, new_name: &str, state: &mut State) -> Result<()> {
|
||||||
|
update_sheet_name(name, new_name, &state.database)?;
|
||||||
|
if state.current_sheet == name {
|
||||||
|
state.update_sheet_name(new_name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style_string("Sheet renamed successfully", Styles::Message)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ pub fn write_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
pub fn update_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||||
let query = "
|
let query = "
|
||||||
UPDATE entries SET
|
UPDATE entries SET
|
||||||
note = :note,
|
note = :note,
|
||||||
@@ -218,4 +218,33 @@ pub fn get_entry_by_id(id: &usize, db: &Connection) -> Result<Option<Entry>> {
|
|||||||
|
|
||||||
let entry = entries.next();
|
let entry = entries.next();
|
||||||
entry.transpose().context(format!("Failed to read entry"))
|
entry.transpose().context(format!("Failed to read entry"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_sheet_name(old_name: &str, new_name: &str, db: &Connection) -> Result<()> {
|
||||||
|
let query = "UPDATE entries SET sheet = ? WHERE sheet = ?";
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
stmt.execute([new_name, &old_name])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_entry_by_id(id: &usize, db: &Connection) -> Result<()> {
|
||||||
|
let query = "
|
||||||
|
DELETE FROM entries WHERE id = ?;
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
stmt.execute([id])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_entries_by_sheet(sheet: &str, db: &Connection) -> Result<()> {
|
||||||
|
let query = "
|
||||||
|
DELETE FROM entries WHERE sheet = ?;
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
stmt.execute([sheet])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Duration, Local};
|
use chrono::{DateTime, Duration, Local};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Eq, PartialOrd, PartialEq ,Ord, Clone)]
|
#[derive(Eq, PartialOrd, PartialEq ,Ord, Clone, Serialize)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
pub id: Option<usize>,
|
pub id: Option<usize>,
|
||||||
pub start: DateTime<Local>,
|
pub start: DateTime<Local>,
|
||||||
|
|||||||
64
src/main.rs
64
src/main.rs
@@ -9,11 +9,10 @@ mod utils;
|
|||||||
use crate::database::create_tables;
|
use crate::database::create_tables;
|
||||||
use crate::style::{style_string, Styles};
|
use crate::style::{style_string, Styles};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use commands::*;
|
use commands::*;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use database::{connect_to_db, ensure_db_exists};
|
use database::{connect_to_db, ensure_db_exists};
|
||||||
use directories::ProjectDirs;
|
|
||||||
pub use entry::Entry;
|
pub use entry::Entry;
|
||||||
use langtime::parse;
|
use langtime::parse;
|
||||||
pub use state::State;
|
pub use state::State;
|
||||||
@@ -60,6 +59,20 @@ enum SubCommands {
|
|||||||
/// The timesehet to display, or the current one
|
/// The timesehet to display, or the current one
|
||||||
sheet: Option<String>,
|
sheet: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Like 'Display' but for a specific month, or the current one
|
||||||
|
Month {
|
||||||
|
/// Show a JSON representation instead of a human-readable one
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
/// Show the tasks IDs
|
||||||
|
#[arg(short, long)]
|
||||||
|
ids: bool,
|
||||||
|
/// The specific month to show. The format is yyyy-mm (e.g. "2024-03")
|
||||||
|
#[arg(short, long)]
|
||||||
|
month: Option<String>,
|
||||||
|
/// The timesheet to display, or the current one
|
||||||
|
sheet: Option<String>,
|
||||||
|
},
|
||||||
/// Change timesheet
|
/// Change timesheet
|
||||||
Sheet {
|
Sheet {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -87,6 +100,21 @@ enum SubCommands {
|
|||||||
},
|
},
|
||||||
/// Shows the active task for the current sheet
|
/// Shows the active task for the current sheet
|
||||||
Current,
|
Current,
|
||||||
|
/// Removes a task or a whole timesheet
|
||||||
|
Kill {
|
||||||
|
#[command(flatten)]
|
||||||
|
kill_args: KillArgs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[group(required = true, multiple = false)]
|
||||||
|
struct KillArgs {
|
||||||
|
/// The ID of the task to remove
|
||||||
|
#[arg(long)]
|
||||||
|
id: Option<usize>,
|
||||||
|
/// The name of the timesheet to remove
|
||||||
|
sheet: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -125,8 +153,29 @@ fn cli() -> Result<()> {
|
|||||||
end,
|
end,
|
||||||
filter_by_date,
|
filter_by_date,
|
||||||
sheet,
|
sheet,
|
||||||
} => {}
|
} => {
|
||||||
SubCommands::Sheet {name, rename} => {}
|
display_tasks(
|
||||||
|
json,
|
||||||
|
sheet.as_ref(),
|
||||||
|
start.as_ref().map(|s| parse(s)).transpose()?,
|
||||||
|
end.as_ref().map(|e| parse(e)).transpose()?,
|
||||||
|
filter_by_date,
|
||||||
|
ids,
|
||||||
|
&state,
|
||||||
|
).context("Could not display tasks")?;
|
||||||
|
}
|
||||||
|
SubCommands::Month {
|
||||||
|
json,
|
||||||
|
ids,
|
||||||
|
month,
|
||||||
|
sheet,
|
||||||
|
} => {
|
||||||
|
display_month(json,ids, month.as_ref(), sheet.as_ref(), &mut state).context("Could not display month")?;
|
||||||
|
}
|
||||||
|
SubCommands::Sheet {name, rename} => match rename {
|
||||||
|
None => checkout_sheet(name, &mut state).context("Could not checkout sheet")?,
|
||||||
|
Some(new_name) => rename_sheet(name, new_name, &mut state).context("Could not rename sheet")?,
|
||||||
|
}
|
||||||
SubCommands::List => {
|
SubCommands::List => {
|
||||||
list_sheets(&state).context("Could not list sheets")?;
|
list_sheets(&state).context("Could not list sheets")?;
|
||||||
}
|
}
|
||||||
@@ -142,6 +191,13 @@ fn cli() -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
edit_task(id,start,end,move_to,notes, &mut state).context("Could not edit task")?;
|
edit_task(id,start,end,move_to,notes, &mut state).context("Could not edit task")?;
|
||||||
}
|
}
|
||||||
|
SubCommands::Kill {kill_args, } => {
|
||||||
|
if let Some(id) = &kill_args.id {
|
||||||
|
kill_task(id, &mut state).context("Could not kill task")?;
|
||||||
|
} else if let Some(sheet) = &kill_args.sheet {
|
||||||
|
kill_sheet(sheet, &mut state).context("Could not delete the timesheet")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/state.rs
13
src/state.rs
@@ -48,6 +48,19 @@ impl State {
|
|||||||
}
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn change_sheet(&mut self, sheet: &str) -> Result<()> {
|
||||||
|
self.last_sheet = self.current_sheet.clone();
|
||||||
|
self.current_sheet = sheet.to_string();
|
||||||
|
self.update_file()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_sheet_name(&mut self, sheet: &str) -> Result<()> {
|
||||||
|
self.current_sheet = sheet.to_string();
|
||||||
|
self.update_file()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn update_file(&self) -> Result<()> {
|
fn update_file(&self) -> Result<()> {
|
||||||
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
||||||
|
|||||||
57
src/utils.rs
57
src/utils.rs
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{DateTime, Local,Duration, LocalResult, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Datelike, Duration, Local,LocalResult, NaiveDateTime, TimeZone};
|
||||||
|
|
||||||
pub fn str_to_datetime(s: &str) -> Result<DateTime<Local>> {
|
pub fn str_to_datetime(s: &str) -> Result<DateTime<Local>> {
|
||||||
let no_tz = s.replace("+00:00", "");
|
let no_tz = s.replace("+00:00", "");
|
||||||
@@ -20,4 +20,59 @@ pub fn format_duration(d: &Duration) -> String {
|
|||||||
let seconds = duration_in_seconds % 60;
|
let seconds = duration_in_seconds % 60;
|
||||||
|
|
||||||
format!("{}:{:0>2}:{:0>2}", hours, minutes, seconds)
|
format!("{}:{:0>2}:{:0>2}", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn day_begin(dt: DateTime<Local>) -> DateTime<Local> {
|
||||||
|
Local
|
||||||
|
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn day_end(dt: DateTime<Local>) -> DateTime<Local> {
|
||||||
|
Local
|
||||||
|
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 23, 59, 59)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_same_day(dt1: &DateTime<Local>, dt2: &DateTime<Local>) -> bool {
|
||||||
|
dt1.year() == dt2.year() && dt1.month() == dt2.month() && dt1.day() == dt2.day()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_month_boundaries(month: &str) -> Result<(DateTime<Local>, DateTime<Local>)> {
|
||||||
|
let start = get_month_from_string(month)?;
|
||||||
|
let end = get_last_day_of_month(start)?;
|
||||||
|
|
||||||
|
Ok((start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The month is written as 2024-01
|
||||||
|
pub fn get_month_from_string(month_str: &str) -> Result<DateTime<Local>> {
|
||||||
|
let year = month_str.split('-').next().unwrap().parse::<i32>().unwrap();
|
||||||
|
let month = month_str.split('-').nth(1).unwrap().parse::<u32>().unwrap();
|
||||||
|
|
||||||
|
let res = Local.with_ymd_and_hms(year, month, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
chrono::LocalResult::None => Err(anyhow::anyhow!("Invalid month")),
|
||||||
|
chrono::LocalResult::Single(dt) => Ok(dt),
|
||||||
|
chrono::LocalResult::Ambiguous(_, _) => Err(anyhow::anyhow!("Ambiguous month")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last_day_of_month(dt: DateTime<Local>) -> Result<DateTime<Local>> {
|
||||||
|
let mut month = dt.month() + 1;
|
||||||
|
let mut year = dt.year();
|
||||||
|
|
||||||
|
if month > 12 {
|
||||||
|
month = 1;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = Local.with_ymd_and_hms(year, month, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
LocalResult::None => Err(anyhow::anyhow!("Invalid month")),
|
||||||
|
LocalResult::Single(dt) => Ok(dt - Duration::days(1)),
|
||||||
|
LocalResult::Ambiguous(_, _) => Err(anyhow::anyhow!("Ambiguous month")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user