• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tamada / sibling / 16365780383

18 Jul 2025 08:18AM UTC coverage: 59.751% (-10.5%) from 70.292%
16365780383

push

github

web-flow
Merge pull request #38 from tamada/release/v2.0.1

Release/v2.0.1

246 of 414 new or added lines in 5 files covered. (59.42%)

288 of 482 relevant lines covered (59.75%)

3.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.09
/lib/src/lib.rs
1
//! The sibling library.
2
//!
3
use std::io::{BufRead, BufReader};
4
use std::path::{Path, PathBuf};
5

6
use clap::ValueEnum;
7

8
pub type Result<T> = std::result::Result<T, SiblingError>;
9

10
/// The type of the nexter.
11
#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)]
12
pub enum NexterType {
13
    First,
14
    Last,
15
    Previous,
16
    Next,
17
    Random,
18
    Keep,
19
}
20

21
/// The error type for sibling.
22
#[derive(Debug)]
23
pub enum SiblingError {
24
    Io(std::io::Error),
25
    NotFound(PathBuf),
26
    NoParent(PathBuf),
27
    NotDir(PathBuf),
28
    NotFile(PathBuf),
29
    Fatal(String),
30
    Array(Vec<SiblingError>),
31
}
32

33
/// The struct of directory list.
34
#[derive(Debug, Clone)]
35
pub struct Dirs {
36
    dirs: Vec<PathBuf>,
37
    parent: PathBuf,
38
    current: usize,
39
}
40

41
/// The struct of directory.
42
#[derive(Debug, Clone)]
43
pub struct Dir<'a> {
44
    dirs: &'a Dirs,
45
    index: usize,
46
    last_item: bool,
47
}
48

49
impl Dir<'_> {
50
    /// Create a new `Dir` instance.
51
    pub fn new(dirs: &Dirs, index: usize) -> Dir<'_> {
7✔
52
        Dir {
7✔
53
            dirs,
7✔
54
            index,
7✔
55
            last_item: false,
7✔
56
        }
7✔
57
    }
7✔
58

59
    /// Create a new `Dir` instance with the last item flag.
60
    pub fn new_of_last_item(dirs: &Dirs, index: usize) -> Dir<'_> {
4✔
61
        Dir {
4✔
62
            dirs,
4✔
63
            index,
4✔
64
            last_item: true,
4✔
65
        }
4✔
66
    }
4✔
67

68
    pub fn path(&self) -> PathBuf {
11✔
69
        self.dirs.dirs[self.index].clone()
11✔
70
    }
11✔
71

NEW
72
    pub fn index(&self) -> usize {
×
NEW
73
        self.index
×
NEW
74
    }
×
75

76
    pub fn is_last_item(&self) -> bool {
1✔
77
        self.last_item
1✔
78
    }
1✔
79
}
80

81
impl Dirs {
82
    pub fn new<P: AsRef<Path>>(current_dir: P) -> Result<Self> {
7✔
83
        let current_dir = current_dir.as_ref();
7✔
84
        if current_dir == PathBuf::from(".") {
7✔
NEW
85
            match std::env::current_dir() {
×
NEW
86
                Ok(dir) => build_dirs(dir.clone().parent(), dir),
×
NEW
87
                Err(e) => Err(SiblingError::Io(e)),
×
88
            }
89
        } else if current_dir.exists() {
7✔
90
            if current_dir.is_dir() {
7✔
91
                let current = std::fs::canonicalize(current_dir).unwrap();
7✔
92
                build_dirs(current.clone().parent(), current)
7✔
93
            } else {
NEW
94
                Err(SiblingError::NotDir(current_dir.to_path_buf()))
×
95
            }
96
        } else {
NEW
97
            Err(SiblingError::NotFound(current_dir.to_path_buf()))
×
98
        }
99
    }
7✔
100

101
    pub fn new_from_file<S: AsRef<str>>(file: S) -> Result<Self> {
2✔
102
        let file = file.as_ref();
2✔
103
        if file == "-" {
2✔
NEW
104
            return build_from_reader(Box::new(std::io::stdin().lock()));
×
105
        }
2✔
106
        let path = PathBuf::from(file);
2✔
107
        if !path.exists() {
2✔
108
            Err(SiblingError::NotFound(path))
1✔
109
        } else if path.is_dir() {
1✔
NEW
110
            Err(SiblingError::NotFile(path))
×
111
        } else {
112
            build_from_list(path)
1✔
113
        }
114
    }
2✔
115

NEW
116
    pub fn parent(&self) -> PathBuf {
×
NEW
117
        self.parent.clone()
×
NEW
118
    }
×
119

120
    pub fn current(&self) -> Dir<'_> {
1✔
121
        Dir::new(self, self.current)
1✔
122
    }
