#include "config.hpp" #include "luacxx.hpp" #include #include #include #include #include #include namespace getsuyomi { using fud::FudStatus; constexpr auto PermNone = fud::PermissionType::None; constexpr auto defaultPermissions = fud::Permissions(fud::PermReadWrite, PermNone, PermNone); std::optional 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{}; for (const auto& entry : shortcutList) { shortcutSet.insert(entry); } return shortcutSet; } ShortcutList shortcutListFromSet(const ShortcutSet& shortcutSet) { ShortcutList shortcutList{}; for (const auto& entry : shortcutSet) { shortcutList.push_back(entry); } return shortcutList; } Shortcuts::Shortcuts(ShortcutMap&& shortcutMap) : m_actionToShortcuts{std::move(shortcutMap)}, m_shortcuts{}, m_shortcutToAction{} { for (const auto& [action, shortcuts] : m_actionToShortcuts) { for (const auto& shortcut : shortcuts) { auto bindResult = bind(action, shortcut); if (bindResult != FudStatus::Success) { qWarning("Error: %s", FudStatusToString(bindResult)); return; } } } m_valid = true; } bool Shortcuts::contains(QKeySequence keySequence) const { return m_shortcuts.contains(keySequence); } bool Shortcuts::contains(ActionType actionType) const { return m_actionToShortcuts.contains(actionType); } std::vector Shortcuts::actions() const { std::vector actionList{}; actionList.reserve(m_actionToShortcuts.size()); for (const auto& [action, discarded] : m_actionToShortcuts) { actionList.push_back(action); } return actionList; } std::optional Shortcuts::action(QKeySequence keySequence) const { if (m_shortcutToAction.contains(keySequence)) { return m_shortcutToAction.at(keySequence); } else { return std::nullopt; } } std::optional Shortcuts::shortcuts(ActionType action) const { if (contains(action)) { const auto& shortcuts = m_actionToShortcuts.at(action); ShortcutList shortcutList{}; shortcutList.reserve(static_cast(shortcuts.size())); for (const auto& shortcut : shortcuts) { shortcutList.push_back(shortcut); } return shortcutList; } else { qWarning("Action %s not found", actionTypeToString(action)); return std::nullopt; } } FudStatus Shortcuts::bind(ActionType action, QKeySequence keySequence) { if (contains(keySequence)) { return FudStatus::Full; } m_shortcuts.insert(keySequence); if (!contains(action)) { m_actionToShortcuts[action] = ShortcutSet{}; } m_actionToShortcuts[action].insert(keySequence); m_shortcutToAction[keySequence] = action; return FudStatus::Success; } fud::FudStatus Shortcuts::remove(QKeySequence keySequence) { if (!contains(keySequence)) { return FudStatus::NotFound; } auto action = m_shortcutToAction[keySequence]; auto& actionShortcuts = m_actionToShortcuts[action]; actionShortcuts.erase(keySequence); m_shortcutToAction.erase(keySequence); m_shortcuts.erase(keySequence); return FudStatus::Success; } fud::FudStatus Shortcuts::clear(ActionType action) { if (!contains(action)) { return FudStatus::NotFound; } auto& actionShortcuts = m_actionToShortcuts[action]; for (const auto& shortcut : actionShortcuts) { m_shortcuts.erase(shortcut); m_shortcutToAction.erase(shortcut); } m_actionToShortcuts[action] = {}; return FudStatus::Success; } bool Shortcuts::valid() const { return m_valid; } const ShortcutMap& Shortcuts::shortcutMap() const { return m_actionToShortcuts; } Shortcuts Shortcuts::fromLuaConfig(const std::filesystem::path& configFileName) { Shortcuts shortcuts{}; shortcuts.m_valid = true; LuaContext luaContext{}; if (!luaContext.valid()) { qCritical("Failed to create lua context"); return shortcuts; } auto luaStatus = luaContext.loadFile(configFileName.c_str()); if (luaStatus != FudStatus::Success) { qCritical("Failed to load file in lua %s", fud::FudStatusToString(luaStatus)); return shortcuts; } std::vector actions{ ActionType::OpenFile, ActionType::OpenDirectory, ActionType::Quit, ActionType::Configure, // ActionType::Help, // ActionType::About // ActionType::GotoFirst, ActionType::Next, ActionType::Back, // ActionType::GotoLast, ActionType::SinglePage, ActionType::DualPage, ActionType::MangaPage}; for (auto action : actions) { auto result = luaContext.getGlobalStringArray(actionTypeToString(action)); if (result.isError()) { qWarning( "Failed to get variable %s from lua %s", actionTypeToString(action), fud::FudStatusToString(result.getError())); // return RetType::error(result.getError()); continue; } auto actionBindings = result.getOkay(); for (const auto& shortcut : actionBindings) { QKeySequence keySeq{shortcut.c_str()}; if (keySeq == Qt::Key_unknown) { qCritical("Error: shortcut %s unknown", shortcut.c_str()); continue; } auto bindStatus = shortcuts.bind(action, keySeq); if (bindStatus != FudStatus::Success) { qCritical( "Error: failure to bind to action %s keySeq %s", actionTypeToString(action), shortcut.c_str()); continue; } } } return shortcuts; } ShortcutMap Shortcuts::fromUserConfig(const std::filesystem::path& configFileName) { 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()); } } auto shortcuts = Shortcuts::fromLuaConfig(configFileName); auto binder = [&](ActionType action, QKeySequence keySeq) { auto bindDefaultStatus = shortcuts.bind(action, keySeq); if (bindDefaultStatus != FudStatus::Success) { qWarning("%s already bound", actionTypeToString(action)); } }; binder(ActionType::OpenFile, QKeySequence(QKeySequence::Open)); binder(ActionType::OpenDirectory, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_O)); binder(ActionType::Quit, QKeySequence(QKeySequence::Quit)); binder(ActionType::Configure, QKeySequence(Qt::CTRL | Qt::Key_P)); ShortcutList nextDefaults{QKeySequence{Qt::Key_L}, QKeySequence{Qt::Key_Right}, QKeySequence{Qt::Key_Down}}; for (const auto& binding : nextDefaults) { binder(ActionType::Next, binding); } ShortcutList backDefaults{QKeySequence{Qt::Key_H}, QKeySequence{Qt::Key_Left}, QKeySequence{Qt::Key_Up}}; for (const auto& binding : backDefaults) { binder(ActionType::Back, binding); } binder(ActionType::SinglePage, QKeySequence{Qt::Key_S}); binder(ActionType::DualPage, QKeySequence{Qt::Key_D}); binder(ActionType::MangaPage, QKeySequence{Qt::Key_M}); return shortcuts.shortcutMap(); } FudStatus backupFile(const fud::String& filepath) { 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()); } } size_t fileSize; auto fileSizeStatus = file.size(); if (fileSizeStatus.isOkay()) { fileSize = fileSizeStatus.getOkay(); } else { qWarning("Could not get file size"); return fileSizeStatus.getError(); } if (fileSize == 0) { return FudStatus::Empty; } std::vector 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; } 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; } auto writeResult = backupFile.write(backupFileData.data(), backupFileData.size()); if (writeResult.status != FudStatus::Success) { qWarning("Could not write backupfile"); } return writeResult.status; } FudStatus Shortcuts::save(const std::filesystem::path& configFileName) const { fud::String filepath(configFileName.c_str()); static_cast(backupFile(filepath)); 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()); } } 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; } for (auto [action, bindings] : m_actionToShortcuts) { auto appendStatus = output.append(actionTypeToString(action)); if (appendStatus == FudStatus::Success) { appendStatus = output.append(" = {"); } 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; } if (!output.utf8Valid()) { qCritical("Output string is invalid."); return FudStatus::Failure; } } auto writeStatus = file.write(output.data(), output.length()); if (writeStatus.status != FudStatus::Success) { qCritical("Could not save user config file %s.", configFileName.c_str()); } return writeStatus.status; } ShortcutMap GetsuyomiConfig::shortcuts() const { ShortcutMap shortcuts{}; shortcuts.insert({ActionType::OpenFile, shortcutSetFromList(openFileShortcuts)}); shortcuts.insert({ActionType::OpenDirectory, shortcutSetFromList(openDirectoryShortcuts)}); shortcuts.insert({ActionType::Quit, shortcutSetFromList(quitShortcuts)}); shortcuts.insert({ActionType::Configure, shortcutSetFromList(settingsShortcuts)}); shortcuts.insert({ActionType::Next, shortcutSetFromList(nextShortcuts)}); shortcuts.insert({ActionType::Back, shortcutSetFromList(backShortcuts)}); shortcuts.insert({ActionType::SinglePage, shortcutSetFromList(singlePageShortcuts)}); shortcuts.insert({ActionType::DualPage, shortcutSetFromList(dualPageShortcuts)}); shortcuts.insert({ActionType::MangaPage, shortcutSetFromList(mangaPageShortcuts)}); return shortcuts; } } // namespace getsuyomi