Skip to content

Commit

Permalink
Search parent directories for collection file
Browse files Browse the repository at this point in the history
- If a collection file isn't found in the current directory, recursively search our parents (up to the fs root) to find it.
- Print a notification on startup to show the loaded collection file (similar to what shows after a reload)

Closes #194
  • Loading branch information
LucasPickering committed May 1, 2024
1 parent d484ad6 commit e58bb40
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

- Reduce UI latency under certain scenarios
- Previously some actions would feel laggy because of an inherent 250ms delay in processing some events
- Search parent directories for collection file ([#194](https://github.com/LucasPickering/slumber/issues/194))

### Fixed

Expand Down
68 changes: 45 additions & 23 deletions src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ pub use recipe_tree::*;

use crate::util::{parse_yaml, ResultExt};
use anyhow::{anyhow, Context};
use itertools::Itertools;
use std::{
env,
fmt::Debug,
fs,
future::Future,
path::{Path, PathBuf},
};
use tokio::task;
use tracing::{info, warn};
use tracing::{info, trace, warn};

/// The support file names to be automatically loaded as a config. We only
/// support loading from one file at a time, so if more than one of these is
Expand Down Expand Up @@ -74,35 +76,55 @@ impl CollectionFile {
/// available. This will use the override if given, otherwise it will fall
/// back to searching the current directory for a collection.
pub fn try_path(override_path: Option<PathBuf>) -> anyhow::Result<PathBuf> {
override_path.or_else(detect_path).ok_or(anyhow!(
"No collection file given and none found in current directory"
))
override_path.map_or_else(detect_path, Ok)
}
}

/// Search the current directory for a config file matching one of the known
/// file names, and return it if found
fn detect_path() -> Option<PathBuf> {
let paths: Vec<&Path> = CONFIG_FILES
.iter()
.map(Path::new)
// This could be async but I'm being lazy and skipping it for now,
// since we only do this at startup anyway (mid-process reloading
// reuses the detected path so we don't re-detect)
.filter(|p| p.exists())
.collect();
match paths.as_slice() {
[] => None,
[path] => Some(path.to_path_buf()),
[first, rest @ ..] => {
// Print a warning, but don't actually fail
warn!(
"Multiple config files detected. {first:?} will be used \
and the following will be ignored: {rest:?}"
);
Some(first.to_path_buf())
fn detect_path() -> anyhow::Result<PathBuf> {
/// Search a directory and its parents for the collection file. Return None
/// only if we got through the whole tree and couldn't find it
fn search_all(dir: &Path) -> Option<PathBuf> {
search(dir).or_else(|| {
let parent = dir.parent()?;
search_all(parent)
})
}

/// Search a single directory for a collection file
fn search(dir: &Path) -> Option<PathBuf> {
trace!("Scanning for collection file in {dir:?}");

let paths = CONFIG_FILES
.iter()
.map(|file| dir.join(file))
// This could be async but I'm being lazy and skipping it for now,
// since we only do this at startup anyway (mid-process reloading
// reuses the detected path so we don't re-detect)
.filter(|p| p.exists())
.collect_vec();
match paths.as_slice() {
[] => None,
[first, rest @ ..] => {
if !rest.is_empty() {
warn!(
"Multiple collection files detected. {first:?} will be \
used and the following will be ignored: {rest:?}"
);
}

trace!("Found collection file at {first:?}");
Some(first.to_path_buf())
}
}
}

// Walk *up* the tree until we've hit the root
let cwd = env::current_dir()?;
search_all(&cwd).ok_or_else(|| {
anyhow!("No collection file found in current or ancestor directories")
})
}

/// Load a collection from the given file. Takes an owned path because it
Expand Down
8 changes: 2 additions & 6 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Tui {
TuiContext::send_message(Message::Error { error });
CollectionFile::with_path(collection_path)
});
let view = View::new(&collection_file.collection);
let view = View::new(&collection_file);

// The code to revert the terminal takeover is in `Tui::drop`, so we
// shouldn't take over the terminal until right before creating the
Expand Down Expand Up @@ -338,12 +338,8 @@ impl Tui {
// old one *first* to make sure UI state is saved before being restored
self.view.replace(|old| {
drop(old);
View::new(&self.collection_file.collection)
View::new(&self.collection_file)
});
self.view.notify(format!(
"Reloaded collection from {}",
self.collection_file.path().to_string_lossy()
));
}

/// GOODBYE
Expand Down
15 changes: 10 additions & 5 deletions src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use theme::Theme;
pub use util::PreviewPrompter;

use crate::{
collection::{Collection, ProfileId, RecipeId},
collection::{CollectionFile, ProfileId, RecipeId},
tui::{
context::TuiContext,
input::Action,
Expand Down Expand Up @@ -47,10 +47,15 @@ pub struct View {
}

impl View {
pub fn new(collection: &Collection) -> Self {
Self {
root: Root::new(collection).into(),
}
pub fn new(collection_file: &CollectionFile) -> Self {
let mut view = Self {
root: Root::new(&collection_file.collection).into(),
};
view.notify(format!(
"Loaded collection from {}",
collection_file.path().to_string_lossy()
));
view
}

/// Draw the view to screen. This needs access to the input engine in order
Expand Down
6 changes: 4 additions & 2 deletions src/util/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ pub struct DataDirectory(PathBuf);

impl DataDirectory {
/// Root directory for all generated files. The value is contextual:
/// - In development, use a directory in the current directory
/// - In development, use a directory from the crate root
/// - In release, use a platform-specific directory in the user's home
pub fn root() -> Self {
if cfg!(debug_assertions) {
Self("./data/".into())
// If env var isn't defined, this will just become ./data/
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("data/");
Self(path)
} else {
// According to the docs, this dir will be present on all platforms
// https://docs.rs/dirs/latest/dirs/fn.data_dir.html
Expand Down

0 comments on commit e58bb40

Please sign in to comment.