fix: editor split, stop flag in actions, write errors, history on failure, expect panic, string match
- src/main.rs: split VISUAL/EDITOR into prog+args, drop expect panic in reap_timer, propagate write_timer errors, check stop during actions, record history on partial failure, use const for stop msg - Cargo.toml: add authors/homepage, bump noyalib 0.0.12->0.0.13 - release.yaml: tighten tag glob, fix curl 404 fallback, URI-encode asset name, upgrade upload-artifact v3->v4 - ci.yaml: add cargo cache step - README.md: sync feature list, fix push to two-step - RELEASING.md: clarify Cargo.lock regeneration
This commit is contained in:
@@ -14,6 +14,14 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache Cargo registry and build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
target
|
||||
key: cargo-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- name: Check formatting
|
||||
shell: bash
|
||||
run: cargo fmt --check
|
||||
|
||||
@@ -3,7 +3,7 @@ name: release
|
||||
"on":
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
- "v[0-9]*.[0-9]*.[0-9]*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
release_url="https://gitea.forust.xyz/api/v1/repos/forust/gosleep/releases"
|
||||
existing="$(curl -fsS \
|
||||
"${release_url}/tags/${tag}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" || true)"
|
||||
-H "Authorization: token ${GITEA_TOKEN}")" || existing='{}'
|
||||
release_id="$(printf '%s' "$existing" | jq -r '.id // empty')"
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
@@ -112,14 +112,15 @@ jobs:
|
||||
-X DELETE "${release_url}/${release_id}/assets/${asset_id}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}"
|
||||
fi
|
||||
encoded="$(printf '%s' "$name" | jq -sRr @uri)"
|
||||
curl -fsS \
|
||||
-X POST "${release_url}/${release_id}/assets?name=${name}" \
|
||||
-X POST "${release_url}/${release_id}/assets?name=${encoded}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${asset}"
|
||||
done
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gosleep-timer-linux-amd64
|
||||
path: dist/*
|
||||
|
||||
Generated
+2
-2
@@ -807,9 +807,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noyalib"
|
||||
version = "0.0.12"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "903f06e76b14b388b47d651b9498b008888a9475084087e51c294731acbf16c1"
|
||||
checksum = "2095d0b3b6017ebbdaffedf83ffb2bff7e468ab379ac391239abea4880833290"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
|
||||
+3
-1
@@ -1,11 +1,13 @@
|
||||
[package]
|
||||
name = "gosleep-timer"
|
||||
version = "1.2.2"
|
||||
authors = ["forust"]
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
description = "Terminal sleep timer for Linux desktop actions, with YAML config and a Rust TUI"
|
||||
license = "MIT"
|
||||
repository = "https://gitea.forust.xyz/forust/gosleep"
|
||||
homepage = "https://gitea.forust.xyz/forust/gosleep"
|
||||
readme = "README.md"
|
||||
keywords = ["timer", "tui", "linux", "desktop", "sleep"]
|
||||
categories = ["command-line-utilities"]
|
||||
@@ -20,7 +22,7 @@ clap = { version = "4.5", features = ["derive"] }
|
||||
crossterm = "0.29"
|
||||
ratatui = "0.30.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
noyalib = "0.0.12"
|
||||
noyalib = "0.0.13"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -15,7 +15,7 @@ Rust TUI sleep timer for Linux desktop actions.
|
||||
- Single Rust binary, independent of Python.
|
||||
- Ratatui/Crossterm terminal UI.
|
||||
- YAML config at `~/.config/gosleep-timer/config.yaml` or `$XDG_CONFIG_HOME/gosleep-timer/config.yaml`.
|
||||
- CLI commands for initialization, preview, and direct timer runs.
|
||||
- CLI commands for init, status, validate, edit, preview, run, history, and stats.
|
||||
- Time-left display and progress bar.
|
||||
- Wrapped command preview for smaller terminals.
|
||||
- Linux desktop action support for niri, Hyprland, KDE, playerctl, brightnessctl/light, PipeWire/PulseAudio, systemd, and custom shell commands.
|
||||
@@ -224,7 +224,8 @@ Release tags must match the package version:
|
||||
|
||||
```bash
|
||||
git tag -a vMAJOR.MINOR.PATCH -m "gosleep-timer vMAJOR.MINOR.PATCH"
|
||||
git push origin main --tags
|
||||
git push origin main
|
||||
git push origin vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ version=1.1.4
|
||||
Update:
|
||||
|
||||
- `Cargo.toml`: `package.version`
|
||||
- `Cargo.lock`: package version, regenerated by Cargo
|
||||
- `Cargo.lock`: run `cargo build --release` (without `--locked`) to regenerate
|
||||
- `VERSION`
|
||||
- `CHANGELOG.md`
|
||||
|
||||
|
||||
+64
-33
@@ -12,6 +12,8 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
const TIMER_STOPPED_MSG: &str = "timer stopped";
|
||||
use clap::{Parser, Subcommand};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
@@ -295,10 +297,23 @@ fn main() -> Result<()> {
|
||||
Some(&mut stdout),
|
||||
)?;
|
||||
let slept_seconds = prompt_wake_confirmation(&config, options)?;
|
||||
let status = if report.failures.is_empty() {
|
||||
"finished"
|
||||
} else {
|
||||
"partial"
|
||||
};
|
||||
append_history(
|
||||
&history_path(&path),
|
||||
report.to_history("finished", slept_seconds),
|
||||
)
|
||||
report.to_history(status, slept_seconds),
|
||||
)?;
|
||||
if !report.failures.is_empty() {
|
||||
bail!(
|
||||
"{} action(s) failed: {}",
|
||||
report.failures.len(),
|
||||
report.failures.join("; ")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(Commands::Status) => {
|
||||
print_status(&path, &config);
|
||||
@@ -466,10 +481,12 @@ fn edit_config(path: &Path) -> Result<()> {
|
||||
let editor = std::env::var("VISUAL")
|
||||
.or_else(|_| std::env::var("EDITOR"))
|
||||
.unwrap_or_else(|_| "vi".to_string());
|
||||
let status = Command::new(editor)
|
||||
.arg(path)
|
||||
.status()
|
||||
.context("open editor")?;
|
||||
let mut parts = editor.split_whitespace();
|
||||
let program = parts.next().context("empty editor command")?;
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(parts);
|
||||
cmd.arg(path);
|
||||
let status = cmd.status().context("open editor")?;
|
||||
if !status.success() {
|
||||
bail!("editor exited with {status}");
|
||||
}
|
||||
@@ -716,13 +733,13 @@ enum RunMode {
|
||||
Tui,
|
||||
}
|
||||
|
||||
fn write_timer_line(output: &mut dyn Write, message: &str) {
|
||||
writeln!(output, "{message}").ok();
|
||||
fn write_timer_line(output: &mut dyn Write, message: &str) -> io::Result<()> {
|
||||
writeln!(output, "{message}")
|
||||
}
|
||||
|
||||
fn write_timer_progress(output: &mut dyn Write, message: &str) {
|
||||
write!(output, "\r{message}").ok();
|
||||
output.flush().ok();
|
||||
fn write_timer_progress(output: &mut dyn Write, message: &str) -> io::Result<()> {
|
||||
write!(output, "\r{message}")?;
|
||||
output.flush()
|
||||
}
|
||||
|
||||
fn run_timer(
|
||||
@@ -745,15 +762,15 @@ fn run_timer(
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
write_timer_line(output, &format!("timer started: {}", config.duration));
|
||||
write_timer_line(output, &format!("timer started: {}", config.duration))?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
write_timer_line(output, "stopped");
|
||||
write_timer_line(output, "stopped")?;
|
||||
}
|
||||
bail!("timer stopped");
|
||||
bail!(TIMER_STOPPED_MSG);
|
||||
}
|
||||
|
||||
if pause.load(Ordering::Relaxed) {
|
||||
@@ -778,7 +795,7 @@ fn run_timer(
|
||||
write_timer_progress(
|
||||
output,
|
||||
&format!("remaining: {}", format_duration(remaining)),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
@@ -786,13 +803,13 @@ fn run_timer(
|
||||
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
write_timer_line(output, "stopped");
|
||||
write_timer_line(output, "stopped")?;
|
||||
}
|
||||
bail!("timer stopped");
|
||||
}
|
||||
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
write_timer_line(output, "finished");
|
||||
write_timer_line(output, "finished")?;
|
||||
}
|
||||
let mut failures = Vec::new();
|
||||
let actions = if options.no_actions {
|
||||
@@ -801,9 +818,15 @@ fn run_timer(
|
||||
build_action_commands(config)
|
||||
};
|
||||
for command in &actions {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
write_timer_line(output, "stopped")?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
let prefix = if options.dry_run { "would run" } else { "run" };
|
||||
write_timer_line(output, &format!("{prefix}: {}", command.display()));
|
||||
write_timer_line(output, &format!("{prefix}: {}", command.display()))?;
|
||||
}
|
||||
if options.dry_run {
|
||||
continue;
|
||||
@@ -820,12 +843,8 @@ fn run_timer(
|
||||
dry_run: options.dry_run,
|
||||
no_actions: options.no_actions,
|
||||
};
|
||||
if !failures.is_empty() {
|
||||
bail!(
|
||||
"{} action(s) failed: {}",
|
||||
failures.len(),
|
||||
failures.join("; ")
|
||||
);
|
||||
if !failures.is_empty() && let Some(output) = &mut output {
|
||||
write_timer_line(output, &format!("{} action(s) failed", failures.len()))?;
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
@@ -1359,7 +1378,9 @@ impl App {
|
||||
.as_ref()
|
||||
.is_some_and(|timer| timer.handle.is_finished())
|
||||
{
|
||||
let timer = self.timer.take().expect("timer exists");
|
||||
let Some(timer) = self.timer.take() else {
|
||||
return;
|
||||
};
|
||||
self.apply_timer_result(timer.handle.join());
|
||||
}
|
||||
}
|
||||
@@ -1367,16 +1388,25 @@ impl App {
|
||||
fn apply_timer_result(&mut self, result: thread::Result<Result<TimerReport>>) {
|
||||
match result {
|
||||
Ok(Ok(report)) => {
|
||||
if let Err(err) = append_history(
|
||||
&history_path(&self.path),
|
||||
report.to_history("finished", None),
|
||||
) {
|
||||
let status = if report.failures.is_empty() {
|
||||
"finished"
|
||||
} else {
|
||||
"partial"
|
||||
};
|
||||
if let Err(err) =
|
||||
append_history(&history_path(&self.path), report.to_history(status, None))
|
||||
{
|
||||
self.status = format!("history failed: {err}");
|
||||
} else if !report.failures.is_empty() {
|
||||
self.status = format!(
|
||||
"timer finished ({} action(s) failed)",
|
||||
report.failures.len()
|
||||
);
|
||||
} else {
|
||||
self.status = "timer finished".to_string();
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) if err.to_string() == "timer stopped" => {
|
||||
Ok(Err(err)) if err.to_string() == TIMER_STOPPED_MSG => {
|
||||
self.status = "timer stopped".to_string()
|
||||
}
|
||||
Ok(Err(err)) => self.status = format!("timer failed: {err}"),
|
||||
@@ -1951,7 +1981,7 @@ mod tests {
|
||||
config.actions.custom.commands = vec!["false".to_string(), "true".to_string()];
|
||||
let mut output = Vec::new();
|
||||
|
||||
let error = run_timer(
|
||||
let report = run_timer(
|
||||
&config,
|
||||
&stop,
|
||||
&Arc::new(AtomicBool::new(false)),
|
||||
@@ -1960,12 +1990,13 @@ mod tests {
|
||||
TimerOptions::default(),
|
||||
Some(&mut output),
|
||||
)
|
||||
.unwrap_err();
|
||||
.unwrap();
|
||||
|
||||
let text = String::from_utf8(output).unwrap();
|
||||
assert!(text.contains("run: false"));
|
||||
assert!(text.contains("run: true"));
|
||||
assert!(error.to_string().contains("1 action(s) failed"));
|
||||
assert_eq!(report.failures.len(), 1);
|
||||
assert!(report.failures[0].contains("custom failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user