545 lines
17 KiB
Rust
545 lines
17 KiB
Rust
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::<crate::CliPart>("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::<SpacetimePart>("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::<SpacetimePart>("spacetime")?;
|
|
let con = spacetime.con();
|
|
|
|
let break_flag = matches.get_flag("break");
|
|
let action = matches.get_many::<String>("action")
|
|
.map(|vals| vals.map(|s| s.as_str()).collect::<Vec<&str>>().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::<SpacetimePart>("spacetime")?;
|
|
let action = get_current_action(&mut mize, spacetime.con())?;
|
|
out!("{}", 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)
|
|
}
|
|
|
|
#[cfg(feature = "target-os")]
|
|
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(())
|
|
}
|
|
|
|
#[cfg(feature = "target-os")]
|
|
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 {
|
|
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::<SpacetimePart>("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<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(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();
|
|
}
|
|
|
|
#[cfg(feature = "target-os")]
|
|
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
|
|
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<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: &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 |_, _| {
|
|
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(())
|
|
}
|