1212//! `alert` | A value which will trigger critical block state | `10.0`
1313//! `info_type` | Determines which information will affect the block state. Possible values are `"available"`, `"free"` and `"used"` | `"available"`
1414//! `alert_unit` | The unit of `alert` and `warning` options. If not set, percents are used. Possible values are `"B"`, `"KB"`, `"KiB"`, `"MB"`, `"MiB"`, `"GB"`, `"Gib"`, `"TB"` and `"TiB"` | `None`
15+ //! `backend` | The backend to use when querying disk usage. Possible values are `"vfs"` (like `du(1)`) and `"btrfs"` | `"vfs"`
1516//!
1617//! Placeholder | Value | Type | Unit
1718//! -------------|--------------------------------------------------------------------|--------|-------
6364
6465// make_log_macro!(debug, "disk_space");
6566
67+ use std:: cell:: OnceCell ;
68+
6669use super :: prelude:: * ;
6770use crate :: formatting:: prefix:: Prefix ;
6871use nix:: sys:: statvfs:: statvfs;
72+ use tokio:: process:: Command ;
6973
7074#[ derive( Copy , Clone , Debug , Deserialize , SmartDefault ) ]
7175#[ serde( rename_all = "lowercase" ) ]
@@ -76,11 +80,20 @@ pub enum InfoType {
7680 Used ,
7781}
7882
83+ #[ derive( Copy , Clone , Debug , Deserialize , SmartDefault ) ]
84+ #[ serde( rename_all = "lowercase" ) ]
85+ pub enum Backend {
86+ #[ default]
87+ Vfs ,
88+ Btrfs ,
89+ }
90+
7991#[ derive( Deserialize , Debug , SmartDefault ) ]
8092#[ serde( deny_unknown_fields, default ) ]
8193pub struct Config {
8294 #[ default( "/" . into( ) ) ]
8395 pub path : ShellString ,
96+ pub backend : Backend ,
8497 pub info_type : InfoType ,
8598 pub format : FormatConfig ,
8699 pub format_alt : Option < FormatConfig > ,
@@ -128,17 +141,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
128141 loop {
129142 let mut widget = Widget :: new ( ) . with_format ( format. clone ( ) ) ;
130143
131- let statvfs = statvfs ( & * path) . error ( "failed to retrieve statvfs" ) ?;
132-
133- // Casting to be compatible with 32-bit systems
134- #[ allow( clippy:: unnecessary_cast) ]
135- let ( total, used, available, free) = {
136- let total = ( statvfs. blocks ( ) as u64 ) * ( statvfs. fragment_size ( ) as u64 ) ;
137- let used = ( ( statvfs. blocks ( ) as u64 ) - ( statvfs. blocks_free ( ) as u64 ) )
138- * ( statvfs. fragment_size ( ) as u64 ) ;
139- let available = ( statvfs. blocks_available ( ) as u64 ) * ( statvfs. block_size ( ) as u64 ) ;
140- let free = ( statvfs. blocks_free ( ) as u64 ) * ( statvfs. block_size ( ) as u64 ) ;
141- ( total, used, available, free)
144+ let ( total, used, available, free) = match config. backend {
145+ Backend :: Vfs => get_vfs ( & * path) ?,
146+ Backend :: Btrfs => get_btrfs ( & path) . await ?,
142147 } ;
143148
144149 let result = match config. info_type {
@@ -205,3 +210,88 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
205210 }
206211 }
207212}
213+
214+ fn get_vfs < P > ( path : & P ) -> Result < ( u64 , u64 , u64 , u64 ) >
215+ where
216+ P : ?Sized + nix:: NixPath ,
217+ {
218+ let statvfs = statvfs ( path) . error ( "failed to retrieve statvfs" ) ?;
219+
220+ // Casting to be compatible with 32-bit systems
221+ #[ 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+
233+ async 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. trim_matches ( '\t' )
238+ . split_once ( "\t " )
239+ . ok_or ( Error :: new ( OUTPUT_CHANGED ) )
240+ . map ( |v| v. 0 )
241+ }
242+
243+ macro_rules! get {
244+ ( $source: expr, $name: expr, $variable: ident) => {
245+ get!( @pre_op ( |a| { Ok :: <_, Error >( a) } ) , $source, $name, $variable)
246+ } ;
247+ ( @pre_op $function: expr, $source: expr, $name: expr, $variable: ident) => {
248+ if $source. starts_with( concat!( $name, ":" ) ) {
249+ let ( found_name, variable_str) =
250+ $source. split_once( ":" ) . ok_or( Error :: new( OUTPUT_CHANGED ) ) ?;
251+
252+ let variable_str = $function( variable_str) ?;
253+
254+ debug_assert_eq!( found_name, $name) ;
255+ $variable
256+ . set( variable_str. trim( ) . parse( ) . error( OUTPUT_CHANGED ) ?)
257+ . map_err( |_| Error :: new( OUTPUT_CHANGED ) ) ?;
258+ }
259+ } ;
260+ }
261+
262+ let filesystem_usage = Command :: new ( "btrfs" )
263+ . args ( [ "filesystem" , "usage" , "--raw" , path] )
264+ . output ( )
265+ . await
266+ . error ( "Failed to collect btrfs filesystem usage info" ) ?
267+ . stdout ;
268+
269+ {
270+ let final_total = OnceCell :: new ( ) ;
271+ let final_used = OnceCell :: new ( ) ;
272+ let final_free = OnceCell :: new ( ) ;
273+
274+ let mut lines = filesystem_usage. lines ( ) ;
275+ while let Some ( line) = lines
276+ . next_line ( )
277+ . await
278+ . error ( "Failed to read output of btrfs filesystem usage" ) ?
279+ {
280+ let line = line. trim ( ) ;
281+
282+ // See btrfs-filesystem(8) for an explanation for the rows.
283+ get ! ( line, "Device size" , final_total) ;
284+ get ! ( line, "Used" , final_used) ;
285+ get ! ( @pre_op remove_estimate_min, line, "Free (estimated)" , final_free) ;
286+ }
287+
288+ Ok ( (
289+ * final_total. get ( ) . ok_or ( Error :: new ( OUTPUT_CHANGED ) ) ?,
290+ * final_used. get ( ) . ok_or ( Error :: new ( OUTPUT_CHANGED ) ) ?,
291+ // HACK(@bpeetz): We also return the free disk space as the available one, because btrfs
292+ // does not tell us which disk space is reserved for the fs. <2025-05-18>
293+ * final_free. get ( ) . ok_or ( Error :: new ( OUTPUT_CHANGED ) ) ?,
294+ * final_free. get ( ) . ok_or ( Error :: new ( OUTPUT_CHANGED ) ) ?,
295+ ) )
296+ }
297+ }
0 commit comments