1✔
123

NEW
124
    pub fn is_empty(&self) -> bool {
×
NEW
125
        self.dirs.is_empty()
×
NEW
126
    }
×
127

NEW
128
    pub fn len(&self) -> usize {
×
NEW
129
        self.dirs.len()
×
NEW
130
    }
×
131

NEW
132
    pub fn next(&self, nexter: &dyn Nexter) -> Option<Dir<'_>> {
×
NEW
133
        self.next_with(nexter, 1)
×
NEW
134
    }
×
135

136
    pub fn next_with(&self, nexter: &dyn Nexter, step: usize) -> Option<Dir<'_>> {
1✔
137
        nexter.next(self, step as i32)
1✔
138
    }
1✔
139

NEW
140
    pub fn directories(&self) -> impl Iterator<Item = &PathBuf> {
×
NEW
141
        self.dirs.iter()
×
NEW
142
    }
×
143
}
144

145
fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result<Dirs> {
7✔
146
    let parent = match parent {
7✔
147
        Some(p) => p,
7✔
NEW
148
        None => return Err(SiblingError::NoParent(current)),
×
149
    };
150
    let mut errs = vec![];
7✔
151
    let dirs = collect_dirs(parent, &mut errs);
7✔
152
    if !errs.is_empty() {
7✔
NEW
153
        Err(SiblingError::Array(errs))
×
154
    } else {
155
        let current_index = find_current(&dirs, &current);
7✔
156
        Ok(Dirs {
7✔
157
            dirs,
7✔
158
            parent: parent.to_path_buf(),
7✔
159
            current: current_index,
7✔
160
        })
7✔
161
    }
162
}
7✔
163

164
fn collect_dirs(parent: &Path, errs: &mut Vec<SiblingError>) -> Vec<PathBuf> {
7✔
165
    let mut dirs = vec![];
7✔
166
    if let Ok(entries) = parent.read_dir() {
7✔
167
        for entry in entries {
160✔
168
            match entry {
153✔
169
                Ok(entry) => {
153✔
170
                    let path = entry.path();
153✔
171
                    if path.is_dir() {
153✔
172
                        dirs.push(path);
141✔
173
                    }
141✔
174
                }
NEW
175
                Err(e) => errs.push(SiblingError::Io(e)),
×
176
            };
177
        }
NEW
178
    }
×
179
    dirs.sort();
7✔
180
    dirs
7✔
181
}
7✔
182

183
fn find_current(dirs: &[PathBuf], current: &PathBuf) -> usize {
7✔
184
    dirs.iter().position(|dir| dir == current).unwrap_or(0)
39✔
185
}
7✔
186

187
fn build_from_reader(reader: Box<dyn BufRead>) -> Result<Dirs> {
1✔
188
    let lines = reader
1✔
189
        .lines()
1✔
190
        .filter_map(|line| line.map(|n| n.trim().to_string()).ok())
5✔
191
        .collect::<Vec<String>>();
1✔
192
    let base = if let Some(base) = lines.iter().find(|l| l.starts_with("parent:")) {
5✔
193
        base.chars().skip(7).collect::<String>().trim().to_string()
1✔
194
    } else {
NEW
195
        ".".to_string()
×
196
    };
197
    let dirs = lines
1✔
198
        .iter()
1✔
199
        .filter(|l| !l.starts_with("parent:"))
5✔
200
        .map(PathBuf::from)
1✔
201
        .collect::<Vec<PathBuf>>();
1✔
202
    let current = find_current_dir_index(&dirs);
1✔
203
    Ok(Dirs {
1✔
204
        dirs,
1✔
205
        parent: PathBuf::from(base),
1✔
206
        current,
1✔
207
    })
1✔
208
}
1✔
209

