1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! A Toggle block
//!
//! You can add commands to be executed to disable the toggle (`command_off`), and to enable it
//! (`command_on`). If these command exit with a non-zero status, the block will not be toggled and
//! the block state will be changed to `critical` to give a visual warning of the failure. You also need to
//! specify a command to determine the state of the toggle (`command_state`). When the command outputs
//! nothing, the toggle is disabled, otherwise enabled. By specifying the interval property you can
//! let the command_state be executed continuously.
//!
//! To run those commands, the shell form `$SHELL` environment variable is used. If such variable
//! is not presented, `sh` is used.
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `format` | A string to customise the output of this block. See below for available placeholders | `" $icon "`
//! `command_on` | Shell command to enable the toggle | **Required**
//! `command_off` | Shell command to disable the toggle | **Required**
//! `command_state` | Shell command to determine the state. Empty output => No, otherwise => Yes. | **Required**
//! `icon_on` | Icon override for the toggle button while on | `"toggle_on"`
//! `icon_off` | Icon override for the toggle button while off | `"toggle_off"`
//! `interval` | Update interval in seconds. If not set, `command_state` will run only on click. | None
//! `state_on` | [`State`] (color) of this block while on | [idle][State::Idle]
//! `state_off` | [`State`] (color) of this block while off | [idle][State::Idle]
//!
//! Placeholder   | Value                                       | Type   | Unit
//! --------------|---------------------------------------------|--------|-----
//! `icon`        | Icon based on toggle's state                | Icon   | -
//!
//! Action   | Default button
//! ---------|---------------
//! `toggle` | Left
//!
//! # Examples
//!
//! This is what can be used to toggle an external monitor configuration:
//!
//! ```toml
//! [[block]]
//! block = "toggle"
//! format = " $icon 4k "
//! command_state = "xrandr | grep 'DP1 connected 38' | grep -v eDP1"
//! command_on = "~/.screenlayout/4kmon_default.sh"
//! command_off = "~/.screenlayout/builtin.sh"
//! interval = 5
//! state_on = "good"
//! state_off = "warning"
//! ```
//!
//! # Icons Used
//! - `toggle_off`
//! - `toggle_on`

use super::prelude::*;
use std::env;
use tokio::process::Command;

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub format: FormatConfig,
    pub command_on: String,
    pub command_off: String,
    pub command_state: String,
    #[serde(default)]
    pub icon_on: Option<String>,
    #[serde(default)]
    pub icon_off: Option<String>,
    #[serde(default)]
    pub interval: Option<u64>,
    pub state_on: Option<State>,
    pub state_off: Option<State>,
}

async fn sleep_opt(dur: Option<Duration>) {
    match dur {
        Some(dur) => tokio::time::sleep(dur).await,
        None => std::future::pending().await,
    }
}

pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
    let mut actions = api.get_actions()?;
    api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;

    let interval = config.interval.map(Duration::from_secs);
    let mut widget = Widget::new().with_format(config.format.with_default(" $icon ")?);

    let icon_on = config.icon_on.as_deref().unwrap_or("toggle_on");
    let icon_off = config.icon_off.as_deref().unwrap_or("toggle_off");

    let shell = env::var("SHELL").unwrap_or_else(|_| "sh".to_string());

    loop {
        // Check state
        let output = Command::new(&shell)
            .args(["-c", &config.command_state])
            .output()
            .await
            .error("Failed to run command_state")?;
        let is_on = !std::str::from_utf8(&output.stdout)
            .error("The output of command_state is invalid UTF-8")?
            .trim()
            .is_empty();

        widget.set_values(map!(
            "icon" => Value::icon(
                if is_on { icon_on.to_string() } else { icon_off.to_string() }
            )
        ));
        if widget.state != State::Critical {
            widget.state = if is_on {
                config.state_on.unwrap_or(State::Idle)
            } else {
                config.state_off.unwrap_or(State::Idle)
            };
        }
        api.set_widget(widget.clone())?;

        loop {
            select! {
                _ = sleep_opt(interval) => break,
                _ = api.wait_for_update_request() => break,
                Some(action) = actions.recv() => match action.as_ref() {
                    "toggle" => {
                        let cmd = if is_on {
                            &config.command_off
                        } else {
                            &config.command_on
                        };
                        let output = Command::new(&shell)
                            .args(["-c", cmd])
                            .output()
                            .await
                            .error("Failed to run command")?;
                        if output.status.success() {
                            // Temporary; it will immediately be updated by the outer loop
                            widget.state = State::Idle;
                            break;
                        } else {
                            widget.state = State::Critical;
                        }
                    }
                    _ => (),
                }
            }
        }
    }
}