diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c0296d6..9e397a6b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Apt Dependencies - run: sudo apt update && sudo apt install ninja-build qtbase5-dev qttools5-dev libboost-dev libboost-date-time-dev libboost-iostreams-dev nlohmann-json3-dev libasound2-dev librtmidi-dev libminizip-dev doctest-dev + run: sudo apt update && sudo apt install ninja-build qtbase5-dev qttools5-dev libboost-dev libboost-date-time-dev libboost-iostreams-dev nlohmann-json3-dev libasound2-dev librtmidi-dev libminizip-dev doctest-dev libfmt-dev - name: Install Other Dependencies run: vcpkg install pugixml - name: Create Build Directory @@ -44,7 +44,7 @@ jobs: - name: Install Dependencies # CMake 3.17 is already installed - run: brew install boost doctest minizip ninja nlohmann-json pugixml qt5 pugixml rtmidi + run: brew install boost doctest minizip ninja nlohmann-json pugixml qt5 pugixml rtmidi fmt - name: Generate Project run: cmake -S ${GITHUB_WORKSPACE} -B ${{runner.workspace}}/build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake - name: Build @@ -98,7 +98,7 @@ jobs: run: echo "::set-output name=VERSION_ID::$(git describe --tags --long --always)" - name: Install Dependencies - run: vcpkg install --triplet ${{ matrix.arch }}-windows boost-algorithm boost-date-time boost-endian boost-functional boost-iostreams boost-range boost-rational boost-signals2 boost-stacktrace doctest minizip nlohmann-json pugixml + run: vcpkg install --triplet ${{ matrix.arch }}-windows boost-algorithm boost-date-time boost-endian boost-functional boost-iostreams boost-range boost-rational boost-signals2 boost-stacktrace doctest minizip nlohmann-json pugixml fmt # Building Qt via vcpkg would take a while ... - name: Install Qt diff --git a/README.md b/README.md index c4991e74..49bf94be 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,10 @@ Power Tab Editor 2.0 - A powerful cross platform guitar tablature viewer and edi * [pugixml](https://pugixml.org/) * [minizip](https://github.com/madler/zlib) * [doctest](https://github.com/onqtam/doctest) +* [fmtlib](https://github.com/fmtlib/fmt) * (Linux only) - ALSA library (e.g. `libasound2-dev`) * (Linux only) - MIDI sequencer (e.g. `timidity-daemon`) -* A compiler with C++17 support. +* A compiler with C++20 support. ### Building: #### Windows: diff --git a/cmake/PTE_ThirdParty.cmake b/cmake/PTE_ThirdParty.cmake index ad5da565..c02c0bb0 100644 --- a/cmake/PTE_ThirdParty.cmake +++ b/cmake/PTE_ThirdParty.cmake @@ -11,3 +11,4 @@ include ( third_party/nlohmann_json ) include ( third_party/pugixml ) include ( third_party/Qt ) include ( third_party/rtmidi ) +include ( third_party/fmt ) diff --git a/cmake/third_party/fmt.cmake b/cmake/third_party/fmt.cmake new file mode 100644 index 00000000..31a2e99a --- /dev/null +++ b/cmake/third_party/fmt.cmake @@ -0,0 +1 @@ +find_package ( fmt 8.0.0 REQUIRED ) diff --git a/source/app/CMakeLists.txt b/source/app/CMakeLists.txt index 68de5d64..6a5cc02f 100644 --- a/source/app/CMakeLists.txt +++ b/source/app/CMakeLists.txt @@ -23,6 +23,7 @@ set( headers command.h documentmanager.h paths.h + log.h powertabeditor.h recentfiles.h scorearea.h diff --git a/source/app/log.h b/source/app/log.h new file mode 100644 index 00000000..50c39fad --- /dev/null +++ b/source/app/log.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Simon Symeonidis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef APP_LOG_H +#define APP_LOG_H + +#include + +#include +#include +#include +#include + +/* Adapted from: https://fmt.dev/latest/api.html#udt */ +template <> +struct fmt::formatter : fmt::formatter +{ + auto format(const QString& qstr, fmt::format_context& ctx) const { + return fmt::formatter::format(qstr.toStdString(), ctx); + } +}; + +#endif diff --git a/source/app/paths.cpp b/source/app/paths.cpp index 4d5bce2c..bbb9e0c8 100644 --- a/source/app/paths.cpp +++ b/source/app/paths.cpp @@ -39,6 +39,11 @@ path getConfigDir() #endif } +path getLogPath() +{ + return getConfigDir() / "log.txt"; +} + path getUserDataDir() { return fromQString( diff --git a/source/app/paths.h b/source/app/paths.h index 898a3ab4..e81bb6cf 100644 --- a/source/app/paths.h +++ b/source/app/paths.h @@ -26,6 +26,9 @@ namespace Paths { /// Return a path to a directory where config files should be written to. path getConfigDir(); + /// Return a path to the log file + path getLogPath(); + /// Return a path to a directory where persistent application data should /// be written to. path getUserDataDir(); diff --git a/source/app/powertabeditor.cpp b/source/app/powertabeditor.cpp index f6c952d2..b596bfb7 100644 --- a/source/app/powertabeditor.cpp +++ b/source/app/powertabeditor.cpp @@ -79,6 +79,7 @@ #include #include #include +#include #include #include #include @@ -260,14 +261,14 @@ void PowerTabEditor::openFile(QString filename) int validationResult = myDocumentManager->findDocument(path); if (validationResult > -1) { - qDebug() << "File: " << filename << " is already open"; + Log::d("file: {} is already open", filename); myTabWidget->setCurrentIndex(validationResult); return; } auto start = std::chrono::high_resolution_clock::now(); - qDebug() << "Opening file: " << filename; + Log::d("opening file: {}", filename); QFileInfo fileInfo(filename); std::optional format = myFileFormatManager->findFormat( @@ -285,9 +286,8 @@ void PowerTabEditor::openFile(QString filename) Document &doc = myDocumentManager->addDocument(*mySettingsManager); myFileFormatManager->importFile(doc.getScore(), path, *format); auto end = std::chrono::high_resolution_clock::now(); - qDebug() << "File loaded in" - << std::chrono::duration_cast(end - start) .count() - << "ms"; + + Log::d("file loaded in: {}ms", std::chrono::duration_cast(end - start).count()); doc.setFilename(path); setPreviousDirectory(filename); @@ -3616,7 +3616,7 @@ void PowerTabEditor::setPreviousDirectory(const QString &fileName) void PowerTabEditor::setupNewTab() { auto start = std::chrono::high_resolution_clock::now(); - qDebug() << "Tab creation started ..."; + Log::d("tab creation started..."); Q_ASSERT(myDocumentManager->hasOpenDocuments()); Document &doc = myDocumentManager->getCurrentDocument(); @@ -3788,9 +3788,7 @@ void PowerTabEditor::setupNewTab() scorearea->setFocus(); auto end = std::chrono::high_resolution_clock::now(); - qDebug() << "Tab opened in" - << std::chrono::duration_cast( - end - start).count() << "ms"; + Log::d("tab opened in: {}ms", std::chrono::duration_cast(end - start).count()); } namespace diff --git a/source/app/scorearea.cpp b/source/app/scorearea.cpp index 4ae21258..3a9c6949 100644 --- a/source/app/scorearea.cpp +++ b/source/app/scorearea.cpp @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + #include "scorearea.h" #include @@ -32,6 +32,7 @@ #include #include #include +#include void ScoreArea::Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) { @@ -114,7 +115,7 @@ void ScoreArea::renderDocument(const Document &document) #endif std::vector> tasks; const int work_size = myRenderedSystems.size() / num_threads; - qDebug() << "Using" << num_threads << "worker thread(s)"; + Log::d("using {} worker thread(s)", num_threads); for (int i = 0; i < num_threads; ++i) { @@ -162,10 +163,13 @@ void ScoreArea::renderDocument(const Document &document) myScene.setSceneRect(myScene.itemsBoundingRect()); auto end = std::chrono::high_resolution_clock::now(); - qDebug() << "Score rendered in" - << std::chrono::duration_cast( - end - start).count() << "ms"; - qDebug() << "Rendered " << myScene.items().size() << "items"; + + namespace sc = std::chrono; + const auto time_elapsed = static_cast + (sc::duration_cast(end - start).count()); + + Log::d("score rendered in {} ms", time_elapsed); + Log::d("rendered {} items", myScene.items().size()); } void ScoreArea::redrawSystem(int index) diff --git a/source/app/settingsmanager.cpp b/source/app/settingsmanager.cpp index b1083223..be0f5bcc 100644 --- a/source/app/settingsmanager.cpp +++ b/source/app/settingsmanager.cpp @@ -17,6 +17,8 @@ #include "settingsmanager.h" +#include + #include #include @@ -47,7 +49,7 @@ void SettingsManager::load(const std::filesystem::path &dir) } catch (const std::exception &e) { - std::cerr << "Error loading " << path << ": " << e.what() << std::endl; + Log::e("error loading {}: {}", path.generic_string(), e.what()); } #endif } diff --git a/source/app/tuningdictionary.cpp b/source/app/tuningdictionary.cpp index 35b8be2e..284e876b 100644 --- a/source/app/tuningdictionary.cpp +++ b/source/app/tuningdictionary.cpp @@ -26,6 +26,7 @@ #include #include #include +#include static const char *theTuningDictFilename = "tunings.json"; @@ -70,11 +71,11 @@ TuningDictionary::load() if (entries.empty()) { - std::cerr << "Could not locate tuning dictionary." << std::endl; - std::cerr << "Candidate paths:" << std::endl; + Log::e("could not locate tuning dictionary"); + Log::e("candidate paths: "); for (std::filesystem::path dir : Paths::getDataDirs()) - std::cerr << (dir / theTuningDictFilename) << std::endl; + Log::e(" - {}", (dir / theTuningDictFilename).generic_string()); } return entries; diff --git a/source/audio/midioutputdevice.cpp b/source/audio/midioutputdevice.cpp index b4aed5a4..1c8d4273 100644 --- a/source/audio/midioutputdevice.cpp +++ b/source/audio/midioutputdevice.cpp @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + #include "midioutputdevice.h" #include @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef __APPLE__ #include "midisoftwaresynth.h" @@ -38,7 +39,7 @@ MidiOutputDevice::MidiOutputDevice() : myMidiOut(nullptr) } catch (std::exception &e) { - std::cerr << e.what() << std::endl; + Log::e("could not start midisoftwaresynth: {}", e.what()); }; #endif diff --git a/source/build/main.cpp b/source/build/main.cpp index 882dc12f..a741a9e5 100644 --- a/source/build/main.cpp +++ b/source/build/main.cpp @@ -14,8 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + #include +#include #include #include #include @@ -60,7 +61,7 @@ static void displayError(const std::string &reason) // If there is no QApplication instance, something went seriously wrong // during startup - just dump the error to the console. if (!QApplication::instance()) - std::cerr << message << std::endl; + Log::e("{}", message); else { CrashDialog dialog(QString::fromStdString(message), @@ -135,23 +136,25 @@ loadTranslations(QApplication &app, QTranslator &qt_translator, QTranslator &ptb_translator) { QLocale locale; - qDebug() << "Finding translations for locale" << locale - << "with UI languages" << locale.uiLanguages(); + + Log::d("finding translations for locale:"); + for (const auto& loc : locale.uiLanguages()) + Log::d(" locale: {}", loc); for (auto &&path : Paths::getTranslationDirs()) { QString dir = Paths::toQString(path); - qDebug() << " - Checking" << dir; + Log::d(" - checking: {}", path.generic_string()); if (ptb_translator.isEmpty() && ptb_translator.load(locale, QStringLiteral("powertabeditor"), QStringLiteral("_"), dir)) { #if (QT_VERSION >= QT_VERSION_CHECK(5,15,0)) - qDebug() << "Loaded application translations from" - << ptb_translator.filePath(); + Log::d("loaded application translations from: {}", + ptb_translator.filePath()); #else - qDebug() << "Loaded application translations"; + Log::d("loaded application translations"); #endif app.installTranslator(&ptb_translator); } @@ -161,10 +164,10 @@ loadTranslations(QApplication &app, QTranslator &qt_translator, QStringLiteral("_"), dir)) { #if (QT_VERSION >= QT_VERSION_CHECK(5,15,0)) - qDebug() << "Loaded Qt base translations from" - << qt_translator.filePath(); + Log::d("loaded qt base translations from {}", + qt_translator.filePath()); #else - qDebug() << "Loaded Qt base translations"; + Log::d("loaded Qt base translations"); #endif app.installTranslator(&qt_translator); } @@ -173,6 +176,10 @@ loadTranslations(QApplication &app, QTranslator &qt_translator, int main(int argc, char *argv[]) { + Log::init(Log::Level::Debug, Paths::getLogPath()); + + Log::d("started powertab editor ({})", AppInfo::APPLICATION_VERSION); + // Register handlers for unhandled exceptions and segmentation faults. std::set_terminate(terminateHandler); std::signal(SIGSEGV, signalHandler); diff --git a/source/dialogs/infodialog.cpp b/source/dialogs/infodialog.cpp index c3aec5fa..827f38a0 100644 --- a/source/dialogs/infodialog.cpp +++ b/source/dialogs/infodialog.cpp @@ -19,6 +19,9 @@ #include "ui_infodialog.h" #include +#include + +#include #include @@ -47,9 +50,15 @@ void InfoDialog::setInfo() tr("You can grab development binaries here:\n" " https://github.com/powertab/powertabeditor/actions"); - const auto message = QString("%1\n\n%2").arg( + const auto message = QString( + "%1\n\n" + "%2\n\n" + "===============================\n" + "Logs:\n%3" + ).arg( qname, - developmentBinaryLocation + developmentBinaryLocation, + QString::fromStdString(Log::all()) ); ui->appInfo->setText(message); diff --git a/source/formats/gp7/from_xml.cpp b/source/formats/gp7/from_xml.cpp index 74800336..1de16272 100644 --- a/source/formats/gp7/from_xml.cpp +++ b/source/formats/gp7/from_xml.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -167,8 +168,7 @@ parseChordNote(const pugi::xml_node &node) note.myAccidental = *accidental; else { - std::cerr << "Unknown accidental type: " << accidental_text - << std::endl; + Log::e("unknown accidental type: {}", accidental_text); } return note; @@ -189,7 +189,7 @@ parseChordDegree(const pugi::xml_node &chord_node, const char *name) if (auto alteration = Util::toEnum(text)) degree.myAlteration = *alteration; else - std::cerr << "Unknown alteration type: " << text << std::endl; + Log::e("unknown alteration type: {}", text); return degree; } @@ -425,8 +425,7 @@ parseMasterBars(const pugi::xml_node &master_bars_node) master_bar.myDirectionTargets.push_back(*target); else { - std::cerr << "Invalid direction target type: " << target_str - << std::endl; + Log::e("invalid direction target type: {}", target_str); } } @@ -438,8 +437,7 @@ parseMasterBars(const pugi::xml_node &master_bars_node) master_bar.myDirectionJumps.push_back(*jump); else { - std::cerr << "Invalid direction jump type: " << jump_str - << std::endl; + Log::e("invalid direction jump type: {}", jump_str); } } } @@ -463,7 +461,7 @@ parseBars(const pugi::xml_node &bars_node) if (auto clef_type = Util::toEnum(clef_name)) bar.myClefType = *clef_type; else - std::cerr << "Invalid clef type: " << clef_name << std::endl; + Log::e("invalid clef type: {}", clef_name); // TODO - import the 'Ottavia' key if the clef has 8va, etc @@ -507,7 +505,7 @@ parseBeats(const pugi::xml_node &beats_node, Gp7::Version version) { beat.myOttavia = Util::toEnum(ottavia); if (!beat.myOttavia) - std::cerr << "Invalid ottavia value: " << ottavia << std::endl; + Log::e("invalid ottavia value: {}", ottavia); } beat.myFreeText = node.child_value("FreeText"); @@ -654,8 +652,7 @@ parseNotes(const pugi::xml_node ¬es_node) note.myHarmonic = Util::toEnum(harmonic_type); if (!note.myHarmonic) { - std::cerr << "Unknown harmonic type: " << harmonic_type - << std::endl; + Log::e("unknown harmonic type: {}", harmonic_type); } } else if (name == "Bended") @@ -718,8 +715,7 @@ parseNotes(const pugi::xml_node ¬es_node) note.myLeftFinger = Util::toEnum(finger_type); if (!note.myLeftFinger) { - std::cerr << "Unknown finger type: " << finger_type - << std::endl; + Log::e("unknown finger type: {}", finger_type); } } diff --git a/source/formats/gp7/to_pt2.cpp b/source/formats/gp7/to_pt2.cpp index 6f8635e0..3bc38941 100644 --- a/source/formats/gp7/to_pt2.cpp +++ b/source/formats/gp7/to_pt2.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -182,7 +183,7 @@ getHarmonicPitchOffset(double harmonic_fret) } else { - std::cerr << "Unexpected harmonic type" << std::endl; + Log::e("unexpected harmonic type"); return 12; } } diff --git a/source/formats/powertab_old/powertabdocument/macros.cpp b/source/formats/powertab_old/powertabdocument/macros.cpp index a3ba272b..a3178c51 100644 --- a/source/formats/powertab_old/powertabdocument/macros.cpp +++ b/source/formats/powertab_old/powertabdocument/macros.cpp @@ -14,14 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + #include "macros.h" + +#include + #include // std::transform #include void logToDebug(const std::string& msg, const std::string& file, int line) { - std::cerr << msg << std::endl << file << std::endl << "Line: " << line << std::endl; + Log::e("{}", msg); + Log::e("{}", file); + Log::e("line: {}", line); } std::string ArabicToRoman(uint32_t number, bool upperCase) diff --git a/source/score/serialization.cpp b/source/score/serialization.cpp index ff426ad1..c1980503 100644 --- a/source/score/serialization.cpp +++ b/source/score/serialization.cpp @@ -42,8 +42,7 @@ InputArchive::InputArchive(std::istream &is) } else { - std::cerr << "Warning: Reading an unknown file version - " << version - << std::endl; + Log::e("warning: reading an unknown file version - {}", version); // Reading in a newer version. Just do the best we can with the latest // file version we're aware of. diff --git a/source/score/serialization.h b/source/score/serialization.h index d36051b1..e060ca47 100644 --- a/source/score/serialization.h +++ b/source/score/serialization.h @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace ScoreUtils @@ -113,8 +114,7 @@ namespace detail val = *result; else { - std::cerr << "Unknown enum value: " << text - << std::endl; + Log::e("unknown enum value: {}", text); } } } diff --git a/source/score/utils/directionindex.cpp b/source/score/utils/directionindex.cpp index 3d663221..51445669 100644 --- a/source/score/utils/directionindex.cpp +++ b/source/score/utils/directionindex.cpp @@ -19,6 +19,7 @@ #include #include +#include DirectionIndex::DirectionIndex(const Score &score) : myScore(score), myActiveSymbol(DirectionSymbol::ActiveNone) @@ -147,8 +148,7 @@ SystemLocation DirectionIndex::followDirection(DirectionSymbol::SymbolType type) else { // This should not happen if the score is properly written. - std::cerr << "Could not find the symbol " - << static_cast(nextSymbol) << std::endl; + Log::e("Could not find the symbol {}", static_cast(nextSymbol)); return SystemLocation(0, 0); } } diff --git a/source/util/CMakeLists.txt b/source/util/CMakeLists.txt index 436aa137..b2037adf 100644 --- a/source/util/CMakeLists.txt +++ b/source/util/CMakeLists.txt @@ -32,6 +32,7 @@ if ( PLATFORM_OSX ) endif () set( srcs + log.cpp settingstree.cpp version.cpp @@ -43,6 +44,7 @@ set( headers enumflags.h enumtostring.h enumtostring_fwd.h + log.h settingstree.h tostring.h toutf8.h @@ -62,5 +64,5 @@ pte_library( HEADERS ${headers} DEPENDS PUBLIC Boost::headers - PRIVATE nlohmann_json::nlohmann_json ${platform_depends} + PRIVATE fmt::fmt nlohmann_json::nlohmann_json ${platform_depends} ) diff --git a/source/util/log.cpp b/source/util/log.cpp new file mode 100644 index 00000000..51147b1b --- /dev/null +++ b/source/util/log.cpp @@ -0,0 +1,138 @@ +/* Copyright (C) 2021-2024 Simon Symeonidis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "log.h" + +#include +#include +#include /* put_time */ +#include +#include +#include +#include +#include + +namespace Log { + +enum Level FilterLevel = Level::Debug; + +std::optional logFile; + +std::filesystem::path logPath; + +std::mutex lock; + +void trim(const unsigned int count) +{ + /* a poorman's circular buffer that keeps the last 'count' log lines. */ + std::vector v(count); + + std::ifstream input(logPath); + std::size_t index = 0; + std::string s; + while (std::getline(input, s)) { + v[index % count] = s; + index++; + } + + input.close(); + + // required because of len/cap discrepancy in vector used as a circular + // buffer. + const auto upto = (count == index ? index : count); + + std::ofstream output(logPath, std::ios::trunc); + if (output.good()) { + for (size_t i = 0; i < upto; ++i) + output << v[(index + i) % count] << std::endl; + } else { + std::cerr << "could not open log file for trimming" << std::endl; + } + output.close(); +} + +void init(enum Level lvl, std::filesystem::path lp) +{ + FilterLevel = lvl; + logPath = lp; + + // keep only the last X lines + trim(1000); + + if (!logFile) { + const auto mode = std::ios_base::out | std::ios_base::app; + logFile = std::ofstream(logPath, mode); + } +} + +void emitLog(enum Level level, const std::string& str) +{ + std::lock_guard _guard(Log::lock); + + /* formats a timestamp in a rfc3339 fashion; I believe the system clock + * defaults to epoch, which should be UTC, but I might be wrong. */ + const auto timestamp_now_str_fn = []() -> std::string { + auto now = std::chrono::system_clock::now(); + auto now_t = std::chrono::system_clock::to_time_t(now); + + std::stringstream ss; + ss << std::put_time(std::localtime(&now_t), "%Y-%m-%dT%H:%M:%SZ"); + return ss.str(); + }; + + const auto level_str_fn = [](enum Level l) noexcept { + switch (l) { + case Level::Debug: return "[debug]"; + case Level::Info: return "[info]"; + case Level::Notify: return "[notify]"; + case Level::Warning: return "[warn]"; + case Level::Error: return "[error]"; + /* unreachable */ + default: + return "unknown"; + } + }; + + std::cout + << timestamp_now_str_fn() << ": " + << level_str_fn(level) << ": " + << str + << std::endl; + + if (logFile.value().good()) { + logFile.value() + << timestamp_now_str_fn() << ": " + << level_str_fn(level) << ": " + << str + << std::endl; + } else { + std::cout << fmt::format( + "{} {} could not open file at ({})", + timestamp_now_str_fn(), + level_str_fn(Level::Warning), + logPath.generic_string() + ); + } +} + +std::string all() +{ + std::stringstream ss; + std::ifstream ifs(logPath); + ss << ifs.rdbuf(); + return ss.str(); +} + +} diff --git a/source/util/log.h b/source/util/log.h new file mode 100644 index 00000000..78543410 --- /dev/null +++ b/source/util/log.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2021-2024 Simon Symeonidis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UTIL_LOG_H +#define UTIL_LOG_H + +#include +#include + +/* TODO: fmtlib: replace with std, when all compilers support `format'. */ +#include + +namespace Log { + +enum class Level { + Debug = 0, + Info = 1, + Notify = 2, + Warning = 3, + Error = 4, +}; + +/** + * toggleable log level - set this to the minimum level to filter from a la + * rfc5424. + */ +extern enum Level CurrentLevel; + +void init(enum Level lvl, std::filesystem::path logPath); + +/** + * return the full contents of the log file + */ +std::string all(); + +/** + * takes a string, and emits it. Our emitters consider only a log file on the + * platform, and print to console if there is a problem if the file can not be + * opened. The emitters will also add the current timestamp, and the log level + * of the message. + */ +void emitLog(enum Level level, const std::string& str); + +template +void backend(const enum Level level, std::string_view fmt, Args&&... args) +{ + emitLog(level, fmt::vformat(fmt, fmt::make_format_args(args...))); +} + +template +void d(std::string_view fmt, Args&&... args) { backend(Level::Debug, fmt, std::forward(args)...); } + +template +void i(std::string_view fmt, Args&&... args) { backend(Level::Info, fmt, std::forward(args)...); } + +template +void n(std::string_view fmt, Args&&... args) { backend(Level::Notify, fmt, std::forward(args)...); } + +template +void w(std::string_view fmt, Args&&... args) { backend(Level::Warning, fmt, std::forward(args)...); } + +template +void e(std::string_view fmt, Args&&... args) { backend(Level::Error, fmt, std::forward(args)...); } + +} +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5ff47c82..3524e10c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -188,6 +188,7 @@ pte_executable( PCH_EXCLUDE test_main.cpp DEPENDS doctest::doctest + fmt::fmt pteapp rtmidi::rtmidi )