210
fn find_current_dir_index(dirs: &[PathBuf]) -> usize {
1✔
211
    if let Ok(pwd) = std::env::current_dir() {
1✔
212
        let cwd = PathBuf::from(".");
1✔
213
        if let Some(pos) = dirs
1✔
214
            .iter()
1✔
215
            .position(|dir| dir == &cwd || pwd.ends_with(dir))
2✔
216
        {
217
            return pos;
1✔
NEW
218
        }
×
NEW
219
    }
×
NEW
220
    0
×
221
}
1✔
222

223
fn build_from_list(filename: PathBuf) -> Result<Dirs> {
1✔
224
    if let Ok(f) = std::fs::File::open(filename) {
1✔
225
        let reader = BufReader::new(f);
1✔
226
        build_from_reader(Box::new(reader))
1✔
227
    } else {
NEW
228
        Err(SiblingError::Io(std::io::Error::last_os_error()))
×
229
    }
230
}
1✔
231

232
pub trait Nexter {
233
    fn next<'a>(&self, dirs: &'a Dirs, step: i32) -> Option<Dir<'a>>;
234
}
235

236
pub struct NexterFactory {}
237

238
impl NexterFactory {
239
    pub fn build(nexter_type: NexterType) -> Box<dyn Nexter> {
6✔
240
        match nexter_type {
6✔
241
            NexterType::First => Box::new(First {}),
1✔
242
            NexterType::Last => Box::new(Last {}),
1✔
243
            NexterType::Previous => Box::new(Previous {}),
2✔
244
            NexterType::Next => Box::new(Next {}),
2✔
NEW
245
            NexterType::Random => Box::new(Random {}),
×
NEW
246
            NexterType::Keep => Box::new(Keep {}),
×
247
        }
248
    }
6✔
249
}
250

251
struct First {}
252
struct Last {}
253
struct Previous {}
254
struct Next {}
255
struct Random {}
256
struct Keep {}
257

258
impl Nexter for First {
259
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
1✔
260
        Some(Dir::new_of_last_item(dirs, 0))
1✔
261
    }
1✔
262
}
263

264
impl Nexter for Last {
265
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
1✔
266
        let next = dirs.dirs.len() - 1;
1✔
267
        Some(Dir::new_of_last_item(dirs, next))
1✔
268
    }
1✔
269
}
270

271
impl Nexter for Previous {
272
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
5✔
273
        next_impl(dirs, -_step)
5✔
274
    }
5✔
275
}
276

277
impl Nexter for Next {
278
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
5✔
279
        next_impl(dirs, _step)
5✔
280
    }
5✔
281
}
282

283
impl Nexter for Random {
NEW
284
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
×
285
        use rand::Rng;
NEW
286
        let mut rng = rand::rng();
×
NEW
287
        let next = rng.random_range(0..dirs.dirs.len()) as usize;
×
NEW
288
        Some(Dir::new(dirs, next))
×
NEW
289
    }
×
290
}
291

292
impl Nexter for Keep {
NEW
293
    fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option<Dir<'a>> {
×
NEW
294
        Some(dirs.current())
×
NEW
295
    }
×
296
}
297

298
fn next_impl(dirs: &Dirs, step: i32) -> Option<Dir<'_>> {
10✔
299
    let next = dirs.current as i32 + step;
10✔
300
    if next < 0 || next >= dirs.dirs.len() as i32 {
10✔
301
        None
2✔
302
    } else if next == 0 {
8✔
303
        Some(Dir::new_of_last_item(dirs, 0))
1✔
304
    } else if next == dirs.dirs.len() as i32 - 1 {
7✔
305
        Some(Dir::new_of_last_item(dirs, dirs.dirs.len() - 1))
1✔
306
    } else {
307
        Some(Dir::new(dirs, next as usize))
6✔
308
    }
309
}
10✔
310

