add: base project

This commit is contained in:
lily 2025-04-15 22:01:07 -04:00
commit f0cb465c85
Signed by: lily
GPG key ID: 601F3263FBCBC4B9
14 changed files with 1571 additions and 0 deletions

18
src/config.h Normal file
View file

@ -0,0 +1,18 @@
#include <filesystem>
#include <optional>
#include "ini.h"
#include "xdg.h"
class Config {
private:
struct config {
std::optional<std::filesystem::path> configDir;
config() {}
~config() {}
};
public:
Config();
~Config();
};

47
src/dock.cpp Normal file
View file

@ -0,0 +1,47 @@
#include "dock.h"
/// @brief Create dock windows on all screens
/// @param app Qt Application callback
void Dock::createDockWindows(QApplication* app) {
for (int i = 0; i < this->m_screens.size(); ++i) {
QScreen* screen = this->m_screens[i];
qDebug() << "Creating dock window for screen:" << screen->name();
QWidget* container = new QWidget(nullptr, Qt::FramelessWindowHint | Qt::Tool);
container->setAttribute(Qt::WA_TranslucentBackground);
container->setAttribute(Qt::WA_NoSystemBackground);
container->setWindowFlag(Qt::WindowStaysOnTopHint);
container->resize(screen->geometry().size().width() / 2, 60);
QLabel* label = new QLabel(QString("✨ Dock %1 ✨").arg(i));
label->setAlignment(Qt::AlignCenter);
label->setStyleSheet(R"(
QLabel {
color: white;
font-size: 18px;
background-color: rgba(0, 0, 0, 180);
border-radius: 10px;
}
)");
QVBoxLayout* layout = new QVBoxLayout(container);
layout->addWidget(label);
layout->setContentsMargins(0, 0, 0, 0);
QRect screenRect = screen->geometry();
int x = screenRect.center().x() - container->width() / 2;
int y = screenRect.bottom() - container->height();
container->move(x, y);
container->show();
}
app->exec();
}
Dock::Dock(QApplication* app) {
this->createDockWindows(app);
}
Dock::~Dock() {};

125
src/dock.h Normal file
View file

@ -0,0 +1,125 @@
#include <QApplication>
#include <QScreen>
#include <QRect>
#include <QList>
#include <QWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QTimer>
#include <QLabel>
#include <filesystem>
#include <fstream>
#include <vector>
#include <optional>
#include <exception>
#include <iostream>
#include <format>
#include "config.h"
class Dock {
private:
/// @brief Stores all dock windows
QList<QWindow*> m_dockWindows;
/// @brief QWidget container for dock windows
QList<QWidget*> m_dockWindowContainters;
/// @brief Stores all configuration windows
QList<QWindow*> m_configWindows;
/// @brief Stores all available screens
QList<QScreen*> m_screens = QGuiApplication::screens();
/// @brief An application in the dock
struct dock_application {
/// @brief Whether the application is pinned to the dock
bool pinned;
/// @brief Index of where the application is pinned (0 when disabled)
int pinIndex;
/// @brief Whether the application is running
bool running;
/// @brief Path to the application's desktop file entry
std::filesystem::path desktopFilePath;
/// @brief Reference the to the icon file of the application
std::ifstream desktopIcon;
/// @brief Name of the application as specified by the desktop file
std::string desktopName;
/// @brief Name of the running process, used if an application desktop file does not exist
std::string processName;
/// @brief Create a new `dock_application` with an optional desktop file
/// @param filename Optional desktop file for metadata
dock_application() {
getDesktopName();
getDesktopIcon();
}
/// @brief Clean up `dock_application` resources
~dock_application() {
if (desktopIcon.is_open()) {
desktopIcon.close();
}
}
/// @brief Get application desktop name from desktop file
/// @return UTF-16 desktop name
std::optional<std::string> getDesktopName() {
inih::INIReader r;
std::string name;
try {
r = std::move(inih::INIReader(desktopFilePath.string()));
} catch (std::runtime_error&) {
return std::nullopt;
}
try {
name = r.Get<std::string>("Desktop Entry", "name");
} catch (std::runtime_error&) {
return std::nullopt;
}
return name;
}
/// @brief Get application desktop icon from desktop file
/// @return Desktop icon file stream
std::optional<std::ifstream> getDesktopIcon() {
inih::INIReader r;
std::filesystem::path icon_path;
std::ifstream icon{icon_path};
try {
r = std::move(inih::INIReader(desktopFilePath.string()));
} catch (std::runtime_error&) {
return std::nullopt;
}
try {
icon_path = r.Get<std::string>("Desktop Entry", "icon");
} catch (std::runtime_error&) {
return std::nullopt;
}
return icon;
}
};
/// @brief List of all applications in the dock
std::vector<dock_application> m_dockApplications;
/// @brief Create dock windows on all screens
/// @param app Qt Application callback
void createDockWindows(QApplication* app);
public:
Dock(QApplication* app);
~Dock();
};

