diff options
-rw-r--r-- | CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/archive.cpp | 7 | ||||
-rw-r--r-- | src/config.cpp | 390 | ||||
-rw-r--r-- | src/config.hpp | 103 | ||||
-rw-r--r-- | src/getsuyomi.cpp | 2 | ||||
-rw-r--r-- | src/luacxx.cpp | 33 | ||||
-rw-r--r-- | src/main_window.cpp | 162 | ||||
-rw-r--r-- | src/main_window.hpp | 15 | ||||
-rw-r--r-- | src/settings.cpp | 167 | ||||
-rw-r--r-- | src/settings.hpp | 80 |
10 files changed, 579 insertions, 381 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 175aa56..ac94964 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ qt_add_executable(getsuyomi src/archive.cpp src/getsuyomi.cpp src/fud_mem.cpp + src/settings.cpp src/config.cpp src/luacxx.cpp ) diff --git a/src/archive.cpp b/src/archive.cpp index d773f73..b4bee29 100644 --- a/src/archive.cpp +++ b/src/archive.cpp @@ -1,8 +1,5 @@ #include "archive.hpp" - -#include "main_window.hpp" - -#include <algorithm> +#include "config.hpp" namespace getsuyomi { @@ -46,6 +43,8 @@ ZipArchive::~ZipArchive() ZipArchive& ZipArchive::operator=(ZipArchive&& rhs) { + cleanup(); + m_archive = rhs.m_archive; m_sortedIndices = std::move(rhs.m_sortedIndices); m_filenames = std::move(rhs.m_filenames); diff --git a/src/config.cpp b/src/config.cpp index 5ffb76e..ab7d3ca 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -3,13 +3,145 @@ #include "luacxx.hpp" #include <algorithm> -#include <fcntl.h> +#include <filesystem> +#include <fud_c_file.hpp> +#include <fud_status.hpp> +#include <fud_string.hpp> #include <vector> namespace getsuyomi { using fud::FudStatus; +constexpr auto PermNone = fud::PermissionType::None; +constexpr auto defaultPermissions = fud::Permissions(fud::PermReadWrite, PermNone, PermNone); + +std::optional<std::string> getEnvVar(const char* envVar) +{ + const QByteArray defaultArray{}; + if (envVar == nullptr) { + return std::nullopt; + } + QByteArray varArray = qgetenv(envVar); + if (varArray == defaultArray) { + return std::nullopt; + } + return varArray.toStdString(); +} + +void getEnvVar(const std::string& homeVar, const std::string& envVar, std::string& envValue, const char* backup) +{ + auto envValueOpt = getEnvVar(envVar.c_str()); + std::filesystem::path envValuePath{homeVar}; + if (envValueOpt == std::nullopt || envValueOpt->length() == 0) { + envValuePath.append(backup); + } else { + envValuePath = *envValueOpt; + } + envValuePath.append(AppName); + envValue = envValuePath; +} + +GetEnvResult getEnvironment() +{ + GetsuyomiConfig config{}; + + auto homeOpt = getEnvVar(HOME); + if (homeOpt == std::nullopt || homeOpt->length() == 0) { + qCritical("Error getting home"); + return GetEnvResult::error(FudStatus::Failure); + } + config.home = *homeOpt; + + /* If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. */ + const std::string XdgDataHome{"XDG_DATA_HOME"}; + getEnvVar(config.home, XdgDataHome, config.dataHome, ".local/share"); + + /* If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used. */ + const std::string XdgConfigHome{"XDG_CONFIG_HOME"}; + getEnvVar(config.home, XdgConfigHome, config.configHome, ".config"); + + /* If $XDG_STATE_HOME is either not set or empty, a default equal to $HOME/.local/state should be used. */ + const std::string XdgStateHome{"XDG_STATE_HOME"}; + getEnvVar(config.home, XdgStateHome, config.stateHome, ".local/state"); + + return GetEnvResult::okay(config); +} + +FudStatus createXdgDirectory(const std::string& directoryName) +{ + constexpr mode_t xdgMode = 0700; + auto dirStatus = mkdir(directoryName.c_str(), xdgMode); + if (dirStatus == 0) { + return FudStatus::Success; + } + + if (errno != EEXIST) { + return FudStatus::Failure; + } + struct stat statBuffer {}; + dirStatus = stat(directoryName.c_str(), &statBuffer); + + if (dirStatus != 0) { + return FudStatus::Failure; + } + + if ((statBuffer.st_mode & S_IFMT) != S_IFDIR) { + return FudStatus::Failure; + } + + return FudStatus::Success; +} + +void setUserConfig(GetsuyomiConfig& config, const ShortcutMap& shortcutMap) +{ + auto binder = [&](ShortcutList& shortcuts, ActionType action) { + shortcuts = shortcutListFromSet(shortcutMap.at(action)); + }; + + binder(config.openFileShortcuts, ActionType::OpenFile); + binder(config.openDirectoryShortcuts, ActionType::OpenDirectory); + binder(config.quitShortcuts, ActionType::Quit); + + binder(config.settingsShortcuts, ActionType::Configure); + + binder(config.nextShortcuts, ActionType::Next); + binder(config.backShortcuts, ActionType::Back); + + binder(config.singlePageShortcuts, ActionType::SinglePage); + binder(config.dualPageShortcuts, ActionType::DualPage); + binder(config.mangaPageShortcuts, ActionType::MangaPage); +} + +GetConfigResult getUserConfig() +{ + auto configResult = getEnvironment(); + if (configResult.isError()) { + return GetConfigResult::error(configResult.getError()); + } + auto config = configResult.getOkay(); + + auto dirStatus = createXdgDirectory(config.dataHome); + if (dirStatus != FudStatus::Success) { + return GetConfigResult::error(dirStatus); + } + dirStatus = createXdgDirectory(config.configHome); + if (dirStatus != FudStatus::Success) { + return GetConfigResult::error(dirStatus); + } + dirStatus = createXdgDirectory(config.stateHome); + if (dirStatus != FudStatus::Success) { + return GetConfigResult::error(dirStatus); + } + + config.configFilename = std::filesystem::path(config.configHome).append("config.lua"); + auto shortcutMap = Shortcuts::fromUserConfig(config.configFilename); + + setUserConfig(config, shortcutMap); + + return GetConfigResult::okay(config); +} + ShortcutSet shortcutSetFromList(const ShortcutList& shortcutList) { ShortcutSet shortcutSet{}; @@ -214,14 +346,14 @@ Shortcuts Shortcuts::fromLuaConfig(const std::filesystem::path& configFileName) ShortcutMap Shortcuts::fromUserConfig(const std::filesystem::path& configFileName) { - constexpr mode_t configFileMode = 0600; - auto fdResult = open(configFileName.c_str(), O_CREAT, configFileMode); - if (fdResult == -1) { + fud::String filepath(configFileName.c_str()); + fud::CTextFile file{filepath, fud::CFileMode::ReadWriteAppend}; + auto status = file.create(defaultPermissions); + if (status != FudStatus::Success) { if (errno != EEXIST) { qCritical("Could not load or create user config file %s.", configFileName.c_str()); } } - close(fdResult); auto shortcuts = Shortcuts::fromLuaConfig(configFileName); @@ -254,181 +386,135 @@ ShortcutMap Shortcuts::fromUserConfig(const std::filesystem::path& configFileNam return shortcuts.shortcutMap(); } -ShortcutMap GetsuyomiConfig::shortcuts() const +FudStatus backupFile(const fud::String& filepath) { - ShortcutMap shortcuts{}; + fud::CTextFile file{filepath, fud::CFileMode::ReadWriteAppend}; + auto status = file.create(defaultPermissions); + if (status != FudStatus::Success) { + if (errno != EEXIST) { + qCritical("Could not load or create user config file %s.", filepath.c_str()); + } + } - shortcuts.insert({ActionType::OpenFile, shortcutSetFromList(openFileShortcuts)}); - shortcuts.insert({ActionType::OpenDirectory, shortcutSetFromList(openDirectoryShortcuts)}); - shortcuts.insert({ActionType::Quit, shortcutSetFromList(quitShortcuts)}); + size_t fileSize; + auto fileSizeStatus = file.size(); + if (fileSizeStatus.isOkay()) { + fileSize = fileSizeStatus.getOkay(); + } else { + qWarning("Could not get file size"); + return fileSizeStatus.getError(); + } - shortcuts.insert({ActionType::Configure, shortcutSetFromList(settingsShortcuts)}); + if (fileSize == 0) { + return FudStatus::Empty; + } - shortcuts.insert({ActionType::Next, shortcutSetFromList(nextShortcuts)}); - shortcuts.insert({ActionType::Back, shortcutSetFromList(backShortcuts)}); + std::vector<char> backupFileData(fileSize); + auto readStatus = file.read(backupFileData.data(), backupFileData.size(), fileSize); + if (readStatus.status != FudStatus::Success) { + qWarning("Could not read existing config"); + return readStatus.status; + } - shortcuts.insert({ActionType::SinglePage, shortcutSetFromList(singlePageShortcuts)}); - shortcuts.insert({ActionType::DualPage, shortcutSetFromList(dualPageShortcuts)}); - shortcuts.insert({ActionType::MangaPage, shortcutSetFromList(mangaPageShortcuts)}); + auto backupFileName{filepath.catenate(".bak")}; + fud::CTextFile backupFile{backupFileName, fud::CFileMode::ReadWriteTruncate}; + status = backupFile.create(defaultPermissions); + if (status != FudStatus::Success) { + qWarning("Could not create or open backup file %s", backupFileName.c_str()); + return status; + } - return shortcuts; -} + auto writeResult = backupFile.write(backupFileData.data(), backupFileData.size()); + if (writeResult.status != FudStatus::Success) { + qWarning("Could not write backupfile"); + } -ShortcutDisplay::ShortcutDisplay(QWidget* parent, QKeySequence shortcut) : QWidget{parent}, m_binding{shortcut} -{ - auto layout = new QHBoxLayout(); - layout->addWidget(new QLabel(shortcut.toString())); - auto* deleteButton = new QPushButton("Delete", this); - connect(deleteButton, &QPushButton::clicked, this, &ShortcutDisplay::removeOnClicked); - layout->addWidget(deleteButton); - setLayout(layout); + return writeResult.status; } -void ShortcutDisplay::removeOnClicked() +FudStatus Shortcuts::save(const std::filesystem::path& configFileName) const { - emit removeClicked(m_binding); -} + fud::String filepath(configFileName.c_str()); + static_cast<void>(backupFile(filepath)); -ShortcutCollector::ShortcutCollector(QWidget* parent, ActionType action, Shortcuts& shortcuts) : - QWidget(parent), m_action{action}, m_shortcuts{shortcuts}, m_bindings{} -{ - m_layout = new QVBoxLayout(); - auto headerLayout = new QHBoxLayout(); - auto* name = new QLabel(actionTypeToString(m_action)); - headerLayout->addWidget(name); - m_shortcutEditor = new QKeySequenceEdit(this); - m_shortcutEditor->setMaximumSequenceLength(1); - connect(m_shortcutEditor, &QKeySequenceEdit::editingFinished, this, &ShortcutCollector::checkBinding); - headerLayout->addWidget(m_shortcutEditor); - - m_acceptButton = new QPushButton("Accept", this); - m_acceptButton->setEnabled(false); - connect(m_acceptButton, &QPushButton::clicked, this, &ShortcutCollector::addBinding); - headerLayout->addWidget(m_acceptButton); - headerLayout->addStretch(); - - m_layout->addLayout(headerLayout); - - auto shortcutOptions = m_shortcuts.shortcuts(m_action); - if (shortcutOptions != std::nullopt) { - for (const auto& binding : *shortcutOptions) { - createBinding(binding); + fud::CTextFile file{filepath, fud::CFileMode::ReadWriteTruncate}; + auto status = file.create(defaultPermissions); + if (status != FudStatus::Success) { + if (errno != EEXIST) { + qCritical("Could not load or create user config file %s.", configFileName.c_str()); } - } else { - qWarning("No shortcuts found for %s", actionTypeToString(action)); } - setLayout(m_layout); -} - -void ShortcutCollector::createBinding(QKeySequence binding) -{ - auto displayItem = new ShortcutDisplay(this, binding); - m_bindings[binding] = displayItem; - m_layout->addWidget(displayItem); - connect(displayItem, &ShortcutDisplay::removeClicked, this, &ShortcutCollector::removeBinding); -} - -void ShortcutCollector::checkBinding() -{ - auto keySequence = m_shortcutEditor->keySequence(); - if (keySequence == QKeySequence::UnknownKey) { - m_acceptButton->setEnabled(false); - return; + fud::String output{}; + constexpr size_t defaultFileSize = 4096; + auto reserveStatus = output.reserve(defaultFileSize); + if (reserveStatus != FudStatus::Success) { + qCritical("Failed to reserve space for new file data."); + return reserveStatus; } - m_acceptButton->setEnabled(not m_shortcuts.contains(keySequence)); -} + for (auto [action, bindings] : m_actionToShortcuts) { + auto appendStatus = output.append(actionTypeToString(action)); + if (appendStatus == FudStatus::Success) { + appendStatus = output.append(" = {"); + } -void ShortcutCollector::addBinding() -{ - auto keySequence = m_shortcutEditor->keySequence(); - if (keySequence == QKeySequence::UnknownKey) { - qWarning("Invalid state - can't accept unknown key"); - return; - } else if (m_shortcuts.contains(keySequence)) { - qWarning("Shortcut %s already bound", qPrintable(keySequence.toString())); - return; - } - auto result = m_shortcuts.bind(m_action, keySequence); - if (result != FudStatus::Success) { - qCritical("Error binding %s to action %s", qPrintable(keySequence.toString()), actionTypeToString(m_action)); - return; - } - if (m_bindings.contains(keySequence)) { - qWarning("binding %s to action %s already exists", qPrintable(keySequence.toString()), actionTypeToString(m_action)); - m_layout->removeWidget(m_bindings[keySequence]); - delete m_bindings[keySequence]; - } - - createBinding(keySequence); - m_shortcutEditor->clear(); -} + for (auto binding : bindings) { + if (appendStatus == FudStatus::Success) { + appendStatus = output.append("\""); + } else { + break; + } + if (appendStatus == FudStatus::Success) { + appendStatus = output.append(binding.toString().toUtf8().data()); + } else { + break; + } + if (appendStatus == FudStatus::Success) { + appendStatus = output.append("\", "); + } else { + break; + } + } + if (appendStatus == FudStatus::Success) { + appendStatus = output.append("}\n"); + } else { + qWarning("Failed to append to file data %s.", fud::FudStatusToString(appendStatus)); + return appendStatus; + } -void ShortcutCollector::removeBinding(QKeySequence binding) -{ - auto result = m_shortcuts.remove(binding); - if (result == FudStatus::NotFound) { - qWarning("binding %s not found", qPrintable(binding.toString())); - } else if (result != FudStatus::Success) { - qWarning("error removing binding %s: %s", qPrintable(binding.toString()), FudStatusToString(result)); - } - auto bindingHandle = m_bindings.extract(binding); - if (bindingHandle) { - m_layout->removeWidget(bindingHandle.mapped()); - delete bindingHandle.mapped(); - } else { - qWarning("Could not remove widget!"); + if (!output.utf8Valid()) { + qCritical("Output string is invalid."); + return FudStatus::Failure; + } } - checkBinding(); -} - -Settings::Settings(QWidget* parent, Shortcuts&& shortcuts) : QDialog{parent}, m_shortcuts{std::move(shortcuts)} -{ - auto* layout = new QVBoxLayout(); - setWindowTitle("getsuyomi settings"); - if (!m_shortcuts.valid()) { - return; + auto writeStatus = file.write(output.data(), output.length()); + if (writeStatus.status != FudStatus::Success) { + qCritical("Could not save user config file %s.", configFileName.c_str()); } - auto containerLayout = new QHBoxLayout(); - auto* columnLayout = new QVBoxLayout(); - size_t counter{0}; - constexpr size_t maxEntriesPerColumn{4}; - - for (const auto& action : m_shortcuts.actions()) { - auto* collector = new ShortcutCollector(this, action, m_shortcuts); - columnLayout->addWidget(collector); - counter++; - if (counter % (maxEntriesPerColumn + 1) == 0) { - containerLayout->addLayout(columnLayout); - columnLayout = new QVBoxLayout(); - } - } + return writeStatus.status; +} - if (columnLayout->count() > 0) { - containerLayout->addLayout(columnLayout); - } +ShortcutMap GetsuyomiConfig::shortcuts() const +{ + ShortcutMap shortcuts{}; - layout->addLayout(containerLayout); + shortcuts.insert({ActionType::OpenFile, shortcutSetFromList(openFileShortcuts)}); + shortcuts.insert({ActionType::OpenDirectory, shortcutSetFromList(openDirectoryShortcuts)}); + shortcuts.insert({ActionType::Quit, shortcutSetFromList(quitShortcuts)}); - auto* dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(dialogButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - layout->addWidget(dialogButtonBox); + shortcuts.insert({ActionType::Configure, shortcutSetFromList(settingsShortcuts)}); - setLayout(layout); -} + shortcuts.insert({ActionType::Next, shortcutSetFromList(nextShortcuts)}); + shortcuts.insert({ActionType::Back, shortcutSetFromList(backShortcuts)}); -bool Settings::valid() const -{ - return m_shortcuts.valid(); -} + shortcuts.insert({ActionType::SinglePage, shortcutSetFromList(singlePageShortcuts)}); + shortcuts.insert({ActionType::DualPage, shortcutSetFromList(dualPageShortcuts)}); + shortcuts.insert({ActionType::MangaPage, shortcutSetFromList(mangaPageShortcuts)}); -const Shortcuts& Settings::shortcuts() const -{ - return m_shortcuts; + return shortcuts; } } // namespace getsuyomi diff --git a/src/config.hpp b/src/config.hpp index 5937a9b..bc5698e 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -1,18 +1,26 @@ #ifndef GETSUYOMI_CONFIG_HPP #define GETSUYOMI_CONFIG_HPP -#include <QtWidgets> -#include <fud_status.hpp> #include <fud_result.hpp> +#include <fud_status.hpp> #include <map> #include <optional> #include <qkeysequence.h> #include <qlist.h> #include <set> #include <string> +#include <filesystem> namespace getsuyomi { +constexpr const char* AppVendor = "libfud"; + +constexpr const char* AppName = "getsuyomi"; + +constexpr const char* AppVersionString = "1.0.0"; + +constexpr const char* HOME{"HOME"}; + enum class ActionType { OpenFile, @@ -30,6 +38,27 @@ enum class ActionType MangaPage }; +struct GetsuyomiConfig; + +using GetEnvResult = fud::Result<GetsuyomiConfig, fud::FudStatus>; +using GetConfigResult = fud::Result<GetsuyomiConfig, fud::FudStatus>; +using ShortcutList = QList<QKeySequence>; +using ShortcutSet = std::set<QKeySequence>; +using ShortcutMap = std::map<ActionType, ShortcutSet>; +using ShortcutRevMap = std::map<QKeySequence, ActionType>; + +std::optional<std::string> getEnvVar(const char* envVar); + +void getEnvVar(const std::string& homeVar, const std::string& envVar, std::string& envValue, const char* backup); + +GetEnvResult getEnvironment(); + +fud::FudStatus createXdgDirectory(const std::string& directoryName); + +void setUserConfig(GetsuyomiConfig& config, const ShortcutMap& shortcutMap); + +GetConfigResult getUserConfig(); + constexpr const char* actionTypeToString(ActionType action) { switch (action) { @@ -64,11 +93,6 @@ constexpr const char* actionTypeToString(ActionType action) } } -using ShortcutList = QList<QKeySequence>; -using ShortcutSet = std::set<QKeySequence>; -using ShortcutMap = std::map<ActionType, ShortcutSet>; -using ShortcutRevMap = std::map<QKeySequence, ActionType>; - ShortcutSet shortcutSetFromList(const ShortcutList& shortcutList); ShortcutList shortcutListFromSet(const ShortcutSet& shortcutList); @@ -95,11 +119,15 @@ class Shortcuts { static ShortcutMap fromUserConfig(const std::filesystem::path& configFileName); + fud::FudStatus save(const std::filesystem::path& configFileName) const; + private: Shortcuts() = default; static Shortcuts fromLuaConfig(const std::filesystem::path& configFileName); + // fud::FudStatus saveAsLuaConfig(const std::f + bool m_valid{false}; ShortcutMap m_actionToShortcuts{}; @@ -112,6 +140,7 @@ struct GetsuyomiConfig { std::string dataHome{}; std::string configHome{}; std::string stateHome{}; + std::filesystem::path configFilename{}; ShortcutList openFileShortcuts{}; ShortcutList openDirectoryShortcuts{}; @@ -129,66 +158,6 @@ struct GetsuyomiConfig { ShortcutMap shortcuts() const; }; -class ShortcutDisplay : public QWidget { - Q_OBJECT -public: - ShortcutDisplay(QWidget* parent, QKeySequence shortcut); - -signals: - void removeClicked(QKeySequence shortcut); -private: - QKeySequence m_binding; -private slots: - void removeOnClicked(); -}; - -constexpr auto foo = sizeof(QKeySequence); - -class ShortcutCollector : public QWidget { - Q_OBJECT - public: - ShortcutCollector(QWidget* parent, ActionType action, Shortcuts& shortcuts); - ~ShortcutCollector() = default; - ShortcutCollector(const ShortcutCollector&) = delete; - ShortcutCollector(ShortcutCollector&&) = delete; - ShortcutCollector& operator=(const ShortcutCollector&) = delete; - ShortcutCollector& operator=(ShortcutCollector&&) = delete; - -private: - void createBinding(QKeySequence binding); - - private slots: - void checkBinding(); - void addBinding(); - void removeBinding(QKeySequence shortcut); - - private: - ActionType m_action; - Shortcuts& m_shortcuts; - std::map<QKeySequence, ShortcutDisplay*> m_bindings; - QKeySequenceEdit* m_shortcutEditor{nullptr}; - QPushButton* m_acceptButton{nullptr}; - QVBoxLayout* m_layout{nullptr}; -}; - -class Settings : public QDialog { - Q_OBJECT - public: - Settings(QWidget* parent, Shortcuts&& shortcuts); - Settings(const Settings&) = delete; - Settings(Settings&& rhs) = delete; - ~Settings() = default; - Settings& operator=(const Settings&) = delete; - Settings& operator=(Settings&& rhs) = delete; - - [[nodiscard]] bool valid() const; - - const Shortcuts& shortcuts() const; - - private: - Shortcuts m_shortcuts; -}; - } // namespace getsuyomi #endif diff --git a/src/getsuyomi.cpp b/src/getsuyomi.cpp index 4e0dad7..9f78ba0 100644 --- a/src/getsuyomi.cpp +++ b/src/getsuyomi.cpp @@ -1,7 +1,5 @@ #include "getsuyomi.hpp" -#include "main_window.hpp" - #include <algorithm> namespace getsuyomi { diff --git a/src/luacxx.cpp b/src/luacxx.cpp index c4bc063..0a6db82 100644 --- a/src/luacxx.cpp +++ b/src/luacxx.cpp @@ -32,6 +32,11 @@ LuaContext::LuaContext(LuaContext&& rhs) : m_state{rhs.m_state} LuaContext& LuaContext::operator=(LuaContext&& rhs) { + if (m_state != nullptr) { + lua_close(m_state); + m_state = nullptr; + } + m_state = rhs.m_state; rhs.m_state = nullptr; return *this; @@ -125,20 +130,34 @@ LuaResult<std::vector<std::string>> LuaContext::getGlobalStringArray(const char* std::vector<std::string> output{}; output.reserve(static_cast<size_t>(length)); + + lua_pushnil(m_state); + + auto result = FudStatus::Success; for (int64_t index = 1; index <= length; ++index) { - const char* result{nullptr}; - lua_pushinteger(m_state, index); - static_cast<void>(lua_gettable(m_state, -2)); - result = lua_tolstring(m_state, -1, nullptr); + const char* luaResult{nullptr}; + auto luaNext = lua_next(m_state, -2); + if (luaNext == 0) { + lua_pop(m_state, 2); + result = FudStatus::Failure; + break; + } + luaResult = lua_tolstring(m_state, -1, nullptr); - if (result == nullptr) { + if (luaResult == nullptr) { lua_pop(m_state, 1); - return RetType::error(FudStatus::Failure); + result = FudStatus::Failure; + break; } - output.emplace_back(result); + output.emplace_back(luaResult); + lua_pop(m_state, 1); } lua_pop(m_state, 1); + if (result != FudStatus::Success) + { + return RetType::error(result); + } return RetType::okay(output); } diff --git a/src/main_window.cpp b/src/main_window.cpp index 82e6ef2..52c14ef 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -1,7 +1,7 @@ #include "main_window.hpp" #include "config.hpp" -#include "luacxx.hpp" +#include "settings.hpp" #include <QString> #include <cerrno> @@ -10,94 +10,11 @@ #include <fud_result.hpp> #include <fud_status.hpp> #include <string> -#include <sys/stat.h> #include <vector> namespace getsuyomi { using fud::FudStatus; -using GetEnvResult = fud::Result<GetsuyomiConfig, FudStatus>; -using GetConfigResult = fud::Result<GetsuyomiConfig, FudStatus>; - -constexpr const char* HOME{"HOME"}; - -std::optional<std::string> getEnvVar(const char* envVar); -void getEnvVar(const std::string& homeVar, const std::string& envVar, std::string& envValue, const char* backup); -GetEnvResult getEnvironment(); - -FudStatus createXdgDirectory(const std::string& directoryName) -{ - constexpr mode_t xdgMode = 0700; - auto dirStatus = mkdir(directoryName.c_str(), xdgMode); - if (dirStatus == 0) { - return FudStatus::Success; - } - - if (errno != EEXIST) { - return FudStatus::Failure; - } - struct stat statBuffer {}; - dirStatus = stat(directoryName.c_str(), &statBuffer); - - if (dirStatus != 0) { - return FudStatus::Failure; - } - - if ((statBuffer.st_mode & S_IFMT) != S_IFDIR) { - return FudStatus::Failure; - } - - return FudStatus::Success; -} - -void setUserConfig(GetsuyomiConfig& config, const ShortcutMap& shortcutMap) -{ - auto binder = [&](ShortcutList& shortcuts, ActionType action) { - shortcuts = shortcutListFromSet(shortcutMap.at(action)); - }; - - binder(config.openFileShortcuts, ActionType::OpenFile); - binder(config.openDirectoryShortcuts, ActionType::OpenDirectory); - binder(config.quitShortcuts, ActionType::Quit); - - binder(config.settingsShortcuts, ActionType::Configure); - - binder(config.nextShortcuts, ActionType::Next); - binder(config.backShortcuts, ActionType::Back); - - binder(config.singlePageShortcuts, ActionType::SinglePage); - binder(config.dualPageShortcuts, ActionType::DualPage); - binder(config.mangaPageShortcuts, ActionType::MangaPage); -} - -GetConfigResult getUserConfig() -{ - auto configResult = getEnvironment(); - if (configResult.isError()) { - return GetConfigResult::error(configResult.getError()); - } - auto config = configResult.getOkay(); - - auto dirStatus = createXdgDirectory(config.dataHome); - if (dirStatus != FudStatus::Success) { - return GetConfigResult::error(dirStatus); - } - dirStatus = createXdgDirectory(config.configHome); - if (dirStatus != FudStatus::Success) { - return GetConfigResult::error(dirStatus); - } - dirStatus = createXdgDirectory(config.stateHome); - if (dirStatus != FudStatus::Success) { - return GetConfigResult::error(dirStatus); - } - - auto configFileName = std::filesystem::path(config.configHome).append("config.lua"); - auto shortcutMap = Shortcuts::fromUserConfig(configFileName); - - setUserConfig(config, shortcutMap); - - return GetConfigResult::okay(config); -} GetsuyomiApp::GetsuyomiApp() : QMainWindow(nullptr), m_getsuyomi{new Getsuyomi(this)} { @@ -127,6 +44,9 @@ FudStatus GetsuyomiApp::setup() constexpr int minimumHeight = 480; setMinimumSize(minimumWidth, minimumHeight); + /* TODO: Wire up a proper status bar */ + statusBar()->addWidget(new QLabel("Status bar")); + show(); return FudStatus::Success; @@ -146,6 +66,9 @@ void GetsuyomiApp::createActions() m_settingsAction = new QAction(QIcon("resources/gear.svg"), tr("&Settings"), this); connect(m_settingsAction, &QAction::triggered, this, &GetsuyomiApp::configure); + m_aboutApp = new QAction(QIcon::fromTheme(QIcon::ThemeIcon::HelpAbout), tr("&About"), this); + connect(m_aboutApp, &QAction::triggered, this, &GetsuyomiApp::aboutApp); + m_nextAction = new QAction(QIcon::fromTheme(QIcon::ThemeIcon::GoNext), tr("Next"), this); connect(m_nextAction, &QAction::triggered, this, &GetsuyomiApp::next); @@ -207,8 +130,11 @@ void GetsuyomiApp::createMenus() m_fileMenu->addAction(m_openDirectory); m_fileMenu->addAction(m_quitAction); - m_SettingsMenu = menuBar()->addMenu(tr("&Settings")); - m_SettingsMenu->addAction(m_settingsAction); + m_settingsMenu = menuBar()->addMenu(tr("&Settings")); + m_settingsMenu->addAction(m_settingsAction); + + m_helpMenu = menuBar()->addMenu(tr("&Help")); + m_helpMenu->addAction(m_aboutApp); } void GetsuyomiApp::createToolBar() @@ -305,9 +231,19 @@ void GetsuyomiApp::configure() const auto& shortcuts = settings.shortcuts(); setUserConfig(m_config, shortcuts.shortcutMap()); bindShortcuts(); + auto saveStatus = shortcuts.save(m_config.configFilename); + if (saveStatus != FudStatus::Success) { + qWarning("Failed to save shortcuts."); + } } } +void GetsuyomiApp::aboutApp() +{ + /* TODO: proper implementation */ + QMessageBox::about(this, "About Getsuyomi", "A comic book reading app."); +} + void GetsuyomiApp::next() { m_getsuyomi->next(); @@ -366,58 +302,4 @@ void GetsuyomiApp::writeSettings() settings.sync(); } -std::optional<std::string> getEnvVar(const char* envVar) -{ - const QByteArray defaultArray{}; - if (envVar == nullptr) { - return std::nullopt; - } - QByteArray varArray = qgetenv(envVar); - if (varArray == defaultArray) { - return std::nullopt; - } - return varArray.toStdString(); -} - -void getEnvVar(const std::string& homeVar, const std::string& envVar, std::string& envValue, const char* backup) -{ - auto envValueOpt = getEnvVar(envVar.c_str()); - std::filesystem::path envValuePath{homeVar}; - if (envValueOpt == std::nullopt || envValueOpt->length() == 0) { - envValuePath.append(backup); - } else { - envValuePath = *envValueOpt; - } - envValuePath.append(AppName); - envValue = envValuePath; - // qDebug("%s is %s", envVar.c_str(), envValue.c_str()); -} - -GetEnvResult getEnvironment() -{ - GetsuyomiConfig config{}; - - auto homeOpt = getEnvVar(HOME); - if (homeOpt == std::nullopt || homeOpt->length() == 0) { - qCritical("Error getting home"); - return GetEnvResult::error(FudStatus::Failure); - } - config.home = *homeOpt; - // qDebug("Home is %s", config.home.c_str()); - - /* If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. */ - const std::string XdgDataHome{"XDG_DATA_HOME"}; - getEnvVar(config.home, XdgDataHome, config.dataHome, ".local/share"); - - /* If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used. */ - const std::string XdgConfigHome{"XDG_CONFIG_HOME"}; - getEnvVar(config.home, XdgConfigHome, config.configHome, ".config"); - - /* If $XDG_STATE_HOME is either not set or empty, a default equal to $HOME/.local/state should be used. */ - const std::string XdgStateHome{"XDG_STATE_HOME"}; - getEnvVar(config.home, XdgStateHome, config.stateHome, ".local/state"); - - return GetEnvResult::okay(config); -} - } // namespace getsuyomi diff --git a/src/main_window.hpp b/src/main_window.hpp index e5363a2..9f72f0a 100644 --- a/src/main_window.hpp +++ b/src/main_window.hpp @@ -1,7 +1,6 @@ #ifndef MAIN_WINDOW_HPP #define MAIN_WINDOW_HPP -#include "archive.hpp" #include "getsuyomi.hpp" #include "config.hpp" @@ -10,12 +9,6 @@ namespace getsuyomi { -constexpr const char* AppVendor = "libfud"; - -constexpr const char* AppName = "getsuyomi"; - -constexpr const char* AppVersionString = "1.0.0"; - class GetsuyomiApp : public QMainWindow { Q_OBJECT @@ -47,6 +40,8 @@ class GetsuyomiApp : public QMainWindow { QAction* m_settingsAction{nullptr}; + QAction* m_aboutApp{nullptr}; + QAction* m_nextAction{nullptr}; QAction* m_backAction{nullptr}; @@ -56,8 +51,8 @@ class GetsuyomiApp : public QMainWindow { QActionGroup* m_setPageLayoutGroup{nullptr}; QMenu* m_fileMenu{nullptr}; - QMenu* m_SettingsMenu{nullptr}; - QMenu* m_HelpMenu{nullptr}; + QMenu* m_settingsMenu{nullptr}; + QMenu* m_helpMenu{nullptr}; QToolBar* m_toolBar{nullptr}; QString m_lastOpenedDirectory{}; @@ -69,6 +64,8 @@ class GetsuyomiApp : public QMainWindow { void configure(); + void aboutApp(); + void readSettings(); void writeSettings(); void closeEvent(QCloseEvent* event) override; diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..abd53c1 --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,167 @@ +#include "settings.hpp" + +namespace getsuyomi { + +using fud::FudStatus; + +Settings::Settings(QWidget* parent, Shortcuts&& shortcuts) : QDialog{parent}, m_shortcuts{std::move(shortcuts)} +{ + auto* layout = new QVBoxLayout(); + setWindowTitle("getsuyomi settings"); + + if (!m_shortcuts.valid()) { + return; + } + + auto containerLayout = new QHBoxLayout(); + auto* columnLayout = new QVBoxLayout(); + size_t counter{0}; + constexpr size_t maxEntriesPerColumn{4}; + + for (const auto& action : m_shortcuts.actions()) { + auto* collector = new ShortcutCollector(this, action, m_shortcuts); + columnLayout->addWidget(collector); + counter++; + if (counter % (maxEntriesPerColumn + 1) == 0) { + containerLayout->addLayout(columnLayout); + columnLayout = new QVBoxLayout(); + } + } + + if (columnLayout->count() > 0) { + containerLayout->addLayout(columnLayout); + } + + layout->addLayout(containerLayout); + + auto* dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(dialogButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(dialogButtonBox); + + setLayout(layout); +} + +bool Settings::valid() const +{ + return m_shortcuts.valid(); +} + +const Shortcuts& Settings::shortcuts() const +{ + return m_shortcuts; +} + +ShortcutCollector::ShortcutCollector(QWidget* parent, ActionType action, Shortcuts& shortcuts) : + QWidget(parent), m_action{action}, m_shortcuts{shortcuts}, m_bindings{} +{ + m_layout = new QVBoxLayout(); + auto headerLayout = new QHBoxLayout(); + auto* name = new QLabel(actionTypeToString(m_action)); + headerLayout->addWidget(name); + m_shortcutEditor = new QKeySequenceEdit(this); + m_shortcutEditor->setMaximumSequenceLength(1); + connect(m_shortcutEditor, &QKeySequenceEdit::editingFinished, this, &ShortcutCollector::checkBinding); + headerLayout->addWidget(m_shortcutEditor); + + m_acceptButton = new QPushButton("Accept", this); + m_acceptButton->setEnabled(false); + connect(m_acceptButton, &QPushButton::clicked, this, &ShortcutCollector::addBinding); + headerLayout->addWidget(m_acceptButton); + headerLayout->addStretch(); + + m_layout->addLayout(headerLayout); + + auto shortcutOptions = m_shortcuts.shortcuts(m_action); + if (shortcutOptions != std::nullopt) { + for (const auto& binding : *shortcutOptions) { + createBinding(binding); + } + } else { + qWarning("No shortcuts found for %s", actionTypeToString(action)); + } + + setLayout(m_layout); +} + +void ShortcutCollector::createBinding(QKeySequence binding) +{ + auto displayItem = new ShortcutDisplay(this, binding); + m_bindings[binding] = displayItem; + m_layout->addWidget(displayItem); + connect(displayItem, &ShortcutDisplay::removeClicked, this, &ShortcutCollector::removeBinding); +} + +void ShortcutCollector::checkBinding() +{ + auto keySequence = m_shortcutEditor->keySequence(); + if (keySequence == QKeySequence::UnknownKey) { + m_acceptButton->setEnabled(false); + return; + } + + m_acceptButton->setEnabled(not m_shortcuts.contains(keySequence)); +} + +void ShortcutCollector::addBinding() +{ + auto keySequence = m_shortcutEditor->keySequence(); + if (keySequence == QKeySequence::UnknownKey) { + qWarning("Invalid state - can't accept unknown key"); + return; + } else if (m_shortcuts.contains(keySequence)) { + qWarning("Shortcut %s already bound", qPrintable(keySequence.toString())); + return; + } + auto result = m_shortcuts.bind(m_action, keySequence); + if (result != FudStatus::Success) { + qCritical("Error binding %s to action %s", qPrintable(keySequence.toString()), actionTypeToString(m_action)); + return; + } + if (m_bindings.contains(keySequence)) { + qWarning( + "binding %s to action %s already exists", + qPrintable(keySequence.toString()), + actionTypeToString(m_action)); + m_layout->removeWidget(m_bindings[keySequence]); + delete m_bindings[keySequence]; + } + + createBinding(keySequence); + m_shortcutEditor->clear(); +} + +void ShortcutCollector::removeBinding(QKeySequence binding) +{ + auto result = m_shortcuts.remove(binding); + if (result == FudStatus::NotFound) { + qWarning("binding %s not found", qPrintable(binding.toString())); + } else if (result != FudStatus::Success) { + qWarning("error removing binding %s: %s", qPrintable(binding.toString()), FudStatusToString(result)); + } + auto bindingHandle = m_bindings.extract(binding); + if (bindingHandle) { + m_layout->removeWidget(bindingHandle.mapped()); + delete bindingHandle.mapped(); + } else { + qWarning("Could not remove widget!"); + } + checkBinding(); +} + +ShortcutDisplay::ShortcutDisplay(QWidget* parent, QKeySequence shortcut) : QWidget{parent}, m_binding{shortcut} +{ + auto layout = new QHBoxLayout(); + layout->addWidget(new QLabel(shortcut.toString())); + auto* deleteButton = new QPushButton("Delete", this); + connect(deleteButton, &QPushButton::clicked, this, &ShortcutDisplay::removeOnClicked); + layout->addWidget(deleteButton); + setLayout(layout); +} + +void ShortcutDisplay::removeOnClicked() +{ + emit removeClicked(m_binding); +} + +} diff --git a/src/settings.hpp b/src/settings.hpp new file mode 100644 index 0000000..54e3486 --- /dev/null +++ b/src/settings.hpp @@ -0,0 +1,80 @@ +#ifndef GETSUYOMI_SETTINGS_HPP +#define GETSUYOMI_SETTINGS_HPP + +#include <QtWidgets> +#include <map> +#include <optional> +#include <qkeysequence.h> +#include <qlist.h> +#include <set> +#include <string> +#include <filesystem> + +#include "config.hpp" + +namespace getsuyomi { + +class Settings : public QDialog { + Q_OBJECT + public: + Settings(QWidget* parent, Shortcuts&& shortcuts); + Settings(const Settings&) = delete; + Settings(Settings&& rhs) = delete; + ~Settings() = default; + Settings& operator=(const Settings&) = delete; + Settings& operator=(Settings&& rhs) = delete; + + [[nodiscard]] bool valid() const; + + const Shortcuts& shortcuts() const; + + private: + Shortcuts m_shortcuts; +}; + +class ShortcutDisplay; + +class ShortcutCollector : public QWidget { + Q_OBJECT + public: + ShortcutCollector(QWidget* parent, ActionType action, Shortcuts& shortcuts); + ~ShortcutCollector() = default; + ShortcutCollector(const ShortcutCollector&) = delete; + ShortcutCollector(ShortcutCollector&&) = delete; + ShortcutCollector& operator=(const ShortcutCollector&) = delete; + ShortcutCollector& operator=(ShortcutCollector&&) = delete; + + private: + void createBinding(QKeySequence binding); + + private slots: + void checkBinding(); + void addBinding(); + void removeBinding(QKeySequence shortcut); + + private: + ActionType m_action; + Shortcuts& m_shortcuts; + std::map<QKeySequence, ShortcutDisplay*> m_bindings; + QKeySequenceEdit* m_shortcutEditor{nullptr}; + QPushButton* m_acceptButton{nullptr}; + QVBoxLayout* m_layout{nullptr}; +}; + +class ShortcutDisplay : public QWidget { + Q_OBJECT + public: + ShortcutDisplay(QWidget* parent, QKeySequence shortcut); + + signals: + void removeClicked(QKeySequence shortcut); + + private: + QKeySequence m_binding; + private slots: + void removeOnClicked(); +}; + +} // namespace getsuyomi + +#endif |