Skip to content

Commit ce6a834

Browse files
committed
support multiple monitors
1 parent 285d5b0 commit ce6a834

File tree

2 files changed

+131
-59
lines changed

2 files changed

+131
-59
lines changed

README.md

+16-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
> This used to be a shell script. Now it is a binary.
55
> The CLI arguments have changed only slightly but the underlying architecture is completely different.
66
> Therefore, if you switch from the shell script version to the binary, please make sure to **fully adapt the new default config**.
7-
> In particular you need to add `tail` for `exec`, remove `interval`, set `exec-on-event` to false, and change `increase -60` to `decrease 60`.
7+
> In particular, you need to add `hook` for `exec`, remove `interval`, set `exec-on-event` to false, and change `increase -60` to `decrease 60`.
8+
> You also need to start a waybar-timer server _before_ you start waybar.
89
910
This script implements a **simple** and **interactive** timer for your bar:
1011
- e.g. scroll to increase / decrease timer
@@ -31,14 +32,15 @@ Use cases: pomodoro timer, self-reminder when next meeting begins, tea/pasta tim
3132
## Installation
3233

3334
1. Download the binary from the [releases](https://github.com/jbirnick/waybar-timer/releases) (or build it yourself with cargo) and put it in a directory of your choice (e.g. `~/.scripts/`).
34-
2. Copy-paste the [example configuration](#example-configuration) from below into your waybar config and style it.
35-
3. Customize. (see [Customization section](#customization))
35+
2. In the startup script of your compositor, run `/path/to/waybar_timer serve` and make sure it starts **before waybar starts**.
36+
3. Copy-paste the [example configuration](#example-configuration) from below into your waybar config and style it.
37+
4. Customize. (see [Customization section](#customization))
3638

3739
## Example Configuration
3840

3941
```json
4042
"custom/timer": {
41-
"exec": "/path/to/waybar_timer tail",
43+
"exec": "/path/to/waybar_timer hook",
4244
"exec-on-event": false,
4345
"return-type": "json",
4446
"format": "{icon} {0}",
@@ -78,12 +80,17 @@ If you need a specific functionality feel free to open an issue and maybe we can
7880

7981
Notation: `<...>` are necessary arguments and `[...]` are optional arguments.
8082

81-
The main command of the script is:
83+
The main commands of the script are :
8284

83-
- #### `tail`
85+
- #### `serve`
86+
This is the command you want to put in the startup script of your compositor.
87+
Make sure you start this server _before_ you start waybar.
88+
It keeps the state of the timer and provides updates to all the clients who call `hook`.
89+
90+
- #### `hook`
8491
This is the command which you want to put in your waybar `exec` field.
85-
It keeps the state of the timer and regularly outputs it in JSON, so that waybar can render it.
86-
We will call the process which runs this `tail` routine the *tail process*.
92+
It subscribes to the server to get all the updates of the timer.
93+
Updates are delivered as JSON which is readable by waybar.
8794

8895
Now the following commands allow you to control the timer.
8996

@@ -112,6 +119,6 @@ You can implement this because `increase` will exit with code 1 when there is no
112119
```
113120
waybar-timer increase 60 || waybar-timer new 1 'notify-send "Timer expired."'
114121
```
115-
Then if there is an existing timer it gets increased, otherwise a new one minute timer is created.
122+
Then, if there is an existing timer it gets increased, otherwise a new one minute timer is created.
116123
This is also implemented in the [example configuration](#example-configuration).
117124
Just try to scroll up when there is no timer running!

src/main.rs

+115-50
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use std::os::unix::net::{UnixListener, UnixStream};
66
use std::sync::{Arc, Mutex};
77
use time::{Duration, OffsetDateTime};
88

9-
const SOCKET_PATH: &str = "/tmp/waybar_timer.sock";
9+
const SOCKET_PATH_COMMANDS: &str = "/tmp/waybar_timer_commands.sock";
10+
const SOCKET_PATH_UPDATES: &str = "/tmp/waybar_timer_updates.sock";
1011
const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
1112

1213
fn send_notification(summary: String) {
@@ -55,8 +56,8 @@ enum Timer {
5556
}
5657

5758
impl Timer {
58-
/// update routine which is called regularly and on every change of the timer
59-
fn update(&mut self) -> std::io::Result<()> {
59+
/// updates timer, potentially executes action, and returns formatted string for waybar
60+
fn update(&mut self) -> String {
6061
let now = OffsetDateTime::now_local().unwrap();
6162

6263
// check if timer expired
@@ -89,8 +90,7 @@ impl Timer {
8990
(minutes_left, "paused", tooltip)
9091
}
9192
};
92-
println!("{{\"text\": \"{text}\", \"alt\": \"{alt}\", \"tooltip\": \"{tooltip}\", \"class\": \"timer\"}}");
93-
std::io::stdout().flush()
93+
format!("{{\"text\": \"{text}\", \"alt\": \"{alt}\", \"tooltip\": \"{tooltip}\", \"class\": \"timer\"}}")
9494
}
9595

9696
fn tooltip(expiry: &OffsetDateTime) -> String {
@@ -170,8 +170,10 @@ impl World for Timer {
170170
/// Waybar Timer (see https://github.com/jbirnick/waybar-timer/)
171171
#[derive(Parser)]
172172
enum Args {
173-
/// Start a server process (should be from within waybar)
174-
Tail,
173+
/// Serve a timer API (should be called once at compositor startup)
174+
Serve,
175+
/// Keep reading the latest status of the timer (should be called by waybar)
176+
Hook,
175177
/// Start a new timer
176178
New {
177179
minutes: u32,
@@ -187,79 +189,142 @@ enum Args {
187189
Cancel,
188190
}
189191

192+
struct ServerState {
193+
timer: Timer,
194+
subs: Vec<UnixStream>,
195+
}
196+
197+
impl ServerState {
198+
fn update(&mut self) {
199+
// update timer and get waybar string
200+
let message = self.timer.update();
201+
202+
// broadcast it to subscribers
203+
let mut i: usize = 0;
204+
loop {
205+
if i == self.subs.len() {
206+
break;
207+
}
208+
match writeln!(self.subs[i], "{}", message) {
209+
Ok(()) => {
210+
let _ = self.subs[i].flush();
211+
i += 1;
212+
}
213+
Err(err) => {
214+
println!("couldn't write to subscriber stream: {}", err);
215+
println!("will drop the subscriber");
216+
self.subs.swap_remove(i);
217+
}
218+
}
219+
}
220+
}
221+
}
222+
223+
fn run_serve() {
224+
let state = Arc::new(Mutex::new(ServerState {
225+
timer: Timer::Idle,
226+
subs: Vec::new(),
227+
}));
228+
229+
// spawn a thread which is responsible for calling update in a regular interval
230+
let state_thread_interval = state.clone();
231+
std::thread::spawn(move || loop {
232+
std::thread::sleep(INTERVAL);
233+
let mut state = state_thread_interval.lock().unwrap();
234+
state.update();
235+
});
236+
237+
// spawn a thread which is responsible for accepting new subscribers
238+
let state_thread_subaccept = state.clone();
239+
std::thread::spawn(move || {
240+
// NOTE: binding is not possible if the file already exists, that's why we delete it first
241+
// this leads to undefined behavior when there is already a tail process running
242+
// maybe would be better to instead remove the file when program exits
243+
let _ = std::fs::remove_file(SOCKET_PATH_UPDATES);
244+
let listener = UnixListener::bind(SOCKET_PATH_UPDATES).unwrap();
245+
for stream in listener.incoming() {
246+
match stream {
247+
Ok(stream) => {
248+
// put to list of subscribers and trigger update so that
249+
// the new subscriber gets the current state
250+
let mut state = state_thread_subaccept.lock().unwrap();
251+
stream.shutdown(std::net::Shutdown::Read).unwrap();
252+
state.subs.push(stream);
253+
state.update();
254+
}
255+
Err(err) => {
256+
panic!("{err}")
257+
}
258+
}
259+
}
260+
});
261+
262+
// the main thread handles handle requests from the CLI
263+
// NOTE: binding is not possible if the file already exists, that's why we delete it first
264+
// this leads to undefined behavior when there is already a tail process running
265+
// maybe would be better to instead remove the file when program exits
266+
let _ = std::fs::remove_file(SOCKET_PATH_COMMANDS);
267+
let listener = UnixListener::bind(SOCKET_PATH_COMMANDS).unwrap();
268+
for stream in listener.incoming() {
269+
match stream {
270+
Ok(stream) => {
271+
// handles a single remote procedure call
272+
let mut state = state.lock().unwrap();
273+
state.timer.handle_with(&stream, &stream).unwrap();
274+
stream.shutdown(std::net::Shutdown::Both).unwrap();
275+
state.update();
276+
}
277+
Err(err) => {
278+
panic!("{err}")
279+
}
280+
}
281+
}
282+
}
283+
190284
fn main() -> Result<(), Box<dyn Error>> {
191285
let args = Args::parse();
192286
match args {
193-
Args::Tail => {
194-
run_tail();
287+
Args::Serve => {
288+
run_serve();
289+
Ok(())
290+
}
291+
Args::Hook => {
292+
let mut stream = UnixStream::connect(SOCKET_PATH_UPDATES)?;
293+
stream.shutdown(std::net::Shutdown::Write)?;
294+
let mut stdout = std::io::stdout();
295+
std::io::copy(&mut stream, &mut stdout)?;
195296
Ok(())
196297
}
197298
Args::New { minutes, command } => {
198-
let stream = UnixStream::connect(SOCKET_PATH)?;
299+
let stream = UnixStream::connect(SOCKET_PATH_COMMANDS)?;
199300
WorldRPCClient::call_with(&stream, &stream).start(&minutes, &command)??;
200301
stream.shutdown(std::net::Shutdown::Both)?;
201302
Ok(())
202303
}
203304
Args::Increase { seconds } => {
204-
let stream = UnixStream::connect(SOCKET_PATH)?;
305+
let stream = UnixStream::connect(SOCKET_PATH_COMMANDS)?;
205306
WorldRPCClient::call_with(&stream, &stream).increase(&seconds.into())??;
206307
stream.shutdown(std::net::Shutdown::Both)?;
207308
Ok(())
208309
}
209310
Args::Decrease { seconds } => {
210311
let seconds: i64 = seconds.into();
211-
let stream = UnixStream::connect(SOCKET_PATH)?;
312+
let stream = UnixStream::connect(SOCKET_PATH_COMMANDS)?;
212313
WorldRPCClient::call_with(&stream, &stream).increase(&-seconds)??;
213314
stream.shutdown(std::net::Shutdown::Both)?;
214315
Ok(())
215316
}
216317
Args::Togglepause => {
217-
let stream = UnixStream::connect(SOCKET_PATH)?;
318+
let stream = UnixStream::connect(SOCKET_PATH_COMMANDS)?;
218319
WorldRPCClient::call_with(&stream, &stream).togglepause()??;
219320
stream.shutdown(std::net::Shutdown::Both)?;
220321
Ok(())
221322
}
222323
Args::Cancel => {
223-
let stream = UnixStream::connect(SOCKET_PATH)?;
324+
let stream = UnixStream::connect(SOCKET_PATH_COMMANDS)?;
224325
WorldRPCClient::call_with(&stream, &stream).cancel()??;
225326
stream.shutdown(std::net::Shutdown::Both)?;
226327
Ok(())
227328
}
228329
}
229330
}
230-
231-
fn run_tail() {
232-
let timer = Arc::new(Mutex::new(Timer::Idle));
233-
{
234-
let mut timer = timer.lock().unwrap();
235-
timer.update().unwrap();
236-
}
237-
238-
let timer_thread = timer.clone();
239-
std::thread::spawn(move || loop {
240-
std::thread::sleep(INTERVAL);
241-
let mut timer = timer_thread.lock().unwrap();
242-
timer.update().unwrap();
243-
});
244-
245-
// handle requests from the CLI
246-
// NOTE: binding is not possible if the file already exists, that's why we delete it first
247-
// this leads to undefined behavior when there is already a tail process running
248-
// maybe would be better to instead remove the file when program exits
249-
let _ = std::fs::remove_file(SOCKET_PATH);
250-
let listener = UnixListener::bind(SOCKET_PATH).unwrap();
251-
for stream in listener.incoming() {
252-
match stream {
253-
Ok(stream) => {
254-
// handles a single remote procedure call
255-
let mut timer = timer.lock().unwrap();
256-
timer.handle_with(&stream, &stream).unwrap();
257-
stream.shutdown(std::net::Shutdown::Both).unwrap();
258-
timer.update().unwrap();
259-
}
260-
Err(err) => {
261-
panic!("{err}")
262-
}
263-
}
264-
}
265-
}

0 commit comments

Comments
 (0)