624
src/ini.h Normal file
View file

@ -0,0 +1,624 @@
/**
* Yet another .ini parser for modern c++ (made for cpp17), inspired and extend
* from @benhoyt's inih. See project page: https://github.com/SSARCandy/ini-cpp
*/
#ifndef __INI_H__
#define __INI_H__
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace inih {
/* Typedef for prototype of handler function. */
typedef int (*ini_handler)(void* user, const char* section, const char* name,
const char* value);
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
#define INI_STOP_ON_FIRST_ERROR 1
#define INI_MAX_LINE 2000
#define INI_INITIAL_ALLOC 200
#define MAX_SECTION 50
#define MAX_NAME 50
#define INI_START_COMMENT_PREFIXES ";#"
#define INI_INLINE_COMMENT_PREFIXES ";"
/* Strip whitespace chars off end of given string, in place. Return s. */
inline static char* rstrip(char* s) {
char* p = s + strlen(s);
while (p > s && isspace((unsigned char)(*--p))) *p = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
inline static char* lskip(const char* s) {
while (*s && isspace((unsigned char)(*s))) s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to null at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
inline static char* find_chars_or_comment(const char* s, const char* chars) {
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
return (char*)s;
}
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
inline static char* strncpy0(char* dest, const char* src, size_t size) {
strncpy(dest, src, size - 1);
dest[size - 1] = '\0';
return dest;
}
/* See documentation in header file. */
inline int ini_parse_stream(ini_reader reader, void* stream,
ini_handler handler, void* user) {
/* Uses a fair bit of stack (use heap instead if you need to) */
char* line;
size_t max_line = INI_INITIAL_ALLOC;
char* new_line;
size_t offset;
char section[MAX_SECTION] = "";
char prev_name[MAX_NAME] = "";
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
line = (char*)malloc(INI_INITIAL_ALLOC);
if (!line) {
return -2;
}
#if INI_HANDLER_LINENO
#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
#else
#define HANDLER(u, s, n, v) handler(u, s, n, v)
#endif
/* Scan through stream line by line */
while (reader(line, (int)max_line, stream) != NULL) {
offset = strlen(line);
while (offset == max_line - 1 && line[offset - 1] != '\n') {
max_line *= 2;
if (max_line > INI_MAX_LINE) max_line = INI_MAX_LINE;
new_line = (char*)realloc(line, max_line);
if (!new_line) {
free(line);
return -2;
}
line = new_line;
if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
break;
if (max_line >= INI_MAX_LINE) break;
offset += strlen(line + offset);
}
lineno++;
start = line;
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
start = lskip(rstrip(start));
if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
/* Start-of-line comment */
} else if (*start == '[') {
/* A "[section]" line */
end = find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
strncpy0(section, start + 1, sizeof(section));
*prev_name = '\0';
} else if (!error) {
/* No ']' found on section line */
error = lineno;
}
} else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = rstrip(start);
value = end + 1;
end = find_chars_or_comment(value, NULL);
if (*end) *end = '\0';
value = lskip(value);
rstrip(value);
/* Valid name[=:]value pair found, call handler */
strncpy0(prev_name, name, sizeof(prev_name));
if (!HANDLER(user, section, name, value) && !error)
error = lineno;
} else if (!error) {
/* No '=' or ':' found on name[=:]value line */
error = lineno;
}
}
if (error) break;
}
free(line);
return error;
}
inline int ini_parse_file(FILE* file, ini_handler handler, void* user) {
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
inline int ini_parse(const char* filename, ini_handler handler, void* user) {
FILE* file;
int error;
file = fopen(filename, "r");
if (!file) return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}
#endif /* __INI_H__ */
#ifndef __INIREADER_H__
#define __INIREADER_H__
// Read an INI file into easy-to-access name/value pairs. (Note that I've gone
// for simplicity here rather than speed, but it should be pretty decent.)
class INIReader {
public:
// Empty Constructor
INIReader(){};
// Construct INIReader and parse given filename. See ini.h for more info
// about the parsing.
INIReader(std::string filename);
// Construct INIReader and parse given file. See ini.h for more info
// about the parsing.
INIReader(FILE* file);
// Return the result of ini_parse(), i.e., 0 on success, line number of
// first error on parse error, or -1 on file open error.
int ParseError() const;
// Return the list of sections found in ini file
const std::set<std::string> Sections() const;
// Return the list of keys in the given section
const std::set<std::string> Keys(std::string section) const;
const std::unordered_map<std::string, std::string> Get(
std::string section) const;
template <typename T = std::string>
T Get(const std::string& section, const std::string& name) const;
template <typename T>
T Get(const std::string& section, const std::string& name,
T&& default_v) const;
template <typename T = std::string>
std::vector<T> GetVector(const std::string& section,
const std::string& name) const;
template <typename T>
std::vector<T> GetVector(const std::string& section,
const std::string& name,
const std::vector<T>& default_v) const;
template <typename T = std::string>
void InsertEntry(const std::string& section, const std::string& name,
const T& v);
template <typename T = std::string>
void InsertEntry(const std::string& section, const std::string& name,
const std::vector<T>& vs);
template <typename T = std::string>
void UpdateEntry(const std::string& section, const std::string& name,
const T& v);
template <typename T = std::string>
void UpdateEntry(const std::string& section, const std::string& name,
const std::vector<T>& vs);
protected:
int _error;
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>
_values;
static int ValueHandler(void* user, const char* section, const char* name,
const char* value);
template <typename T>
T Converter(const std::string& s) const;
const bool BoolConverter(std::string s) const;
template <typename T>
std::string V2String(const T& v) const;
template <typename T>
std::string Vec2String(const std::vector<T>& v) const;
};
#endif // __INIREADER_H__
#ifndef __INIREADER__
#define __INIREADER__
/**
* @brief Construct an INIReader object from a file name
* @param filename The name of the INI file to parse
* @throws std::runtime_error if there is an error parsing the INI file
*/
inline INIReader::INIReader(std::string filename) {
_error = ini_parse(filename.c_str(), ValueHandler, this);
ParseError();
}
/**
* @brief Construct an INIReader object from a file pointer
* @param file A pointer to the INI file to parse
* @throws std::runtime_error if there is an error parsing the INI file
*/
inline INIReader::INIReader(FILE* file) {
_error = ini_parse_file(file, ValueHandler, this);
ParseError();
}
inline int INIReader::ParseError() const {
switch (_error) {
case 0:
break;
case -1:
throw std::runtime_error("ini file not found.");
case -2:
throw std::runtime_error("memory alloc error");
default:
throw std::runtime_error("parse error on line no: " +
std::to_string(_error));
}
return 0;
}
/**
* @brief Return the list of sections found in ini file
* @return The list of sections found in ini file
*/
inline const std::set<std::string> INIReader::Sections() const {
std::set<std::string> retval;
for (auto const& element : _values) {
retval.insert(element.first);
}
return retval;
}
/**
* @brief Return the list of keys in the given section
* @param section The section name
* @return The list of keys in the given section
*/
inline const std::set<std::string> INIReader::Keys(std::string section) const {
auto const _section = Get(section);
std::set<std::string> retval;
for (auto const& element : _section) {
retval.insert(element.first);
}
return retval;
}
/**
* @brief Get the map representing the values in a section of the INI file
* @param section The name of the section to retrieve
* @return The map representing the values in the given section
* @throws std::runtime_error if the section is not found
*/
inline const std::unordered_map<std::string, std::string> INIReader::Get(
std::string section) const {
auto const _section = _values.find(section);
if (_section == _values.end()) {
throw std::runtime_error("section '" + section + "' not found.");
}
return _section->second;
}
/**
* @brief Return the value of the given key in the given section
* @param section The section name
* @param name The key name
* @return The value of the given key in the given section
*/
template <typename T>
inline T INIReader::Get(const std::string& section,
const std::string& name) const {
auto const _section = Get(section);
auto const _value = _section.find(name);
if (_value == _section.end()) {
throw std::runtime_error("key '" + name + "' not found in section '" +
section + "'.");
}
std::string value = _value->second;
if constexpr (std::is_same<T, std::string>()) {
return value;
} else if constexpr (std::is_same<T, bool>()) {
return BoolConverter(value);
} else {
return Converter<T>(value);
};
}
/**
* @brief Return the value of the given key in the given section, return default
* if not found
* @param section The section name
* @param name The key name
* @param default_v The default value
* @return The value of the given key in the given section, return default if
* not found
*/
template <typename T>
inline T INIReader::Get(const std::string& section, const std::string& name,
T&& default_v) const {
try {
return Get<T>(section, name);
} catch (std::runtime_error&) {
return default_v;
}
}
/**
* @brief Return the value array of the given key in the given section.
* @param section The section name
* @param name The key name
* @return The value array of the given key in the given section.
*
* For example:
* ```ini
* [section]
* key = 1 2 3 4
* ```
* ```cpp
* const auto vs = ini.GetVector<std::vector<int>>("section", "key");
* // vs = {1, 2, 3, 4}
* ```
*/
template <typename T>
inline std::vector<T> INIReader::GetVector(const std::string& section,
const std::string& name) const {
std::string value = Get(section, name);
std::istringstream out{value};
const std::vector<std::string> strs{std::istream_iterator<std::string>{out},
std::istream_iterator<std::string>()};
try {
std::vector<T> vs{};
for (const std::string& s : strs) {
vs.emplace_back(Converter<T>(s));
}
return vs;
} catch (std::exception&) {
throw std::runtime_error("cannot parse value " + value +
" to vector<T>.");
}
}
/**
* @brief Return the value array of the given key in the given section, return
* default if not found
* @param section The section name
* @param name The key name
* @param default_v The default value
* @return The value array of the given key in the given section, return default
* if not found
*
* @see INIReader::GetVector
*/
template <typename T>
inline std::vector<T> INIReader::GetVector(
const std::string& section, const std::string& name,
const std::vector<T>& default_v) const {
try {
return GetVector<T>(section, name);
} catch (std::runtime_error&) {
return default_v;
};
}
/**
* @brief Insert a key-value pair into the INI file
* @param section The section name
* @param name The key name
* @param v The value to insert
* @throws std::runtime_error if the key already exists in the section
*/
template <typename T>
inline void INIReader::InsertEntry(const std::string& section,
const std::string& name, const T& v) {
if (_values[section][name].size() > 0) {
throw std::runtime_error("duplicate key '" + std::string(name) +
"' in section '" + section + "'.");
}
_values[section][name] = V2String(v);
}
/**
* @brief Insert a vector of values into the INI file
* @param section The section name
* @param name The key name
* @param vs The vector of values to insert
* @throws std::runtime_error if the key already exists in the section
*/
template <typename T>
inline void INIReader::InsertEntry(const std::string& section,
const std::string& name,
const std::vector<T>& vs) {
if (_values[section][name].size() > 0) {
throw std::runtime_error("duplicate key '" + std::string(name) +
"' in section '" + section + "'.");
}
_values[section][name] = Vec2String(vs);
}
/**
* @brief Update a key-value pair in the INI file
* @param section The section name
* @param name The key name
* @param v The new value to set
* @throws std::runtime_error if the key does not exist in the section
*/
template <typename T>
inline void INIReader::UpdateEntry(const std::string& section,
const std::string& name, const T& v) {
if (!_values[section][name].size()) {
throw std::runtime_error("key '" + std::string(name) +
"' not exist in section '" + section + "'.");
}
_values[section][name] = V2String(v);
}
/**
* @brief Update a vector of values in the INI file
* @param section The section name
* @param name The key name
* @param vs The new vector of values to set
* @throws std::runtime_error if the key does not exist in the section
*/
template <typename T>
inline void INIReader::UpdateEntry(const std::string& section,
const std::string& name,
const std::vector<T>& vs) {
if (!_values[section][name].size()) {
throw std::runtime_error("key '" + std::string(name) +
"' not exist in section '" + section + "'.");
}
_values[section][name] = Vec2String(vs);
}
template <typename T>
inline std::string INIReader::V2String(const T& v) const {
std::stringstream ss;
ss << v;
return ss.str();
}
template <typename T>
inline std::string INIReader::Vec2String(const std::vector<T>& v) const {
if (v.empty()) {
return "";
}
std::ostringstream oss;
std::copy(v.begin(), v.end() - 1, std::ostream_iterator<T>(oss, " "));
oss << v.back();
return oss.str();
}
template <typename T>
inline T INIReader::Converter(const std::string& s) const {
try {
T v{};
std::istringstream _{s};
_.exceptions(std::ios::failbit);
_ >> v;
return v;
} catch (std::exception&) {
throw std::runtime_error("cannot parse value '" + s + "' to type<T>.");
};
}
inline const bool INIReader::BoolConverter(std::string s) const {
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
static const std::unordered_map<std::string, bool> s2b{
{"1", true}, {"true", true}, {"yes", true}, {"on", true},
{"0", false}, {"false", false}, {"no", false}, {"off", false},
};
auto const value = s2b.find(s);
if (value == s2b.end()) {
throw std::runtime_error("'" + s + "' is not a valid boolean value.");
}
return value->second;
}
inline int INIReader::ValueHandler(void* user, const char* section,
const char* name, const char* value) {
INIReader* reader = (INIReader*)user;
if (reader->_values[section][name].size() > 0) {
throw std::runtime_error("duplicate key '" + std::string(name) +
"' in section '" + section + "'.");
}
reader->_values[section][name] = value;
return 1;
}
#endif // __INIREADER__
#ifndef __INIWRITER_H__
#define __INIWRITER_H__
class INIWriter {
public:
INIWriter(){};
/**
* @brief Write the contents of an INI file to a new file
* @param filepath The path of the output file
* @param reader The INIReader object to write to the file
* @throws std::runtime_error if the output file already exists or cannot be
* opened
*/
inline static void write(const std::string& filepath,
const INIReader& reader) {
if (struct stat buf; stat(filepath.c_str(), &buf) == 0) {
throw std::runtime_error("file: " + filepath + " already exist.");
}
std::ofstream out;
out.open(filepath);
if (!out.is_open()) {
throw std::runtime_error("cannot open output file: " + filepath);
}
for (const auto& section : reader.Sections()) {
out << "[" << section << "]\n";
for (const auto& key : reader.Keys(section)) {
out << key << "=" << reader.Get(section, key) << "\n";
}
}
out.close();
}
};
}
#endif /* __INIWRITER_H__ */