311
#[cfg(test)]
312
mod tests {
313
    use super::*;
314

315
    #[test]
316
    fn test_dirs_new() {
1✔
317
        let dirs = Dirs::new(PathBuf::from("../testdata/d"));
1✔
318
        assert!(dirs.is_ok());
1✔
319
        let dirs = dirs.unwrap();
1✔
320
        assert_eq!(dirs.dirs.len(), 26);
1✔
321
        assert_eq!(dirs.current, 3);
1✔
322
    }
1✔
323

324
    #[test]
325
    fn test_dir_dot() {
1✔
326
        let dirs = Dirs::new(PathBuf::from(".."));
1✔
327
        assert!(dirs.is_ok());
1✔
328
        let dirs = dirs.unwrap();
1✔
329
        assert_eq!(
1✔
330
            dirs.current().path().file_name().map(|s| s.to_str()),
1✔
331
            Some("sibling".into())
1✔
332
        );
333
    }
1✔
334

335
    #[test]
336
    fn test_dir_from_file() {
1✔
337
        let dirs = Dirs::new_from_file("../testdata/dirlist.txt");
1✔
338
        assert!(dirs.is_ok());
1✔
339
        let dirs = dirs.unwrap();
1✔
340
        assert_eq!(dirs.dirs.len(), 4);
1✔
341
        assert_eq!(dirs.current, 1);
1✔
342
        assert_eq!(dirs.parent, PathBuf::from("testdata"));
1✔
343
    }
1✔
344

345
    #[test]
346
    fn test_nexter_first() {
1✔
347
        let dirs = Dirs::new("../testdata/c").unwrap();
1✔
348
        let nexter = NexterFactory::build(NexterType::First);
1✔
349
        match nexter.next(&dirs, 1) {
1✔
350
            Some(p) => assert!(p.path().ends_with("testdata/a")),
1✔
NEW
351
            None => panic!("unexpected None"),
×
352
        }
353
    }
1✔
354

355
    #[test]
356
    fn test_nexter_last() {
1✔
357
        let dirs = Dirs::new("../testdata/k").unwrap();
1✔
358
        let nexter = NexterFactory::build(NexterType::Last);
1✔
359
        match nexter.next(&dirs, 1) {
1✔
360
            Some(p) => assert!(p.path().ends_with("testdata/z")),
1✔
NEW
361
            None => panic!("unexpected None"),
×
362
        }
363
    }
1✔
364

365
    #[test]
366
    fn test_nexter_next() {
1✔
367
        let dirs = Dirs::new("../testdata/c").unwrap();
1✔
368
        let nexter = NexterFactory::build(NexterType::Next);
1✔
369
        match nexter.next(&dirs, 1) {
1✔
370
            Some(p) => assert!(p.path().ends_with("testdata/d")),
1✔
NEW
371
            None => panic!("unexpected None"),
×
372
        }
373
        match nexter.next(&dirs, 2) {
1✔
374
            Some(p) => assert!(p.path().ends_with("testdata/e"), "{:?}", p.path()),
1✔
NEW
375
            None => panic!("unexpected None"),
×
376
        }
377
        match nexter.next(&dirs, 23) {
1✔
378
            Some(p) => assert!(p.path().ends_with("testdata/z"), "{:?}", p.path()),
1✔
NEW
379
            None => panic!("unexpected None"),
×
380
        }
381
        match nexter.next(&dirs, 24) {
1✔
382
            None => {}
1✔
NEW
383
            Some(p) => panic!("unexpected {:?}", p.path()),
×
384
        }
385
    }
1✔
386

387
    #[test]
388
    fn test_nexter_prev() {
1✔
389
        let dirs = Dirs::new("../testdata/k").unwrap();
1✔
390
        let nexter = NexterFactory::build(NexterType::Previous);
1✔
391
        match nexter.next(&dirs, 1) {
1✔
392
            Some(p) => assert!(p.path().ends_with("testdata/j")),
1✔
NEW
393
            None => panic!("unexpected None"),
×
394
        }
395
        match nexter.next(&dirs, 1) {
1✔
396
            Some(p) => assert!(p.path().ends_with("testdata/j")),
1✔
NEW
397
            None => panic!("unexpected None"),
×
398
        }
399
        match nexter.next(&dirs, 4) {
1✔
400
            Some(p) => assert!(p.path().ends_with("testdata/g")),
1✔
NEW
401
            None => panic!("unexpected None"),
×
402
        }
403
        match nexter.next(&dirs, 10) {
1✔
404
            Some(p) => assert!(p.path().ends_with("testdata/a")),
1✔
NEW
405
            None => panic!("unexpected None"),
×
406
        }
407
        if let Some(p) = nexter.next(&dirs, 11) {
1✔
NEW
408
            panic!("unexpected {:?}", p.path())
×
409
        }
1✔
410
    }
1✔
411
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc