7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_split.h"
12#include "imgui/imgui.h"
17namespace fs = std::filesystem;
24 std::ifstream file(gitignore_path);
25 if (!file.is_open()) {
30 while (std::getline(file, line)) {
32 if (line.empty() || line[0] ==
'#') {
37 size_t start = line.find_first_not_of(
" \t");
38 size_t end = line.find_last_not_of(
" \t\r\n");
39 if (start == std::string::npos || end == std::string::npos) {
42 line = line.substr(start, end - start + 1);
55 if (!pattern.empty() && pattern[0] ==
'!') {
75 bool is_directory)
const {
77 fs::path filepath(path);
78 std::string filename = filepath.filename().string();
84 if (pattern.directory_only && !is_directory) {
89 ignored = !pattern.is_negation;
104 const std::string& pattern)
const {
107 size_t pattern_pos = 0;
108 size_t star_pos = std::string::npos;
109 size_t text_backup = 0;
111 while (text_pos < text.length()) {
112 if (pattern_pos < pattern.length() &&
113 (pattern[pattern_pos] == text[text_pos] || pattern[pattern_pos] ==
'?')) {
117 }
else if (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
119 star_pos = pattern_pos;
120 text_backup = text_pos;
122 }
else if (star_pos != std::string::npos) {
124 pattern_pos = star_pos + 1;
126 text_pos = text_backup;
134 while (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
138 return pattern_pos == pattern.length();
154 fs::path resolved_path;
156 if (fs::path(path).is_relative()) {
157 resolved_path = fs::absolute(path, ec);
159 resolved_path = path;
162 if (ec || !fs::exists(resolved_path, ec) ||
163 !fs::is_directory(resolved_path, ec)) {
176 fs::path gitignore = resolved_path /
".gitignore";
177 if (fs::exists(gitignore, ec)) {
218 std::vector<FileEntry> entries;
220 for (
const auto& entry : fs::directory_iterator(
221 path, fs::directory_options::skip_permission_denied, ec)) {
231 fs::path entry_path = entry.path();
232 std::string filename = entry_path.filename().string();
233 bool is_dir = entry.is_directory(ec);
242 std::string relative_path =
243 fs::relative(entry_path, fs::path(
root_path_), ec).string();
264 entries.push_back(std::move(fe));
268 std::sort(entries.begin(), entries.end(), [](
const FileEntry& a,
const FileEntry& b) {
269 if (a.is_directory != b.is_directory) {
270 return a.is_directory;
275 parent.children = std::move(entries);
278bool FileBrowser::ShouldShow(
const fs::path& path,
bool is_directory)
const {
279 std::string filename = path.filename().string();
282 if (!show_hidden_files_ && !filename.empty() && filename[0] ==
'.') {
287 if (!is_directory && !file_filter_.empty()) {
288 return MatchesFilter(filename);
294bool FileBrowser::MatchesFilter(
const std::string& filename)
const {
295 if (file_filter_.empty()) {
300 size_t dot_pos = filename.rfind(
'.');
301 if (dot_pos == std::string::npos) {
305 std::string ext = filename.substr(dot_pos);
307 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
309 return file_filter_.count(ext) > 0;
312void FileBrowser::SetFileFilter(
const std::vector<std::string>& extensions) {
313 file_filter_.clear();
314 for (
const auto& ext : extensions) {
315 std::string lower_ext = ext;
316 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
319 if (!lower_ext.empty() && lower_ext[0] !=
'.') {
320 lower_ext =
"." + lower_ext;
322 file_filter_.insert(lower_ext);
324 needs_refresh_ =
true;
327void FileBrowser::ClearFileFilter() {
328 file_filter_.clear();
329 needs_refresh_ =
true;
333 const std::string& filename)
const {
335 size_t dot_pos = filename.rfind(
'.');
336 if (dot_pos == std::string::npos) {
337 return FileEntry::FileType::kUnknown;
340 std::string ext = filename.substr(dot_pos);
341 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
344 if (ext ==
".asm" || ext ==
".s" || ext ==
".65c816") {
345 return FileEntry::FileType::kAssembly;
349 if (ext ==
".cc" || ext ==
".cpp" || ext ==
".c" || ext ==
".py" ||
350 ext ==
".js" || ext ==
".ts" || ext ==
".rs" || ext ==
".go") {
351 return FileEntry::FileType::kSource;
355 if (ext ==
".h" || ext ==
".hpp" || ext ==
".hxx") {
356 return FileEntry::FileType::kHeader;
360 if (ext ==
".txt" || ext ==
".md" || ext ==
".rst") {
361 return FileEntry::FileType::kText;
365 if (ext ==
".cfg" || ext ==
".ini" || ext ==
".conf" || ext ==
".yaml" ||
366 ext ==
".yml" || ext ==
".toml") {
367 return FileEntry::FileType::kConfig;
371 if (ext ==
".json") {
372 return FileEntry::FileType::kJson;
376 if (ext ==
".png" || ext ==
".jpg" || ext ==
".jpeg" || ext ==
".gif" ||
378 return FileEntry::FileType::kImage;
382 if (ext ==
".bin" || ext ==
".sfc" || ext ==
".smc" || ext ==
".rom") {
383 return FileEntry::FileType::kBinary;
386 return FileEntry::FileType::kUnknown;
391 case FileEntry::FileType::kDirectory:
393 case FileEntry::FileType::kAssembly:
395 case FileEntry::FileType::kSource:
397 case FileEntry::FileType::kHeader:
399 case FileEntry::FileType::kText:
401 case FileEntry::FileType::kConfig:
403 case FileEntry::FileType::kJson:
405 case FileEntry::FileType::kImage:
407 case FileEntry::FileType::kBinary:
414void FileBrowser::Draw() {
415 if (root_path_.empty()) {
416 ImGui::TextDisabled(
"No folder selected");
424 if (needs_refresh_) {
431 ImGui::PopStyleColor();
433 ImGui::Text(
"%s", root_entry_.name.c_str());
434 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 24.0f);
436 needs_refresh_ =
true;
438 if (ImGui::IsItemHovered()) {
439 ImGui::SetTooltip(
"Refresh file list");
446 ImGui::Text(
"%zu files, %zu folders", file_count_, directory_count_);
447 ImGui::PopStyleColor();
452 ImGui::BeginChild(
"##FileTree", ImVec2(0, 0),
false);
453 for (
auto& child : root_entry_.children) {
459void FileBrowser::DrawCompact() {
460 if (root_path_.empty()) {
461 ImGui::TextDisabled(
"No folder");
465 if (needs_refresh_) {
470 for (
auto& child : root_entry_.children) {
475void FileBrowser::DrawEntry(
FileEntry& entry,
int depth) {
476 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
477 ImGuiTreeNodeFlags_OpenOnArrow |
478 ImGuiTreeNodeFlags_OpenOnDoubleClick;
481 flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
485 flags |= ImGuiTreeNodeFlags_Selected;
492 bool node_open = ImGui::TreeNodeEx(label.c_str(), flags);
495 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
499 if (on_directory_clicked_) {
503 if (on_file_clicked_) {
510 if (ImGui::IsItemHovered()) {
511 ImGui::SetTooltip(
"%s", entry.
full_path.c_str());
516 for (
auto& child : entry.
children) {
517 DrawEntry(child, depth + 1);
void Refresh()
Refresh the file tree from disk.
bool ShouldShow(const std::filesystem::path &path, bool is_directory) const
void SetRootPath(const std::string &path)
Set the root path for the file browser.
static constexpr size_t kMaxEntries
FileEntry::FileType DetectFileType(const std::string &filename) const
void ScanDirectory(const std::filesystem::path &path, FileEntry &parent, int depth=0)
static constexpr int kMaxDepth
GitignoreParser gitignore_parser_
std::vector< Pattern > patterns_
bool MatchPattern(const std::string &path, const Pattern &pattern) const
void AddPattern(const std::string &pattern)
bool IsIgnored(const std::string &path, bool is_directory) const
bool MatchGlob(const std::string &text, const std::string &pattern) const
void LoadFromFile(const std::string &gitignore_path)
#define ICON_MD_INSERT_DRIVE_FILE
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_DATA_OBJECT
#define ICON_MD_DEVELOPER_BOARD
#define ICON_MD_DESCRIPTION
ImVec4 GetTextDisabledVec4()
ImVec4 GetTextSecondaryVec4()
Represents a file or folder in the file browser.
std::vector< FileEntry > children