Skip to content

Commit a4a8b34

Browse files
committed
Add file metadata to find file
1 parent 78e7db7 commit a4a8b34

File tree

3 files changed

+260
-1
lines changed

3 files changed

+260
-1
lines changed

Diff for: Cargo.lock

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: helix-term/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ grep-searcher = "0.1.8"
6868

6969
# Remove once retain_mut lands in stable rust
7070
retain_mut = "0.1.7"
71+
libc = "0.2.126"
72+
number_prefix = "0.4.0"
7173

7274
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
7375
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

Diff for: helix-term/src/ui/picker.rs

+250-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,242 @@ use helix_view::{
3131
Document, Editor,
3232
};
3333

34+
// based on exa but not sure where to put this
35+
/// More readable aliases for the permission bits exposed by libc.
36+
#[allow(trivial_numeric_casts)]
37+
mod modes {
38+
// The `libc::mode_t` type’s actual type varies, but the value returned
39+
// from `metadata.permissions().mode()` is always `u32`.
40+
pub type Mode = u32;
41+
42+
pub const USER_READ: Mode = libc::S_IRUSR as Mode;
43+
pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;
44+
pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode;
45+
46+
pub const GROUP_READ: Mode = libc::S_IRGRP as Mode;
47+
pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode;
48+
pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
49+
50+
pub const OTHER_READ: Mode = libc::S_IROTH as Mode;
51+
pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode;
52+
pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
53+
54+
pub const STICKY: Mode = libc::S_ISVTX as Mode;
55+
pub const SETGID: Mode = libc::S_ISGID as Mode;
56+
pub const SETUID: Mode = libc::S_ISUID as Mode;
57+
}
58+
59+
// based on exa but not sure where to put this
60+
mod fields {
61+
use super::modes;
62+
use std::fmt;
63+
use std::fs::Metadata;
64+
use std::os::unix::fs::{FileTypeExt, MetadataExt};
65+
66+
/// The file’s base type, which gets displayed in the very first column of the
67+
/// details output.
68+
///
69+
/// This type is set entirely by the filesystem, rather than relying on a
70+
/// file’s contents. So “link” is a type, but “image” is just a type of
71+
/// regular file. (See the `filetype` module for those checks.)
72+
///
73+
/// Its ordering is used when sorting by type.
74+
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
75+
pub enum Type {
76+
Directory,
77+
File,
78+
Link,
79+
Pipe,
80+
Socket,
81+
CharDevice,
82+
BlockDevice,
83+
Special,
84+
}
85+
86+
pub fn filetype(metadata: &Metadata) -> Type {
87+
let filetype = metadata.file_type();
88+
if metadata.is_file() {
89+
Type::File
90+
} else if metadata.is_dir() {
91+
Type::Directory
92+
} else if filetype.is_fifo() {
93+
Type::Pipe
94+
} else if filetype.is_symlink() {
95+
Type::Link
96+
} else if filetype.is_char_device() {
97+
Type::CharDevice
98+
} else if filetype.is_block_device() {
99+
Type::BlockDevice
100+
} else if filetype.is_socket() {
101+
Type::Socket
102+
} else {
103+
Type::Special
104+
}
105+
}
106+
107+
impl fmt::Display for Type {
108+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109+
write!(
110+
f,
111+
"{}",
112+
match self {
113+
Type::Directory => 'd',
114+
Type::File => '.',
115+
Type::Link => 'l',
116+
Type::Pipe => '|',
117+
Type::Socket => 's',
118+
Type::CharDevice => 'c',
119+
Type::BlockDevice => 'b',
120+
Type::Special => '?',
121+
}
122+
)
123+
}
124+
}
125+
126+
/// The file’s Unix permission bitfield, with one entry per bit.
127+
#[derive(Copy, Clone)]
128+
pub struct Permissions {
129+
pub user_read: bool,
130+
pub user_write: bool,
131+
pub user_execute: bool,
132+
133+
pub group_read: bool,
134+
pub group_write: bool,
135+
pub group_execute: bool,
136+
137+
pub other_read: bool,
138+
pub other_write: bool,
139+
pub other_execute: bool,
140+
141+
pub sticky: bool,
142+
pub setgid: bool,
143+
pub setuid: bool,
144+
}
145+
146+
pub fn permissions(metadata: &Metadata) -> Permissions {
147+
let bits = metadata.mode();
148+
let has_bit = |bit| bits & bit == bit;
149+
Permissions {
150+
user_read: has_bit(modes::USER_READ),
151+
user_write: has_bit(modes::USER_WRITE),
152+
user_execute: has_bit(modes::USER_EXECUTE),
153+
154+
group_read: has_bit(modes::GROUP_READ),
155+
group_write: has_bit(modes::GROUP_WRITE),
156+
group_execute: has_bit(modes::GROUP_EXECUTE),
157+
158+
other_read: has_bit(modes::OTHER_READ),
159+
other_write: has_bit(modes::OTHER_WRITE),
160+
other_execute: has_bit(modes::OTHER_EXECUTE),
161+
162+
sticky: has_bit(modes::STICKY),
163+
setgid: has_bit(modes::SETGID),
164+
setuid: has_bit(modes::SETUID),
165+
}
166+
}
167+
168+
impl fmt::Display for Permissions {
169+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170+
let bit = |bit, char| if bit { char } else { "-" };
171+
write!(
172+
f,
173+
"{}{}{}{}{}{}{}{}{}",
174+
bit(self.user_read, "r"),
175+
bit(self.user_write, "w"),
176+
bit(self.user_execute, "x"),
177+
bit(self.group_read, "r"),
178+
bit(self.group_write, "w"),
179+
bit(self.group_execute, "x"),
180+
bit(self.other_read, "r"),
181+
bit(self.other_write, "w"),
182+
bit(self.other_execute, "x"),
183+
)
184+
}
185+
}
186+
187+
/// A file’s size, in bytes. This is usually formatted by the `number_prefix`
188+
/// crate into something human-readable.
189+
#[derive(Copy, Clone)]
190+
pub enum Size {
191+
/// This file has a defined size.
192+
Some(u64),
193+
194+
/// This file has no size, or has a size but we aren’t interested in it.
195+
///
196+
/// Under Unix, directory entries that aren’t regular files will still
197+
/// have a file size. For example, a directory will just contain a list of
198+
/// its files as its “contents” and will be specially flagged as being a
199+
/// directory, rather than a file. However, seeing the “file size” of this
200+
/// data is rarely useful — I can’t think of a time when I’ve seen it and
201+
/// learnt something. So we discard it and just output “-” instead.
202+
///
203+
/// See this answer for more: http://unix.stackexchange.com/a/68266
204+
None,
205+
206+
/// This file is a block or character device, so instead of a size, print
207+
/// out the file’s major and minor device IDs.
208+
///
209+
/// This is what ls does as well. Without it, the devices will just have
210+
/// file sizes of zero.
211+
DeviceIDs(DeviceIDs),
212+
}
213+
214+
/// The major and minor device IDs that gets displayed for device files.
215+
///
216+
/// You can see what these device numbers mean:
217+
/// - <http://www.lanana.org/docs/device-list/>
218+
/// - <http://www.lanana.org/docs/device-list/devices-2.6+.txt>
219+
#[derive(Copy, Clone)]
220+
pub struct DeviceIDs {
221+
pub major: u8,
222+
pub minor: u8,
223+
}
224+
225+
/// This file’s size, if it’s a regular file.
226+
///
227+
/// For directories, no size is given. Although they do have a size on
228+
/// some filesystems, I’ve never looked at one of those numbers and gained
229+
/// any information from it. So it’s going to be hidden instead.
230+
///
231+
/// Block and character devices return their device IDs, because they
232+
/// usually just have a file size of zero.
233+
pub fn size(metadata: &Metadata) -> Size {
234+
let filetype = metadata.file_type();
235+
if metadata.is_dir() {
236+
Size::None
237+
} else if filetype.is_char_device() || filetype.is_block_device() {
238+
let device_ids = metadata.rdev().to_be_bytes();
239+
240+
// In C-land, getting the major and minor device IDs is done with
241+
// preprocessor macros called `major` and `minor` that depend on
242+
// the size of `dev_t`, but we just take the second-to-last and
243+
// last bytes.
244+
Size::DeviceIDs(DeviceIDs {
245+
major: device_ids[6],
246+
minor: device_ids[7],
247+
})
248+
} else {
249+
Size::Some(metadata.len())
250+
}
251+
}
252+
253+
impl fmt::Display for Size {
254+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255+
use number_prefix::NumberPrefix;
256+
match self {
257+
Size::Some(s) => match NumberPrefix::decimal(*s as f64) {
258+
NumberPrefix::Standalone(n) => write!(f, "{}", n),
259+
NumberPrefix::Prefixed(p, n) => write!(f, "{:.1}{}", n, p),
260+
},
261+
Size::None => write!(f, "-"),
262+
Size::DeviceIDs(DeviceIDs { major, minor }) => {
263+
write!(f, "{},{}", major, minor)
264+
}
265+
}
266+
}
267+
}
268+
}
269+
34270
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
35271
/// Biggest file size to preview in bytes
36272
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@@ -93,10 +329,23 @@ impl FindFilePicker {
93329
let dir1 = dir.clone();
94330
let mut picker = FilePicker::new(
95331
files,
332+
// TODO: prevent this from running within score function that skews
333+
// the score, and only calculate it once during initialization
96334
move |path| {
97335
let suffix = if path.is_dir() { "/" } else { "" };
336+
let metadata = fs::metadata(&*path).unwrap();
98337
let path = path.strip_prefix(&dir1).unwrap_or(path).to_string_lossy();
99-
path + suffix
338+
let filetype = fields::filetype(&metadata);
339+
let permissions = fields::permissions(&metadata);
340+
let size = format!("{}", fields::size(&metadata));
341+
Cow::Owned(format!(
342+
"{:<22} {}{} {:>6}",
343+
path + suffix, // TODO this should check for size and handle truncation
344+
filetype,
345+
permissions,
346+
size,
347+
// TODO add absolute/relative time? may need to handle truncation
348+
))
100349
},
101350
|_cx, _path, _action| {}, // we use custom callback_fn
102351
|_editor, path| Some((path.clone(), None)),

0 commit comments

Comments
 (0)