From b2882ae0b0e1521edd82bdb74b59dc2733f78a15 Mon Sep 17 00:00:00 2001 From: Vidhu Kant Sharma Date: Mon, 8 Sep 2025 22:34:26 +0530 Subject: first commit --- src/client/command.rs | 37 ++++++++++++ src/client/listener.rs | 73 ++++++++++++++++++++++ src/client/mod.rs | 19 ++++++ src/daemon/clock.rs | 82 +++++++++++++++++++++++++ src/daemon/command.rs | 71 ++++++++++++++++++++++ src/daemon/mod.rs | 32 ++++++++++ src/daemon/pomo.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 36 +++++++++++ 8 files changed, 510 insertions(+) create mode 100644 src/client/command.rs create mode 100644 src/client/listener.rs create mode 100644 src/client/mod.rs create mode 100644 src/daemon/clock.rs create mode 100644 src/daemon/command.rs create mode 100644 src/daemon/mod.rs create mode 100644 src/daemon/pomo.rs create mode 100644 src/main.rs (limited to 'src') diff --git a/src/client/command.rs b/src/client/command.rs new file mode 100644 index 0000000..030d33a --- /dev/null +++ b/src/client/command.rs @@ -0,0 +1,37 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::os::unix::net::UnixStream; +use std::io::Write; + +pub fn handle_command(c: String) { + // check if the sock exists or not + let sock = "/tmp/polydorocmd.sock"; + if !std::fs::metadata(sock).is_ok() { + eprintln!("Could not connect to socket {}, is the daemon running?", sock); + std::process::exit(1); + } + + let mut stream = UnixStream::connect(sock) + .unwrap_or_else(|_| panic!("could not connect to daemon at {}", sock)); + + if let Err(e) = stream.write_all(format!("{}\n", c).as_bytes()) { + eprintln!("error: {}", e); + std::process::exit(1); + } + let _ = stream.flush(); +} diff --git a/src/client/listener.rs b/src/client/listener.rs new file mode 100644 index 0000000..2e223f6 --- /dev/null +++ b/src/client/listener.rs @@ -0,0 +1,73 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::os::unix::net::UnixStream; +use std::io::{BufReader}; + +use bincode::{decode_from_std_read, config}; +use crate::daemon::pomo::{Pomo,State}; + +pub fn start_listener() { + let sock_path = "/tmp/polydorod.sock"; + let stream = UnixStream::connect(sock_path).unwrap_or_else(|_| panic!("could not connect to daemon")); + let mut reader = BufReader::new(stream); + + loop { + // blocks until a struct is received + match decode_from_std_read::(&mut reader, config::standard()) { + Ok(p) => { + pretty_print(p); + } + Err(e) => { + eprintln!("polydoro listener error: {}", e); + std::process::exit(1); + } + } + } +} + +pub fn format_time(secs: u64) -> String { + format!("{:02}:{:02}", secs / 60, secs % 60) +} + +pub fn pretty_print(p: Pomo) { + let format_work = "work"; + let format_work_paused = "[work]"; + let format_work_idle = "(work)"; + let format_break = "break"; + let format_break_paused = "[break]"; + let format_break_idle = "(break)"; + let format_long_break = "long break"; + let format_long_break_paused = "[long break]"; + let format_long_break_idle = "(long break)"; + + let f = match p.current_state { + State::Work => format_work, + State::Break => format_break, + State::LongBreak => format_long_break, + + State::WorkPaused => format_work_paused, + State::BreakPaused => format_break_paused, + State::LongBreakPaused => format_long_break_paused, + + State::WorkIdle => format_work_idle, + State::BreakIdle => format_break_idle, + State::LongBreakIdle => format_long_break_idle, + }; + + println!("{} {} - {}", f, p.counter, format_time(p.secs_elapsed)); +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..b86d042 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,19 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pub mod command; +pub mod listener; diff --git a/src/daemon/clock.rs b/src/daemon/clock.rs new file mode 100644 index 0000000..98ad292 --- /dev/null +++ b/src/daemon/clock.rs @@ -0,0 +1,82 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::sync::mpsc::Receiver; +use std::thread; +use std::time::Duration; +use std::os::unix::net::{UnixStream,UnixListener}; +use bincode::{encode_into_std_write,config}; + +use crate::daemon::pomo::Pomo; +use crate::daemon::command::Command; + +pub fn start_clock(rx: Receiver) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut p = Pomo::new(); + + // check if a socket already exists (only one daemon allowed!) + let sock = "/tmp/polydorod.sock"; + if std::fs::metadata(sock).is_ok() { + eprintln!("socket {} already exists, is another daemon running?", sock); + std::process::exit(1); + } + + // all OK, create a sock + let listener = UnixListener::bind(sock).expect("failed to bind socket /tmp/polydorod.sock"); + listener.set_nonblocking(true).unwrap(); + + // all of the connected listeners + let mut listeners: Vec = Vec::new(); + + loop { + // if new listener is connected + match listener.accept() { + Ok((stream, _addr)) => { + listeners.push(stream); + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {} // no new client + Err(e) => eprintln!("error while adding new listener: {}", e), + } + + // listen to commands + while let Ok(cmd) = rx.try_recv() { + match cmd { + Command::Run => p.run(), + Command::Pause => p.pause(), + Command::Toggle => p.toggle(), + Command::Skip => p.end_cycle(), + Command::SoftReset => p.soft_reset(), + Command::HardReset => p.hard_reset(), + Command::Reset => p.reset(), + } + } + + p.tick(); + + // send p struct to the listeners listening to polydorod.sock + listeners.retain_mut(|l| { + if let Err(_) = encode_into_std_write(&p, l, config::standard()) { + false // drop this listener, seems to be disconnected + } else { + true + } + }); + + thread::sleep(Duration::from_secs(1)); + } + }) +} diff --git a/src/daemon/command.rs b/src/daemon/command.rs new file mode 100644 index 0000000..18ee41d --- /dev/null +++ b/src/daemon/command.rs @@ -0,0 +1,71 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::os::unix::net::UnixListener; +use std::io::{BufRead, BufReader}; +use std::thread; +use std::sync::mpsc::Sender; + +pub enum Command { + Run, + Pause, + Toggle, // conditional run/pause + Skip, + SoftReset, + HardReset, + Reset, // conditional soft/hard reset +} + +pub fn start_command_listener(tx: Sender) -> thread::JoinHandle<()> { + thread::spawn(move || { + // check if a socket already exists (only one daemon allowed!) + let sock = "/tmp/polydorocmd.sock"; + if std::fs::metadata(sock).is_ok() { + eprintln!("socket {} already exists, is another daemon running?", sock); + std::process::exit(1); + } + + // all OK, create a sock + let listener = UnixListener::bind(sock).expect("failed to bind socket /tmp/polydorocmd.sock"); + + for stream in listener.incoming() { + match stream { + Ok(s) => { + let mut reader = BufReader::new(s); + let mut buf = String::new(); + + if let Ok(_) = reader.read_line(&mut buf) { + let msg = match buf.trim() { + "run" => Command::Run, + "pause" => Command::Pause, + "toggle" => Command::Toggle, + "skip" => Command::Skip, + "soft-reset" => Command::SoftReset, + "hard-reset" => Command::HardReset, + "reset" => Command::Reset, + &_ => Command::Run, // doesn't matter, pretty sure it won't ever hit this case + }; + + let _ = tx.send(msg).unwrap(); + } + }, + Err(_) => {} + } + } + + }) +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 0000000..9b40822 --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,32 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::sync::mpsc; + +mod clock; +pub mod command; +pub mod pomo; + +pub fn start_daemon() { + let (tx, rx) = mpsc::channel::(); + + let _clock = clock::start_clock(rx); + let _command_listener = command::start_command_listener(tx); + + _clock.join().unwrap(); + _command_listener.join().unwrap(); +} diff --git a/src/daemon/pomo.rs b/src/daemon/pomo.rs new file mode 100644 index 0000000..3129e7d --- /dev/null +++ b/src/daemon/pomo.rs @@ -0,0 +1,160 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use bincode::{Encode, Decode}; + +#[derive(Encode, Decode, PartialEq, Clone, Copy)] +pub enum State { + WorkIdle, + BreakIdle, + LongBreakIdle, + Work, + Break, + LongBreak, + WorkPaused, + BreakPaused, + LongBreakPaused, +} + +#[derive(Encode, Decode)] +pub struct Pomo { + work_duration: u64, + break_duration: u64, + long_break_duration: u64, + long_break_interval: u64, + + // only these are supposed to change + pub secs_elapsed: u64, + pub counter: u64, + pub current_state: State, +} + +impl Pomo { + pub fn new() -> Self { + Pomo { + secs_elapsed: 0, + counter: 1, + current_state: State::WorkIdle, + + // TODO: load from config, probably + work_duration: 25,// * 60, + break_duration: 5,// * 60, + long_break_duration: 20,// * 60, + long_break_interval: 4, + } + } + + pub fn tick(&mut self) { + // only tick on running states + if matches!(self.current_state, State::Work | State::Break | State::LongBreak) { + self.secs_elapsed += 1; + + let duration = match self.current_state { + State::Work => self.work_duration, + State::Break => self.break_duration, + State::LongBreak => self.long_break_duration, + _ => 0, // will never hit this case + }; + + if self.secs_elapsed >= duration { + self.end_cycle(); + } + } + } + + // start/unpause a cycle + pub fn run(&mut self) { + self.current_state = match self.current_state { + State::WorkIdle => State::Work, + State::BreakIdle => State::Break, + State::LongBreakIdle => State::LongBreak, + + State::WorkPaused => State::Work, + State::BreakPaused => State::Break, + State::LongBreakPaused => State::LongBreak, + + _ => return // already running + }; + } + + pub fn pause(&mut self) { + self.current_state = match self.current_state { + State::Work => State::WorkPaused, + State::Break => State::BreakPaused, + State::LongBreak => State::LongBreakPaused, + _ => return, // idle and paused don't have a place here + }; + } + + // conditionally pause/run + pub fn toggle(&mut self) { + match self.current_state { + State::Work | State::Break | State::LongBreak => self.pause(), + _ => self.run(), + } + } + + // also used in the skip command + pub fn end_cycle(&mut self) { + match self.current_state { + State::Work => { + self.secs_elapsed = 0; + + self.current_state = if self.counter == self.long_break_interval { + State::LongBreakIdle + } else { + State::BreakIdle + }; + }, + State::Break => { + self.secs_elapsed = 0; + self.current_state = State::WorkIdle; + + // because we move on to the next task after the break + self.counter += 1; + } + State::LongBreak => self.hard_reset(), + _ => return, // paused and idle state can't be ended + } + } + + // reset current cycle + pub fn soft_reset(&mut self) { + self.secs_elapsed = 0; + self.current_state = match self.current_state { + State::Work | State::WorkPaused => State::WorkIdle, + State::Break | State::BreakPaused => State::BreakIdle, + State::LongBreak | State::LongBreakPaused => State::LongBreakIdle, + _ => self.current_state, // if already in an idle state, don't change (probably won't hit anyways) + }; + } + + // reset all cycles + pub fn hard_reset(&mut self) { + self.secs_elapsed = 0; + self.counter = 1; + self.current_state = State::WorkIdle; + } + + // conditionally do a soft or a hard reset + pub fn reset(&mut self) { + match self.current_state { + State::WorkIdle | State::BreakIdle | State::LongBreakIdle => self.hard_reset(), + _ => self.soft_reset(), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b66df77 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,36 @@ +/* Polydoro - Pomodoro widget for polybar and friends + * Copyright (C) 2025 Vidhu Kant Sharma + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use std::env; + +mod client; +mod daemon; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() == 2 { + match args[1].as_str() { + "run" | "pause" | "toggle" | "skip" | "soft-reset" | "hard-reset" | "reset" => client::command::handle_command(args[1].clone()), + "-d" => daemon::start_daemon(), + "-l" => client::listener::start_listener(), + "-h" | _ => eprintln!(""), + } + } else { + eprintln!(""); + } +} -- cgit v1.2.3