Merge pull request #35 from JakeStanger/feat/script-watch

feat(script): new watch mode
This commit is contained in:
Jake Stanger 2022-11-07 21:26:38 +00:00 committed by GitHub
commit 47420d83bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 19 deletions

10
Cargo.lock generated
View File

@ -1948,6 +1948,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.7"
@ -2209,6 +2218,7 @@ dependencies = [
"mio",
"num_cpus",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"winapi",

View File

@ -10,7 +10,7 @@ derive_builder = "0.11.2"
gtk = "0.16.0"
gtk-layer-shell = "0.5.0"
glib = "0.16.2"
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time"] }
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread", "time", "process"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-error = "0.2.0"

View File

@ -5,10 +5,17 @@ Pango markup is supported.
> Type: `script`
| Name | Type | Default | Description |
|-----------|----------|---------|---------------------------------------------------------|
| `path` | `string` | `null` | Path to the script on disk |
| `interval` | `number` | `5000` | Number of milliseconds to wait between executing script |
| Name | Type | Default | Description |
|------------|-----------------------|---------|---------------------------------------------------------|
| `path` | `string` | `null` | Path to the script on disk |
| `mode` | `'poll'` or `'watch'` | `poll` | See [#modes](#modes) |
| `interval` | `number` | `5000` | Number of milliseconds to wait between executing script |
### Modes
- Use `poll` to run the script wait for it to exit. On exit, the label is updated to show everything the script wrote to `stdout`.
- Use `watch` to start a long-running script. Every time the script writes to `stdout`, the label is updated to show the latest line.
Note this does not work for all programs as they may use block-buffering instead of line-buffering when they detect output being piped.
<details>
<summary>JSON</summary>
@ -19,6 +26,7 @@ Pango markup is supported.
{
"type": "script",
"path": "/home/jake/.local/bin/phone-battery",
"mode": "poll",
"interval": 5000
}
]
@ -35,6 +43,7 @@ Pango markup is supported.
[[end]]
type = "script"
path = "/home/jake/.local/bin/phone-battery"
mode = "poll"
interval = 5000
```
@ -47,6 +56,7 @@ interval = 5000
end:
- type: "script"
path: "/home/jake/.local/bin/phone-battery"
mode: 'poll'
interval : 5000
```
@ -61,6 +71,7 @@ end:
{
type = "script"
path = "/home/jake/.local/bin/phone-battery"
mode = "poll"
interval = 5000
}
]

View File

@ -1,23 +1,41 @@
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::script::exec_command;
use color_eyre::Result;
use color_eyre::{Help, Report, Result};
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use tokio::spawn;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::time::sleep;
use tokio::{select, spawn};
use tracing::error;
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
enum Mode {
Poll,
Watch,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule {
/// Path to script to execute.
path: String,
/// Script execution mode
#[serde(default = "default_mode")]
mode: Mode,
/// Time in milliseconds between executions.
#[serde(default = "default_interval")]
interval: u64,
}
/// `Mode::Poll`
const fn default_mode() -> Mode {
Mode::Poll
}
/// 5000ms
const fn default_interval() -> u64 {
5000
@ -35,19 +53,76 @@ impl Module<Label> for ScriptModule {
) -> Result<()> {
let interval = self.interval;
let path = self.path.clone();
spawn(async move {
loop {
match exec_command(&path) {
Ok(stdout) => tx
.send(ModuleUpdateEvent::Update(stdout))
.await
.expect("Failed to send stdout"),
Err(err) => error!("{:?}", err),
}
sleep(tokio::time::Duration::from_millis(interval)).await;
}
});
match self.mode {
Mode::Poll => spawn(async move {
loop {
match exec_command(&path) {
Ok(stdout) => tx
.send(ModuleUpdateEvent::Update(stdout))
.await
.expect("Failed to send stdout"),
Err(err) => error!("{:?}", err),
}
sleep(tokio::time::Duration::from_millis(interval)).await;
}
}),
Mode::Watch => spawn(async move {
loop {
let mut handle = Command::new("sh")
.args(["-c", &path])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.spawn()
.expect("Failed to spawn process");
let mut stdout_lines = BufReader::new(
handle
.stdout
.take()
.expect("Failed to take script handle stdout"),
)
.lines();
let mut stderr_lines = BufReader::new(
handle
.stderr
.take()
.expect("Failed to take script handle stderr"),
)
.lines();
loop {
select! {
_ = handle.wait() => break,
Ok(Some(line)) = stdout_lines.next_line() => {
tx.send(ModuleUpdateEvent::Update(line.to_string()))
.await
.expect("Failed to send stdout");
}
Ok(Some(line)) = stderr_lines.next_line() => {
error!("{:?}", Report::msg(line)
.wrap_err("Watched script error:")
.suggestion("Check the path to your script")
.suggestion("Check the script for errors")
.suggestion("If you expect the script to write to stderr, consider redirecting its output to /dev/null to suppress these messages")
)
}
}
}
while let Ok(Some(line)) = stdout_lines.next_line().await {
tx.send(ModuleUpdateEvent::Update(line.to_string()))
.await
.expect("Failed to send stdout");
}
sleep(tokio::time::Duration::from_millis(interval)).await;
}
}),
};
Ok(())
}