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.
This commit is contained in:
+134
-4
@@ -75,6 +75,7 @@ struct Actions {
|
|||||||
brightness: BrightnessAction,
|
brightness: BrightnessAction,
|
||||||
mute: ToggleAction,
|
mute: ToggleAction,
|
||||||
lock: LockAction,
|
lock: LockAction,
|
||||||
|
monitor: MonitorAction,
|
||||||
kill: KillAction,
|
kill: KillAction,
|
||||||
power: PowerAction,
|
power: PowerAction,
|
||||||
custom: CustomAction,
|
custom: CustomAction,
|
||||||
@@ -115,6 +116,13 @@ struct LockAction {
|
|||||||
command: String,
|
command: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
struct MonitorAction {
|
||||||
|
enabled: bool,
|
||||||
|
backend: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
struct KillAction {
|
struct KillAction {
|
||||||
@@ -216,6 +224,15 @@ impl Default for LockAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for MonitorAction {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
backend: "auto".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for PowerAction {
|
impl Default for PowerAction {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -533,6 +550,9 @@ fn normalize_config(config: &mut Config) {
|
|||||||
if config.actions.lock.command.is_empty() {
|
if config.actions.lock.command.is_empty() {
|
||||||
config.actions.lock.command = "loginctl lock-session".to_string();
|
config.actions.lock.command = "loginctl lock-session".to_string();
|
||||||
}
|
}
|
||||||
|
if config.actions.monitor.backend.is_empty() {
|
||||||
|
config.actions.monitor.backend = "auto".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();
|
||||||
}
|
}
|
||||||
@@ -545,9 +565,13 @@ fn validate_config(config: &Config) -> Result<()> {
|
|||||||
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.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:?}"),
|
||||||
@@ -636,6 +660,9 @@ fn build_action_commands(config: &Config) -> Vec<ActionCommand> {
|
|||||||
shell: Some(actions.lock.command.clone()),
|
shell: Some(actions.lock.command.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if actions.monitor.enabled {
|
||||||
|
commands.push(monitor_command(&actions.monitor));
|
||||||
|
}
|
||||||
if actions.kill.enabled {
|
if actions.kill.enabled {
|
||||||
for process in &actions.kill.processes {
|
for process in &actions.kill.processes {
|
||||||
let process = process.trim();
|
let process = process.trim();
|
||||||
@@ -695,6 +722,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![
|
||||||
@@ -713,13 +756,73 @@ 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 {
|
||||||
|
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 preview_commands(config: &Config) -> Vec<String> {
|
fn preview_commands(config: &Config) -> Vec<String> {
|
||||||
build_action_commands(config)
|
build_action_commands(config)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -843,7 +946,9 @@ 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() && let Some(output) = &mut output {
|
if !failures.is_empty()
|
||||||
|
&& let Some(output) = &mut output
|
||||||
|
{
|
||||||
write_timer_line(output, &format!("{} action(s) failed", failures.len()))?;
|
write_timer_line(output, &format!("{} action(s) failed", failures.len()))?;
|
||||||
}
|
}
|
||||||
Ok(report)
|
Ok(report)
|
||||||
@@ -1441,6 +1546,8 @@ impl App {
|
|||||||
FieldKey::MuteEnabled => on_off(self.config.actions.mute.enabled),
|
FieldKey::MuteEnabled => on_off(self.config.actions.mute.enabled),
|
||||||
FieldKey::LockEnabled => on_off(self.config.actions.lock.enabled),
|
FieldKey::LockEnabled => on_off(self.config.actions.lock.enabled),
|
||||||
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::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(),
|
||||||
@@ -1469,6 +1576,9 @@ impl App {
|
|||||||
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::CustomEnabled => {
|
FieldKey::CustomEnabled => {
|
||||||
self.config.actions.custom.enabled = !self.config.actions.custom.enabled
|
self.config.actions.custom.enabled = !self.config.actions.custom.enabled
|
||||||
}
|
}
|
||||||
@@ -1481,6 +1591,7 @@ 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,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let index = options
|
let index = options
|
||||||
@@ -1529,6 +1640,8 @@ enum FieldKey {
|
|||||||
MuteEnabled,
|
MuteEnabled,
|
||||||
LockEnabled,
|
LockEnabled,
|
||||||
LockCommand,
|
LockCommand,
|
||||||
|
MonitorEnabled,
|
||||||
|
MonitorBackend,
|
||||||
KillEnabled,
|
KillEnabled,
|
||||||
KillProcesses,
|
KillProcesses,
|
||||||
PowerMode,
|
PowerMode,
|
||||||
@@ -1550,7 +1663,7 @@ 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",
|
||||||
@@ -1592,6 +1705,16 @@ const FIELDS: &[Field] = &[
|
|||||||
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 {
|
Field {
|
||||||
label: "kill enabled",
|
label: "kill enabled",
|
||||||
key: FieldKey::KillEnabled,
|
key: FieldKey::KillEnabled,
|
||||||
@@ -1632,6 +1755,7 @@ fn field_visible(config: &Config, key: FieldKey) -> bool {
|
|||||||
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::LockCommand => config.actions.lock.enabled,
|
||||||
|
FieldKey::MonitorBackend => config.actions.monitor.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
|
||||||
@@ -1640,6 +1764,7 @@ fn field_visible(config: &Config, key: FieldKey) -> bool {
|
|||||||
| FieldKey::BrightnessEnabled
|
| FieldKey::BrightnessEnabled
|
||||||
| FieldKey::MuteEnabled
|
| FieldKey::MuteEnabled
|
||||||
| FieldKey::LockEnabled
|
| FieldKey::LockEnabled
|
||||||
|
| FieldKey::MonitorEnabled
|
||||||
| FieldKey::KillEnabled
|
| FieldKey::KillEnabled
|
||||||
| FieldKey::PowerMode
|
| FieldKey::PowerMode
|
||||||
| FieldKey::CustomEnabled => true,
|
| FieldKey::CustomEnabled => true,
|
||||||
@@ -1882,6 +2007,7 @@ mod tests {
|
|||||||
config.actions.brightness.enabled = false;
|
config.actions.brightness.enabled = false;
|
||||||
config.actions.mute.enabled = false;
|
config.actions.mute.enabled = false;
|
||||||
config.actions.lock.enabled = false;
|
config.actions.lock.enabled = false;
|
||||||
|
config.actions.monitor.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;
|
||||||
@@ -2051,11 +2177,13 @@ 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::MediaEnabled));
|
assert!(keys.contains(&FieldKey::MediaEnabled));
|
||||||
|
assert!(keys.contains(&FieldKey::MonitorEnabled));
|
||||||
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::BrightnessValue));
|
assert!(!keys.contains(&FieldKey::BrightnessValue));
|
||||||
assert!(!keys.contains(&FieldKey::LockCommand));
|
assert!(!keys.contains(&FieldKey::LockCommand));
|
||||||
|
assert!(!keys.contains(&FieldKey::MonitorBackend));
|
||||||
assert!(!keys.contains(&FieldKey::KillProcesses));
|
assert!(!keys.contains(&FieldKey::KillProcesses));
|
||||||
assert!(!keys.contains(&FieldKey::CustomCommands));
|
assert!(!keys.contains(&FieldKey::CustomCommands));
|
||||||
|
|
||||||
@@ -2063,6 +2191,7 @@ 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.kill.enabled = true;
|
config.actions.kill.enabled = true;
|
||||||
config.actions.custom.enabled = true;
|
config.actions.custom.enabled = true;
|
||||||
|
|
||||||
@@ -2072,6 +2201,7 @@ mod tests {
|
|||||||
assert!(keys.contains(&FieldKey::WorkspaceNumber));
|
assert!(keys.contains(&FieldKey::WorkspaceNumber));
|
||||||
assert!(keys.contains(&FieldKey::BrightnessValue));
|
assert!(keys.contains(&FieldKey::BrightnessValue));
|
||||||
assert!(keys.contains(&FieldKey::LockCommand));
|
assert!(keys.contains(&FieldKey::LockCommand));
|
||||||
|
assert!(keys.contains(&FieldKey::MonitorBackend));
|
||||||
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