1use std::cell::OnceCell;
69
70use super::prelude::*;
71use crate::formatting::prefix::Prefix;
72use nix::sys::statvfs::statvfs;
73use tokio::process::Command;
74
75#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
76#[serde(rename_all = "lowercase")]
77pub enum InfoType {
78 #[default]
79 Available,
80 Free,
81 Used,
82}
83
84#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
85#[serde(rename_all = "lowercase")]
86pub enum Backend {
87 #[default]
88 Vfs,
89 Btrfs,
90}
91
92#[derive(Deserialize, Debug, SmartDefault)]
93#[serde(default)]
94pub struct Config {
95 #[default("/".into())]
96 pub path: ShellString,
97 pub backend: Backend,
98 pub info_type: InfoType,
99 #[serde(flatten)]
100 pub formats: MaybeMultiFormatConfig,
101 pub alert_unit: Option<String>,
102 #[default(20.into())]
103 pub interval: Seconds,
104 #[default(20.0)]
105 pub warning: f64,
106 #[default(10.0)]
107 pub alert: f64,
108}
109
110pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
111 let mut actions = api.get_actions()?;
112 api.set_default_actions(&[
113 (MouseButton::Left, None, "next_format"),
114 (MouseButton::Right, None, "prev_format"),
115 ])?;
116
117 let mut formats = config.formats.with_default(" $icon $available ")?;
118
119 let unit = match config.alert_unit.as_deref() {
120 Some("TB") => Some(Prefix::Tera),
122 Some("GB") => Some(Prefix::Giga),
123 Some("MB") => Some(Prefix::Mega),
124 Some("KB") | Some("kB") => Some(Prefix::Kilo),
125 Some("TiB") => Some(Prefix::Tebi),
127 Some("GiB") => Some(Prefix::Gibi),
128 Some("MiB") => Some(Prefix::Mebi),
129 Some("KiB") => Some(Prefix::Kibi),
130 Some("B") => Some(Prefix::One),
132 Some(x) => return Err(Error::new(format!("Unknown unit: '{x}'"))),
134 None => None,
135 };
136
137 let path = config.path.expand()?;
138
139 let mut timer = config.interval.timer();
140
141 loop {
142 let mut widget = Widget::new().with_format(formats.get_format());
143
144 let (total, used, available, free) = match config.backend {
145 Backend::Vfs => get_vfs(&*path)?,
146 Backend::Btrfs => get_btrfs(&path).await?,
147 };
148
149 let result = match config.info_type {
150 InfoType::Available => available,
151 InfoType::Free => free,
152 InfoType::Used => used,
153 } as f64;
154
155 let percentage = result / (total as f64) * 100.;
156 widget.set_values(map! {
157 "icon" => Value::icon("disk_drive"),
158 "path" => Value::text(path.to_string()),
159 "percentage" => Value::percents(percentage),
160 "total" => Value::bytes(total as f64),
161 "used" => Value::bytes(used as f64),
162 "available" => Value::bytes(available as f64),
163 "free" => Value::bytes(free as f64),
164 });
165
166 let alert_val_in_config_units = match unit {
168 Some(p) => p.apply(result),
169 None => percentage,
170 };
171
172 widget.state = match config.info_type {
174 InfoType::Used => {
175 if alert_val_in_config_units >= config.alert {
176 State::Critical
177 } else if alert_val_in_config_units >= config.warning {
178 State::Warning
179 } else {
180 State::Idle
181 }
182 }
183 InfoType::Free | InfoType::Available => {
184 if alert_val_in_config_units <= config.alert {
185 State::Critical
186 } else if alert_val_in_config_units <= config.warning {
187 State::Warning
188 } else {
189 State::Idle
190 }
191 }
192 };
193
194 api.set_widget(widget)?;
195
196 loop {
197 select! {
198 _ = timer.tick() => break,
199 _ = api.wait_for_update_request() => break,
200 Some(action) = actions.recv() => match action.as_ref() {
201 "next_format" | "toggle_format" => {
202 formats.next_format();
203 break;
204 }
205 "prev_format" => {
206 formats.prev_format();
207 break;
208 }
209 _ => (),
210 }
211 }
212 }
213 }
214}
215
216fn get_vfs<P>(path: &P) -> Result<(u64, u64, u64, u64)>
217where
218 P: ?Sized + nix::NixPath,
219{
220 let statvfs = statvfs(path).error("failed to retrieve statvfs")?;
221
222 #[allow(clippy::unnecessary_cast)]
224 {
225 let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64);
226 let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64))
227 * (statvfs.fragment_size() as u64);
228 let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64);
229 let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64);
230
231 Ok((total, used, available, free))
232 }
233}
234
235async fn get_btrfs(path: &str) -> Result<(u64, u64, u64, u64)> {
236 const OUTPUT_CHANGED: &str = "Btrfs filesystem usage output format changed";
237
238 fn remove_estimate_min(estimate_str: &str) -> Result<&str> {
239 estimate_str
240 .trim_matches('\t')
241 .split_once("\t")
242 .ok_or(Error::new(OUTPUT_CHANGED))
243 .map(|v| v.0)
244 }
245
246 macro_rules! get {
247 ($source:expr, $name:expr, $variable:ident) => {
248 get!(@pre_op (|a| {Ok::<_, Error>(a)}), $source, $name, $variable)
249 };
250 (@pre_op $function:expr, $source:expr, $name:expr, $variable:ident) => {
251 if $source.starts_with(concat!($name, ":")) {
252 let (found_name, variable_str) =
253 $source.split_once(":").ok_or(Error::new(OUTPUT_CHANGED))?;
254
255 let variable_str = $function(variable_str)?;
256
257 debug_assert_eq!(found_name, $name);
258 $variable
259 .set(variable_str.trim().parse().error(OUTPUT_CHANGED)?)
260 .map_err(|_| Error::new(OUTPUT_CHANGED))?;
261 }
262 };
263 }
264
265 let filesystem_usage = Command::new("btrfs")
266 .args(["filesystem", "usage", "--raw", path])
267 .output()
268 .await
269 .error("Failed to collect btrfs filesystem usage info")?
270 .stdout;
271
272 {
273 let final_total = OnceCell::new();
274 let final_used = OnceCell::new();
275 let final_free = OnceCell::new();
276
277 let mut lines = filesystem_usage.lines();
278 while let Some(line) = lines
279 .next_line()
280 .await
281 .error("Failed to read output of btrfs filesystem usage")?
282 {
283 let line = line.trim();
284
285 get!(line, "Device size", final_total);
287 get!(line, "Used", final_used);
288 get!(@pre_op remove_estimate_min, line, "Free (estimated)", final_free);
289 }
290
291 Ok((
292 *final_total.get().ok_or(Error::new(OUTPUT_CHANGED))?,
293 *final_used.get().ok_or(Error::new(OUTPUT_CHANGED))?,
294 *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
297 *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
298 ))
299 }
300}