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] ==
'!') {
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;
105 const Pattern& pattern)
const {
110 const std::string& pattern)
const {
113 size_t pattern_pos = 0;
114 size_t star_pos = std::string::npos;
115 size_t text_backup = 0;
117 while (text_pos < text.length()) {
118 if (pattern_pos < pattern.length() &&
119 (pattern[pattern_pos] == text[text_pos] || pattern[pattern_pos] ==
'?')) {
123 }
else if (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
125 star_pos = pattern_pos;
126 text_backup = text_pos;
128 }
else if (star_pos != std::string::npos) {
130 pattern_pos = star_pos + 1;
132 text_pos = text_backup;
140 while (pattern_pos < pattern.length() && pattern[pattern_pos] ==
'*') {
144 return pattern_pos == pattern.length();
160 fs::path resolved_path;
162 if (fs::path(path).is_relative()) {
163 resolved_path = fs::absolute(path, ec);
165 resolved_path = path;
168 if (ec || !fs::exists(resolved_path, ec) ||
169 !fs::is_directory(resolved_path, ec)) {
182 fs::path gitignore = resolved_path /
".gitignore";
183#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
184 if (fs::exists(gitignore, ec)) {
230 std::vector<FileEntry> entries;
232 for (
const auto& entry : fs::directory_iterator(
233 path, fs::directory_options::skip_permission_denied, ec)) {
243 fs::path entry_path = entry.path();
244 std::string filename = entry_path.filename().string();
245 bool is_dir = entry.is_directory(ec);
254 std::string relative_path =
255 fs::relative(entry_path, fs::path(
root_path_), ec).string();
276 entries.push_back(std::move(fe));
280 std::sort(entries.begin(), entries.end(), [](
const FileEntry& a,
const FileEntry& b) {
281 if (a.is_directory != b.is_directory) {
282 return a.is_directory;
287 parent.children = std::move(entries);
290bool FileBrowser::ShouldShow(
const fs::path& path,
bool is_directory)
const {
291 std::string filename = path.filename().string();
294 if (!show_hidden_files_ && !filename.empty() && filename[0] ==
'.') {
299 if (!is_directory && !file_filter_.empty()) {
300 return MatchesFilter(filename);
306bool FileBrowser::MatchesFilter(
const std::string& filename)
const {
307 if (file_filter_.empty()) {
312 size_t dot_pos = filename.rfind(
'.');
313 if (dot_pos == std::string::npos) {
317 std::string ext = filename.substr(dot_pos);
319 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
321 return file_filter_.count(ext) > 0;
324void FileBrowser::SetFileFilter(
const std::vector<std::string>& extensions) {
325 file_filter_.clear();
326 for (
const auto& ext : extensions) {
327 std::string lower_ext = ext;
328 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
331 if (!lower_ext.empty() && lower_ext[0] !=
'.') {
332 lower_ext =
"." + lower_ext;
334 file_filter_.insert(lower_ext);
336 needs_refresh_ =
true;
339void FileBrowser::ClearFileFilter() {
340 file_filter_.clear();
341 needs_refresh_ =
true;
345 const std::string& filename)
const {
347 size_t dot_pos = filename.rfind(
'.');
348 if (dot_pos == std::string::npos) {
349 return FileEntry::FileType::kUnknown;
352 std::string ext = filename.substr(dot_pos);
353 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
356 if (ext ==
".asm" || ext ==
".s" || ext ==
".65c816") {
357 return FileEntry::FileType::kAssembly;
361 if (ext ==
".cc" || ext ==
".cpp" || ext ==
".c" || ext ==
".py" ||
362 ext ==
".js" || ext ==
".ts" || ext ==
".rs" || ext ==
".go") {
363 return FileEntry::FileType::kSource;
367 if (ext ==
".h" || ext ==
".hpp" || ext ==
".hxx") {
368 return FileEntry::FileType::kHeader;
372 if (ext ==
".txt" || ext ==
".md" || ext ==
".rst") {
373 return FileEntry::FileType::kText;
377 if (ext ==
".cfg" || ext ==
".ini" || ext ==
".conf" || ext ==
".yaml" ||
378 ext ==
".yml" || ext ==
".toml") {
379 return FileEntry::FileType::kConfig;
383 if (ext ==
".json") {
384 return FileEntry::FileType::kJson;
388 if (ext ==
".png" || ext ==
".jpg" || ext ==
".jpeg" || ext ==
".gif" ||
390 return FileEntry::FileType::kImage;
394 if (ext ==
".bin" || ext ==
".sfc" || ext ==
".smc" || ext ==
".rom") {
395 return FileEntry::FileType::kBinary;
398 return FileEntry::FileType::kUnknown;
403 case FileEntry::FileType::kDirectory:
405 case FileEntry::FileType::kAssembly:
407 case FileEntry::FileType::kSource:
409 case FileEntry::FileType::kHeader:
411 case FileEntry::FileType::kText:
413 case FileEntry::FileType::kConfig:
415 case FileEntry::FileType::kJson:
417 case FileEntry::FileType::kImage:
419 case FileEntry::FileType::kBinary:
426void FileBrowser::Draw() {
427 if (root_path_.empty()) {
428 ImGui::TextDisabled(
"No folder selected");
436 if (needs_refresh_) {
443 ImGui::Text(
"%s", root_entry_.name.c_str());
444 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 24.0f);
446 needs_refresh_ =
true;
448 if (ImGui::IsItemHovered()) {
449 ImGui::SetTooltip(
"Refresh file list");
456 file_count_, directory_count_);
461 ImGui::BeginChild(
"##FileTree", ImVec2(0, 0),
false);
462 for (
auto& child : root_entry_.children) {
468void FileBrowser::DrawCompact() {
469 if (root_path_.empty()) {
470 ImGui::TextDisabled(
"No folder");
474 if (needs_refresh_) {
479 for (
auto& child : root_entry_.children) {
484void FileBrowser::DrawEntry(
FileEntry& entry,
int depth) {
485 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
486 ImGuiTreeNodeFlags_OpenOnArrow |
487 ImGuiTreeNodeFlags_OpenOnDoubleClick;
490 flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
494 flags |= ImGuiTreeNodeFlags_Selected;
501 bool node_open = ImGui::TreeNodeEx(label.c_str(), flags);
504 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
508 if (on_directory_clicked_) {
512 if (on_file_clicked_) {
519 if (ImGui::IsItemHovered()) {
520 ImGui::SetTooltip(
"%s", entry.
full_path.c_str());
525 for (
auto& child : entry.
children) {
526 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