A Rust CLI for publishing thought records. Designed to work with thought.stream.
at main 4.5 kB view raw
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}