Skip to main content

qframe/widgets/file_manager/
details.rs

1//! What an entry is besides its name: how big it is, when it changed last and who may do what
2//! with it.
3//!
4//! Every one of these means another call to the system for that one entry, so they are never read
5//! for a whole folder. A tree row shows a name and nothing else and reads nothing; the views that
6//! show details ask for a page of entries around the cursor, and an application that knows exactly
7//! which rows it draws asks for those.
8
9use std::path::Path;
10use std::time::UNIX_EPOCH;
11
12use crate::date::{DateTime, local_offset_minutes};
13
14/// How many entries one ask reads: always more than a screen holds and far less than a large
15/// folder, so scrolling rarely waits and ten thousand entries never mean ten thousand calls.
16pub(super) const PAGE: usize = 200;
17
18/// Units a size is said in, each a thousand of the one before.
19const UNITS: [&str; 5] = ["B", "kB", "MB", "GB", "TB"];
20
21/// A size below this is said in whole units; above it one decimal is kept.
22const WHOLE: f64 = 10.0;
23
24/// What one entry is besides its name: its size, when it changed last, and its permissions.
25///
26/// A symbolic link is read as itself rather than as what it points at, the way the rows show it:
27/// a link out of the folder must not be followed for a number on screen.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct FileDetails {
30    /// Its size in bytes. A folder's own size says how big the folder itself is on disk, not what
31    /// is in it; adding up a folder means reading all of it, which a manager never does by itself.
32    pub size: u64,
33    /// When it changed last, in seconds since 1970-01-01 UTC, or `None` where the system does not
34    /// say.
35    pub modified: Option<i64>,
36    /// The permission bits, where the platform has them (unix), and `None` where it does not.
37    pub mode: Option<u32>,
38    /// Whether it may not be written, which every platform says.
39    pub readonly: bool,
40}
41
42impl FileDetails {
43    /// Reads the details of the entry at `path`, or `None` when the system says nothing about it
44    /// — it is gone, or may not be looked at.
45    ///
46    /// This touches the disk, so it belongs on a background thread;
47    /// [`FileManagerState`](super::FileManagerState) asks for it with
48    /// [`Command::perform`](crate::runtime::Command::perform) and never while drawing.
49    #[must_use]
50    pub fn read(path: &Path) -> Option<Self> {
51        let data = std::fs::symlink_metadata(path).ok()?;
52        let modified = data
53            .modified()
54            .ok()
55            .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
56            .and_then(|since| i64::try_from(since.as_secs()).ok());
57        Some(Self { size: data.len(), modified, mode: mode_of(&data), readonly: data.permissions().readonly() })
58    }
59
60    /// The size in the largest unit that leaves a number worth reading: `840 B`, `9.4 kB`,
61    /// `12 MB`. A folder's size is left out, because its own size says nothing about what is in
62    /// it and a number that means nothing is worse than none.
63    #[must_use]
64    pub fn size_text(&self, folder: bool) -> String {
65        if folder {
66            return String::new();
67        }
68        let mut size = self.size as f64;
69        let mut unit = 0;
70        while size >= 1000.0 && unit + 1 < UNITS.len() {
71            size /= 1000.0;
72            unit += 1;
73        }
74        let number =
75            if unit == 0 || size >= WHOLE { format!("{}", size.round() as u64) } else { crate::i18n::number(size, 1) };
76        crate::t!("quvyta.file-manager.size", n = number.as_str(), unit = UNITS[unit])
77    }
78
79    /// When it changed last, as the year, the month, the day and the clock the machine stands by:
80    /// `2026-09-20 14:32`. Empty where the system does not say.
81    #[must_use]
82    pub fn modified_text(&self) -> String {
83        let Some(seconds) = self.modified else { return String::new() };
84        let moment = DateTime::from_unix(seconds, local_offset_minutes());
85        let (date, time) = (moment.date, moment.time);
86        format!("{date} {:02}:{:02}", time.hour, time.minute)
87    }
88
89    /// Who may do what with it: the nine letters unix writes them with (`rwxr-xr-x`), and on a
90    /// platform without them the one thing it does say, whether the entry may be written.
91    #[must_use]
92    pub fn permissions_text(&self, folder: bool) -> String {
93        let Some(mode) = self.mode else {
94            let key =
95                if self.readonly { "quvyta.file-manager.read-only" } else { "quvyta.file-manager.read-and-write" };
96            return crate::t!(key);
97        };
98        let kind = if folder { 'd' } else { '-' };
99        let letters = ['r', 'w', 'x'];
100        let mut text = String::with_capacity(10);
101        text.push(kind);
102        for group in 0..3 {
103            for (bit, letter) in letters.iter().enumerate() {
104                let shift = (2 - group) * 3 + (2 - bit);
105                if mode & (1 << shift) == 0 {
106                    text.push('-');
107                } else {
108                    text.push(*letter);
109                }
110            }
111        }
112        text
113    }
114}
115
116/// The permission bits where the platform has them.
117#[cfg(unix)]
118fn mode_of(data: &std::fs::Metadata) -> Option<u32> {
119    use std::os::unix::fs::PermissionsExt;
120    Some(data.permissions().mode())
121}
122
123/// Nothing where it does not; [`FileDetails::readonly`] is all such a platform says.
124#[cfg(not(unix))]
125fn mode_of(_data: &std::fs::Metadata) -> Option<u32> {
126    None
127}