use argh::FromArgs;
use dirs;
use inquire::Confirm;
use pomo_cli::controller::Controller;
use pomo_cli::timer::{Timer, TimerEvent, TimerType};
use std::io::prelude::*;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::task;
const SOCKET_PATH: &str = "/tmp/pomo.sock";
const HOOKS_PATH: &str = ".config/pomo/hooks";
#[derive(FromArgs)]
struct Args {
#[argh(subcommand)]
subcommand: SubCommands,
}
#[derive(FromArgs)]
#[argh(subcommand)]
enum SubCommands {
Start(Start),
Pause(Pause),
Resume(Resume),
Stop(Stop),
Status(Status),
Next(Next),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "start")]
struct Start {
#[argh(switch, short = 'a')]
auto: bool,
#[argh(option, short = 'd', default = "25")]
duration: u64,
#[argh(option, short = 'b', default = "5")]
break_duration: u64,
#[argh(option, default = "4")]
long_break_interval: u64,
#[argh(option, default = "15")]
long_break_duration: u64,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "pause")]
struct Pause {}
#[derive(FromArgs)]
#[argh(subcommand, name = "resume")]
struct Resume {}
#[derive(FromArgs)]
#[argh(subcommand, name = "stop")]
struct Stop {}
#[derive(FromArgs)]
#[argh(subcommand, name = "status")]
struct Status {}
#[derive(FromArgs)]
#[argh(subcommand, name = "next")]
struct Next {}
#[tokio::main]
async fn main() {
let args: Args = argh::from_env();
match args.subcommand {
SubCommands::Start(args) => {
start(args).await;
}
SubCommands::Pause(_) => pause(),
SubCommands::Resume(_) => resume(),
SubCommands::Stop(_) => stop(),
SubCommands::Status(_) => status(),
SubCommands::Next(_) => next(),
};
}
async fn start(args: Start) {
if std::path::Path::new(SOCKET_PATH).exists() {
let answer = inquire::Confirm::new("Pomo is already running or was not terminated properly. Do you want to start a new pomodoro?").with_default(false).prompt();
match answer {
Ok(true) => {
if let Ok(mut stream) = UnixStream::connect(SOCKET_PATH) {
stream
.write_all(b"abort")
.expect("Failed to write to socket");
stream
.shutdown(std::net::Shutdown::Write)
.expect("Failed to shutdown socket");
}
cleanup();
}
Ok(false) => {
std::process::exit(exitcode::OK);
}
_ => std::process::exit(exitcode::USAGE),
}
}
ctrlc::set_handler(move || {
cleanup();
std::process::exit(exitcode::OK);
})
.expect("Error setting Ctrl-C handler");
let Start {
auto,
break_duration,
duration,
long_break_duration,
long_break_interval,
} = args;
let duration = Duration::from_secs(duration * 60);
let break_duration = Duration::from_secs(break_duration * 60);
let listener = UnixListener::bind(SOCKET_PATH).expect("Failed to bind to socket");
let controller_config = pomo_cli::controller::Config {
auto,
break_duration,
long_break_duration: Duration::from_secs(long_break_duration),
long_break_interval,
work_duration: duration,
};
let controller = Controller::new(controller_config);
let on_timer_finished = move |timer: &Timer| {
run_hook("finish.sh", timer.timer_type());
if auto {
return;
}
task::spawn_blocking(move || {
if let Ok(true) = Confirm::new("Start the next timer?")
.with_default(true)
.prompt()
{
write_socket_message("next");
}
});
};
Controller::on(&controller, TimerEvent::Start, Arc::new(on_timer_started));
Controller::on(&controller, TimerEvent::Finish, Arc::new(on_timer_finished));
Controller::on(&controller, TimerEvent::Pause, Arc::new(on_timer_paused));
Controller::start(&controller);
let handle = tokio::task::spawn_blocking(move || {
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let mut incoming_string = String::new();
stream
.read_to_string(&mut incoming_string)
.expect("Failed to read from socket");
match incoming_string.as_str() {
"abort" => {
return;
}
"pause" => {
Controller::pause(&controller);
}
"resume" => {
Controller::start(&controller);
}
"stop" => {
Controller::stop(&controller);
cleanup();
return;
}
"next" => {
Controller::next(&controller);
}
"status" => {
let timer = Controller::get_current_timer(&controller);
let timer = timer.lock().expect("Failed to lock timer");
let time_left = timer.time_left();
let prefix = match timer.timer_type() {
TimerType::Work => "W",
TimerType::Break => "B",
};
let minutes = time_left.as_secs() / 60;
let seconds = time_left.as_secs() % 60;
stream
.write_all(
format!("{} {:02}:{:02}", prefix, minutes, seconds).as_bytes(),
)
.expect("Failed to write to socket");
}
_ => {}
}
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
});
handle.await.expect("Failed to run socket listener");
}
fn on_timer_started(timer: &Timer) {
run_hook("start.sh", timer.timer_type());
}
fn on_timer_paused(timer: &Timer) {
run_hook("pause.sh", timer.timer_type());
}
fn run_hook(hook_name: &str, timer_type: TimerType) {
let mut path = dirs::home_dir().expect("Failed to get home directory");
path.push(HOOKS_PATH);
path.push(Path::new(hook_name));
let _ = std::process::Command::new(path)
.env("TIMER_TYPE", timer_type.to_string())
.spawn();
}
fn cleanup() {
std::fs::remove_file(SOCKET_PATH).unwrap_or(());
}
fn write_socket_message(message: &str) -> UnixStream {
let mut stream = match UnixStream::connect(SOCKET_PATH) {
Ok(stream) => stream,
Err(_) => {
println!("Failed to connect to socket. Please start a timer using 'pomo start' first.");
std::process::exit(exitcode::SOFTWARE);
}
};
stream
.write_all(message.as_bytes())
.expect("Failed to write to socket");
stream
.shutdown(std::net::Shutdown::Write)
.expect("Failed to shutdown socket");
stream
}
fn pause() {
write_socket_message("pause");
}
fn resume() {
write_socket_message("resume");
}
fn stop() {
write_socket_message("stop");
}
fn status() {
let mut stream = write_socket_message("status");
let mut incoming_string = String::new();
stream
.read_to_string(&mut incoming_string)
.expect("Failed to read from socket");
}
fn next() {
write_socket_message("next");
}