mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-05 04:17:51 +00:00
Merge branch 'split-details'
This commit is contained in:
commit
bd860b8fab
@ -69,6 +69,15 @@ pub struct Permissions {
|
|||||||
pub other_execute: bool,
|
pub other_execute: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The three pieces of information that are displayed as a single column in
|
||||||
|
/// the details view. These values are fused together to make the output a
|
||||||
|
/// little more compressed.
|
||||||
|
pub struct PermissionsPlus {
|
||||||
|
pub file_type: Type,
|
||||||
|
pub permissions: Permissions,
|
||||||
|
pub xattrs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A file’s number of hard links on the filesystem.
|
/// A file’s number of hard links on the filesystem.
|
||||||
///
|
///
|
||||||
@ -135,14 +144,17 @@ pub enum Size {
|
|||||||
///
|
///
|
||||||
/// This is what ls does as well. Without it, the devices will just have
|
/// This is what ls does as well. Without it, the devices will just have
|
||||||
/// file sizes of zero.
|
/// file sizes of zero.
|
||||||
///
|
DeviceIDs(DeviceIDs),
|
||||||
/// You can see what these device numbers mean:
|
}
|
||||||
/// - http://www.lanana.org/docs/device-list/
|
|
||||||
/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
|
/// The major and minor device IDs that gets displayed for device files.
|
||||||
DeviceIDs {
|
///
|
||||||
major: u8,
|
/// You can see what these device numbers mean:
|
||||||
minor: u8,
|
/// - http://www.lanana.org/docs/device-list/
|
||||||
}
|
/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
|
||||||
|
pub struct DeviceIDs {
|
||||||
|
pub major: u8,
|
||||||
|
pub minor: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,26 +10,6 @@ use fs::dir::Dir;
|
|||||||
use fs::fields as f;
|
use fs::fields as f;
|
||||||
|
|
||||||
|
|
||||||
#[allow(trivial_numeric_casts)]
|
|
||||||
mod modes {
|
|
||||||
use libc;
|
|
||||||
|
|
||||||
pub type Mode = u32;
|
|
||||||
// The `libc::mode_t` type’s actual type varies, but the value returned
|
|
||||||
// from `metadata.permissions().mode()` is always `u32`.
|
|
||||||
|
|
||||||
pub const USER_READ: Mode = libc::S_IRUSR as Mode;
|
|
||||||
pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;
|
|
||||||
pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode;
|
|
||||||
pub const GROUP_READ: Mode = libc::S_IRGRP as Mode;
|
|
||||||
pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode;
|
|
||||||
pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
|
|
||||||
pub const OTHER_READ: Mode = libc::S_IROTH as Mode;
|
|
||||||
pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode;
|
|
||||||
pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A **File** is a wrapper around one of Rust's Path objects, along with
|
/// A **File** is a wrapper around one of Rust's Path objects, along with
|
||||||
/// associated data about the file.
|
/// associated data about the file.
|
||||||
///
|
///
|
||||||
@ -273,10 +253,10 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
else if self.is_char_device() || self.is_block_device() {
|
else if self.is_char_device() || self.is_block_device() {
|
||||||
let dev = self.metadata.rdev();
|
let dev = self.metadata.rdev();
|
||||||
f::Size::DeviceIDs {
|
f::Size::DeviceIDs(f::DeviceIDs {
|
||||||
major: (dev / 256) as u8,
|
major: (dev / 256) as u8,
|
||||||
minor: (dev % 256) as u8,
|
minor: (dev % 256) as u8,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
f::Size::Some(self.metadata.len())
|
f::Size::Some(self.metadata.len())
|
||||||
@ -327,11 +307,7 @@ impl<'dir> File<'dir> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This file's permissions, with flags for each bit.
|
/// This file’s permissions, with flags for each bit.
|
||||||
///
|
|
||||||
/// The extended-attribute '@' character that you see in here is in fact
|
|
||||||
/// added in later, to avoid querying the extended attributes more than
|
|
||||||
/// once. (Yes, it's a little hacky.)
|
|
||||||
pub fn permissions(&self) -> f::Permissions {
|
pub fn permissions(&self) -> f::Permissions {
|
||||||
let bits = self.metadata.permissions().mode();
|
let bits = self.metadata.permissions().mode();
|
||||||
let has_bit = |bit| { bits & bit == bit };
|
let has_bit = |bit| { bits & bit == bit };
|
||||||
@ -449,6 +425,27 @@ impl<'dir> FileTarget<'dir> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// More readable aliases for the permission bits exposed by libc.
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
mod modes {
|
||||||
|
use libc;
|
||||||
|
|
||||||
|
pub type Mode = u32;
|
||||||
|
// The `libc::mode_t` type’s actual type varies, but the value returned
|
||||||
|
// from `metadata.permissions().mode()` is always `u32`.
|
||||||
|
|
||||||
|
pub const USER_READ: Mode = libc::S_IRUSR as Mode;
|
||||||
|
pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;
|
||||||
|
pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode;
|
||||||
|
pub const GROUP_READ: Mode = libc::S_IRGRP as Mode;
|
||||||
|
pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode;
|
||||||
|
pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
|
||||||
|
pub const OTHER_READ: Mode = libc::S_IROTH as Mode;
|
||||||
|
pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode;
|
||||||
|
pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::ext;
|
use super::ext;
|
||||||
|
@ -80,17 +80,13 @@
|
|||||||
use std::io::{Write, Error as IOError, Result as IOResult};
|
use std::io::{Write, Error as IOError, Result as IOResult};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::string::ToString;
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use ansi_term::Style;
|
|
||||||
|
|
||||||
use datetime::fmt::DateFormat;
|
use datetime::fmt::DateFormat;
|
||||||
use datetime::{LocalDateTime, DatePiece};
|
use datetime::{LocalDateTime, DatePiece};
|
||||||
use datetime::TimeZone;
|
use datetime::TimeZone;
|
||||||
use zoneinfo_compiled::{CompiledData, Result as TZResult};
|
use zoneinfo_compiled::{CompiledData, Result as TZResult};
|
||||||
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
use locale;
|
use locale;
|
||||||
|
|
||||||
use users::{Users, Groups, UsersCache};
|
use users::{Users, Groups, UsersCache};
|
||||||
@ -99,8 +95,8 @@ use fs::{Dir, File, fields as f};
|
|||||||
use fs::feature::xattr::{Attribute, FileAttributes};
|
use fs::feature::xattr::{Attribute, FileAttributes};
|
||||||
use options::{FileFilter, RecurseOptions};
|
use options::{FileFilter, RecurseOptions};
|
||||||
use output::colours::Colours;
|
use output::colours::Colours;
|
||||||
use output::column::{Alignment, Column, Columns, SizeFormat};
|
use output::column::{Alignment, Column, Columns};
|
||||||
use output::cell::{TextCell, TextCellContents, DisplayWidth};
|
use output::cell::{TextCell, TextCellContents};
|
||||||
use output::tree::TreeTrunk;
|
use output::tree::TreeTrunk;
|
||||||
use output::file_name::{FileName, LinkStyle, Classify};
|
use output::file_name::{FileName, LinkStyle, Classify};
|
||||||
|
|
||||||
@ -150,7 +146,7 @@ pub struct Details {
|
|||||||
/// running instances of exa, depending on the user's computer's configuration.
|
/// running instances of exa, depending on the user's computer's configuration.
|
||||||
///
|
///
|
||||||
/// Any environment field should be able to be mocked up for test runs.
|
/// Any environment field should be able to be mocked up for test runs.
|
||||||
pub struct Environment<U: Users+Groups> {
|
pub struct Environment<U> { // where U: Users+Groups
|
||||||
|
|
||||||
/// The year of the current time. This gets used to determine which date
|
/// The year of the current time. This gets used to determine which date
|
||||||
/// format to use.
|
/// format to use.
|
||||||
@ -176,10 +172,17 @@ pub struct Environment<U: Users+Groups> {
|
|||||||
users: Mutex<U>,
|
users: Mutex<U>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<U> Environment<U> {
|
||||||
|
pub fn lock_users(&self) -> MutexGuard<U> {
|
||||||
|
self.users.lock().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Environment<UsersCache> {
|
impl Default for Environment<UsersCache> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let tz = determine_time_zone();
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
let tz = determine_time_zone();
|
||||||
if let Err(ref e) = tz {
|
if let Err(ref e) = tz {
|
||||||
println!("Unable to determine time zone: {}", e);
|
println!("Unable to determine time zone: {}", e);
|
||||||
}
|
}
|
||||||
@ -193,6 +196,7 @@ impl Default for Environment<UsersCache> {
|
|||||||
// Some locales use a three-character wide month name (Jan to Dec);
|
// Some locales use a three-character wide month name (Jan to Dec);
|
||||||
// others vary between three and four (1月 to 12月). We assume that
|
// others vary between three and four (1月 to 12月). We assume that
|
||||||
// December is the month with the maximum width, and use the width of
|
// December is the month with the maximum width, and use the width of
|
||||||
|
// that to determine how to pad the other months.
|
||||||
let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
|
let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
|
||||||
let date_and_time = match december_width {
|
let date_and_time = match december_width {
|
||||||
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
|
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
|
||||||
@ -414,7 +418,7 @@ impl Row {
|
|||||||
|
|
||||||
/// A **Table** object gets built up by the view as it lists files and
|
/// A **Table** object gets built up by the view as it lists files and
|
||||||
/// directories.
|
/// directories.
|
||||||
pub struct Table<'a, U: Users+Groups+'a> {
|
pub struct Table<'a, U: 'a> { // where U: Users+Groups
|
||||||
pub rows: Vec<Row>,
|
pub rows: Vec<Row>,
|
||||||
|
|
||||||
pub columns: &'a [Column],
|
pub columns: &'a [Column],
|
||||||
@ -488,234 +492,32 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
|
||||||
|
f::PermissionsPlus {
|
||||||
|
file_type: file.type_char(),
|
||||||
|
permissions: file.permissions(),
|
||||||
|
xattrs: xattrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
|
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
|
||||||
use output::column::TimeType::*;
|
use output::column::TimeType::*;
|
||||||
|
|
||||||
match *column {
|
match *column {
|
||||||
Column::Permissions => self.render_permissions(file.type_char(), file.permissions(), xattrs),
|
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.opts.colours),
|
||||||
Column::FileSize(fmt) => self.render_size(file.size(), fmt),
|
Column::FileSize(fmt) => file.size().render(&self.opts.colours, fmt, &self.env.numeric),
|
||||||
Column::Timestamp(Modified) => self.render_time(file.modified_time()),
|
Column::Timestamp(Modified) => file.modified_time().render(&self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
|
||||||
Column::Timestamp(Created) => self.render_time(file.created_time()),
|
Column::Timestamp(Created) => file.created_time().render( &self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
|
||||||
Column::Timestamp(Accessed) => self.render_time(file.accessed_time()),
|
Column::Timestamp(Accessed) => file.accessed_time().render(&self.opts.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
|
||||||
Column::HardLinks => self.render_links(file.links()),
|
Column::HardLinks => file.links().render(&self.opts.colours, &self.env.numeric),
|
||||||
Column::Inode => self.render_inode(file.inode()),
|
Column::Inode => file.inode().render(&self.opts.colours),
|
||||||
Column::Blocks => self.render_blocks(file.blocks()),
|
Column::Blocks => file.blocks().render(&self.opts.colours),
|
||||||
Column::User => self.render_user(file.user()),
|
Column::User => file.user().render(&self.opts.colours, &*self.env.lock_users()),
|
||||||
Column::Group => self.render_group(file.group()),
|
Column::Group => file.group().render(&self.opts.colours, &*self.env.lock_users()),
|
||||||
Column::GitStatus => self.render_git_status(file.git_status()),
|
Column::GitStatus => file.git_status().render(&self.opts.colours),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_permissions(&self, file_type: f::Type, permissions: f::Permissions, xattrs: bool) -> TextCell {
|
|
||||||
let perms = self.opts.colours.perms;
|
|
||||||
let types = self.opts.colours.filetypes;
|
|
||||||
|
|
||||||
let bit = |bit, chr: &'static str, style: Style| {
|
|
||||||
if bit { style.paint(chr) } else { self.opts.colours.punctuation.paint("-") }
|
|
||||||
};
|
|
||||||
|
|
||||||
let type_char = match file_type {
|
|
||||||
f::Type::File => types.normal.paint("."),
|
|
||||||
f::Type::Directory => types.directory.paint("d"),
|
|
||||||
f::Type::Pipe => types.pipe.paint("|"),
|
|
||||||
f::Type::Link => types.symlink.paint("l"),
|
|
||||||
f::Type::CharDevice => types.device.paint("c"),
|
|
||||||
f::Type::BlockDevice => types.device.paint("b"),
|
|
||||||
f::Type::Socket => types.socket.paint("s"),
|
|
||||||
f::Type::Special => types.special.paint("?"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let x_colour = if file_type.is_regular_file() { perms.user_execute_file }
|
|
||||||
else { perms.user_execute_other };
|
|
||||||
|
|
||||||
let mut chars = vec![
|
|
||||||
type_char,
|
|
||||||
bit(permissions.user_read, "r", perms.user_read),
|
|
||||||
bit(permissions.user_write, "w", perms.user_write),
|
|
||||||
bit(permissions.user_execute, "x", x_colour),
|
|
||||||
bit(permissions.group_read, "r", perms.group_read),
|
|
||||||
bit(permissions.group_write, "w", perms.group_write),
|
|
||||||
bit(permissions.group_execute, "x", perms.group_execute),
|
|
||||||
bit(permissions.other_read, "r", perms.other_read),
|
|
||||||
bit(permissions.other_write, "w", perms.other_write),
|
|
||||||
bit(permissions.other_execute, "x", perms.other_execute),
|
|
||||||
];
|
|
||||||
|
|
||||||
if xattrs {
|
|
||||||
chars.push(perms.attribute.paint("@"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// As these are all ASCII characters, we can guarantee that they’re
|
|
||||||
// all going to be one character wide, and don’t need to compute the
|
|
||||||
// cell’s display width.
|
|
||||||
let width = DisplayWidth::from(chars.len());
|
|
||||||
|
|
||||||
TextCell {
|
|
||||||
contents: chars.into(),
|
|
||||||
width: width,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_links(&self, links: f::Links) -> TextCell {
|
|
||||||
let style = if links.multiple { self.opts.colours.links.multi_link_file }
|
|
||||||
else { self.opts.colours.links.normal };
|
|
||||||
|
|
||||||
TextCell::paint(style, self.env.numeric.format_int(links.count))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_blocks(&self, blocks: f::Blocks) -> TextCell {
|
|
||||||
match blocks {
|
|
||||||
f::Blocks::Some(blk) => TextCell::paint(self.opts.colours.blocks, blk.to_string()),
|
|
||||||
f::Blocks::None => TextCell::blank(self.opts.colours.punctuation),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_inode(&self, inode: f::Inode) -> TextCell {
|
|
||||||
TextCell::paint(self.opts.colours.inode, inode.0.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_size(&self, size: f::Size, size_format: SizeFormat) -> TextCell {
|
|
||||||
use number_prefix::{binary_prefix, decimal_prefix};
|
|
||||||
use number_prefix::{Prefixed, Standalone, PrefixNames};
|
|
||||||
|
|
||||||
let size = match size {
|
|
||||||
f::Size::Some(s) => s,
|
|
||||||
f::Size::None => return TextCell::blank(self.opts.colours.punctuation),
|
|
||||||
f::Size::DeviceIDs { major, minor } => return self.render_device_ids(major, minor),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = match size_format {
|
|
||||||
SizeFormat::DecimalBytes => decimal_prefix(size as f64),
|
|
||||||
SizeFormat::BinaryBytes => binary_prefix(size as f64),
|
|
||||||
SizeFormat::JustBytes => {
|
|
||||||
let string = self.env.numeric.format_int(size);
|
|
||||||
return TextCell::paint(self.opts.colours.file_size(size), string);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let (prefix, n) = match result {
|
|
||||||
Standalone(b) => return TextCell::paint(self.opts.colours.file_size(b as u64), b.to_string()),
|
|
||||||
Prefixed(p, n) => (p, n)
|
|
||||||
};
|
|
||||||
|
|
||||||
let symbol = prefix.symbol();
|
|
||||||
let number = if n < 10f64 { self.env.numeric.format_float(n, 1) }
|
|
||||||
else { self.env.numeric.format_int(n as isize) };
|
|
||||||
|
|
||||||
// The numbers and symbols are guaranteed to be written in ASCII, so
|
|
||||||
// we can skip the display width calculation.
|
|
||||||
let width = DisplayWidth::from(number.len() + symbol.len());
|
|
||||||
|
|
||||||
TextCell {
|
|
||||||
width: width,
|
|
||||||
contents: vec![
|
|
||||||
self.opts.colours.file_size(size).paint(number),
|
|
||||||
self.opts.colours.size.unit.paint(symbol),
|
|
||||||
].into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_device_ids(&self, major: u8, minor: u8) -> TextCell {
|
|
||||||
let major = major.to_string();
|
|
||||||
let minor = minor.to_string();
|
|
||||||
|
|
||||||
TextCell {
|
|
||||||
width: DisplayWidth::from(major.len() + 1 + minor.len()),
|
|
||||||
contents: vec![
|
|
||||||
self.opts.colours.size.major.paint(major),
|
|
||||||
self.opts.colours.punctuation.paint(","),
|
|
||||||
self.opts.colours.size.minor.paint(minor),
|
|
||||||
].into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(trivial_numeric_casts)]
|
|
||||||
fn render_time(&self, timestamp: f::Time) -> TextCell {
|
|
||||||
// TODO(ogham): This method needs some serious de-duping!
|
|
||||||
// zoned and local times have different types at the moment,
|
|
||||||
// so it's tricky.
|
|
||||||
|
|
||||||
if let Some(ref tz) = self.env.tz {
|
|
||||||
let date = tz.to_zoned(LocalDateTime::at(timestamp.0 as i64));
|
|
||||||
|
|
||||||
let datestamp = if date.year() == self.env.current_year {
|
|
||||||
self.env.date_and_time.format(&date, &self.env.time)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.env.date_and_year.format(&date, &self.env.time)
|
|
||||||
};
|
|
||||||
|
|
||||||
TextCell::paint(self.opts.colours.date, datestamp)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let date = LocalDateTime::at(timestamp.0 as i64);
|
|
||||||
|
|
||||||
let datestamp = if date.year() == self.env.current_year {
|
|
||||||
self.env.date_and_time.format(&date, &self.env.time)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.env.date_and_year.format(&date, &self.env.time)
|
|
||||||
};
|
|
||||||
|
|
||||||
TextCell::paint(self.opts.colours.date, datestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_git_status(&self, git: f::Git) -> TextCell {
|
|
||||||
let git_char = |status| match status {
|
|
||||||
f::GitStatus::NotModified => self.opts.colours.punctuation.paint("-"),
|
|
||||||
f::GitStatus::New => self.opts.colours.git.new.paint("N"),
|
|
||||||
f::GitStatus::Modified => self.opts.colours.git.modified.paint("M"),
|
|
||||||
f::GitStatus::Deleted => self.opts.colours.git.deleted.paint("D"),
|
|
||||||
f::GitStatus::Renamed => self.opts.colours.git.renamed.paint("R"),
|
|
||||||
f::GitStatus::TypeChange => self.opts.colours.git.typechange.paint("T"),
|
|
||||||
};
|
|
||||||
|
|
||||||
TextCell {
|
|
||||||
width: DisplayWidth::from(2),
|
|
||||||
contents: vec![
|
|
||||||
git_char(git.staged),
|
|
||||||
git_char(git.unstaged)
|
|
||||||
].into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_user(&self, user: f::User) -> TextCell {
|
|
||||||
let users = self.env.users.lock().unwrap();
|
|
||||||
|
|
||||||
|
|
||||||
let user_name = match users.get_user_by_uid(user.0) {
|
|
||||||
Some(user) => user.name().to_owned(),
|
|
||||||
None => user.0.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = if users.get_current_uid() == user.0 { self.opts.colours.users.user_you }
|
|
||||||
else { self.opts.colours.users.user_someone_else };
|
|
||||||
TextCell::paint(style, user_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_group(&self, group: f::Group) -> TextCell {
|
|
||||||
use users::os::unix::GroupExt;
|
|
||||||
|
|
||||||
let mut style = self.opts.colours.users.group_not_yours;
|
|
||||||
|
|
||||||
let users = self.env.users.lock().unwrap();
|
|
||||||
let group = match users.get_group_by_gid(group.0) {
|
|
||||||
Some(g) => (*g).clone(),
|
|
||||||
None => return TextCell::paint(style, group.0.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_uid = users.get_current_uid();
|
|
||||||
if let Some(current_user) = users.get_user_by_uid(current_uid) {
|
|
||||||
if current_user.primary_group_id() == group.gid()
|
|
||||||
|| group.members().contains(¤t_user.name().to_owned()) {
|
|
||||||
style = self.opts.colours.users.group_yours;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextCell::paint(style, group.name().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the table as a vector of Cells, to be displayed on standard output.
|
/// Render the table as a vector of Cells, to be displayed on standard output.
|
||||||
pub fn print_table(self) -> Vec<TextCell> {
|
pub fn print_table(self) -> Vec<TextCell> {
|
||||||
let mut tree_trunk = TreeTrunk::default();
|
let mut tree_trunk = TreeTrunk::default();
|
||||||
@ -771,209 +573,3 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
|
|||||||
cells
|
cells
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod test {
|
|
||||||
pub use super::{Table, Environment, Details};
|
|
||||||
pub use std::sync::Mutex;
|
|
||||||
|
|
||||||
pub use fs::{File, fields as f};
|
|
||||||
pub use output::column::{Column, Columns};
|
|
||||||
pub use output::cell::TextCell;
|
|
||||||
|
|
||||||
pub use users::{User, Group, uid_t, gid_t};
|
|
||||||
pub use users::mock::MockUsers;
|
|
||||||
pub use users::os::unix::{UserExt, GroupExt};
|
|
||||||
pub use datetime::fmt::DateFormat;
|
|
||||||
pub use ansi_term::Style;
|
|
||||||
pub use ansi_term::Colour::*;
|
|
||||||
|
|
||||||
impl Default for Environment<MockUsers> {
|
|
||||||
fn default() -> Self {
|
|
||||||
use locale;
|
|
||||||
use users::mock::MockUsers;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
Environment {
|
|
||||||
current_year: 1234,
|
|
||||||
numeric: locale::Numeric::english(),
|
|
||||||
time: locale::Time::english(),
|
|
||||||
date_and_time: DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
|
|
||||||
date_and_year: DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap(),
|
|
||||||
tz: None,
|
|
||||||
users: Mutex::new(MockUsers::with_current_uid(0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_table<'a>(columns: &'a [Column], details: &'a Details, users: MockUsers) -> Table<'a, MockUsers> {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
Table {
|
|
||||||
columns: columns,
|
|
||||||
opts: details,
|
|
||||||
env: Arc::new(Environment { users: Mutex::new(users), ..Environment::default() }),
|
|
||||||
rows: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod users {
|
|
||||||
#![allow(unused_results)]
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn named() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.user_you = Red.bold();
|
|
||||||
|
|
||||||
let mut users = MockUsers::with_current_uid(1000);
|
|
||||||
users.add_user(User::new(1000, "enoch", 100));
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let user = f::User(1000);
|
|
||||||
let expected = TextCell::paint_str(Red.bold(), "enoch");
|
|
||||||
assert_eq!(expected, table.render_user(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unnamed() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.user_you = Cyan.bold();
|
|
||||||
|
|
||||||
let users = MockUsers::with_current_uid(1000);
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let user = f::User(1000);
|
|
||||||
let expected = TextCell::paint_str(Cyan.bold(), "1000");
|
|
||||||
assert_eq!(expected, table.render_user(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn different_named() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.user_someone_else = Green.bold();
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
|
|
||||||
table.env.users.lock().unwrap().add_user(User::new(1000, "enoch", 100));
|
|
||||||
|
|
||||||
let user = f::User(1000);
|
|
||||||
let expected = TextCell::paint_str(Green.bold(), "enoch");
|
|
||||||
assert_eq!(expected, table.render_user(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn different_unnamed() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.user_someone_else = Red.normal();
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
|
|
||||||
|
|
||||||
let user = f::User(1000);
|
|
||||||
let expected = TextCell::paint_str(Red.normal(), "1000");
|
|
||||||
assert_eq!(expected, table.render_user(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn overflow() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.user_someone_else = Blue.underline();
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
|
|
||||||
|
|
||||||
let user = f::User(2_147_483_648);
|
|
||||||
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
|
||||||
assert_eq!(expected, table.render_user(user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod groups {
|
|
||||||
#![allow(unused_results)]
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn named() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.group_not_yours = Fixed(101).normal();
|
|
||||||
|
|
||||||
let mut users = MockUsers::with_current_uid(1000);
|
|
||||||
users.add_group(Group::new(100, "folk"));
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let group = f::Group(100);
|
|
||||||
let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
|
|
||||||
assert_eq!(expected, table.render_group(group))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unnamed() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.group_not_yours = Fixed(87).normal();
|
|
||||||
|
|
||||||
let users = MockUsers::with_current_uid(1000);
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let group = f::Group(100);
|
|
||||||
let expected = TextCell::paint_str(Fixed(87).normal(), "100");
|
|
||||||
assert_eq!(expected, table.render_group(group));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn primary() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.group_yours = Fixed(64).normal();
|
|
||||||
|
|
||||||
let mut users = MockUsers::with_current_uid(2);
|
|
||||||
users.add_user(User::new(2, "eve", 100));
|
|
||||||
users.add_group(Group::new(100, "folk"));
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let group = f::Group(100);
|
|
||||||
let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
|
|
||||||
assert_eq!(expected, table.render_group(group))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn secondary() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.group_yours = Fixed(31).normal();
|
|
||||||
|
|
||||||
let mut users = MockUsers::with_current_uid(2);
|
|
||||||
users.add_user(User::new(2, "eve", 666));
|
|
||||||
|
|
||||||
let test_group = Group::new(100, "folk").add_member("eve");
|
|
||||||
users.add_group(test_group);
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, users);
|
|
||||||
|
|
||||||
let group = f::Group(100);
|
|
||||||
let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
|
|
||||||
assert_eq!(expected, table.render_group(group))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn overflow() {
|
|
||||||
let columns = Columns::default().for_dir(None);
|
|
||||||
let mut details = Details::default();
|
|
||||||
details.colours.users.group_not_yours = Blue.underline();
|
|
||||||
|
|
||||||
let table = new_table(&columns, &details, MockUsers::with_current_uid(0));
|
|
||||||
|
|
||||||
let group = f::Group(2_147_483_648);
|
|
||||||
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
|
||||||
assert_eq!(expected, table.render_group(group));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -16,3 +16,4 @@ mod colours;
|
|||||||
mod tree;
|
mod tree;
|
||||||
pub mod file_name;
|
pub mod file_name;
|
||||||
mod escape;
|
mod escape;
|
||||||
|
mod render;
|
||||||
|
44
src/output/render/blocks.rs
Normal file
44
src/output/render/blocks.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use output::cell::TextCell;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Blocks {
|
||||||
|
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||||
|
match *self {
|
||||||
|
f::Blocks::Some(ref blk) => TextCell::paint(colours.blocks, blk.to_string()),
|
||||||
|
f::Blocks::None => TextCell::blank(colours.punctuation),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blocklessness() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.punctuation = Green.italic();
|
||||||
|
|
||||||
|
let blox = f::Blocks::None;
|
||||||
|
let expected = TextCell::blank(Green.italic());
|
||||||
|
assert_eq!(expected, blox.render(&details.colours).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blockfulity() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.blocks = Red.blink();
|
||||||
|
|
||||||
|
let blox = f::Blocks::Some(3005);
|
||||||
|
let expected = TextCell::paint_str(Red.blink(), "3005");
|
||||||
|
assert_eq!(expected, blox.render(&details.colours).into());
|
||||||
|
}
|
||||||
|
}
|
86
src/output/render/git.rs
Normal file
86
src/output/render/git.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use ansi_term::ANSIString;
|
||||||
|
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use output::colours::Colours;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Git {
|
||||||
|
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||||
|
TextCell {
|
||||||
|
width: DisplayWidth::from(2),
|
||||||
|
contents: vec![
|
||||||
|
self.staged.render(colours),
|
||||||
|
self.unstaged.render(colours),
|
||||||
|
].into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl f::GitStatus {
|
||||||
|
fn render(&self, colours: &Colours) -> ANSIString<'static> {
|
||||||
|
match *self {
|
||||||
|
f::GitStatus::NotModified => colours.punctuation.paint("-"),
|
||||||
|
f::GitStatus::New => colours.git.new.paint("N"),
|
||||||
|
f::GitStatus::Modified => colours.git.modified.paint("M"),
|
||||||
|
f::GitStatus::Deleted => colours.git.deleted.paint("D"),
|
||||||
|
f::GitStatus::Renamed => colours.git.renamed.paint("R"),
|
||||||
|
f::GitStatus::TypeChange => colours.git.typechange.paint("T"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_blank() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.punctuation = Fixed(44).normal();
|
||||||
|
|
||||||
|
let stati = f::Git {
|
||||||
|
staged: f::GitStatus::NotModified,
|
||||||
|
unstaged: f::GitStatus::NotModified,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(2),
|
||||||
|
contents: vec![
|
||||||
|
Fixed(44).paint("-"),
|
||||||
|
Fixed(44).paint("-"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, stati.render(&details.colours).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_new_changed() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.git.new = Red.normal();
|
||||||
|
details.colours.git.modified = Purple.normal();
|
||||||
|
|
||||||
|
let stati = f::Git {
|
||||||
|
staged: f::GitStatus::New,
|
||||||
|
unstaged: f::GitStatus::Modified,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(2),
|
||||||
|
contents: vec![
|
||||||
|
Red.paint("N"),
|
||||||
|
Purple.paint("M"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, stati.render(&details.colours).into())
|
||||||
|
}
|
||||||
|
}
|
110
src/output/render/groups.rs
Normal file
110
src/output/render/groups.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use users::{Users, Groups};
|
||||||
|
|
||||||
|
use fs::fields as f;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Group {
|
||||||
|
pub fn render<U: Users+Groups>(&self, colours: &Colours, users: &U) -> TextCell {
|
||||||
|
use users::os::unix::GroupExt;
|
||||||
|
|
||||||
|
let mut style = colours.users.group_not_yours;
|
||||||
|
|
||||||
|
let group = match users.get_group_by_gid(self.0) {
|
||||||
|
Some(g) => (*g).clone(),
|
||||||
|
None => return TextCell::paint(style, self.0.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_uid = users.get_current_uid();
|
||||||
|
if let Some(current_user) = users.get_user_by_uid(current_uid) {
|
||||||
|
if current_user.primary_group_id() == group.gid()
|
||||||
|
|| group.members().contains(¤t_user.name().to_owned()) {
|
||||||
|
style = colours.users.group_yours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextCell::paint(style, group.name().to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(unused_results)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
|
||||||
|
use fs::fields as f;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
|
||||||
|
use users::{User, Group};
|
||||||
|
use users::mock::MockUsers;
|
||||||
|
use users::os::unix::GroupExt;
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.group_not_yours = Fixed(101).normal();
|
||||||
|
|
||||||
|
let mut users = MockUsers::with_current_uid(1000);
|
||||||
|
users.add_group(Group::new(100, "folk"));
|
||||||
|
|
||||||
|
let group = f::Group(100);
|
||||||
|
let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
|
||||||
|
assert_eq!(expected, group.render(&details.colours, &users))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unnamed() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.group_not_yours = Fixed(87).normal();
|
||||||
|
|
||||||
|
let users = MockUsers::with_current_uid(1000);
|
||||||
|
|
||||||
|
let group = f::Group(100);
|
||||||
|
let expected = TextCell::paint_str(Fixed(87).normal(), "100");
|
||||||
|
assert_eq!(expected, group.render(&details.colours, &users));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn primary() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.group_yours = Fixed(64).normal();
|
||||||
|
|
||||||
|
let mut users = MockUsers::with_current_uid(2);
|
||||||
|
users.add_user(User::new(2, "eve", 100));
|
||||||
|
users.add_group(Group::new(100, "folk"));
|
||||||
|
|
||||||
|
let group = f::Group(100);
|
||||||
|
let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
|
||||||
|
assert_eq!(expected, group.render(&details.colours, &users))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secondary() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.group_yours = Fixed(31).normal();
|
||||||
|
|
||||||
|
let mut users = MockUsers::with_current_uid(2);
|
||||||
|
users.add_user(User::new(2, "eve", 666));
|
||||||
|
|
||||||
|
let test_group = Group::new(100, "folk").add_member("eve");
|
||||||
|
users.add_group(test_group);
|
||||||
|
|
||||||
|
let group = f::Group(100);
|
||||||
|
let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
|
||||||
|
assert_eq!(expected, group.render(&details.colours, &users))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overflow() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.group_not_yours = Blue.underline();
|
||||||
|
|
||||||
|
let group = f::Group(2_147_483_648);
|
||||||
|
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
||||||
|
assert_eq!(expected, group.render(&details.colours, &MockUsers::with_current_uid(0)));
|
||||||
|
}
|
||||||
|
}
|
31
src/output/render/inode.rs
Normal file
31
src/output/render/inode.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use output::cell::TextCell;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Inode {
|
||||||
|
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||||
|
TextCell::paint(colours.inode, self.0.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blocklessness() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.inode = Cyan.underline();
|
||||||
|
|
||||||
|
let io = f::Inode(1414213);
|
||||||
|
let expected = TextCell::paint_str(Cyan.underline(), "1414213");
|
||||||
|
assert_eq!(expected, io.render(&details.colours).into());
|
||||||
|
}
|
||||||
|
}
|
81
src/output/render/links.rs
Normal file
81
src/output/render/links.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use output::cell::TextCell;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use locale;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Links {
|
||||||
|
pub fn render(&self, colours: &Colours, numeric: &locale::Numeric) -> TextCell {
|
||||||
|
let style = if self.multiple { colours.links.multi_link_file }
|
||||||
|
else { colours.links.normal };
|
||||||
|
|
||||||
|
TextCell::paint(style, numeric.format_int(self.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
use locale;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regular_file() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.links.normal = Blue.normal();
|
||||||
|
|
||||||
|
let stati = f::Links {
|
||||||
|
count: 1,
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(1),
|
||||||
|
contents: vec![ Blue.paint("1") ].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regular_directory() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.links.normal = Blue.normal();
|
||||||
|
|
||||||
|
let stati = f::Links {
|
||||||
|
count: 3005,
|
||||||
|
multiple: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(5),
|
||||||
|
contents: vec![ Blue.paint("3,005") ].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn popular_file() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.links.multi_link_file = Blue.on(Red);
|
||||||
|
|
||||||
|
let stati = f::Links {
|
||||||
|
count: 3005,
|
||||||
|
multiple: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(5),
|
||||||
|
contents: vec![ Blue.on(Red).paint("3,005") ].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, stati.render(&details.colours, &locale::Numeric::english()).into());
|
||||||
|
}
|
||||||
|
}
|
9
src/output/render/mod.rs
Normal file
9
src/output/render/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
mod blocks;
|
||||||
|
mod git;
|
||||||
|
mod groups;
|
||||||
|
mod inode;
|
||||||
|
mod links;
|
||||||
|
mod permissions;
|
||||||
|
mod size;
|
||||||
|
mod times;
|
||||||
|
mod users;
|
125
src/output/render/permissions.rs
Normal file
125
src/output/render/permissions.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
use fs::fields as f;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use ansi_term::{ANSIString, Style};
|
||||||
|
|
||||||
|
|
||||||
|
impl f::PermissionsPlus {
|
||||||
|
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||||
|
let x_colour = if self.file_type.is_regular_file() { colours.perms.user_execute_file }
|
||||||
|
else { colours.perms.user_execute_other };
|
||||||
|
|
||||||
|
let mut chars = vec![ self.file_type.render(colours) ];
|
||||||
|
chars.extend(self.permissions.render(colours, x_colour));
|
||||||
|
|
||||||
|
if self.xattrs {
|
||||||
|
chars.push(colours.perms.attribute.paint("@"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// As these are all ASCII characters, we can guarantee that they’re
|
||||||
|
// all going to be one character wide, and don’t need to compute the
|
||||||
|
// cell’s display width.
|
||||||
|
TextCell {
|
||||||
|
width: DisplayWidth::from(chars.len()),
|
||||||
|
contents: chars.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl f::Permissions {
|
||||||
|
pub fn render(&self, colours: &Colours, x_colour: Style) -> Vec<ANSIString<'static>> {
|
||||||
|
let bit = |bit, chr: &'static str, style: Style| {
|
||||||
|
if bit { style.paint(chr) } else { colours.punctuation.paint("-") }
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![
|
||||||
|
bit(self.user_read, "r", colours.perms.user_read),
|
||||||
|
bit(self.user_write, "w", colours.perms.user_write),
|
||||||
|
bit(self.user_execute, "x", x_colour),
|
||||||
|
bit(self.group_read, "r", colours.perms.group_read),
|
||||||
|
bit(self.group_write, "w", colours.perms.group_write),
|
||||||
|
bit(self.group_execute, "x", colours.perms.group_execute),
|
||||||
|
bit(self.other_read, "r", colours.perms.other_read),
|
||||||
|
bit(self.other_write, "w", colours.perms.other_write),
|
||||||
|
bit(self.other_execute, "x", colours.perms.other_execute),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl f::Type {
|
||||||
|
pub fn render(&self, colours: &Colours) -> ANSIString<'static> {
|
||||||
|
match *self {
|
||||||
|
f::Type::File => colours.filetypes.normal.paint("."),
|
||||||
|
f::Type::Directory => colours.filetypes.directory.paint("d"),
|
||||||
|
f::Type::Pipe => colours.filetypes.pipe.paint("|"),
|
||||||
|
f::Type::Link => colours.filetypes.symlink.paint("l"),
|
||||||
|
f::Type::CharDevice => colours.filetypes.device.paint("c"),
|
||||||
|
f::Type::BlockDevice => colours.filetypes.device.paint("b"),
|
||||||
|
f::Type::Socket => colours.filetypes.socket.paint("s"),
|
||||||
|
f::Type::Special => colours.filetypes.special.paint("?"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(unused_results)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::cell::TextCellContents;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.punctuation = Fixed(44).normal();
|
||||||
|
|
||||||
|
let bits = f::Permissions {
|
||||||
|
user_read: false, user_write: false, user_execute: false,
|
||||||
|
group_read: false, group_write: false, group_execute: false,
|
||||||
|
other_read: false, other_write: false, other_execute: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCellContents::from(vec![
|
||||||
|
Fixed(44).paint("-"), Fixed(44).paint("-"), Fixed(44).paint("-"),
|
||||||
|
Fixed(44).paint("-"), Fixed(44).paint("-"), Fixed(44).paint("-"),
|
||||||
|
Fixed(44).paint("-"), Fixed(44).paint("-"), Fixed(44).paint("-"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(expected, bits.render(&details.colours, Fixed(66).normal()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn affirm() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.perms.user_read = Fixed(101).normal();
|
||||||
|
details.colours.perms.user_write = Fixed(102).normal();
|
||||||
|
|
||||||
|
details.colours.perms.group_read = Fixed(104).normal();
|
||||||
|
details.colours.perms.group_write = Fixed(105).normal();
|
||||||
|
details.colours.perms.group_execute = Fixed(106).normal();
|
||||||
|
|
||||||
|
details.colours.perms.other_read = Fixed(107).normal();
|
||||||
|
details.colours.perms.other_write = Fixed(108).normal();
|
||||||
|
details.colours.perms.other_execute = Fixed(109).normal();
|
||||||
|
|
||||||
|
let bits = f::Permissions {
|
||||||
|
user_read: true, user_write: true, user_execute: true,
|
||||||
|
group_read: true, group_write: true, group_execute: true,
|
||||||
|
other_read: true, other_write: true, other_execute: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = TextCellContents::from(vec![
|
||||||
|
Fixed(101).paint("r"), Fixed(102).paint("w"), Fixed(103).paint("x"),
|
||||||
|
Fixed(104).paint("r"), Fixed(105).paint("w"), Fixed(106).paint("x"),
|
||||||
|
Fixed(107).paint("r"), Fixed(108).paint("w"), Fixed(109).paint("x"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(expected, bits.render(&details.colours, Fixed(103).normal()).into())
|
||||||
|
}
|
||||||
|
}
|
164
src/output/render/size.rs
Normal file
164
src/output/render/size.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use fs::fields as f;
|
||||||
|
use output::column::SizeFormat;
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use output::colours::Colours;
|
||||||
|
use locale;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::Size {
|
||||||
|
pub fn render(&self, colours: &Colours, size_format: SizeFormat, numerics: &locale::Numeric) -> TextCell {
|
||||||
|
use number_prefix::{binary_prefix, decimal_prefix};
|
||||||
|
use number_prefix::{Prefixed, Standalone, PrefixNames};
|
||||||
|
|
||||||
|
let size = match *self {
|
||||||
|
f::Size::Some(s) => s,
|
||||||
|
f::Size::None => return TextCell::blank(colours.punctuation),
|
||||||
|
f::Size::DeviceIDs(ref ids) => return ids.render(colours),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match size_format {
|
||||||
|
SizeFormat::DecimalBytes => decimal_prefix(size as f64),
|
||||||
|
SizeFormat::BinaryBytes => binary_prefix(size as f64),
|
||||||
|
SizeFormat::JustBytes => {
|
||||||
|
let string = numerics.format_int(size);
|
||||||
|
return TextCell::paint(colours.file_size(size), string);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let (prefix, n) = match result {
|
||||||
|
Standalone(b) => return TextCell::paint(colours.file_size(b as u64), b.to_string()),
|
||||||
|
Prefixed(p, n) => (p, n)
|
||||||
|
};
|
||||||
|
|
||||||
|
let symbol = prefix.symbol();
|
||||||
|
let number = if n < 10f64 { numerics.format_float(n, 1) }
|
||||||
|
else { numerics.format_int(n as isize) };
|
||||||
|
|
||||||
|
// The numbers and symbols are guaranteed to be written in ASCII, so
|
||||||
|
// we can skip the display width calculation.
|
||||||
|
let width = DisplayWidth::from(number.len() + symbol.len());
|
||||||
|
|
||||||
|
TextCell {
|
||||||
|
width: width,
|
||||||
|
contents: vec![
|
||||||
|
colours.file_size(size).paint(number),
|
||||||
|
colours.size.unit.paint(symbol),
|
||||||
|
].into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl f::DeviceIDs {
|
||||||
|
fn render(&self, colours: &Colours) -> TextCell {
|
||||||
|
let major = self.major.to_string();
|
||||||
|
let minor = self.minor.to_string();
|
||||||
|
|
||||||
|
TextCell {
|
||||||
|
width: DisplayWidth::from(major.len() + 1 + minor.len()),
|
||||||
|
contents: vec![
|
||||||
|
colours.size.major.paint(major),
|
||||||
|
colours.punctuation.paint(","),
|
||||||
|
colours.size.minor.paint(minor),
|
||||||
|
].into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
use output::column::SizeFormat;
|
||||||
|
use output::cell::{TextCell, DisplayWidth};
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use locale;
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directory() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.punctuation = Green.italic();
|
||||||
|
|
||||||
|
let directory = f::Size::None;
|
||||||
|
let expected = TextCell::blank(Green.italic());
|
||||||
|
assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_decimal() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.size.numbers = Blue.on(Red);
|
||||||
|
details.colours.size.unit = Yellow.bold();
|
||||||
|
|
||||||
|
let directory = f::Size::Some(2_100_000);
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(4),
|
||||||
|
contents: vec![
|
||||||
|
Blue.on(Red).paint("2.1"),
|
||||||
|
Yellow.bold().paint("M"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, directory.render(&details.colours, SizeFormat::DecimalBytes, &locale::Numeric::english()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_binary() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.size.numbers = Blue.on(Red);
|
||||||
|
details.colours.size.unit = Yellow.bold();
|
||||||
|
|
||||||
|
let directory = f::Size::Some(1_048_576);
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(5),
|
||||||
|
contents: vec![
|
||||||
|
Blue.on(Red).paint("1.0"),
|
||||||
|
Yellow.bold().paint("Mi"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, directory.render(&details.colours, SizeFormat::BinaryBytes, &locale::Numeric::english()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_bytes() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.size.numbers = Blue.on(Red);
|
||||||
|
|
||||||
|
let directory = f::Size::Some(1048576);
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(9),
|
||||||
|
contents: vec![
|
||||||
|
Blue.on(Red).paint("1,048,576"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn device_ids() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.size.major = Blue.on(Red);
|
||||||
|
details.colours.punctuation = Green.italic();
|
||||||
|
details.colours.size.minor = Cyan.on(Yellow);
|
||||||
|
|
||||||
|
let directory = f::Size::DeviceIDs(f::DeviceIDs { major: 10, minor: 80 });
|
||||||
|
let expected = TextCell {
|
||||||
|
width: DisplayWidth::from(5),
|
||||||
|
contents: vec![
|
||||||
|
Blue.on(Red).paint("10"),
|
||||||
|
Green.italic().paint(","),
|
||||||
|
Cyan.on(Yellow).paint("80"),
|
||||||
|
].into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(expected, directory.render(&details.colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||||
|
}
|
||||||
|
}
|
45
src/output/render/times.rs
Normal file
45
src/output/render/times.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use output::cell::TextCell;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use fs::fields as f;
|
||||||
|
|
||||||
|
use datetime::{LocalDateTime, TimeZone, DatePiece};
|
||||||
|
use datetime::fmt::DateFormat;
|
||||||
|
use locale;
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(trivial_numeric_casts)]
|
||||||
|
impl f::Time {
|
||||||
|
pub fn render(&self, colours: &Colours, tz: &Option<TimeZone>,
|
||||||
|
date_and_time: &DateFormat<'static>, date_and_year: &DateFormat<'static>,
|
||||||
|
time: &locale::Time, current_year: i64) -> TextCell {
|
||||||
|
|
||||||
|
// TODO(ogham): This method needs some serious de-duping!
|
||||||
|
// zoned and local times have different types at the moment,
|
||||||
|
// so it's tricky.
|
||||||
|
|
||||||
|
if let Some(ref tz) = *tz {
|
||||||
|
let date = tz.to_zoned(LocalDateTime::at(self.0 as i64));
|
||||||
|
|
||||||
|
let datestamp = if date.year() == current_year {
|
||||||
|
date_and_time.format(&date, time)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
date_and_year.format(&date, time)
|
||||||
|
};
|
||||||
|
|
||||||
|
TextCell::paint(colours.date, datestamp)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let date = LocalDateTime::at(self.0 as i64);
|
||||||
|
|
||||||
|
let datestamp = if date.year() == current_year {
|
||||||
|
date_and_time.format(&date, time)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
date_and_year.format(&date, time)
|
||||||
|
};
|
||||||
|
|
||||||
|
TextCell::paint(colours.date, datestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/output/render/users.rs
Normal file
90
src/output/render/users.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use users::Users;
|
||||||
|
|
||||||
|
use fs::fields as f;
|
||||||
|
use output::colours::Colours;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
|
||||||
|
|
||||||
|
impl f::User {
|
||||||
|
pub fn render(&self, colours: &Colours, users: &Users) -> TextCell {
|
||||||
|
let user_name = match users.get_user_by_uid(self.0) {
|
||||||
|
Some(user) => user.name().to_owned(),
|
||||||
|
None => self.0.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = if users.get_current_uid() == self.0 { colours.users.user_you }
|
||||||
|
else { colours.users.user_someone_else };
|
||||||
|
TextCell::paint(style, user_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(unused_results)]
|
||||||
|
pub mod test {
|
||||||
|
use output::details::Details;
|
||||||
|
|
||||||
|
use fs::fields as f;
|
||||||
|
use output::cell::TextCell;
|
||||||
|
|
||||||
|
use users::User;
|
||||||
|
use users::mock::MockUsers;
|
||||||
|
use ansi_term::Colour::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.user_you = Red.bold();
|
||||||
|
|
||||||
|
let mut users = MockUsers::with_current_uid(1000);
|
||||||
|
users.add_user(User::new(1000, "enoch", 100));
|
||||||
|
|
||||||
|
let user = f::User(1000);
|
||||||
|
let expected = TextCell::paint_str(Red.bold(), "enoch");
|
||||||
|
assert_eq!(expected, user.render(&details.colours, &users))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unnamed() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.user_you = Cyan.bold();
|
||||||
|
|
||||||
|
let users = MockUsers::with_current_uid(1000);
|
||||||
|
|
||||||
|
let user = f::User(1000);
|
||||||
|
let expected = TextCell::paint_str(Cyan.bold(), "1000");
|
||||||
|
assert_eq!(expected, user.render(&details.colours, &users));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_named() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.user_someone_else = Green.bold();
|
||||||
|
|
||||||
|
let mut users = MockUsers::with_current_uid(0);
|
||||||
|
users.add_user(User::new(1000, "enoch", 100));
|
||||||
|
|
||||||
|
let user = f::User(1000);
|
||||||
|
let expected = TextCell::paint_str(Green.bold(), "enoch");
|
||||||
|
assert_eq!(expected, user.render(&details.colours, &users));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_unnamed() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.user_someone_else = Red.normal();
|
||||||
|
|
||||||
|
let user = f::User(1000);
|
||||||
|
let expected = TextCell::paint_str(Red.normal(), "1000");
|
||||||
|
assert_eq!(expected, user.render(&details.colours, &MockUsers::with_current_uid(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overflow() {
|
||||||
|
let mut details = Details::default();
|
||||||
|
details.colours.users.user_someone_else = Blue.underline();
|
||||||
|
|
||||||
|
let user = f::User(2_147_483_648);
|
||||||
|
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
||||||
|
assert_eq!(expected, user.render(&details.colours, &MockUsers::with_current_uid(0)));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user