summaryrefslogtreecommitdiffstats
path: root/src/main.rs
blob: 4c75191510885246d14f43dcdd0135c6c53bd0f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#![forbid(unsafe_code)]
use anyhow::Result;
use clap::Clap;
use dua::{ByteFormat, TraversalSorting};
use std::{fs, io, io::Write, path::PathBuf, process};

mod crossdev;
#[cfg(any(feature = "tui-unix", feature = "tui-crossplatform"))]
mod interactive;
mod options;

fn stderr_if_tty() -> Option<io::Stderr> {
    if atty::is(atty::Stream::Stderr) {
        Some(io::stderr())
    } else {
        None
    }
}

#[cfg(not(all(target_os = "macos", target_arch = "aarch64"),))]
fn derive_default_threads(threads: usize) -> usize {
    threads
}

/// On Apple Silicon, high-efficiency cores make file accesses slower over all, so avoid over committing for
/// this one.
///
/// On macos with apple silicon, the IO subsystem is entirely different and one thread can mostly max it out.
/// Thus using more threads just burns energy unnecessarily.
/// It's notable that `du` is very fast even on a single core and more power efficient than dua with a single core.
/// The default of '4' seems related to the amount of performance cores present in the system.
/// On everything else, it's usually a good idea to use as many threads as possible for noticeable speedups.
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn derive_default_threads(threads: usize) -> usize {
    use sysinfo::{ProcessorExt, RefreshKind, SystemExt};
    if threads == 0 {
        sysinfo::System::new_with_specifics(RefreshKind::new().with_cpu())
            .get_processors()
            .get(0)
            .map_or(0, |p| match p.get_brand() {
                "Apple M1" => 4,
                other => {
                    eprintln!(
                        "Couldn't auto-configure correct amount of threads for {}. Create an issue here: https://github.com/byron/dua-cli/issues",
                        other
                    );
                    0
                }
            })
    } else {
        threads
    }
}

fn main() -> Result<()> {
    use options::Command::*;

    let opt: options::Args = options::Args::parse_from(wild::args_os());
    let threads = derive_default_threads(opt.threads);
    let walk_options = dua::WalkOptions {
        threads,
        byte_format: opt.format.map(Into::into).unwrap_or(ByteFormat::Metric),
        apparent_size: opt.apparent_size,
        count_hard_links: opt.count_hard_links,
        sorting: TraversalSorting::None,
        cross_filesystems: !opt.stay_on_filesystem,
    };
    let res = match opt.command {
        #[cfg(any(feature = "tui-unix", feature = "tui-crossplatform"))]
        Some(Interactive { input }) => {
            use crate::interactive::{Interaction, TerminalApp};
            use anyhow::{anyhow, Context};
            use crosstermion::terminal::{tui::new_terminal, AlternateRawScreen};

            let no_tty_msg = "Interactive mode requires a connected terminal";
            if atty::isnt(atty::Stream::Stdout) {
                return Err(anyhow!(no_tty_msg));
            }

            let mut terminal = new_terminal(
                AlternateRawScreen::try_from(io::stdout()).with_context(|| no_tty_msg)?,
            )
            .with_context(|| "Could not instantiate terminal")?;
            let res = TerminalApp::initialize(
                &mut terminal,
                walk_options,
                paths_from(input, !opt.stay_on_filesystem)?,
                Interaction::Full,
            )?
            .map(|(keys_rx, mut app)| {
                let res = app.process_events(&mut terminal, keys_rx.into_iter());
                // Leak app memory to avoid having to wait for the hashmap to deallocate, which causes a noticeable delay shortly before the the
                // program exits anyway.
                std::mem::forget(app);
                res
            });

            drop(terminal);
            io::stdout().flush().ok();

            // Exit 'quickly' to avoid having to not have to deal with slightly different types in the other match branches
            std::process::exit(res.transpose()?.map(|e| e.to_exit_code()).unwrap_or(0));
        }
        Some(Aggregate {
            input,
            no_total,
            no_sort,
            statistics,
        }) => {
            let stdout = io::stdout();
            let stdout_locked = stdout.lock();
            let (res, stats) = dua::aggregate(
                stdout_locked,
                stderr_if_tty(),
                walk_options,
                !no_total,
                !no_sort,
                paths_from(input, !opt.stay_on_filesystem)?,
            )?;
            if statistics {
                writeln!(io::stderr(), "{:?}", stats).ok();
            }
            res
        }
        None => {
            let stdout = io::stdout();
            let stdout_locked = stdout.lock();
            dua::aggregate(
                stdout_locked,
                stderr_if_tty(),
                walk_options,
                true,
                true,
                paths_from(opt.input, !opt.stay_on_filesystem)?,
            )?
            .0
        }
    };

    process::exit(res.to_exit_code());
}

fn paths_from(paths: Vec<PathBuf>, cross_filesystems: bool) -> Result<Vec<PathBuf>, io::Error> {
    let device_id = std::env::current_dir()
        .ok()
        .and_then(|cwd| crossdev::init(&cwd).ok());

    if paths.is_empty() {
        cwd_dirlist().map(|paths| match device_id {
            Some(device_id) if !cross_filesystems => paths
                .into_iter()
                .filter(|p| match p.metadata() {
                    Ok(meta) => crossdev::is_same_device(device_id, &meta),
                    Err(_) => true,
                })
                .collect(),
            _ => paths,
        })
    } else {
        Ok(paths)
    }
}

fn cwd_dirlist() -> Result<Vec<PathBuf>, io::Error> {
    let mut v: Vec<_> = fs::read_dir(".")?
        .filter_map(|e| {
            e.ok()
                .and_then(|e| e.path().strip_prefix(".").ok().map(ToOwned::to_owned))
        })
        .filter(|p| {
            if let Ok(meta) = p.symlink_metadata() {
                if meta.file_type().is_symlink() {
                    return false;
                }
            };
            true
        })
        .collect();
    v.sort();
    Ok(v)
}