use std::io::{Read, Stdout, stdout};
use anyhow::{Context, Result};
use clap::Parser;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use tokio::sync::mpsc;
use tazuna::session::{SessionEvent, SessionId};
use tazuna::tui::TuiApp;
use tazuna::{
Cli, Commands, Config, HookEvent, HooksClient, HooksServer, NativeGitHubClient, NativePty,
NativeWorktreeManager, SessionManager, current_socket_path,
};
struct TerminalGuard {
stdout: Stdout,
}
impl TerminalGuard {
fn new() -> Result<Self> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
Ok(Self { stdout })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(self.stdout, LeaveAlternateScreen, DisableMouseCapture);
}
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Notify) => run_notify().await,
None => run_tui().await,
}
}
async fn run_notify() -> Result<()> {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("failed to read from stdin")?;
let payload: serde_json::Value =
serde_json::from_str(&input).context("failed to parse JSON from stdin")?;
let session_id_str = std::env::var("TAZUNA_SESSION_ID")
.context("TAZUNA_SESSION_ID environment variable not set")?;
let session_id: SessionId = session_id_str
.parse()
.context("failed to parse TAZUNA_SESSION_ID")?;
let event =
HookEvent::from_payload(session_id, payload).context("failed to create hook event")?;
let client = HooksClient::from_env().context("failed to create hooks client")?;
client
.send(&event)
.await
.context("failed to send hook event")?;
Ok(())
}
async fn run_tui() -> Result<()> {
let config = Config::load()?;
let _log_guard = tazuna::logging::init_file_logging(&config.log_directory())?;
let (event_tx, event_rx) = mpsc::channel(256);
let (cmd_tx, cmd_rx) = mpsc::channel(64);
let (hook_tx, mut hook_rx) = mpsc::channel(64);
let socket_path = current_socket_path();
let hooks_server =
HooksServer::new(&socket_path, hook_tx).context("failed to create hooks server")?;
tokio::spawn(hooks_server.run());
let event_tx_clone = event_tx.clone();
tokio::spawn(async move {
while let Some(event) = hook_rx.recv().await {
let _ = event_tx_clone
.send(SessionEvent::HookReceived { event })
.await;
}
});
let pty = NativePty::new();
let worktree = NativeWorktreeManager::discover(config.worktree.clone()).ok();
let mut manager = SessionManager::new(
config.session.max_sessions,
pty,
worktree,
config.worktree.auto_cleanup,
config.worktree.pull_strategy,
event_tx,
NativeGitHubClient,
);
manager.set_socket_path(socket_path);
tokio::spawn(manager.run(cmd_rx));
let _guard = TerminalGuard::new()?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
let size = terminal.size()?;
let mut app = TuiApp::new(
cmd_tx.clone(),
config.notification.clone(),
config.claude.default_args.clone(),
);
app.set_size(size.height.saturating_sub(2), size.width);
let _ = cmd_tx
.send(tazuna::session::SessionCommand::FetchCostAsync)
.await;
let cmd_tx_cost = cmd_tx.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
interval.tick().await; loop {
interval.tick().await;
if cmd_tx_cost
.send(tazuna::session::SessionCommand::FetchCostAsync)
.await
.is_err()
{
break;
}
}
});
let result = app.run(event_rx, &mut terminal).await;
terminal.show_cursor()?;
result
}