use async_stream::try_stream;
use iced::widget::{button, column, container, row, text, text_input};
use iced::{Element, Font, Length, Padding, Pixels};
use iced_utils::{task, Task};
use normpath::{BasePathBuf, PathExt};
use nucleo_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo_matcher::Matcher;
use tokio::fs;
use std::borrow::Cow;
use std::cmp;
use std::collections::{BTreeSet, VecDeque};
use std::path::{self, Path, PathBuf};
use crate::nav_scrollable;
#[derive(Debug)]
pub struct State {
pub left_nav: nav_scrollable::State,
pub right_nav: nav_scrollable::State,
pub input: String,
pub current_dir: BasePathBuf,
pub current_kind: Option<RepoKind>,
pub child_dirs: Vec<PathBuf>,
pub matched_child_dirs: Vec<String>,
pub found_repos_dirs_pijul: Vec<PathBuf>,
pub found_repos_dirs_git: Vec<PathBuf>,
pub find_child_dirs_handle: Option<task::Handle>,
pub find_repos_handle: Option<task::Handle>,
pub matcher: Matcher,
pub selection: Selection,
}
#[derive(Debug, Default)]
pub enum Selection {
#[default]
Input,
SubDir(usize),
ProjectPijul(usize),
ProjectGit(usize),
}
#[derive(Debug, Clone, Copy)]
pub enum RepoKind {
Pijul,
Git,
}
#[derive(Debug, Clone)]
pub enum Msg {
Input(String),
SelectChildDir(PathBuf),
EditSegment { ix: usize },
FindChildDirsSuccess(Vec<PathBuf>),
FindChildDirsFailed(String),
FindReposDirsSuccess((PathBuf, RepoKind)),
FindReposDirsDone,
FindReposDirsFailed(String),
SelectRepo(PathBuf),
}
pub fn init(start_path: impl Into<PathBuf>) -> (State, Task<Msg>) {
let mut start_path = start_path.into();
let current_dir = loop {
match PathExt::normalize(start_path.as_path()) {
Ok(path) => break path,
Err(_) => {
if let Some(path) = start_path.parent() {
start_path = path.to_path_buf();
} else {
break BasePathBuf::new(PathBuf::from_iter([
path::Component::RootDir,
]))
.expect("Must be able to construct root dir");
}
}
}
};
let (find_child_dirs_task, find_child_dirs_handle) =
find_child_dirs(current_dir.as_path());
let find_child_dirs_handle = Some(find_child_dirs_handle);
let (find_repos_task, find_repos_handle) =
find_repos_dirs(current_dir.as_path());
let find_repos_handle = Some(find_repos_handle);
let mut state = State {
left_nav: nav_scrollable::State::default(),
right_nav: nav_scrollable::State::default(),
input: String::default(),
current_dir,
current_kind: None,
child_dirs: vec![],
matched_child_dirs: vec![],
found_repos_dirs_pijul: vec![],
found_repos_dirs_git: vec![],
find_child_dirs_handle,
find_repos_handle,
matcher: Matcher::default(),
selection: Selection::default(),
};
update_current_kind(&mut state);
(
state,
Task::batch([
find_child_dirs_task,
find_repos_task,
task::widget_focus_next(),
]),
)
}
#[derive(Debug)]
pub enum Action {
Picked(PathBuf),
CreateNew(PathBuf),
}
pub fn update(state: &mut State, msg: Msg) -> (Task<Msg>, Option<Action>) {
let mut action = None;
let task = match msg {
Msg::Input(input) => {
state.selection = Selection::Input;
nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
if input.is_empty() && state.input.is_empty() {
if let Ok(Some(parent)) = state.current_dir.parent() {
change_dir(state, parent.to_owned())
} else {
Task::none()
}
} else {
if input.is_empty() {
state.matched_child_dirs = vec![];
state.input = input;
Task::none()
} else if input.len() > state.input.len()
&& (is_pijul(state.current_dir.as_path())
|| is_git(state.current_dir.as_path()))
{
Task::none()
} else {
let ends_with_path_sep = input.ends_with(PATH_SEP);
let mut matches = if ends_with_path_sep {
let (exact, _) = input.split_at(input.len() - 1);
if let Some(matched) =
state.child_dirs.iter().find_map(|dir| {
dir.file_name()
.and_then(|name| name.to_str())
.and_then(|name| {
(name.to_lowercase()
== exact.to_lowercase())
.then_some(name)
})
})
{
vec![matched.to_string()]
} else {
vec![]
}
} else {
match_child_dirs(state, &input)
};
if ends_with_path_sep {
if !matches.is_empty() {
let matched =
state.current_dir.join(matches.pop().unwrap());
state.input = String::default();
change_dir(state, matched)
} else {
Task::none()
}
} else {
state.matched_child_dirs = matches;
state.input = input;
Task::none()
}
}
}
}
Msg::SelectChildDir(dir) => {
state.input = String::default();
let path = PathExt::normalize(dir.as_path()).unwrap();
change_dir(state, path)
}
Msg::EditSegment { ix } => {
let ix = cmp::max(ix, 1);
let path: PathBuf =
state.current_dir.as_path().iter().take(ix).collect();
let path = PathExt::normalize(path.as_path()).unwrap();
change_dir(state, path)
}
Msg::FindChildDirsSuccess(dirs) => {
state.find_child_dirs_handle = None;
state.child_dirs = dirs;
if state.input.len() > 1 {
let (_, last_char) =
state.input.split_at(state.input.len() - 1);
if last_char != PATH_SEP_STR {
let input = PathBuf::from(&state.input);
let needle = input.file_name().unwrap().to_string_lossy();
let matches = match_child_dirs(state, &needle);
state.matched_child_dirs = matches;
}
}
Task::none()
}
Msg::FindChildDirsFailed(_err) => {
state.find_child_dirs_handle = None;
Task::none()
}
Msg::FindReposDirsSuccess((dir, kind)) => {
match kind {
RepoKind::Pijul => state.found_repos_dirs_pijul.push(dir),
RepoKind::Git => state.found_repos_dirs_git.push(dir),
}
Task::none()
}
Msg::FindReposDirsFailed(_err) => {
Task::none()
}
Msg::FindReposDirsDone => {
state.find_repos_handle = None;
Task::none()
}
Msg::SelectRepo(dir) => {
action = Some(Action::Picked(dir));
Task::none()
}
};
update_current_kind(state);
(task, action)
}
#[derive(Debug, Clone, Copy)]
pub enum SelectionDir {
Down,
Up,
Left,
Right,
}
pub fn move_selection(state: &mut State, dir: SelectionDir) {
let selection = &mut state.selection;
match dir {
SelectionDir::Down => match selection {
Selection::Input => {
if !state.matched_child_dirs.is_empty()
|| !state.child_dirs.is_empty()
{
let new_ix = 0;
*selection = Selection::SubDir(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.left_nav,
new_ix,
);
}
}
Selection::SubDir(ix) => {
if (!state.matched_child_dirs.is_empty()
&& state.matched_child_dirs.len() - 1 > *ix)
|| (!state.child_dirs.is_empty()
&& state.child_dirs.len() - 1 > *ix)
{
let new_ix = *ix + 1;
*selection = Selection::SubDir(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.left_nav,
new_ix,
);
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.left_nav,
0,
);
}
}
Selection::ProjectPijul(ix) => {
if !state.found_repos_dirs_pijul.is_empty() {
if state.found_repos_dirs_pijul.len() - 1 > *ix {
let new_ix = *ix + 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
new_ix,
);
} else if !state.found_repos_dirs_git.is_empty() {
*selection = Selection::ProjectGit(0);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1),
);
} else {
*selection = Selection::ProjectPijul(0);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
}
} else if !state.found_repos_dirs_git.is_empty() {
*selection = Selection::ProjectGit(0);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
state.found_repos_dirs_pijul.len().saturating_sub(1),
);
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
}
}
Selection::ProjectGit(ix) => {
if !state.found_repos_dirs_git.is_empty() {
if state.found_repos_dirs_git.len() - 1 > *ix {
let new_ix = *ix + 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1)
+ new_ix,
);
} else if !state.found_repos_dirs_pijul.is_empty() {
*selection = Selection::ProjectPijul(0);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
} else {
*selection = Selection::ProjectGit(0);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1),
);
}
} else if !state.found_repos_dirs_pijul.is_empty() {
*selection = Selection::ProjectPijul(0);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
}
}
},
SelectionDir::Up => match selection {
Selection::Input => {
if !state.matched_child_dirs.is_empty() {
let new_ix = state.matched_child_dirs.len() - 1;
*selection = Selection::SubDir(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.left_nav,
new_ix,
);
} else if !state.child_dirs.is_empty() {
let new_ix = state.child_dirs.len() - 1;
*selection = Selection::SubDir(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.left_nav,
new_ix,
);
}
}
Selection::SubDir(ix) => {
if *ix > 0
&& (!state.matched_child_dirs.is_empty()
|| !state.child_dirs.is_empty())
{
let new_ix = *ix - 1;
*selection = Selection::SubDir(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.left_nav,
new_ix,
);
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.left_nav,
0,
);
}
}
Selection::ProjectPijul(ix) => {
if !state.found_repos_dirs_pijul.is_empty() {
if *ix > 0 {
let new_ix = *ix - 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
} else if !state.found_repos_dirs_git.is_empty() {
let new_ix = state.found_repos_dirs_git.len() - 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1)
+ new_ix,
);
} else {
let new_ix = state.found_repos_dirs_pijul.len() - 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
}
} else if !state.found_repos_dirs_git.is_empty() {
if *ix > 0 {
let new_ix = *ix - 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
} else {
let new_ix = state.found_repos_dirs_git.len() - 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
new_ix,
);
}
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
}
}
Selection::ProjectGit(ix) => {
if !state.found_repos_dirs_git.is_empty() {
if *ix > 0 {
let new_ix = *ix - 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1)
+ new_ix,
);
} else if !state.found_repos_dirs_pijul.is_empty() {
let new_ix = state.found_repos_dirs_pijul.len() - 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
} else {
let new_ix = state.found_repos_dirs_git.len() - 1;
*selection = Selection::ProjectGit(new_ix);
nav_scrollable::scroll_down_to_section(
&mut state.right_nav,
state
.found_repos_dirs_pijul
.len()
.saturating_sub(1)
+ new_ix,
);
}
} else if !state.found_repos_dirs_pijul.is_empty() {
if *ix > 0 {
let new_ix = *ix - 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
} else {
let new_ix = state.found_repos_dirs_pijul.len() - 1;
*selection = Selection::ProjectPijul(new_ix);
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
new_ix,
);
}
} else {
*selection = Selection::Input;
nav_scrollable::scroll_up_to_section(
&mut state.right_nav,
0,
);
}
}
},
SelectionDir::Right | SelectionDir::Left => match selection {
Selection::Input | Selection::SubDir(_) => {
if !state.found_repos_dirs_pijul.is_empty() {
*selection = Selection::ProjectPijul(0);
} else if !state.found_repos_dirs_git.is_empty() {
*selection = Selection::ProjectGit(0);
}
nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
}
Selection::ProjectPijul(_) => {
if !state.matched_child_dirs.is_empty()
|| !state.child_dirs.is_empty()
{
*selection = Selection::SubDir(0);
} else {
*selection = Selection::Input;
}
nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
}
Selection::ProjectGit(_) => {
if !state.matched_child_dirs.is_empty()
|| !state.child_dirs.is_empty()
{
*selection = Selection::SubDir(0);
} else {
*selection = Selection::Input;
}
nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
}
},
}
update_current_kind(state);
}
pub fn confirm_input_or_selection(
state: &mut State,
) -> (Task<Msg>, Option<Action>) {
let mut action = None;
let task = match state.selection {
Selection::Input => {
if state.input.is_empty() {
action = Some(Action::Picked(
state.current_dir.as_path().to_path_buf(),
));
Task::none()
} else {
nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
let mut matches = if state.child_dirs.iter().any(|dir| {
dir.file_name().map(|name| name.to_string_lossy())
== Some(Cow::Borrowed(&state.input))
}) {
vec![state.input.clone()]
} else {
vec![]
};
if matches.len() == 1 {
let matched =
state.current_dir.join(matches.pop().unwrap());
state.input = String::default();
change_dir(state, matched)
} else {
action = Some(Action::CreateNew(
state
.current_dir
.as_path()
.to_path_buf()
.join(&state.input),
));
Task::none()
}
}
}
Selection::SubDir(ix) => {
if !state.matched_child_dirs.is_empty() {
if let Some(dir) = state.matched_child_dirs.get(ix) {
let path = state.current_dir.join(dir);
state.input = String::default();
change_dir(state, path)
} else {
Task::none()
}
} else if !state.child_dirs.is_empty() {
if let Some(dir) = state.child_dirs.get(ix) {
let path = state.current_dir.join(dir);
state.input = String::default();
change_dir(state, path)
} else {
Task::none()
}
} else {
Task::none()
}
}
Selection::ProjectPijul(ix) => {
if let Some(path) = state.found_repos_dirs_pijul.get(ix) {
action = Some(Action::Picked(path.clone()));
}
Task::none()
}
Selection::ProjectGit(ix) => {
if let Some(path) = state.found_repos_dirs_git.get(ix) {
action = Some(Action::Picked(path.clone()));
}
Task::none()
}
};
state.selection = Selection::Input;
update_current_kind(state);
(task, action)
}
#[allow(clippy::too_many_arguments)]
pub fn view<'a, Theme, Renderer>(
state: &'a State,
container_class_bordered: <Theme as container::Catalog>::Class<'a>,
container_class_bordered_selected: <Theme as container::Catalog>::Class<'a>,
button_class_normal: <Theme as button::Catalog>::Class<'a>,
button_class_selected: <Theme as button::Catalog>::Class<'a>,
scrollable_normal: <Theme as nav_scrollable::Catalog>::Class<'a>,
scrollable_selected: <Theme as nav_scrollable::Catalog>::Class<'a>,
text_class_decorative: <Theme as text::Catalog>::Class<'a>,
) -> Element<'a, Msg, Theme, Renderer>
where
Theme: 'a
+ button::Catalog
+ container::Catalog
+ nav_scrollable::Catalog
+ text::Catalog
+ text_input::Catalog,
<Theme as button::Catalog>::Class<'a>: Copy,
<Theme as nav_scrollable::Catalog>::Class<'a>: Copy,
<Theme as text::Catalog>::Class<'a>: Copy,
Renderer: 'a + iced::advanced::Renderer + iced::advanced::text::Renderer,
<Renderer as iced_core::text::Renderer>::Font: From<iced::Font>,
{
let State {
left_nav,
right_nav,
input,
current_dir,
child_dirs,
matched_child_dirs,
found_repos_dirs_pijul,
found_repos_dirs_git,
find_child_dirs_handle: _,
find_repos_handle,
matcher: _,
selection,
current_kind: _,
} = state;
const CORNER: &str = "├─";
const LAST_CORNER: &str = "└─";
const CORNER_SIZE: Pixels = Pixels(24.0);
let mut left_children =
Vec::with_capacity(if state.find_child_dirs_handle.is_some() {
1
} else {
if !matched_child_dirs.is_empty() {
matched_child_dirs.len()
} else {
child_dirs.len()
}
});
let left_header = Element::new(
container(row([
Element::new(row(itertools::intersperse_with(
current_dir.as_path().iter().enumerate().map(|(ix, comp)| {
Element::new(
button(text(comp.to_string_lossy()))
.on_press(Msg::EditSegment { ix }),
)
}),
|| {
Element::new(
container(text(PATH_SEP_STR)).padding(
text_input::DEFAULT_PADDING.left(0).right(0),
),
)
},
))),
Element::new(
container(text(PATH_SEP_STR))
.padding(text_input::DEFAULT_PADDING.left(0).right(0)),
),
Element::new(text_input("", input).on_input(Msg::Input)),
]))
.class(if matches!(selection, Selection::Input) {
container_class_bordered_selected
} else {
container_class_bordered
})
.padding(button::DEFAULT_PADDING),
);
const CHILDREN_PADDING: Padding = Padding {
left: 20.0,
..Padding::ZERO
};
if state.find_child_dirs_handle.is_some() {
left_children.push(Element::new(text("Loading child directories...")));
} else if !matched_child_dirs.is_empty() {
let last_matched_child_ix = matched_child_dirs.len().saturating_sub(1);
left_children.extend(matched_child_dirs.iter().enumerate().map(
|(ix, dir)| {
Element::new(
row([
Element::new(
text(if ix == last_matched_child_ix {
LAST_CORNER
} else {
CORNER
})
.size(CORNER_SIZE)
.font(Font::MONOSPACE)
.shaping(text::Shaping::Advanced)
.class(text_class_decorative),
),
Element::new(
button(text(dir))
.on_press_with(move || {
Msg::SelectChildDir(
current_dir.as_path().join(dir),
)
})
.class(
if let Selection::SubDir(dir_ix) = selection
&& *dir_ix == ix
{
button_class_selected
} else {
button_class_normal
},
),
),
])
.padding(CHILDREN_PADDING),
)
},
));
} else {
let last_child_ix = child_dirs.len().saturating_sub(1);
left_children.extend(child_dirs.iter().enumerate().map(|(ix, dir)| {
let dir_str = dir.file_name().unwrap().to_string_lossy();
Element::new(
row([
Element::new(
text(if ix == last_child_ix {
LAST_CORNER
} else {
CORNER
})
.size(CORNER_SIZE)
.shaping(text::Shaping::Advanced)
.class(text_class_decorative),
),
Element::new(
button(text(dir_str))
.on_press_with(|| Msg::SelectChildDir(dir.clone()))
.class(
if let Selection::SubDir(dir_ix) = selection
&& *dir_ix == ix
{
button_class_selected
} else {
button_class_normal
},
),
),
])
.padding(CHILDREN_PADDING),
)
}));
}
let right_header = Element::new(text(
if !found_repos_dirs_pijul.is_empty()
|| !found_repos_dirs_git.is_empty()
{
"Found repositories:"
} else if find_repos_handle.is_none() {
"No repositories found"
} else if is_pijul(current_dir.as_path()) {
"This is a Pijul repository"
} else if is_git(current_dir.as_path()) {
"This is a Git repository"
} else {
"Searching for repositories"
},
));
let right_children = found_repos_dirs_pijul
.iter()
.enumerate()
.map(|(ix, dir)| {
Element::new(
button(text(dir.to_string_lossy()))
.on_press_with(|| Msg::SelectRepo(dir.clone()))
.class(
if let Selection::ProjectPijul(dir_ix) = selection
&& *dir_ix == ix
{
button_class_selected
} else {
button_class_normal
},
),
)
})
.chain(found_repos_dirs_git.iter().enumerate().map(|(ix, dir)| {
Element::new(
button(text(format!("{} (git)", dir.to_string_lossy())))
.on_press_with(|| Msg::SelectRepo(dir.clone()))
.class(
if let Selection::ProjectGit(dir_ix) = selection
&& *dir_ix == ix
{
button_class_selected
} else {
button_class_normal
},
),
)
}))
.collect::<Vec<_>>();
let left = Element::new(
container(column([
left_header,
Element::new(
nav_scrollable(left_nav, left_children)
.class(if matches!(selection, Selection::SubDir(_)) {
scrollable_selected
} else {
scrollable_normal
})
.height(Length::Fill)
.width(Length::Fill),
),
]))
.height(Length::Fill)
.width(Length::FillPortion(1)),
);
let right = Element::new(
container(column([
right_header,
Element::new(
nav_scrollable(right_nav, right_children)
.class(
if matches!(
selection,
Selection::ProjectPijul(_)
| Selection::ProjectGit(_)
) {
scrollable_selected
} else {
scrollable_normal
},
)
.height(Length::Fill)
.width(Length::Fill),
),
]))
.height(Length::Fill)
.width(Length::FillPortion(1)),
);
Element::new(
row([left, right])
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill),
)
}
const SPACING: u32 = 10;
fn change_dir(state: &mut State, path: BasePathBuf) -> Task<Msg> {
state.current_dir = path;
state.found_repos_dirs_pijul = vec![];
state.found_repos_dirs_git = vec![];
state.matched_child_dirs = vec![];
state.child_dirs = vec![];
let (find_child_dirs_task, find_child_dirs_handle) =
find_child_dirs(state.current_dir.as_path());
state.find_child_dirs_handle = Some(find_child_dirs_handle);
let (find_repos_dirs_task, find_repos_dirs_handle) =
find_repos_dirs(state.current_dir.as_path());
state.find_repos_handle = Some(find_repos_dirs_handle);
Task::batch([find_child_dirs_task, find_repos_dirs_task])
}
fn find_child_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {
let current_dir = dir.to_owned();
let (task, handle) = Task::perform(
async move {
let mut read_dir = fs::read_dir(¤t_dir).await?;
let mut child_dirs = BTreeSet::new();
while let Some(entry) = read_dir.next_entry().await? {
let path = entry.path();
let metadata = fs::metadata(&path).await?;
if metadata.is_dir() && path.file_name().is_some() {
child_dirs.insert(path);
}
}
Ok(child_dirs.into_iter().collect())
},
|result: std::io::Result<_>| match result {
Ok(dirs) => Msg::FindChildDirsSuccess(dirs),
Err(error) => Msg::FindChildDirsFailed(error.to_string()),
},
)
.abortable();
let handle = handle.abort_on_drop();
(task, handle)
}
fn find_repos_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {
let dir = dir.to_path_buf();
let stream = try_stream! {
let mut to_try = VecDeque::from([dir.to_path_buf()]);
while let Some(path) = to_try.pop_front() {
let mut reader = fs::read_dir(&path).await.map_err(|err| format!("Cannot read dir {} ({err})", path.to_string_lossy()
))?;
while let Ok(Some(entry)) = reader.next_entry().await {
let metadata = fs::metadata(entry.path()).await;
if metadata.map(|meta| meta.is_dir()).unwrap_or(false) {
let path = entry.path();
if is_pijul(&path) {
yield Some((path, RepoKind::Pijul));
}
else if is_git(&path) {
yield Some((path, RepoKind::Git));
} else {
to_try.push_back(path);
}
}
}
}
yield None
};
let (task, handle) =
Task::run(stream, |result: Result<_, String>| match result {
Ok(Some((dir, kind))) => Msg::FindReposDirsSuccess((dir, kind)),
Ok(None) => Msg::FindReposDirsDone,
Err(error) => Msg::FindReposDirsFailed(error.to_string()),
})
.abortable();
let handle = handle.abort_on_drop();
(task, handle)
}
fn match_child_dirs(state: &mut State, needle: &str) -> Vec<String> {
let atom = Atom::new(
needle,
CaseMatching::Ignore,
Normalization::Never,
AtomKind::Fuzzy,
false,
);
itertools::Itertools::sorted_by(
atom.match_list(
state.child_dirs.iter().filter_map(|dir| {
dir.file_name().map(|name| name.to_string_lossy())
}),
&mut state.matcher,
)
.into_iter(),
|(_, left), (_, right)| Ord::cmp(right, left),
)
.map(|(dir, _score)| dir.to_string())
.collect()
}
fn update_current_kind(state: &mut State) {
let path_to_check = match &state.selection {
Selection::Input => {
Some(state.current_dir.join(&state.input).into_path_buf())
}
Selection::SubDir(ix) => {
Some(if !state.matched_child_dirs.is_empty() {
let subdir = state.matched_child_dirs.get(*ix).unwrap();
state.current_dir.join(subdir).into_path_buf()
} else {
state.child_dirs.get(*ix).unwrap().clone()
})
}
Selection::ProjectPijul(_) => {
state.current_kind = Some(RepoKind::Pijul);
None
}
Selection::ProjectGit(_) => {
state.current_kind = Some(RepoKind::Git);
None
}
};
if let Some(path) = path_to_check {
if is_pijul(path.as_path()) {
state.current_kind = Some(RepoKind::Pijul);
} else if is_git(path.as_path()) {
state.current_kind = Some(RepoKind::Git);
} else {
state.current_kind = None;
}
}
}
fn is_pijul(dir: &Path) -> bool {
dir.join(PIJUL_DIR).exists()
}
fn is_git(dir: &Path) -> bool {
dir.join(GIT_DIR).exists()
}
const PIJUL_DIR: &str = ".pijul";
const GIT_DIR: &str = ".git";
const PATH_SEP: char = path::MAIN_SEPARATOR;
const PATH_SEP_STR: &str = path::MAIN_SEPARATOR_STR;