use std::{
fs,
io::{BufRead, Cursor},
path::Path,
str,
};
use byteorder::{BigEndian, ReadBytesExt};
use palmrs::{
database::{
info::ExtraInfoRecord,
record::DatabaseRecord,
PalmDatabase,
PdbWithCategoriesDatabase,
},
sync::{
conduit::{ConduitRequirements, WithinConduit},
SyncMode,
},
};
use stable_eyre::eyre::{eyre, Report, WrapErr};
#[derive(Debug, Clone, PartialEq)]
pub struct ToDoTask {
pub task_text: String,
pub note_text: Option<String>,
pub category: Option<String>,
pub priority: u8,
pub completed: bool,
pub due_date: Option<(u16, u8, u8)>,
}
impl ToDoTask {
pub fn as_todotxt_entry(&self) -> String {
let mut s = String::new();
if self.completed {
s.push_str("x ");
}
s.push('(');
s.push((self.priority + 64) as char);
s.push(')');
s.push(' ');
if let Some((year, month, day)) = &self.due_date {
s.push_str(&format!("{:04}-{:02}-{:02} ", year, month, day));
}
s.push_str(&self.task_text.replace("\n", " "));
if let Some(category) = &self.category {
s.push_str(&format!(" @{}", category.replace(" ", "_")));
}
if let Some(note_text) = &self.note_text {
s.push_str(&format!(" note={:?}", note_text));
}
s
}
}
pub fn device_database_parse(db_path: &Path) -> Result<Vec<ToDoTask>, Report> {
let db_content =
fs::read(db_path).wrap_err_with(|| eyre!("Failed to read database: {:?}", db_path))?;
let database = PalmDatabase::<PdbWithCategoriesDatabase>::from_bytes(&db_content)
.wrap_err_with(|| eyre!("Failed to parse device ToDoDB: {:?}", db_path))?;
let categories: Vec<(u8, String)> = {
let mut categories = Vec::new();
if let Some(db_cats) = database.app_info.data_item_categories() {
for cat in db_cats.iter() {
categories.push((
cat.category_id,
String::from(cat.name_try_str().unwrap_or("ErrCategory")),
));
}
}
categories
};
let mut tasks = Vec::new();
'dbtaskparse: for (idx, (rec_hdr, rec_data)) in
(0..).zip(database.list_records_resources().iter())
{
log::trace!("records[{}].rec_hdr = {:?}", idx, &rec_hdr);
if rec_hdr.data_len().unwrap_or(0) == 0 {
break 'dbtaskparse;
}
let rec_attributes = rec_hdr.attributes().unwrap_or_default();
let category = {
let mut catname: Option<String> = None;
let catid = rec_attributes.category;
'catsearch: for (x_catid, x_catname) in categories.iter() {
if catid == *x_catid {
catname = Some(x_catname.clone());
break 'catsearch;
}
}
catname
};
let mut cursor = Cursor::new(&rec_data);
let due_date = {
let mut due_date: Option<(u16, u8, u8)> = None;
let raw_date = cursor.read_u16::<BigEndian>()?;
if raw_date != 0xFFFF {
due_date = Some((
((raw_date >> 9) & 0x007F) + 1904,
((raw_date >> 5) & 0x000F) as u8,
(raw_date & 0x001F) as u8,
));
}
due_date
};
let priority = cursor.read_u8()?;
let completed = priority & 0x80 != 0;
let priority = priority & 0x7F;
let task_text = {
let mut buf: Vec<u8> = Vec::new();
let count = cursor
.read_until(0x00, &mut buf)
.expect("cursor read fail?");
str::from_utf8(&buf[0..(count - 1)])
.wrap_err("UTF-8 conversion failed")?
.to_string()
};
let note_text = {
let mut buf: Vec<u8> = Vec::new();
let count = cursor
.read_until(0x00, &mut buf)
.expect("cursor read fail?");
if count > 1 {
let s = str::from_utf8(&buf[0..(count - 1)])
.wrap_err("UTF-8 conversion failed")?
.to_string();
Some(s)
} else {
None
}
};
tasks.push(ToDoTask {
task_text,
note_text,
category,
priority,
completed,
due_date,
});
}
Ok(tasks)
}
pub fn palm_to_todotxt(conduit: &WithinConduit) -> Result<(), Report> {
log::info!("palm_to_todotxt: parsing Palm OS tasks database");
let mut device_database = conduit.config.path_device.clone();
device_database.push("ToDoDB.pdb");
let device_tasks = device_database_parse(&device_database)?;
log::trace!("device_tasks = {:#?}", &device_tasks);
let mut todotxt_incomplete: Vec<String> = Vec::new();
let mut todotxt_completed: Vec<String> = Vec::new();
for task in device_tasks.iter() {
if task.completed {
todotxt_completed.push(task.as_todotxt_entry());
} else {
todotxt_incomplete.push(task.as_todotxt_entry());
}
}
log::info!(
"palm_to_todotxt: {} incomplete, {} complete tasks",
todotxt_incomplete.len(),
todotxt_completed.len(),
);
let mut todotxt_path = conduit.config.path_local.clone();
todotxt_path.push("todo.txt");
log::debug!("todotxt_path = {:?}", &todotxt_path);
let mut donetxt_path = conduit.config.path_local.clone();
donetxt_path.push("done.txt");
log::debug!("donetxt_path = {:?}", &donetxt_path);
let single_file = conduit
.config
.environment
.get("SINGLE_FILE")
.unwrap_or(&String::from("0"))
.as_str()
== "1";
if single_file {
log::info!("palm_to_todotxt: operating in single-file mode");
fs::write(&todotxt_path, {
let mut v = Vec::new();
for task in todotxt_incomplete.iter() {
v.push(task.clone());
}
for task in todotxt_completed.iter() {
v.push(task.clone());
}
v.join("\n")
})?;
} else {
log::info!("palm_to_todotxt: operating in dual-file mode");
fs::write(&todotxt_path, todotxt_incomplete.join("\n"))?;
fs::write(&donetxt_path, todotxt_completed.join("\n"))?;
}
log::info!("palm_to_todotxt: wrote our todo.txt file(s) :)");
Ok(())
}
pub fn main() -> Result<(), Report> {
env_logger::init();
stable_eyre::install()?;
let conduit = WithinConduit::new(
"palmrs-conduit-todotxt",
ConduitRequirements::new()
.with_databases(&["ToDoDB"])
.finish(),
)
.from_env()
.wrap_err("WithinConduit build failed")?;
log::trace!("conduit = {:#?}", &conduit);
match conduit.config.sync_mode {
SyncMode::KeepDevice => palm_to_todotxt(&conduit),
_ => unimplemented!(),
}
}