Files
mize/marts/src/timetrack/mod.rs
2026-10-11 22:20:46 +02:00

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