eza 0.23.4

A modern replacement for ls
Documentation
// SPDX-FileCopyrightText: 2024 Christina Sørensen
// SPDX-License-Identifier: EUPL-1.2
//
// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
// SPDX-FileCopyrightText: 2014 Benjamin Sago
// SPDX-License-Identifier: MIT
use std::iter;

use nu_ansi_term::{AnsiString as ANSIString, Style};

use crate::fs::fields as f;
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::render::FiletypeColours;

pub trait PermissionsPlusRender {
    fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell;
}

impl PermissionsPlusRender for Option<f::PermissionsPlus> {
    #[cfg(unix)]
    fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell {
        if let Some(p) = self {
            let mut chars = vec![p.file_type.render(colours)];
            let permissions = p.permissions;
            chars.extend(Some(permissions).render(colours, p.file_type.is_regular_file()));

            if p.xattrs {
                chars.push(colours.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(),
            }
        } else {
            let chars: Vec<_> = iter::repeat_n(colours.dash().paint("-"), 10).collect();
            TextCell {
                width: DisplayWidth::from(chars.len()),
                contents: chars.into(),
            }
        }
    }

    #[cfg(windows)]
    fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell {
        match self {
            Some(p) => {
                let mut chars = vec![p.attributes.render_type(colours)];
                chars.extend(p.attributes.render(colours));

                TextCell {
                    width: DisplayWidth::from(chars.len()),
                    contents: chars.into(),
                }
            }
            None => TextCell {
                width: DisplayWidth::from(0),
                contents: vec![].into(),
            },
        }
    }
}

#[allow(unused)]
pub trait RenderPermissions {
    fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>>;
}

impl RenderPermissions for Option<f::Permissions> {
    fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>> {
        match self {
            Some(p) => {
                let bit = |bit, chr: &'static str, style: Style| {
                    if bit {
                        style.paint(chr)
                    } else {
                        colours.dash().paint("-")
                    }
                };

                vec![
                    bit(p.user_read, "r", colours.user_read()),
                    bit(p.user_write, "w", colours.user_write()),
                    p.user_execute_bit(colours, is_regular_file),
                    bit(p.group_read, "r", colours.group_read()),
                    bit(p.group_write, "w", colours.group_write()),
                    p.group_execute_bit(colours),
                    bit(p.other_read, "r", colours.other_read()),
                    bit(p.other_write, "w", colours.other_write()),
                    p.other_execute_bit(colours),
                ]
            }
            None => iter::repeat_n(colours.dash().paint("-"), 9).collect(),
        }
    }
}

impl f::Permissions {
    fn user_execute_bit<C: Colours>(
        &self,
        colours: &C,
        is_regular_file: bool,
    ) -> ANSIString<'static> {
        #[rustfmt::skip]
        return match (self.user_execute, self.setuid, is_regular_file) {
            (false, false, _)      => colours.dash().paint("-"),
            (true,  false, false)  => colours.user_execute_other().paint("x"),
            (true,  false, true)   => colours.user_execute_file().paint("x"),
            (false, true,  _)      => colours.special_other().paint("S"),
            (true,  true,  false)  => colours.special_other().paint("s"),
            (true,  true,  true)   => colours.special_user_file().paint("s"),
        };
    }

    fn group_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
        #[rustfmt::skip]
        return match (self.group_execute, self.setgid) {
            (false, false)  => colours.dash().paint("-"),
            (true,  false)  => colours.group_execute().paint("x"),
            (false, true)   => colours.special_other().paint("S"),
            (true,  true)   => colours.special_other().paint("s"),
        };
    }

    fn other_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
        #[rustfmt::skip]
        return match (self.other_execute, self.sticky) {
            (false, false)  => colours.dash().paint("-"),
            (true,  false)  => colours.other_execute().paint("x"),
            (false, true)   => colours.special_other().paint("T"),
            (true,  true)   => colours.special_other().paint("t"),
        };
    }
}

#[cfg(windows)]
impl f::Attributes {
    pub fn render<C: Colours + FiletypeColours>(self, colours: &C) -> Vec<ANSIString<'static>> {
        let bit = |bit, chr: &'static str, style: Style| {
            if bit {
                style.paint(chr)
            } else {
                colours.dash().paint("-")
            }
        };

        vec![
            bit(self.archive, "a", colours.normal()),
            bit(self.readonly, "r", colours.user_read()),
            bit(self.hidden, "h", colours.special_user_file()),
            bit(self.system, "s", colours.special_other()),
        ]
    }

    pub fn render_type<C: Colours + FiletypeColours>(self, colours: &C) -> ANSIString<'static> {
        if self.reparse_point {
            return colours.pipe().paint("l");
        } else if self.directory {
            return colours.directory().paint("d");
        }
        colours.dash().paint("-")
    }
}

