Programm weitergeschrieben
This commit is contained in:
@@ -5,9 +5,12 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
|
chrono = {version = "0.4.41", features = ["serde"]}
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
clap = { version = "4.5.38", features = ["derive"] }
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
|
|
||||||
directories = "6.0.0"
|
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"] }
|
||||||
|
tabled = "0.19.0"
|
||||||
40
src/commands/current.rs
Normal file
40
src/commands/current.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::State;
|
||||||
|
use crate::database::running_entries;
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::utils::{time_from_now,format_duration};
|
||||||
|
|
||||||
|
pub fn current_task(state: &State) -> Result<()> {
|
||||||
|
let mut entries = running_entries(&state.database)?;
|
||||||
|
|
||||||
|
entries.sort_unstable();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}{}",
|
||||||
|
style_string("Currently on sheet: ", Styles::Title),
|
||||||
|
style_string(&state.current_sheet, Styles::Primary)
|
||||||
|
);
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style_string("There is no active task.", Styles::Message)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// There will always be only one active entry for each sheet
|
||||||
|
println!("{}", style_string("Active tasks:", Styles::Title));
|
||||||
|
for entry in &entries {
|
||||||
|
println!(
|
||||||
|
"{}: {} ({})",
|
||||||
|
style_string(&entry.sheet, Styles::Primary),
|
||||||
|
style_string(&entry.name, Styles::Secondary),
|
||||||
|
format_duration(&time_from_now(&entry.start))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
33
src/commands/in_cmd.rs
Normal file
33
src/commands/in_cmd.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
|
||||||
|
use crate::database::{running_entry, write_entry};
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::Entry;
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
pub fn start_task(task: &str, at: Option<DateTime<Local>>, state: &State) -> Result<()> {
|
||||||
|
let start = at.unwrap_or(Local::now());
|
||||||
|
let cur_task = running_entry(&state.database, &state.current_sheet)?;
|
||||||
|
|
||||||
|
if cur_task.is_some() {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Already checked into sheet:", Styles::Message),
|
||||||
|
cur_task.unwrap().sheet
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = Entry::start(task, &state.current_sheet, start);
|
||||||
|
|
||||||
|
write_entry(&entry, &state.database)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Checked into sheet:", Styles::Message),
|
||||||
|
entry.sheet
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
71
src/commands/lists.rs
Normal file
71
src/commands/lists.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{Duration, Local};
|
||||||
|
|
||||||
|
use tabled::builder::Builder;
|
||||||
|
use tabled::settings::object::Rows;
|
||||||
|
use tabled::settings::themes::Colorization;
|
||||||
|
use tabled::settings::{Color, Style};
|
||||||
|
|
||||||
|
use crate::database::{get_all_sheets, get_sheet_entries};
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::utils::{format_duration, time_from_now};
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn list_sheets(state: &State) -> Result<()> {
|
||||||
|
let mut sheets = get_all_sheets(&state.database)?;
|
||||||
|
|
||||||
|
if sheets.is_empty() {
|
||||||
|
sheets.push(state.current_sheet.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = Builder::new();
|
||||||
|
|
||||||
|
println!("{}", style_string("Timesheets:", Styles::Title));
|
||||||
|
|
||||||
|
builder.push_record(vec!["Name", "Running", "Today", "Total time"]);
|
||||||
|
|
||||||
|
for sheet in sheets {
|
||||||
|
let entries = get_sheet_entries(&sheet, &state.database)?;
|
||||||
|
|
||||||
|
let running = entries.iter().find(|e| e.end.is_none());
|
||||||
|
let running_time = match running {
|
||||||
|
Some(e) => time_from_now(&e.start),
|
||||||
|
None => Duration::seconds(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let today_total: Duration = entries
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.start.date_naive() == Local::now().date_naive())
|
||||||
|
.map(|e| e.end.unwrap_or(Local::now()) - e.start)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let total: Duration = entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.end.unwrap_or(Local::now()) - e.start)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let s = if sheet == state.current_sheet {
|
||||||
|
format!("{}{}", "*", sheet)
|
||||||
|
} else if sheet == state.last_sheet {
|
||||||
|
format!("{}{}", "-", sheet)
|
||||||
|
} else {
|
||||||
|
sheet
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.push_record(vec![
|
||||||
|
s,
|
||||||
|
format_duration(&running_time),
|
||||||
|
format_duration(&today_total),
|
||||||
|
format_duration(&total),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = builder.build();
|
||||||
|
table.with(Style::empty());
|
||||||
|
table.with(Colorization::exact([Color::BOLD], Rows::first()));
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
9
src/commands/mod.rs
Normal file
9
src/commands/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod current;
|
||||||
|
mod in_cmd;
|
||||||
|
mod out;
|
||||||
|
mod lists;
|
||||||
|
|
||||||
|
pub use current::current_task;
|
||||||
|
pub use in_cmd::start_task;
|
||||||
|
pub use out::stop_task;
|
||||||
|
pub use lists::list_sheets;
|
||||||
39
src/commands/out.rs
Normal file
39
src/commands/out.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
|
||||||
|
|
||||||
|
use crate::database::{running_entry, write_entry};
|
||||||
|
use crate::style::{style_string, Styles};
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn stop_task(at: Option<DateTime<Local>>, state: &mut State) -> Result<()> {
|
||||||
|
let end = at.unwrap_or(Local::now());
|
||||||
|
|
||||||
|
let cur = running_entry(&state.database, &state.current_sheet)?;
|
||||||
|
|
||||||
|
match cur {
|
||||||
|
None => println!(
|
||||||
|
"{}",
|
||||||
|
style_string("There is no active task.", Styles::Message)
|
||||||
|
),
|
||||||
|
Some(mut e) => {
|
||||||
|
if e.start > end {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style_string("Cannot stop a task before it started", Styles::Message)
|
||||||
|
);
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
e.stop(end);
|
||||||
|
write_entry(&e,&state.database)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style_string("Stopped the task", Styles::Message),
|
||||||
|
e.sheet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Result };
|
use anyhow::{anyhow, Result};
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -8,14 +8,14 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn build() -> Result<Config> {
|
pub fn build() -> Result<Config> {
|
||||||
let proj_dirs = ProjectDirs::from("de","schacht-analyse","timetracker")
|
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
||||||
.ok_or(anyhow!("Couldn't get project directories"))?;
|
.ok_or(anyhow!("Couldn't get project directories"))?;
|
||||||
|
|
||||||
let data_dir = proj_dirs.data_dir();
|
let data_dir = proj_dirs.data_dir();
|
||||||
let mut db_file = data_dir.to_path_buf();
|
let mut db_file = data_dir.to_path_buf();
|
||||||
|
|
||||||
db_file.push("database.db");
|
db_file.push("database.db");
|
||||||
|
|
||||||
if let Some(db_file_str) = db_file.to_str() {
|
if let Some(db_file_str) = db_file.to_str() {
|
||||||
return Ok(Config {
|
return Ok(Config {
|
||||||
database_file: db_file_str.to_string(),
|
database_file: db_file_str.to_string(),
|
||||||
@@ -24,4 +24,4 @@ impl Config {
|
|||||||
}
|
}
|
||||||
Err(anyhow!("Couldn't get database file"))
|
Err(anyhow!("Couldn't get database file"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
175
src/database.rs
175
src/database.rs
@@ -1,47 +1,198 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use rusqlite::{named_params, Connection};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use rusqlite::Connection;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::entry::Entry;
|
||||||
|
use crate::utils::str_to_datetime;
|
||||||
|
|
||||||
pub fn connect_to_db(config: &Config) -> Result<Connection> {
|
pub fn connect_to_db(config: &Config) -> Result<Connection> {
|
||||||
if let Ok(conn) = Connection::open(&config.database_file) {
|
if let Ok(conn) = Connection::open(&config.database_file) {
|
||||||
return Ok(conn);
|
return Ok(conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!(
|
Err(anyhow!("Cannot create connection to database"))
|
||||||
"Cannot create connection to database"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_tables(db: &Connection) -> Result<()> {
|
pub fn create_tables(db: &Connection) -> Result<()> {
|
||||||
let query = "
|
let query = "
|
||||||
CREATE TABLE IF NOT EXISTS entries (
|
CREATE TABLE IF NOT EXISTS entries (
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
note VARCHAR(255) NOT NULL,
|
note VARCHAR(255) NOT NULL,
|
||||||
start TIMESTAMP NOT NULL,
|
start TIMESTAMP NOT NULL,
|
||||||
end TIMESTAMP NOT NULL,
|
end TIMESTAMP,
|
||||||
sheet VARCHAR(255) NOT NULL
|
sheet VARCHAR(255) NOT NULL
|
||||||
)
|
)
|
||||||
";
|
";
|
||||||
db.execute(query, ())
|
db.execute(query, ())
|
||||||
.context("Failed to create entries table")?;
|
.context("Failed to create entries table")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_db_exists(config: &Config) -> Result<()> {
|
pub fn ensure_db_exists(config: &Config) -> Result<()> {
|
||||||
let mut db_path = PathBuf::from_str(&config.database_file).unwrap();
|
let mut db_path = PathBuf::from_str(&config.database_file).unwrap();
|
||||||
db_path.pop();
|
db_path.pop();
|
||||||
|
|
||||||
if!db_path.exists() {
|
if !db_path.exists() {
|
||||||
std::fs::create_dir_all(&db_path).context(format!(
|
std::fs::create_dir_all(&db_path).context(format!(
|
||||||
"Cannot create the data folder for Timetracker. The expected path was {:?}",
|
"Cannot create the data folder for Timetracker. The expected path was {:?}",
|
||||||
&db_path
|
&db_path
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn running_entries(db: &Connection) -> Result<Vec<Entry>> {
|
||||||
|
let query = "SELECT id, note, start, end, sheet FROM entries WHERE end IS NULL;";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
let entries = stmt.query_map([], |row| {
|
||||||
|
let end = row
|
||||||
|
.get::<usize, Option<String>>(3)?
|
||||||
|
.map(|t| str_to_datetime(&t).unwrap());
|
||||||
|
|
||||||
|
let start = str_to_datetime(&row.get::<usize, String>(2)?).unwrap();
|
||||||
|
|
||||||
|
Ok(Entry {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
sheet: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let entries_vec: Vec<Result<Entry,_>> = entries.collect();
|
||||||
|
if entries_vec.iter().any(|e| e.is_err()) {
|
||||||
|
return Err(anyhow!("Cannot read entries from database"));
|
||||||
|
}
|
||||||
|
Ok(entries_vec.into_iter().map(|e| e.unwrap()).collect())
|
||||||
|
}
|
||||||
|
pub fn running_entry(db: &Connection, sheet: &str) -> Result<Option<Entry>> {
|
||||||
|
let query = "SELECT id, note,start,end,sheet FROM entries WHERE end IS NULL AND sheet = ?";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
let mut entries = stmt.query_map([sheet], |row| {
|
||||||
|
let end = row
|
||||||
|
.get::<usize, Option<String>>(3)?
|
||||||
|
.map(|t| str_to_datetime(&t).unwrap());
|
||||||
|
|
||||||
|
let start = str_to_datetime(&row.get::<usize, String>(2)?).unwrap();
|
||||||
|
|
||||||
|
Ok(Entry {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
sheet: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let running_entry = entries.next();
|
||||||
|
running_entry
|
||||||
|
.transpose()
|
||||||
|
.context(format!("Failed to run entry"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||||
|
match entry.id {
|
||||||
|
Some(_) => update_entry(entry, db)?,
|
||||||
|
None => create_entry(entry, db)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||||
|
let query = "
|
||||||
|
UPDATE entries SET
|
||||||
|
note = :note,
|
||||||
|
start = :start,
|
||||||
|
end = :end,
|
||||||
|
sheet = :sheet
|
||||||
|
WHERE
|
||||||
|
id = :id
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
stmt.execute(named_params! {
|
||||||
|
":note": entry.name,
|
||||||
|
":start": entry.start,
|
||||||
|
":end": entry.end,
|
||||||
|
":sheet": entry.sheet,
|
||||||
|
":id": entry.id
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||||
|
let query = "
|
||||||
|
INSERT INTO entries (note, start, end, sheet) VALUES (
|
||||||
|
:note, :start, :end, :sheet
|
||||||
|
)
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
|
||||||
|
stmt.execute(named_params! {
|
||||||
|
":note": entry.name,
|
||||||
|
":start": entry.start,
|
||||||
|
":end": entry.end,
|
||||||
|
":sheet": entry.sheet
|
||||||
|
}).context("Failed to create entry")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn get_all_entries(db: &Connection) -> Result<Vec<Entry>> {
|
||||||
|
let query = "
|
||||||
|
SELECT id, note, start, end, sheet FROM entries;
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
let entries = stmt.query_map([], |row| {
|
||||||
|
let end = row
|
||||||
|
.get::<usize, Option<String>>(3)?
|
||||||
|
.map(|t| str_to_datetime(&t).unwrap());
|
||||||
|
|
||||||
|
let start = str_to_datetime(&row.get::<usize, String>(2)?).unwrap();
|
||||||
|
|
||||||
|
Ok(Entry {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
sheet: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut entries_vec = Vec::new();
|
||||||
|
|
||||||
|
for sheet in entries {
|
||||||
|
entries_vec.push(sheet?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries_vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sheet_entries(sheet: &str, db: &Connection) -> Result<Vec<Entry>> {
|
||||||
|
let entries = get_all_entries(db)?;
|
||||||
|
Ok(entries
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.sheet == sheet)
|
||||||
|
.cloned()
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_sheets(db: &Connection) -> Result<Vec<String>> {
|
||||||
|
let query = "SELECT DISTINCT sheet FROM entries;";
|
||||||
|
let mut stmt = db.prepare(query)?;
|
||||||
|
let entries = stmt.query_map([], |row| row.get::<usize, String>(0))?;
|
||||||
|
|
||||||
|
let mut sheets = Vec::new();
|
||||||
|
for sheet in entries {
|
||||||
|
sheets.push(sheet?);
|
||||||
|
}
|
||||||
|
Ok(sheets)
|
||||||
}
|
}
|
||||||
32
src/entry.rs
Normal file
32
src/entry.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use chrono::{DateTime, Duration, Local};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Eq, PartialOrd, PartialEq ,Ord, Clone)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub id: Option<usize>,
|
||||||
|
pub start: DateTime<Local>,
|
||||||
|
pub end: Option<DateTime<Local>>,
|
||||||
|
pub name: String,
|
||||||
|
pub sheet: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn start(name: &str, sheet: &str, start: DateTime<Local>) -> Self {
|
||||||
|
Entry {
|
||||||
|
id: None,
|
||||||
|
start,
|
||||||
|
end: None,
|
||||||
|
name: name.to_string(),
|
||||||
|
sheet: sheet.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self, end: DateTime<Local>) {
|
||||||
|
self.end = Some(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_duration(&self) -> Duration {
|
||||||
|
let end = self.end.unwrap_or(Local::now());
|
||||||
|
end - self.start
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/main.rs
66
src/main.rs
@@ -1,19 +1,25 @@
|
|||||||
mod style;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod entry;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod style;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use database::{ensure_db_exists, connect_to_db};
|
|
||||||
use config::Config;
|
|
||||||
use anyhow::{Result, Context};
|
|
||||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
|
||||||
use crate::database::create_tables;
|
use crate::database::create_tables;
|
||||||
use crate::style::{style_string, Styles};
|
use crate::style::{style_string, Styles};
|
||||||
pub use state::State;
|
use anyhow::{Context, Result};
|
||||||
|
use clap::{ArgAction, Args, Parser, Subcommand};
|
||||||
|
use commands::*;
|
||||||
|
use config::Config;
|
||||||
|
use database::{connect_to_db, ensure_db_exists};
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
pub use entry::Entry;
|
||||||
use langtime::parse;
|
use langtime::parse;
|
||||||
|
pub use state::State;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author,version, about, infer_subcommands = true)]
|
#[command(author, version, about, infer_subcommands = true)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: SubCommands,
|
command: SubCommands,
|
||||||
@@ -36,9 +42,9 @@ enum SubCommands {
|
|||||||
},
|
},
|
||||||
/// Display the current timesheet
|
/// Display the current timesheet
|
||||||
Display {
|
Display {
|
||||||
/// Show an JSON
|
/// Show an JSON
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json:bool,
|
json: bool,
|
||||||
/// Show the Task IDs
|
/// Show the Task IDs
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
ids: bool,
|
ids: bool,
|
||||||
@@ -50,10 +56,14 @@ enum SubCommands {
|
|||||||
end: Option<String>,
|
end: Option<String>,
|
||||||
/// Just filter by whole days, do not take into account the time
|
/// Just filter by whole days, do not take into account the time
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
filter_by_date:bool,
|
filter_by_date: bool,
|
||||||
/// The timesehet to display, or the current one
|
/// The timesehet to display, or the current one
|
||||||
sheet:Option<String>,
|
sheet: Option<String>,
|
||||||
}
|
},
|
||||||
|
/// List available timesheet
|
||||||
|
List,
|
||||||
|
/// Shows the active task for the current sheet
|
||||||
|
Current,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -63,7 +73,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn cli() -> Result<()> {
|
fn cli() -> Result<()> {
|
||||||
let config = Config::build().context("Failed to build configuration")?;
|
let config = Config::build().context("Failed to build configuration")?;
|
||||||
setup(&config).context("Programmdatanbank konnte nicht erstellt werden")?;
|
setup(&config).context("Programmdatanbank konnte nicht erstellt werden")?;
|
||||||
@@ -72,6 +81,35 @@ fn cli() -> Result<()> {
|
|||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
SubCommands::In { task, at } => {
|
||||||
|
let target_time = at.as_ref().map(|at| parse(at)).transpose()?;
|
||||||
|
|
||||||
|
let task = task.as_ref();
|
||||||
|
let default_task = "".to_string();
|
||||||
|
let task = task.unwrap_or(&default_task);
|
||||||
|
|
||||||
|
start_task(task, target_time, &state).context("Could not start task")?;
|
||||||
|
}
|
||||||
|
SubCommands::Out { at } => {
|
||||||
|
let target_time = at.as_ref().map(|at| parse(at)).transpose()?;
|
||||||
|
stop_task(target_time, &mut state).context("Could not stop task")?;
|
||||||
|
}
|
||||||
|
SubCommands::Display {
|
||||||
|
json,
|
||||||
|
ids,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filter_by_date,
|
||||||
|
sheet,
|
||||||
|
} => {}
|
||||||
|
SubCommands::List => {
|
||||||
|
list_sheets(&state).context("Could not list sheets")?;
|
||||||
|
}
|
||||||
|
SubCommands::Current => {
|
||||||
|
current_task(&state).context("could not get current task")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -81,4 +119,4 @@ fn setup(config: &Config) -> Result<()> {
|
|||||||
let db = connect_to_db(config)?;
|
let db = connect_to_db(config)?;
|
||||||
create_tables(&db)?;
|
create_tables(&db)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/state.rs
34
src/state.rs
@@ -1,6 +1,6 @@
|
|||||||
use rusqlite::Connection;
|
use anyhow::{anyhow, Context, Result};
|
||||||
use anyhow::{anyhow, Context, Result };
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
use rusqlite::Connection;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -17,19 +17,19 @@ impl State {
|
|||||||
let db = connect_to_db(config)?;
|
let db = connect_to_db(config)?;
|
||||||
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
||||||
.ok_or(anyhow!("Could not determine project directories"))?;
|
.ok_or(anyhow!("Could not determine project directories"))?;
|
||||||
|
|
||||||
let mut state = State {
|
let mut state = State {
|
||||||
current_sheet : "default".to_string(),
|
current_sheet: "default".to_string(),
|
||||||
last_sheet: "default".to_string(),
|
last_sheet: "default".to_string(),
|
||||||
database: db,
|
database: db,
|
||||||
};
|
};
|
||||||
|
|
||||||
let data_dir = proj_dirs.data_local_dir();
|
let data_dir = proj_dirs.data_dir();
|
||||||
let mut data_file = data_dir.to_path_buf();
|
let mut data_file = data_dir.to_path_buf();
|
||||||
data_file.push("data.txt");
|
data_file.push("data.txt");
|
||||||
|
|
||||||
let content_res = fs::read_to_string(&data_file);
|
let content_res = fs::read_to_string(&data_file);
|
||||||
|
|
||||||
match content_res {
|
match content_res {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let mut lines = content.lines();
|
let mut lines = content.lines();
|
||||||
@@ -48,19 +48,19 @@ impl State {
|
|||||||
}
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
.ok_or(anyhow!("Could not determine project directories"))?;
|
.ok_or(anyhow!("Could not determine project directories"))?;
|
||||||
|
|
||||||
let data_dir = proj_dirs.data_local_dir();
|
let data_dir = proj_dirs.data_dir();
|
||||||
let mut data_file = data_dir.to_path_buf();
|
let mut data_file = data_dir.to_path_buf();
|
||||||
data_file.push("data.txt");
|
data_file.push("data.txt");
|
||||||
|
|
||||||
fs::write(&data_file,
|
fs::write(
|
||||||
format!("{}\n{}",self.current_sheet,self.last_sheet),)?;
|
&data_file,
|
||||||
|
format!("{}\n{}", self.current_sheet, self.last_sheet),
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ pub fn style_string(label: &str, style: Styles) -> ColoredString {
|
|||||||
Styles::Primary => label.green().bold(),
|
Styles::Primary => label.green().bold(),
|
||||||
Styles::Secondary => label.cyan(),
|
Styles::Secondary => label.cyan(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/utils.rs
Normal file
23
src/utils.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Local,Duration, LocalResult, NaiveDateTime, TimeZone};
|
||||||
|
|
||||||
|
pub fn str_to_datetime(s: &str) -> Result<DateTime<Local>> {
|
||||||
|
let no_tz = s.replace("+00:00", "");
|
||||||
|
let dt = NaiveDateTime::parse_from_str(&no_tz, "%Y-%m-%d %H:%M:%S%.f")?;
|
||||||
|
Ok(Local.from_utc_datetime(&dt))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_from_now(dt: &DateTime<Local>) -> Duration {
|
||||||
|
let now = Local::now();
|
||||||
|
|
||||||
|
now - dt
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_duration(d: &Duration) -> String {
|
||||||
|
let duration_in_seconds = d.num_seconds();
|
||||||
|
let hours = duration_in_seconds / 60 / 60;
|
||||||
|
let minutes = (duration_in_seconds % 3600) / 60;
|
||||||
|
let seconds = duration_in_seconds % 60;
|
||||||
|
|
||||||
|
format!("{}:{:0>2}:{:0>2}", hours, minutes, seconds)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user