Skip to main content

things3_cloud/
app.rs

1use crate::commands::{Command, Commands};
2use crate::dirs::append_log_dir;
3use crate::log_cache::{fold_state_from_append_log, get_state_with_append_log};
4use crate::logging;
5use crate::store::{fold_items, RawState, ThingsStore};
6use crate::wire::wire_object::WireItem;
7use crate::{auth::load_auth, client::ThingsCloudClient};
8use anyhow::{Context, Result};
9use clap::Parser;
10use std::io::Read;
11use std::path::PathBuf;
12
13#[derive(Debug, Parser)]
14#[command(name = "things3")]
15#[command(bin_name = "things3")]
16#[command(version)]
17#[command(before_help = concat!("things3 ", env!("CARGO_PKG_VERSION")))]
18#[command(disable_help_subcommand = true)]
19#[command(about = concat!(
20    "things3 v",
21    env!("CARGO_PKG_VERSION"),
22    ": Command-line interface for Things 3 via Cloud API"
23))]
24pub struct Cli {
25    /// Disable color output
26    #[arg(long)]
27    pub no_color: bool,
28    /// Skip cloud sync and use local cache only
29    #[arg(long)]
30    pub no_sync: bool,
31    /// For testing: disable cloud sync and cloud writes
32    #[arg(long, hide = true)]
33    pub no_cloud: bool,
34    /// Set the log level filter
35    #[arg(long, global = true, value_enum, default_value_t = logging::Level::Info)]
36    pub log_level: logging::Level,
37    /// Set the logging output format
38    #[arg(long, global = true, value_enum, default_value_t = logging::LogFormat::Auto)]
39    pub log_format: logging::LogFormat,
40    /// For testing: advanced tracing filter directive
41    #[arg(long, global = true, hide = true, value_name = "DIRECTIVE")]
42    pub log_filter: Option<String>,
43    /// For testing: override "today" UTC midnight timestamp
44    #[arg(long, global = true, hide = true, value_name = "TIMESTAMP")]
45    pub today_ts: Option<i64>,
46    /// For testing: override current UNIX timestamp
47    #[arg(long, global = true, hide = true, value_name = "TIMESTAMP")]
48    pub now_ts: Option<f64>,
49    /// For testing: load state from a JSON journal file instead of syncing.
50    /// The file must contain a JSON array of WireItem objects (each is a
51    /// map of uuid -> WireObject).
52    #[arg(long, value_name = "FILE", hide = true)]
53    pub load_journal: Option<PathBuf>,
54    #[command(subcommand)]
55    pub command: Option<Commands>,
56}
57
58impl Cli {
59    pub fn load_state(&self) -> Result<RawState> {
60        if let Some(journal_path) = &self.load_journal {
61            let raw = if journal_path == std::path::Path::new("-") {
62                let mut buf = String::new();
63                std::io::stdin()
64                    .read_to_string(&mut buf)
65                    .with_context(|| "failed to read journal JSON from stdin")?;
66                buf
67            } else {
68                std::fs::read_to_string(journal_path).with_context(|| {
69                    format!("failed to read journal file {}", journal_path.display())
70                })?
71            };
72            let items: Vec<WireItem> =
73                serde_json::from_str(&raw).with_context(|| "failed to parse journal JSON")?;
74            return Ok(fold_items(items));
75        }
76
77        if self.no_sync || self.no_cloud {
78            let cache_dir = append_log_dir();
79            return fold_state_from_append_log(&cache_dir);
80        }
81
82        let (email, password) = load_auth()?;
83        let mut client = ThingsCloudClient::new(email, password)?;
84        let cache_dir = append_log_dir();
85        get_state_with_append_log(&mut client, cache_dir)
86    }
87
88    pub fn load_store(&self) -> Result<ThingsStore> {
89        let state = self.load_state()?;
90        Ok(ThingsStore::from_raw_state(&state))
91    }
92}
93
94pub fn run() -> Result<()> {
95    let mut cli = Cli::parse();
96    logging::init(cli.log_level, cli.log_format, cli.log_filter.as_deref());
97    let command = cli
98        .command
99        .take()
100        .unwrap_or(Commands::Today(Default::default()));
101    command.run(&cli, &mut std::io::stdout())
102}