Files
mize/packages/ppc/timetrack/mod.rs
Sebastian Moser c9ea571744 c2vi dash working
2026-05-06 12:48:20 +02:00

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(())
}