From 520c28f6760da298c5f417ef7e403cc04032c1d3 Mon Sep 17 00:00:00 2001 From: Jason Ly Date: Sun, 11 Jan 2026 21:48:04 -0500 Subject: [PATCH] initial --- README.md | 63 ++++++++ joomer-ftxui-bsz-browser.cpp | 249 +++++++++++++++++++++++++++++++ joomer-ftxui-bsz-browser.vcxproj | 92 ++++++++++++ makefile | 78 ++++++++++ 4 files changed, 482 insertions(+) create mode 100644 README.md create mode 100644 joomer-ftxui-bsz-browser.cpp create mode 100644 joomer-ftxui-bsz-browser.vcxproj create mode 100644 makefile diff --git a/README.md b/README.md new file mode 100644 index 0000000..4edb52e --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# joomer-ftxui-file-browser +Terminal User Interface for file browsing using ftxui + +# Demonstrates +- file navigation + +# Usage + +``` + joomer-ftxui-file-browser [path] +[path] - path to start browsing +- use arrow keys or mouse to navigate directories +- alternatively you can press the numbers on your keyboard to select directories +``` + +# Build +``` +learndir/ +└── FTXUI/ +└── joomer-ftxui-file-browser/ +└── vcpkg/ +``` + +## Ubuntu Linux (kasm-ubuntu) +``` +mkdir learndir +cd learndir +git clone https://github.com/ArthurSonzogni/FTXUI +cd FTXUI +mkdir build +cd build +cmake .. +cmake --build . --config Release +cd ../.. +git clone https://git.indoodle.com/jason/joomer-ftxui-file-browser.git +cd joomer-ftxui-file-browser +make all +bin/Linux/release/joomer-ftxui-file-browser +``` + +## Windows (win10 enterprise) +- Download Visual Studio Community Edition 2022 +- Run VisualStudioSetup.exe +- Workload = [x] Desktop developemnt with C++ +- Individial components = [x] Git For Windows + +Run **x64 Native Tools Command Prompt for VS 2022** +``` +mkdir learndir +cd learndir +git clone https://github.com/ArthurSonzogni/FTXUI +mkdir FTXUI\build +cd FTXUI\build +cmake .. +cmake --build . --config Release +cd ..\.. +git clone https://git.indoodle.com/jason/joomer-ftxui-file-browser.git +cd joomer-ftxui-file-browser +msbuild joomer-ftxui-file-browser.vcxproj +bin\Release\joomer-ftxui-file-browser.exe +``` + + diff --git a/joomer-ftxui-bsz-browser.cpp b/joomer-ftxui-bsz-browser.cpp new file mode 100644 index 0000000..66f18f5 --- /dev/null +++ b/joomer-ftxui-bsz-browser.cpp @@ -0,0 +1,249 @@ +#include +#include +#include +#include +#include +#include + +std::string directory_path = "."; +int selected_index = -1; // Global variable to hold the selected index + +void CleanTrailingSlash(std::string& path) { + // Only strip if it's longer than a root path (e.g., "/" or "C:\") +#if defined(_WIN32) + if (path.size() > 3 && (path.back() == '/' || path.back() == '\\')) path.pop_back(); +#else + if (path.size() > 1 && path.back() == '/') path.pop_back(); +#endif +} + +std::string FormatSize(uintmax_t size) { + if (size == 0) return "0 B"; + const char* units[] = { "B", "KB", "MB", "GB", "TB" }; + int i = 0; + double d_size = static_cast(size); + while (d_size >= 1024 && i < 4) { + d_size /= 1024; + i++; + } + std::stringstream ss; + ss << std::fixed << std::setprecision(1) << d_size << " " << units[i]; + return ss.str(); +} + +// Note: You must pass the ScreenInteractive object to trigger a redraw. +void ReloadDirectory(ftxui::ScreenInteractive& screen, const std::filesystem::path& new_path, std::vector& entries, std::vector& clean_names) { + if (!std::filesystem::exists(new_path) || !std::filesystem::is_directory(new_path)) return; + + entries.clear(); + clean_names.clear(); + + //std::string directory_path = new_path.string(); // Replace with your directory path //windows + // Check if the path is a valid directory + if (!std::filesystem::exists(new_path) || !std::filesystem::is_directory(new_path)) { + // Handle error case (optional: display a warning in the TUI) + std::cerr << "Error: Not a directory or path does not exist." << std::endl; + std::cerr << "Path not a directory: " + new_path.string(); + return; + } + + // 2. Read the new directory entries + int i = 1; + + clean_names.clear(); + entries.clear(); + entries.push_back("0. .. (go up one level)"); // Option to go up one level + clean_names.push_back(".."); + if (std::filesystem::is_directory(new_path)) { + // Append slash to directories for clarity + for (const auto& entry : std::filesystem::directory_iterator(new_path)) { + + if (std::filesystem::is_directory(entry.path()) || std::filesystem::is_regular_file(entry.path())) { + + std::string name = entry.path().filename().string(); + if (!name.empty() && name[0] == '.') // Works in C++11, C++14, and C++17 + continue; // Skip hidden files and directories + if (std::filesystem::is_directory(entry.path())) { + name += "/"; // Append slash to directories for clarity + } + if (std::filesystem::is_regular_file(entry.path()) && i == selected_index) { + // Optionally, append file size + auto size = std::filesystem::file_size(entry.path()); + name += " (" + FormatSize(size) + ")"; + } + entries.push_back(std::to_string(i) + ". " + name); + clean_names.push_back(name); + i++; + } + } + } +} + +int main(int argc, char* argv[]) { + // Define a variable to hold the final selection index + int final_selected_index = -1; // Use -1 to indicate no selection was made + std::vector entries; + std::vector clean_names; // New vector for logic + + std::string current_path = std::filesystem::current_path().string(); + std::filesystem::path start_path = std::filesystem::current_path(); + + if (argc > 1) + { + // Convert relative input (like "../../..") into a full absolute path + start_path = std::filesystem::absolute(argv[1]); + + // Convert to string + start_path = start_path.lexically_normal(); + std::string path_str = start_path.string(); + // Manually remove trailing slash if it exists and isn't the root directory + if (path_str.size() > 1 && (path_str.back() == '/' || path_str.back() == '\\')) { + path_str.pop_back(); + } + + current_path = path_str; + + } + + directory_path = current_path; // Update global + + // Check if the directory exists + if (!std::filesystem::exists(directory_path) || !std::filesystem::is_directory(directory_path)) { + std::cerr << "Error: Directory '" << directory_path << "' not found or is not a directory." << std::endl; + return 1; + } + + auto screen = ftxui::ScreenInteractive::TerminalOutput(); + ReloadDirectory(screen, start_path, entries, clean_names); + int selected = 0; + auto menu = ftxui::Menu({ + .entries = &entries, + .selected = &selected, + }); + + std::string input_buffer = ""; // Stores digits as the user types them + + // --- The key part: Applying CatchEvent() --- + auto menu_with_event_handler = menu | ftxui::CatchEvent([&](ftxui::Event event) { + // 1. Handle Digit Input (0-9) + if (event.is_character()) { + char c = event.character()[0]; + if (std::isdigit(c)) { + input_buffer += c; // Add the digit to our "typing" buffer + return true; + } + } + + // 2. Handle Backspace (to correct typing errors) + if (event == ftxui::Event::Backspace && !input_buffer.empty()) { + input_buffer.pop_back(); + return true; + } + + // Handle the 'Enter' key press + if (event == ftxui::Event::Return) { + if (!input_buffer.empty()) { + try + { + // Convert buffer to integer and check bounds + int requested_index = std::stoi(input_buffer);// -1; + if (requested_index >= 0 && requested_index < (int)clean_names.size()) { + selected = requested_index; // Move the highlight to the typed index + } + } catch (...) {} + input_buffer.clear(); // Clear buffer after use + } + std::string selected_name; + std::filesystem::path new_path; + + if (clean_names.size() > 0) + selected_name = clean_names[selected]; + // Construct the new path + if (selected_name == "..") { + new_path = std::filesystem::path(current_path).parent_path(); + } + else { + new_path = std::filesystem::path(current_path) / selected_name; + } + + new_path = std::filesystem::absolute(new_path).lexically_normal(); + + new_path.make_preferred(); + + + + CleanTrailingSlash(current_path); + directory_path = current_path; // Update global variable + if (std::filesystem::is_regular_file(new_path)) + { + selected_index = selected; // Update global selected index + final_selected_index = selected; // Store the final selection index + // screen.Exit(); // Exit the application + } + else + { + current_path = new_path.string(); + } + // Call the function to switch and reload + ReloadDirectory(screen, current_path, entries, clean_names); + selected = 0; // Reset selection to the first item + + return true; // Event handled + } + + // Handle the 'q' key press to quit anytime + if (event == ftxui::Event::Character('q')) { + screen.Exit(); + return true; // Event handled + } + + // If the event is not one we want to catch, return false + // so the Menu can handle it (like arrow keys) + return false; + }); + + // Create a renderer that defines the layout of your application + auto main_renderer = ftxui::Renderer(menu_with_event_handler, [&] { + return ftxui::vbox({ + + // Header showing the current directory + ftxui::hbox({ + ftxui::text(" Current Directory: ") | ftxui::bold, + ftxui::text(current_path) | ftxui::color(ftxui::Color::Cyan), + }), + ftxui::separator(), + + // The menu itself, wrapped in a frame to allow scrolling if the list is long + menu_with_event_handler->Render() | ftxui::vscroll_indicator | ftxui::frame, + + ftxui::filler(), // Pushes the footer to the bottom + ftxui::separator(), + + ftxui::hbox({ + ftxui::text("Enter folder #: "), + ftxui::text(input_buffer), + }), + + // Footer with instructions + ftxui::hbox({ + ftxui::text(" [Enter] Open [q] Quit "), + ftxui::filler(), + ftxui::text(" Items: " + std::to_string(clean_names.size())) | ftxui::dim, + }), + }) | ftxui::border; // Adds a border around the whole app + }); + + screen.Loop(main_renderer); + // This will print to your terminal AFTER you press 'q' to quit + std::cout << "Final Path on Exit: " << current_path << std::endl; + // ---------------------------------------------------- + // --- OUTPUT AFTER THE LOOP HAS CLEANLY EXITED --- + // ---------------------------------------------------- + std::cout<< "directory path: " << directory_path << std::endl; + if (final_selected_index >= 0 && final_selected_index < entries.size()) { + std::cout << "Selected Entry: " << entries[final_selected_index] << std::endl; + } else { + std::cout << "Selection cancelled or no valid entry selected." << std::endl; + } + +} \ No newline at end of file diff --git a/joomer-ftxui-bsz-browser.vcxproj b/joomer-ftxui-bsz-browser.vcxproj new file mode 100644 index 0000000..6d67fb3 --- /dev/null +++ b/joomer-ftxui-bsz-browser.vcxproj @@ -0,0 +1,92 @@ + + + + Release + x64 + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + {161A1C11-0965-0C89-738B-B0F41F004E31} + Win32Proj + joomerftxuifilebrowser + 10.0 + + + false + + + + Application + $(DefaultPlatformToolset) + Unicode + + + true + + + false + + + + + + true + $(SolutionDir)bin\$(Configuration)\ + $(SolutionDir)obj\$(Configuration)\ + + + false + $(SolutionDir)bin\$(Configuration)\ + $(SolutionDir)obj\$(Configuration)\ + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDebugDLL + $(SolutionDir)\..\FTXUI\include;%(AdditionalIncludeDirectories) + + + Console + true + $(SolutionDir)\..\FTXUI\build\Release;%(AdditionalLibraryDirectories) + ftxui-component.lib;ftxui-dom.lib;ftxui-screen.lib;msvcprtd.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + + Level3 + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDLL + $(SolutionDir)\..\FTXUI\include;%(AdditionalIncludeDirectories) + + + Console + false + $(SolutionDir)\..\FTXUI\build\Release;%(AdditionalLibraryDirectories) + ftxui-component.lib;ftxui-dom.lib;ftxui-screen.lib;msvcprt.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + + + + \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..635e34d --- /dev/null +++ b/makefile @@ -0,0 +1,78 @@ + +# Project configuration +EXECUTABLE_NAME = joomer-ftxui-file-browser +PLATFORM = $(shell uname) +BUILD_TYPE ?= release# Default to release build if not specified + +#STB_PATH = $(HOME)/workdir/stb +LINUX_LIB_EXT = so + +# Version configuration (can be overridden) +FTXUI_PATH = ../FTXUI +LIB_PATHS = -L$(FTXUI_PATH)/build +INC_DIRS = -I$(FTXUI_PATH)/include + +# Common paths +OBJ_DIR = obj/$(PLATFORM)/$(BUILD_TYPE) +BIN_DIR = bin/$(PLATFORM)/$(BUILD_TYPE) +OUTPUT_FILE = $(BIN_DIR)/$(EXECUTABLE_NAME) + +CXX = g++ +CXX_FLAGS = $(COMMON_FLAGS) -std=c++17 -Wall -g + +# Build type specific flags +ifeq ($(BUILD_TYPE), debug) + CPP_DEFINES = -D_DEBUG + COMMON_FLAGS = $(ARCH_FLAGS) + COMP_FLAGS = -g -O0 -std=c++17 -Wall + #LINK_FLAGS = -fvisibility=hidden +else + CPP_DEFINES = -DNDEBUG=1 + COMMON_FLAGS = $(ARCH_FLAGS) + COMP_FLAGS = -O3 -std=c++17 -Wall + #LINK_FLAGS = -fvisibility=hidden +endif + +# Language-specific flags +CXX = g++ +CXX_FLAGS = $(COMMON_FLAGS) +LINK_FLAGS = $(LIB_PATHS) #-fvisibility=hidden +FTXUI_LIB_FLAGS = -lftxui-component -lftxui-dom -lftxui-screen + +# Linker directive flags (L for library search path, l for library) +# Need to link against the D++ library and pthread (common for C++ applications with threading) +LDFLAGS = -Wl,--start-group $(FTXUI_LIB_FLAGS) -Wl,--end-group +//LDFLAGS = $(FTXUI_LIB_FLAGS) #$(BELLA_LIB_FLAGS) #-L$(LIB_PATHS) -ldpp -lpthread -Wl,-rpath='$$ORIGIN' # search for lib in the same place as the executable file + +# List of object files for your executable +# We've changed this back to use joomer-ftxui-file-browser.o as the source of the executable +OBJECTS = $(OBJ_DIR)/joomer-ftxui-file-browser.o + +$(OBJ_DIR)/%.o: %.cpp + @echo "Compiling $< -> $@" + @mkdir -p $(@D) # Ensure the output binary directory exists (e.g., bin/Linux/release/) + $(CXX) -c $(CPP_DEFINES) $(COMP_FLAGS) $(INC_DIRS) $< -o $@ + +# --- Main Target --- +# 'all' is the default target that builds your executable +all: $(OUTPUT_FILE) + @echo "Building executables complete." + +# Rule to link the executable: +# It depends on the object files and uses CXX to link them with specified libraries. +$(OUTPUT_FILE): $(OBJECTS) + @echo "Linking $(OUTPUT_FILE)..." + @mkdir -p $(@D) + $(CXX) -o $@ $(OBJECTS) $(LINK_FLAGS) $(LDFLAGS) + @echo "Building links complete." + + +# --- Clean Target --- +# Removes all generated build files and directories +clean: + @echo "Cleaning build directory..." + $(RM) -r $(BIN_DIR) + $(RM) -r $(OBJ_DIR) + +# .PHONY specifies targets that are not actual files to prevent conflicts with file names +.PHONY: all clean \ No newline at end of file