5 Commits

Author SHA1 Message Date
forust d0841d2c9b chore: release v1.3.1
release / verify (push) Successful in 28s
release / linux-amd64 (push) Failing after 47s
2026-07-12 14:22:45 +02:00
forust af2deabca4 feat: add mute_input, inhibit_sleep, monitor-off, notify actions
- mute_input: toggle mic via wpctl/pactl
- inhibit_sleep: systemd-inhibit during timer
- monitor-off: dpms backends (hyprland, niri, sway, kde, gnome, x11)
- notify: notify-send on timer finish
- lock: refactored with backend selection (loginctl, hyprlock,
  swaylock, i3lock, gnome, kde, xscreensaver)
- workspace: custom command override, sway/i3 backends
- power: add suspend, hibernate, hybrid-sleep modes
- editor: split VISUAL/EDITOR into program+args
- run: check stop flag during action loop
- history: record "partial" on action failures
2026-07-12 10:08:38 +02:00
forust 1edb82a057 feat: add monitor-off action, sway/i3 workspace
Monitor-off follows same per-backend dispatch as workspace.
Backends: hyprland, niri, sway, kde, gnome, x11, auto.
New workspace backends: sway, i3.
2026-07-07 23:23:50 +02:00
forust 1a77043a71 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
2026-07-07 23:13:19 +02:00
forust 69e20c89ee chore: release v1.2.2
ci / rust (push) Successful in 53s
ci / lint-yaml (push) Successful in 5s
release / verify (push) Successful in 27s
release / linux-amd64 (push) Successful in 38s
2026-07-05 03:26:48 +02:00
9 changed files with 1641 additions and 241 deletions
+8
View File
@@ -14,6 +14,14 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 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 - name: Check formatting
shell: bash shell: bash
run: cargo fmt --check run: cargo fmt --check
+5 -4
View File
@@ -3,7 +3,7 @@ name: release
"on": "on":
push: push:
tags: tags:
- "v*.*.*" - "v[0-9]*.[0-9]*.[0-9]*"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -69,7 +69,7 @@ jobs:
release_url="https://gitea.forust.xyz/api/v1/repos/forust/gosleep/releases" release_url="https://gitea.forust.xyz/api/v1/repos/forust/gosleep/releases"
existing="$(curl -fsS \ existing="$(curl -fsS \
"${release_url}/tags/${tag}" \ "${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')" release_id="$(printf '%s' "$existing" | jq -r '.id // empty')"
if [ -z "$release_id" ]; then if [ -z "$release_id" ]; then
@@ -112,14 +112,15 @@ jobs:
-X DELETE "${release_url}/${release_id}/assets/${asset_id}" \ -X DELETE "${release_url}/${release_id}/assets/${asset_id}" \
-H "Authorization: token ${GITEA_TOKEN}" -H "Authorization: token ${GITEA_TOKEN}"
fi fi
encoded="$(printf '%s' "$name" | jq -sRr @uri)"
curl -fsS \ 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}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${asset}" -F "attachment=@${asset}"
done done
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: gosleep-timer-linux-amd64 name: gosleep-timer-linux-amd64
path: dist/* path: dist/*
+31
View File
@@ -4,6 +4,35 @@ All notable changes to this project are documented here.
The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`. The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`.
## [1.3.1] - 2026-07-12
### Added
- Added `mute_input` action to toggle microphone mute via `wpctl`/`pactl`.
- Added `inhibit_sleep` config option to run `systemd-inhibit` during the timer.
- Added `monitor` action with backend selection (hyprland, niri, sway, kde, gnome, x11) to turn off displays.
- Added `notify` action to send desktop notification on timer finish.
- Added `suspend`, `hibernate`, `hybrid-sleep` power modes.
- Added workspace backend support for `sway` and `i3`.
- Added custom `workspace command` override.
### Changed
- Lock action refactored with backend selection: `loginctl`, `hyprlock`, `swaylock`, `i3lock`, `gnome`, `kde`, `xscreensaver`.
- Editor command now correctly splits `VISUAL`/`EDITOR` into program and arguments.
- Timer action loop checks the stop flag between actions.
### Fixed
- History now records `partial` status when some actions fail instead of bailing entirely.
## [1.2.2] - 2026-07-05
### Changed
- Updated Ratatui to 0.30.2 to remove the unmaintained `paste` transitive dependency and pick up the fixed `lru` dependency line.
- Replaced deprecated `serde_yaml`/`unsafe-libyaml` with the pure-Rust `noyalib` YAML parser.
## [1.2.1] - 2026-07-05 ## [1.2.1] - 2026-07-05
### Changed ### Changed
@@ -48,6 +77,8 @@ The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`
- Added YAML configuration, CLI `init`, `preview`, and `run` commands. - Added YAML configuration, CLI `init`, `preview`, and `run` commands.
- Added Gitea CI and tag-based release workflows. - Added Gitea CI and tag-based release workflows.
[1.3.1]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.3.1
[1.2.2]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.2
[1.2.1]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.1 [1.2.1]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.1
[1.2.0]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.0 [1.2.0]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.0
[1.1.3]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.3 [1.1.3]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.3
Generated
+1107 -174
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -1,11 +1,13 @@
[package] [package]
name = "gosleep-timer" name = "gosleep-timer"
version = "1.2.1" version = "1.3.1"
authors = ["forust"]
edition = "2024" edition = "2024"
rust-version = "1.85" rust-version = "1.88"
description = "Terminal sleep timer for Linux desktop actions, with YAML config and a Rust TUI" description = "Terminal sleep timer for Linux desktop actions, with YAML config and a Rust TUI"
license = "MIT" license = "MIT"
repository = "https://gitea.forust.xyz/forust/gosleep" repository = "https://gitea.forust.xyz/forust/gosleep"
homepage = "https://gitea.forust.xyz/forust/gosleep"
readme = "README.md" readme = "README.md"
keywords = ["timer", "tui", "linux", "desktop", "sleep"] keywords = ["timer", "tui", "linux", "desktop", "sleep"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
@@ -18,9 +20,9 @@ path = "src/main.rs"
anyhow = "1.0" anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
crossterm = "0.29" crossterm = "0.29"
ratatui = "0.29" ratatui = "0.30.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9" noyalib = "0.0.13"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
+5 -2
View File
@@ -15,7 +15,7 @@ Rust TUI sleep timer for Linux desktop actions.
- Single Rust binary, independent of Python. - Single Rust binary, independent of Python.
- Ratatui/Crossterm terminal UI. - Ratatui/Crossterm terminal UI.
- YAML config at `~/.config/gosleep-timer/config.yaml` or `$XDG_CONFIG_HOME/gosleep-timer/config.yaml`. - 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. - Time-left display and progress bar.
- Wrapped command preview for smaller terminals. - Wrapped command preview for smaller terminals.
- Linux desktop action support for niri, Hyprland, KDE, playerctl, brightnessctl/light, PipeWire/PulseAudio, systemd, and custom shell commands. - Linux desktop action support for niri, Hyprland, KDE, playerctl, brightnessctl/light, PipeWire/PulseAudio, systemd, and custom shell commands.
@@ -145,6 +145,8 @@ actions:
value: 30 value: 30
mute: mute:
enabled: false enabled: false
mute_input:
enabled: false
lock: lock:
enabled: false enabled: false
command: loginctl lock-session command: loginctl lock-session
@@ -224,7 +226,8 @@ Release tags must match the package version:
```bash ```bash
git tag -a vMAJOR.MINOR.PATCH -m "gosleep-timer vMAJOR.MINOR.PATCH" 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 ## Releases
+1 -1
View File
@@ -52,7 +52,7 @@ version=1.1.4
Update: Update:
- `Cargo.toml`: `package.version` - `Cargo.toml`: `package.version`
- `Cargo.lock`: package version, regenerated by Cargo - `Cargo.lock`: run `cargo build --release` (without `--locked`) to regenerate
- `VERSION` - `VERSION`
- `CHANGELOG.md` - `CHANGELOG.md`
+1 -1
View File
@@ -1 +1 @@
1.2.1 1.3.1
+476 -54
View File
@@ -12,6 +12,8 @@ use std::{
}; };
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
const TIMER_STOPPED_MSG: &str = "timer stopped";
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
@@ -62,6 +64,7 @@ enum Commands {
#[serde(default)] #[serde(default)]
struct Config { struct Config {
duration: String, duration: String,
inhibit_sleep: bool,
actions: Actions, actions: Actions,
} }
@@ -72,7 +75,10 @@ struct Actions {
media: MediaAction, media: MediaAction,
brightness: BrightnessAction, brightness: BrightnessAction,
mute: ToggleAction, mute: ToggleAction,
mute_input: ToggleAction,
lock: LockAction, lock: LockAction,
monitor: MonitorAction,
notify: NotifyAction,
kill: KillAction, kill: KillAction,
power: PowerAction, power: PowerAction,
custom: CustomAction, custom: CustomAction,
@@ -84,6 +90,7 @@ struct WorkspaceAction {
enabled: bool, enabled: bool,
backend: String, backend: String,
number: u32, number: u32,
command: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -110,9 +117,25 @@ struct ToggleAction {
#[serde(default)] #[serde(default)]
struct LockAction { struct LockAction {
enabled: bool, enabled: bool,
backend: String,
command: String, command: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
struct MonitorAction {
enabled: bool,
backend: String,
command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
struct NotifyAction {
enabled: bool,
message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)] #[serde(default)]
struct KillAction { struct KillAction {
@@ -168,10 +191,22 @@ struct TimerOptions {
no_actions: bool, no_actions: bool,
} }
struct InhibitGuard(Option<std::process::Child>);
impl Drop for InhibitGuard {
fn drop(&mut self) {
if let Some(mut child) = self.0.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
duration: "25m".to_string(), duration: "25m".to_string(),
inhibit_sleep: false,
actions: Actions::default(), actions: Actions::default(),
} }
} }
@@ -183,6 +218,7 @@ impl Default for WorkspaceAction {
enabled: true, enabled: true,
backend: "auto".to_string(), backend: "auto".to_string(),
number: 3, number: 3,
command: String::new(),
} }
} }
} }
@@ -209,7 +245,27 @@ impl Default for LockAction {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: false, enabled: false,
command: "loginctl lock-session".to_string(), backend: "auto".to_string(),
command: String::new(),
}
}
}
impl Default for MonitorAction {
fn default() -> Self {
Self {
enabled: false,
backend: "auto".to_string(),
command: String::new(),
}
}
}
impl Default for NotifyAction {
fn default() -> Self {
Self {
enabled: false,
message: "Timer finished".to_string(),
} }
} }
} }
@@ -295,10 +351,23 @@ fn main() -> Result<()> {
Some(&mut stdout), Some(&mut stdout),
)?; )?;
let slept_seconds = prompt_wake_confirmation(&config, options)?; let slept_seconds = prompt_wake_confirmation(&config, options)?;
let status = if report.failures.is_empty() {
"finished"
} else {
"partial"
};
append_history( append_history(
&history_path(&path), &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) => { Some(Commands::Status) => {
print_status(&path, &config); print_status(&path, &config);
@@ -351,7 +420,7 @@ fn ensure_config(path: &Path) -> Result<Config> {
return Ok(config); return Ok(config);
} }
let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let mut config: Config = serde_yaml::from_str(&data).context("parse config")?; let mut config: Config = noyalib::from_str(&data).context("parse config")?;
normalize_config(&mut config); normalize_config(&mut config);
validate_config(&config)?; validate_config(&config)?;
Ok(config) Ok(config)
@@ -364,7 +433,7 @@ fn save_config(path: &Path, config: &Config) -> Result<()> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
} }
let data = serde_yaml::to_string(&config).context("serialize config")?; let data = noyalib::to_string(&config).context("serialize config")?;
fs::write(path, data).with_context(|| format!("write {}", path.display()))?; fs::write(path, data).with_context(|| format!("write {}", path.display()))?;
Ok(()) Ok(())
} }
@@ -385,7 +454,7 @@ fn read_history(path: &Path) -> Result<Vec<HistoryEntry>> {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?; let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
serde_yaml::from_str(&data).with_context(|| format!("parse {}", path.display())) noyalib::from_str(&data).with_context(|| format!("parse {}", path.display()))
} }
fn append_history(path: &Path, entry: HistoryEntry) -> Result<()> { fn append_history(path: &Path, entry: HistoryEntry) -> Result<()> {
@@ -394,7 +463,7 @@ fn append_history(path: &Path, entry: HistoryEntry) -> Result<()> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
} }
let data = serde_yaml::to_string(&entries).context("serialize history")?; let data = noyalib::to_string(&entries).context("serialize history")?;
fs::write(path, data).with_context(|| format!("write {}", path.display()))?; fs::write(path, data).with_context(|| format!("write {}", path.display()))?;
Ok(()) Ok(())
} }
@@ -466,10 +535,12 @@ fn edit_config(path: &Path) -> Result<()> {
let editor = std::env::var("VISUAL") let editor = std::env::var("VISUAL")
.or_else(|_| std::env::var("EDITOR")) .or_else(|_| std::env::var("EDITOR"))
.unwrap_or_else(|_| "vi".to_string()); .unwrap_or_else(|_| "vi".to_string());
let status = Command::new(editor) let mut parts = editor.split_whitespace();
.arg(path) let program = parts.next().context("empty editor command")?;
.status() let mut cmd = Command::new(program);
.context("open editor")?; cmd.args(parts);
cmd.arg(path);
let status = cmd.status().context("open editor")?;
if !status.success() { if !status.success() {
bail!("editor exited with {status}"); bail!("editor exited with {status}");
} }
@@ -513,8 +584,14 @@ fn normalize_config(config: &mut Config) {
if config.actions.brightness.value > 100 { if config.actions.brightness.value > 100 {
config.actions.brightness.value = 100; config.actions.brightness.value = 100;
} }
if config.actions.lock.command.is_empty() { if config.actions.lock.backend.is_empty() {
config.actions.lock.command = "loginctl lock-session".to_string(); config.actions.lock.backend = "auto".to_string();
}
if config.actions.monitor.backend.is_empty() {
config.actions.monitor.backend = "auto".to_string();
}
if config.actions.notify.message.is_empty() {
config.actions.notify.message = "Timer finished".to_string();
} }
if config.actions.power.mode.is_empty() { if config.actions.power.mode.is_empty() {
config.actions.power.mode = "none".to_string(); config.actions.power.mode = "none".to_string();
@@ -524,13 +601,22 @@ fn normalize_config(config: &mut Config) {
fn validate_config(config: &Config) -> Result<()> { fn validate_config(config: &Config) -> Result<()> {
parse_duration(&config.duration)?; parse_duration(&config.duration)?;
match config.actions.power.mode.as_str() { match config.actions.power.mode.as_str() {
"none" | "poweroff" | "reboot" => {} "none" | "suspend" | "hibernate" | "hybrid-sleep" | "poweroff" | "reboot" => {}
mode => bail!("invalid power mode {mode:?}"), mode => bail!("invalid power mode {mode:?}"),
} }
match config.actions.workspace.backend.as_str() { match config.actions.workspace.backend.as_str() {
"auto" | "niri" | "hyprland" | "kde" => {} "auto" | "niri" | "hyprland" | "sway" | "i3" | "kde" => {}
backend => bail!("invalid workspace backend {backend:?}"), backend => bail!("invalid workspace backend {backend:?}"),
} }
match config.actions.monitor.backend.as_str() {
"auto" | "hyprland" | "niri" | "sway" | "kde" | "gnome" | "x11" => {}
backend => bail!("invalid monitor backend {backend:?}"),
}
match config.actions.lock.backend.as_str() {
"auto" | "loginctl" | "hyprlock" | "swaylock" | "i3lock" | "gnome" | "kde"
| "xscreensaver" => {}
backend => bail!("invalid lock backend {backend:?}"),
}
match config.actions.media.action.as_str() { match config.actions.media.action.as_str() {
"none" | "stop" | "pause" | "play-pause" | "next" | "previous" => {} "none" | "stop" | "pause" | "play-pause" | "next" | "previous" => {}
action => bail!("invalid media action {action:?}"), action => bail!("invalid media action {action:?}"),
@@ -612,11 +698,29 @@ fn build_action_commands(config: &Config) -> Vec<ActionCommand> {
), ),
}); });
} }
if actions.lock.enabled { if actions.mute_input.enabled {
commands.push(ActionCommand { commands.push(ActionCommand {
label: "lock", label: "mute_input",
args: vec![], args: vec![],
shell: Some(actions.lock.command.clone()), shell: Some(
"wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle || pactl set-source-mute @DEFAULT_SOURCE@ toggle".to_string(),
),
});
}
if actions.lock.enabled {
commands.push(lock_command(&actions.lock));
}
if actions.monitor.enabled {
commands.push(monitor_command(&actions.monitor));
}
if actions.notify.enabled {
commands.push(ActionCommand {
label: "notify",
args: vec![],
shell: Some(format!(
"notify-send 'gosleep-timer' '{}'",
actions.notify.message
)),
}); });
} }
if actions.kill.enabled { if actions.kill.enabled {
@@ -632,6 +736,21 @@ fn build_action_commands(config: &Config) -> Vec<ActionCommand> {
} }
} }
match actions.power.mode.as_str() { match actions.power.mode.as_str() {
"suspend" => commands.push(ActionCommand {
label: "power",
args: vec!["systemctl".to_string(), "suspend".to_string()],
shell: None,
}),
"hibernate" => commands.push(ActionCommand {
label: "power",
args: vec!["systemctl".to_string(), "hibernate".to_string()],
shell: None,
}),
"hybrid-sleep" => commands.push(ActionCommand {
label: "power",
args: vec!["systemctl".to_string(), "hybrid-sleep".to_string()],
shell: None,
}),
"poweroff" => commands.push(ActionCommand { "poweroff" => commands.push(ActionCommand {
label: "power", label: "power",
args: vec!["systemctl".to_string(), "poweroff".to_string()], args: vec!["systemctl".to_string(), "poweroff".to_string()],
@@ -660,6 +779,13 @@ fn build_action_commands(config: &Config) -> Vec<ActionCommand> {
} }
fn workspace_command(action: &WorkspaceAction) -> ActionCommand { fn workspace_command(action: &WorkspaceAction) -> ActionCommand {
if !action.command.is_empty() {
return ActionCommand {
label: "workspace",
args: vec![],
shell: Some(action.command.clone()),
};
}
let number = action.number.to_string(); let number = action.number.to_string();
match action.backend.as_str() { match action.backend.as_str() {
"niri" => ActionCommand { "niri" => ActionCommand {
@@ -678,6 +804,22 @@ fn workspace_command(action: &WorkspaceAction) -> ActionCommand {
.collect(), .collect(),
shell: None, shell: None,
}, },
"sway" => ActionCommand {
label: "workspace",
args: vec!["swaymsg", "workspace", "number", &number]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"i3" => ActionCommand {
label: "workspace",
args: vec!["i3-msg", "workspace", "number", &number]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"kde" => ActionCommand { "kde" => ActionCommand {
label: "workspace", label: "workspace",
args: vec![ args: vec![
@@ -696,13 +838,140 @@ fn workspace_command(action: &WorkspaceAction) -> ActionCommand {
label: "workspace", label: "workspace",
args: vec![], args: vec![],
shell: Some(format!( shell: Some(format!(
"if command -v niri >/dev/null 2>&1; then niri msg action focus-workspace {0}; elif command -v hyprctl >/dev/null 2>&1; then hyprctl dispatch workspace {0}; elif command -v qdbus >/dev/null 2>&1; then qdbus org.kde.KWin /KWin org.kde.KWin.setCurrentDesktop {0}; fi", "if command -v niri >/dev/null 2>&1; then niri msg action focus-workspace {0}; elif command -v hyprctl >/dev/null 2>&1; then hyprctl dispatch workspace {0}; elif command -v swaymsg >/dev/null 2>&1; then swaymsg workspace number {0}; elif command -v i3-msg >/dev/null 2>&1; then i3-msg workspace number {0}; elif command -v qdbus >/dev/null 2>&1; then qdbus org.kde.KWin /KWin org.kde.KWin.setCurrentDesktop {0}; fi",
number number
)), )),
}, },
} }
} }
fn monitor_command(action: &MonitorAction) -> ActionCommand {
if !action.command.is_empty() {
return ActionCommand {
label: "monitor",
args: vec![],
shell: Some(action.command.clone()),
};
}
match action.backend.as_str() {
"hyprland" => ActionCommand {
label: "monitor",
args: vec!["hyprctl", "dispatch", "dpms", "off"]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"niri" => ActionCommand {
label: "monitor",
args: vec!["niri", "msg", "action", "power-off-monitors"]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"sway" => ActionCommand {
label: "monitor",
args: vec!["swaymsg", "output", "*", "dpms", "off"]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"kde" => ActionCommand {
label: "monitor",
args: vec![
"qdbus",
"org.kde.ScreenSaver",
"/ScreenSaver",
"org.freedesktop.ScreenSaver.SetActive",
"true",
]
.into_iter()
.map(str::to_string)
.collect(),
shell: None,
},
"gnome" => ActionCommand {
label: "monitor",
args: vec![],
shell: Some("gnome-screensaver-command -l".to_string()),
},
"x11" => ActionCommand {
label: "monitor",
args: vec![],
shell: Some("xset dpms force off".to_string()),
},
_ => ActionCommand {
label: "monitor",
args: vec![],
shell: Some(
"if command -v hyprctl >/dev/null 2>&1; then hyprctl dispatch dpms off; elif command -v niri >/dev/null 2>&1; then niri msg action power-off-monitors; elif command -v swaymsg >/dev/null 2>&1; then swaymsg output '*' dpms off; elif command -v qdbus >/dev/null 2>&1; then qdbus org.kde.ScreenSaver /ScreenSaver org.freedesktop.ScreenSaver.SetActive true; elif command -v gnome-screensaver-command >/dev/null 2>&1; then gnome-screensaver-command -l; elif command -v xset >/dev/null 2>&1; then xset dpms force off; fi".to_string()
),
},
}
}
fn lock_command(action: &LockAction) -> ActionCommand {
if !action.command.is_empty() {
return ActionCommand {
label: "lock",
args: vec![],
shell: Some(action.command.clone()),
};
}
match action.backend.as_str() {
"loginctl" => ActionCommand {
label: "lock",
args: vec!["loginctl".to_string(), "lock-session".to_string()],
shell: None,
},
"hyprlock" => ActionCommand {
label: "lock",
args: vec![],
shell: Some("hyprctl dispatch exec hyprlock".to_string()),
},
"swaylock" => ActionCommand {
label: "lock",
args: vec!["swaylock".to_string()],
shell: None,
},
"i3lock" => ActionCommand {
label: "lock",
args: vec!["i3lock".to_string()],
shell: None,
},
"gnome" => ActionCommand {
label: "lock",
args: vec!["gnome-screensaver-command".to_string(), "-l".to_string()],
shell: None,
},
"kde" => ActionCommand {
label: "lock",
args: vec![
"qdbus".to_string(),
"org.kde.ScreenSaver".to_string(),
"/ScreenSaver".to_string(),
"org.freedesktop.ScreenSaver.SetActive".to_string(),
"true".to_string(),
],
shell: None,
},
"xscreensaver" => ActionCommand {
label: "lock",
args: vec!["xscreensaver-command".to_string(), "-lock".to_string()],
shell: None,
},
_ => ActionCommand {
label: "lock",
args: vec![],
shell: Some(
"if command -v loginctl >/dev/null 2>&1; then loginctl lock-session; elif command -v hyprctl >/dev/null 2>&1; then hyprctl dispatch exec hyprlock; elif command -v swaylock >/dev/null 2>&1; then swaylock; elif command -v i3lock >/dev/null 2>&1; then i3lock; elif command -v gnome-screensaver-command >/dev/null 2>&1; then gnome-screensaver-command -l; elif command -v qdbus >/dev/null 2>&1; then qdbus org.kde.ScreenSaver /ScreenSaver org.freedesktop.ScreenSaver.SetActive true; elif command -v xscreensaver-command >/dev/null 2>&1; then xscreensaver-command -lock; fi".to_string()
),
},
}
}
fn preview_commands(config: &Config) -> Vec<String> { fn preview_commands(config: &Config) -> Vec<String> {
build_action_commands(config) build_action_commands(config)
.iter() .iter()
@@ -716,13 +985,13 @@ enum RunMode {
Tui, Tui,
} }
fn write_timer_line(output: &mut dyn Write, message: &str) { fn write_timer_line(output: &mut dyn Write, message: &str) -> io::Result<()> {
writeln!(output, "{message}").ok(); writeln!(output, "{message}")
} }
fn write_timer_progress(output: &mut dyn Write, message: &str) { fn write_timer_progress(output: &mut dyn Write, message: &str) -> io::Result<()> {
write!(output, "\r{message}").ok(); write!(output, "\r{message}")?;
output.flush().ok(); output.flush()
} }
fn run_timer( fn run_timer(
@@ -744,16 +1013,32 @@ fn run_timer(
duration.as_millis().min(u128::from(u64::MAX)) as u64, duration.as_millis().min(u128::from(u64::MAX)) as u64,
Ordering::Relaxed, Ordering::Relaxed,
); );
let _inhibit = if config.inhibit_sleep {
let child = Command::new("systemd-inhibit")
.args([
"--what=sleep",
"--who=gosleep-timer",
"--why=Timer running",
"sleep",
"infinity",
])
.spawn()
.context("spawn systemd-inhibit")?;
Some(InhibitGuard(Some(child)))
} else {
None
};
if let Some(output) = output.as_deref_mut() { 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 { loop {
if stop.load(Ordering::Relaxed) { if stop.load(Ordering::Relaxed) {
if let Some(output) = output.as_deref_mut() { 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) { if pause.load(Ordering::Relaxed) {
@@ -778,7 +1063,7 @@ fn run_timer(
write_timer_progress( write_timer_progress(
output, output,
&format!("remaining: {}", format_duration(remaining)), &format!("remaining: {}", format_duration(remaining)),
); )?;
} }
thread::sleep(Duration::from_millis(250)); thread::sleep(Duration::from_millis(250));
} }
@@ -786,13 +1071,13 @@ fn run_timer(
if stop.load(Ordering::Relaxed) { if stop.load(Ordering::Relaxed) {
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
write_timer_line(output, "stopped"); write_timer_line(output, "stopped")?;
} }
bail!("timer stopped"); bail!("timer stopped");
} }
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
write_timer_line(output, "finished"); write_timer_line(output, "finished")?;
} }
let mut failures = Vec::new(); let mut failures = Vec::new();
let actions = if options.no_actions { let actions = if options.no_actions {
@@ -801,9 +1086,15 @@ fn run_timer(
build_action_commands(config) build_action_commands(config)
}; };
for command in &actions { 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() { if let Some(output) = output.as_deref_mut() {
let prefix = if options.dry_run { "would run" } else { "run" }; 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 { if options.dry_run {
continue; continue;
@@ -820,12 +1111,10 @@ fn run_timer(
dry_run: options.dry_run, dry_run: options.dry_run,
no_actions: options.no_actions, no_actions: options.no_actions,
}; };
if !failures.is_empty() { if !failures.is_empty()
bail!( && let Some(output) = &mut output
"{} action(s) failed: {}", {
failures.len(), write_timer_line(output, &format!("{} action(s) failed", failures.len()))?;
failures.join("; ")
);
} }
Ok(report) Ok(report)
} }
@@ -1229,12 +1518,12 @@ impl App {
match key { match key {
KeyCode::Esc => self.edit = None, KeyCode::Esc => self.edit = None,
KeyCode::Enter => { KeyCode::Enter => {
if let Some(edit) = self.edit.take() { if let Some(edit) = self.edit.take()
if let Err(err) = self.apply_edit(edit.field, edit.value) { && let Err(err) = self.apply_edit(edit.field, edit.value)
{
self.status = format!("edit failed: {err}"); self.status = format!("edit failed: {err}");
} }
} }
}
KeyCode::Backspace => { KeyCode::Backspace => {
if let Some(edit) = &mut self.edit { if let Some(edit) = &mut self.edit {
edit.value.pop(); edit.value.pop();
@@ -1253,7 +1542,10 @@ impl App {
match field.kind { match field.kind {
FieldKind::Edit => match field.key { FieldKind::Edit => match field.key {
FieldKey::Duration => self.config.duration = value, FieldKey::Duration => self.config.duration = value,
FieldKey::WorkspaceCommand => self.config.actions.workspace.command = value,
FieldKey::LockCommand => self.config.actions.lock.command = value, FieldKey::LockCommand => self.config.actions.lock.command = value,
FieldKey::MonitorCommand => self.config.actions.monitor.command = value,
FieldKey::NotifyMessage => self.config.actions.notify.message = value,
_ => {} _ => {}
}, },
FieldKind::Int => { FieldKind::Int => {
@@ -1359,7 +1651,9 @@ impl App {
.as_ref() .as_ref()
.is_some_and(|timer| timer.handle.is_finished()) .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()); self.apply_timer_result(timer.handle.join());
} }
} }
@@ -1367,16 +1661,25 @@ impl App {
fn apply_timer_result(&mut self, result: thread::Result<Result<TimerReport>>) { fn apply_timer_result(&mut self, result: thread::Result<Result<TimerReport>>) {
match result { match result {
Ok(Ok(report)) => { Ok(Ok(report)) => {
if let Err(err) = append_history( let status = if report.failures.is_empty() {
&history_path(&self.path), "finished"
report.to_history("finished", None), } else {
) { "partial"
};
if let Err(err) =
append_history(&history_path(&self.path), report.to_history(status, None))
{
self.status = format!("history failed: {err}"); self.status = format!("history failed: {err}");
} else if !report.failures.is_empty() {
self.status = format!(
"timer finished ({} action(s) failed)",
report.failures.len()
);
} else { } else {
self.status = "timer finished".to_string(); 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() self.status = "timer stopped".to_string()
} }
Ok(Err(err)) => self.status = format!("timer failed: {err}"), Ok(Err(err)) => self.status = format!("timer failed: {err}"),
@@ -1401,16 +1704,25 @@ impl App {
fn field_value(&self, field: &Field) -> String { fn field_value(&self, field: &Field) -> String {
match field.key { match field.key {
FieldKey::Duration => self.config.duration.clone(), FieldKey::Duration => self.config.duration.clone(),
FieldKey::InhibitSleep => on_off(self.config.inhibit_sleep),
FieldKey::WorkspaceEnabled => on_off(self.config.actions.workspace.enabled), FieldKey::WorkspaceEnabled => on_off(self.config.actions.workspace.enabled),
FieldKey::WorkspaceBackend => self.config.actions.workspace.backend.clone(), FieldKey::WorkspaceBackend => self.config.actions.workspace.backend.clone(),
FieldKey::WorkspaceNumber => self.config.actions.workspace.number.to_string(), FieldKey::WorkspaceNumber => self.config.actions.workspace.number.to_string(),
FieldKey::WorkspaceCommand => self.config.actions.workspace.command.clone(),
FieldKey::MediaEnabled => on_off(self.config.actions.media.enabled), FieldKey::MediaEnabled => on_off(self.config.actions.media.enabled),
FieldKey::MediaAction => self.config.actions.media.action.clone(), FieldKey::MediaAction => self.config.actions.media.action.clone(),
FieldKey::BrightnessEnabled => on_off(self.config.actions.brightness.enabled), FieldKey::BrightnessEnabled => on_off(self.config.actions.brightness.enabled),
FieldKey::BrightnessValue => self.config.actions.brightness.value.to_string(), FieldKey::BrightnessValue => self.config.actions.brightness.value.to_string(),
FieldKey::MuteEnabled => on_off(self.config.actions.mute.enabled), FieldKey::MuteEnabled => on_off(self.config.actions.mute.enabled),
FieldKey::MuteInputEnabled => on_off(self.config.actions.mute_input.enabled),
FieldKey::LockEnabled => on_off(self.config.actions.lock.enabled), FieldKey::LockEnabled => on_off(self.config.actions.lock.enabled),
FieldKey::LockBackend => self.config.actions.lock.backend.clone(),
FieldKey::LockCommand => self.config.actions.lock.command.clone(), FieldKey::LockCommand => self.config.actions.lock.command.clone(),
FieldKey::MonitorEnabled => on_off(self.config.actions.monitor.enabled),
FieldKey::MonitorBackend => self.config.actions.monitor.backend.clone(),
FieldKey::MonitorCommand => self.config.actions.monitor.command.clone(),
FieldKey::NotifyEnabled => on_off(self.config.actions.notify.enabled),
FieldKey::NotifyMessage => self.config.actions.notify.message.clone(),
FieldKey::KillEnabled => on_off(self.config.actions.kill.enabled), FieldKey::KillEnabled => on_off(self.config.actions.kill.enabled),
FieldKey::KillProcesses => self.config.actions.kill.processes.join(","), FieldKey::KillProcesses => self.config.actions.kill.processes.join(","),
FieldKey::PowerMode => self.config.actions.power.mode.clone(), FieldKey::PowerMode => self.config.actions.power.mode.clone(),
@@ -1421,6 +1733,7 @@ impl App {
fn toggle_bool(&mut self, key: FieldKey) { fn toggle_bool(&mut self, key: FieldKey) {
match key { match key {
FieldKey::InhibitSleep => self.config.inhibit_sleep = !self.config.inhibit_sleep,
FieldKey::WorkspaceEnabled => { FieldKey::WorkspaceEnabled => {
self.config.actions.workspace.enabled = !self.config.actions.workspace.enabled self.config.actions.workspace.enabled = !self.config.actions.workspace.enabled
} }
@@ -1433,12 +1746,21 @@ impl App {
FieldKey::MuteEnabled => { FieldKey::MuteEnabled => {
self.config.actions.mute.enabled = !self.config.actions.mute.enabled self.config.actions.mute.enabled = !self.config.actions.mute.enabled
} }
FieldKey::MuteInputEnabled => {
self.config.actions.mute_input.enabled = !self.config.actions.mute_input.enabled
}
FieldKey::LockEnabled => { FieldKey::LockEnabled => {
self.config.actions.lock.enabled = !self.config.actions.lock.enabled self.config.actions.lock.enabled = !self.config.actions.lock.enabled
} }
FieldKey::KillEnabled => { FieldKey::KillEnabled => {
self.config.actions.kill.enabled = !self.config.actions.kill.enabled self.config.actions.kill.enabled = !self.config.actions.kill.enabled
} }
FieldKey::MonitorEnabled => {
self.config.actions.monitor.enabled = !self.config.actions.monitor.enabled
}
FieldKey::NotifyEnabled => {
self.config.actions.notify.enabled = !self.config.actions.notify.enabled
}
FieldKey::CustomEnabled => { FieldKey::CustomEnabled => {
self.config.actions.custom.enabled = !self.config.actions.custom.enabled self.config.actions.custom.enabled = !self.config.actions.custom.enabled
} }
@@ -1451,6 +1773,8 @@ impl App {
FieldKey::WorkspaceBackend => &mut self.config.actions.workspace.backend, FieldKey::WorkspaceBackend => &mut self.config.actions.workspace.backend,
FieldKey::MediaAction => &mut self.config.actions.media.action, FieldKey::MediaAction => &mut self.config.actions.media.action,
FieldKey::PowerMode => &mut self.config.actions.power.mode, FieldKey::PowerMode => &mut self.config.actions.power.mode,
FieldKey::MonitorBackend => &mut self.config.actions.monitor.backend,
FieldKey::LockBackend => &mut self.config.actions.lock.backend,
_ => return, _ => return,
}; };
let index = options let index = options
@@ -1489,16 +1813,25 @@ enum FieldKind {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FieldKey { enum FieldKey {
Duration, Duration,
InhibitSleep,
WorkspaceEnabled, WorkspaceEnabled,
WorkspaceBackend, WorkspaceBackend,
WorkspaceNumber, WorkspaceNumber,
WorkspaceCommand,
MediaEnabled, MediaEnabled,
MediaAction, MediaAction,
BrightnessEnabled, BrightnessEnabled,
BrightnessValue, BrightnessValue,
MuteEnabled, MuteEnabled,
MuteInputEnabled,
LockEnabled, LockEnabled,
LockBackend,
LockCommand, LockCommand,
MonitorEnabled,
MonitorBackend,
MonitorCommand,
NotifyEnabled,
NotifyMessage,
KillEnabled, KillEnabled,
KillProcesses, KillProcesses,
PowerMode, PowerMode,
@@ -1512,6 +1845,11 @@ const FIELDS: &[Field] = &[
key: FieldKey::Duration, key: FieldKey::Duration,
kind: FieldKind::Edit, kind: FieldKind::Edit,
}, },
Field {
label: "inhibit sleep",
key: FieldKey::InhibitSleep,
kind: FieldKind::Bool,
},
Field { Field {
label: "workspace enabled", label: "workspace enabled",
key: FieldKey::WorkspaceEnabled, key: FieldKey::WorkspaceEnabled,
@@ -1520,13 +1858,18 @@ const FIELDS: &[Field] = &[
Field { Field {
label: "workspace backend", label: "workspace backend",
key: FieldKey::WorkspaceBackend, key: FieldKey::WorkspaceBackend,
kind: FieldKind::Cycle(&["auto", "niri", "hyprland", "kde"]), kind: FieldKind::Cycle(&["auto", "niri", "hyprland", "sway", "i3", "kde"]),
}, },
Field { Field {
label: "workspace number", label: "workspace number",
key: FieldKey::WorkspaceNumber, key: FieldKey::WorkspaceNumber,
kind: FieldKind::Int, kind: FieldKind::Int,
}, },
Field {
label: "workspace command",
key: FieldKey::WorkspaceCommand,
kind: FieldKind::Edit,
},
Field { Field {
label: "media enabled", label: "media enabled",
key: FieldKey::MediaEnabled, key: FieldKey::MediaEnabled,
@@ -1552,16 +1895,60 @@ const FIELDS: &[Field] = &[
key: FieldKey::MuteEnabled, key: FieldKey::MuteEnabled,
kind: FieldKind::Bool, kind: FieldKind::Bool,
}, },
Field {
label: "mute input enabled",
key: FieldKey::MuteInputEnabled,
kind: FieldKind::Bool,
},
Field { Field {
label: "lock enabled", label: "lock enabled",
key: FieldKey::LockEnabled, key: FieldKey::LockEnabled,
kind: FieldKind::Bool, kind: FieldKind::Bool,
}, },
Field {
label: "lock backend",
key: FieldKey::LockBackend,
kind: FieldKind::Cycle(&[
"auto",
"loginctl",
"hyprlock",
"swaylock",
"i3lock",
"gnome",
"kde",
"xscreensaver",
]),
},
Field { Field {
label: "lock command", label: "lock command",
key: FieldKey::LockCommand, key: FieldKey::LockCommand,
kind: FieldKind::Edit, kind: FieldKind::Edit,
}, },
Field {
label: "monitor enabled",
key: FieldKey::MonitorEnabled,
kind: FieldKind::Bool,
},
Field {
label: "monitor backend",
key: FieldKey::MonitorBackend,
kind: FieldKind::Cycle(&["auto", "hyprland", "niri", "sway", "kde", "gnome", "x11"]),
},
Field {
label: "monitor command",
key: FieldKey::MonitorCommand,
kind: FieldKind::Edit,
},
Field {
label: "notify enabled",
key: FieldKey::NotifyEnabled,
kind: FieldKind::Bool,
},
Field {
label: "notify message",
key: FieldKey::NotifyMessage,
kind: FieldKind::Edit,
},
Field { Field {
label: "kill enabled", label: "kill enabled",
key: FieldKey::KillEnabled, key: FieldKey::KillEnabled,
@@ -1575,7 +1962,14 @@ const FIELDS: &[Field] = &[
Field { Field {
label: "power mode", label: "power mode",
key: FieldKey::PowerMode, key: FieldKey::PowerMode,
kind: FieldKind::Cycle(&["none", "poweroff", "reboot"]), kind: FieldKind::Cycle(&[
"none",
"suspend",
"hibernate",
"hybrid-sleep",
"poweroff",
"reboot",
]),
}, },
Field { Field {
label: "custom enabled", label: "custom enabled",
@@ -1598,18 +1992,26 @@ fn visible_fields(config: &Config) -> Vec<&'static Field> {
fn field_visible(config: &Config, key: FieldKey) -> bool { fn field_visible(config: &Config, key: FieldKey) -> bool {
match key { match key {
FieldKey::WorkspaceBackend | FieldKey::WorkspaceNumber => config.actions.workspace.enabled, FieldKey::WorkspaceBackend | FieldKey::WorkspaceNumber | FieldKey::WorkspaceCommand => {
config.actions.workspace.enabled
}
FieldKey::MediaAction => config.actions.media.enabled, FieldKey::MediaAction => config.actions.media.enabled,
FieldKey::BrightnessValue => config.actions.brightness.enabled, FieldKey::BrightnessValue => config.actions.brightness.enabled,
FieldKey::LockCommand => config.actions.lock.enabled, FieldKey::LockBackend | FieldKey::LockCommand => config.actions.lock.enabled,
FieldKey::MonitorBackend | FieldKey::MonitorCommand => config.actions.monitor.enabled,
FieldKey::NotifyMessage => config.actions.notify.enabled,
FieldKey::KillProcesses => config.actions.kill.enabled, FieldKey::KillProcesses => config.actions.kill.enabled,
FieldKey::CustomCommands => config.actions.custom.enabled, FieldKey::CustomCommands => config.actions.custom.enabled,
FieldKey::Duration FieldKey::Duration
| FieldKey::InhibitSleep
| FieldKey::WorkspaceEnabled | FieldKey::WorkspaceEnabled
| FieldKey::MediaEnabled | FieldKey::MediaEnabled
| FieldKey::BrightnessEnabled | FieldKey::BrightnessEnabled
| FieldKey::MuteEnabled | FieldKey::MuteEnabled
| FieldKey::MuteInputEnabled
| FieldKey::LockEnabled | FieldKey::LockEnabled
| FieldKey::MonitorEnabled
| FieldKey::NotifyEnabled
| FieldKey::KillEnabled | FieldKey::KillEnabled
| FieldKey::PowerMode | FieldKey::PowerMode
| FieldKey::CustomEnabled => true, | FieldKey::CustomEnabled => true,
@@ -1851,7 +2253,10 @@ mod tests {
config.actions.media.enabled = false; config.actions.media.enabled = false;
config.actions.brightness.enabled = false; config.actions.brightness.enabled = false;
config.actions.mute.enabled = false; config.actions.mute.enabled = false;
config.actions.mute_input.enabled = false;
config.actions.lock.enabled = false; config.actions.lock.enabled = false;
config.actions.monitor.enabled = false;
config.actions.notify.enabled = false;
config.actions.kill.enabled = false; config.actions.kill.enabled = false;
config.actions.power.mode = "none".to_string(); config.actions.power.mode = "none".to_string();
config.actions.custom.enabled = false; config.actions.custom.enabled = false;
@@ -1871,6 +2276,7 @@ mod tests {
fn preview_matches_expected_commands() { fn preview_matches_expected_commands() {
let mut config = Config::default(); let mut config = Config::default();
config.actions.lock.enabled = true; config.actions.lock.enabled = true;
config.actions.lock.backend = "loginctl".to_string();
config.actions.kill.enabled = true; config.actions.kill.enabled = true;
config.actions.kill.processes = vec!["firefox".to_string(), "telegram-desktop".to_string()]; config.actions.kill.processes = vec!["firefox".to_string(), "telegram-desktop".to_string()];
config.actions.custom.enabled = true; config.actions.custom.enabled = true;
@@ -1951,7 +2357,7 @@ mod tests {
config.actions.custom.commands = vec!["false".to_string(), "true".to_string()]; config.actions.custom.commands = vec!["false".to_string(), "true".to_string()];
let mut output = Vec::new(); let mut output = Vec::new();
let error = run_timer( let report = run_timer(
&config, &config,
&stop, &stop,
&Arc::new(AtomicBool::new(false)), &Arc::new(AtomicBool::new(false)),
@@ -1960,12 +2366,13 @@ mod tests {
TimerOptions::default(), TimerOptions::default(),
Some(&mut output), Some(&mut output),
) )
.unwrap_err(); .unwrap();
let text = String::from_utf8(output).unwrap(); let text = String::from_utf8(output).unwrap();
assert!(text.contains("run: false")); assert!(text.contains("run: false"));
assert!(text.contains("run: true")); 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] #[test]
@@ -1977,7 +2384,7 @@ mod tests {
)); ));
fs::write( fs::write(
&path, &path,
"duration: 25m\nactions:\n power:\n mode: suspend\n", "duration: 25m\nactions:\n power:\n mode: bogus\n",
) )
.unwrap(); .unwrap();
@@ -2019,12 +2426,20 @@ mod tests {
let keys = visible_field_keys(&config); let keys = visible_field_keys(&config);
assert!(keys.contains(&FieldKey::Duration)); assert!(keys.contains(&FieldKey::Duration));
assert!(keys.contains(&FieldKey::InhibitSleep));
assert!(keys.contains(&FieldKey::MediaEnabled)); assert!(keys.contains(&FieldKey::MediaEnabled));
assert!(keys.contains(&FieldKey::MonitorEnabled));
assert!(keys.contains(&FieldKey::NotifyEnabled));
assert!(!keys.contains(&FieldKey::MediaAction)); assert!(!keys.contains(&FieldKey::MediaAction));
assert!(!keys.contains(&FieldKey::WorkspaceBackend)); assert!(!keys.contains(&FieldKey::WorkspaceBackend));
assert!(!keys.contains(&FieldKey::WorkspaceNumber)); assert!(!keys.contains(&FieldKey::WorkspaceNumber));
assert!(!keys.contains(&FieldKey::WorkspaceCommand));
assert!(!keys.contains(&FieldKey::BrightnessValue)); assert!(!keys.contains(&FieldKey::BrightnessValue));
assert!(!keys.contains(&FieldKey::LockBackend));
assert!(!keys.contains(&FieldKey::LockCommand)); assert!(!keys.contains(&FieldKey::LockCommand));
assert!(!keys.contains(&FieldKey::MonitorBackend));
assert!(!keys.contains(&FieldKey::MonitorCommand));
assert!(!keys.contains(&FieldKey::NotifyMessage));
assert!(!keys.contains(&FieldKey::KillProcesses)); assert!(!keys.contains(&FieldKey::KillProcesses));
assert!(!keys.contains(&FieldKey::CustomCommands)); assert!(!keys.contains(&FieldKey::CustomCommands));
@@ -2032,6 +2447,8 @@ mod tests {
config.actions.workspace.enabled = true; config.actions.workspace.enabled = true;
config.actions.brightness.enabled = true; config.actions.brightness.enabled = true;
config.actions.lock.enabled = true; config.actions.lock.enabled = true;
config.actions.monitor.enabled = true;
config.actions.notify.enabled = true;
config.actions.kill.enabled = true; config.actions.kill.enabled = true;
config.actions.custom.enabled = true; config.actions.custom.enabled = true;
@@ -2039,8 +2456,13 @@ mod tests {
assert!(keys.contains(&FieldKey::MediaAction)); assert!(keys.contains(&FieldKey::MediaAction));
assert!(keys.contains(&FieldKey::WorkspaceBackend)); assert!(keys.contains(&FieldKey::WorkspaceBackend));
assert!(keys.contains(&FieldKey::WorkspaceNumber)); assert!(keys.contains(&FieldKey::WorkspaceNumber));
assert!(keys.contains(&FieldKey::WorkspaceCommand));
assert!(keys.contains(&FieldKey::BrightnessValue)); assert!(keys.contains(&FieldKey::BrightnessValue));
assert!(keys.contains(&FieldKey::LockBackend));
assert!(keys.contains(&FieldKey::LockCommand)); assert!(keys.contains(&FieldKey::LockCommand));
assert!(keys.contains(&FieldKey::MonitorBackend));
assert!(keys.contains(&FieldKey::MonitorCommand));
assert!(keys.contains(&FieldKey::NotifyMessage));
assert!(keys.contains(&FieldKey::KillProcesses)); assert!(keys.contains(&FieldKey::KillProcesses));
assert!(keys.contains(&FieldKey::CustomCommands)); assert!(keys.contains(&FieldKey::CustomCommands));
} }