9#include <TargetConditionals.h>
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_split.h"
19#include "imgui/imgui.h"
24namespace fs = std::filesystem;
31 std::ifstream file(gitignore_path);
32 if (!file.is_open()) {
37 while (std::getline(file, line)) {
39 if (line.empty() || line[0] ==
'#') {
44 size_t start = line.find_first_not_of(
" \t");
45 size_t end = line.find_last_not_of(
" \t\r\n");
46 if (start == std::string::npos || end == std::string::npos) {
49 line = line.substr(start, end - start + 1);
62 if (!pattern.empty() && pattern[0] ==
'!') {
82 bool is_directory)
const {
84 fs::path filepath(path);
85 std::string filename = filepath.filename().string();
91 if (pattern.directory_only && !is_directory) {
96 ignored = !pattern.is_negation;
108 const Pattern& pattern)
const {
113 const std::string& pattern)
const {
116 size_t pattern_pos = 0;
117 size_t star_pos = std::string::npos;
118 size_t text_backup = 0;
120 while (text_pos < text.length()) {
121 if (pattern_pos < pattern.length() &&
122 (pattern[pattern_pos] == text[text_pos] ||
123 pattern[pattern_pos] ==
'?')) {
127 }
else if (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
129 star_pos = pattern_pos;
130 text_backup = text_pos;
132 }
else if (star_pos != std::string::npos) {
134 pattern_pos = star_pos + 1;
136 text_pos = text_backup;
144 while (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
148 return pattern_pos == pattern.length();
164 fs::path resolved_path;
166 if (fs::path(path).is_relative()) {
167 resolved_path = fs::absolute(path, ec);
169 resolved_path = path;
172 if (ec || !fs::exists(resolved_path, ec) ||
173 !fs::is_directory(resolved_path, ec)) {
186 fs::path gitignore = resolved_path /
".gitignore";
187#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
188 if (fs::exists(gitignore, ec)) {
234 std::vector<FileEntry> entries;
236 for (
const auto& entry : fs::directory_iterator(
237 path, fs::directory_options::skip_permission_denied, ec)) {
247 fs::path entry_path = entry.path();
248 std::string filename = entry_path.filename().string();
249 bool is_dir = entry.is_directory(ec);
258 std::string relative_path =
259 fs::relative(entry_path, fs::path(
root_path_), ec).string();
280 entries.push_back(std::move(fe));
284 std::sort(entries.begin(), entries.end(),
286 if (a.is_directory != b.is_directory) {
287 return a.is_directory;
292 parent.children = std::move(entries);
295bool FileBrowser::ShouldShow(
const fs::path& path,
bool is_directory)
const {
296 std::string filename = path.filename().string();
299 if (!show_hidden_files_ && !filename.empty() && filename[0] ==
'.') {
304 if (!is_directory && !file_filter_.empty()) {
305 return MatchesFilter(filename);
311bool FileBrowser::MatchesFilter(
const std::string& filename)
const {
312 if (file_filter_.empty()) {
317 size_t dot_pos = filename.rfind(
'.');
318 if (dot_pos == std::string::npos) {
322 std::string ext = filename.substr(dot_pos);
324 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
326 return file_filter_.count(ext) > 0;
329void FileBrowser::SetFileFilter(
const std::vector<std::string>& extensions) {
330 file_filter_.clear();
331 for (
const auto& ext : extensions) {
332 std::string lower_ext = ext;
333 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
336 if (!lower_ext.empty() && lower_ext[0] !=
'.') {
337 lower_ext =
"." + lower_ext;
339 file_filter_.insert(lower_ext);
341 needs_refresh_ =
true;
344void FileBrowser::ClearFileFilter() {
345 file_filter_.clear();
346 needs_refresh_ =
true;
350 const std::string& filename)
const {
352 size_t dot_pos = filename.rfind(
'.');
353 if (dot_pos == std::string::npos) {
354 return FileEntry::FileType::kUnknown;
357 std::string ext = filename.substr(dot_pos);
358 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
361 if (ext ==
".asm" || ext ==
".s" || ext ==
".65c816") {
362 return FileEntry::FileType::kAssembly;
366 if (ext ==
".cc" || ext ==
".cpp" || ext ==
".c" || ext ==
".py" ||
367 ext ==
".js" || ext ==
".ts" || ext ==
".rs" || ext ==
".go") {
368 return FileEntry::FileType::kSource;
372 if (ext ==
".h" || ext ==
".hpp" || ext ==
".hxx") {
373 return FileEntry::FileType::kHeader;
377 if (ext ==
".txt" || ext ==
".md" || ext ==
".rst") {
378 return FileEntry::FileType::kText;
382 if (ext ==
".cfg" || ext ==
".ini" || ext ==
".conf" || ext ==
".yaml" ||
383 ext ==
".yml" || ext ==
".toml") {
384 return FileEntry::FileType::kConfig;
388 if (ext ==
".json") {
389 return FileEntry::FileType::kJson;
393 if (ext ==
".png" || ext ==
".jpg" || ext ==
".jpeg" || ext ==
".gif" ||
395 return FileEntry::FileType::kImage;
399 if (ext ==
".bin" || ext ==
".sfc" || ext ==
".smc" || ext ==
".rom") {
400 return FileEntry::FileType::kBinary;
403 return FileEntry::FileType::kUnknown;
408 case FileEntry::FileType::kDirectory:
410 case FileEntry::FileType::kAssembly:
412 case FileEntry::FileType::kSource:
414 case FileEntry::FileType::kHeader:
416 case FileEntry::FileType::kText:
418 case FileEntry::FileType::kConfig:
420 case FileEntry::FileType::kJson:
422 case FileEntry::FileType::kImage:
424 case FileEntry::FileType::kBinary:
431void FileBrowser::Draw() {
432 if (root_path_.empty()) {
433 ImGui::TextDisabled(tr(
"No folder selected"));
441 if (needs_refresh_) {
448 ImGui::Text(
"%s", root_entry_.name.c_str());
449 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 24.0f);
451 needs_refresh_ =
true;
453 if (ImGui::IsItemHovered()) {
454 ImGui::SetTooltip(tr(
"Refresh file list"));
461 file_count_, directory_count_);
466 ImGui::BeginChild(
"##FileTree", ImVec2(0, 0),
false);
467 for (
auto& child : root_entry_.children) {
473void FileBrowser::DrawCompact() {
474 if (root_path_.empty()) {
475 ImGui::TextDisabled(tr(
"No folder"));
479 if (needs_refresh_) {
484 for (
auto& child : root_entry_.children) {
489void FileBrowser::DrawEntry(
FileEntry& entry,
int depth) {
490 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
491 ImGuiTreeNodeFlags_OpenOnArrow |
492 ImGuiTreeNodeFlags_OpenOnDoubleClick;
495 flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
499 flags |= ImGuiTreeNodeFlags_Selected;
506 bool node_open = ImGui::TreeNodeEx(label.c_str(), flags);
509 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
513 if (on_directory_clicked_) {
517 if (on_file_clicked_) {
524 if (ImGui::IsItemHovered()) {
525 ImGui::SetTooltip(
"%s", entry.
full_path.c_str());
530 for (
auto& child : entry.
children) {
531 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,...)
Represents a file or folder in the file browser.
std::vector< FileEntry > children