A Rust CLI for publishing thought records. Designed to work with thought.stream.
1mod client;
2mod credentials;
3mod interactive;
4mod jetstream;
5mod tui;
6
7use anyhow::{Context, Result};
8use clap::{Parser, Subcommand};
9use std::io::{self, Write};
10
11use client::AtProtoClient;
12use credentials::{CredentialStore, Credentials};
13
14#[derive(Parser)]
15#[command(name = "thought")]
16#[command(about = "CLI tool for publishing stream.thought.blip records")]
17#[command(version = "0.1.0")]
18struct Cli {
19 #[command(subcommand)]
20 command: Option<Commands>,
21
22 /// Message to publish (if no subcommand is provided)
23 message: Option<String>,
24}
25
26#[derive(Subcommand)]
27enum Commands {
28 /// Login to Bluesky with credentials
29 Login,
30 /// Logout and clear stored credentials
31 Logout,
32 /// Enter interactive mode for rapid posting
33 Interactive {
34 /// Use TUI interface with live message feed
35 #[arg(long)]
36 tui: bool,
37 },
38 /// Enter the thought stream (TUI mode with live feed)
39 Stream,
40}
41
42fn prompt_for_input(prompt: &str) -> Result<String> {
43 print!("{}", prompt);
44 io::stdout().flush().context("Failed to flush stdout")?;
45
46 let mut input = String::new();
47 io::stdin()
48 .read_line(&mut input)
49 .context("Failed to read input")?;
50
51 Ok(input.trim().to_string())
52}
53
54async fn handle_login() -> Result<()> {
55 let store = CredentialStore::new()?;
56
57 if store.exists() {
58 println!("You are already logged in. Use 'thought logout' to clear credentials first.");
59 return Ok(());
60 }
61
62 println!("Login to Bluesky");
63 println!("================");
64
65 let username = prompt_for_input("Username (handle or email): ")?;
66 if username.is_empty() {
67 anyhow::bail!("Username cannot be empty");
68 }
69
70 let password = rpassword::prompt_password("App Password: ")
71 .context("Failed to read password")?;
72 if password.is_empty() {
73 anyhow::bail!("Password cannot be empty");
74 }
75
76 let pds_uri = prompt_for_input("PDS URI (press Enter for https://bsky.social): ")?;
77 let pds_uri = if pds_uri.is_empty() {
78 "https://bsky.social".to_string()
79 } else {
80 pds_uri
81 };
82
83 // Test the credentials by attempting to authenticate
84 println!("Testing credentials...");
85 let credentials = Credentials::new(username, password, pds_uri);
86 let mut client = AtProtoClient::new(&credentials.pds_uri);
87
88 client.login(&credentials).await
89 .context("Login failed. Please check your credentials.")?;
90
91 // Store credentials if login was successful
92 store.store(&credentials)?;
93
94 Ok(())
95}
96
97async fn handle_logout() -> Result<()> {
98 let store = CredentialStore::new()?;
99 store.clear()?;
100 Ok(())
101}
102
103async fn handle_publish(message: &str) -> Result<()> {
104 let store = CredentialStore::new()?;
105
106 let credentials = store.load()?
107 .context("Not logged in. Please run 'thought login' first.")?;
108
109 let mut client = AtProtoClient::new(&credentials.pds_uri);
110 client.login(&credentials).await?;
111
112 let uri = client.publish_blip(message).await?;
113 println!("Published: {}", uri);
114
115 Ok(())
116}
117
118async fn handle_interactive(use_tui: bool) -> Result<()> {
119 let store = CredentialStore::new()?;
120
121 let credentials = store.load()?
122 .context("Not logged in. Please run 'thought login' first.")?;
123
124 let mut client = AtProtoClient::new(&credentials.pds_uri);
125 client.login(&credentials).await?;
126
127 interactive::run_interactive_mode(&client, use_tui).await?;
128
129 Ok(())
130}
131
132#[tokio::main]
133async fn main() -> Result<()> {
134 let cli = Cli::parse();
135
136 match cli.command {
137 Some(Commands::Login) => handle_login().await?,
138 Some(Commands::Logout) => handle_logout().await?,
139 Some(Commands::Interactive { tui }) => handle_interactive(tui).await?,
140 Some(Commands::Stream) => handle_interactive(true).await?,
141 None => {
142 if let Some(message) = cli.message {
143 handle_publish(&message).await?;
144 } else {
145 println!("Usage:");
146 println!(" thought login # Login to Bluesky");
147 println!(" thought logout # Logout and clear credentials");
148 println!(" thought interactive # Enter simple REPL mode");
149 println!(" thought stream # Enter TUI mode with live feed");
150 println!(" thought \"message\" # Publish a blip");
151 }
152 }
153 }
154
155 Ok(())
156}