422 lines
16 KiB
C++
422 lines
16 KiB
C++
#define _CRT_SECURE_NO_WARNINGS
|
|
#include <ftxui/component/component.hpp>
|
|
#include <ftxui/dom/elements.hpp>
|
|
#include <ftxui/component/screen_interactive.hpp>
|
|
#include <ftxui/screen/screen.hpp>
|
|
#include <iostream>
|
|
#include <filesystem>
|
|
|
|
#include "bella_sdk/bella_engine.h" // Core rendering engine
|
|
#include "bella_sdk/bella_scene.h" // Scene management
|
|
#include "dl_core/dl_logging.h" // Logging utilities
|
|
#include <thread> // For std::this_thread (C++ standard library)
|
|
#include <chrono> // For timing operations (C++ standard library)
|
|
#include <cstdio> // For freopen
|
|
|
|
std::string directory_path = ".";
|
|
int selected_index = -1; // Global variable to hold the selected index
|
|
dl::String currentRender;
|
|
|
|
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<double>(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();
|
|
}
|
|
|
|
/*
|
|
* MyEngineObserver Class
|
|
* This class receives callbacks from the Bella rendering engine to track rendering progress.
|
|
* It implements the EngineObserver interface and provides methods to:
|
|
* - Handle render start/stop events
|
|
* - Track rendering progress
|
|
* - Handle error conditions
|
|
* - Store and retrieve the current progress state
|
|
*/
|
|
struct MyEngineObserver : public dl::bella_sdk::EngineObserver
|
|
{
|
|
public:
|
|
// Called when a rendering pass starts
|
|
void onStarted(dl::String pass) override
|
|
{
|
|
//std::cout << "Started pass " << pass.buf() << std::endl;
|
|
// dl::logInfo("Started pass %s", pass.buf());
|
|
}
|
|
|
|
// Called to update the current status of rendering
|
|
//void onStatus(String pass, String status) override
|
|
//{
|
|
// logInfo("%s [%s]", status.buf(), pass.buf());
|
|
//}
|
|
|
|
// Called to update rendering progress (percentage, time remaining, etc)
|
|
void onProgress(dl::String pass, dl::bella_sdk::Progress progress) override
|
|
{
|
|
//std::cout << progress.toString().buf() << std::endl;
|
|
setString(new std::string(progress.toString().buf()));
|
|
//dl::logInfo("%s [%s]", progress.toString().buf(), pass.buf());
|
|
}
|
|
|
|
//void onImage(String pass, Image image) override
|
|
//{
|
|
// logInfo("We got an image %d x %d.", (int)image.width(), (int)image.height());
|
|
//}
|
|
|
|
// Called when an error occurs during rendering
|
|
void onError(dl::String pass, dl::String msg) override
|
|
{
|
|
//dl::logError("%s [%s]", msg.buf(), pass.buf());
|
|
}
|
|
|
|
// Called when a rendering pass completes
|
|
void onStopped(dl::String pass) override
|
|
{
|
|
// dl::logInfo("Stopped %s", pass.buf());
|
|
std::atomic_bool active_render = false;
|
|
}
|
|
|
|
// Returns the current progress as a string
|
|
std::string getProgress() const {
|
|
std::string* currentProgress = progressPtr.load();
|
|
if (currentProgress) {
|
|
return *currentProgress;
|
|
}
|
|
else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// Cleanup resources in destructor
|
|
~MyEngineObserver() {
|
|
setString(nullptr);
|
|
}
|
|
private:
|
|
// Thread-safe pointer to current progress string
|
|
std::atomic<std::string*> progressPtr{ nullptr };
|
|
|
|
// Helper function to safely update the progress string
|
|
void setString(std::string* newStatus) {
|
|
std::string* oldStatus = progressPtr.exchange(newStatus);
|
|
delete oldStatus; // Clean up old string if it exists
|
|
}
|
|
};
|
|
|
|
// Note: You must pass the ScreenInteractive object to trigger a redraw.
|
|
void ReloadDirectory(ftxui::ScreenInteractive& screen, const std::filesystem::path& new_path, std::vector<std::string>& entries, std::vector<std::string>& 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++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void render_thread(dl::bella_sdk::Engine& engine, MyEngineObserver& engineObserver, std::string selected_name, ftxui::ScreenInteractive& screen) {
|
|
// REDIRECT BOTH STDOUT AND STDERR TO NULL
|
|
// This stops warnings and logs from breaking the FTXUI layout
|
|
/*std::FILE* dummyOut;
|
|
std::FILE* dummyErr;
|
|
freopen_s(&dummyOut, "NUL", "w", stdout);
|
|
freopen_s(&dummyErr, "NUL", "w", stderr);*/
|
|
|
|
// Redirect stdout and stderr to a physical file instead of "NUL"
|
|
/*std::FILE* logFile;
|
|
if (freopen_s(&logFile, "render_log.txt", "w", stdout) != 0) {
|
|
// Optional: Handle redirection failure
|
|
}
|
|
|
|
// Redirect stderr to the same file (or a different one)
|
|
if (freopen_s(&logFile, "render_log.txt", "a", stderr) != 0) {
|
|
// Optional: Handle redirection failure
|
|
}*/
|
|
|
|
dl::String belPath = dl::String(directory_path.c_str()) + "\\";
|
|
engine.loadScene(belPath + dl::String(selected_name.c_str()));
|
|
|
|
if (!engine.scene().read(dl::String(selected_name.c_str()))) return;
|
|
|
|
dl::bella_sdk::Node bPass = engine.scene().beautyPass();
|
|
|
|
// Setup the outputImagePath node as we did in the synchronous version
|
|
dl::bella_sdk::Node pathNode = engine.scene().createNode("outputImagePath");
|
|
pathNode["dir"] = dl::String(directory_path.c_str());
|
|
pathNode["name"] = "render_result";
|
|
pathNode["ext"] = ".png";
|
|
|
|
bPass["overridePath"] = pathNode;
|
|
bPass["timeLimit"] = 30.0f;
|
|
|
|
engine.start();
|
|
|
|
// While rendering, sleep and then tell the UI to refresh
|
|
while (engine.rendering()) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
// This is the key to seeing progress while it's "asynchronous"
|
|
screen.PostEvent(ftxui::Event::Custom);
|
|
}
|
|
}
|
|
|
|
void renderSynchronous(dl::bella_sdk::Engine& engine, MyEngineObserver& engineObserver, std::string selected_name) {
|
|
dl::String belPath = dl::String(directory_path.c_str()) + "\\";
|
|
engine.loadScene(belPath + dl::String(selected_name.c_str()));
|
|
|
|
if (!engine.scene().read(dl::String(selected_name.c_str()))) return;
|
|
|
|
dl::bella_sdk::Node bPass = engine.scene().beautyPass();
|
|
|
|
// Create the mandatory output node type
|
|
dl::bella_sdk::Node pathNode = engine.scene().createNode("outputImagePath");
|
|
pathNode["dir"] = dl::String(directory_path.c_str());
|
|
pathNode["name"] = "render_result";
|
|
pathNode["ext"] = ".png";
|
|
|
|
// Link the node to the beauty pass
|
|
bPass["overridePath"] = pathNode;
|
|
bPass["timeLimit"] = 30.0f;
|
|
|
|
// This call will now "freeze" the UI until finished
|
|
engine.start();
|
|
|
|
// Explicitly wait until the engine is done to ensure the file is written
|
|
while (engine.rendering()) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
|
|
// 1. Initialize engine and observer locally within the logic block
|
|
dl::bella_sdk::Engine engine;
|
|
MyEngineObserver engineObserver;
|
|
engine.subscribe(&engineObserver);
|
|
|
|
// Define a variable to hold the final selection index
|
|
int final_selected_index = -1; // Use -1 to indicate no selection was made
|
|
std::vector<std::string> entries;
|
|
std::vector<std::string> 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;
|
|
}
|
|
|
|
ftxui::ScreenInteractive screen = ftxui::ScreenInteractive::TerminalOutput();
|
|
ReloadDirectory(screen, start_path, entries, clean_names);
|
|
int selected = 0;
|
|
auto menu = ftxui::Menu(&entries,&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);
|
|
// std::string normalized = std::filesystem::path(current_path).generic_string();
|
|
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
|
|
if (selected_name.substr(selected_name.length() - 3) == "bsz") {
|
|
// 2. Start the thread and DETACH it
|
|
// Note: We pass 'screen' as a reference so render_thread can call PostEvent
|
|
std::thread(render_thread, std::ref(engine), std::ref(engineObserver), selected_name, std::ref(screen)).detach();
|
|
}
|
|
}
|
|
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
|
|
ftxui::Component main_renderer = ftxui::Renderer(menu_with_event_handler, [&] {
|
|
// Get the latest progress string from the observer
|
|
std::string current_progress = engineObserver.getProgress();
|
|
|
|
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
|
|
|
|
// Progress Section
|
|
ftxui::separator(),
|
|
ftxui::hbox({
|
|
ftxui::text(" Render Progress: ") | ftxui::bold,
|
|
ftxui::text(current_progress.empty() ? "Idle" : current_progress)
|
|
| ftxui::color(ftxui::Color::Green),
|
|
}),
|
|
|
|
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;
|
|
}
|
|
*/
|
|
} |