74
src/main.cpp Normal file
View file

@ -0,0 +1,74 @@
#include <QApplication>
#include <QLabel>
#include <QCoreApplication>
#include <QDebug>
#include <QDateTime>
#include <QFile>
#include <QTextStream>
#include "dock.h"
void customMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
// static QFile logFile("debug_output.log");
// if (!logFile.isOpen()) {
// logFile.open(QIODevice::Append | QIODevice::Text);
// }
// QTextStream out(&logFile);
QTextStream console(stdout);
QString levelStr;
QString colorStart;
const QString colorEnd = "\033[0m";
switch (type) {
case QtDebugMsg:
levelStr = "DEBUG";
colorStart = "\033[36m"; // Cyan
break;
case QtInfoMsg:
levelStr = "INFO";
colorStart = "\033[32m"; // Green
break;
case QtWarningMsg:
levelStr = "WARNING";
colorStart = "\033[33m"; // Yellow
break;
case QtCriticalMsg:
levelStr = "CRITICAL";
colorStart = "\033[31m"; // Red
break;
case QtFatalMsg:
levelStr = "FATAL";
colorStart = "\033[41m\033[97m"; // Red background, white text
break;
}
QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
// Plain version for log file
QString plainText = QString("[%1] [%2] %3 (%4:%5)")
.arg(time, levelStr, msg, context.file, QString::number(context.line));
// Colored level only for console
QString coloredText = QString("[%1] [%2%3%4] %5 (%6:%7)")
.arg(time, colorStart, levelStr, colorEnd, msg, context.file, QString::number(context.line));
// out << plainText << "\n";
// out.flush();
console << coloredText << "\n";
console.flush();
if (type == QtFatalMsg)
abort();
}
int main(int argc, char *argv[]) {
qInstallMessageHandler(customMessageHandler);
QApplication app(argc, argv);
Dock dock{&app};
return 0;
}

