aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bill.rs69
-rw-r--r--src/cli.rs122
-rw-r--r--src/edit.rs55
-rw-r--r--src/main.rs1
-rw-r--r--tmp.md0
5 files changed, 218 insertions, 29 deletions
diff --git a/src/bill.rs b/src/bill.rs
index e03cd22..4b66554 100644
--- a/src/bill.rs
+++ b/src/bill.rs
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
-use crate::who::Who;
+use crate::{display::SimpleTable, string_vec, who::Who};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Bills {
@@ -169,3 +169,70 @@ impl SplitResult {
self.final_result.get(&(payer, payee)).copied()
}
}
+
+impl Bills {
+ pub fn table(self) -> String {
+ let mut table = SimpleTable::new(string_vec![
+ "#", "Who", "|", "Paid", "|", "Split", "|", "Reason"
+ ]);
+ let mut items: Vec<_> = self.items.into_iter().collect();
+ items.sort_by(|a, b| {
+ b.1.paid
+ .partial_cmp(&a.1.paid)
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+ for (_, items) in items {
+ let split = items
+ .split
+ .iter()
+ .map(|i| i.to_string())
+ .collect::<Vec<String>>()
+ .join(", ");
+ table.push_item(string_vec![
+ "",
+ items.who_paid,
+ "|",
+ items.paid,
+ "|",
+ split,
+ "|",
+ items.reason
+ ]);
+ }
+ table.to_string()
+ }
+
+ pub fn from_table_str(table_str: impl Into<String>) -> Bills {
+ let mut bills = Bills::default();
+ let table_str = table_str.into();
+
+ for line in table_str.lines() {
+ let line = line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ let parts: Vec<&str> = line.split('|').map(|s| s.trim()).collect();
+ if parts.len() != 4 {
+ continue;
+ }
+
+ let who_paid = parts[0];
+ let paid_str = parts[1];
+ let split_str = parts[2];
+ let reason = parts[3];
+
+ let paid = paid_str.parse::<f64>().unwrap_or(0.0);
+
+ let split: Vec<&str> = split_str
+ .split(',')
+ .map(|s| s.trim())
+ .filter(|s| !s.is_empty())
+ .collect();
+
+ bills.add_bill(who_paid, reason, paid, split);
+ }
+
+ bills
+ }
+}
diff --git a/src/cli.rs b/src/cli.rs
index aa0d317..a39624f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,4 +1,4 @@
-use std::{fs::create_dir_all, path::PathBuf};
+use std::{fs::create_dir_all, io::ErrorKind, path::PathBuf};
use mingling::{
AnyOutput, Groupped,
@@ -13,6 +13,7 @@ use crate::{
bill::{BillItem, Bills, SplitResult},
calc::calculate_from,
display::SimpleTable,
+ edit::{get_default_editor, input_with_editor_cutsom},
error::BillSplitError,
string_vec,
};
@@ -30,19 +31,25 @@ pub async fn entry() {
program.with_dispatchers((
ClearAllBillCommand,
AddBillCommand,
- // RenameMemberCommand,
- // RenameBillCommand,
+ EditCommand,
ListAllBillCommand,
));
+ program.with_dispatchers((
+ EditWithViCommand,
+ EditWithVimCommand,
+ EditWithNvimCommand,
+ EditWithHelixCommand,
+ EditWithNanoCommand,
+ ));
+
// Execute
program.exec().await;
}
dispatcher!("clear", ClearAllBillCommand => ClearAllBillEntry);
dispatcher!("add", AddBillCommand => AddBillEntry);
-// dispatcher!("rename.member", RenameMemberCommand => RenameMemberEntry);
-// dispatcher!("rename.bill", RenameBillCommand => RenameBillEntry);
+dispatcher!("edit", EditCommand => EditEntry);
dispatcher!("ls", ListAllBillCommand => ListAllBillEntry);
#[chain]
@@ -129,36 +136,56 @@ async fn handle_list_bills(prev: StateListBills) -> NextProcess {
#[renderer]
fn render_bills(prev: ResultBills) {
- let mut table = SimpleTable::new(string_vec!["Who", "|", "Paid", "|", "Split", "|", "Reason"]);
- for (_, items) in prev.inner.items {
- let split = items
- .split
- .iter()
- .map(|i| i.to_string())
- .collect::<Vec<String>>()
- .join(", ");
- table.push_item(string_vec![
- items.who_paid,
- "",
- items.paid,
- "",
- split,
- "",
- items.reason
- ]);
- }
- r_println!("{}", table)
+ r_println!("{}", prev.inner.table())
}
#[renderer]
fn render_split_result(prev: ResultSplitResult) {
let mut table = SimpleTable::new(string_vec!["Who", "|", "Should Pay", "|", "To"]);
for ((who, to), paid) in prev.inner.final_result {
- table.push_item(string_vec![who, "", paid, "", to]);
+ table.push_item(string_vec![who, "|", paid, "|", to]);
}
r_println!("{}", table)
}
+pack!(StateEditBills = String); // Editor name
+pack!(ErrorEditorNotFound = String);
+
+#[chain]
+async fn parse_edit_cmd(prev: EditEntry) -> NextProcess {
+ let editor = Picker::<()>::new(prev.inner)
+ .pick_or::<String>(["--editor", "-e"], get_default_editor())
+ .unpack_directly()
+ .0;
+ let state = StateEditBills::new(editor);
+ AnyOutput::new(state).route_chain()
+}
+
+#[chain]
+async fn exec_edit_cmd(prev: StateEditBills) -> NextProcess {
+ let text = match input_with_editor_cutsom(
+ read_bills().table(),
+ state_edit_file_path(),
+ "#",
+ prev.inner.clone(),
+ ) {
+ Ok(v) => v,
+ Err(e) => match e.kind() {
+ ErrorKind::NotFound => {
+ return AnyOutput::new(ErrorEditorNotFound::new(prev.inner)).route_renderer();
+ }
+ _ => panic!("Error editing bills: {}", e),
+ },
+ };
+ write_bills(Bills::from_table_str(text));
+ Empty::new(()).to_render()
+}
+
+#[renderer]
+fn render_error_editor_not_found(prev: ErrorEditorNotFound) {
+ r_println!("Error: Editor \"{}\" not found", prev.inner);
+}
+
#[renderer]
fn render_error_duplicate_split_members(_prev: ErrorDuplicateSplitMembers) {
r_println!("Error: Duplicate members found in split list");
@@ -200,6 +227,10 @@ fn state_file_path() -> PathBuf {
cobill_dir().join("state.yml")
}
+fn state_edit_file_path() -> PathBuf {
+ cobill_dir().join("edit.state.md")
+}
+
fn read_bills() -> Bills {
let dir = cobill_dir();
create_dir_all(dir).unwrap();
@@ -218,12 +249,47 @@ fn read_bills() -> Bills {
}
}
-fn op_bills<F: FnOnce(&mut Bills)>(op: F) {
- let mut bills = read_bills();
- op(&mut bills);
+fn write_bills(bills: Bills) {
let state_file = state_file_path();
let contents = serde_yaml::to_string(&bills).unwrap();
std::fs::write(state_file, contents).unwrap();
}
+fn op_bills<F: FnOnce(&mut Bills)>(op: F) {
+ let mut bills = read_bills();
+ op(&mut bills);
+ write_bills(bills);
+}
+
+dispatcher!("vi", EditWithViCommand => EditWithViEntry);
+dispatcher!("vim", EditWithVimCommand => EditWithVimEntry);
+dispatcher!("nvim", EditWithNvimCommand => EditWithNvimEntry);
+dispatcher!("helix", EditWithHelixCommand => EditWithHelixEntry);
+dispatcher!("nano", EditWithNanoCommand => EditWithNanoEntry);
+
+#[chain]
+async fn edit_with_vi(_prev: EditWithViEntry) -> NextProcess {
+ EditEntry::new(string_vec!["-e", "vi"]).to_chain()
+}
+
+#[chain]
+async fn edit_with_vim(_prev: EditWithVimEntry) -> NextProcess {
+ EditEntry::new(string_vec!["-e", "vim"]).to_chain()
+}
+
+#[chain]
+async fn edit_with_nvim(_prev: EditWithNvimEntry) -> NextProcess {
+ EditEntry::new(string_vec!["-e", "nvim"]).to_chain()
+}
+
+#[chain]
+async fn edit_with_helix(_prev: EditWithHelixEntry) -> NextProcess {
+ EditEntry::new(string_vec!["-e", "helix"]).to_chain()
+}
+
+#[chain]
+async fn edit_with_nano(_prev: EditWithNanoEntry) -> NextProcess {
+ EditEntry::new(string_vec!["-e", "nano"]).to_chain()
+}
+
gen_program!();
diff --git a/src/edit.rs b/src/edit.rs
new file mode 100644
index 0000000..f9c751c
--- /dev/null
+++ b/src/edit.rs
@@ -0,0 +1,55 @@
+use std::process::Command;
+
+pub fn input_with_editor_cutsom(
+ default_text: impl AsRef<str>,
+ cache_file: impl AsRef<std::path::Path>,
+ comment_char: impl AsRef<str>,
+ editor: String,
+) -> Result<String, std::io::Error> {
+ let cache_path = cache_file.as_ref();
+ let default_content = default_text.as_ref();
+ let comment_prefix = comment_char.as_ref();
+
+ // Write default text to cache file
+ std::fs::write(cache_path, default_content)?;
+
+ // Open editor with cache file
+ let status = Command::new(editor).arg(cache_path).status()?;
+
+ if !status.success() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "Editor exited with non-zero status",
+ ));
+ }
+
+ // Read the modified content
+ let content = std::fs::read_to_string(cache_path)?;
+
+ // Remove comment lines and trim
+ let processed_content: String = content
+ .lines()
+ .filter_map(|line| {
+ let trimmed = line.trim();
+ if trimmed.starts_with(comment_prefix) {
+ None
+ } else {
+ Some(line)
+ }
+ })
+ .collect::<Vec<&str>>()
+ .join("\n");
+
+ // Delete the cache file
+ let _ = std::fs::remove_file(cache_path);
+
+ Ok(processed_content)
+}
+
+pub fn get_default_editor() -> String {
+ if let Ok(editor) = std::env::var("EDITOR") {
+ return editor;
+ }
+
+ "nano".to_string()
+}
diff --git a/src/main.rs b/src/main.rs
index 614693b..8a31d09 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@ mod bill;
mod calc;
mod cli;
mod display;
+mod edit;
mod error;
mod macros;
mod who;
diff --git a/tmp.md b/tmp.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tmp.md