use std::sync::oneshot; #[cfg(feature = "target-os")] use clap::{Arg, ArgAction, ArgMatches, Command}; use generated::spacetime::{Profile, TaskPart}; use mize::out; use mize::proto::MizeMessage; use mize::{Mize, MizeError, MizePart, MizePartCreate, MizeResult, mize_err, mize_part}; use spacetimedb_sdk::{DbContext, Table}; use generated::spacetime::{ Action, ActivitiesTableAccess, Activity, DbConnection, TaskPartTableAccess, activitiesQueryTableAccess, next_action, remove_activity, task_partQueryTableAccess, update_activity, update_upcoming, }; use crate::spacetime::SpacetimePart; use spacetimedb_sdk::TableWithPrimaryKey; #[mize_part] #[derive(Default)] pub struct TimeTrack { mize: Mize, } pub fn part_timetrack(mize: &mut Mize) -> MizeResult<()> { #[cfg(feature = "target-os")] { let mut cli = mize.get_part_native::("cli")?; cli.set_command("sci"); 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").arg( Arg::new("break") .short('b') .action(ArgAction::SetTrue), ).arg( Arg::new("action") .trailing_var_arg(true) .allow_hyphen_values(true) .num_args(0..), // Accepts 0 or more arguments ), 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.add_listener( "timetrack.sub_current", Box::new(timetrack_sub_current_listener), )?; } mize.register_part(Box::new(TimeTrack { mize: mize.clone() }))?; Ok(()) } fn send_current_update_msg(conn_id: u64, data: TaskPart, mize: &mut Mize) -> MizeResult<()> { let now = fixed_len(data.current.name.clone(), 48); let next = fixed_len( data.upcoming .iter() .nth(0) .unwrap_or(&Action { name: "".to_string(), activity: None, task: None, start: None, end: None, duration: None, }) .name .clone(), 48, ); let msg = MizeMessage::new_custom( "timetrack.current", format!(" {} NEXT: {} ", now, next), conn_id, ); let con = mize.get_connection(conn_id)?; con.send(msg)?; Ok(()) } fn fixed_len(mut buf: String, len: usize) -> String { if buf.len() > len { buf.truncate(len - 4); // Cut off extra characters buf.push_str(" ..."); } else { while buf.len() < len { buf.push(' '); // Fill with whitespace } } buf } pub fn timetrack_sub_current_listener(msg: MizeMessage, mize: &mut Mize) -> MizeResult<()> { let spacetime = mize.get_part_native::("spacetime").unwrap(); let con = spacetime.con(); let mut mize_clone = mize.clone(); let mut mize_clone_two = mize.clone(); con.subscription_builder() .on_applied(move |ctx| { let data = ctx.db.task_part().identity().find(&ctx.identity()).unwrap(); if let Err(e) = send_current_update_msg(msg.conn_id, data, &mut mize_clone) { e.log(); } }) .add_query(|q| q.from.task_part()) .subscribe(); con.db .task_part() .on_update(move |_, _old: &TaskPart, new: &TaskPart| { if let Err(e) = send_current_update_msg(msg.conn_id, new.clone(), &mut mize_clone_two) { e.log(); } }); Ok(()) } impl MizePartCreate for TimeTrack {} impl MizePart for TimeTrack { fn name(&self) -> &'static str { "timetrack" } } #[cfg(feature = "target-os")] fn command_next_action(matches: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("spacetime")?; let con = spacetime.con(); let break_flag = matches.get_flag("break"); let action = matches.get_many::("action") .map(|vals| vals.map(|s| s.as_str()).collect::>().join(" ")); let action = match action { Some(action) => Some(Action { name: action.clone(), activity: None, task: None, start: None, end: None, duration: None, }), None => None, }; let old_action = get_current_action(&mut mize, con)?; out!("before action: {}", old_action.name); let (wait_tx, waiter_rx) = std::sync::oneshot::channel::<()>(); con.reducers.next_action_then(break_flag, action, |_, _| { wait_tx.send(()).unwrap(); })?; waiter_rx.recv()?; let new_action = get_current_action(&mut mize, con)?; out!("new action: {}", new_action.name); Ok(()) } #[cfg(feature = "target-os")] fn command_current_action(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("spacetime")?; let action = get_current_action(&mut mize, spacetime.con())?; out!("{}", action.name); Ok(()) } fn get_current_action(_mize: &mut Mize, con: &DbConnection) -> MizeResult { let (wait_tx, wait_rx) = std::sync::oneshot::channel::(); 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) } #[cfg(feature = "target-os")] fn command_list_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("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(()) } #[cfg(feature = "target-os")] fn command_list_actions(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("spacetime")?; let actions = get_actions(spacetime.con())?; for action in actions { out!("{}", action.name) } Ok(()) } #[cfg(feature = "target-os")] fn command_edit_action_list(matches: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("spacetime")?; // maybe append a template to the action list if matches.get_flag("defaultTemplate") { out!("Appending template..."); return Ok(()); } // get list of actions let mut actions = get_actions(spacetime.con())?; // write to tmp file let 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 crate::cli_spawn(&mut mize, format!("nvim {}", tmp_file.display()))?; // 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(new_file_content)?; set_actions(actions, spacetime.con())?; // delete tmpfile let _ = std::fs::remove_file(&tmp_file); Ok(()) } fn get_actions(con: &DbConnection) -> MizeResult> { let (exit_tx, exit_rx) = std::sync::oneshot::channel::>(); 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, 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) -> MizeResult { 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(string: String) -> MizeResult> { 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(); } #[cfg(feature = "target-os")] fn command_edit_activities(_: &ArgMatches, mut mize: Mize) -> MizeResult<()> { let mut spacetime = mize.get_part_native::("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 crate::cli_spawn(&mut mize, format!("nvim {}", tmp_file.display()))?; 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) { out!("error parsing the activities from the file: {:?}", err); out!("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: &DbConnection) -> MizeResult> { let (exit_tx, exit_rx) = std::sync::oneshot::channel::>(); 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: &DbConnection, text: String, old_activities: Vec, ) -> 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 |_, _| { let _ = wait_tx.send(()); }, )?; } else { con.reducers.update_activity_then( Activity { id: 0, name, task: None, parents_ids: Vec::new(), }, move |_, _| { let _ = 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 |_, _| { let _ = 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)?; out!("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 |_, _| { let _ = wait_tx.send(()); }, )?; } } // wait for db stuff to finish for wait_rx in wait_revievers { wait_rx.recv().unwrap(); } Ok(()) }