aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-04-16 23:24:52 +0800
committer魏曹先生 <1992414357@qq.com>2026-04-16 23:24:52 +0800
commit6e36fc3707e791c3c748133d648957706b54fd3a (patch)
tree3851ed69d60f331a803a6c19c97a56829a11f2f5
parent363fbc6e98f832471a17a10ec18e8823df6a2ed5 (diff)
Add CLI commands for bill management and persistence
-rw-r--r--Cargo.lock102
-rw-r--r--Cargo.toml7
-rw-r--r--src/bill.rs24
-rw-r--r--src/cli.rs235
-rw-r--r--src/cli/calc_cmd.rs9
-rw-r--r--src/cli/consts.rs1
-rw-r--r--src/cli/dispatchers.rs12
-rw-r--r--src/cli/entry.rs21
-rw-r--r--src/cli/io_error.rs42
-rw-r--r--src/cli/ops_cmd.rs80
-rw-r--r--src/display.rs144
-rw-r--r--src/macros.rs6
-rw-r--r--src/main.rs14
-rw-r--r--src/who.rs4
14 files changed, 503 insertions, 198 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fcdbdc8..02d2ef5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -39,14 +39,38 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
name = "cobill"
version = "0.1.0"
dependencies = [
+ "dirs",
"mingling",
"serde",
+ "serde_yaml",
+ "strip-ansi-escapes",
"thiserror 1.0.69",
"tokio",
"uuid",
]
[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -60,6 +84,17 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
@@ -154,6 +189,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -208,6 +252,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -248,6 +298,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
name = "ron"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -351,6 +412,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
+dependencies = [
+ "vte",
+]
+
+[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -492,12 +562,27 @@ version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
- "getrandom",
+ "getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]]
+name = "vte"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -595,6 +680,21 @@ dependencies = [
]
[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
name = "winnow"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 0d58106..627fde0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,14 +9,13 @@ path = "src/main.rs"
[dependencies]
mingling = { path = "../mingling/mingling", features = ["full"] }
-
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread" ] }
-
uuid = { version = "1", features = ["v4"] }
-
serde = { version = "1", features = ["derive"] }
-
+serde_yaml = "0.9.33"
+strip-ansi-escapes = "0.2.1"
thiserror = "1.0.69"
+dirs = "6"
[build-dependencies]
mingling = { path = "../mingling/mingling", features = ["comp"] }
diff --git a/src/bill.rs b/src/bill.rs
index 16923f9..e03cd22 100644
--- a/src/bill.rs
+++ b/src/bill.rs
@@ -1,14 +1,16 @@
use std::collections::BTreeMap;
+use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::who::Who;
-#[derive(Default)]
+#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Bills {
- pub items: BTreeMap<Uuid, BillItem>,
+ pub items: BTreeMap<String, BillItem>,
}
+#[derive(Debug, Default, Serialize, Deserialize)]
pub struct BillItem {
pub who_paid: Who,
pub reason: String,
@@ -16,11 +18,13 @@ pub struct BillItem {
pub split: Vec<Who>,
}
+#[derive(Debug, Default, Serialize, Deserialize)]
pub struct SplitResult {
pub items: BTreeMap<Who, Vec<SplitResultItem>>,
pub final_result: BTreeMap<(Who, Who), f64>,
}
+#[derive(Debug, Serialize, Deserialize)]
pub struct SplitResultItem {
pub payee: Who,
pub bill: f64,
@@ -42,24 +46,24 @@ impl Bills {
/// Add a new bill item
pub fn add_item(&mut self, item: BillItem) -> Uuid {
let id = Uuid::new_v4();
- self.items.insert(id, item);
+ self.items.insert(id.to_string(), item);
id
}
/// Get a bill item by ID (immutable reference)
pub fn get_item(&self, id: &Uuid) -> Option<&BillItem> {
- self.items.get(id)
+ self.items.get(&id.to_string())
}
/// Get a bill item by ID (mutable reference)
pub fn get_item_mut(&mut self, id: &Uuid) -> Option<&mut BillItem> {
- self.items.get_mut(id)
+ self.items.get_mut(&id.to_string())
}
/// Update the bill item with the specified ID
pub fn update_item(&mut self, id: &Uuid, item: BillItem) -> bool {
- if self.items.contains_key(id) {
- self.items.insert(*id, item);
+ if self.items.contains_key(&id.to_string()) {
+ self.items.insert(id.to_string(), item);
true
} else {
false
@@ -68,17 +72,17 @@ impl Bills {
/// Delete the bill item with the specified ID
pub fn delete_item(&mut self, id: &Uuid) -> Option<BillItem> {
- self.items.remove(id)
+ self.items.remove(&id.to_string())
}
/// Get all bill items
- pub fn get_all_items(&self) -> &BTreeMap<Uuid, BillItem> {
+ pub fn get_all_items(&self) -> &BTreeMap<String, BillItem> {
&self.items
}
/// Check if a bill item with the specified ID exists
pub fn contains_item(&self, id: &Uuid) -> bool {
- self.items.contains_key(id)
+ self.items.contains_key(&id.to_string())
}
/// Clear all bill items
diff --git a/src/cli.rs b/src/cli.rs
index c1d1240..aa0d317 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,6 +1,229 @@
-pub mod calc_cmd;
-pub mod consts;
-pub mod dispatchers;
-pub mod entry;
-pub mod io_error;
-pub mod ops_cmd;
+use std::{fs::create_dir_all, path::PathBuf};
+
+use mingling::{
+ AnyOutput, Groupped,
+ macros::{chain, dispatcher, gen_program, pack, r_println, renderer},
+ marker::NextProcess,
+ parser::Picker,
+ setup::GeneralRendererSetup,
+};
+use serde::Serialize;
+
+use crate::{
+ bill::{BillItem, Bills, SplitResult},
+ calc::calculate_from,
+ display::SimpleTable,
+ error::BillSplitError,
+ string_vec,
+};
+
+pub async fn entry() {
+ let mut program = ThisProgram::new();
+
+ // Add Completion
+ program.with_dispatcher(CompletionDispatcher);
+
+ // Add General Renderer
+ program.with_setup(GeneralRendererSetup);
+
+ // Add Dispatchers
+ program.with_dispatchers((
+ ClearAllBillCommand,
+ AddBillCommand,
+ // RenameMemberCommand,
+ // RenameBillCommand,
+ ListAllBillCommand,
+ ));
+
+ // Execute
+ program.exec().await;
+}
+
+dispatcher!("clear", ClearAllBillCommand => ClearAllBillEntry);
+dispatcher!("add", AddBillCommand => AddBillEntry);
+// dispatcher!("rename.member", RenameMemberCommand => RenameMemberEntry);
+// dispatcher!("rename.bill", RenameBillCommand => RenameBillEntry);
+dispatcher!("ls", ListAllBillCommand => ListAllBillEntry);
+
+#[chain]
+async fn do_clear_cmd(_prev: ClearAllBillEntry) -> NextProcess {
+ op_bills(|b| b.clear_items());
+ Empty::new(()).to_render()
+}
+
+pack!(StateAddBillItem = BillItem);
+
+#[chain]
+async fn parse_add_cmd(prev: AddBillEntry) -> NextProcess {
+ let picked = Picker::new(prev.inner)
+ .pick_or_route::<f64>(["--paid", "-p"], PaidRequired::new(()).to_render())
+ .pick_or_route::<Vec<String>>(["--for", "-f"], ForMembersRequired::new(()).to_render())
+ .pick_or::<String>(
+ ["--reason", "-r", "--message", "-m"],
+ "No reason".to_string(),
+ )
+ .pick_or_route::<String>((), MemberRequired::new(()).to_render())
+ .unpack();
+
+ match picked {
+ Ok((paid, for_members, reason, who)) => {
+ let bill_item = BillItem {
+ who_paid: who.into(),
+ reason,
+ paid,
+ split: for_members.iter().map(|i| i.as_str().into()).collect(),
+ };
+ let state = StateAddBillItem::new(bill_item);
+ AnyOutput::new(state).route_chain()
+ }
+ Err(e) => e,
+ }
+}
+
+#[chain]
+async fn handle_add_bill_item(prev: StateAddBillItem) -> NextProcess {
+ op_bills(|b| {
+ b.add_item(prev.inner);
+ });
+ Empty::new(()).to_render()
+}
+
+#[derive(Serialize, Groupped)]
+struct StateListBills {
+ optimize: bool,
+}
+
+pack!(ResultBills = Bills);
+pack!(ResultSplitResult = SplitResult);
+pack!(ErrorDuplicateSplitMembers = ());
+pack!(ErrorNegativePaidAmount = ());
+
+#[chain]
+async fn parse_ls_cmd(prev: ListAllBillEntry) -> NextProcess {
+ let optimize = Picker::<()>::new(prev.inner)
+ .pick::<bool>(["-O", "--optimize"])
+ .unpack_directly()
+ .0;
+ let state = StateListBills { optimize };
+ AnyOutput::new(state).route_chain()
+}
+
+#[chain]
+async fn handle_list_bills(prev: StateListBills) -> NextProcess {
+ if prev.optimize {
+ let bills = read_bills();
+ match calculate_from(bills) {
+ Ok(r) => AnyOutput::new(ResultSplitResult::new(r)).route_renderer(),
+ Err(BillSplitError::DuplicateSplitMembers) => {
+ AnyOutput::new(ErrorDuplicateSplitMembers::new(())).route_renderer()
+ }
+ Err(BillSplitError::NegativePaidAmount) => {
+ AnyOutput::new(ErrorNegativePaidAmount::new(())).route_renderer()
+ }
+ }
+ } else {
+ let bills = read_bills();
+ AnyOutput::new(ResultBills::new(bills)).route_renderer()
+ }
+}
+
+#[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)
+}
+
+#[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]);
+ }
+ r_println!("{}", table)
+}
+
+#[renderer]
+fn render_error_duplicate_split_members(_prev: ErrorDuplicateSplitMembers) {
+ r_println!("Error: Duplicate members found in split list");
+}
+
+#[renderer]
+fn render_error_negative_paid_amount(_prev: ErrorNegativePaidAmount) {
+ r_println!("Error: Paid amount cannot be negative");
+}
+
+pack!(Empty = ());
+pack!(PaidRequired = ());
+pack!(ForMembersRequired = ());
+pack!(MemberRequired = ());
+
+#[renderer]
+fn render_empty(_prev: Empty) {}
+
+#[renderer]
+fn render_paid_required(_prev: PaidRequired) {
+ r_println!("Error: Paid amount required, use \"--paid\" or \"-p\"");
+}
+
+#[renderer]
+fn render_for_members_required(_prev: ForMembersRequired) {
+ r_println!("Error: For members required, use \"--for\" or \"-f\"");
+}
+
+#[renderer]
+fn render_member_required(_prev: MemberRequired) {
+ r_println!("Error: Member required");
+}
+
+fn cobill_dir() -> PathBuf {
+ dirs::config_dir().unwrap().join(".cobill")
+}
+
+fn state_file_path() -> PathBuf {
+ cobill_dir().join("state.yml")
+}
+
+fn read_bills() -> Bills {
+ let dir = cobill_dir();
+ create_dir_all(dir).unwrap();
+
+ let state_file = state_file_path();
+ if state_file.exists() {
+ match std::fs::read_to_string(&state_file) {
+ Ok(contents) => match serde_yaml::from_str(&contents) {
+ Ok(bills) => bills,
+ Err(_) => Bills::default(),
+ },
+ Err(_) => Bills::default(),
+ }
+ } else {
+ Bills::default()
+ }
+}
+
+fn op_bills<F: FnOnce(&mut Bills)>(op: F) {
+ let mut bills = read_bills();
+ op(&mut bills);
+ let state_file = state_file_path();
+ let contents = serde_yaml::to_string(&bills).unwrap();
+ std::fs::write(state_file, contents).unwrap();
+}
+
+gen_program!();
diff --git a/src/cli/calc_cmd.rs b/src/cli/calc_cmd.rs
deleted file mode 100644
index 09f4e03..0000000
--- a/src/cli/calc_cmd.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-// use mingling::macros::dispatcher;
-// use mingling::{macros::chain, marker::NextProcess};
-
-// use crate::cli::entry::*;
-
-// dispatcher!("calc", CalculateCommand => CalculateEntry);
-
-// #[chain]
-// pub async fn parse_calc_entry(prev: CalculateEntry) -> NextProcess {}
diff --git a/src/cli/consts.rs b/src/cli/consts.rs
deleted file mode 100644
index 254b611..0000000
--- a/src/cli/consts.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub const BILL_WORKSPACE_CONFIG_FILE: &str = "cobill.yml";
diff --git a/src/cli/dispatchers.rs b/src/cli/dispatchers.rs
deleted file mode 100644
index 27b7747..0000000
--- a/src/cli/dispatchers.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use mingling::{Program, macros::program_setup};
-
-use crate::ThisProgram;
-use crate::cli::ops_cmd::{CreateCommand, InitHereCommand};
-
-#[program_setup]
-pub fn chaos_billing_setup(program: &mut Program<ThisProgram, ThisProgram>) {
- program.with_dispatcher(InitHereCommand);
- program.with_dispatcher(CreateCommand);
-
- // program.with_dispatcher(CalculateCommand);
-}
diff --git a/src/cli/entry.rs b/src/cli/entry.rs
deleted file mode 100644
index e68b7b4..0000000
--- a/src/cli/entry.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use mingling::setup::GeneralRendererSetup;
-
-use crate::__completion_gen::CompletionDispatcher;
-use crate::ThisProgram;
-use crate::cli::dispatchers::*;
-
-pub async fn entry() {
- let mut program = ThisProgram::new();
-
- // Add Completion
- program.with_dispatcher(CompletionDispatcher);
-
- // Add General Renderer
- program.with_setup(GeneralRendererSetup);
-
- // Setup `cobill`
- program.with_setup(ChaosBillingSetup);
-
- // Execute
- program.exec().await;
-}
diff --git a/src/cli/io_error.rs b/src/cli/io_error.rs
deleted file mode 100644
index 49b9939..0000000
--- a/src/cli/io_error.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use mingling::{
- Groupped,
- macros::{r_println, renderer},
-};
-use serde::Serialize;
-
-use crate::ThisProgram;
-
-#[derive(Groupped)]
-pub struct IOError {
- inner: std::io::Error,
-}
-
-impl IOError {
- pub fn new(error: std::io::Error) -> Self {
- Self { inner: error }
- }
-}
-
-impl From<std::io::Error> for IOError {
- fn from(error: std::io::Error) -> Self {
- Self::new(error)
- }
-}
-
-impl Serialize for IOError {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- use serde::ser::SerializeStruct;
- let mut state = serializer.serialize_struct("IOError", 2)?;
- state.serialize_field("kind", &self.inner.kind().to_string())?;
- state.serialize_field("info", &self.inner.to_string())?;
- state.end()
- }
-}
-
-#[renderer]
-pub fn render_io_error(prev: IOError) {
- r_println!("{}: {}", prev.inner.kind(), prev.inner.to_string())
-}
diff --git a/src/cli/ops_cmd.rs b/src/cli/ops_cmd.rs
deleted file mode 100644
index 4b0eea7..0000000
--- a/src/cli/ops_cmd.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use std::{
- env::current_dir,
- fs::{self, create_dir_all},
- path::PathBuf,
-};
-
-use mingling::{
- AnyOutput,
- macros::{chain, dispatcher, pack, r_println, renderer},
- marker::NextProcess,
- parser::Picker,
-};
-
-use crate::{
- ThisProgram,
- cli::{consts::BILL_WORKSPACE_CONFIG_FILE, io_error::IOError},
-};
-
-dispatcher!("init", InitHereCommand => InitEntry);
-dispatcher!("create", CreateCommand => CreateEntry);
-
-pack!(StateCreateWorkspace = PathBuf);
-
-#[chain]
-pub async fn handle_init_command(_prev: InitEntry) -> NextProcess {
- let current_dir = match current_dir() {
- Ok(d) => d,
- Err(e) => return AnyOutput::new(IOError::from(e)).route_renderer(),
- };
- StateCreateWorkspace::new(current_dir).to_chain()
-}
-
-#[chain]
-pub async fn handle_create_command(prev: CreateEntry) -> NextProcess {
- let path = pick_path(prev.inner);
- StateCreateWorkspace::new(path).to_chain()
-}
-
-#[chain]
-pub async fn handle_state_create_workspace(prev: StateCreateWorkspace) -> NextProcess {
- let dir = prev.inner;
- let file = dir.join(BILL_WORKSPACE_CONFIG_FILE);
-
- match create_dir_all(&dir) {
- Ok(d) => d,
- Err(e) => return AnyOutput::new(IOError::from(e)).route_renderer(),
- };
-
- if file.exists() {
- return AnyOutput::new(WorkspaceConfigAlreadyExists::new(dir)).route_renderer();
- }
-
- if let Err(e) = fs::write(file, "") {
- return AnyOutput::new(IOError::from(e)).route_renderer();
- }
-
- StateWorkspaceCreated::new(dir).to_render()
-}
-
-pack!(StateWorkspaceCreated = PathBuf);
-
-#[renderer]
-pub fn render_workspace_created(prev: StateWorkspaceCreated) {
- r_println!("Workspace created at: {:?}", prev.inner);
-}
-
-pack!(WorkspaceConfigAlreadyExists = PathBuf);
-
-#[renderer]
-pub fn render_workspace_config_already_exists(prev: WorkspaceConfigAlreadyExists) {
- r_println!("Workspace config already exists: {:?}", prev.inner);
-}
-
-fn pick_path(args: Vec<String>) -> PathBuf {
- let path = Picker::<()>::new(args)
- .pick::<String>(())
- .unpack_directly()
- .0;
- PathBuf::from(path)
-}
diff --git a/src/display.rs b/src/display.rs
new file mode 100644
index 0000000..3b6f938
--- /dev/null
+++ b/src/display.rs
@@ -0,0 +1,144 @@
+pub struct SimpleTable {
+ items: Vec<String>,
+ line: Vec<Vec<String>>,
+ length: Vec<usize>,
+ padding: usize,
+}
+
+#[allow(unused)]
+impl SimpleTable {
+ /// Create a new Table
+ pub fn new(items: Vec<impl Into<String>>) -> Self {
+ Self::new_with_padding(items, 2)
+ }
+
+ /// Create a new Table with padding
+ pub fn new_with_padding(items: Vec<impl Into<String>>, padding: usize) -> Self {
+ let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
+ let mut length = Vec::with_capacity(items.len());
+
+ for item in &items {
+ length.push(display_width(item));
+ }
+
+ SimpleTable {
+ items,
+ padding,
+ line: Vec::new(),
+ length,
+ }
+ }
+
+ /// Push a new row of items to the table
+ pub fn push_item(&mut self, items: Vec<impl Into<String>>) {
+ let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
+
+ let mut processed_items = Vec::with_capacity(self.items.len());
+
+ for i in 0..self.items.len() {
+ if i < items.len() {
+ processed_items.push(items[i].clone());
+ } else {
+ processed_items.push(String::new());
+ }
+ }
+
+ for (i, d) in processed_items.iter().enumerate() {
+ let d_len = display_width(d);
+ if d_len > self.length[i] {
+ self.length[i] = d_len;
+ }
+ }
+
+ self.line.push(processed_items);
+ }
+
+ /// Insert a new row of items at the specified index
+ pub fn insert_item(&mut self, index: usize, items: Vec<impl Into<String>>) {
+ let items: Vec<String> = items.into_iter().map(|v| v.into()).collect();
+
+ let mut processed_items = Vec::with_capacity(self.items.len());
+
+ for i in 0..self.items.len() {
+ if i < items.len() {
+ processed_items.push(items[i].clone());
+ } else {
+ processed_items.push(String::new());
+ }
+ }
+
+ for (i, d) in processed_items.iter().enumerate() {
+ let d_len = display_width(d);
+ if d_len > self.length[i] {
+ self.length[i] = d_len;
+ }
+ }
+
+ self.line.insert(index, processed_items);
+ }
+
+ /// Get the current maximum column widths
+ fn get_column_widths(&self) -> &[usize] {
+ &self.length
+ }
+}
+
+impl std::fmt::Display for SimpleTable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let column_widths = self.get_column_widths();
+
+ // Build the header row
+ let header: Vec<String> = self
+ .items
+ .iter()
+ .enumerate()
+ .map(|(i, item)| {
+ let target_width = column_widths[i] + self.padding;
+ let current_width = display_width(item);
+ let space_count = target_width - current_width;
+ let space = " ".repeat(space_count);
+ let result = format!("{}{}", item, space);
+ result
+ })
+ .collect();
+ writeln!(f, "{}", header.join(""))?;
+
+ // Build each data row
+ for row in &self.line {
+ let formatted_row: Vec<String> = row
+ .iter()
+ .enumerate()
+ .map(|(i, cell)| {
+ let target_width = column_widths[i] + self.padding;
+ let current_width = display_width(cell);
+ let space_count = target_width - current_width;
+ let spaces = " ".repeat(space_count);
+ let result = format!("{}{}", cell, spaces);
+ result
+ })
+ .collect();
+ writeln!(f, "{}", formatted_row.join(""))?;
+ }
+
+ Ok(())
+ }
+}
+
+pub fn display_width(s: &str) -> usize {
+ // Filter out ANSI escape sequences before calculating width
+ let filtered_bytes = strip_ansi_escapes::strip(s);
+ let filtered_str = match std::str::from_utf8(&filtered_bytes) {
+ Ok(s) => s,
+ Err(_) => s, // Fallback to original string if UTF-8 conversion fails
+ };
+
+ let mut width = 0;
+ for c in filtered_str.chars() {
+ if c.is_ascii() {
+ width += 1;
+ } else {
+ width += 2;
+ }
+ }
+ width
+}
diff --git a/src/macros.rs b/src/macros.rs
new file mode 100644
index 0000000..c4070c3
--- /dev/null
+++ b/src/macros.rs
@@ -0,0 +1,6 @@
+#[macro_export]
+macro_rules! string_vec {
+ ($($elem:expr),* $(,)?) => {
+ vec![$($elem.to_string()),*]
+ };
+}
diff --git a/src/main.rs b/src/main.rs
index a97d952..614693b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,9 @@
-use mingling::macros::gen_program;
-
mod bill;
mod calc;
mod cli;
+mod display;
mod error;
+mod macros;
mod who;
#[cfg(test)]
@@ -11,13 +11,5 @@ mod test;
#[tokio::main]
async fn main() {
- cli::entry::entry().await
+ cli::entry().await
}
-
-use crate::cli::calc_cmd::*;
-use crate::cli::dispatchers::*;
-use crate::cli::entry::*;
-use crate::cli::io_error::*;
-use crate::cli::ops_cmd::*;
-
-gen_program!();
diff --git a/src/who.rs b/src/who.rs
index 9d6e5b5..32f536b 100644
--- a/src/who.rs
+++ b/src/who.rs
@@ -1,4 +1,6 @@
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Who {
name: String,
}