From 90265c2a7e8b55a0e0fa288c5b2484e5d0265840 Mon Sep 17 00:00:00 2001 From: tjfunction412 <265773061+tjfunction412@users.noreply.github.com> Date: Wed, 13 May 2026 06:37:52 -0400 Subject: [PATCH 1/2] Windows: fix command-line file opening and Unicode path support (#30) Two related bugs prevented Windows users from opening CSV files via file association or command line. (1) WinMain in main.cpp received lpCmdLine but never parsed it. The non-Windows main(argc, argv) had an argv loop calling CsvApplication::droppedFileCB, but it was excluded from _WIN64 builds via #ifndef. Files passed by Explorer or PowerShell were silently dropped and an empty Empty 1 window appeared. Fixed by parsing the wide command line with CommandLineToArgvW (which preserves Unicode characters that lpCmdLine and __argv would lose to ANSI codepage conversion) and routing each argument through Helper::ws_to_utf8 and droppedFileCB, matching the existing non-Windows code path. (2) Helper::ws_to_utf8 and Helper::utf8_to_ws were declared in helper.hh but never implemented. Added Windows implementations using WideCharToMultiByte / MultiByteToWideChar with CP_UTF8. (3) Tablecruncher file I/O used std::ifstream::open(const char*) and std::ofstream(const char*, ...) with UTF-8 std::string paths. MSVC interprets these narrow paths as the active ANSI codepage, not UTF-8, so any filename outside that codepage failed to load even when the new argv parser delivered it correctly. This broke Unicode filenames passed via Explorer. Added Helper::openInputStream / openOutputStream wrappers that transcode UTF-8 paths to wide on Windows and use the MSVC wstring overloads, while keeping the narrow path on POSIX. Routed all 5 file-open call sites through the new helpers. Fixed Helper::getFileSize similarly (stat -> _wstat64). Replaced std::filesystem::path(std::string) calls in the recent-files loader and menu-label paths with std::filesystem::u8path, which correctly interprets the input as UTF-8 across platforms. Tested manually with ASCII paths, paths with spaces, Latin-1 supplement diacritics, Cyrillic, and CJK filenames; via command line, multi-file launch, file association double-click, and File > Save As round-trip. --- src/csvapplication.cpp | 6 +++--- src/csvmenu.cpp | 2 +- src/csvtable.cpp | 8 +++++--- src/csvwindow.cpp | 2 +- src/helper.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++ src/helper.hh | 2 ++ src/main.cpp | 19 ++++++++++++++++- 7 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/csvapplication.cpp b/src/csvapplication.cpp index adb49bf..c1477f7 100644 --- a/src/csvapplication.cpp +++ b/src/csvapplication.cpp @@ -195,7 +195,7 @@ CsvApplication::CsvApplication() { std::string tmp_key = TCRUNCHER_PREF_RECENT_FILES_STUB + std::to_string(i); std::string tmp_val = getPreference(&preferences, tmp_key, ""); if( tmp_val != "" ) { - std::filesystem::path recent_file(tmp_val); + std::filesystem::path recent_file = std::filesystem::u8path(tmp_val); if( std::filesystem::exists(recent_file) ) { recentFiles.add(tmp_val); } @@ -584,7 +584,7 @@ bool CsvApplication::splitCsvFiles() { bool splitFileExists = false; for(int i=0; i files) { std::string item_tpl = "&File/" TCRUNCHER_MENUTEXT_OPEN_RECENT "/&"; int i = 0; for( auto filepath : files ) { - std::string filename_only = std::filesystem::path(filepath).filename().u8string(); + std::string filename_only = std::filesystem::u8path(filepath).filename().u8string(); std::string item = item_tpl + filename_only; // + " (" + std::to_string(i+1) + ")"; add(item.c_str(), FL_COMMAND + ('1' + i), MyMenuCallback, (void *) &(INTEGERS[i]), 0); ++i; diff --git a/src/csvtable.cpp b/src/csvtable.cpp index 5b830e8..95c134e 100644 --- a/src/csvtable.cpp +++ b/src/csvtable.cpp @@ -694,8 +694,9 @@ int CsvTable::saveCsv(std::string path, void (*cb)(const char*, void *), void *w char msg[MAX_MSG_LEN + 1]; std::string tempStr; int retCode = 0; - std::ofstream output(path, std::ios::binary); - + std::ofstream output; + Helper::openOutputStream(output, path, std::ios::binary); + // rather stupid TODO fix when `headerRow` gets fixed std::vector headerRowCopy; headerRowCopy.resize( headerRow->size() ); @@ -757,7 +758,8 @@ int CsvTable::saveCsv(std::string path, void (*cb)(const char*, void *), void *w int CsvTable::exportJSON(std::string path, void (*cb)(const char*, void *), void *win, bool convertNumbers) { table_index_t rowCount, colCount; int retCode = saveReturnCode::SAVE_OKAY; - std::ofstream output(path, std::ios::binary); + std::ofstream output; + Helper::openOutputStream(output, path, std::ios::binary); const int MAX_MSG_LEN = 500; char msg[MAX_MSG_LEN + 1]; std::map item; diff --git a/src/csvwindow.cpp b/src/csvwindow.cpp index 9781abe..96904fa 100644 --- a/src/csvwindow.cpp +++ b/src/csvwindow.cpp @@ -344,7 +344,7 @@ bool CsvWindow::loadFile(std::string filename, bool askUser, bool reopen) { return false; } - input.open(filename); + Helper::openInputStream(input, filename); if( !input ) { CsvApplication::myFlChoice("", "Could not open file!", {"Okay"}); return false; diff --git a/src/helper.cpp b/src/helper.cpp index dcb8cf6..16b1d21 100644 --- a/src/helper.cpp +++ b/src/helper.cpp @@ -495,9 +495,15 @@ std::string Helper::padInteger(int num, int length) { // https://stackoverflow.com/questions/5840148/how-can-i-get-a-files-size-in-c long Helper::getFileSize(std::string filename) { +#ifdef _WIN64 + struct _stat64 stat_buf; + int rc = _wstat64(utf8_to_ws(filename).c_str(), &stat_buf); + return rc == 0 ? (long)stat_buf.st_size : -1; +#else struct stat stat_buf; int rc = stat(filename.c_str(), &stat_buf); return rc == 0 ? stat_buf.st_size : -1; +#endif } @@ -792,6 +798,46 @@ unsigned int Helper::getFltkFontCode(std::string fontname) { } +void Helper::openInputStream(std::ifstream& stream, const std::string& utf8Path, std::ios_base::openmode mode) { +#ifdef _WIN64 + stream.open(utf8_to_ws(utf8Path), mode); +#else + stream.open(utf8Path, mode); +#endif +} + + +void Helper::openOutputStream(std::ofstream& stream, const std::string& utf8Path, std::ios_base::openmode mode) { +#ifdef _WIN64 + stream.open(utf8_to_ws(utf8Path), mode); +#else + stream.open(utf8Path, mode); +#endif +} + + +#ifdef _WIN64 +std::string Helper::ws_to_utf8(std::wstring const& s) { + if( s.empty() ) return std::string(); + int needed = WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0, nullptr, nullptr); + if( needed <= 0 ) return std::string(); + std::string out((size_t)needed, '\0'); + WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), needed, nullptr, nullptr); + return out; +} + + +std::wstring Helper::utf8_to_ws(std::string const& utf8) { + if( utf8.empty() ) return std::wstring(); + int needed = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), (int)utf8.size(), nullptr, 0); + if( needed <= 0 ) return std::wstring(); + std::wstring out((size_t)needed, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, utf8.data(), (int)utf8.size(), out.data(), needed); + return out; +} +#endif + + diff --git a/src/helper.hh b/src/helper.hh index ba0f1fc..51a1341 100644 --- a/src/helper.hh +++ b/src/helper.hh @@ -96,6 +96,8 @@ public: static unsigned int getFltkFontCode(std::string fontname); static std::string ws_to_utf8(std::wstring const& s); static std::wstring utf8_to_ws(std::string const& utf8); + static void openInputStream(std::ifstream& stream, const std::string& utf8Path, std::ios_base::openmode mode = std::ios_base::in); + static void openOutputStream(std::ofstream& stream, const std::string& utf8Path, std::ios_base::openmode mode = std::ios_base::out); static void log(std::string msg); private: static std::map unicode2win1252; diff --git a/src/main.cpp b/src/main.cpp index c9ef7ec..1b05760 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,6 +49,10 @@ #include #include +#ifdef _WIN64 +#include +#endif + /** Global function to access the application's preferences as stored in the preferences file. */ @@ -116,7 +120,20 @@ int main(int argc, char** argv) { app.setTheme(app.getTheme()); #endif - #ifndef _WIN64 + #ifdef _WIN64 + { + int wargc = 0; + LPWSTR* wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); + if( wargv != nullptr ) { + for( int i = 1; i < wargc; ++i ) { + // open all files passed on the command line + std::string path = Helper::ws_to_utf8(std::wstring(wargv[i])); + CsvApplication::droppedFileCB(path.c_str()); + } + LocalFree(wargv); + } + } + #else for( int i = 1; i < argc; ++i ) { // open all files passed on the command line CsvApplication::droppedFileCB(argv[i]); From 2f8a30917ca1571c6ef4cba629147ec5d54a660e Mon Sep 17 00:00:00 2001 From: tjfunction412 <265773061+tjfunction412@users.noreply.github.com> Date: Wed, 13 May 2026 06:39:00 -0400 Subject: [PATCH 2/2] Embed application icon at build time via .rc resource Previously the build relied on a post-build Resource Hacker step (scripts/buildwin.bat) to inject the application icon into the built Tablecruncher.exe. This added an external-tool dependency and meant building from source without the Inno Setup release workflow produced an icon-less binary. Added a minimal assets/windows/app_icon.rc pointing to the existing app_icon.ico, and wired it into CMakeLists.txt via enable_language(RC) and an extra source on Windows. MSVC's rc.exe now embeds the icon during the normal build. The Resource Hacker step in buildwin.bat is left in place for now but is redundant for icon embedding and could be removed in a follow-up. --- CMakeLists.txt | 3 ++- assets/windows/app_icon.rc | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 assets/windows/app_icon.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index 961217f..6be7f52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,7 +115,8 @@ set(SOURCES include_directories(${SRCDIR} ${EXTERNAL} ${FLTKINCDIR} ${FLTKLIBDIR}) if(WIN32) - add_executable(${PROJECT_NAME} WIN32 ${SOURCES}) + enable_language(RC) + add_executable(${PROJECT_NAME} WIN32 ${SOURCES} assets/windows/app_icon.rc) else() add_executable(${PROJECT_NAME} ${SOURCES}) endif() diff --git a/assets/windows/app_icon.rc b/assets/windows/app_icon.rc new file mode 100644 index 0000000..76fa796 --- /dev/null +++ b/assets/windows/app_icon.rc @@ -0,0 +1 @@ +MAINICON ICON "app_icon.ico"