Bladeren bron

Merge pull request #820 from skyline75489/chesterliu/dev/win-support

Initial support for Windows
Mélanie Chauvel 3 jaren geleden
bovenliggende
commit
f3ca1fe6f7

+ 3 - 1
Cargo.toml

@@ -31,9 +31,11 @@ scoped_threadpool = "0.1"
 term_grid = "0.2.0"
 terminal_size = "0.1.16"
 unicode-width = "0.1"
-users = "0.11"
 zoneinfo_compiled = "0.5.1"
 
+[target.'cfg(unix)'.dependencies]
+users = "0.11"
+
 [dependencies.datetime]
 version = "0.5.2"
 default-features = false

+ 7 - 0
src/fs/dir.rs

@@ -111,6 +111,13 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
                     continue;
                 }
 
+                // Also hide _prefix files on Windows because it's used by old applications
+                // as an alternative to dot-prefix files.
+                #[cfg(windows)]
+                if ! self.dotfiles && filename.starts_with('_') {
+                    continue;
+                }
+
                 if self.git_ignoring {
                     let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default();
                     if git_status.unstaged == GitStatus::Ignored {

+ 9 - 0
src/fs/feature/git.rs

@@ -296,6 +296,7 @@ impl Git {
 /// Paths need to be absolute for them to be compared properly, otherwise
 /// you’d ask a repo about “./README.md” but it only knows about
 /// “/vagrant/README.md”, prefixed by the workdir.
+#[cfg(unix)]
 fn reorient(path: &Path) -> PathBuf {
     use std::env::current_dir;
 
@@ -308,6 +309,14 @@ fn reorient(path: &Path) -> PathBuf {
     path.canonicalize().unwrap_or(path)
 }
 
+#[cfg(windows)]
+fn reorient(path: &Path) -> PathBuf {
+    let unc_path = path.canonicalize().unwrap();
+    // On Windows UNC path is returned. We need to strip the prefix for it to work.
+    let normal_path = unc_path.as_os_str().to_str().unwrap().trim_left_matches("\\\\?\\");
+    return PathBuf::from(normal_path);
+}
+
 /// The character to display if the file has been modified, but not staged.
 fn working_tree_status(status: git2::Status) -> f::GitStatus {
     match status {

+ 14 - 0
src/fs/fields.rs

@@ -82,13 +82,27 @@ pub struct Permissions {
     pub setuid:         bool,
 }
 
+/// The file's FileAttributes field, available only on Windows.
+#[derive(Copy, Clone)]
+pub struct Attributes {
+    pub archive:         bool,
+    pub directory:       bool,
+    pub readonly:        bool,
+    pub hidden:          bool,
+    pub system:          bool,
+    pub reparse_point:   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.
 #[derive(Copy, Clone)]
 pub struct PermissionsPlus {
     pub file_type:   Type,
+    #[cfg(unix)]
     pub permissions: Permissions,
+    #[cfg(windows)]
+    pub attributes:  Attributes,
     pub xattrs:      bool,
 }
 

+ 63 - 0
src/fs/file.rs

@@ -1,7 +1,10 @@
 //! Files, and methods and fields to access their metadata.
 
 use std::io;
+#[cfg(unix)]
 use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
+#[cfg(windows)]
+use std::os::windows::fs::MetadataExt;
 use std::path::{Path, PathBuf};
 use std::time::{Duration, SystemTime, UNIX_EPOCH};
 
@@ -174,6 +177,7 @@ impl<'dir> File<'dir> {
     /// Whether this file is both a regular file *and* executable for the
     /// current user. An executable file has a different purpose from an
     /// executable directory, so they should be highlighted differently.
+    #[cfg(unix)]
     pub fn is_executable_file(&self) -> bool {
         let bit = modes::USER_EXECUTE;
         self.is_file() && (self.metadata.permissions().mode() & bit) == bit
@@ -185,21 +189,25 @@ impl<'dir> File<'dir> {
     }
 
     /// Whether this file is a named pipe on the filesystem.
+    #[cfg(unix)]
     pub fn is_pipe(&self) -> bool {
         self.metadata.file_type().is_fifo()
     }
 
     /// Whether this file is a char device on the filesystem.
+    #[cfg(unix)]
     pub fn is_char_device(&self) -> bool {
         self.metadata.file_type().is_char_device()
     }
 
     /// Whether this file is a block device on the filesystem.
+    #[cfg(unix)]
     pub fn is_block_device(&self) -> bool {
         self.metadata.file_type().is_block_device()
     }
 
     /// Whether this file is a socket on the filesystem.
+    #[cfg(unix)]
     pub fn is_socket(&self) -> bool {
         self.metadata.file_type().is_socket()
     }
@@ -270,6 +278,7 @@ impl<'dir> File<'dir> {
     /// is uncommon, while you come across directories and other types
     /// with multiple links much more often. Thus, it should get highlighted
     /// more attentively.
+    #[cfg(unix)]
     pub fn links(&self) -> f::Links {
         let count = self.metadata.nlink();
 
@@ -280,6 +289,7 @@ impl<'dir> File<'dir> {
     }
 
     /// This file’s inode.
+    #[cfg(unix)]
     pub fn inode(&self) -> f::Inode {
         f::Inode(self.metadata.ino())
     }
@@ -287,6 +297,7 @@ impl<'dir> File<'dir> {
     /// This file’s number of filesystem blocks.
     ///
     /// (Not the size of each block, which we don’t actually report on)
+    #[cfg(unix)]
     pub fn blocks(&self) -> f::Blocks {
         if self.is_file() || self.is_link() {
             f::Blocks::Some(self.metadata.blocks())
@@ -297,11 +308,13 @@ impl<'dir> File<'dir> {
     }
 
     /// The ID of the user that own this file.
+    #[cfg(unix)]
     pub fn user(&self) -> f::User {
         f::User(self.metadata.uid())
     }
 
     /// The ID of the group that owns this file.
+    #[cfg(unix)]
     pub fn group(&self) -> f::Group {
         f::Group(self.metadata.gid())
     }
@@ -314,6 +327,7 @@ impl<'dir> File<'dir> {
     ///
     /// Block and character devices return their device IDs, because they
     /// usually just have a file size of zero.
+    #[cfg(unix)]
     pub fn size(&self) -> f::Size {
         if self.is_directory() {
             f::Size::None
@@ -335,12 +349,23 @@ impl<'dir> File<'dir> {
         }
     }
 
+    #[cfg(windows)]
+    pub fn size(&self) -> f::Size {
+        if self.is_directory() {
+            f::Size::None
+        }
+        else {
+            f::Size::Some(self.metadata.len())
+        }
+    }
+
     /// This file’s last modified timestamp, if available on this platform.
     pub fn modified_time(&self) -> Option<SystemTime> {
         self.metadata.modified().ok()
     }
 
     /// This file’s last changed timestamp, if available on this platform.
+    #[cfg(unix)]
     pub fn changed_time(&self) -> Option<SystemTime> {
         let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
 
@@ -359,6 +384,11 @@ impl<'dir> File<'dir> {
         }
     }
 
+    #[cfg(windows)]
+    pub fn changed_time(&self) -> Option<SystemTime> {
+        return self.modified_time()
+    }
+
     /// This file’s last accessed timestamp, if available on this platform.
     pub fn accessed_time(&self) -> Option<SystemTime> {
         self.metadata.accessed().ok()
@@ -374,6 +404,7 @@ impl<'dir> File<'dir> {
     /// This is used a the leftmost character of the permissions column.
     /// The file type can usually be guessed from the colour of the file, but
     /// ls puts this character there.
+    #[cfg(unix)]
     pub fn type_char(&self) -> f::Type {
         if self.is_file() {
             f::Type::File
@@ -401,7 +432,21 @@ impl<'dir> File<'dir> {
         }
     }
 
+    #[cfg(windows)]
+    pub fn type_char(&self) -> f::Type {
+        if self.is_file() {
+            f::Type::File
+        }
+        else if self.is_directory() {
+            f::Type::Directory
+        }
+        else {
+            f::Type::Special
+        }
+    }
+
     /// This file’s permissions, with flags for each bit.
+    #[cfg(unix)]
     pub fn permissions(&self) -> f::Permissions {
         let bits = self.metadata.mode();
         let has_bit = |bit| bits & bit == bit;
@@ -425,6 +470,22 @@ impl<'dir> File<'dir> {
         }
     }
 
+    #[cfg(windows)]
+    pub fn attributes(&self) -> f::Attributes {
+        let bits = self.metadata.file_attributes();
+        let has_bit = |bit| bits & bit == bit;
+
+        // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
+        f::Attributes {
+            directory:      has_bit(0x10),
+            archive:        has_bit(0x20),
+            readonly:       has_bit(0x1),
+            hidden:         has_bit(0x2),
+            system:         has_bit(0x4),
+            reparse_point:  has_bit(0x400),
+        }
+    }
+
     /// Whether this file’s extension is any of the strings that get passed in.
     ///
     /// This will always return `false` if the file has no extension.
@@ -482,6 +543,7 @@ impl<'dir> FileTarget<'dir> {
 
 /// More readable aliases for the permission bits exposed by libc.
 #[allow(trivial_numeric_casts)]
+#[cfg(unix)]
 mod modes {
 
     // The `libc::mode_t` type’s actual type varies, but the value returned
@@ -559,6 +621,7 @@ mod filename_test {
     }
 
     #[test]
+    #[cfg(unix)]
     fn topmost() {
         assert_eq!("/", File::filename(Path::new("/")))
     }

+ 3 - 0
src/fs/filter.rs

@@ -2,6 +2,7 @@
 
 use std::cmp::Ordering;
 use std::iter::FromIterator;
+#[cfg(unix)]
 use std::os::unix::fs::MetadataExt;
 
 use crate::fs::DotFilter;
@@ -130,6 +131,7 @@ pub enum SortField {
 
     /// The file’s inode, which usually corresponds to the order in which
     /// files were created on the filesystem, more or less.
+    #[cfg(unix)]
     FileInode,
 
     /// The time the file was modified (the “mtime”).
@@ -223,6 +225,7 @@ impl SortField {
             Self::Name(AaBbCc)  => natord::compare_ignore_case(&a.name, &b.name),
 
             Self::Size          => a.metadata.len().cmp(&b.metadata.len()),
+            #[cfg(unix)]
             Self::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
             Self::ModifiedDate  => a.modified_time().cmp(&b.modified_time()),
             Self::AccessedDate  => a.accessed_time().cmp(&b.accessed_time()),

+ 6 - 0
src/main.rs

@@ -49,12 +49,18 @@ mod theme;
 fn main() {
     use std::process::exit;
 
+    #[cfg(unix)]
     unsafe {
         libc::signal(libc::SIGPIPE, libc::SIG_DFL);
     }
 
     logger::configure(env::var_os(vars::EXA_DEBUG));
 
+    #[cfg(windows)]
+    if let Err(e) = ansi_term::enable_ansi_support() {
+        warn!("Failed to enable ANSI support: {}", e);
+    }
+
     let args: Vec<_> = env::args_os().skip(1).collect();
     match Options::parse(args.iter().map(|e| e.as_ref()), &LiveVars) {
         OptionsResult::Ok(options, mut input_paths) => {

+ 1 - 0
src/options/filter.rs

@@ -88,6 +88,7 @@ impl SortField {
             "cr" | "created" => {
                 Self::CreatedDate
             }
+            #[cfg(unix)]
             "inode" => {
                 Self::FileInode
             }

+ 34 - 13
src/options/parser.rs

@@ -146,8 +146,6 @@ impl Args {
     pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
     where I: IntoIterator<Item = &'args OsStr>
     {
-        use std::os::unix::ffi::OsStrExt;
-
         let mut parsing = true;
 
         // The results that get built up.
@@ -159,7 +157,7 @@ impl Args {
         // doesn’t have one in its string so it needs the next one.
         let mut inputs = inputs.into_iter();
         while let Some(arg) = inputs.next() {
-            let bytes = arg.as_bytes();
+            let bytes = os_str_to_bytes(arg);
 
             // Stop parsing if one of the arguments is the literal string “--”.
             // This allows a file named “--arg” to be specified by passing in
@@ -174,7 +172,7 @@ impl Args {
 
             // If the string starts with *two* dashes then it’s a long argument.
             else if bytes.starts_with(b"--") {
-                let long_arg_name = OsStr::from_bytes(&bytes[2..]);
+                let long_arg_name = bytes_to_os_str(&bytes[2..]);
 
                 // If there’s an equals in it, then the string before the
                 // equals will be the flag’s name, and the string after it
@@ -221,7 +219,7 @@ impl Args {
             // If the string starts with *one* dash then it’s one or more
             // short arguments.
             else if bytes.starts_with(b"-") && arg != "-" {
-                let short_arg = OsStr::from_bytes(&bytes[1..]);
+                let short_arg = bytes_to_os_str(&bytes[1..]);
 
                 // If there’s an equals in it, then the argument immediately
                 // before the equals was the one that has the value, with the
@@ -236,7 +234,7 @@ impl Args {
                 // it’s an error if any of the first set of arguments actually
                 // takes a value.
                 if let Some((before, after)) = split_on_equals(short_arg) {
-                    let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap();
+                    let (arg_with_value, other_args) = os_str_to_bytes(before).split_last().unwrap();
 
                     // Process the characters immediately following the dash...
                     for byte in other_args {
@@ -291,7 +289,7 @@ impl Args {
                             TakesValue::Optional(values) => {
                                 if index < bytes.len() - 1 {
                                     let remnants = &bytes[index+1 ..];
-                                    result_flags.push((flag, Some(OsStr::from_bytes(remnants))));
+                                    result_flags.push((flag, Some(bytes_to_os_str(remnants))));
                                     break;
                                 }
                                 else if let Some(next_arg) = inputs.next() {
@@ -495,19 +493,42 @@ impl fmt::Display for ParseError {
     }
 }
 
+#[cfg(unix)]
+fn os_str_to_bytes<'b>(s: &'b OsStr) ->  &'b [u8]{
+    use std::os::unix::ffi::OsStrExt;
+
+    return s.as_bytes()
+}
+
+#[cfg(unix)]
+fn bytes_to_os_str<'b>(b:  &'b [u8]) ->  &'b OsStr{
+    use std::os::unix::ffi::OsStrExt;
+
+    return OsStr::from_bytes(b);
+}
+
+#[cfg(windows)]
+fn os_str_to_bytes<'b>(s: &'b OsStr) ->  &'b [u8]{
+    return s.to_str().unwrap().as_bytes()
+}
+
+#[cfg(windows)]
+fn bytes_to_os_str<'b>(b:  &'b [u8]) ->  &'b OsStr{
+    use std::str;
+
+    return OsStr::new(str::from_utf8(b).unwrap());
+}
 
 /// Splits a string on its `=` character, returning the two substrings on
 /// either side. Returns `None` if there’s no equals or a string is missing.
 fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
-    use std::os::unix::ffi::OsStrExt;
-
-    if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') {
-        let (before, after) = input.as_bytes().split_at(index);
+    if let Some(index) = os_str_to_bytes(input).iter().position(|elem| *elem == b'=') {
+        let (before, after) = os_str_to_bytes(input).split_at(index);
 
         // The after string contains the = that we need to remove.
         if ! before.is_empty() && after.len() >= 2 {
-            return Some((OsStr::from_bytes(before),
-                         OsStr::from_bytes(&after[1..])))
+            return Some((bytes_to_os_str(before),
+                         bytes_to_os_str(&after[1..])))
         }
     }
 

+ 21 - 2
src/output/file_name.rs

@@ -226,7 +226,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         let coconut = parent.components().count();
 
         if coconut == 1 && parent.has_root() {
-            bits.push(self.colours.symlink_path().paint("/"));
+            bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
         }
         else if coconut >= 1 {
             escape(
@@ -235,12 +235,13 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                 self.colours.symlink_path(),
                 self.colours.control_char(),
             );
-            bits.push(self.colours.symlink_path().paint("/"));
+            bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
         }
     }
 
     /// The character to be displayed after a file when classifying is on, if
     /// the file’s type has one associated with it.
+    #[cfg(unix)]
     fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
         if file.is_executable_file() {
             Some("*")
@@ -262,6 +263,19 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         }
     }
 
+    #[cfg(windows)]
+    fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
+        if file.is_directory() {
+            Some("/")
+        }
+        else if file.is_link() {
+            Some("@")
+        }
+        else {
+            None
+        }
+    }
+
     /// Returns at least one ANSI-highlighted string representing this file’s
     /// name using the given set of colours.
     ///
@@ -301,11 +315,16 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
 
         match self.file {
             f if f.is_directory()        => self.colours.directory(),
+            #[cfg(unix)]
             f if f.is_executable_file()  => self.colours.executable_file(),
             f if f.is_link()             => self.colours.symlink(),
+            #[cfg(unix)]
             f if f.is_pipe()             => self.colours.pipe(),
+            #[cfg(unix)]
             f if f.is_block_device()     => self.colours.block_device(),
+            #[cfg(unix)]
             f if f.is_char_device()      => self.colours.char_device(),
+            #[cfg(unix)]
             f if f.is_socket()           => self.colours.socket(),
             f if ! f.is_file()           => self.colours.special(),
             _                            => self.colours.colour_file(self.file),

+ 4 - 0
src/output/render/mod.rs

@@ -7,7 +7,9 @@ pub use self::filetype::Colours as FiletypeColours;
 mod git;
 pub use self::git::Colours as GitColours;
 
+#[cfg(unix)]
 mod groups;
+#[cfg(unix)]
 pub use self::groups::Colours as GroupColours;
 
 mod inode;
@@ -26,7 +28,9 @@ mod times;
 pub use self::times::Render as TimeRender;
 // times does too
 
+#[cfg(unix)]
 mod users;
+#[cfg(unix)]
 pub use self::users::Colours as UserColours;
 
 mod octal;

+ 39 - 0
src/output/render/permissions.rs

@@ -6,6 +6,7 @@ use crate::output::render::FiletypeColours;
 
 
 impl f::PermissionsPlus {
+    #[cfg(unix)]
     pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
         let mut chars = vec![ self.file_type.render(colours) ];
         chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
@@ -22,6 +23,17 @@ impl f::PermissionsPlus {
             contents: chars.into(),
         }
     }
+
+    #[cfg(windows)]
+    pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
+        let mut chars = vec![ self.attributes.render_type(colours) ];
+        chars.extend(self.attributes.render(colours));
+
+        TextCell {
+            width:    DisplayWidth::from(chars.len()),
+            contents: chars.into(),
+        }
+    }
 }
 
 
@@ -76,6 +88,33 @@ impl f::Permissions {
     }
 }
 
+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")
+        }
+        else {
+            return colours.dash().paint("-")
+        }
+    }
+}
 
 pub trait Colours {
     fn dash(&self) -> Style;

+ 73 - 1
src/output/table.rs

@@ -1,6 +1,7 @@
 use std::cmp::max;
 use std::env;
 use std::ops::Deref;
+#[cfg(unix)]
 use std::sync::{Mutex, MutexGuard};
 
 use datetime::TimeZone;
@@ -8,6 +9,7 @@ use zoneinfo_compiled::{CompiledData, Result as TZResult};
 
 use lazy_static::lazy_static;
 use log::*;
+#[cfg(unix)]
 use users::UsersCache;
 
 use crate::fs::{File, fields as f};
@@ -54,10 +56,12 @@ impl Columns {
         let mut columns = Vec::with_capacity(4);
 
         if self.inode {
+            #[cfg(unix)]
             columns.push(Column::Inode);
         }
 
         if self.octal {
+            #[cfg(unix)]
             columns.push(Column::Octal);
         }
 
@@ -66,6 +70,7 @@ impl Columns {
         }
 
         if self.links {
+            #[cfg(unix)]
             columns.push(Column::HardLinks);
         }
 
@@ -74,14 +79,17 @@ impl Columns {
         }
 
         if self.blocks {
+            #[cfg(unix)]
             columns.push(Column::Blocks);
         }
 
         if self.user {
+            #[cfg(unix)]
             columns.push(Column::User);
         }
 
         if self.group {
+            #[cfg(unix)]
             columns.push(Column::Group);
         }
 
@@ -116,12 +124,18 @@ pub enum Column {
     Permissions,
     FileSize,
     Timestamp(TimeType),
+    #[cfg(unix)]
     Blocks,
+    #[cfg(unix)]
     User,
+    #[cfg(unix)]
     Group,
+    #[cfg(unix)]
     HardLinks,
+    #[cfg(unix)]
     Inode,
     GitStatus,
+    #[cfg(unix)]
     Octal,
 }
 
@@ -136,6 +150,7 @@ pub enum Alignment {
 impl Column {
 
     /// Get the alignment this column should use.
+    #[cfg(unix)]
     pub fn alignment(self) -> Alignment {
         match self {
             Self::FileSize   |
@@ -147,19 +162,37 @@ impl Column {
         }
     }
 
+    #[cfg(windows)]
+    pub fn alignment(&self) -> Alignment {
+        match self {
+            Self::FileSize   |
+            Self::GitStatus  => Alignment::Right,
+            _                => Alignment::Left,
+        }
+    }
+
     /// Get the text that should be printed at the top, when the user elects
     /// to have a header row printed.
     pub fn header(self) -> &'static str {
         match self {
+            #[cfg(unix)]
             Self::Permissions   => "Permissions",
+            #[cfg(windows)]
+            Self::Permissions   => "Mode",
             Self::FileSize      => "Size",
             Self::Timestamp(t)  => t.header(),
+            #[cfg(unix)]
             Self::Blocks        => "Blocks",
+            #[cfg(unix)]
             Self::User          => "User",
+            #[cfg(unix)]
             Self::Group         => "Group",
+            #[cfg(unix)]
             Self::HardLinks     => "Links",
+            #[cfg(unix)]
             Self::Inode         => "inode",
             Self::GitStatus     => "Git",
+            #[cfg(unix)]
             Self::Octal         => "Octal",
         }
     }
@@ -274,10 +307,12 @@ pub struct Environment {
     tz: Option<TimeZone>,
 
     /// Mapping cache of user IDs to usernames.
+    #[cfg(unix)]
     users: Mutex<UsersCache>,
 }
 
 impl Environment {
+    #[cfg(unix)]
     pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
         self.users.lock().unwrap()
     }
@@ -296,12 +331,14 @@ impl Environment {
         let numeric = locale::Numeric::load_user_locale()
                              .unwrap_or_else(|_| locale::Numeric::english());
 
+        #[cfg(unix)]
         let users = Mutex::new(UsersCache::new());
 
-        Self { numeric, tz, users }
+        Self { numeric, tz, #[cfg(unix)] users }
     }
 }
 
+#[cfg(unix)]
 fn determine_time_zone() -> TZResult<TimeZone> {
     if let Ok(file) = env::var("TZ") {
         TimeZone::from_file({
@@ -322,6 +359,31 @@ fn determine_time_zone() -> TZResult<TimeZone> {
     }
 }
 
+#[cfg(windows)]
+fn determine_time_zone() -> TZResult<TimeZone> {
+    use datetime::zone::{FixedTimespan, FixedTimespanSet, StaticTimeZone, TimeZoneSource};
+    use std::borrow::Cow;
+
+    Ok(TimeZone(TimeZoneSource::Static(&StaticTimeZone {
+        name: "Unsupported",
+        fixed_timespans: FixedTimespanSet {
+            first: FixedTimespan {
+                offset: 0,
+                is_dst: false,
+                name: Cow::Borrowed("ZONE_A"),
+            },
+            rest: &[(
+                1206838800,
+                FixedTimespan {
+                    offset: 3600,
+                    is_dst: false,
+                    name: Cow::Borrowed("ZONE_B"),
+                },
+            )],
+        },
+    })))
+}
+
 lazy_static! {
     static ref ENVIRONMENT: Environment = Environment::load_all();
 }
@@ -388,11 +450,15 @@ impl<'a, 'f> Table<'a> {
     fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus {
         f::PermissionsPlus {
             file_type: file.type_char(),
+            #[cfg(unix)]
             permissions: file.permissions(),
+            #[cfg(windows)]
+            attributes: file.attributes(),
             xattrs,
         }
     }
 
+    #[cfg(unix)]
     fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions {
         f::OctalPermissions {
             permissions: file.permissions(),
@@ -407,24 +473,30 @@ impl<'a, 'f> Table<'a> {
             Column::FileSize => {
                 file.size().render(self.theme, self.size_format, &self.env.numeric)
             }
+            #[cfg(unix)]
             Column::HardLinks => {
                 file.links().render(self.theme, &self.env.numeric)
             }
+            #[cfg(unix)]
             Column::Inode => {
                 file.inode().render(self.theme.ui.inode)
             }
+            #[cfg(unix)]
             Column::Blocks => {
                 file.blocks().render(self.theme)
             }
+            #[cfg(unix)]
             Column::User => {
                 file.user().render(self.theme, &*self.env.lock_users(), self.user_format)
             }
+            #[cfg(unix)]
             Column::Group => {
                 file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
             }
             Column::GitStatus => {
                 self.git_status(file).render(self.theme)
             }
+            #[cfg(unix)]
             Column::Octal => {
                 self.octal_permissions(file).render(self.theme.ui.octal)
             }

+ 2 - 0
src/theme/mod.rs

@@ -229,6 +229,7 @@ impl render::GitColours for Theme {
     fn conflicted(&self)    -> Style { self.ui.git.conflicted }
 }
 
+#[cfg(unix)]
 impl render::GroupColours for Theme {
     fn yours(&self)      -> Style { self.ui.users.group_yours }
     fn not_yours(&self)  -> Style { self.ui.users.group_not_yours }
@@ -287,6 +288,7 @@ impl render::SizeColours for Theme {
     fn minor(&self)   -> Style { self.ui.size.minor }
 }
 
+#[cfg(unix)]
 impl render::UserColours for Theme {
     fn you(&self)           -> Style { self.ui.users.user_you }
     fn someone_else(&self)  -> Style { self.ui.users.user_someone_else }