217
src/xdg.h Normal file
View file

@ -0,0 +1,217 @@
/*
* Copyright © 2020 Danilo Spinella <oss@danyspin97.org>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <algorithm>
#include <filesystem>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <unistd.h>
namespace xdg {
class BaseDirectoryException : public std::exception {
public:
explicit BaseDirectoryException(std::string msg) : msg_(std::move(msg)) {}
[[nodiscard]] auto what() const noexcept -> const char * override {
return msg_.c_str();
}
[[nodiscard]] auto msg() const noexcept -> std::string { return msg_; }
private:
const std::string msg_;
};
class BaseDirectories {
public:
BaseDirectories() {
const char *home_env = getenv("HOME");
if (home_env == nullptr) {
throw BaseDirectoryException("$HOME must be set");
}
home_ = std::filesystem::path{home_env};
data_home_ = GetAbsolutePathFromEnvOrDefault(
"XDG_DATA_HOME", home_ / ".local" / "share");
config_home_ = GetAbsolutePathFromEnvOrDefault("XDG_CONFIG_HOME",
home_ / ".config");
data_ = GetPathsFromEnvOrDefault("XDG_DATA_DIRS",
std::vector<std::filesystem::path>{
"/usr/local/share", "/usr/share"});
config_ = GetPathsFromEnvOrDefault(
"XDG_CONFIG_DIRS", std::vector<std::filesystem::path>{"/etc/xdg"});
cache_home_ =
GetAbsolutePathFromEnvOrDefault("XDG_CACHE_HOME", home_ / ".cache");
SetRuntimeDir();
}
static auto GetInstance() -> BaseDirectories & {
static BaseDirectories dirs;
return dirs;
}
[[nodiscard]] auto DataHome() -> const std::filesystem::path & {
return data_home_;
}
[[nodiscard]] auto ConfigHome() -> const std::filesystem::path & {
return config_home_;
}
[[nodiscard]] auto Data() -> const std::vector<std::filesystem::path> & {
return data_;
}
[[nodiscard]] auto Config() -> const std::vector<std::filesystem::path> & {
return config_;
}
[[nodiscard]] auto CacheHome() -> const std::filesystem::path & {
return cache_home_;
}
[[nodiscard]] auto Runtime()
-> const std::optional<std::filesystem::path> & {
return runtime_;
}
private:
void SetRuntimeDir() {
const char *runtime_env = getenv("XDG_RUNTIME_DIR");
if (runtime_env != nullptr) {
std::filesystem::path runtime_dir{runtime_env};
if (runtime_dir.is_absolute()) {
if (!std::filesystem::exists(runtime_dir)) {
throw BaseDirectoryException(
"$XDG_RUNTIME_DIR must exist on the system");
}
auto runtime_dir_perms =
std::filesystem::status(runtime_dir).permissions();
using perms = std::filesystem::perms;
// Check XDG_RUNTIME_DIR permissions are 0700
if (((runtime_dir_perms & perms::owner_all) == perms::none) ||
((runtime_dir_perms & perms::group_all) != perms::none) ||
((runtime_dir_perms & perms::others_all) != perms::none)) {
throw BaseDirectoryException(
"$XDG_RUNTIME_DIR must have 0700 as permissions");
}
runtime_.emplace(runtime_dir);
}
}
}
static auto
GetAbsolutePathFromEnvOrDefault(const char *env_name,
std::filesystem::path &&default_path)
-> std::filesystem::path {
const char *env_var = getenv(env_name);
if (env_var == nullptr) {
return std::move(default_path);
}
auto path = std::filesystem::path{env_var};
if (!path.is_absolute()) {
return std::move(default_path);
}
return path;
}
static auto
GetPathsFromEnvOrDefault(const char *env_name,
std::vector<std::filesystem::path> &&default_paths)
-> std::vector<std::filesystem::path> {
auto *env = getenv(env_name);
if (env == nullptr) {
return std::move(default_paths);
}
std::string paths{env};
std::vector<std::filesystem::path> dirs{};
size_t start = 0;
size_t pos = 0;
while ((pos = paths.find_first_of(':', start)) != std::string::npos) {
std::filesystem::path current_path{
paths.substr(start, pos - start)};
if (current_path.is_absolute() &&
!VectorContainsPath(dirs, current_path)) {
dirs.emplace_back(current_path);
}
start = pos + 1;
}
std::filesystem::path current_path{paths.substr(start, pos - start)};
if (current_path.is_absolute() &&
!VectorContainsPath(dirs, current_path)) {
dirs.emplace_back(current_path);
}
if (dirs.empty()) {
return std::move(default_paths);
}
return dirs;
}
static auto
VectorContainsPath(const std::vector<std::filesystem::path> &paths,
const std::filesystem::path &path) -> bool {
return std::find(std::begin(paths), std::end(paths), path) !=
paths.end();
}
std::filesystem::path home_;
std::filesystem::path data_home_;
std::filesystem::path config_home_;
std::vector<std::filesystem::path> data_;
std::vector<std::filesystem::path> config_;
std::filesystem::path cache_home_;
std::optional<std::filesystem::path> runtime_;
}; // namespace xdg
[[nodiscard]] inline auto DataHomeDir() -> const std::filesystem::path & {
return BaseDirectories::GetInstance().DataHome();
}
[[nodiscard]] inline auto ConfigHomeDir() -> const std::filesystem::path & {
return BaseDirectories::GetInstance().ConfigHome();
}
[[nodiscard]] inline auto DataDirs()
-> const std::vector<std::filesystem::path> & {
return BaseDirectories::GetInstance().Data();
}
[[nodiscard]] inline auto ConfigDirs()
-> const std::vector<std::filesystem::path> & {
return BaseDirectories::GetInstance().Config();
}
[[nodiscard]] inline auto CacheHomeDir() -> const std::filesystem::path & {
return BaseDirectories::GetInstance().CacheHome();
}
[[nodiscard]] inline auto RuntimeDir()
-> const std::optional<std::filesystem::path> & {
return BaseDirectories::GetInstance().Runtime();
}
} // namespace xdg