3. DirNav
DirNav is a depth-first directory walker driven by two
std::function callbacks — one fired on entering a
directory, one on encountering a matching file:
using DirCallback = std::function<void(const std::string& dir_path)>;
using FileCallback = std::function<void(const std::string& file_name)>;
Where the Rust variant expresses the callback contract as a trait
implemented by an application-specific struct, the C++ variant uses
two std::function members that any caller can populate
with a lambda, free function, or method pointer. This trade-off
removes the type-parameter plumbing but pays a small runtime cost per
call (indirect call through the type-erased wrapper).
3.1 Design Points
-
Skip list is baked in. Build directories
(
build, out, bin,
obj, target), Python caches, VCS/IDE
folders, and archive are pre-loaded into
skip_list_ in the constructor initializer list so no
callers have to know the list. Extra names can be added via
add_skip().
-
Extensions normalised at insert.
normalise_ext() strips a leading . and,
on Windows, lowercases the string — done once when the pattern is
registered and again on each file's extension. This makes
patterns_.contains(...) a straight
O(1) hash lookup on the hot path with no per-lookup
allocation.
-
Subdirectories are collected, then recursed. The
scan pass fires
file_callback_ immediately for each
matching file, but subdirectories are buffered in a
std::vector and processed only after the current
directory is fully drained. This keeps the
directory_iterator in one linear pass — no
interleaved recursion inside its loop.
-
Errors swallowed via
std::error_code. Every
std::filesystem call is the non-throwing overload;
ec parameters are checked but errors do not abort
traversal — a permission-denied subdirectory simply drops out and
the walk continues.
-
Counts reset on each
visit().
file_count_ and dir_count_ are zeroed at
the top of every top-level visit(), so the same
DirNav instance can drive multiple sequential walks.
-
Directory fires unconditionally. Unlike the Rust
walker,
DirNav fires dir_callback_
every time it enters a directory — the "hide empty" logic lives
in Output, which defers printing the directory
header until it sees a matching file.
3.2 visit_impl() — the traversal loop
- Increment
dir_count_ and fire
dir_callback_ with the directory's
generic_string().
- Open a
directory_iterator; on error, return.
- For each entry: if it is a regular file whose extension matches,
bump
file_count_ and fire
file_callback_. If it is a directory whose bare
name is not in the skip list and recursion is enabled, push its
path onto the subdirs vector.
- After the scan pass, recurse into each buffered subdirectory.
3.3 Source — DirNav/src/DirNav.ixx
export module dir_nav;
import std;
export {
using DirCallback = std::function<void(const std::string& dir_path)>;
using FileCallback = std::function<void(const std::string& file_name)>;
class DirNav {
public:
explicit DirNav(bool recurse = true)
: recurse_(recurse)
, skip_list_({
// C#/.NET
"bin", "obj",
// Rust
"target",
// C++
"build", "out",
// Python
"__pycache__", ".venv", "venv", "dist",
// VCS / IDE
".git", ".vs", ".idea",
// archives
"archive"
})
, file_count_(0)
, dir_count_(0)
{}
void set_dir_handler(DirCallback cb) {
dir_callback_ = std::move(cb);
}
void set_file_handler(FileCallback cb) {
file_callback_ = std::move(cb);
}
void add_pattern(const std::string& ext) {
patterns_.insert(normalise_ext(ext)); // normalise once at insert
}
void add_skip(const std::string& name) {
skip_list_.insert(name);
}
bool visit(const std::filesystem::path& root) {
std::error_code ec;
bool exists = std::filesystem::exists(root, ec);
if (ec || !exists) return false;
bool is_dir = std::filesystem::is_directory(root, ec);
if (ec || !is_dir) return false;
file_count_ = 0;
dir_count_ = 0;
visit_impl(root);
return true;
}
std::size_t file_count() const { return file_count_; }
std::size_t dir_count() const { return dir_count_; }
private:
bool recurse_;
std::unordered_set<std::string> skip_list_;
std::unordered_set<std::string> patterns_;
DirCallback dir_callback_;
FileCallback file_callback_;
std::size_t file_count_;
std::size_t dir_count_;
// Strip leading dot and lowercase on Windows so lookups are O(1) and allocation-free.
static std::string normalise_ext(std::string s) {
if (!s.empty() && s.front() == '.') s.erase(s.begin());
#if defined(_WIN32)
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
#endif
return s;
}
bool extension_matches(const std::filesystem::path& file_path) const {
if (patterns_.empty()) return true;
return patterns_.contains(normalise_ext(file_path.extension().string()));
}
void visit_impl(const std::filesystem::path& dir) {
++dir_count_;
if (dir_callback_)
dir_callback_(dir.generic_string());
std::error_code ec;
std::filesystem::directory_iterator it(dir, ec);
if (ec) return;
std::vector<std::filesystem::path> subdirs;
for (const auto& entry : it) {
std::error_code entry_ec;
bool is_regular = entry.is_regular_file(entry_ec);
if (!entry_ec && is_regular) {
if (extension_matches(entry.path())) {
++file_count_;
if (file_callback_)
file_callback_(entry.path().filename().generic_string());
}
continue;
}
bool is_directory = entry.is_directory(entry_ec);
if (!entry_ec && is_directory) {
std::string bare_name = entry.path().filename().string();
if (!skip_list_.contains(bare_name) && recurse_)
subdirs.push_back(entry.path());
}
}
for (const auto& subdir : subdirs)
visit_impl(subdir);
}
};
} // export