use crate::{vec2, Context, FlagsBuilder, Key, LayoutFormat, String as NkString};
use chrono::{DateTime, Local};
use std::cmp::Ordering;
use std::ffi::{OsStr, OsString};
use std::fs::{read_dir, DirEntry};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, PartialEq, Eq)]
pub struct FileInfo {
pub file_name: OsString,
pub path: PathBuf,
pub len: u64,
pub modified: SystemTime,
}
impl PartialOrd for FileInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.modified.partial_cmp(&other.modified)
}
}
impl Ord for FileInfo {
fn cmp(&self, other: &Self) -> Ordering {
self.modified.cmp(&other.modified)
}
}
#[derive(Debug)]
pub struct FileList {
path: PathBuf,
ext_filter: OsString,
files: Vec<FileInfo>,
selected: usize,
}
impl FileList {
fn scan_files<P: AsRef<Path>, T: AsRef<OsStr>>(path: P, ext_filter: T) -> Vec<FileInfo> {
let mut files: Vec<FileInfo> = vec![];
let ext_filter = ext_filter.as_ref();
let pattern_filter = |x: &Result<DirEntry, std::io::Error>| -> bool {
if ext_filter.is_empty() || ext_filter == "*" {
true
} else {
x.as_ref()
.map(|v| v.path().extension() == Some(ext_filter))
.unwrap_or(false)
}
};
if let Ok(entries) = read_dir(path) {
for entry in entries.filter(pattern_filter) {
if let Ok(entry) = entry {
let (len, modified) = if let Ok(m) = entry.metadata() {
(m.len(), m.modified().unwrap_or(SystemTime::UNIX_EPOCH))
} else {
(0, SystemTime::UNIX_EPOCH)
};
files.push(FileInfo {
file_name: entry.file_name(),
path: entry.path(),
len,
modified,
});
}
}
}
files.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
files
}
pub fn new<P: AsRef<Path>, T: AsRef<OsStr>>(path: P, ext_filter: T) -> Self {
let files = Self::scan_files(&path, &ext_filter);
Self {
path: path.as_ref().to_path_buf(),
ext_filter: ext_filter.as_ref().to_os_string(),
files,
selected: 0,
}
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn get(&self, index: usize) -> Option<&FileInfo> {
self.files.get(index)
}
pub fn iter(&self) -> std::slice::Iter<'_, FileInfo> {
self.files.iter()
}
pub fn select_prev(&mut self) {
if self.selected > 0 {
self.selected = self.selected.saturating_sub(1);
}
}
pub fn select_prev_wrapped(&mut self) {
if self.selected == 0 {
self.selected = self.len().saturating_sub(1);
} else {
self.selected = self.selected.saturating_sub(1);
}
}
pub fn select_next(&mut self) {
self.selected = self.selected.saturating_add(1);
if self.selected >= self.len() {
self.selected = self.len().saturating_sub(1);
}
}
pub fn select_next_wrapped(&mut self) {
self.selected = self.selected.saturating_add(1);
if self.selected >= self.len() {
self.selected = 0;
}
}
pub fn selected(&self) -> usize {
self.selected
}
pub fn selected_file(&self) -> Option<&FileInfo> {
if self.is_empty() {
None
} else {
self.get(self.selected)
}
}
pub fn refresh(&mut self) {
self.files = Self::scan_files(&self.path, &self.ext_filter);
self.selected = 0;
}
}
#[derive(Debug)]
pub struct FileListInputCtrl;
impl Default for FileListInputCtrl {
fn default() -> Self {
Self::new()
}
}
impl FileListInputCtrl {
pub fn new() -> Self {
Self {}
}
pub fn process(self, ctx: &Context, fb: &mut FileList) {
let input = ctx.input();
if input.is_key_pressed(Key::Enter) {
}
if input.is_key_pressed(Key::Up) {
fb.select_prev_wrapped();
}
if input.is_key_pressed(Key::Down) {
fb.select_next_wrapped();
}
}
}
#[derive(Debug)]
pub struct FileListPresenter {
row_height: f32,
}
impl Default for FileListPresenter {
fn default() -> Self {
Self::new(32.0)
}
}
impl FileListPresenter {
pub fn new(row_height: f32) -> Self {
Self { row_height }
}
fn scroll_to_selected(&self, ctx: &mut Context, fl: &FileList) {
let mut y: i32 = 0;
for (i, _f) in fl.iter().enumerate() {
y += self.row_height as i32;
if fl.selected == i {
break;
}
}
let win_size = ctx.window_get_size();
let offset = y - win_size.y as i32 + (self.row_height * 2.0) as i32;
if offset > 0 {
ctx.window_set_scroll(0, offset as u32);
} else {
ctx.window_set_scroll(0, 0);
}
}
pub fn present(self, ctx: &mut Context, fl: &FileList) {
let spacing = *ctx.style().window().spacing();
let padding = *ctx.style().window().padding();
ctx.style_mut().window_mut().set_spacing(vec2(0.0, 0.0));
ctx.style_mut().window_mut().set_padding(vec2(0.0, 0.0));
self.scroll_to_selected(ctx, fl);
let selected_bg_color = ctx.style().window().background().inverted();
let selected_fg_color = ctx.style().text().color.inverted();
for (i, f) in fl.iter().enumerate() {
if fl.selected == i {
ctx.layout_row_colored(
LayoutFormat::Dynamic,
self.row_height,
&[0.2, 0.4, 0.4],
selected_bg_color,
);
ctx.label_colored(
format!("{:-4}", i).into(),
FlagsBuilder::align().left().middle().into(),
selected_fg_color,
);
ctx.label_colored(
NkString::from(&f.file_name),
FlagsBuilder::align().left().middle().into(),
selected_fg_color,
);
ctx.label_colored(
NkString::from(
DateTime::<Local>::from(f.modified)
.format("%F %T")
.to_string(),
),
FlagsBuilder::align().left().middle().into(),
selected_fg_color,
);
} else {
ctx.layout_row(LayoutFormat::Dynamic, self.row_height, &[0.2, 0.4, 0.4]);
ctx.label(
format!("{:-4}", i).into(),
FlagsBuilder::align().left().middle().into(),
);
ctx.label(
NkString::from(&f.file_name),
FlagsBuilder::align().left().middle().into(),
);
ctx.label(
NkString::from(
DateTime::<Local>::from(f.modified)
.format("%F %T")
.to_string(),
),
FlagsBuilder::align().left().middle().into(),
);
}
}
ctx.style_mut().window_mut().set_spacing(spacing);
ctx.style_mut().window_mut().set_padding(padding);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_list() {
let fb = FileList::new("./src", "rs");
println!("{:#?}", fb);
}
}