1use std::cell::OnceCell;
68
69use super::prelude::*;
70use crate::formatting::prefix::Prefix;
71use nix::sys::statvfs::statvfs;
72use tokio::process::Command;
73
74#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
75#[serde(rename_all = "lowercase")]
76pub enum InfoType {
77 #[default]
78 Available,
79 Free,
80 Used,
81}
82
83#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
84#[serde(rename_all = "lowercase")]
85pub enum Backend {
86 #[default]
87 Vfs,
88 Btrfs,
89}
90
91#[derive(Deserialize, Debug, SmartDefault)]
92#[serde(deny_unknown_fields, default)]
93pub struct Config {
94 #[default("/".into())]
95 pub path: ShellString,
96 pub backend: Backend,
97 pub info_type: InfoType,
98 pub format: FormatConfig,
99 pub format_alt: Option<FormatConfig>,
100 pub alert_unit: Option<String>,
101 #[default(20.into())]
102 pub interval: Seconds,
103 #[default(20.0)]
104 pub warning: f64,
105 #[default(10.0)]
106 pub alert: f64,
107}
108
109pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
110 let mut actions = api.get_actions()?;
111 api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
112
113 let mut format = config.format.with_default(" $icon $available ")?;
114 let mut format_alt = match &config.format_alt {
115 Some(f) => Some(f.with_default("")?),
116 None => None,
117 };
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(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(format.clone());
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 "toggle_format" => {
202 if let Some(format_alt) = &mut format_alt {
203 std::mem::swap(format_alt, &mut format);
204 break;
205 }
206 }
207 _ => (),
208 }
209 }
210 }
211 }
212}
213
214fn get_vfs<P>(path: &P) -> Result<(u64, u64, u64, u64)>
215where
216 P: ?Sized + nix::NixPath,
217{
218 let statvfs = statvfs(path).error("failed to retrieve statvfs")?;
219
220 #[allow(clippy::unnecessary_cast)]
222 {
223 let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64);
224 let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64))
225 * (statvfs.fragment_size() as u64);
226 let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64);
227 let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64);
228
229 Ok((total, used, available, free))
230 }
231}
232
233async fn get_btrfs(path: &str) -> Result<(u64, u64, u64, u64)> {
234 const OUTPUT_CHANGED: &str = "Btrfs filesystem usage output format changed";
235
236 fn remove_estimate_min(estimate_str: &str) -> Result<&str> {
237 estimate_str
238 .trim_matches('\t')
239 .split_once("\t")
240 .ok_or(Error::new(OUTPUT_CHANGED))
241 .map(|v| v.0)
242 }
243
244 macro_rules! get {
245 ($source:expr, $name:expr, $variable:ident) => {
246 get!(@pre_op (|a| {Ok::<_, Error>(a)}), $source, $name, $variable)
247 };
248 (@pre_op $function:expr, $source:expr, $name:expr, $variable:ident) => {
249 if $source.starts_with(concat!($name, ":")) {
250 let (found_name, variable_str) =
251 $source.split_once(":").ok_or(Error::new(OUTPUT_CHANGED))?;
252
253 let variable_str = $function(variable_str)?;
254
255 debug_assert_eq!(found_name, $name);
256 $variable
257 .set(variable_str.trim().parse().error(OUTPUT_CHANGED)?)
258 .map_err(|_| Error::new(OUTPUT_CHANGED))?;
259 }
260 };
261 }
262
263 let filesystem_usage = Command::new("btrfs")
264 .args(["filesystem", "usage", "--raw", path])
265 .output()
266 .await
267 .error("Failed to collect btrfs filesystem usage info")?
268 .stdout;
269
270 {
271 let final_total = OnceCell::new();
272 let final_used = OnceCell::new();
273 let final_free = OnceCell::new();
274
275 let mut lines = filesystem_usage.lines();
276 while let Some(line) = lines
277 .next_line()
278 .await
279 .error("Failed to read output of btrfs filesystem usage")?
280 {
281 let line = line.trim();
282
283 get!(line, "Device size", final_total);
285 get!(line, "Used", final_used);
286 get!(@pre_op remove_estimate_min, line, "Free (estimated)", final_free);
287 }
288
289 Ok((
290 *final_total.get().ok_or(Error::new(OUTPUT_CHANGED))?,
291 *final_used.get().ok_or(Error::new(OUTPUT_CHANGED))?,
292 *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
295 *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
296 ))
297 }
298}