440 lines
14 KiB
Rust
440 lines
14 KiB
Rust
use std::{io::Read, sync::oneshot, thread, time::Duration};
|
|
|
|
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
use mize::{Mize, MizeError, MizePart, MizePartGuard, MizeResult, mize_err, mize_part};
|
|
use spacetimedb_sdk::{
|
|
DbContext, Error, Event, Identity, Status, SubscriptionHandle, Table, TableWithPrimaryKey,
|
|
credentials,
|
|
};
|
|
|
|
use crate::{
|
|
generated::{
|
|
Action, ActivitiesTableAccess, Activity, DbConnection, TaskPartTableAccess,
|
|
activitiesQueryTableAccess, next_action, remove_activity, task_partQueryTableAccess,
|
|
update_activity, update_upcoming,
|
|
},
|
|
spacetime::SpacetimePart,
|
|
};
|
|
|
|
#[mize_part]
|
|
#[derive(Default)]
|
|
pub struct TimeTrack {
|
|
mize: Mize,
|
|
}
|
|
|
|
pub fn timetrack(mize: &mut Mize) -> MizeResult<()> {
|
|
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;
|
|
|
|
cli.subcommand(
|
|
Command::new("editActionList").alias("aal").arg(
|
|
Arg::new("defaultTemplate")
|
|
.short('d')
|
|
.action(ArgAction::SetTrue),
|
|
),
|
|
command_edit_action_list,
|
|
);
|
|
cli.subcommand(Command::new("nextAction").alias("ne"), command_next_action);
|
|
cli.subcommand(
|
|
Command::new("printCurrentAction").alias("cu"),
|
|
command_current_action,
|
|
);
|
|
cli.subcommand(
|
|
Command::new("listActions").alias("la"),
|
|
command_list_actions,
|
|
);
|
|
cli.subcommand(
|
|
Command::new("listActivities").alias("lac"),
|
|
command_list_activities,
|
|
);
|
|
cli.subcommand(
|
|
Command::new("editActivities").alias("eac"),
|
|
command_edit_activities,
|
|
);
|
|
|
|
mize.register_part(Box::new(TimeTrack { mize: mize.clone() }));
|
|
Ok(())
|
|
}
|
|
|
|
impl MizePart for TimeTrack {
|
|
fn name(&self) -> &'static str {
|
|
"timetrack"
|
|
}
|
|
}
|
|
|
|
fn command_next_action(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
let con = spacetime.con();
|
|
let (wait_tx, waiter_rx) = std::sync::oneshot::channel::<()>();
|
|
con.reducers.next_action_then(|_, _| {
|
|
wait_tx.send(()).unwrap();
|
|
})?;
|
|
waiter_rx.recv()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn command_current_action(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
let action = get_current_action(&mut mize, spacetime.con())?;
|
|
println!("{}", action.name);
|
|
Ok(())
|
|
}
|
|
|
|
fn get_current_action(mize: &mut Mize, con: &DbConnection) -> MizeResult<Action> {
|
|
let (wait_tx, wait_rx) = std::sync::oneshot::channel::<Action>();
|
|
con.subscription_builder()
|
|
.on_applied(|ctx| {
|
|
let task_part_data = ctx.db.task_part().identity().find(&ctx.identity());
|
|
if task_part_data.is_none() {
|
|
wait_tx
|
|
.send(Action {
|
|
name: "fillier".to_string(),
|
|
activity: None,
|
|
task: None,
|
|
start: None,
|
|
end: None,
|
|
duration: None,
|
|
})
|
|
.unwrap();
|
|
} else {
|
|
wait_tx.send(task_part_data.unwrap().current).unwrap();
|
|
}
|
|
})
|
|
.add_query(|q| {
|
|
q.from.task_part()
|
|
//.r#where(|row| row.identity.eq(con.identity()))
|
|
})
|
|
.subscribe();
|
|
let action = wait_rx.recv().unwrap();
|
|
Ok(action)
|
|
}
|
|
|
|
fn command_list_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
let activities = get_activities(&mut mize, spacetime.con())?;
|
|
|
|
for ac in activities.clone() {
|
|
print!("ac {}: {}", ac.id, ac.name);
|
|
if !ac.parents_ids.is_empty() {
|
|
print!(" -");
|
|
}
|
|
for child_id in ac.parents_ids {
|
|
let child_ac = activities.iter().find(|ac| ac.id == child_id).unwrap();
|
|
print!(" {}", child_ac.name);
|
|
}
|
|
print!("\n")
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn command_list_actions(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
|
|
let actions = get_actions(spacetime.con())?;
|
|
|
|
for action in actions {
|
|
println!("{}", action.name)
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn command_edit_action_list(matches: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
|
|
// maybe append a template to the action list
|
|
if matches.get_flag("defaultTemplate") {
|
|
println!("Appending template...");
|
|
return Ok(());
|
|
}
|
|
|
|
// get list of actions
|
|
let mut actions = get_actions(spacetime.con())?;
|
|
|
|
// write to tmp file
|
|
let mut tmp_file_content = actions_to_string(&actions)?;
|
|
let tmp_file = std::env::temp_dir().join(format!(
|
|
"ppc-edit-actions-{}.txt",
|
|
chrono::Utc::now().timestamp()
|
|
));
|
|
std::fs::write(&tmp_file, tmp_file_content)
|
|
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
|
|
|
// open $EDITOR blocking
|
|
std::process::Command::new("nvim")
|
|
.arg(&tmp_file)
|
|
.status()
|
|
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
|
|
|
|
// read file and update db
|
|
let new_file_content = std::fs::read_to_string(&tmp_file)
|
|
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
|
|
|
actions = actions_from_string(actions, new_file_content)?;
|
|
set_actions(actions, spacetime.con())?;
|
|
|
|
// delete tmpfile
|
|
let _ = std::fs::remove_file(&tmp_file);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_actions(con: &DbConnection) -> MizeResult<Vec<Action>> {
|
|
let (exit_tx, exit_rx) = std::sync::oneshot::channel::<Vec<Action>>();
|
|
con.subscription_builder()
|
|
.on_applied(|ctx| {
|
|
let task_part_data = ctx.db.task_part().identity().find(&ctx.identity());
|
|
if task_part_data.is_none() {
|
|
exit_tx.send(Vec::new()).unwrap();
|
|
} else {
|
|
exit_tx.send(task_part_data.unwrap().upcoming).unwrap();
|
|
}
|
|
})
|
|
.add_query(|q| {
|
|
q.from.task_part()
|
|
//.r#where(|row| row.identity.eq(con.identity()))
|
|
})
|
|
.subscribe();
|
|
let activities = exit_rx.recv().unwrap();
|
|
Ok(activities)
|
|
}
|
|
|
|
fn set_actions(actions: Vec<Action>, con: &DbConnection) -> MizeResult<()> {
|
|
let (wait_tx, waiter_rx) = std::sync::oneshot::channel::<()>();
|
|
con.reducers
|
|
.update_upcoming_then(actions, |_, _| wait_tx.send(()).unwrap())?;
|
|
waiter_rx.recv().unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn actions_to_string(actions: &Vec<Action>) -> MizeResult<String> {
|
|
let mut string = String::new();
|
|
|
|
for action in actions.iter() {
|
|
string.push_str(format!("{}\n", action.name).as_str());
|
|
}
|
|
|
|
Ok(string)
|
|
}
|
|
|
|
fn actions_from_string(mut old_actions: Vec<Action>, string: String) -> MizeResult<Vec<Action>> {
|
|
let mut new_actions = Vec::new();
|
|
for line in string.lines() {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let mut action = Action {
|
|
name: line.to_owned(),
|
|
activity: None,
|
|
task: None,
|
|
start: None,
|
|
end: None,
|
|
duration: None,
|
|
};
|
|
parse_action(&mut action);
|
|
new_actions.push(action);
|
|
}
|
|
Ok(new_actions)
|
|
}
|
|
|
|
fn parse_action(action: &mut Action) {
|
|
let parts: Vec<&str> = action.name.split_whitespace().collect();
|
|
}
|
|
|
|
fn command_edit_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> {
|
|
let mut spacetime = mize.get_part_native::<SpacetimePart>("spacetime")?;
|
|
|
|
// get list of activities
|
|
let activities = get_activities(&mut mize, spacetime.con())?;
|
|
|
|
let con = spacetime.con();
|
|
|
|
// write to tmp file
|
|
let mut tmp_file_content = String::new();
|
|
for ac in activities.iter() {
|
|
tmp_file_content.push_str(format!("{} {}", ac.id, ac.name).as_str());
|
|
if !ac.parents_ids.is_empty() {
|
|
tmp_file_content.push_str(" -");
|
|
for parent_id in ac.parents_ids.iter() {
|
|
let parent_ac = activities.iter().find(|ac| ac.id == *parent_id).unwrap();
|
|
tmp_file_content.push_str(" ");
|
|
tmp_file_content.push_str(&parent_ac.name);
|
|
}
|
|
}
|
|
tmp_file_content.push_str("\n");
|
|
}
|
|
let tmp_file = std::env::temp_dir().join(format!(
|
|
"ppc-edit-activities-{}.txt",
|
|
chrono::Utc::now().timestamp()
|
|
));
|
|
std::fs::write(&tmp_file, tmp_file_content)
|
|
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
|
|
|
|
loop {
|
|
// open $EDITOR blocking
|
|
std::process::Command::new("nvim")
|
|
.arg(&tmp_file)
|
|
.status()
|
|
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
|
|
|
|
let new_file_content = std::fs::read_to_string(&tmp_file)
|
|
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
|
|
|
|
let mut input = String::new();
|
|
|
|
let old_activities = get_activities(&mut mize, con)?;
|
|
if let Err(err) =
|
|
parse_activities_from_text(&mut mize, con, new_file_content, old_activities)
|
|
{
|
|
println!("error parsing the activities from the file: {:?}", err);
|
|
println!("press ENTER to open the file again");
|
|
|
|
// wait for user input
|
|
// so that they can se the err msg, before running nvim again
|
|
std::io::stdin().read_line(&mut input)?;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// delete tmpfile
|
|
let _ = std::fs::remove_file(&tmp_file);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_activities(mize: &mut Mize, con: &mut DbConnection) -> MizeResult<Vec<Activity>> {
|
|
let (exit_tx, exit_rx) = std::sync::oneshot::channel::<Vec<Activity>>();
|
|
con.subscription_builder()
|
|
.on_applied(|ctx| {
|
|
exit_tx.send(ctx.db.activities().iter().collect()).unwrap();
|
|
})
|
|
.add_query(|q| q.from.activities())
|
|
.subscribe();
|
|
let activities = exit_rx.recv().unwrap();
|
|
Ok(activities)
|
|
}
|
|
|
|
fn parse_activities_from_text(
|
|
mize: &mut Mize,
|
|
con: &mut DbConnection,
|
|
text: String,
|
|
old_activities: Vec<Activity>,
|
|
) -> MizeResult<()> {
|
|
let mut activities_to_delete = old_activities.clone();
|
|
let mut wait_revievers = Vec::new();
|
|
for line in text.lines() {
|
|
if line == "" {
|
|
continue;
|
|
}
|
|
|
|
// split
|
|
let mut parts = line.split(" ");
|
|
let id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
|
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
|
|
|
// remove from to-delete list
|
|
if let Some(pos) = activities_to_delete.iter().position(|ac| ac.id == id) {
|
|
activities_to_delete.remove(pos);
|
|
}
|
|
|
|
// wait rx
|
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
|
wait_revievers.push(wait_rx);
|
|
|
|
// update or create activity
|
|
if let Some(old_activity) = old_activities.iter().find(|ac| ac.id == id) {
|
|
con.reducers.update_activity_then(
|
|
Activity {
|
|
id,
|
|
name,
|
|
task: old_activity.task,
|
|
parents_ids: old_activity.parents_ids.clone(),
|
|
},
|
|
move |_, _| {
|
|
wait_tx.send(());
|
|
},
|
|
)?;
|
|
} else {
|
|
con.reducers.update_activity_then(
|
|
Activity {
|
|
id: 0,
|
|
name,
|
|
task: None,
|
|
parents_ids: Vec::new(),
|
|
},
|
|
move |_, _| {
|
|
wait_tx.send(());
|
|
},
|
|
)?;
|
|
}
|
|
}
|
|
|
|
// remove deleted activities
|
|
for activity in activities_to_delete {
|
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
|
wait_revievers.push(wait_rx);
|
|
con.reducers.remove_activity_then(activity, move |_, _| {
|
|
wait_tx.send(());
|
|
})?;
|
|
}
|
|
|
|
// wait for db stuff to finish
|
|
for wait_rx in wait_revievers {
|
|
wait_rx.recv().unwrap();
|
|
}
|
|
let mut wait_revievers = Vec::new();
|
|
|
|
// check for declared parents
|
|
let new_activities = get_activities(mize, con)?;
|
|
println!("new_activities: {:?}", new_activities);
|
|
for line in text.lines() {
|
|
if line == "" {
|
|
continue;
|
|
}
|
|
|
|
// split
|
|
let mut parts = line.split(" ");
|
|
let id: u64 = parts.next().ok_or(mize_err!(".."))?.parse()?;
|
|
let name = parts.next().ok_or(mize_err!(".."))?.to_string();
|
|
|
|
// parse parents
|
|
let seperator = parts.next();
|
|
if seperator.is_some() && seperator.unwrap() == "-" {
|
|
let mut parents_ids = Vec::new();
|
|
for parent_name in parts {
|
|
// find id from old_parts by this name
|
|
let res_from_old = old_activities.iter().find(|ac| ac.name == parent_name);
|
|
let res_from_new = new_activities.iter().find(|ac| ac.name == parent_name);
|
|
let parent_activity = res_from_old.or(res_from_new).ok_or(mize_err!(
|
|
"activity '{name}' declares parent '{parent_name}'"
|
|
))?;
|
|
parents_ids.push(parent_activity.id);
|
|
}
|
|
let child_activity = new_activities
|
|
.iter()
|
|
.find(|ac| ac.name == name)
|
|
.ok_or(mize_err!(".."))?;
|
|
let (wait_tx, wait_rx) = oneshot::channel::<()>();
|
|
wait_revievers.push(wait_rx);
|
|
con.reducers.update_activity_then(
|
|
Activity {
|
|
parents_ids,
|
|
..child_activity.to_owned()
|
|
},
|
|
move |_, _| {
|
|
wait_tx.send(());
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// wait for db stuff to finish
|
|
for wait_rx in wait_revievers {
|
|
wait_rx.recv().unwrap();
|
|
}
|
|
|
|
Ok(())
|
|
}
|