mod app;
mod attach;
mod chats;
mod clipboard;
mod complete;
mod config;
mod demo;
mod images;
mod messages;
mod notify;
mod picker;
mod poll;
mod reactions;
mod search;
mod settings;
mod stickers;
mod text;
mod tg;
mod theme;
mod tmux;
mod ui;
use std::io::{Write, stdout};
use anyhow::Result;
use crossterm::event::{
DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::execute;
use ratatui_image::picker::Picker;
#[tokio::main]
async fn main() -> Result<()> {
config::load_dotenv();
match std::env::args().nth(1).as_deref() {
None => {}
Some("-h" | "--help") => return print(&help()?),
Some("-V" | "--version") => {
return print(&format!("tuigram {}\n", env!("CARGO_PKG_VERSION")));
}
Some("--demo") => return demo::run().await,
Some(other) => anyhow::bail!("unknown argument {other:?}; see tuigram --help"),
}
let config = config::Config::load()?;
let settings_path = settings::path(&config.data_dir);
let settings = settings::Settings::load(&settings_path)?;
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let env_keys = config.api_keys.clone();
let outbox = config.data_dir.join("outbox");
clipboard::clean_outbox(&outbox);
let tg = tg::Tg::start(config, tx).await?;
let mut terminal = ratatui::init();
notify::send(notify::SAVE_TITLE);
notify::send(¬ify::title(0));
std::panic::set_hook(Box::new(|info| {
if images::panic_is_contained() {
return;
}
let _ = execute!(
stdout(),
PopKeyboardEnhancementFlags,
DisableBracketedPaste,
DisableFocusChange
);
notify::send(notify::RESTORE_TITLE);
ratatui::restore();
tmux::restore();
eprintln!("tuigram crashed: {}", text::clean(&info.to_string()));
tg::offline_now();
std::process::exit(101);
}));
tmux::save();
let picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
execute!(stdout(), EnableBracketedPaste)?;
execute!(stdout(), EnableFocusChange)?;
let enhanced = crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
if enhanced {
execute!(
stdout(),
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
)?;
}
let (image_tx, image_rx) = tokio::sync::mpsc::unbounded_channel();
let images = images::Images::new(picker, image_tx);
let (clipboard_tx, clipboard_rx) = tokio::sync::mpsc::unbounded_channel();
let clipboard = clipboard::Clipboard::new(clipboard_tx, outbox);
let result = app::App::new(tg, images, clipboard, settings, settings_path, env_keys)
.run(&mut terminal, rx, image_rx, clipboard_rx)
.await;
if enhanced {
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
}
let _ = execute!(stdout(), DisableBracketedPaste, DisableFocusChange);
notify::send(notify::RESTORE_TITLE);
ratatui::restore();
tmux::restore();
result
}
fn print(text: &str) -> Result<()> {
match stdout().lock().write_all(text.as_bytes()) {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
result => Ok(result?),
}
}
fn help() -> Result<String> {
let keys = if config::built_in_keys().is_some() {
"This build comes with tuigram's own API key, so you only need to log in.
To use your own key instead, set TG_API_ID and TG_API_HASH."
} else {
"This build has no API key: on first run, tuigram asks for your Telegram
API ID and hash, which you get once at https://my.telegram.org (API
development tools). Or install the ready-made app, which has one:
cargo binstall tuigram-cli"
};
Ok(format!(
"tuigram {version}
Telegram in your terminal, with vim-style keys.
Usage: tuigram [-h | --help] [-V | --version] [--demo]
Inside the app, the status bar lists the keys for wherever you are;
press ? for settings and q to quit.
--demo shows made-up chats without logging in or touching your session:
1-5 or Tab switch scenes, t changes the theme, q quits.
{keys}
Your login session, API credentials, downloaded files and settings are
kept in this folder, and your own color themes in its themes folder:
{data}
Environment:
TG_DATA_DIR keep them somewhere else
TG_API_ID your own API key, used before saved or built-in ones
TG_API_HASH
",
version = env!("CARGO_PKG_VERSION"),
data = config::shown(&config::data_dir()?),
))
}