Compare commits
4 Commits
main
...
d0841d2c9b
| Author | SHA1 | Date | |
|---|---|---|---|
| d0841d2c9b | |||
| af2deabca4 | |||
| 1edb82a057 | |||
| 1a77043a71 |
@@ -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
|
||||||
|
|||||||
@@ -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/*
|
||||||
|
|||||||
@@ -4,6 +4,28 @@ 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
|
## [1.2.2] - 2026-07-05
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -55,6 +77,7 @@ 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.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
|
||||||
|
|||||||
Generated
+3
-3
@@ -537,7 +537,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gosleep-timer"
|
name = "gosleep-timer"
|
||||||
version = "1.2.2"
|
version = "1.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -807,9 +807,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noyalib"
|
name = "noyalib"
|
||||||
version = "0.0.12"
|
version = "0.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "903f06e76b14b388b47d651b9498b008888a9475084087e51c294731acbf16c1"
|
checksum = "2095d0b3b6017ebbdaffedf83ffb2bff7e468ab379ac391239abea4880833290"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
|||||||
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gosleep-timer"
|
name = "gosleep-timer"
|
||||||
version = "1.2.2"
|
version = "1.3.1"
|
||||||
|
authors = ["forust"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
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"]
|
||||||
@@ -20,7 +22,7 @@ clap = { version = "4.5", features = ["derive"] }
|
|||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
ratatui = "0.30.2"
|
ratatui = "0.30.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
noyalib = "0.0.12"
|
noyalib = "0.0.13"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
@@ -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
@@ -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`
|
||||||
|
|
||||||
|
|||||||
+469
-47
@@ -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);
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user