8#include <TargetConditionals.h>
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_split.h"
18#include "imgui/imgui.h"
23namespace fs = std::filesystem;
30 std::ifstream file(gitignore_path);
31 if (!file.is_open()) {
36 while (std::getline(file, line)) {
38 if (line.empty() || line[0] ==
'#') {
43 size_t start = line.find_first_not_of(
" \t");
44 size_t end = line.find_last_not_of(
" \t\r\n");
45 if (start == std::string::npos || end == std::string::npos) {
48 line = line.substr(start, end - start + 1);
61 if (!pattern.empty() && pattern[0] ==
'!') {
63 p.pattern = pattern.substr(1);
67 if (!p.pattern.empty() && p.pattern.back() ==
'/') {
68 p.directory_only =
true;
73 if (!p.pattern.empty() && p.pattern[0] ==
'/') {
74 p.pattern = p.pattern.substr(1);
81 bool is_directory)
const {
83 fs::path filepath(path);
84 std::string filename = filepath.filename().string();
90 if (pattern.directory_only && !is_directory) {
95 ignored = !pattern.is_negation;
107 const Pattern& pattern)
const {
112 const std::string& pattern)
const {
115 size_t pattern_pos = 0;
116 size_t star_pos = std::string::npos;
117 size_t text_backup = 0;
119 while (text_pos < text.length()) {
120 if (pattern_pos < pattern.length() &&
121 (pattern[pattern_pos] == text[text_pos] ||
122 pattern[pattern_pos] ==
'?')) {
126 }
else if (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
128 star_pos = pattern_pos;
129 text_backup = text_pos;
131 }
else if (star_pos != std::string::npos) {
133 pattern_pos = star_pos + 1;
135 text_pos = text_backup;
143 while (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
147 return pattern_pos == pattern.length();
163 fs::path resolved_path;
165 if (fs::path(path).is_relative()) {
166 resolved_path = fs::absolute(path, ec);
168 resolved_path = path;
171 if (ec || !fs::exists(resolved_path, ec) ||
172 !fs::is_directory(resolved_path, ec)) {
185 fs::path gitignore = resolved_path /
".gitignore";
186#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
187 if (fs::exists(gitignore, ec)) {
233 std::vector<FileEntry> entries;
235 for (
const auto& entry : fs::directory_iterator(
236 path, fs::directory_options::skip_permission_denied, ec)) {
246 fs::path entry_path = entry.path();
247 std::string filename = entry_path.filename().string();
248 bool is_dir = entry.is_directory(ec);
257 std::string relative_path =
258 fs::relative(entry_path, fs::path(
root_path_), ec).string();
266 fe.full_path = entry_path.string();
267 fe.is_directory = is_dir;
279 entries.push_back(std::move(fe));
283 std::sort(entries.begin(), entries.end(),
284 [](
const FileEntry& a,
const FileEntry& b) {
285 if (a.is_directory != b.is_directory) {
286 return a.is_directory;
288 return a.name < b.name;
291 parent.children = std::move(entries);
294bool FileBrowser::ShouldShow(
const fs::path& path,
bool is_directory)
const {
295 std::string filename = path.filename().string();
298 if (!show_hidden_files_ && !filename.empty() && filename[0] ==
'.') {
303 if (!is_directory && !file_filter_.empty()) {
304 return MatchesFilter(filename);
310bool FileBrowser::MatchesFilter(
const std::string& filename)
const {
311 if (file_filter_.empty()) {
316 size_t dot_pos = filename.rfind(
'.');
317 if (dot_pos == std::string::npos) {
321 std::string ext = filename.substr(dot_pos);
323 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
325 return file_filter_.count(ext) > 0;
328void FileBrowser::SetFileFilter(
const std::vector<std::string>& extensions) {
329 file_filter_.clear();
330 for (
const auto& ext : extensions) {
331 std::string lower_ext = ext;
332 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
335 if (!lower_ext.empty() && lower_ext[0] !=
'.') {
336 lower_ext =
"." + lower_ext;
338 file_filter_.insert(lower_ext);
340 needs_refresh_ =
true;
343void FileBrowser::ClearFileFilter() {
344 file_filter_.clear();
345 needs_refresh_ =
true;
348FileEntry::FileType FileBrowser::DetectFileType(
349 const std::string& filename)
const {
351 size_t dot_pos = filename.rfind(
'.');
352 if (dot_pos == std::string::npos) {
353 return FileEntry::FileType::kUnknown;
356 std::string ext = filename.substr(dot_pos);
357 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
360 if (ext ==
".asm" || ext ==
".s" || ext ==
".65c816") {
361 return FileEntry::FileType::kAssembly;
365 if (ext ==
".cc" || ext ==
".cpp" || ext ==
".c" || ext ==
".py" ||
366 ext ==
".js" || ext ==
".ts" || ext ==
".rs" || ext ==
".go") {
367 return FileEntry::FileType::kSource;
371 if (ext ==
".h" || ext ==
".hpp" || ext ==
".hxx") {
372 return FileEntry::FileType::kHeader;
376 if (ext ==
".txt" || ext ==
".md" || ext ==
".rst") {
377 return FileEntry::FileType::kText;
381 if (ext ==
".cfg" || ext ==
".ini" || ext ==
".conf" || ext ==
".yaml" ||
382 ext ==
".yml" || ext ==
".toml") {
383 return FileEntry::FileType::kConfig;
387 if (ext ==
".json") {
388 return FileEntry::FileType::kJson;
392 if (ext ==
".png" || ext ==
".jpg" || ext ==
".jpeg" || ext ==
".gif" ||
394 return FileEntry::FileType::kImage;
398 if (ext ==
".bin" || ext ==
".sfc" || ext ==
".smc" || ext ==
".rom") {
399 return FileEntry::FileType::kBinary;
402 return FileEntry::FileType::kUnknown;
405const char* FileBrowser::GetFileIcon(FileEntry::FileType type)
const {
407 case FileEntry::FileType::kDirectory:
409 case FileEntry::FileType::kAssembly:
411 case FileEntry::FileType::kSource:
413 case FileEntry::FileType::kHeader:
415 case FileEntry::FileType::kText:
417 case FileEntry::FileType::kConfig:
419 case FileEntry::FileType::kJson:
421 case FileEntry::FileType::kImage:
423 case FileEntry::FileType::kBinary:
430void FileBrowser::Draw() {
431 if (root_path_.empty()) {
432 ImGui::TextDisabled(
"No folder selected");
440 if (needs_refresh_) {
447 ImGui::Text(
"%s", root_entry_.name.c_str());
448 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 24.0f);
450 needs_refresh_ =
true;
452 if (ImGui::IsItemHovered()) {
453 ImGui::SetTooltip(
"Refresh file list");
460 file_count_, directory_count_);
465 ImGui::BeginChild(
"##FileTree", ImVec2(0, 0),
false);
466 for (
auto& child : root_entry_.children) {
472void FileBrowser::DrawCompact() {
473 if (root_path_.empty()) {
474 ImGui::TextDisabled(
"No folder");
478 if (needs_refresh_) {
483 for (
auto& child : root_entry_.children) {
488void FileBrowser::DrawEntry(FileEntry& entry,
int depth) {
489 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
490 ImGuiTreeNodeFlags_OpenOnArrow |
491 ImGuiTreeNodeFlags_OpenOnDoubleClick;
493 if (!entry.is_directory || entry.children.empty()) {
494 flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
497 if (entry.full_path == selected_path_) {
498 flags |= ImGuiTreeNodeFlags_Selected;
503 absl::StrCat(GetFileIcon(entry.file_type),
" ", entry.name);
505 bool node_open = ImGui::TreeNodeEx(label.c_str(), flags);
508 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
509 selected_path_ = entry.full_path;
511 if (entry.is_directory) {
512 if (on_directory_clicked_) {
513 on_directory_clicked_(entry.full_path);
516 if (on_file_clicked_) {
517 on_file_clicked_(entry.full_path);
523 if (ImGui::IsItemHovered()) {
524 ImGui::SetTooltip(
"%s", entry.full_path.c_str());
528 if (node_open && entry.is_directory && !entry.children.empty()) {
529 for (
auto& child : entry.children) {
530 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
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetTextDisabledVec4()
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)