pub trait Colours {
    fn dash(&self) -> Style;

    fn user_read(&self) -> Style;
    fn user_write(&self) -> Style;
    fn user_execute_file(&self) -> Style;
    fn user_execute_other(&self) -> Style;

    fn group_read(&self) -> Style;
    fn group_write(&self) -> Style;
    fn group_execute(&self) -> Style;

    fn other_read(&self) -> Style;
    fn other_write(&self) -> Style;
    fn other_execute(&self) -> Style;

    fn special_user_file(&self) -> Style;
    fn special_other(&self) -> Style;

    fn attribute(&self) -> Style;
}

#[cfg(test)]
#[allow(unused_results)]
pub mod test {
    use super::{Colours, RenderPermissions};
    use crate::fs::fields as f;
    use crate::output::cell::TextCellContents;

    use nu_ansi_term::Color::*;
    use nu_ansi_term::Style;

    struct TestColours;

    #[rustfmt::skip]
    impl Colours for TestColours {
        fn dash(&self)                -> Style { Fixed(11).normal() }
        fn user_read(&self)           -> Style { Fixed(101).normal() }
        fn user_write(&self)          -> Style { Fixed(102).normal() }
        fn user_execute_file(&self)   -> Style { Fixed(103).normal() }
        fn user_execute_other(&self)  -> Style { Fixed(113).normal() }
        fn group_read(&self)          -> Style { Fixed(104).normal() }
        fn group_write(&self)         -> Style { Fixed(105).normal() }
        fn group_execute(&self)       -> Style { Fixed(106).normal() }
        fn other_read(&self)          -> Style { Fixed(107).normal() }
        fn other_write(&self)         -> Style { Fixed(108).normal() }
        fn other_execute(&self)       -> Style { Fixed(109).normal() }
        fn special_user_file(&self)   -> Style { Fixed(110).normal() }
        fn special_other(&self)       -> Style { Fixed(111).normal() }
        fn attribute(&self)           -> Style { Fixed(112).normal() }
    }

    #[test]
    fn negate() {
        let bits = Some(f::Permissions {
            user_read: false,
            user_write: false,
            user_execute: false,
            setuid: false,
            group_read: false,
            group_write: false,
            group_execute: false,
            setgid: false,
            other_read: false,
            other_write: false,
            other_execute: false,
            sticky: false,
        });

        let expected = TextCellContents::from(vec![
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
        ]);

        assert_eq!(expected, bits.render(&TestColours, false).into());
    }

    #[test]
    fn affirm() {
        let bits = Some(f::Permissions {
            user_read: true,
            user_write: true,
            user_execute: true,
            setuid: false,
            group_read: true,
            group_write: true,
            group_execute: true,
            setgid: false,
            other_read: true,
            other_write: true,
            other_execute: true,
            sticky: false,
        });

        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(&TestColours, true).into());
    }

    #[test]
    fn specials() {
        let bits = Some(f::Permissions {
            user_read: false,
            user_write: false,
            user_execute: true,
            setuid: true,
            group_read: false,
            group_write: false,
            group_execute: true,
            setgid: true,
            other_read: false,
            other_write: false,
            other_execute: true,
            sticky: true,
        });

        let expected = TextCellContents::from(vec![
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(110).paint("s"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(111).paint("s"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(111).paint("t"),
        ]);

        assert_eq!(expected, bits.render(&TestColours, true).into());
    }

    #[test]
    fn extra_specials() {
        let bits = Some(f::Permissions {
            user_read: false,
            user_write: false,
            user_execute: false,
            setuid: true,
            group_read: false,
            group_write: false,
            group_execute: false,
            setgid: true,
            other_read: false,
            other_write: false,
            other_execute: false,
            sticky: true,
        });

        let expected = TextCellContents::from(vec![
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(111).paint("S"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(111).paint("S"),
            Fixed(11).paint("-"),
            Fixed(11).paint("-"),
            Fixed(111).paint("T"),
        ]);

        assert_eq!(expected, bits.render(&TestColours, true).into());
    }
}