1use std::{io::Read, path::PathBuf};
2
3use anyhow::{Context, Result};
4use clap::Parser;
5
6use crate::{
7 auth::load_auth,
8 client::ThingsCloudClient,
9 commands::{Command, Commands},
10 dirs::append_log_dir,
11 log_cache::{fold_state_from_append_log, get_state_with_append_log},
12 logging,
13 store::{RawState, ThingsStore, fold_items},
14 wire::wire_object::WireItem,
15};
16
17#[derive(Debug, Parser)]
18#[command(name = "things3")]
19#[command(bin_name = "things3")]
20#[command(version)]
21#[command(before_help = concat!("things3 ", env!("CARGO_PKG_VERSION")))]
22#[command(disable_help_subcommand = true)]
23#[command(about = concat!(
24 "things3 v",
25 env!("CARGO_PKG_VERSION"),
26 ": Command-line interface for Things 3 via Cloud API"
27))]
28pub struct Cli {
29 #[arg(long)]
31 pub no_color: bool,
32 #[arg(long, global = true)]
34 pub json: bool,
35 #[arg(long)]
37 pub no_sync: bool,
38 #[arg(long, hide = true)]
40 pub no_cloud: bool,
41 #[arg(long, value_enum, default_value_t = logging::Level::Info)]
43 pub log_level: logging::Level,
44 #[arg(long, value_enum, default_value_t = logging::LogFormat::Auto)]
46 pub log_format: logging::LogFormat,
47 #[arg(long, global = true, hide = true, value_name = "DIRECTIVE")]
49 pub log_filter: Option<String>,
50 #[arg(long, global = true, hide = true, value_name = "TIMESTAMP")]
52 pub today_ts: Option<i64>,
53 #[arg(long, global = true, hide = true, value_name = "TIMESTAMP")]
55 pub now_ts: Option<f64>,
56 #[arg(long, value_name = "FILE", hide = true)]
60 pub load_journal: Option<PathBuf>,
61 #[command(subcommand)]
62 pub command: Option<Commands>,
63}
64
65impl Cli {
66 pub fn load_state(&self) -> Result<RawState> {
67 if let Some(journal_path) = &self.load_journal {
68 let raw = if journal_path == std::path::Path::new("-") {
69 let mut buf = String::new();
70 std::io::stdin()
71 .read_to_string(&mut buf)
72 .with_context(|| "failed to read journal JSON from stdin")?;
73 buf
74 } else {
75 std::fs::read_to_string(journal_path).with_context(|| {
76 format!("failed to read journal file {}", journal_path.display())
77 })?
78 };
79 let items: Vec<WireItem> =
80 serde_json::from_str(&raw).with_context(|| "failed to parse journal JSON")?;
81 return Ok(fold_items(items));
82 }
83
84 if self.no_sync || self.no_cloud {
85 let cache_dir = append_log_dir();
86 return fold_state_from_append_log(&cache_dir);
87 }
88
89 let (email, password) = load_auth()?;
90 let mut client = ThingsCloudClient::new(email, password)?;
91 let cache_dir = append_log_dir();
92 get_state_with_append_log(&mut client, cache_dir)
93 }
94
95 pub fn load_store(&self) -> Result<ThingsStore> {
96 let state = self.load_state()?;
97 Ok(ThingsStore::from_raw_state(&state))
98 }
99}
100
101pub fn run() -> Result<()> {
102 let mut cli = Cli::parse();
103 logging::init(cli.log_level, cli.log_format, cli.log_filter.as_deref());
104 let command = cli
105 .command
106 .take()
107 .unwrap_or(Commands::Today(Default::default()));
108 command.run(&cli, &mut std::io::stdout())
109}