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 = if unit == 0 || size >= WHOLE { format!("{}", size.round() as u64) } else { format!("{size:.1}") };
75        crate::t!("quvyta.file-manager.size", n = number.as_str(), unit = UNITS[unit])
76    }
77
78    /// When it changed last, as the year, the month, the day and the clock the machine stands by:
79    /// `2026-09-20 14:32`. Empty where the system does not say.
80    #[must_use]
81    pub fn modified_text(&self) -> String {
82        let Some(seconds) = self.modified else { return String::new() };
83        let moment = DateTime::from_unix(seconds, local_offset_minutes());
84        let (date, time) = (moment.date, moment.time);
85        format!("{date} {:02}:{:02}", time.hour, time.minute)
86    }
87
88    /// Who may do what with it: the nine letters unix writes them with (`rwxr-xr-x`), and on a
89    /// platform without them the one thing it does say, whether the entry may be written.
90    #[must_use]
91    pub fn permissions_text(&self, folder: bool) -> String {
92        let Some(mode) = self.mode else {
93            let key =
94                if self.readonly { "quvyta.file-manager.read-only" } else { "quvyta.file-manager.read-and-write" };
95            return crate::t!(key);
96        };
97        let kind = if folder { 'd' } else { '-' };
98        let letters = ['r', 'w', 'x'];
99        let mut text = String::with_capacity(10);
100        text.push(kind);
101        for group in 0..3 {
102            for (bit, letter) in letters.iter().enumerate() {
103                let shift = (2 - group) * 3 + (2 - bit);
104                if mode & (1 << shift) == 0 {
105                    text.push('-');
106                } else {
107                    text.push(*letter);
108                }
109            }
110        }
111        text
112    }
113}
114
115/// The permission bits where the platform has them.
116#[cfg(unix)]
117fn mode_of(data: &std::fs::Metadata) -> Option<u32> {
118    use std::os::unix::fs::PermissionsExt;
119    Some(data.permissions().mode())
120}
121
122/// Nothing where it does not; [`FileDetails::readonly`] is all such a platform says.
123#[cfg(not(unix))]
124fn mode_of(_data: &std::fs::Metadata) -> Option<u32> {
125    None
126}