Compare commits

...

No commits in common. "main" and "windows" have entirely different histories.

56 changed files with 271 additions and 13016 deletions

View file

@ -1,47 +1,25 @@
BasedOnStyle: Chromium
AlignAfterOpenBracket: BlockIndent
AlignArrayOfStructures: Right
AlignConsecutiveAssignments:
Enabled: true
AlignConsecutiveDeclarations:
Enabled: true
PadOperators: true
AlignConsecutiveMacros:
Enabled: true
AlignConsecutiveShortCaseStatements:
Enabled: true
AlignOperands: DontAlign
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Empty
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLoopsOnASingleLine: true
BasedOnStyle: Chromium
BinPackArguments: false
BreakBeforeBraces: Attach
ColumnLimit: 0
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
Cpp11BracedListStyle: false
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<.*>$'
Priority: 1
- Regex: '^"Config/.*"'
Priority: 2
- Regex: '^"Core/.*"'
Priority: 3
- Regex: '^"Services/.*"'
Priority: 4
- Regex: '^"UI/.*"'
Priority: 5
- Regex: '^"Util/.*"'
Priority: 6
- Regex: '^"Wrappers/.*"'
Priority: 7
- Regex: '^".*"$'
Priority: 8
BinPackParameters: false
IndentAccessModifiers: false
IndentExternBlock: Indent
IndentPPDirectives: BeforeHash
LineEnding: LF
NamespaceIndentation: All
QualifierAlignment: Left
SpaceBeforeCpp11BracedList: true
SpacesBeforeTrailingComments: 1
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '".*"'
Priority: 1
- Regex: '<.*>'
Priority: -1

View file

@ -1,52 +1,42 @@
Checks: >
*,
-ctad-maybe-unsupported,
-abseil-*,
-altera-*,
-boost-*,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-cert-env33-c,
-concurrency-mt-unsafe,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-pro-type-vararg,
-fuchsia-*,
-google-*,
-hicpp-*,
-llvm-include-order,
-llvm-include-order,
-llvm-namespace-comment,
-llvmlibc-*,
-misc-non-private-member-variables-in-classes,
-readability-avoid-nested-conditional-operator,
-modernize-use-trailing-return-type,
-readability-braces-around-statements,
-readability-function-cognitive-complexity,
-readability-implicit-bool-conversion,
-readability-isolate-declaration,
-readability-magic-numbers
CheckOptions:
cppcoreguidelines-avoid-do-while.IgnoreMacros: true
readability-else-after-return.WarnOnUnfixable: false
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.LocalConstantCase: camelBack
readability-identifier-naming.LocalVariableCase: camelBack
readability-identifier-naming.GlobalFunctionCase: CamelCase
readability-identifier-naming.MemberCase: camelBack
readability-identifier-naming.MemberCase: lower_case
readability-identifier-naming.MethodCase: camelBack
readability-identifier-naming.MethodIgnoredRegexp: ((to|from)_class)
readability-identifier-naming.ParameterPackCase: lower_case
readability-identifier-naming.PrivateMemberCase: camelBack
readability-identifier-naming.PrivateMemberCase: CamelCase
readability-identifier-naming.PrivateMemberPrefix: 'm_'
readability-identifier-naming.PrivateMethodCase: camelBack
readability-identifier-naming.PrivateMethodCase: CamelCase
readability-identifier-naming.PrivateMethodPrefix: ''
readability-identifier-naming.ProtectedMemberPrefix: 'm_'
readability-identifier-naming.ProtectedMethodPrefix: ''
readability-identifier-naming.PublicMemberCase: camelBack
readability-identifier-naming.PublicMemberCase: lower_case
readability-identifier-naming.StaticConstantCase: UPPER_CASE
readability-identifier-naming.StaticVariableCase: CamelCase
readability-identifier-naming.StructCase: CamelCase

1
.envrc
View file

@ -1 +0,0 @@
use_flake

1
.gitattributes vendored
View file

@ -1 +0,0 @@
* text eol=lf

28
.gitignore vendored
View file

@ -1,28 +1,2 @@
.cache/
.cmake/
.direnv/
.idea/
.vs/
.vscode/
.xmake/
.DS_Store
bin/
build/
cmake-build-debug/
CMakeFiles/
out/
Testing/
build.ninja
CMakeCache.txt
cmake_install.cmake
CMakeSettings.json
compile_commands.json
config.toml
draconis++
Makefile
result
subprojects/*
!subprojects/*.wrap
subprojects/sqlite3.wrap
cmake-build-*/

6
.gitmodules vendored
View file

@ -1,6 +0,0 @@
[submodule "subprojects/glaze"]
path = subprojects/glaze
url = https://github.com/stephenberry/glaze
[submodule "subprojects/dbus_cxx"]
path = subprojects/dbus_cxx
url = https://github.com/dbus-cxx/dbus-cxx

15
CMakeLists.txt Normal file
View file

@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.28)
project(draconis__)
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_MAKE_PROGRAM "Ninja")
add_executable(draconis__ main.cpp
util.h)
find_package(fmt CONFIG REQUIRED)
find_package(cppwinrt CONFIG REQUIRED)
target_link_libraries(draconis__ PRIVATE fmt::fmt-header-only)
target_link_libraries(draconis__ PRIVATE WindowsApp)

96
flake.lock generated
View file

@ -1,96 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1746576598,
"narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1745377448,
"narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1746216483,
"narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "29ec5026372e0dec56f890e50dbe4f45930320fd",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

288
flake.nix
View file

@ -1,288 +0,0 @@
{
description = "C/C++ environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
treefmt-nix.url = "github:numtide/treefmt-nix";
utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
treefmt-nix,
utils,
...
}:
utils.lib.eachDefaultSystem (
system:
if system == "x86_64-linux"
then let
pkgs = import nixpkgs {inherit system;};
muslPkgs = import nixpkgs {
system = "x86_64-linux-musl";
overlays = [
(self: super: {
mimalloc = super.mimalloc.overrideAttrs (oldAttrs: {
cmakeFlags =
(oldAttrs.cmakeFlags or [])
++ [(self.lib.cmakeBool "MI_LIBC_MUSL" true)];
postPatch = ''
sed -i '\|<linux/prctl.h>|s|^|// |' src/prim/unix/prim.c
'';
});
})
];
};
llvmPackages = muslPkgs.llvmPackages_20;
stdenv =
muslPkgs.stdenvAdapters.useMoldLinker
llvmPackages.libcxxStdenv;
glaze = (muslPkgs.glaze.override {inherit stdenv;}).overrideAttrs (oldAttrs: {
cmakeFlags =
(oldAttrs.cmakeFlags or [])
++ [
"-Dglaze_DEVELOPER_MODE=OFF"
"-Dglaze_BUILD_EXAMPLES=OFF"
];
doCheck = false;
enableAvx2 = stdenv.hostPlatform.isx86;
});
mkOverridden = buildSystem: pkg: ((pkg.override {inherit stdenv;}).overrideAttrs (oldAttrs: {
"${buildSystem}Flags" =
(oldAttrs."${buildSystem}Flags" or [])
++ (
if buildSystem == "meson"
then ["-Ddefault_library=static"]
else if buildSystem == "cmake"
then [
"-D${pkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF"
"-D${pkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF"
"-DBUILD_SHARED_LIBS=OFF"
]
else throw "Invalid build system: ${buildSystem}"
);
}));
deps = with pkgs.pkgsStatic; [
curlMinimal
dbus
glaze
llvmPackages.libcxx
openssl
sqlite
wayland
xorg.libXau
xorg.libXdmcp
xorg.libxcb
(mkOverridden "cmake" ftxui)
(mkOverridden "cmake" pugixml)
(mkOverridden "cmake" sqlitecpp)
(mkOverridden "meson" tomlplusplus)
];
in {
packages = rec {
draconisplusplus = stdenv.mkDerivation {
name = "draconis++";
version = "0.1.0";
src = self;
nativeBuildInputs = with muslPkgs; [
cmake
meson
ninja
pkg-config
];
buildInputs = deps;
configurePhase = ''
meson setup build --buildtype release
'';
buildPhase = ''
meson compile -C build
'';
installPhase = ''
mkdir -p $out/bin
mv build/draconis++ $out/bin/draconis++
'';
NIX_ENFORCE_NO_NATIVE = 0;
meta.staticExecutable = true;
};
draconisplusplus-generic = draconisplusplus.overrideAttrs {NIX_ENFORCE_NO_NATIVE = 1;};
default = draconisplusplus;
};
devShell = muslPkgs.mkShell.override {inherit stdenv;} {
packages =
(with pkgs; [
bear
cachix
cmake
])
++ (with muslPkgs; [
llvmPackages_20.clang-tools
meson
ninja
pkg-config
(pkgs.writeScriptBin "build" "meson compile -C build")
(pkgs.writeScriptBin "clean" "meson setup build --wipe")
(pkgs.writeScriptBin "run" "meson compile -C build && build/draconis++")
])
++ deps;
NIX_ENFORCE_NO_NATIVE = 0;
};
formatter = treefmt-nix.lib.mkWrapper pkgs {
projectRootFile = "flake.nix";
programs = {
alejandra.enable = true;
deadnix.enable = true;
clang-format = {
enable = true;
package = pkgs.llvmPackages.clang-tools;
};
};
};
}
else let
pkgs = import nixpkgs {inherit system;};
llvmPackages = pkgs.llvmPackages_20;
stdenv = with pkgs;
(
if hostPlatform.isLinux
then stdenvAdapters.useMoldLinker
else lib.id
)
llvmPackages.libcxxStdenv;
deps = with pkgs;
[
(glaze.override {enableAvx2 = hostPlatform.isx86;})
]
++ (with pkgsStatic; [
curl
ftxui
sqlitecpp
(tomlplusplus.overrideAttrs {
doCheck = false;
})
])
++ darwinPkgs
++ linuxPkgs;
darwinPkgs = nixpkgs.lib.optionals stdenv.isDarwin (with pkgs.pkgsStatic; [
libiconv
apple-sdk_15
]);
linuxPkgs = nixpkgs.lib.optionals stdenv.isLinux (with pkgs;
[
valgrind
]
++ (with pkgsStatic; [
dbus
pugixml
xorg.libxcb
wayland
]));
in
with pkgs; {
packages = rec {
draconisplusplus = stdenv.mkDerivation {
name = "draconis++";
version = "0.1.0";
src = self;
nativeBuildInputs = [
cmake
meson
ninja
pkg-config
];
buildInputs = deps;
configurePhase = ''
meson setup build --buildtype release
'';
buildPhase = ''
meson compile -C build
'';
installPhase = ''
mkdir -p $out/bin
mv build/draconis++ $out/bin/draconis++
'';
};
default = draconisplusplus;
};
formatter = treefmt-nix.lib.mkWrapper pkgs {
projectRootFile = "flake.nix";
programs = {
alejandra.enable = true;
deadnix.enable = true;
clang-format = {
enable = true;
package = pkgs.llvmPackages.clang-tools;
};
};
};
devShell = mkShell.override {inherit stdenv;} {
packages =
[
alejandra
bear
llvmPackages.clang-tools
cmake
include-what-you-use
lldb
hyperfine
meson
ninja
nvfetcher
pkg-config
unzip
(writeScriptBin "build" "meson compile -C build")
(writeScriptBin "clean" "meson setup build --wipe")
(writeScriptBin "run" "meson compile -C build && build/draconis++")
]
++ deps;
LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}";
NIX_ENFORCE_NO_NATIVE = 0;
shellHook = lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ''
export SDKROOT=${pkgs.pkgsStatic.apple-sdk_15}/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
export DEVELOPER_DIR=${pkgs.pkgsStatic.apple-sdk_15}
export NIX_CFLAGS_COMPILE="-isysroot $SDKROOT"
export NIX_CXXFLAGS_COMPILE="-isysroot $SDKROOT"
export NIX_OBJCFLAGS_COMPILE="-isysroot $SDKROOT"
export NIX_OBJCXXFLAGS_COMPILE="-isysroot $SDKROOT"
'';
};
}
);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

199
main.cpp Normal file
View file

@ -0,0 +1,199 @@
#include <fmt/chrono.h>
#include <fmt/core.h>
#include <iostream>
#include <windows.h>
#include <winrt/Windows.Foundation.h> // ReSharper disable once CppUnusedIncludeDirective
#include <winrt/Windows.Media.Control.h>
#include <winrt/base.h>
#include <winrt/impl/windows.media.control.2.h>
#include "util.h"
using namespace winrt;
using namespace winrt::Windows::Media::Control;
struct KBToGiB {
u64 value;
};
template <>
struct fmt::formatter<KBToGiB> : formatter<double> {
template <typename FormatContext>
typename FormatContext::iterator
format(const KBToGiB KTG, FormatContext& ctx) {
typename FormatContext::iterator out = formatter<double>::format(
static_cast<double>(KTG.value) / pow(1024, 2), ctx
);
*out++ = 'G';
*out++ = 'i';
*out++ = 'B';
return out;
}
};
enum DateNum : u8 { Ones, Twos, Threes, Default };
DateNum ParseDate(std::string const& input) {
if (input == "1" || input == "21" || input == "31") return Ones;
if (input == "2" || input == "22") return Twos;
if (input == "3" || input == "23") return Threes;
return Default;
}
std::string GetNowPlaying() {
using namespace winrt::Windows::Media::Control;
using namespace winrt::Windows::Foundation;
using MediaProperties =
GlobalSystemMediaTransportControlsSessionMediaProperties;
using Session = GlobalSystemMediaTransportControlsSession;
using SessionManager = GlobalSystemMediaTransportControlsSessionManager;
try {
// Request the session manager asynchronously
const IAsyncOperation<SessionManager> sessionManagerOp =
SessionManager::RequestAsync();
const SessionManager sessionManager = sessionManagerOp.get();
if (const Session currentSession = sessionManager.GetCurrentSession()) {
// Try to get the media properties asynchronously
const IAsyncOperation<MediaProperties> mediaPropertiesOp =
currentSession.TryGetMediaPropertiesAsync();
const MediaProperties mediaProperties = mediaPropertiesOp.get();
// Convert the hstring title to std::string
return to_string(mediaProperties.Title());
}
// If we reach this point, there is no current session
return "No current media session.";
} catch (...) { return "Failed to get media properties."; }
}
std::string GetRegistryValue(
const HKEY& hKey,
const std::string& subKey,
const std::string& valueName
) {
HKEY key = nullptr;
if (RegOpenKeyExA(hKey, subKey.c_str(), 0, KEY_READ, &key) != ERROR_SUCCESS)
return "";
DWORD dataSize = 0;
if (RegQueryValueExA(
key, valueName.c_str(), nullptr, nullptr, nullptr, &dataSize
) != ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
std::string value(dataSize, '\0');
if (RegQueryValueExA(
key,
valueName.c_str(),
nullptr,
nullptr,
reinterpret_cast<LPBYTE>(value.data()), // NOLINT(*-reinterpret-cast)
&dataSize
) != ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
RegCloseKey(key);
// Remove null terminator if present
if (!value.empty() && value.back() == '\0') value.pop_back();
return value;
}
std::string GetPrettyWindowsName() {
std::string productName = GetRegistryValue(
HKEY_LOCAL_MACHINE,
R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
"ProductName"
);
const std::string displayVersion = GetRegistryValue(
HKEY_LOCAL_MACHINE,
R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
"DisplayVersion"
);
const std::string releaseId = GetRegistryValue(
HKEY_LOCAL_MACHINE,
R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
"ReleaseId"
);
const int buildNumber = stoi(GetRegistryValue(
HKEY_LOCAL_MACHINE,
R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
"CurrentBuildNumber"
));
fmt::println("Build number: {}", buildNumber);
// Check if the build number is 22000 or higher
if (buildNumber >= 22000 &&
productName.find("Windows 10") != std::string::npos)
productName.replace(productName.find("Windows 10"), 10, "Windows 11");
if (!productName.empty()) {
std::string result = productName;
if (!displayVersion.empty())
result += " " + displayVersion;
else if (!releaseId.empty())
result += " " + releaseId;
return result;
}
return "";
}
int main() {
init_apartment();
u64 memInfo = 0;
GetPhysicallyInstalledSystemMemory(&memInfo);
fmt::println("Installed RAM: {:.2f}", KBToGiB {memInfo});
fmt::println("Now playing: {}", GetNowPlaying());
const std::tm localTime = fmt::localtime(time(nullptr));
std::string date = fmt::format("{:%e}", localTime);
auto start = date.begin();
while (start != date.end() && std::isspace(*start)) ++start;
date.erase(date.begin(), start);
switch (ParseDate(date)) {
case Ones:
date += "st";
break;
case Twos:
date += "nd";
break;
case Threes:
date += "rd";
break;
case Default:
date += "th";
break;
}
fmt::println("{:%B} {}, {:%-I:%0M %p}", localTime, date, localTime);
fmt::println("Version: {}", GetPrettyWindowsName());
uninit_apartment();
return 0;
}

View file

@ -1,234 +0,0 @@
# ----------------------- #
# Project Configuration #
# ----------------------- #
project(
'draconis++',
'cpp',
version: '0.1.0',
default_options: [
'default_library=static',
'buildtype=debugoptimized',
'b_vscrt=mt',
'b_lto=true',
'b_ndebug=if-release',
'warning_level=3',
],
)
add_project_arguments(
'-DDRACONISPLUSPLUS_VERSION="' + meson.project_version() + '"',
language: ['cpp', 'objcpp'],
)
cpp = meson.get_compiler('cpp')
host_system = host_machine.system()
# ------------------------ #
# Compiler Configuration #
# ------------------------ #
common_warning_flags = [
'-Wno-c++20-compat',
'-Wno-c++20-extensions',
'-Wno-c++98-compat',
'-Wno-c++98-compat-pedantic',
'-Wno-disabled-macro-expansion',
'-Wno-missing-prototypes',
'-Wno-padded',
'-Wno-pre-c++20-compat-pedantic',
'-Wno-unused-command-line-argument',
'-Wunused-function',
]
common_cpp_flags = {
'common': [
'-fno-strict-enums',
'-fvisibility=hidden',
'-fvisibility-inlines-hidden',
'-std=c++26',
],
'msvc': [
'-DNOMINMAX', '/MT',
'/Zc:__cplusplus',
'/Zc:preprocessor',
'/external:W0',
'/external:anglebrackets',
'/std:c++latest',
],
'unix_extra': '-march=native',
'windows_extra': '-DCURL_STATICLIB',
}
# Configure Objective-C++ for macOS
if host_system == 'darwin'
add_languages('objcpp', native: false)
objcpp = meson.get_compiler('objcpp')
objcpp_flags = common_warning_flags + [
'-std=c++26',
'-fvisibility=hidden',
'-fvisibility-inlines-hidden',
]
add_project_arguments(objcpp.get_supported_arguments(objcpp_flags), language: 'objcpp')
endif
# Apply C++ compiler arguments
if cpp.get_id() in ['msvc', 'clang-cl']
common_cpp_args = common_cpp_flags['msvc']
if cpp.get_id() == 'clang-cl'
common_cpp_args += common_warning_flags + common_cpp_flags['common'] + ['-fcolor-diagnostics', '-fdiagnostics-format=clang']
endif
else
common_cpp_args = common_warning_flags + common_cpp_flags['common']
if host_system == 'windows'
common_cpp_args += common_cpp_flags['windows_extra']
elif host_system != 'serenity'
common_cpp_args += common_cpp_flags['unix_extra']
endif
endif
add_project_arguments(common_cpp_args, language: 'cpp')
# --------------------- #
# Include Directories #
# --------------------- #
project_internal_includes = include_directories('src')
project_public_includes = include_directories('include', is_system: true)
# ------- #
# Files #
# ------- #
base_sources = files(
'src/Config/Config.cpp',
'src/Core/SystemData.cpp',
'src/Services/PackageCounting.cpp',
'src/Services/Weather/MetNoService.cpp',
'src/Services/Weather/OpenMeteoService.cpp',
'src/Services/Weather/OpenWeatherMapService.cpp',
'src/UI/UI.cpp',
'src/main.cpp',
)
platform_sources = {
'darwin': ['src/OS/macOS.cpp', 'src/OS/macOS/bridge.mm'],
'dragonfly': ['src/OS/BSD.cpp'],
'freebsd': ['src/OS/BSD.cpp'],
'haiku': ['src/OS/Haiku.cpp'],
'linux': ['src/OS/Linux.cpp'],
'netbsd': ['src/OS/BSD.cpp'],
'serenity': ['src/OS/Serenity.cpp'],
'windows': ['src/OS/Windows.cpp'],
}
sources = base_sources + files(platform_sources.get(host_system, []))
# --------------------- #
# Dependencies Config #
# --------------------- #
common_deps = [
dependency('libcurl', include_type: 'system', static: true),
dependency('tomlplusplus', include_type: 'system', static: true),
dependency('openssl', include_type: 'system', static: true, required: false),
]
# Platform-specific dependencies
platform_deps = []
if host_system == 'darwin'
platform_deps += [
dependency('SQLiteCpp'),
dependency(
'appleframeworks',
modules: ['foundation', 'mediaplayer', 'systemconfiguration'],
static: true,
),
dependency('iconv'),
]
elif host_system == 'windows'
platform_deps += [
cpp.find_library('dwmapi'),
cpp.find_library('windowsapp'),
]
elif host_system != 'serenity' and host_system != 'haiku'
# Make dbus, x11, and wayland dependencies optional
dbus_dep = dependency('dbus-1', required: false)
xcb_dep = dependency('xcb', required: false)
xau_dep = dependency('xau', required: false)
xdmcp_dep = dependency('xdmcp', required: false)
wayland_dep = dependency('wayland-client', required: false)
platform_deps += dependency('SQLiteCpp')
if host_system == 'linux'
platform_deps += dependency('pugixml')
endif
if dbus_dep.found()
platform_deps += dbus_dep
add_project_arguments('-DHAVE_DBUS', language: 'cpp')
endif
if xcb_dep.found() and xau_dep.found() and xdmcp_dep.found()
platform_deps += [xcb_dep, xau_dep, xdmcp_dep]
add_project_arguments('-DHAVE_XCB', language: 'cpp')
endif
if wayland_dep.found()
platform_deps += wayland_dep
add_project_arguments('-DHAVE_WAYLAND', language: 'cpp')
endif
endif
# FTXUI configuration
ftxui_components = ['ftxui::screen', 'ftxui::dom', 'ftxui::component']
ftxui_dep = dependency(
'ftxui',
modules: ftxui_components,
include_type: 'system',
static: true,
required: false,
)
if not ftxui_dep.found()
ftxui_dep = declare_dependency(
dependencies: [
dependency('ftxui-dom', fallback: ['ftxui', 'dom_dep']),
dependency('ftxui-screen', fallback: ['ftxui', 'screen_dep']),
dependency('ftxui-component', fallback: ['ftxui', 'component_dep']),
],
)
endif
glaze_dep = dependency('glaze', include_type: 'system', required: false)
if not glaze_dep.found()
cmake = import('cmake')
glaze_proj = cmake.subproject('glaze')
glaze_dep = glaze_proj.dependency('glaze_glaze', include_type: 'system')
endif
# Combine all dependencies
deps = common_deps + platform_deps + ftxui_dep + glaze_dep
# ------------------------- #
# Link/ObjC Configuration #
# ------------------------- #
link_args = []
objc_args = []
if host_system == 'darwin'
objc_args += ['-fobjc-arc']
elif host_system == 'linux'
link_args += ['-static']
elif host_system == 'haiku'
link_args += ['-lpackage', '-lbe']
endif
# ------------------- #
# Executable Target #
# ------------------- #
executable(
'draconis++',
sources,
include_directories: [project_internal_includes, project_public_includes],
objc_args: objc_args,
link_args: link_args,
dependencies: deps,
install: true,
)

View file

@ -1,183 +0,0 @@
#include "Config.hpp"
#include <filesystem> // std::filesystem::{path, operator/, exists, create_directories}
#include <format> // std::{format, format_error}
#include <fstream> // std::{ifstream, ofstream, operator<<}
#include <system_error> // std::error_code
#include <toml++/impl/node_view.hpp> // toml::node_view
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result}
#include <toml++/impl/table.hpp> // toml::table
#include "Util/Definitions.hpp"
#include "Util/Env.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
namespace fs = std::filesystem;
namespace {
using util::types::Vec, util::types::CStr, util::types::Exception;
constexpr const char* defaultConfigTemplate = R"cfg(# Draconis++ Configuration File
# General settings
[general]
name = "{}" # Your display name
# Now Playing integration
[now_playing]
enabled = false # Set to true to enable media integration
# Weather settings
[weather]
enabled = false # Set to true to enable weather display
show_town_name = false # Show location name in weather display
api_key = "" # Your weather API key
units = "metric" # Use "metric" for °C or "imperial" for °F
location = "London" # Your city name
# Alternatively, you can specify coordinates instead of a city name:
# [weather.location]
# lat = 51.5074
# lon = -0.1278
)cfg";
fn GetConfigPath() -> fs::path {
using util::helpers::GetEnv;
Vec<fs::path> possiblePaths;
#ifdef _WIN32
if (Result<String> result = GetEnv("LOCALAPPDATA"))
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
if (Result<String> result = GetEnv("USERPROFILE")) {
possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml");
possiblePaths.emplace_back(fs::path(*result) / "AppData" / "Local" / "draconis++" / "config.toml");
}
if (Result<String> result = GetEnv("APPDATA"))
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
#else
if (Result<String> result = GetEnv("XDG_CONFIG_HOME"))
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
if (Result<String> result = GetEnv("HOME")) {
possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml");
possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml");
}
#endif
possiblePaths.emplace_back(fs::path(".") / "config.toml");
for (const fs::path& path : possiblePaths)
if (std::error_code errc; fs::exists(path, errc) && !errc)
return path;
if (!possiblePaths.empty()) {
const fs::path defaultDir = possiblePaths[0].parent_path();
if (std::error_code errc; !fs::exists(defaultDir, errc) || !errc) {
create_directories(defaultDir, errc);
if (errc)
warn_log("Warning: Failed to create config directory: {}", errc.message());
}
return possiblePaths[0];
}
warn_log("Could not determine a preferred config path. Falling back to './config.toml'");
return fs::path(".") / "config.toml";
}
fn CreateDefaultConfig(const fs::path& configPath) -> bool {
try {
std::error_code errc;
create_directories(configPath.parent_path(), errc);
if (errc) {
error_log("Failed to create config directory: {}", errc.message());
return false;
}
std::ofstream file(configPath);
if (!file) {
error_log("Failed to open config file for writing: {}", configPath.string());
return false;
}
try {
const String defaultName = General::getDefaultName();
const String formattedConfig = std::vformat(defaultConfigTemplate, std::make_format_args(defaultName));
file << formattedConfig;
} catch (const std::format_error& fmtErr) {
error_log("Failed to format default config string: {}", fmtErr.what());
return false;
}
if (!file) {
error_log("Failed to write to config file: {}", configPath.string());
return false;
}
info_log("Created default config file at {}", configPath.string());
return true;
} catch (const fs::filesystem_error& fsErr) {
error_log("Filesystem error during default config creation: {}", fsErr.what());
return false;
} catch (const Exception& e) {
error_log("Failed to create default config file: {}", e.what());
return false;
} catch (...) {
error_log("An unexpected error occurred during default config creation.");
return false;
}
}
} // namespace
Config::Config(const toml::table& tbl) {
const toml::node_view genTbl = tbl["general"];
const toml::node_view npTbl = tbl["now_playing"];
const toml::node_view wthTbl = tbl["weather"];
this->general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {};
this->nowPlaying = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {};
this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {};
}
fn Config::getInstance() -> Config {
try {
const fs::path configPath = GetConfigPath();
std::error_code errc;
const bool exists = fs::exists(configPath, errc);
if (errc)
warn_log("Failed to check if config file exists at {}: {}. Assuming it doesn't.", configPath.string(), errc.message());
if (!exists) {
info_log("Config file not found at {}, creating defaults.", configPath.string());
if (!CreateDefaultConfig(configPath)) {
warn_log("Failed to create default config file at {}. Using in-memory defaults.", configPath.string());
return {};
}
}
const toml::table config = toml::parse_file(configPath.string());
debug_log("Config loaded from {}", configPath.string());
return Config(config);
} catch (const Exception& e) {
debug_log("Config loading failed: {}, using defaults", e.what());
return {};
} catch (...) {
error_log("An unexpected error occurred during config loading. Using in-memory defaults.");
return {};
}
}

View file

@ -1,203 +0,0 @@
#pragma once
#include <memory> // std::{make_unique, unique_ptr}
#include <toml++/impl/node.hpp> // toml::node
#include <toml++/impl/node_view.hpp> // toml::node_view
#include <toml++/impl/table.hpp> // toml::table
#include <variant> // std::variant
#ifdef _WIN32
#include <windows.h> // GetUserNameA
#else
#include <pwd.h> // getpwuid, passwd
#include <unistd.h> // getuid
#include "Util/Env.hpp"
#endif
#include "Services/Weather.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "../Services/Weather/MetNoService.hpp"
#include "../Services/Weather/OpenMeteoService.hpp"
#include "../Services/Weather/OpenWeatherMapService.hpp"
using util::error::DracError;
using util::types::CStr, util::types::String, util::types::Array, util::types::Option, util::types::Result;
/// Alias for the location type used in Weather config, can be a city name (String) or coordinates (Coords).
using Location = std::variant<String, weather::Coords>;
/**
* @struct General
* @brief Holds general configuration settings.
*/
struct General {
String name; ///< Default display name, retrieved from the system.
/**
* @brief Retrieves the default name for the user.
* @return The default name for the user, either from the system or a fallback.
*
* Retrieves the default name for the user based on the operating system.
* On Windows, it uses GetUserNameA to get the username.
* On POSIX systems, it first tries to get the username using getpwuid,
* then checks the USER and LOGNAME environment variables.
*/
static fn getDefaultName() -> String {
#ifdef _WIN32
// Try to get the username using GetUserNameA
Array<char, 256> username;
DWORD size = username.size();
return GetUserNameA(username.data(), &size) ? username.data() : "User";
#else
using util::helpers::GetEnv;
const passwd* pwd = getpwuid(getuid());
CStr pwdName = pwd ? pwd->pw_name : nullptr;
const Result<String> envUser = GetEnv("USER");
const Result<String> envLogname = GetEnv("LOGNAME");
return pwdName ? pwdName
: envUser ? *envUser
: envLogname ? *envLogname
: "User";
#endif
}
/**
* @brief Parses a TOML table to create a General instance.
* @param tbl The TOML table to parse, containing [general].
* @return A General instance with the parsed values, or defaults otherwise.
*/
static fn fromToml(const toml::table& tbl) -> General {
const toml::node_view<const toml::node> nameNode = tbl["name"];
return { .name = nameNode ? *nameNode.value<String>() : getDefaultName() };
}
};
/**
* @struct NowPlaying
* @brief Holds configuration settings for the Now Playing feature.
*/
struct NowPlaying {
bool enabled = false; ///< Flag to enable or disable the Now Playing feature.
/**
* @brief Parses a TOML table to create a NowPlaying instance.
* @param tbl The TOML table to parse, containing [now_playing].
* @return A NowPlaying instance with the parsed values, or defaults otherwise.
*/
static fn fromToml(const toml::table& tbl) -> NowPlaying {
return { .enabled = tbl["enabled"].value_or(false) };
}
};
/**
* @struct Weather
* @brief Holds configuration settings for the Weather feature.
*/
struct Weather {
Location location; ///< Location for weather data, can be a city name or coordinates.
String apiKey; ///< API key for the weather service.
String units; ///< Units for temperature, either "metric" or "imperial".
bool enabled = false; ///< Flag to enable or disable the Weather feature.
bool showTownName = false; ///< Flag to show the town name in the output.
std::unique_ptr<weather::IWeatherService> service = nullptr; ///< Pointer to the weather service.
/**
* @brief Parses a TOML table to create a Weather instance.
* @param tbl The TOML table to parse, containing [weather].
* @return A Weather instance with the parsed values, or defaults otherwise.
*/
static fn fromToml(const toml::table& tbl) -> Weather {
Weather weather;
const Option<String> apiKey = tbl["api_key"].value<String>();
weather.enabled = tbl["enabled"].value_or<bool>(false) && apiKey;
if (!weather.enabled)
return weather;
weather.apiKey = *apiKey;
weather.showTownName = tbl["show_town_name"].value_or(false);
weather.units = tbl["units"].value_or("metric");
// Read provider (default to "openweathermap" if not set)
String provider = tbl["provider"].value_or("openweathermap");
if (const toml::node_view<const toml::node> location = tbl["location"]) {
if (location.is_string())
weather.location = *location.value<String>();
else if (location.is_table())
weather.location = weather::Coords {
.lat = *location.as_table()->get("lat")->value<double>(),
.lon = *location.as_table()->get("lon")->value<double>(),
};
else {
error_log("Invalid location format in config.");
weather.enabled = false;
}
} else {
error_log("No location provided in config.");
weather.enabled = false;
}
if (weather.enabled) {
if (provider == "openmeteo") {
if (std::holds_alternative<weather::Coords>(weather.location)) {
const auto& coords = std::get<weather::Coords>(weather.location);
weather.service = std::make_unique<weather::OpenMeteoService>(coords.lat, coords.lon, weather.units);
} else {
error_log("OpenMeteo requires coordinates for location.");
weather.enabled = false;
}
} else if (provider == "metno") {
if (std::holds_alternative<weather::Coords>(weather.location)) {
const auto& coords = std::get<weather::Coords>(weather.location);
weather.service = std::make_unique<weather::MetNoService>(coords.lat, coords.lon, weather.units);
} else {
error_log("MetNo requires coordinates for location.");
weather.enabled = false;
}
} else if (provider == "openweathermap") {
weather.service = std::make_unique<weather::OpenWeatherMapService>(weather.location, weather.apiKey, weather.units);
} else {
error_log("Unknown weather provider: {}", provider);
weather.enabled = false;
}
}
return weather;
}
};
/**
* @struct Config
* @brief Holds the application configuration settings.
*/
struct Config {
General general; ///< General configuration settings.
Weather weather; ///< Weather configuration settings.`
NowPlaying nowPlaying; ///< Now Playing configuration settings.
Config() = default;
explicit Config(const toml::table& tbl);
/**
* @brief Retrieves the path to the configuration file.
* @return The path to the configuration file.
*
* This function constructs the path to the configuration file based on
* the operating system and user directory. It returns a std::filesystem::path
* object representing the configuration file path.
*/
static fn getInstance() -> Config;
};

View file

@ -1,109 +0,0 @@
#include "SystemData.hpp"
#include <chrono> // std::chrono::system_clock
#include <ctime> // localtime_r/s, strftime, time_t, tm
#include <format> // std::format
#include <future> // std::{async, launch}
#include <matchit.hpp> // matchit::{match, is, in, _}
#include "Config/Config.hpp"
#include "Services/PackageCounting.hpp"
#include "Services/Weather.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
#include "OS/OperatingSystem.hpp"
using util::error::DracError, util::error::DracErrorCode;
namespace {
using util::types::i32, util::types::CStr;
fn getOrdinalSuffix(const i32 day) -> CStr {
using matchit::match, matchit::is, matchit::_, matchit::in;
return match(day)(
is | in(11, 13) = "th",
is | (_ % 10 == 1) = "st",
is | (_ % 10 == 2) = "nd",
is | (_ % 10 == 3) = "rd",
is | _ = "th"
);
}
fn getDate() -> Result<String> {
using std::chrono::system_clock;
using util::types::String, util::types::usize, util::types::Err;
const system_clock::time_point nowTp = system_clock::now();
const std::time_t nowTt = system_clock::to_time_t(nowTp);
std::tm nowTm;
#ifdef _WIN32
if (localtime_s(&nowTm, &nowTt) == 0) {
#else
if (localtime_r(&nowTt, &nowTm) != nullptr) {
#endif
i32 day = nowTm.tm_mday;
String monthBuffer(32, '\0');
if (const usize monthLen = std::strftime(monthBuffer.data(), monthBuffer.size(), "%B", &nowTm); monthLen > 0) {
monthBuffer.resize(monthLen);
CStr suffix = getOrdinalSuffix(day);
try {
return std::format("{} {}{}", monthBuffer, day, suffix);
} catch (const std::format_error& e) { return Err(DracError(DracErrorCode::ParseError, e.what())); }
}
return Err(DracError(DracErrorCode::ParseError, "Failed to format date"));
}
return Err(DracError(DracErrorCode::ParseError, "Failed to get local time"));
}
} // namespace
namespace os {
SystemData::SystemData(const Config& config) {
using package::GetTotalCount;
using util::types::Future, util::types::Err;
using weather::WeatherReport;
using enum std::launch;
using enum util::error::DracErrorCode;
Future<Result<String>> hostFut = std::async(async, GetHost);
Future<Result<String>> kernelFut = std::async(async, GetKernelVersion);
Future<Result<String>> osFut = std::async(async, GetOSVersion);
Future<Result<u64>> memFut = std::async(async, GetMemInfo);
Future<Result<String>> deFut = std::async(async, GetDesktopEnvironment);
Future<Result<String>> wmFut = std::async(async, GetWindowManager);
Future<Result<DiskSpace>> diskFut = std::async(async, GetDiskUsage);
Future<Result<String>> shellFut = std::async(async, GetShell);
Future<Result<u64>> pkgFut = std::async(async, GetTotalCount);
Future<Result<MediaInfo>> npFut = std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying);
Future<Result<WeatherReport>> wthrFut = std::async(config.weather.enabled ? async : deferred, [&config]() -> Result<WeatherReport> {
return config.weather.enabled && config.weather.service
? config.weather.service->getWeatherInfo()
: Err(DracError(ApiUnavailable, "Weather API disabled"));
});
this->date = getDate();
this->host = hostFut.get();
this->kernelVersion = kernelFut.get();
this->osVersion = osFut.get();
this->memInfo = memFut.get();
this->desktopEnv = deFut.get();
this->windowMgr = wmFut.get();
this->diskUsage = diskFut.get();
this->shell = shellFut.get();
this->packageCount = pkgFut.get();
this->weather = config.weather.enabled ? wthrFut.get() : Err(DracError(ApiUnavailable, "Weather API disabled"));
this->nowPlaying = config.nowPlaying.enabled ? npFut.get() : Err(DracError(ApiUnavailable, "Now Playing API disabled"));
}
} // namespace os

View file

@ -1,96 +0,0 @@
#pragma once
#include <format> // std::{formatter, format_to}
#include "Services/Weather.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
struct Config;
using util::types::u64, util::types::f64, util::types::String, util::types::Option, util::types::Result,
util::types::MediaInfo, util::types::DiskSpace;
/**
* @struct BytesToGiB
* @brief Helper struct to format a byte value to GiB (Gibibytes).
*
* Encapsulates a byte value and provides a custom formatter
* to convert it to GiB for display purposes.
*/
struct BytesToGiB {
u64 value; ///< The byte value to be converted.
/**
* @brief Constructor for BytesToGiB.
* @param value The byte value to be converted.
*/
explicit constexpr BytesToGiB(u64 value)
: value(value) {}
};
/// @brief Conversion factor from bytes to GiB
constexpr u64 GIB = 1'073'741'824;
/**
* @brief Custom formatter for BytesToGiB.
*
* Allows formatting BytesToGiB values using std::format.
* Outputs the value in GiB with two decimal places.
*
* @code{.cpp}
* #include <format>
* #include "system_data.h"
*
* i32 main() {
* BytesToGiB data_size{2'147'483'648}; // 2 GiB
* String formatted = std::format("Size: {}", data_size);
* std::println("{}", formatted); // formatted will be "Size: 2.00GiB"
* return 0;
* }
* @endcode
*/
template <>
struct std::formatter<BytesToGiB> : std::formatter<double> {
/**
* @brief Formats the BytesToGiB value.
* @param BTG The BytesToGiB instance to format.
* @param ctx The formatting context.
* @return An iterator to the end of the formatted output.
*/
fn format(const BytesToGiB& BTG, auto& ctx) const {
return std::format_to(ctx.out(), "{:.2f}GiB", static_cast<f64>(BTG.value) / GIB);
}
};
namespace os {
/**
* @struct SystemData
* @brief Holds various pieces of system information collected from the OS.
*
* This structure aggregates information about the system,
* in order to display it at all at once during runtime.
*/
struct SystemData {
Result<String> date; ///< Current date (e.g., "April 26th").
Result<String> host; ///< Host/product family (e.g., "MacBook Air").
Result<String> kernelVersion; ///< OS kernel version (e.g., "6.14.4").
Result<String> osVersion; ///< OS pretty name (e.g., "Ubuntu 24.04.2 LTS").
Result<u64> memInfo; ///< Total physical RAM in bytes.
Result<String> desktopEnv; ///< Desktop environment (e.g., "KDE").
Result<String> windowMgr; ///< Window manager (e.g., "KWin").
Result<DiskSpace> diskUsage; ///< Used/Total disk space for root filesystem.
Result<String> shell; ///< Name of the current user shell (e.g., "zsh").
Result<u64> packageCount; ///< Total number of packages installed.
Result<MediaInfo> nowPlaying; ///< Result of fetching media info.
Result<weather::WeatherReport> weather; ///< Result of fetching weather info.
/**
* @brief Constructs a SystemData object and initializes its members.
* @param config The configuration object containing settings for the system data.
*/
explicit SystemData(const Config& config);
};
} // namespace os

View file

@ -1,505 +0,0 @@
#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
// clang-format off
#include <dbus/dbus-protocol.h> // DBUS_TYPE_*
#include <dbus/dbus-shared.h> // DBUS_BUS_SESSION
#include <fstream> // ifstream
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/statvfs.h> // statvfs
#include <sys/sysctl.h> // sysctlbyname
#include <sys/un.h> // LOCAL_PEERCRED
#include <sys/utsname.h> // uname, utsname
#ifndef __NetBSD__
#include <kenv.h> // kenv
#include <sys/ucred.h> // xucred
#endif
#include "Services/PackageCounting.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Env.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "Wrappers/DBus.hpp"
#include "Wrappers/Wayland.hpp"
#include "Wrappers/XCB.hpp"
#include "OperatingSystem.hpp"
// clang-format on
using namespace util::types;
using util::error::DracError, util::error::DracErrorCode;
namespace {
#ifdef __FreeBSD__
fn GetPathByPid(pid_t pid) -> Result<String> {
Array<char, PATH_MAX> exePathBuf;
usize size = exePathBuf.size();
Array<i32, 4> mib;
mib.at(0) = CTL_KERN;
mib.at(1) = KERN_PROC_ARGS;
mib.at(2) = pid;
mib.at(3) = KERN_PROC_PATHNAME;
if (sysctl(mib.data(), 4, exePathBuf.data(), &size, nullptr, 0) == -1)
return Err(DracError(std::format("sysctl KERN_PROC_PATHNAME failed for pid {}", pid)));
if (size == 0 || exePathBuf[0] == '\0')
return Err(
DracError(DracErrorCode::NotFound, std::format("sysctl KERN_PROC_PATHNAME returned empty path for pid {}", pid))
);
exePathBuf.at(std::min(size, exePathBuf.size() - 1)) = '\0';
return String(exePathBuf.data());
}
#endif
#ifdef HAVE_XCB
fn GetX11WindowManager() -> Result<String> {
using namespace xcb;
using namespace matchit;
using enum ConnError;
using util::types::StringView;
const DisplayGuard conn;
if (!conn)
if (const i32 err = ConnectionHasError(conn.get()))
return Err(
DracError(
DracErrorCode::ApiUnavailable,
match(err)(
is | Generic = "Stream/Socket/Pipe Error",
is | ExtNotSupported = "Extension Not Supported",
is | MemInsufficient = "Insufficient Memory",
is | ReqLenExceed = "Request Length Exceeded",
is | ParseErr = "Display String Parse Error",
is | InvalidScreen = "Invalid Screen",
is | FdPassingFailed = "FD Passing Failed",
is | _ = std::format("Unknown Error Code ({})", err)
)
)
);
fn internAtom = [&conn](const StringView name) -> Result<atom_t> {
using util::types::u16;
const ReplyGuard<intern_atom_reply_t> reply(InternAtomReply(conn.get(), InternAtom(conn.get(), 0, static_cast<u16>(name.size()), name.data()), nullptr));
if (!reply)
return Err(DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)));
return reply->atom;
};
const Result<atom_t> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<atom_t> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<atom_t> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom)
error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom");
if (!wmNameAtom)
error_log("Failed to get _NET_WM_NAME atom");
if (!utf8StringAtom)
error_log("Failed to get UTF8_STRING atom");
return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
}
const ReplyGuard<get_property_reply_t> wmWindowReply(GetPropertyReply(
conn.get(),
GetProperty(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1),
nullptr
));
if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 ||
GetPropertyValueLength(wmWindowReply.get()) == 0)
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
const window_t wmRootWindow = *static_cast<window_t*>(GetPropertyValue(wmWindowReply.get()));
const ReplyGuard<get_property_reply_t> wmNameReply(GetPropertyReply(
conn.get(), GetProperty(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || GetPropertyValueLength(wmNameReply.get()) == 0)
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
const char* nameData = static_cast<const char*>(GetPropertyValue(wmNameReply.get()));
const usize length = GetPropertyValueLength(wmNameReply.get());
return String(nameData, length);
}
#else
fn GetX11WindowManager() -> Result<String> {
return Err(DracError(DracErrorCode::NotSupported, "XCB (X11) support not available"));
}
#endif
fn GetWaylandCompositor() -> Result<String> {
#ifndef __FreeBSD__
return "Wayland Compositor";
#else
const wl::DisplayGuard display;
if (!display)
return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
pid_t peerPid = -1; // Initialize PID
struct xucred cred;
socklen_t len = sizeof(cred);
if (getsockopt(fileDescriptor, SOL_SOCKET, LOCAL_PEERCRED, &cred, &len) == -1)
return Err(DracError("Failed to get socket credentials (LOCAL_PEERCRED)"));
peerPid = cred.cr_pid;
if (peerPid <= 0)
return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to obtain a valid peer PID"));
Result<String> exePathResult = GetPathByPid(peerPid);
if (!exePathResult)
return Err(std::move(exePathResult).error());
const String& exeRealPath = *exePathResult;
StringView compositorNameView;
if (const usize lastSlash = exeRealPath.rfind('/'); lastSlash != String::npos)
compositorNameView = StringView(exeRealPath).substr(lastSlash + 1);
else
compositorNameView = exeRealPath;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/")
return Err(DracError(DracErrorCode::NotFound, "Failed to get compositor name from path"));
if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() &&
compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) {
const StringView cleanedView =
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty())
return Err(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic"));
return String(cleanedView);
}
return String(compositorNameView);
#endif
}
} // namespace
namespace os {
using util::helpers::GetEnv;
fn GetOSVersion() -> Result<String> {
constexpr CStr path = "/etc/os-release";
std::ifstream file(path);
if (file) {
String line;
constexpr StringView prefix = "NAME=";
while (std::getline(file, line)) {
if (StringView(line).starts_with(prefix)) {
String value = line.substr(prefix.size());
if ((value.length() >= 2 && value.front() == '"' && value.back() == '"') ||
(value.length() >= 2 && value.front() == '\'' && value.back() == '\''))
value = value.substr(1, value.length() - 2);
return value;
}
}
}
utsname uts;
if (uname(&uts) == -1)
return Err(DracError(std::format("Failed to open {} and uname() call also failed", path)));
String osName = uts.sysname;
if (osName.empty())
return Err(DracError(DracErrorCode::ParseError, "uname() returned empty sysname or release"));
return osName;
}
fn GetMemInfo() -> Result<u64> {
u64 mem = 0;
usize size = sizeof(mem);
#ifdef __NetBSD__
sysctlbyname("hw.physmem64", &mem, &size, nullptr, 0);
#else
sysctlbyname("hw.physmem", &mem, &size, nullptr, 0);
#endif
return mem;
}
fn GetNowPlaying() -> Result<MediaInfo> {
using namespace dbus;
Result<Connection> connectionResult = Connection::busGet(DBUS_BUS_SESSION);
if (!connectionResult)
return Err(connectionResult.error());
const Connection& connection = *connectionResult;
Option<String> activePlayer = None;
{
Result<Message> listNamesResult =
Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
if (!listNamesResult)
return Err(listNamesResult.error());
Result<Message> listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100);
if (!listNamesReplyResult)
return Err(listNamesReplyResult.error());
MessageIter iter = listNamesReplyResult->iterInit();
if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY)
return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array"));
MessageIter subIter = iter.recurse();
if (!subIter.isValid())
return Err(
DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array")
);
while (subIter.getArgType() != DBUS_TYPE_INVALID) {
if (Option<String> name = subIter.getString())
if (name->starts_with("org.mpris.MediaPlayer2.")) {
activePlayer = std::move(*name);
break;
}
if (!subIter.next())
break;
}
}
if (!activePlayer)
return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found"));
Result<Message> msgResult = Message::newMethodCall(
activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"
);
if (!msgResult)
return Err(msgResult.error());
Message& msg = *msgResult;
if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata"))
return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message"));
Result<Message> replyResult = connection.sendWithReplyAndBlock(msg, 100);
if (!replyResult)
return Err(replyResult.error());
Option<String> title = None;
Option<String> artist = None;
MessageIter propIter = replyResult->iterInit();
if (!propIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator"));
if (propIter.getArgType() != DBUS_TYPE_VARIANT)
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant"));
MessageIter variantIter = propIter.recurse();
if (!variantIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant"));
if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY)
return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})"));
MessageIter dictIter = variantIter.recurse();
if (!dictIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array"));
while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) {
MessageIter entryIter = dictIter.recurse();
if (!entryIter.isValid()) {
debug_log("Warning: Could not recurse into dict entry, skipping.");
if (!dictIter.next())
break;
continue;
}
Option<String> key = entryIter.getString();
if (!key) {
debug_log("Warning: Could not get key string from dict entry, skipping.");
if (!dictIter.next())
break;
continue;
}
if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) {
if (!dictIter.next())
break;
continue;
}
MessageIter valueVariantIter = entryIter.recurse();
if (!valueVariantIter.isValid()) {
if (!dictIter.next())
break;
continue;
}
if (*key == "xesam:title") {
title = valueVariantIter.getString();
} else if (*key == "xesam:artist") {
if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) {
if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid())
artist = artistArrayIter.getString();
} else {
debug_log("Warning: Artist value was not an array of strings as expected.");
}
}
if (!dictIter.next())
break;
}
return MediaInfo(std::move(title), std::move(artist));
}
fn GetWindowManager() -> Result<String> {
if (!GetEnv("DISPLAY") && !GetEnv("WAYLAND_DISPLAY") && !GetEnv("XDG_SESSION_TYPE"))
return Err(DracError(DracErrorCode::NotFound, "Could not find a graphical session"));
if (Result<String> waylandResult = GetWaylandCompositor())
return *waylandResult;
if (Result<String> x11Result = GetX11WindowManager())
return *x11Result;
return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed"));
}
fn GetDesktopEnvironment() -> Result<String> {
if (!GetEnv("DISPLAY") && !GetEnv("WAYLAND_DISPLAY") && !GetEnv("XDG_SESSION_TYPE"))
return Err(DracError(DracErrorCode::NotFound, "Could not find a graphical session"));
return GetEnv("XDG_CURRENT_DESKTOP")
.transform([](String xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
xdgDesktop.resize(colon);
return xdgDesktop;
})
.or_else([](const DracError&) -> Result<String> { return GetEnv("DESKTOP_SESSION"); });
}
fn GetShell() -> Result<String> {
if (const Result<String> shellPath = GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
{ "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh"
}};
// clang-format on
for (const auto& [exe, name] : shellMap)
if (shellPath->contains(exe))
return String(name);
return *shellPath; // fallback to the raw shell path
}
return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable"));
}
fn GetHost() -> Result<String> {
Array<char, 256> buffer {};
usize size = buffer.size();
#if defined(__FreeBSD__) || defined(__DragonFly__)
int result = kenv(KENV_GET, "smbios.system.product", buffer.data(), buffer.size() - 1); // Ensure space for null
if (result == -1) {
if (sysctlbyname("hw.model", buffer.data(), &size, nullptr, 0) == -1)
return Err(DracError("kenv smbios.system.product failed and sysctl hw.model also failed"));
buffer.at(std::min(size, buffer.size() - 1)) = '\0';
return String(buffer.data());
}
if (result > 0)
buffer.at(result) = '\0';
else
buffer.at(0) = '\0';
#elifdef __NetBSD__
if (sysctlbyname("machdep.dmi.system-product", buffer.data(), &size, nullptr, 0) == -1)
return Err(DracError(std::format("sysctlbyname failed for")));
buffer[std::min(size, buffer.size() - 1)] = '\0';
#endif
if (buffer[0] == '\0')
return Err(DracError(DracErrorCode::NotFound, "Failed to get host product information (empty result)"));
return String(buffer.data());
}
fn GetKernelVersion() -> Result<String> {
utsname uts;
if (uname(&uts) == -1)
return Err(DracError("uname call failed"));
if (std::strlen(uts.release) == 0)
return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release"));
return uts.release;
}
fn GetDiskUsage() -> Result<DiskSpace> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(DracError(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return DiskSpace {
.usedBytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize),
.totalBytes = stat.f_blocks * stat.f_frsize,
};
}
} // namespace os
namespace package {
#ifdef __NetBSD__
fn GetPkgSrcCount() -> Result<u64> {
return GetCountFromDirectory("pkgsrc", fs::current_path().root_path() / "usr" / "pkg" / "pkgdb", true);
}
#else
fn GetPkgNgCount() -> Result<u64> {
return GetCountFromDb("pkgng", "/var/db/pkg/local.sqlite", "SELECT COUNT(*) FROM packages");
}
#endif
} // namespace package
#endif // __FreeBSD__ || __DragonFly__ || __NetBSD__

View file

@ -1,153 +0,0 @@
#ifdef __HAIKU__
// clang-format off
#include <AppFileInfo.h> // For BAppFileInfo and version_info
#include <Errors.h> // B_OK, strerror, status_t
#include <File.h> // For BFile
#include <OS.h> // get_system_info, system_info
#include <climits> // PATH_MAX
#include <cstring> // std::strlen
#include <os/package/PackageDefs.h> // BPackageKit::BPackageInfoSet
#include <os/package/PackageInfoSet.h> // BPackageKit::BPackageInfo
#include <os/package/PackageRoster.h> // BPackageKit::BPackageRoster
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/statvfs.h> // statvfs
#include <utility> // std::move
#include "Services/PackageCounting.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Env.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "OperatingSystem.hpp"
// clang-format on
using namespace util::types;
using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
namespace os {
fn GetOSVersion() -> Result<String> {
BFile file;
status_t status = file.SetTo("/boot/system/lib/libbe.so", B_READ_ONLY);
if (status != B_OK)
return Err(DracError(DracErrorCode::InternalError, "Error opening /boot/system/lib/libbe.so"));
BAppFileInfo appInfo;
status = appInfo.SetTo(&file);
if (status != B_OK)
return Err(DracError(DracErrorCode::InternalError, "Error initializing BAppFileInfo"));
version_info versionInfo;
status = appInfo.GetVersionInfo(&versionInfo, B_APP_VERSION_KIND);
if (status != B_OK)
return Err(DracError(DracErrorCode::InternalError, "Error reading version info attribute"));
String versionShortString = versionInfo.short_info;
if (versionShortString.empty())
return Err(DracError(DracErrorCode::InternalError, "Version info short_info is empty"));
return std::format("Haiku {}", versionShortString);
}
fn GetMemInfo() -> Result<u64> {
system_info sysinfo;
const status_t status = get_system_info(&sysinfo);
if (status != B_OK)
return Err(DracError(DracErrorCode::InternalError, std::format("get_system_info failed: {}", strerror(status))));
return static_cast<u64>(sysinfo.max_pages) * B_PAGE_SIZE;
}
fn GetNowPlaying() -> Result<MediaInfo> {
return Err(DracError(DracErrorCode::NotSupported, "Now playing is not supported on Haiku"));
}
fn GetWindowManager() -> Result<String> {
return "app_server";
}
fn GetDesktopEnvironment() -> Result<String> {
return "Haiku Desktop Environment";
}
fn GetShell() -> Result<String> {
if (const Result<String> shellPath = GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
{ "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh"
}};
// clang-format on
for (const auto& [exe, name] : shellMap)
if (shellPath->contains(exe))
return String(name);
return *shellPath; // fallback to the raw shell path
}
return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable"));
}
fn GetHost() -> Result<String> {
Array<char, HOST_NAME_MAX + 1> hostnameBuffer {};
if (gethostname(hostnameBuffer.data(), hostnameBuffer.size()) != 0)
return Err(DracError(
DracErrorCode::ApiUnavailable, std::format("gethostname() failed: {} (errno {})", strerror(errno), errno)
));
hostnameBuffer.at(HOST_NAME_MAX) = '\0';
return String(hostnameBuffer.data(), hostnameBuffer.size());
}
fn GetKernelVersion() -> Result<String> {
system_info sysinfo;
const status_t status = get_system_info(&sysinfo);
if (status != B_OK)
return Err(DracError(DracErrorCode::InternalError, std::format("get_system_info failed: {}", strerror(status))));
return std::to_string(sysinfo.kernel_version);
}
fn GetDiskUsage() -> Result<DiskSpace> {
struct statvfs stat;
if (statvfs("/boot", &stat) == -1)
return Err(DracError(std::format("Failed to get filesystem stats for '/boot' (statvfs call failed)")));
return DiskSpace {
.usedBytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize),
.totalBytes = stat.f_blocks * stat.f_frsize,
};
}
} // namespace os
namespace package {
fn GetHaikuCount() -> Result<u64> {
BPackageKit::BPackageRoster roster;
BPackageKit::BPackageInfoSet packageList;
const status_t status = roster.GetActivePackages(BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, packageList);
if (status != B_OK)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get active package list"));
return static_cast<u64>(packageList.CountInfos());
}
} // namespace package
#endif // __HAIKU__

View file

@ -1,650 +0,0 @@
#ifdef __linux__
// clang-format off
#include <climits> // PATH_MAX
#include <cstring> // std::strlen
#include <expected> // std::{unexpected, expected}
#include <filesystem> // std::filesystem::{current_path, directory_entry, directory_iterator, etc.}
#include <format> // std::{format, format_to_n}
#include <fstream> // std::ifstream
#include <glaze/beve/read.hpp> // glz::read_beve
#include <glaze/beve/write.hpp> // glz::write_beve
#include <limits> // std::numeric_limits
#include <matchit.hpp> // matchit::{is, is_not, is_any, etc.}
#include <string> // std::{getline, string (String)}
#include <string_view> // std::string_view (StringView)
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/statvfs.h> // statvfs
#include <sys/sysinfo.h> // sysinfo
#include <sys/utsname.h> // utsname, uname
#include <unistd.h> // readlink
#include <utility> // std::move
#include "Services/PackageCounting.hpp"
#include "Util/Caching.hpp"
#include "Util/Definitions.hpp"
#include "Util/Env.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "Wrappers/DBus.hpp"
#include "Wrappers/Wayland.hpp"
#include "Wrappers/XCB.hpp"
#include "OperatingSystem.hpp"
// clang-format on
using util::error::DracError, util::error::DracErrorCode;
using util::types::String, util::types::Result, util::types::Err, util::types::usize;
namespace {
#ifdef HAVE_XCB
fn GetX11WindowManager() -> Result<String> {
using namespace XCB;
using namespace matchit;
using enum ConnError;
using util::types::StringView;
const DisplayGuard conn;
if (!conn)
if (const i32 err = ConnectionHasError(conn.get()))
return Err(
DracError(
DracErrorCode::ApiUnavailable,
match(err)(
is | Generic = "Stream/Socket/Pipe Error",
is | ExtNotSupported = "Extension Not Supported",
is | MemInsufficient = "Insufficient Memory",
is | ReqLenExceed = "Request Length Exceeded",
is | ParseErr = "Display String Parse Error",
is | InvalidScreen = "Invalid Screen",
is | FdPassingFailed = "FD Passing Failed",
is | _ = std::format("Unknown Error Code ({})", err)
)
)
);
fn internAtom = [&conn](const StringView name) -> Result<Atom> {
using util::types::u16;
const ReplyGuard<IntAtomReply> reply(InternAtomReply(conn.get(), InternAtom(conn.get(), 0, static_cast<u16>(name.size()), name.data()), nullptr));
if (!reply)
return Err(DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)));
return reply->atom;
};
const Result<Atom> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<Atom> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<Atom> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom)
error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom");
if (!wmNameAtom)
error_log("Failed to get _NET_WM_NAME atom");
if (!utf8StringAtom)
error_log("Failed to get UTF8_STRING atom");
return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
}
const ReplyGuard<GetPropReply> wmWindowReply(GetPropertyReply(
conn.get(),
GetProperty(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1),
nullptr
));
if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 ||
GetPropertyValueLength(wmWindowReply.get()) == 0)
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
const Window wmRootWindow = *static_cast<Window*>(GetPropertyValue(wmWindowReply.get()));
const ReplyGuard<GetPropReply> wmNameReply(GetPropertyReply(
conn.get(), GetProperty(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || GetPropertyValueLength(wmNameReply.get()) == 0)
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
const char* nameData = static_cast<const char*>(GetPropertyValue(wmNameReply.get()));
const usize length = GetPropertyValueLength(wmNameReply.get());
return String(nameData, length);
}
#else
fn GetX11WindowManager() -> Result<String> {
return Err(DracError(DracErrorCode::NotSupported, "XCB (X11) support not available"));
}
#endif
#ifdef HAVE_WAYLAND
fn GetWaylandCompositor() -> Result<String> {
using util::types::i32, util::types::Array, util::types::isize, util::types::StringView;
const Wayland::DisplayGuard display;
if (!display)
return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
ucred cred;
socklen_t len = sizeof(cred);
if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1)
return Err(DracError("Failed to get socket credentials (SO_PEERCRED)"));
Array<char, 128> exeLinkPathBuf {};
auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid);
if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1)
return Err(DracError(DracErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
*out = '\0';
const char* exeLinkPath = exeLinkPathBuf.data();
Array<char, PATH_MAX> exeRealPathBuf {}; // NOLINT(misc-include-cleaner) - PATH_MAX is in <climits>
const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1);
if (bytesRead == -1)
return Err(DracError(std::format("Failed to read link '{}'", exeLinkPath)));
exeRealPathBuf.at(bytesRead) = '\0';
StringView compositorNameView;
const StringView pathView(exeRealPathBuf.data(), bytesRead);
StringView filenameView;
if (const usize lastCharPos = pathView.find_last_not_of('/'); lastCharPos != StringView::npos) {
const StringView relevantPart = pathView.substr(0, lastCharPos + 1);
if (const usize separatorPos = relevantPart.find_last_of('/'); separatorPos == StringView::npos)
filenameView = relevantPart;
else
filenameView = relevantPart.substr(separatorPos + 1);
}
if (!filenameView.empty())
compositorNameView = filenameView;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/")
return Err(DracError(DracErrorCode::NotFound, "Failed to get compositor name from path"));
if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() &&
compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) {
const StringView cleanedView =
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty())
return Err(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic"));
return String(cleanedView);
}
return String(compositorNameView);
}
#else
fn GetWaylandCompositor() -> Result<String> {
return Err(DracError(DracErrorCode::NotSupported, "Wayland support not available"));
}
#endif
} // namespace
namespace os {
using util::helpers::GetEnv;
fn GetOSVersion() -> Result<String> {
using util::types::StringView;
std::ifstream file("/etc/os-release");
if (!file)
return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open /etc/os-release")));
String line;
constexpr StringView prefix = "PRETTY_NAME=";
while (std::getline(file, line)) {
if (StringView(line).starts_with(prefix)) {
String value = line.substr(prefix.size());
if ((value.length() >= 2 && value.front() == '"' && value.back() == '"') ||
(value.length() >= 2 && value.front() == '\'' && value.back() == '\''))
value = value.substr(1, value.length() - 2);
if (value.empty())
return Err(DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in /etc/os-release")));
return value;
}
}
return Err(DracError(DracErrorCode::NotFound, "PRETTY_NAME line not found in /etc/os-release"));
}
fn GetMemInfo() -> Result<u64> {
struct sysinfo info;
if (sysinfo(&info) != 0)
return Err(DracError("sysinfo call failed"));
const u64 totalRam = info.totalram;
const u64 memUnit = info.mem_unit;
if (memUnit == 0)
return Err(DracError(DracErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
if (totalRam > std::numeric_limits<u64>::max() / memUnit)
return Err(DracError(DracErrorCode::InternalError, "Potential overflow calculating total RAM"));
return info.totalram * info.mem_unit;
}
fn GetNowPlaying() -> Result<MediaInfo> {
#ifdef HAVE_DBUS
using namespace DBus;
Result<Connection> connectionResult = Connection::busGet(DBUS_BUS_SESSION);
if (!connectionResult)
return Err(connectionResult.error());
const Connection& connection = *connectionResult;
Option<String> activePlayer = None;
{
Result<Message> listNamesResult = Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
if (!listNamesResult)
return Err(listNamesResult.error());
Result<Message> listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100);
if (!listNamesReplyResult)
return Err(listNamesReplyResult.error());
MessageIter iter = listNamesReplyResult->iterInit();
if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY)
return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array"));
MessageIter subIter = iter.recurse();
if (!subIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array"));
while (subIter.getArgType() != DBUS_TYPE_INVALID) {
if (Option<String> name = subIter.getString())
if (name->starts_with("org.mpris.MediaPlayer2.")) {
activePlayer = std::move(*name);
break;
}
if (!subIter.next())
break;
}
}
if (!activePlayer)
return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found"));
Result<Message> msgResult = Message::newMethodCall(activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
if (!msgResult)
return Err(msgResult.error());
Message& msg = *msgResult;
if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata"))
return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message"));
Result<Message> replyResult = connection.sendWithReplyAndBlock(msg, 100);
if (!replyResult)
return Err(replyResult.error());
Option<String> title = None;
Option<String> artist = None;
MessageIter propIter = replyResult->iterInit();
if (!propIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator"));
if (propIter.getArgType() != DBUS_TYPE_VARIANT)
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant"));
MessageIter variantIter = propIter.recurse();
if (!variantIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant"));
if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY)
return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})"));
MessageIter dictIter = variantIter.recurse();
if (!dictIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array"));
while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) {
MessageIter entryIter = dictIter.recurse();
if (!entryIter.isValid()) {
debug_log("Warning: Could not recurse into dict entry, skipping.");
if (!dictIter.next())
break;
continue;
}
Option<String> key = entryIter.getString();
if (!key) {
debug_log("Warning: Could not get key string from dict entry, skipping.");
if (!dictIter.next())
break;
continue;
}
if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) {
if (!dictIter.next())
break;
continue;
}
MessageIter valueVariantIter = entryIter.recurse();
if (!valueVariantIter.isValid()) {
if (!dictIter.next())
break;
continue;
}
if (*key == "xesam:title") {
title = valueVariantIter.getString();
} else if (*key == "xesam:artist") {
if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) {
if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid())
artist = artistArrayIter.getString();
} else
debug_log("Warning: Artist value was not an array of strings as expected.");
}
if (!dictIter.next())
break;
}
return MediaInfo(std::move(title), std::move(artist));
#else
return Err(DracError(DracErrorCode::NotSupported, "DBus support not available"));
#endif
}
fn GetWindowManager() -> Result<String> {
#if !defined(HAVE_WAYLAND) && !defined(HAVE_XCB)
return Err(DracError(DracErrorCode::NotSupported, "Wayland or XCB support not available"));
#else
if (Result<String> waylandResult = GetWaylandCompositor())
return *waylandResult;
if (Result<String> x11Result = GetX11WindowManager())
return *x11Result;
return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed"));
#endif
}
fn GetDesktopEnvironment() -> Result<String> {
return GetEnv("XDG_CURRENT_DESKTOP")
.transform([](String xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
xdgDesktop.resize(colon);
return xdgDesktop;
})
.or_else([](const DracError&) -> Result<String> { return GetEnv("DESKTOP_SESSION"); });
}
fn GetShell() -> Result<String> {
using util::types::Pair, util::types::Array, util::types::StringView;
if (const Result<String> shellPath = GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
{ "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh"
}};
// clang-format on
for (const auto& [exe, name] : shellMap)
if (shellPath->contains(exe))
return String(name);
return *shellPath; // fallback to the raw shell path
}
return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable"));
}
fn GetHost() -> Result<String> {
using util::types::CStr;
constexpr CStr primaryPath = "/sys/class/dmi/id/product_family";
constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name";
fn readFirstLine = [&](const String& path) -> Result<String> {
std::ifstream file(path);
String line;
if (!file)
return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)));
if (!std::getline(file, line) || line.empty())
return Err(DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)));
return line;
};
Result<String> primaryResult = readFirstLine(primaryPath);
if (primaryResult)
return primaryResult;
DracError primaryError = primaryResult.error();
Result<String> fallbackResult = readFirstLine(fallbackPath);
if (fallbackResult)
return fallbackResult;
DracError fallbackError = fallbackResult.error();
return Err(DracError(
DracErrorCode::InternalError,
std::format(
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
primaryPath,
primaryError.message,
fallbackPath,
fallbackError.message
)
));
}
fn GetKernelVersion() -> Result<String> {
utsname uts;
if (uname(&uts) == -1)
return Err(DracError("uname call failed"));
if (std::strlen(uts.release) == 0)
return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release"));
return uts.release;
}
fn GetDiskUsage() -> Result<DiskSpace> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(DracError("Failed to get filesystem stats for '/' (statvfs call failed)"));
return DiskSpace {
.usedBytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize),
.totalBytes = stat.f_blocks * stat.f_frsize,
};
}
} // namespace os
namespace package {
using namespace std::string_literals;
fn CountApk() -> Result<u64> {
using namespace util::cache;
const String pmId = "apk";
const fs::path apkDbPath = "/lib/apk/db/installed";
const String cacheKey = "pkg_count_" + pmId;
std::error_code fsErrCode;
if (!fs::exists(apkDbPath, fsErrCode)) {
if (fsErrCode) {
warn_log("Filesystem error checking for Apk DB at '{}': {}", apkDbPath.string(), fsErrCode.message());
return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Apk DB: " + fsErrCode.message()));
}
return Err(DracError(DracErrorCode::NotFound, std::format("Apk database path '{}' does not exist", apkDbPath.string())));
}
if (Result<PkgCountCacheData> cachedDataResult = ReadCache<PkgCountCacheData>(cacheKey)) {
const auto& [cachedCount, timestamp] = *cachedDataResult;
std::error_code modTimeErrCode;
const fs::file_time_type dbModTime = fs::last_write_time(apkDbPath, modTimeErrCode);
if (modTimeErrCode) {
warn_log(
"Could not get modification time for '{}': {}. Invalidating {} cache.",
apkDbPath.string(),
modTimeErrCode.message(),
pmId
);
} else {
using std::chrono::system_clock, std::chrono::seconds, std::chrono::floor;
const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
if (cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) {
debug_log("Using valid {} package count cache (DB file unchanged since {}). Count: {}", pmId, std::format("{:%F %T %Z}", floor<seconds>(cacheTimePoint)), cachedCount);
return cachedCount;
}
debug_log("{} package count cache stale (DB file modified).", pmId);
}
} else {
if (cachedDataResult.error().code != DracErrorCode::NotFound)
debug_at(cachedDataResult.error());
debug_log("{} package count cache not found or unreadable.", pmId);
}
debug_log("Fetching fresh {} package count from file: {}", pmId, apkDbPath.string());
std::ifstream file(apkDbPath);
if (!file.is_open())
return Err(DracError(DracErrorCode::IoError, std::format("Failed to open Apk database file '{}'", apkDbPath.string())));
String line;
u64 count = 0;
try {
while (std::getline(file, line))
if (line.empty())
count++;
} catch (const std::ios_base::failure& e) {
return Err(DracError(
DracErrorCode::IoError,
std::format("Error reading Apk database file '{}': {}", apkDbPath.string(), e.what())
));
}
if (file.bad())
return Err(DracError(DracErrorCode::IoError, std::format("IO error while reading Apk database file '{}'", apkDbPath.string())));
{
using std::chrono::duration_cast, std::chrono::system_clock, std::chrono::seconds;
const i64 timestampEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache(count, timestampEpochSeconds);
if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult)
debug_at(writeResult.error());
}
return count;
}
fn CountDpkg() -> Result<u64> {
return GetCountFromDirectory("Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", ".list"s);
}
fn CountMoss() -> Result<u64> {
Result<u64> countResult = GetCountFromDb("moss", "/.moss/db/install", "SELECT COUNT(*) FROM meta");
if (countResult)
if (*countResult > 0)
return *countResult - 1;
return countResult;
}
fn CountPacman() -> Result<u64> {
return GetCountFromDirectory("Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", true);
}
fn CountRpm() -> Result<u64> {
return GetCountFromDb("rpm", "/var/lib/rpm/rpmdb.sqlite", "SELECT COUNT(*) FROM Installtid");
}
fn CountXbps() -> Result<u64> {
using util::types::CStr;
const CStr xbpsDbPath = "/var/db/xbps";
if (!fs::exists(xbpsDbPath))
return Err(DracError(DracErrorCode::NotFound, std::format("Xbps database path '{}' does not exist", xbpsDbPath)));
fs::path plistPath;
for (const fs::directory_entry& entry : fs::directory_iterator(xbpsDbPath)) {
const String filename = entry.path().filename().string();
if (filename.starts_with("pkgdb-") && filename.ends_with(".plist")) {
plistPath = entry.path();
break;
}
}
if (plistPath.empty())
return Err(DracError(DracErrorCode::NotFound, "No Xbps database found"));
return GetCountFromPlist("xbps", plistPath);
}
} // namespace package
#endif // __linux__

View file

@ -1,92 +0,0 @@
#pragma once
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
/**
* @namespace os
* @brief Provides a platform-abstracted interface for retrieving Operating System information.
*
* This namespace declares functions to get various system details like memory,
* OS version, hardware identifiers, media playback status, etc.
* The actual implementation for each function is platform-specific
* (found in linux.cpp, windows.cpp, macos.cpp).
*/
namespace os {
using util::types::u64, util::types::String, util::types::Result, util::types::MediaInfo, util::types::DiskSpace;
/**
* @brief Get the total amount of physical RAM installed in the system.
* @return A Result containing the total RAM in bytes (u64) on success,
* or a DracError on failure.
*/
fn GetMemInfo() -> Result<u64>;
/**
* @brief Gets structured metadata about the currently playing media.
* @return A Result containing the media information (MediaInfo struct) on success,
* or a NowPlayingError (indicating player state or system error) on failure.
*/
fn GetNowPlaying() -> Result<MediaInfo>;
/**
* @brief Gets the "pretty" name of the operating system.
* @details Examples: "Ubuntu 24.04.2 LTS", "Windows 11 Pro 24H2", "macOS 15 Sequoia".
* @return A Result containing the OS version String on success, or a DracError on failure.
*/
fn GetOSVersion() -> Result<String>;
/**
* @brief Attempts to retrieve the desktop environment name.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), session files,
* or running processes. On Windows/macOS, it might return a UI theme identifier (e.g., "Fluent", "Aqua") or None.
* @return A Result containing the DE name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetDesktopEnvironment() -> Result<String>;
/**
* @brief Attempts to retrieve the window manager name.
* @details On Linux, checks Wayland compositor or X11 WM properties. On Windows, returns "DWM" or similar.
* On macOS, might return "Quartz Compositor" or a specific tiling WM name if active.
* @return A Result containing the detected WM name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetWindowManager() -> Result<String>;
/**
* @brief Attempts to detect the current user shell name.
* @details Checks the SHELL environment variable on Linux/macOS. On Windows, inspects the process tree
* to identify known shells like PowerShell, Cmd, or MSYS2 shells (Bash, Zsh).
* @return A Result containing the shell name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetShell() -> Result<String>;
/**
* @brief Gets a system identifier, often the hardware model or product family.
* @details Examples: "MacBookPro18,3", "Latitude 5420", "ThinkPad T490".
* Implementation varies: reads DMI info on Linux, registry on Windows, sysctl on macOS.
* @return A Result containing the host/product identifier String on success,
* or a DracError on failure (e.g., permission reading DMI/registry, API error).
*/
fn GetHost() -> Result<String>;
/**
* @brief Gets the operating system's kernel version string.
* @details Examples: "5.15.0-76-generic", "10.0.22621", "23.1.0".
* Uses uname() on Linux/macOS, WinRT/registry on Windows.
* @return A Result containing the kernel version String on success,
* or a DracError on failure.
*/
fn GetKernelVersion() -> Result<String>;
/**
* @brief Gets the disk usage for the primary/root filesystem.
* @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows.
* @return A Result containing the DiskSpace struct (used/total bytes) on success,
* or a DracError on failure (e.g., filesystem not found, permission error).
*/
fn GetDiskUsage() -> Result<DiskSpace>;
} // namespace os

View file

@ -1,179 +0,0 @@
#ifdef __serenity__
// clang-format off
#include <format> // std::format
#include <fstream> // std::ifstream
#include <glaze/core/common.hpp> // glz::object
#include <glaze/core/context.hpp> // glz::{error_ctx, error_code}
#include <glaze/core/meta.hpp> // glz::detail::Object
#include <glaze/core/read.hpp> // glz::read
#include <glaze/core/reflect.hpp> // glz::format_error
#include <glaze/json/read.hpp> // glz::read<glaze_opts>
#include <iterator> // std::istreambuf_iterator
#include <pwd.h> // getpwuid, passwd
#include <string> // std::string (String)
#include <sys/statvfs.h> // statvfs
#include <sys/types.h> // uid_t
#include <sys/utsname.h> // utsname, uname
#include <unistd.h> // getuid, gethostname
#include <unordered_set> // std::unordered_set
#include "Services/PackageCounting.hpp"
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Env.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "OperatingSystem.hpp"
// clang-format on
using namespace util::types;
using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
namespace {
using glz::opts, glz::detail::Object, glz::object;
constexpr opts glaze_opts = { .error_on_unknown_keys = false };
struct MemStatData {
u64 physical_allocated = 0;
u64 physical_available = 0;
// NOLINTBEGIN(readability-identifier-naming)
struct glaze {
using T = MemStatData;
static constexpr Object value =
object("physical_allocated", &T::physical_allocated, "physical_available", &T::physical_available);
};
// NOLINTEND(readability-identifier-naming)
};
fn CountUniquePackages(const String& dbPath) -> Result<u64> {
std::ifstream dbFile(dbPath);
if (!dbFile.is_open())
return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open file: {}", dbPath)));
std::unordered_set<String> uniquePackages;
String line;
while (std::getline(dbFile, line))
if (line.starts_with("manual ") || line.starts_with("auto "))
uniquePackages.insert(line);
return uniquePackages.size();
}
} // namespace
namespace os {
fn GetOSVersion() -> Result<String> {
utsname uts;
if (uname(&uts) == -1)
return Err(DracError("uname call failed for OS Version"));
return uts.sysname;
}
fn GetMemInfo() -> Result<u64> {
CStr path = "/sys/kernel/memstat";
std::ifstream file(path);
if (!file)
return Err(DracError(DracErrorCode::NotFound, std::format("Could not open {}", path)));
String buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
if (buffer.empty())
return Err(DracError(DracErrorCode::IoError, std::format("File is empty: {}", path)));
MemStatData data;
glz::error_ctx error_context = glz::read<glaze_opts>(data, buffer);
if (error_context)
return Err(DracError(
DracErrorCode::ParseError,
std::format("Failed to parse JSON from {}: {}", path, glz::format_error(error_context, buffer))
));
if (data.physical_allocated > std::numeric_limits<u64>::max() - data.physical_available)
return Err(DracError(DracErrorCode::InternalError, "Memory size overflow during calculation"));
return (data.physical_allocated + data.physical_available) * PAGE_SIZE;
}
fn GetNowPlaying() -> Result<MediaInfo> {
return Err(DracError(DracErrorCode::NotSupported, "Now playing is not supported on SerenityOS"));
}
fn GetWindowManager() -> Result<String> {
return "WindowManager";
}
fn GetDesktopEnvironment() -> Result<String> {
return "SerenityOS Desktop";
}
fn GetShell() -> Result<String> {
uid_t userId = getuid();
passwd* pw = getpwuid(userId);
if (pw == nullptr)
return Err(DracError(DracErrorCode::NotFound, std::format("User ID {} not found in /etc/passwd", userId)));
if (pw->pw_shell == nullptr || *(pw->pw_shell) == '\0')
return Err(DracError(
DracErrorCode::NotFound, std::format("User shell entry is empty in /etc/passwd for user ID {}", userId)
));
String shell = pw->pw_shell;
if (shell.starts_with("/bin/"))
shell = shell.substr(5);
return shell;
}
fn GetHost() -> Result<String> {
Array<char, HOST_NAME_MAX> hostname_buffer;
if (gethostname(hostname_buffer.data(), hostname_buffer.size()) != 0)
return Err(DracError("gethostname() failed: {}"));
return String(hostname_buffer.data());
}
fn GetKernelVersion() -> Result<String> {
utsname uts;
if (uname(&uts) == -1)
return Err(DracError("uname call failed for Kernel Version"));
return uts.release;
}
fn GetDiskUsage() -> Result<DiskSpace> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(DracError("statvfs call failed for '/'"));
const u64 totalBytes = static_cast<u64>(stat.f_blocks) * stat.f_frsize;
const u64 freeBytes = static_cast<u64>(stat.f_bfree) * stat.f_frsize;
const u64 usedBytes = totalBytes - freeBytes;
return DiskSpace { usedBytes, totalBytes };
}
} // namespace os
namespace package {
fn GetSerenityCount() -> Result<u64> {
return CountUniquePackages("/usr/Ports/installed.db");
}
} // namespace package
#endif // __serenity__

View file

@ -1,382 +0,0 @@
#ifdef _WIN32
// clang-format off
#define WIN32_LEAN_AND_MEAN
#include <dwmapi.h>
#include <ranges>
#include <tlhelp32.h>
#include <wincrypt.h>
#include <windows.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.System.Profile.h>
#include "Services/PackageCounting.hpp"
#include "Util/Env.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
#include "OperatingSystem.hpp"
// clang-format on
using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW);
namespace {
using util::error::DracError, util::error::DracErrorCode;
using namespace util::types;
struct ProcessData {
DWORD parentPid = 0;
String baseExeNameLower;
};
// clang-format off
constexpr Array<Pair<StringView, StringView>, 3> windowsShellMap = {{
{ "cmd", "Command Prompt" },
{ "powershell", "PowerShell" },
{ "pwsh", "PowerShell Core" },
}};
constexpr Array<Pair<StringView, StringView>, 3> msysShellMap = {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
}};
// clang-format on
fn GetRegistryValue(const HKEY& hKey, const String& subKey, const String& valueName) -> String {
HKEY key = nullptr;
if (RegOpenKeyExA(hKey, subKey.c_str(), 0, KEY_READ, &key) != ERROR_SUCCESS)
return "";
DWORD dataSize = 0;
DWORD type = 0;
if (RegQueryValueExA(key, valueName.c_str(), nullptr, &type, nullptr, &dataSize) != ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
String value((type == REG_SZ || type == REG_EXPAND_SZ) ? dataSize - 1 : dataSize, '\0');
// NOLINTNEXTLINE(*-pro-type-reinterpret-cast) - required here
if (RegQueryValueExA(key, valueName.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(value.data()), &dataSize) != ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
RegCloseKey(key);
return value;
}
template <usize sz>
fn FindShellInProcessTree(const DWORD startPid, const Array<Pair<StringView, StringView>, sz>& shellMap) -> Option<String> {
if (startPid == 0)
return None;
std::unordered_map<DWORD, ProcessData> processMap;
// ReSharper disable once CppLocalVariableMayBeConst
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
error_log("FindShellInProcessTree: Failed snapshot, error {}", GetLastError());
return None;
}
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnap, &pe32)) {
// NOLINTNEXTLINE(*-avoid-do-while)
do {
const String fullName = pe32.szExeFile;
String baseName;
const size_t lastSlash = fullName.find_last_of("\\/");
baseName = (lastSlash == String::npos) ? fullName : fullName.substr(lastSlash + 1);
std::transform(baseName.begin(), baseName.end(), baseName.begin(), [](const u8 character) {
return std::tolower(character);
});
if (baseName.length() > 4 && baseName.ends_with(".exe"))
baseName.resize(baseName.length() - 4);
processMap[pe32.th32ProcessID] =
ProcessData { .parentPid = pe32.th32ParentProcessID, .baseExeNameLower = std::move(baseName) };
} while (Process32Next(hSnap, &pe32));
} else
error_log("FindShellInProcessTree: Process32First failed, error {}", GetLastError());
CloseHandle(hSnap);
DWORD currentPid = startPid;
i32 depth = 0;
constexpr int maxDepth = 32;
while (currentPid != 0 && depth < maxDepth) {
auto procIt = processMap.find(currentPid);
if (procIt == processMap.end())
break;
const String& processName = procIt->second.baseExeNameLower;
auto mapIter =
std::ranges::find_if(shellMap, [&](const auto& pair) { return StringView { processName } == pair.first; });
if (mapIter != std::ranges::end(shellMap))
return String { mapIter->second };
currentPid = procIt->second.parentPid;
depth++;
}
if (depth >= maxDepth)
error_log("FindShellInProcessTree: Reached max depth limit ({}) walking parent PIDs from {}", maxDepth, startPid);
return None;
}
fn GetBuildNumber() -> Option<u64> {
try {
using namespace winrt::Windows::System::Profile;
const auto versionInfo = AnalyticsInfo::VersionInfo();
const winrt::hstring familyVersion = versionInfo.DeviceFamilyVersion();
if (!familyVersion.empty()) {
const u64 versionUl = std::stoull(winrt::to_string(familyVersion));
return (versionUl >> 16) & 0xFFFF;
}
} catch (const winrt::hresult_error& e) {
debug_log("WinRT error getting build number: {}", winrt::to_string(e.message()));
} catch (const Exception& e) { debug_log("Standard exception getting build number: {}", e.what()); }
return None;
}
} // namespace
namespace os {
fn GetMemInfo() -> Result<u64> {
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(MEMORYSTATUSEX);
if (GlobalMemoryStatusEx(&memInfo))
return memInfo.ullTotalPhys;
return Err(DracError(DracErrorCode::PlatformSpecific, std::format("GlobalMemoryStatusEx failed with error code {}", GetLastError())));
}
fn GetNowPlaying() -> Result<MediaInfo> {
using namespace winrt::Windows::Media::Control;
using namespace winrt::Windows::Foundation;
using Session = GlobalSystemMediaTransportControlsSession;
using SessionManager = GlobalSystemMediaTransportControlsSessionManager;
using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties;
try {
const IAsyncOperation<SessionManager> sessionManagerOp = SessionManager::RequestAsync();
const SessionManager sessionManager = sessionManagerOp.get();
if (const Session currentSession = sessionManager.GetCurrentSession()) {
const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
return MediaInfo(winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()));
}
return Err(DracError(DracErrorCode::NotFound, "No media session found"));
} catch (const winrt::hresult_error& e) { return Err(DracError(e)); }
}
fn GetOSVersion() -> Result<String> {
try {
const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)";
String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName");
const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion");
if (productName.empty())
return Err(DracError(DracErrorCode::NotFound, "ProductName not found in registry"));
if (const Option<u64> buildNumberOpt = GetBuildNumber()) {
if (const u64 buildNumber = *buildNumberOpt; buildNumber >= 22000) {
if (const size_t pos = productName.find("Windows 10"); pos != String::npos) {
const bool startBoundary = (pos == 0 || !isalnum(static_cast<u8>(productName[pos - 1])));
const bool endBoundary = (pos + 10 == productName.length() || !isalnum(static_cast<u8>(productName[pos + 10])));
if (startBoundary && endBoundary)
productName.replace(pos, 10, "Windows 11");
}
}
} else
debug_log("Warning: Could not get build number via WinRT; Win11 detection might be inaccurate.");
return displayVersion.empty() ? productName : productName + " " + displayVersion;
} catch (const std::exception& e) { return Err(DracError(e)); }
}
fn GetHost() -> Result<String> {
return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily");
}
fn GetKernelVersion() -> Result<String> {
if (const HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) {
// NOLINTNEXTLINE(*-pro-type-reinterpret-cast) - required here
if (const auto rtlGetVersion = reinterpret_cast<RtlGetVersionPtr>(GetProcAddress(ntdllHandle, "RtlGetVersion"))) {
RTL_OSVERSIONINFOW osInfo = {};
osInfo.dwOSVersionInfoSize = sizeof(osInfo);
if (rtlGetVersion(&osInfo) == 0)
return std::format("{}.{}.{}.{}", osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, osInfo.dwPlatformId);
}
}
return Err(DracError(DracErrorCode::NotFound, "Could not determine kernel version using RtlGetVersion"));
}
fn GetWindowManager() -> Result<String> {
BOOL compositionEnabled = FALSE;
if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled)))
return compositionEnabled ? "DWM" : "Windows Manager (Basic)";
return Err(DracError(DracErrorCode::NotFound, "Failed to get window manager (DwmIsCompositionEnabled failed"));
}
fn GetDesktopEnvironment() -> Result<String> {
const String buildStr =
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "CurrentBuildNumber");
if (buildStr.empty())
return Err(DracError(DracErrorCode::InternalError, "Failed to get CurrentBuildNumber from registry"));
try {
const i32 build = stoi(buildStr);
// Windows 11+ (Fluent)
if (build >= 22000)
return "Fluent (Windows 11)";
// Windows 10 Fluent Era
if (build >= 15063)
return "Fluent (Windows 10)";
// Windows 8.1/10 Metro Era
if (build >= 9200) { // Windows 8+
// Distinguish between Windows 8 and 10
const String productName =
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "ProductName");
if (productName.find("Windows 10") != String::npos)
return "Metro (Windows 10)";
if (build >= 9600)
return "Metro (Windows 8.1)";
return "Metro (Windows 8)";
}
// Windows 7 Aero
if (build >= 7600)
return "Aero (Windows 7)";
// Pre-Win7
return "Classic";
} catch (...) { return Err(DracError(DracErrorCode::ParseError, "Failed to parse CurrentBuildNumber")); }
}
fn GetShell() -> Result<String> {
using util::helpers::GetEnv;
if (const Result<String> msystemResult = GetEnv("MSYSTEM"); msystemResult && !msystemResult->empty()) {
String shellPath;
if (const Result<String> shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty())
shellPath = *shellResult;
else if (const Result<String> loginShellResult = GetEnv("LOGINSHELL");
loginShellResult && !loginShellResult->empty())
shellPath = *loginShellResult;
if (!shellPath.empty()) {
const usize lastSlash = shellPath.find_last_of("\\/");
String shellExe = (lastSlash != String::npos) ? shellPath.substr(lastSlash + 1) : shellPath;
std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) { return std::tolower(character); });
if (shellExe.ends_with(".exe"))
shellExe.resize(shellExe.length() - 4);
const auto iter =
std::ranges::find_if(msysShellMap, [&](const auto& pair) { return StringView { shellExe } == pair.first; });
if (iter != std::ranges::end(msysShellMap))
return String { iter->second };
}
const DWORD currentPid = GetCurrentProcessId();
if (const Option<String> msysShell = FindShellInProcessTree(currentPid, msysShellMap))
return *msysShell;
return "MSYS2 Environment";
}
const DWORD currentPid = GetCurrentProcessId();
if (const Option<String> windowsShell = FindShellInProcessTree(currentPid, windowsShellMap))
return *windowsShell;
return Err(DracError(DracErrorCode::NotFound, "Shell not found"));
}
fn GetDiskUsage() -> Result<DiskSpace> {
ULARGE_INTEGER freeBytes, totalBytes;
if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes))
return DiskSpace { .usedBytes = totalBytes.QuadPart - freeBytes.QuadPart, .totalBytes = totalBytes.QuadPart };
return Err(DracError(util::error::DracErrorCode::NotFound, "Failed to get disk usage"));
}
} // namespace os
namespace package {
using util::helpers::GetEnv;
fn CountChocolatey() -> Result<u64> {
const fs::path chocoPath = fs::path(GetEnv("ChocolateyInstall").value_or("C:\\ProgramData\\chocolatey")) / "lib";
if (!fs::exists(chocoPath) || !fs::is_directory(chocoPath))
return Err(
DracError(DracErrorCode::NotFound, std::format("Chocolatey directory not found: {}", chocoPath.string()))
);
return GetCountFromDirectory("Chocolatey", chocoPath);
}
fn CountScoop() -> Result<u64> {
fs::path scoopAppsPath;
if (const Result<String> scoopEnvPath = GetEnv("SCOOP"))
scoopAppsPath = fs::path(*scoopEnvPath) / "apps";
else if (const Result<String> userProfilePath = GetEnv("USERPROFILE"))
scoopAppsPath = fs::path(*userProfilePath) / "scoop" / "apps";
else
return Err(DracError(
DracErrorCode::NotFound,
"Could not determine Scoop installation directory (SCOOP and USERPROFILE environment variables not found)"
));
return GetCountFromDirectory("Scoop", scoopAppsPath, true);
}
fn CountWinGet() -> Result<u64> {
try {
return std::ranges::distance(winrt::Windows::Management::Deployment::PackageManager().FindPackagesForUser(L""));
} catch (const winrt::hresult_error& e) { return Err(DracError(e)); }
}
} // namespace package
#endif

View file

@ -1,398 +0,0 @@
#ifdef __APPLE__
// clang-format off
#include <chrono> // std::chrono::{system_clock, seconds}
#include <flat_map> // std::flat_map
#include <sys/statvfs.h> // statvfs
#include <sys/sysctl.h> // {CTL_KERN, KERN_PROC, KERN_PROC_ALL, kinfo_proc, sysctl, sysctlbyname}
#include "OperatingSystem.hpp"
#include "Services/PackageCounting.hpp"
#include "Util/Caching.hpp"
#include "Util/Definitions.hpp"
#include "Util/Env.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
#include "macOS/Bridge.hpp"
// clang-format on
using namespace util::types;
using std::chrono::system_clock, std::chrono::seconds;
using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
namespace {
fn StrEqualsIgnoreCase(StringView strA, StringView strB) -> bool {
return std::ranges::equal(strA, strB, [](char aChar, char bChar) {
return std::tolower(static_cast<u8>(aChar)) == std::tolower(static_cast<u8>(bChar));
});
}
fn Capitalize(std::string_view sview) -> Option<String> {
if (sview.empty())
return None;
String result(sview);
result.front() = static_cast<char>(std::toupper(static_cast<u8>(result.front())));
return result;
}
} // namespace
namespace os {
fn GetMemInfo() -> Result<u64> {
u64 mem = 0;
usize size = sizeof(mem);
if (sysctlbyname("hw.memsize", &mem, &size, nullptr, 0) == -1)
return Err(DracError("Failed to get memory info"));
return mem;
}
fn GetNowPlaying() -> Result<MediaInfo> {
return GetCurrentPlayingInfo();
}
fn GetOSVersion() -> Result<String> {
return GetMacOSVersion();
}
fn GetDesktopEnvironment() -> Result<String> {
return "Aqua";
}
fn GetWindowManager() -> Result<String> {
constexpr Array<StringView, 6> knownWms = {
"yabai",
"kwm",
"chunkwm",
"amethyst",
"spectacle",
"rectangle",
};
Array<i32, 3> request = { CTL_KERN, KERN_PROC, KERN_PROC_ALL };
usize len = 0;
if (sysctl(request.data(), request.size(), nullptr, &len, nullptr, 0) == -1)
return Err(DracError("sysctl size query failed for KERN_PROC_ALL"));
if (len == 0)
return Err(DracError(DracErrorCode::NotFound, "sysctl for KERN_PROC_ALL returned zero length"));
Vec<char> buf(len);
if (sysctl(request.data(), request.size(), buf.data(), &len, nullptr, 0) == -1)
return Err(DracError("sysctl data fetch failed for KERN_PROC_ALL"));
if (len % sizeof(kinfo_proc) != 0)
return Err(DracError(
DracErrorCode::PlatformSpecific,
std::format("sysctl returned size {} which is not a multiple of kinfo_proc size {}", len, sizeof(kinfo_proc))
));
usize count = len / sizeof(kinfo_proc);
std::span<const kinfo_proc> processes = std::span(
reinterpret_cast<const kinfo_proc*>(buf.data()), count // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
);
for (const kinfo_proc& procInfo : processes) {
StringView comm(procInfo.kp_proc.p_comm);
for (const auto& wmName : knownWms)
if (StrEqualsIgnoreCase(comm, wmName)) {
const auto capitalized = Capitalize(comm);
return capitalized ? Result<String>(*capitalized)
: Err(DracError(DracErrorCode::ParseError, "Failed to capitalize window manager name"));
}
}
return "Quartz";
}
fn GetKernelVersion() -> Result<String> {
Array<char, 256> kernelVersion {};
usize kernelVersionLen = sizeof(kernelVersion);
if (sysctlbyname("kern.osrelease", kernelVersion.data(), &kernelVersionLen, nullptr, 0) == -1)
return Err(DracError("Failed to get kernel version"));
return kernelVersion.data();
}
fn GetHost() -> Result<String> {
Array<char, 256> hwModel {};
usize hwModelLen = sizeof(hwModel);
if (sysctlbyname("hw.model", hwModel.data(), &hwModelLen, nullptr, 0) == -1)
return Err(DracError("Failed to get host info"));
// taken from https://github.com/fastfetch-cli/fastfetch/blob/dev/src/detection/host/host_mac.c
// shortened a lot of the entries to remove unnecessary info
std::flat_map<StringView, StringView> modelNameByHwModel = {
// MacBook Pro
{ "MacBookPro18,3", "MacBook Pro (14-inch, 2021)" },
{ "MacBookPro18,4", "MacBook Pro (14-inch, 2021)" },
{ "MacBookPro18,1", "MacBook Pro (16-inch, 2021)" },
{ "MacBookPro18,2", "MacBook Pro (16-inch, 2021)" },
{ "MacBookPro17,1", "MacBook Pro (13-inch, M1, 2020)" },
{ "MacBookPro16,3", "MacBook Pro (13-inch, 2020)" },
{ "MacBookPro16,2", "MacBook Pro (13-inch, 2020)" },
{ "MacBookPro16,4", "MacBook Pro (16-inch, 2019)" },
{ "MacBookPro16,1", "MacBook Pro (16-inch, 2019)" },
{ "MacBookPro15,4", "MacBook Pro (13-inch, 2019)" },
{ "MacBookPro15,3", "MacBook Pro (15-inch, 2019)" },
{ "MacBookPro15,2", "MacBook Pro (13-inch, 2018/2019)" },
{ "MacBookPro15,1", "MacBook Pro (15-inch, 2018/2019)" },
{ "MacBookPro14,3", "MacBook Pro (15-inch, 2017)" },
{ "MacBookPro14,2", "MacBook Pro (13-inch, 2017)" },
{ "MacBookPro14,1", "MacBook Pro (13-inch, 2017)" },
{ "MacBookPro13,3", "MacBook Pro (15-inch, 2016)" },
{ "MacBookPro13,2", "MacBook Pro (13-inch, 2016)" },
{ "MacBookPro13,1", "MacBook Pro (13-inch, 2016)" },
{ "MacBookPro12,1", "MacBook Pro (13-inch, 2015)" },
{ "MacBookPro11,4", "MacBook Pro (15-inch, 2015)" },
{ "MacBookPro11,5", "MacBook Pro (15-inch, 2015)" },
{ "MacBookPro11,2", "MacBook Pro (15-inch, 2013/2014)" },
{ "MacBookPro11,3", "MacBook Pro (15-inch, 2013/2014)" },
{ "MacBookPro11,1", "MacBook Pro (13-inch, 2013/2014)" },
{ "MacBookPro10,2", "MacBook Pro (13-inch, 2012/2013)" },
{ "MacBookPro10,1", "MacBook Pro (15-inch, 2012/2013)" },
{ "MacBookPro9,2", "MacBook Pro (13-inch, 2012)" },
{ "MacBookPro9,1", "MacBook Pro (15-inch, 2012)" },
{ "MacBookPro8,3", "MacBook Pro (17-inch, 2011)" },
{ "MacBookPro8,2", "MacBook Pro (15-inch, 2011)" },
{ "MacBookPro8,1", "MacBook Pro (13-inch, 2011)" },
{ "MacBookPro7,1", "MacBook Pro (13-inch, 2010)" },
{ "MacBookPro6,2", "MacBook Pro (15-inch, 2010)" },
{ "MacBookPro6,1", "MacBook Pro (17-inch, 2010)" },
{ "MacBookPro5,5", "MacBook Pro (13-inch, 2009)" },
{ "MacBookPro5,3", "MacBook Pro (15-inch, 2009)" },
{ "MacBookPro5,2", "MacBook Pro (17-inch, 2009)" },
{ "MacBookPro5,1", "MacBook Pro (15-inch, 2008)" },
{ "MacBookPro4,1", "MacBook Pro (17/15-inch, 2008)" },
// MacBook Air
{ "MacBookAir10,1", "MacBook Air (M1, 2020)" },
{ "MacBookAir9,1", "MacBook Air (13-inch, 2020)" },
{ "MacBookAir8,2", "MacBook Air (13-inch, 2019)" },
{ "MacBookAir8,1", "MacBook Air (13-inch, 2018)" },
{ "MacBookAir7,2", "MacBook Air (13-inch, 2015/2017)" },
{ "MacBookAir7,1", "MacBook Air (11-inch, 2015)" },
{ "MacBookAir6,2", "MacBook Air (13-inch, 2013/2014)" },
{ "MacBookAir6,1", "MacBook Air (11-inch, 2013/2014)" },
{ "MacBookAir5,2", "MacBook Air (13-inch, 2012)" },
{ "MacBookAir5,1", "MacBook Air (11-inch, 2012)" },
{ "MacBookAir4,2", "MacBook Air (13-inch, 2011)" },
{ "MacBookAir4,1", "MacBook Air (11-inch, 2011)" },
{ "MacBookAir3,2", "MacBook Air (13-inch, 2010)" },
{ "MacBookAir3,1", "MacBook Air (11-inch, 2010)" },
{ "MacBookAir2,1", "MacBook Air (2009)" },
// Mac mini
{ "Macmini9,1", "Mac mini (M1, 2020)" },
{ "Macmini8,1", "Mac mini (2018)" },
{ "Macmini7,1", "Mac mini (2014)" },
{ "Macmini6,1", "Mac mini (2012)" },
{ "Macmini6,2", "Mac mini (2012)" },
{ "Macmini5,1", "Mac mini (2011)" },
{ "Macmini5,2", "Mac mini (2011)" },
{ "Macmini4,1", "Mac mini (2010)" },
{ "Macmini3,1", "Mac mini (2009)" },
// MacBook
{ "MacBook10,1", "MacBook (12-inch, 2017)" },
{ "MacBook9,1", "MacBook (12-inch, 2016)" },
{ "MacBook8,1", "MacBook (12-inch, 2015)" },
{ "MacBook7,1", "MacBook (13-inch, 2010)" },
{ "MacBook6,1", "MacBook (13-inch, 2009)" },
{ "MacBook5,2", "MacBook (13-inch, 2009)" },
// Mac Pro
{ "MacPro7,1", "Mac Pro (2019)" },
{ "MacPro6,1", "Mac Pro (2013)" },
{ "MacPro5,1", "Mac Pro (2010 - 2012)" },
{ "MacPro4,1", "Mac Pro (2009)" },
// Mac (Generic)
{ "Mac16,3", "iMac (24-inch, 2024)" },
{ "Mac16,2", "iMac (24-inch, 2024)" },
{ "Mac16,1", "MacBook Pro (14-inch, 2024)" },
{ "Mac16,6", "MacBook Pro (14-inch, 2024)" },
{ "Mac16,8", "MacBook Pro (14-inch, 2024)" },
{ "Mac16,7", "MacBook Pro (16-inch, 2024)" },
{ "Mac16,5", "MacBook Pro (16-inch, 2024)" },
{ "Mac16,15", "Mac mini (2024)" },
{ "Mac16,10", "Mac mini (2024)" },
{ "Mac15,13", "MacBook Air (15-inch, M3, 2024)" },
{ "Mac15,2", "MacBook Air (13-inch, M3, 2024)" },
{ "Mac15,3", "MacBook Pro (14-inch, Nov 2023)" },
{ "Mac15,4", "iMac (24-inch, 2023)" },
{ "Mac15,5", "iMac (24-inch, 2023)" },
{ "Mac15,6", "MacBook Pro (14-inch, Nov 2023)" },
{ "Mac15,8", "MacBook Pro (14-inch, Nov 2023)" },
{ "Mac15,10", "MacBook Pro (14-inch, Nov 2023)" },
{ "Mac15,7", "MacBook Pro (16-inch, Nov 2023)" },
{ "Mac15,9", "MacBook Pro (16-inch, Nov 2023)" },
{ "Mac15,11", "MacBook Pro (16-inch, Nov 2023)" },
{ "Mac14,15", "MacBook Air (15-inch, M2, 2023)" },
{ "Mac14,14", "Mac Studio (M2 Ultra, 2023)" },
{ "Mac14,13", "Mac Studio (M2 Max, 2023)" },
{ "Mac14,8", "Mac Pro (2023)" },
{ "Mac14,6", "MacBook Pro (16-inch, 2023)" },
{ "Mac14,10", "MacBook Pro (16-inch, 2023)" },
{ "Mac14,5", "MacBook Pro (14-inch, 2023)" },
{ "Mac14,9", "MacBook Pro (14-inch, 2023)" },
{ "Mac14,3", "Mac mini (M2, 2023)" },
{ "Mac14,12", "Mac mini (M2, 2023)" },
{ "Mac14,7", "MacBook Pro (13-inch, M2, 2022)" },
{ "Mac14,2", "MacBook Air (M2, 2022)" },
{ "Mac13,1", "Mac Studio (M1 Max, 2022)" },
{ "Mac13,2", "Mac Studio (M1 Ultra, 2022)" },
// iMac
{ "iMac21,1", "iMac (24-inch, M1, 2021)" },
{ "iMac21,2", "iMac (24-inch, M1, 2021)" },
{ "iMac20,1", "iMac (27-inch, 2020)" },
{ "iMac20,2", "iMac (27-inch, 2020)" },
{ "iMac19,1", "iMac (27-inch, 2019)" },
{ "iMac19,2", "iMac (21.5-inch, 2019)" },
{ "iMacPro1,1", "iMac Pro (2017)" },
{ "iMac18,3", "iMac (27-inch, 2017)" },
{ "iMac18,2", "iMac (21.5-inch, 2017)" },
{ "iMac18,1", "iMac (21.5-inch, 2017)" },
{ "iMac17,1", "iMac (27-inch, 2015)" },
{ "iMac16,2", "iMac (21.5-inch, 2015)" },
{ "iMac16,1", "iMac (21.5-inch, 2015)" },
{ "iMac15,1", "iMac (27-inch, 2014/2015)" },
{ "iMac14,4", "iMac (21.5-inch, 2014)" },
{ "iMac14,2", "iMac (27-inch, 2013)" },
{ "iMac14,1", "iMac (21.5-inch, 2013)" },
{ "iMac13,2", "iMac (27-inch, 2012)" },
{ "iMac13,1", "iMac (21.5-inch, 2012)" },
{ "iMac12,2", "iMac (27-inch, 2011)" },
{ "iMac12,1", "iMac (21.5-inch, 2011)" },
{ "iMac11,3", "iMac (27-inch, 2010)" },
{ "iMac11,2", "iMac (21.5-inch, 2010)" },
{ "iMac10,1", "iMac (27/21.5-inch, 2009)" },
{ "iMac9,1", "iMac (24/20-inch, 2009)" },
};
const auto iter = modelNameByHwModel.find(hwModel.data());
if (iter == modelNameByHwModel.end())
return Err(DracError("Failed to get host info"));
return String(iter->second);
}
fn GetDiskUsage() -> Result<DiskSpace> {
struct statvfs vfs;
if (statvfs("/", &vfs) != 0)
return Err(DracError("Failed to get disk usage"));
return DiskSpace {
.usedBytes = (vfs.f_blocks - vfs.f_bfree) * vfs.f_frsize,
.totalBytes = vfs.f_blocks * vfs.f_frsize,
};
}
fn GetShell() -> Result<String> {
if (const Result<String> shellPath = GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
{ "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh"
}};
// clang-format on
for (const auto& [exe, name] : shellMap)
if (shellPath->contains(exe))
return String(name);
return *shellPath; // fallback to the raw shell path
}
return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable"));
}
}; // namespace os
namespace package {
fn GetHomebrewCount() -> Result<u64> {
using util::cache::ReadCache, util::cache::WriteCache;
Array<fs::path, 2> cellarPaths {
"/opt/homebrew/Cellar",
"/usr/local/Cellar",
};
if (Result<PkgCountCacheData> cachedDataResult = ReadCache<PkgCountCacheData>("homebrew_total")) {
const auto& [cachedCount, timestamp] = *cachedDataResult;
bool cacheValid = true;
for (const fs::path& cellarPath : cellarPaths) {
if (std::error_code errc; fs::exists(cellarPath, errc) && !errc) {
const fs::file_time_type dirModTime = fs::last_write_time(cellarPath, errc);
if (!errc) {
const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
if (cacheTimePoint.time_since_epoch() < dirModTime.time_since_epoch()) {
cacheValid = false;
break;
}
}
}
}
if (cacheValid) {
debug_log("Using valid Homebrew total count cache. Count: {}", cachedCount);
return cachedCount;
}
}
u64 count = 0;
for (const fs::path& cellarPath : cellarPaths) {
if (std::error_code errc; !fs::exists(cellarPath, errc) || errc) {
if (errc && errc != std::errc::no_such_file_or_directory)
return Err(DracError(errc));
continue;
}
const String cacheKey = "homebrew_" + cellarPath.filename().string();
Result dirCount = GetCountFromDirectory(cacheKey, cellarPath, true);
if (!dirCount) {
if (dirCount.error().code != DracErrorCode::NotFound)
return dirCount;
continue;
}
count += *dirCount;
}
if (count == 0)
return Err(DracError(DracErrorCode::NotFound, "No Homebrew packages found in any Cellar directory"));
const i64 timestampEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache(count, timestampEpochSeconds);
if (Result writeResult = WriteCache("homebrew_total", dataToCache); !writeResult)
debug_at(writeResult.error());
return count;
}
fn GetMacPortsCount() -> Result<u64> {
return GetCountFromDb("macports", "/opt/local/var/macports/registry/registry.db", "SELECT COUNT(*) FROM ports WHERE state='installed';");
}
} // namespace package
#endif

View file

@ -1,27 +0,0 @@
#pragma once
#ifdef __APPLE__
// clang-format off
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
// clang-format on
using util::error::DracError;
using util::types::MediaInfo, util::types::String, util::types::Result;
#ifdef __OBJC__
#import <Foundation/Foundation.h> // Foundation
@interface Bridge : NSObject
+ (void)fetchCurrentPlayingMetadata:(void (^_Nonnull)(NSDictionary* __nullable, NSError* __nullable))completion;
+ (NSString* __nullable)macOSVersion;
@end
#else
extern "C++" {
fn GetCurrentPlayingInfo() -> Result<MediaInfo>;
fn GetMacOSVersion() -> Result<String>;
}
#endif
#endif

View file

@ -1,165 +0,0 @@
#ifdef __APPLE__
// clang-format off
#import "Bridge.hpp"
#import <dispatch/dispatch.h>
#import <objc/runtime.h>
#include <expected>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include "Util/Error.hpp"
// clang-format on
using util::error::DracError, util::error::DracErrorCode;
using util::types::Err, util::types::Option, util::types::None, util::types::Result;
using MRMediaRemoteGetNowPlayingInfoFunction =
void (*)(dispatch_queue_t queue, void (^handler)(NSDictionary* information));
@implementation Bridge
+ (void)fetchCurrentPlayingMetadata:(void (^)(NSDictionary* __nullable, NSError* __nullable))completion {
CFURLRef urlRef = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
CFSTR("/System/Library/PrivateFrameworks/MediaRemote.framework"),
kCFURLPOSIXPathStyle,
false
);
if (!urlRef) {
completion(nil, [NSError errorWithDomain:@"com.draconis.error" code:1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed to create CFURL for MediaRemote framework" }]);
return;
}
CFBundleRef bundleRef = CFBundleCreate(kCFAllocatorDefault, urlRef);
CFRelease(urlRef);
if (!bundleRef) {
completion(nil, [NSError errorWithDomain:@"com.draconis.error" code:1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed to create bundle for MediaRemote framework" }]);
return;
}
auto mrMediaRemoteGetNowPlayingInfo = std::bit_cast<MRMediaRemoteGetNowPlayingInfoFunction>(
CFBundleGetFunctionPointerForName(bundleRef, CFSTR("MRMediaRemoteGetNowPlayingInfo"))
);
if (!mrMediaRemoteGetNowPlayingInfo) {
CFRelease(bundleRef);
completion(nil, [NSError errorWithDomain:@"com.draconis.error" code:1 userInfo:@{ NSLocalizedDescriptionKey : @"Failed to get MRMediaRemoteGetNowPlayingInfo function pointer" }]);
return;
}
std::shared_ptr<std::remove_pointer_t<CFBundleRef>> sharedBundle(bundleRef, [](CFBundleRef bundle) {
if (bundle)
CFRelease(bundle);
});
mrMediaRemoteGetNowPlayingInfo(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^(NSDictionary* information) {
if (!information) {
completion(nil, [NSError errorWithDomain:@"com.draconis.error" code:1 userInfo:@ { NSLocalizedDescriptionKey : @"No now playing information" }]);
return;
}
completion(information, nil);
}
);
}
+ (NSString*)macOSVersion {
NSProcessInfo* processInfo = [NSProcessInfo processInfo];
if (!processInfo)
return nil;
NSOperatingSystemVersion osVersion = [processInfo operatingSystemVersion];
if (osVersion.majorVersion == 0)
return nil;
NSString* versionNumber = nil;
if (osVersion.patchVersion == 0)
versionNumber = [NSString stringWithFormat:@"%ld.%ld",
osVersion.majorVersion,
osVersion.minorVersion];
else
versionNumber = [NSString stringWithFormat:@"%ld.%ld.%ld",
osVersion.majorVersion,
osVersion.minorVersion,
osVersion.patchVersion];
if (!versionNumber)
return nil;
NSDictionary* versionNames =
@{
@11 : @"Big Sur",
@12 : @"Monterey",
@13 : @"Ventura",
@14 : @"Sonoma",
@15 : @"Sequoia"
};
NSNumber* majorVersion = @(osVersion.majorVersion);
NSString* versionName = versionNames[majorVersion] ? versionNames[majorVersion] : @"Unknown";
NSString* fullVersion = [NSString stringWithFormat:@"macOS %@ %@", versionNumber, versionName];
return fullVersion ? fullVersion : nil;
}
@end
extern "C++" {
// NOLINTBEGIN(misc-use-internal-linkage)
fn GetCurrentPlayingInfo() -> Result<MediaInfo> {
__block Result<MediaInfo> result;
const dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[Bridge fetchCurrentPlayingMetadata:^(NSDictionary* __nullable information, NSError* __nullable error) {
if (error) {
result = Err(DracError(DracErrorCode::InternalError, [error.localizedDescription UTF8String]));
dispatch_semaphore_signal(semaphore);
return;
}
if (!information) {
result = Err(DracError(DracErrorCode::InternalError, "No metadata"));
dispatch_semaphore_signal(semaphore);
return;
}
const NSString* const title = information[@"kMRMediaRemoteNowPlayingInfoTitle"];
const NSString* const artist = information[@"kMRMediaRemoteNowPlayingInfoArtist"];
result = MediaInfo(
title
? Option(String([title UTF8String]))
: None,
artist
? Option(String([artist UTF8String]))
: None
);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return result;
}
fn GetMacOSVersion() -> Result<String> {
NSString* version = [Bridge macOSVersion];
return version
? Result<String>(String([version UTF8String]))
: Err(DracError(DracErrorCode::InternalError, "Failed to get macOS version"));
}
// NOLINTEND(misc-use-internal-linkage)
}
#endif

View file

@ -1,472 +0,0 @@
#include "PackageCounting.hpp"
#if !defined(__serenity__) && !defined(_WIN32)
#include <SQLiteCpp/Database.h> // SQLite::{Database, OPEN_READONLY}
#include <SQLiteCpp/Exception.h> // SQLite::Exception
#include <SQLiteCpp/Statement.h> // SQLite::Statement
#endif
#ifdef __linux__
#include <pugixml.hpp> // pugi::{xml_document, xml_node, xml_parse_result}
#endif
#include <chrono> // std::chrono
#include <filesystem> // std::filesystem
#include <format> // std::format
#include <future> // std::{async, future, launch}
#include <matchit.hpp> // matchit::{match, is, or_, _}
#include <system_error> // std::{errc, error_code}
#include "Util/Caching.hpp"
#include "Util/Env.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
namespace {
namespace fs = std::filesystem;
using std::chrono::system_clock, std::chrono::seconds, std::chrono::floor, std::chrono::duration_cast;
using util::cache::ReadCache, util::cache::WriteCache;
using util::error::DracError, util::error::DracErrorCode;
using util::types::Err, util::types::Exception, util::types::Result, util::types::String, util::types::u64, util::types::i64, util::types::Option;
fn GetCountFromDirectoryImpl(
const String& pmId,
const fs::path& dirPath,
const Option<String>& fileExtensionFilter,
const bool subtractOne
) -> Result<u64> {
using package::PkgCountCacheData;
std::error_code fsErrCode;
if (Result<PkgCountCacheData> cachedDataResult = ReadCache<PkgCountCacheData>(pmId)) {
const auto& [cachedCount, timestamp] = *cachedDataResult;
if (!fs::exists(dirPath, fsErrCode) || fsErrCode)
warn_log(
"Error checking existence for directory '{}' before cache validation: {}, Invalidating {} cache",
dirPath.string(),
fsErrCode.message(),
pmId
);
else {
fsErrCode.clear();
const fs::file_time_type dirModTime = fs::last_write_time(dirPath, fsErrCode);
if (fsErrCode)
warn_log(
"Could not get modification time for directory '{}': {}. Invalidating {} cache",
dirPath.string(),
fsErrCode.message(),
pmId
);
else {
if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
cacheTimePoint.time_since_epoch() >= dirModTime.time_since_epoch()) {
debug_log(
"Using valid {} directory count cache (Dir '{}' unchanged since {}). Count: {}",
pmId,
dirPath.string(),
std::format("{:%F %T %Z}", floor<seconds>(cacheTimePoint)),
cachedCount
);
return cachedCount;
}
}
}
} else if (cachedDataResult.error().code != DracErrorCode::NotFound) {
debug_at(cachedDataResult.error());
} else
debug_log("{} directory count cache not found or unreadable", pmId, pmId);
fsErrCode.clear();
if (!fs::is_directory(dirPath, fsErrCode)) {
if (fsErrCode && fsErrCode != std::errc::no_such_file_or_directory)
return Err(DracError(
DracErrorCode::IoError,
std::format("Filesystem error checking if '{}' is a directory: {}", dirPath.string(), fsErrCode.message())
));
return Err(DracError(DracErrorCode::NotFound, std::format("{} path is not a directory: {}", pmId, dirPath.string())));
}
fsErrCode.clear();
u64 count = 0;
try {
const fs::directory_iterator dirIter(dirPath, fs::directory_options::skip_permission_denied, fsErrCode);
if (fsErrCode)
return Err(DracError(
DracErrorCode::IoError,
std::format(
"Failed to create iterator for {} directory '{}': {}", pmId, dirPath.string(), fsErrCode.message()
)
));
for (const fs::directory_entry& entry : dirIter) {
fsErrCode.clear();
if (entry.path().empty())
continue;
if (fileExtensionFilter) {
bool isFile = false;
isFile = entry.is_regular_file(fsErrCode);
if (fsErrCode) {
warn_log("Error stating entry '{}' in {} directory: {}", entry.path().string(), pmId, fsErrCode.message());
continue;
}
if (isFile && entry.path().extension().string() == *fileExtensionFilter)
count++;
continue;
}
if (!fileExtensionFilter)
count++;
}
} catch (const fs::filesystem_error& fsCatchErr) {
return Err(DracError(
DracErrorCode::IoError,
std::format("Filesystem error during {} directory iteration: {}", pmId, fsCatchErr.what())
));
} catch (const Exception& exc) { return Err(DracError(DracErrorCode::InternalError, exc.what())); } catch (...) {
return Err(DracError(DracErrorCode::Other, std::format("Unknown error iterating {} directory", pmId)));
}
if (subtractOne && count > 0)
count--;
if (count == 0)
return Err(DracError(DracErrorCode::NotFound, std::format("No packages found in {} directory", pmId)));
const i64 timestampEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache(count, timestampEpochSeconds);
if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult)
debug_at(writeResult.error());
return count;
}
} // namespace
namespace package {
namespace fs = std::filesystem;
using util::types::Err, util::types::None, util::types::Option, util::types::Result, util::types::String, util::types::u64;
fn GetCountFromDirectory(
const String& pmId,
const fs::path& dirPath,
const String& fileExtensionFilter,
const bool subtractOne
) -> Result<u64> {
return GetCountFromDirectoryImpl(pmId, dirPath, fileExtensionFilter, subtractOne);
}
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const String& fileExtensionFilter)
-> Result<u64> {
return GetCountFromDirectoryImpl(pmId, dirPath, fileExtensionFilter, false);
}
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const bool subtractOne) -> Result<u64> {
return GetCountFromDirectoryImpl(pmId, dirPath, None, subtractOne);
}
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath) -> Result<u64> {
return GetCountFromDirectoryImpl(pmId, dirPath, None, false);
}
#if !defined(__serenity__) && !defined(_WIN32)
fn GetCountFromDb(const String& pmId, const fs::path& dbPath, const String& countQuery) -> Result<u64> {
using util::cache::ReadCache, util::cache::WriteCache;
using util::error::DracError, util::error::DracErrorCode;
using util::types::Exception, util::types::i64;
const String cacheKey = "pkg_count_" + pmId;
if (Result<PkgCountCacheData> cachedDataResult = ReadCache<PkgCountCacheData>(cacheKey)) {
const auto& [count, timestamp] = *cachedDataResult;
std::error_code errc;
const fs::file_time_type dbModTime = fs::last_write_time(dbPath, errc);
if (errc) {
warn_log(
"Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId
);
} else {
if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) {
debug_log(
"Using valid {} package count cache (DB file unchanged since {}). Count: {}",
pmId,
std::format("{:%F %T %Z}", floor<seconds>(cacheTimePoint)),
count
);
return count;
}
debug_log("{} package count cache stale (DB file modified).", pmId);
}
} else {
if (cachedDataResult.error().code != DracErrorCode::NotFound)
debug_at(cachedDataResult.error());
debug_log("{} package count cache not found or unreadable.", pmId);
}
debug_log("Fetching fresh {} package count from database: {}", pmId, dbPath.string());
u64 count = 0;
try {
std::error_code existsErr;
if (!fs::exists(dbPath, existsErr) || existsErr) {
if (existsErr) {
warn_log("Error checking existence of {} DB '{}': {}", pmId, dbPath.string(), existsErr.message());
}
return Err(
DracError(DracErrorCode::NotFound, std::format("{} database not found at '{}'", pmId, dbPath.string()))
);
}
const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY);
SQLite::Statement queryStmt(database, countQuery);
if (queryStmt.executeStep()) {
const i64 countInt64 = queryStmt.getColumn(0).getInt64();
if (countInt64 < 0)
return Err(
DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId))
);
count = static_cast<u64>(countInt64);
} else
return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId)));
} catch (const SQLite::Exception& e) {
error_log("SQLite error occurred accessing {} DB '{}': {}", pmId, dbPath.string(), e.what());
return Err(
DracError(DracErrorCode::ApiUnavailable, std::format("Failed to query {} database: {}", pmId, dbPath.string()))
);
} catch (const Exception& e) {
error_log("Standard exception accessing {} DB '{}': {}", pmId, dbPath.string(), e.what());
return Err(DracError(DracErrorCode::InternalError, e.what()));
} catch (...) {
error_log("Unknown error occurred accessing {} DB '{}'", pmId, dbPath.string());
return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId)));
}
debug_log("Successfully fetched {} package count: {}.", pmId, count);
const i64 timestampEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache(count, timestampEpochSeconds);
if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult)
debug_at(writeResult.error());
return count;
}
#endif // __serenity__ || _WIN32
#ifdef __linux__
fn GetCountFromPlist(const String& pmId, const fs::path& plistPath) -> Result<u64> {
using pugi::xml_document, pugi::xml_node, pugi::xml_parse_result;
using util::cache::ReadCache, util::cache::WriteCache;
using util::error::DracError, util::error::DracErrorCode;
using util::types::i64, util::types::StringView;
const String cacheKey = "pkg_count_" + pmId;
std::error_code fsErrCode;
if (Result<PkgCountCacheData> cachedDataResult = ReadCache<PkgCountCacheData>(cacheKey)) {
const auto& [cachedCount, timestamp] = *cachedDataResult;
if (fs::exists(plistPath, fsErrCode) && !fsErrCode) {
const fs::file_time_type plistModTime = fs::last_write_time(plistPath, fsErrCode);
if (!fsErrCode) {
if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
cacheTimePoint.time_since_epoch() >= plistModTime.time_since_epoch()) {
debug_log("Using valid {} plist count cache (file '{}' unchanged since {}). Count: {}", pmId, plistPath.string(), std::format("{:%F %T %Z}", std::chrono::floor<std::chrono::seconds>(cacheTimePoint)), cachedCount);
return cachedCount;
}
} else {
warn_log("Could not get modification time for '{}': {}. Invalidating {} cache.", plistPath.string(), fsErrCode.message(), pmId);
}
}
} else if (cachedDataResult.error().code != DracErrorCode::NotFound) {
debug_at(cachedDataResult.error());
} else {
debug_log("{} plist count cache not found or unreadable", pmId);
}
xml_document doc;
xml_parse_result result = doc.load_file(plistPath.c_str());
if (!result)
return Err(DracError(DracErrorCode::ParseError, std::format("Failed to parse plist file '{}': {}", plistPath.string(), result.description())));
xml_node dict = doc.child("plist").child("dict");
if (!dict)
return Err(DracError(DracErrorCode::ParseError, std::format("No <dict> in plist file '{}'.", plistPath.string())));
u64 count = 0;
for (xml_node node = dict.first_child(); node; node = node.next_sibling()) {
if (StringView(node.name()) != "key")
continue;
const StringView keyName = node.child_value();
if (keyName == "_XBPS_ALTERNATIVES_")
continue;
xml_node pkgDict = node.next_sibling("dict");
if (!pkgDict)
continue;
bool isInstalled = false;
for (xml_node pkgNode = pkgDict.first_child(); pkgNode; pkgNode = pkgNode.next_sibling())
if (StringView(pkgNode.name()) == "key" && StringView(pkgNode.child_value()) == "state") {
xml_node stateValue = pkgNode.next_sibling("string");
if (stateValue && StringView(stateValue.child_value()) == "installed") {
isInstalled = true;
break;
}
}
if (isInstalled)
++count;
}
if (count == 0)
return Err(DracError(DracErrorCode::NotFound, std::format("No installed packages found in plist file '{}'.", plistPath.string())));
const i64 timestampEpochSeconds = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache(count, timestampEpochSeconds);
if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult)
debug_at(writeResult.error());
return count;
}
#endif // __linux__
#if defined(__linux__) || defined(__APPLE__)
fn CountNix() -> Result<u64> {
return GetCountFromDb("nix", "/nix/var/nix/db/db.sqlite", "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL");
}
#endif // __linux__ || __APPLE__
fn CountCargo() -> Result<u64> {
using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
fs::path cargoPath {};
if (const Result<String> cargoHome = GetEnv("CARGO_HOME"))
cargoPath = fs::path(*cargoHome) / "bin";
else if (const Result<String> homeDir = GetEnv("HOME"))
cargoPath = fs::path(*homeDir) / ".cargo" / "bin";
if (cargoPath.empty() || !fs::exists(cargoPath))
return Err(DracError(DracErrorCode::NotFound, "Could not find cargo directory"));
return GetCountFromDirectory("cargo", cargoPath);
}
fn GetTotalCount() -> Result<u64> {
using util::error::DracError;
using util::types::Array, util::types::Exception, util::types::Future;
#ifdef __linux__
constexpr size_t platformSpecificCount = 6; // Apk, Dpkg, Moss, Pacman, Rpm, Xbps
#elifdef __APPLE__
constexpr size_t platformSpecificCount = 2; // Homebrew, MacPorts
#elifdef _WIN32
constexpr size_t platformSpecificCount = 3; // WinGet, Chocolatey, Scoop
#elif defined(__FreeBSD__) || defined(__DragonFly__)
constexpr size_t platformSpecificCount = 1; // GetPkgNgCount
#elifdef __NetBSD__
constexpr size_t platformSpecificCount = 1; // GetPkgSrcCount
#elifdef __HAIKU__
constexpr size_t platformSpecificCount = 1; // GetHaikuCount
#elifdef __serenity__
constexpr size_t platformSpecificCount = 1; // GetSerenityCount
#endif
#if defined(__linux__) || defined(__APPLE__)
// platform specific + cargo + nix
constexpr size_t numFutures = platformSpecificCount + 2;
#else
// platform specific + cargo
constexpr size_t numFutures = platformSpecificCount + 1;
#endif
Array<Future<Result<u64>>, numFutures>
futures = {
{
#ifdef __linux__
std::async(std::launch::async, CountApk),
std::async(std::launch::async, CountDpkg),
std::async(std::launch::async, CountMoss),
std::async(std::launch::async, CountPacman),
std::async(std::launch::async, CountRpm),
std::async(std::launch::async, CountXbps),
// std::async(std::launch::async, CountZypper),
#elifdef __APPLE__
std::async(std::launch::async, GetHomebrewCount),
std::async(std::launch::async, GetMacPortsCount),
#elifdef _WIN32
std::async(std::launch::async, CountWinGet),
std::async(std::launch::async, CountChocolatey),
std::async(std::launch::async, CountScoop),
#elif defined(__FreeBSD__) || defined(__DragonFly__)
std::async(std::launch::async, GetPkgNgCount),
#elifdef __NetBSD__
std::async(std::launch::async, GetPkgSrcCount),
#elifdef __HAIKU__
std::async(std::launch::async, GetHaikuCount),
#elifdef __serenity__
std::async(std::launch::async, GetSerenityCount),
#endif
#if defined(__linux__) || defined(__APPLE__)
std::async(std::launch::async, CountNix),
#endif
std::async(std::launch::async, CountCargo),
}
};
u64 totalCount = 0;
bool oneSucceeded = false;
for (Future<Result<u64>>& fut : futures) {
try {
using matchit::match, matchit::is, matchit::or_, matchit::_;
using enum util::error::DracErrorCode;
if (Result<u64> result = fut.get()) {
totalCount += *result;
oneSucceeded = true;
debug_log("Added {} packages. Current total: {}", *result, totalCount);
} else
match(result.error().code)(
is | or_(NotFound, ApiUnavailable, NotSupported) = [&] -> void { debug_at(result.error()); },
is | _ = [&] -> void { error_at(result.error()); }
);
} catch (const Exception& exc) {
error_log("Caught exception while getting package count future: {}", exc.what());
} catch (...) { error_log("Caught unknown exception while getting package count future."); }
}
if (!oneSucceeded && totalCount == 0)
return Err(DracError(DracErrorCode::NotFound, "No package managers found or none reported counts."));
debug_log("Final total package count: {}", totalCount);
return totalCount;
}
} // namespace package

View file

@ -1,140 +0,0 @@
#pragma once
#include <filesystem> // std::filesystem::path
#include <glaze/core/common.hpp> // glz::object
#include <glaze/core/meta.hpp> // glz::detail::Object
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
namespace package {
namespace fs = std::filesystem;
using util::error::DracError;
using util::types::Future, util::types::i64, util::types::Result, util::types::String, util::types::u64;
/**
* @struct PkgCountCacheData
* @brief Structure for caching package count results along with a timestamp.
*/
struct PkgCountCacheData {
u64 count {};
i64 timestampEpochSeconds {};
PkgCountCacheData() = default;
PkgCountCacheData(u64 count, i64 timestampEpochSeconds)
: count(count), timestampEpochSeconds(timestampEpochSeconds) {}
// NOLINTBEGIN(readability-identifier-naming)
struct [[maybe_unused]] glaze {
using T = PkgCountCacheData;
static constexpr glz::detail::Object value =
glz::object("count", &T::count, "timestamp", &T::timestampEpochSeconds);
};
// NOLINTEND(readability-identifier-naming)
};
/**
* @struct PackageManagerInfo
* @brief Holds information needed to query a database-backed package manager.
*/
struct PackageManagerInfo {
String id; ///< Unique identifier (e.g., "pacman", "dpkg", used for cache key).
fs::path dbPath; ///< Filesystem path to the database or primary directory.
String countQuery; ///< Query string (e.g., SQL) or specific file/pattern if not DB.
};
/**
* @brief Gets the total package count by querying all relevant package managers.
* @return Result containing the total package count (u64) on success,
* or a DracError if aggregation fails (individual errors logged).
*/
fn GetTotalCount() -> Result<u64>;
/**
* @brief Gets package count from a database using SQLite.
* @param pmInfo Information about the package manager database.
* @return Result containing the count (u64) or a DracError.
*/
fn GetCountFromDb(const String& pmId, const fs::path& dbPath, const String& countQuery) -> Result<u64>;
/**
* @brief Gets package count by iterating entries in a directory, optionally filtering and subtracting.
* @param pmId Identifier for the package manager (for logging/cache).
* @param dirPath Path to the directory to iterate.
* @param fileExtensionFilter Only count files with this extension (e.g., ".list").
* @param subtractOne Subtract one from the final count.
* @return Result containing the count (u64) or a DracError.
*/
fn GetCountFromDirectory(
const String& pmId,
const fs::path& dirPath,
const String& fileExtensionFilter,
bool subtractOne
) -> Result<u64>;
/**
* @brief Gets package count by iterating entries in a directory, filtering by extension.
* @param pmId Identifier for the package manager (for logging/cache).
* @param dirPath Path to the directory to iterate.
* @param fileExtensionFilter Only count files with this extension (e.g., ".list").
* @return Result containing the count (u64) or a DracError. Defaults subtractOne to false.
*/
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const String& fileExtensionFilter)
-> Result<u64>;
/**
* @brief Gets package count by iterating entries in a directory, optionally subtracting one.
* @param pmId Identifier for the package manager (for logging/cache).
* @param dirPath Path to the directory to iterate.
* @param subtractOne Subtract one from the final count.
* @return Result containing the count (u64) or a DracError. Defaults fileExtensionFilter to "".
*/
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, bool subtractOne) -> Result<u64>;
/**
* @brief Gets package count by iterating all entries in a directory.
* @param pmId Identifier for the package manager (for logging/cache).
* @param dirPath Path to the directory to iterate.
* @return Result containing the count (u64) or a DracError. Defaults filter to "" and subtractOne to false.
*/
fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath) -> Result<u64>;
#ifdef __linux__
fn CountApk() -> Result<u64>;
fn CountDpkg() -> Result<u64>;
fn CountMoss() -> Result<u64>;
fn CountPacman() -> Result<u64>;
fn CountRpm() -> Result<u64>;
fn CountXbps() -> Result<u64>;
/**
* @brief Counts installed packages in a plist file (used by xbps and potentially others).
* @param pmId Identifier for the package manager (for logging/cache).
* @param plistPath Path to the plist file.
* @return Result containing the count (u64) or a DracError.
*/
fn GetCountFromPlist(const String& pmId, const std::filesystem::path& plistPath) -> Result<u64>;
#elifdef __APPLE__
fn GetHomebrewCount() -> Result<u64>;
fn GetMacPortsCount() -> Result<u64>;
#elifdef _WIN32
fn CountWinGet() -> Result<u64>;
fn CountChocolatey() -> Result<u64>;
fn CountScoop() -> Result<u64>;
#elif defined(__FreeBSD__) || defined(__DragonFly__)
fn GetPkgNgCount() -> Result<u64>;
#elifdef __NetBSD__
fn GetPkgSrcCount() -> Result<u64>;
#elifdef __HAIKU__
fn GetHaikuCount() -> Result<u64>;
#elifdef __serenity__
fn GetSerenityCount() -> Result<u64>;
#endif
#if defined(__linux__) || defined(__APPLE__)
fn CountNix() -> Result<u64>;
#endif
fn CountCargo() -> Result<u64>;
} // namespace package

View file

@ -1,49 +0,0 @@
#pragma once
#include <glaze/core/common.hpp> // object
#include <glaze/core/meta.hpp> // Object
#include "Util/Types.hpp"
namespace weather {
using glz::detail::Object, glz::object;
using util::types::String, util::types::Vec, util::types::f64, util::types::usize;
// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze`
/**
* @struct WeatherReport
* @brief Represents a weather report.
*
* Contains temperature, conditions, and timestamp.
*/
struct WeatherReport {
f64 temperature; ///< Degrees (C/F)
util::types::Option<String> name; ///< Optional town/city name (may be missing for some providers)
String description; ///< Weather description (e.g., "clear sky", "rain")
usize timestamp; ///< Seconds since epoch
/**
* @brief Glaze serialization and deserialization for WeatherReport.
*/
struct [[maybe_unused]] glaze {
using T = WeatherReport;
static constexpr Object value = object(
"temperature",
&T::temperature,
"name",
&T::name,
"description",
&T::description,
"timestamp",
&T::timestamp
);
};
};
struct Coords {
f64 lat;
f64 lon;
};
// NOLINTEND(readability-identifier-naming)
} // namespace weather

View file

@ -1,25 +0,0 @@
#pragma once
#include "Services/Weather.hpp"
#include "Util/Error.hpp"
namespace weather {
using util::types::Result;
class IWeatherService {
public:
IWeatherService(const IWeatherService&) = delete;
IWeatherService(IWeatherService&&) = delete;
fn operator=(const IWeatherService&)->IWeatherService& = delete;
fn operator=(IWeatherService&&)->IWeatherService& = delete;
virtual ~IWeatherService() = default;
[[nodiscard]] virtual fn getWeatherInfo() const -> Result<WeatherReport> = 0;
protected:
IWeatherService() = default;
};
} // namespace weather

View file

@ -1,305 +0,0 @@
#define NOMINMAX
#ifdef __HAIKU__
#define _DEFAULT_SOURCE // exposes timegm
#endif
#include "MetNoService.hpp"
#include <chrono> // std::chrono::{system_clock, minutes, seconds}
#include <ctime> // std::tm, std::timegm
#include <curl/curl.h> // CURL, CURLcode, CURLOPT_*, CURLE_OK
#include <curl/easy.h> // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_strerror, curl_easy_cleanup
#include <format> // std::format
#include <glaze/json/read.hpp> // glz::read
#include <sstream> // std::istringstream
#include <unordered_map> // std::unordered_map
#include "Util/Caching.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
using weather::MetNoService;
using weather::WeatherReport;
namespace weather {
using util::types::f64, util::types::i32, util::types::String, util::types::usize, util::logging::Option;
struct MetNoTimeseriesDetails {
f64 airTemperature;
};
struct MetNoTimeseriesNext1hSummary {
String symbolCode;
};
struct MetNoTimeseriesNext1h {
MetNoTimeseriesNext1hSummary summary;
};
struct MetNoTimeseriesInstant {
MetNoTimeseriesDetails details;
};
struct MetNoTimeseriesData {
MetNoTimeseriesInstant instant;
Option<MetNoTimeseriesNext1h> next1Hours;
};
struct MetNoTimeseries {
String time;
MetNoTimeseriesData data;
};
struct MetNoProperties {
Vec<MetNoTimeseries> timeseries;
};
struct MetNoResponse {
MetNoProperties properties;
};
struct MetNoTimeseriesDetailsGlaze {
using T = MetNoTimeseriesDetails;
static constexpr auto value = glz::object("air_temperature", &T::airTemperature);
};
struct MetNoTimeseriesNext1hSummaryGlaze {
using T = MetNoTimeseriesNext1hSummary;
static constexpr auto value = glz::object("symbol_code", &T::symbolCode);
};
struct MetNoTimeseriesNext1hGlaze {
using T = MetNoTimeseriesNext1h;
static constexpr auto value = glz::object("summary", &T::summary);
};
struct MetNoTimeseriesInstantGlaze {
using T = MetNoTimeseriesInstant;
static constexpr auto value = glz::object("details", &T::details);
};
struct MetNoTimeseriesDataGlaze {
using T = MetNoTimeseriesData;
// clang-format off
static constexpr auto value = glz::object(
"instant", &T::instant,
"next_1_hours", &T::next1Hours
);
// clang-format on
};
struct MetNoTimeseriesGlaze {
using T = MetNoTimeseries;
// clang-format off
static constexpr auto value = glz::object(
"time", &T::time,
"data", &T::data
);
// clang-format on
};
struct MetNoPropertiesGlaze {
using T = MetNoProperties;
static constexpr auto value = glz::object("timeseries", &T::timeseries);
};
struct MetNoResponseGlaze {
using T = MetNoResponse;
static constexpr auto value = glz::object("properties", &T::properties);
};
} // namespace weather
template <>
struct glz::meta<weather::MetNoTimeseriesDetails> : weather::MetNoTimeseriesDetailsGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesNext1hSummary> : weather::MetNoTimeseriesNext1hSummaryGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesNext1h> : weather::MetNoTimeseriesNext1hGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesInstant> : weather::MetNoTimeseriesInstantGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesData> : weather::MetNoTimeseriesDataGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseries> : weather::MetNoTimeseriesGlaze {};
template <>
struct glz::meta<weather::MetNoProperties> : weather::MetNoPropertiesGlaze {};
template <>
struct glz::meta<weather::MetNoResponse> : weather::MetNoResponseGlaze {};
namespace {
using glz::opts;
using util::error::DracError, util::error::DracErrorCode;
using util::types::usize, util::types::Err, util::types::String;
constexpr opts glazeOpts = { .error_on_unknown_keys = false };
fn SYMBOL_DESCRIPTIONS() -> const std::unordered_map<String, String>& {
static const std::unordered_map<String, String> MAP = {
{ "clearsky_day", "clear sky" },
{ "clearsky_night", "clear sky" },
{ "clearsky_polartwilight", "clear sky" },
{ "cloudy", "cloudy" },
{ "fair_day", "fair" },
{ "fair_night", "fair" },
{ "fair_polartwilight", "fair" },
{ "fog", "fog" },
{ "heavyrain", "heavy rain" },
{ "heavyrainandthunder", "heavy rain and thunder" },
{ "heavyrainshowers_day", "heavy rain showers" },
{ "heavyrainshowers_night", "heavy rain showers" },
{ "heavyrainshowers_polartwilight", "heavy rain showers" },
{ "heavysleet", "heavy sleet" },
{ "heavysleetandthunder", "heavy sleet and thunder" },
{ "heavysleetshowers_day", "heavy sleet showers" },
{ "heavysleetshowers_night", "heavy sleet showers" },
{ "heavysleetshowers_polartwilight", "heavy sleet showers" },
{ "heavysnow", "heavy snow" },
{ "heavysnowandthunder", "heavy snow and thunder" },
{ "heavysnowshowers_day", "heavy snow showers" },
{ "heavysnowshowers_night", "heavy snow showers" },
{ "heavysnowshowers_polartwilight", "heavy snow showers" },
{ "lightrain", "light rain" },
{ "lightrainandthunder", "light rain and thunder" },
{ "lightrainshowers_day", "light rain showers" },
{ "lightrainshowers_night", "light rain showers" },
{ "lightrainshowers_polartwilight", "light rain showers" },
{ "lightsleet", "light sleet" },
{ "lightsleetandthunder", "light sleet and thunder" },
{ "lightsleetshowers_day", "light sleet showers" },
{ "lightsleetshowers_night", "light sleet showers" },
{ "lightsleetshowers_polartwilight", "light sleet showers" },
{ "lightsnow", "light snow" },
{ "lightsnowandthunder", "light snow and thunder" },
{ "lightsnowshowers_day", "light snow showers" },
{ "lightsnowshowers_night", "light snow showers" },
{ "lightsnowshowers_polartwilight", "light snow showers" },
{ "partlycloudy_day", "partly cloudy" },
{ "partlycloudy_night", "partly cloudy" },
{ "partlycloudy_polartwilight", "partly cloudy" },
{ "rain", "rain" },
{ "rainandthunder", "rain and thunder" },
{ "rainshowers_day", "rain showers" },
{ "rainshowers_night", "rain showers" },
{ "rainshowers_polartwilight", "rain showers" },
{ "sleet", "sleet" },
{ "sleetandthunder", "sleet and thunder" },
{ "sleetshowers_day", "sleet showers" },
{ "sleetshowers_night", "sleet showers" },
{ "sleetshowers_polartwilight", "sleet showers" },
{ "snow", "snow" },
{ "snowandthunder", "snow and thunder" },
{ "snowshowers_day", "snow showers" },
{ "snowshowers_night", "snow showers" },
{ "snowshowers_polartwilight", "snow showers" },
{ "unknown", "unknown" }
};
return MAP;
}
fn WriteCallback(void* contents, usize size, usize nmemb, String* str) -> usize {
usize totalSize = size * nmemb;
str->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
fn parse_iso8601_to_epoch(const String& iso8601) -> usize {
std::tm time = {};
std::istringstream stream(iso8601);
stream >> std::get_time(&time, "%Y-%m-%dT%H:%M:%SZ");
if (stream.fail())
return 0;
#ifdef _WIN32
return static_cast<usize>(_mkgmtime(&time));
#else
return static_cast<usize>(timegm(&time));
#endif
}
} // namespace
MetNoService::MetNoService(f64 lat, f64 lon, String units)
: m_lat(lat), m_lon(lon), m_units(std::move(units)) {}
fn MetNoService::getWeatherInfo() const -> util::types::Result<WeatherReport> {
using glz::error_ctx, glz::error_code, glz::read, glz::format_error;
using util::cache::ReadCache, util::cache::WriteCache;
using util::types::String, util::types::Result, util::types::None;
if (Result<WeatherReport> data = ReadCache<WeatherReport>("weather")) {
using std::chrono::system_clock, std::chrono::minutes, std::chrono::seconds;
const WeatherReport& dataVal = *data;
if (const auto cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.timestamp)); cacheAge < minutes(60))
return dataVal;
}
String url = std::format("https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={:.4f}&lon={:.4f}", m_lat, m_lon);
CURL* curl = curl_easy_init();
if (!curl)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to initialize cURL"));
String responseBuffer;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "draconisplusplus/" DRACONISPLUSPLUS_VERSION " git.pupbrained.xyz/draconisplusplus");
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK)
return Err(DracError(DracErrorCode::ApiUnavailable, std::format("cURL error: {}", curl_easy_strerror(res))));
weather::MetNoResponse apiResp {};
if (error_ctx errc = read<glazeOpts>(apiResp, responseBuffer); errc)
return Err(DracError(DracErrorCode::ParseError, "Failed to parse met.no JSON response"));
if (apiResp.properties.timeseries.empty())
return Err(DracError(DracErrorCode::ParseError, "No timeseries data in met.no response"));
const MetNoTimeseries& first = apiResp.properties.timeseries.front();
f64 temp = first.data.instant.details.airTemperature;
if (m_units == "imperial")
temp = temp * 9.0 / 5.0 + 32.0;
String symbolCode = first.data.next1Hours ? first.data.next1Hours->summary.symbolCode : "";
String description = symbolCode;
if (!symbolCode.empty()) {
auto iter = SYMBOL_DESCRIPTIONS().find(symbolCode);
if (iter != SYMBOL_DESCRIPTIONS().end())
description = iter->second;
}
WeatherReport out = {
.temperature = temp,
.name = None,
.description = description,
.timestamp = parse_iso8601_to_epoch(first.time),
};
if (Result<> writeResult = WriteCache("weather", out); !writeResult)
return Err(writeResult.error());
return out;
}

View file

@ -1,16 +0,0 @@
#pragma once
#include "IWeatherService.hpp"
namespace weather {
class MetNoService : public IWeatherService {
public:
MetNoService(f64 lat, f64 lon, String units = "metric");
[[nodiscard]] fn getWeatherInfo() const -> Result<WeatherReport> override;
private:
f64 m_lat;
f64 m_lon;
String m_units;
};
} // namespace weather

View file

@ -1,160 +0,0 @@
#define NOMINMAX
#ifdef __HAIKU__
#define _DEFAULT_SOURCE // exposes timegm
#endif
#include "OpenMeteoService.hpp"
#include <chrono> // std::chrono::{system_clock, minutes, seconds}
#include <ctime> // std::tm, std::timegm
#include <curl/curl.h> // CURL, CURLcode, CURLOPT_*, CURLE_OK
#include <curl/easy.h> // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_strerror, curl_easy_cleanup
#include <format> // std::format
#include <glaze/json/read.hpp> // glz::read
#include <sstream> // std::istringstream
#include "Util/Caching.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
using weather::OpenMeteoService;
using weather::WeatherReport;
namespace weather {
using util::types::f64, util::types::i32, util::types::String;
struct OpenMeteoResponse {
struct CurrentWeather {
f64 temperature;
i32 weathercode;
String time;
} currentWeather;
};
struct OpenMeteoGlaze {
using T = OpenMeteoResponse;
// clang-format off
static constexpr auto value = glz::object(
"current_weather", &T::currentWeather
);
// clang-format on
};
struct CurrentWeatherGlaze {
using T = OpenMeteoResponse::CurrentWeather;
// clang-format off
static constexpr auto value = glz::object(
"temperature", &T::temperature,
"weathercode", &T::weathercode,
"time", &T::time
);
// clang-format on
};
} // namespace weather
template <>
struct glz::meta<weather::OpenMeteoResponse> : weather::OpenMeteoGlaze {};
template <>
struct glz::meta<weather::OpenMeteoResponse::CurrentWeather> : weather::CurrentWeatherGlaze {};
namespace {
using glz::opts;
using util::error::DracError, util::error::DracErrorCode;
using util::types::usize, util::types::Err, util::types::String;
constexpr opts glazeOpts = { .error_on_unknown_keys = false };
fn WriteCallback(void* contents, usize size, usize nmemb, String* str) -> usize {
usize totalSize = size * nmemb;
str->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
fn parse_iso8601_to_epoch(const String& iso8601) -> usize {
std::tm time = {};
std::istringstream stream(iso8601);
stream >> std::get_time(&time, "%Y-%m-%dT%H:%M");
if (stream.fail())
return 0;
#ifdef _WIN32
return static_cast<usize>(_mkgmtime(&time));
#else
return static_cast<usize>(timegm(&time));
#endif
}
} // namespace
OpenMeteoService::OpenMeteoService(f64 lat, f64 lon, String units)
: m_lat(lat), m_lon(lon), m_units(std::move(units)) {}
fn OpenMeteoService::getWeatherInfo() const -> util::types::Result<WeatherReport> {
using glz::error_ctx, glz::error_code, glz::read, glz::format_error;
using util::cache::ReadCache, util::cache::WriteCache;
using util::types::Array, util::types::String, util::types::Result, util::types::None;
if (Result<WeatherReport> data = ReadCache<WeatherReport>("weather")) {
using std::chrono::system_clock, std::chrono::minutes, std::chrono::seconds;
const WeatherReport& dataVal = *data;
if (const auto cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.timestamp)); cacheAge < minutes(60))
return dataVal;
}
String url = std::format(
"https://api.open-meteo.com/v1/forecast?latitude={:.4f}&longitude={:.4f}&current_weather=true&temperature_unit={}",
m_lat,
m_lon,
m_units == "imperial" ? "fahrenheit" : "celsius"
);
CURL* curl = curl_easy_init();
if (!curl)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to initialize cURL"));
String responseBuffer;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK)
return Err(DracError(DracErrorCode::ApiUnavailable, std::format("cURL error: {}", curl_easy_strerror(res))));
OpenMeteoResponse apiResp {};
if (error_ctx errc = read<glazeOpts>(apiResp, responseBuffer); errc)
return Err(DracError(DracErrorCode::ParseError, "Failed to parse Open-Meteo JSON response"));
static constexpr Array<const char*, 9> CODE_DESC = {
"clear sky",
"mainly clear",
"partly cloudy",
"overcast",
"fog",
"drizzle",
"rain",
"snow",
"thunderstorm"
};
WeatherReport out = {
.temperature = apiResp.currentWeather.temperature,
.name = None,
.description = CODE_DESC.at(apiResp.currentWeather.weathercode),
.timestamp = parse_iso8601_to_epoch(apiResp.currentWeather.time),
};
if (Result<> writeResult = WriteCache("weather", out); !writeResult)
return Err(writeResult.error());
return out;
}

View file

@ -1,16 +0,0 @@
#pragma once
#include "IWeatherService.hpp"
namespace weather {
class OpenMeteoService : public IWeatherService {
public:
OpenMeteoService(f64 lat, f64 lon, String units = "metric");
[[nodiscard]] fn getWeatherInfo() const -> Result<WeatherReport> override;
private:
f64 m_lat;
f64 m_lon;
String m_units;
};
} // namespace weather

View file

@ -1,184 +0,0 @@
#define NOMINMAX
#include "OpenWeatherMapService.hpp"
#include <chrono>
#include <curl/curl.h>
#include <curl/easy.h>
#include <format>
#include <glaze/core/meta.hpp>
#include <glaze/json/read.hpp>
#include <matchit.hpp>
#include <variant>
#include "Util/Caching.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
using weather::OpenWeatherMapService;
using weather::WeatherReport;
namespace weather {
using util::types::f64, util::types::i64, util::types::String, util::types::Vec;
struct OWMResponse {
struct Main {
f64 temp;
} main;
struct Weather {
String description;
};
Vec<Weather> weather;
String name;
i64 dt;
};
struct OWMMainGlaze {
using T = OWMResponse::Main;
static constexpr auto value = glz::object("temp", &T::temp);
};
struct OWMWeatherGlaze {
using T = OWMResponse::Weather;
static constexpr auto value = glz::object("description", &T::description);
};
struct OWMResponseGlaze {
using T = OWMResponse;
// clang-format off
static constexpr auto value = glz::object(
"main", &T::main,
"weather", &T::weather,
"name", &T::name,
"dt", &T::dt
);
// clang-format on
};
} // namespace weather
template <>
struct glz::meta<weather::OWMResponse::Main> : weather::OWMMainGlaze {};
template <>
struct glz::meta<weather::OWMResponse::Weather> : weather::OWMWeatherGlaze {};
template <>
struct glz::meta<weather::OWMResponse> : weather::OWMResponseGlaze {};
namespace {
using glz::opts, glz::error_ctx, glz::error_code, glz::read, glz::format_error;
using util::error::DracError, util::error::DracErrorCode;
using util::types::usize, util::types::Err, util::types::Exception, util::types::Result;
using namespace util::cache;
constexpr opts glaze_opts = { .error_on_unknown_keys = false };
fn WriteCallback(void* contents, const usize size, const usize nmemb, weather::String* str) -> usize {
const usize totalSize = size * nmemb;
str->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
fn MakeApiRequest(const weather::String& url) -> Result<WeatherReport> {
debug_log("Making API request to URL: {}", url);
CURL* curl = curl_easy_init();
weather::String responseBuffer;
if (!curl)
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to initialize cURL"));
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5);
const CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK)
return Err(DracError(DracErrorCode::ApiUnavailable, std::format("cURL error: {}", curl_easy_strerror(res))));
weather::OWMResponse owm;
if (const error_ctx errc = read<glaze_opts>(owm, responseBuffer); errc.ec != error_code::none)
return Err(DracError(DracErrorCode::ParseError, std::format("Failed to parse JSON response: {}", format_error(errc, responseBuffer))));
WeatherReport report = {
.temperature = owm.main.temp,
.name = owm.name.empty() ? std::nullopt : util::types::Option<std::string>(owm.name),
.description = !owm.weather.empty() ? owm.weather[0].description : "",
.timestamp = static_cast<usize>(owm.dt),
};
return report;
}
} // namespace
OpenWeatherMapService::OpenWeatherMapService(std::variant<String, Coords> location, String apiKey, String units)
: m_location(std::move(location)), m_apiKey(std::move(apiKey)), m_units(std::move(units)) {}
fn OpenWeatherMapService::getWeatherInfo() const -> Result<WeatherReport> {
using namespace std::chrono;
if (Result<WeatherReport> data = ReadCache<WeatherReport>("weather")) {
const WeatherReport& dataVal = *data;
if (const duration<double> cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.timestamp)); cacheAge < 60min)
return dataVal;
debug_log("Cache expired");
} else {
using matchit::match, matchit::is, matchit::_;
using enum DracErrorCode;
DracError err = data.error();
match(err.code)(
is | NotFound = [&] { debug_at(err); },
is | _ = [&] { error_at(err); }
);
}
fn handleApiResult = [](const Result<WeatherReport>& result) -> Result<WeatherReport> {
if (!result)
return Err(result.error());
if (Result<> writeResult = WriteCache("weather", *result); !writeResult)
return Err(writeResult.error());
return *result;
};
if (std::holds_alternative<String>(m_location)) {
using util::types::i32;
const auto& city = std::get<String>(m_location);
char* escaped = curl_easy_escape(nullptr, city.c_str(), static_cast<i32>(city.length()));
const String apiUrl =
std::format("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}", escaped, m_apiKey, m_units);
curl_free(escaped);
return handleApiResult(MakeApiRequest(apiUrl));
}
if (std::holds_alternative<Coords>(m_location)) {
const auto& [lat, lon] = std::get<Coords>(m_location);
const String apiUrl = std::format(
"https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, m_apiKey, m_units
);
return handleApiResult(MakeApiRequest(apiUrl));
}
return util::types::Err(util::error::DracError(util::error::DracErrorCode::ParseError, "Invalid location type in configuration."));
}

View file

@ -1,18 +0,0 @@
#pragma once
#include <variant>
#include "IWeatherService.hpp"
namespace weather {
class OpenWeatherMapService : public IWeatherService {
public:
OpenWeatherMapService(std::variant<String, Coords> location, String apiKey, String units);
fn getWeatherInfo() const -> Result<WeatherReport> override;
private:
std::variant<String, Coords> m_location;
String m_apiKey;
String m_units;
};
} // namespace weather

View file

@ -1,368 +0,0 @@
#include "UI.hpp"
#include "Util/Types.hpp"
#include "OS/OperatingSystem.hpp"
namespace ui {
using namespace ftxui;
using namespace util::types;
constexpr Theme DEFAULT_THEME = {
.icon = Color::Cyan,
.label = Color::Yellow,
.value = Color::White,
.border = Color::GrayLight,
};
[[maybe_unused]] static constexpr Icons NONE = {
.user = "",
.palette = "",
.calendar = "",
.host = "",
.kernel = "",
.os = "",
.memory = "",
.weather = "",
.music = "",
.disk = "",
.shell = "",
.package = "",
.desktop = "",
.windowManager = "",
};
[[maybe_unused]] static constexpr Icons NERD = {
.user = "",
.palette = "",
.calendar = "",
.host = " 󰌢 ",
.kernel = "",
#ifdef __linux__
.os = " 󰌽 ",
#elifdef __APPLE__
.os = "",
#elifdef _WIN32
.os = "",
#elifdef __FreeBSD__
.os = "",
#else
.os = "",
#endif
.memory = "",
.weather = "",
.music = "",
.disk = " 󰋊 ",
.shell = "",
.package = " 󰏖 ",
.desktop = " 󰇄 ",
.windowManager = "  ",
};
[[maybe_unused]] static constexpr Icons EMOJI = {
.user = " 👤 ",
.palette = " 🎨 ",
.calendar = " 📅 ",
.host = " 💻 ",
.kernel = " 🫀 ",
.os = " 🤖 ",
.memory = " 🧠 ",
.weather = " 🌈 ",
.music = " 🎵 ",
.disk = " 💾 ",
.shell = " 💲 ",
.package = " 📦 ",
.desktop = " 🖥️ ",
.windowManager = " 🪟 ",
};
constexpr inline Icons ICON_TYPE = NERD;
struct RowInfo {
StringView icon;
StringView label;
String value;
};
namespace {
#ifdef __linux__
// clang-format off
constexpr Array<Pair<String, String>, 13> distro_icons {{
{ "NixOS", "" },
{ "Zorin", "" },
{ "Debian", "" },
{ "Fedora", "" },
{ "Gentoo", "" },
{ "Ubuntu", "" },
{ "Manjaro", "" },
{ "Pop!_OS", "" },
{ "Arch Linux", "" },
{ "Linux Mint", "" },
{ "Void Linux", "" },
{ "Alpine Linux", "" },
}};
// clang-format on
fn GetDistroIcon(StringView distro) -> Option<StringView> {
using namespace matchit;
for (const auto& [distroName, distroIcon] : distro_icons)
if (distro.contains(distroName))
return distroIcon;
return None;
}
#endif
fn CreateColorCircles() -> Element {
auto colorView =
std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) {
return ftxui::hbox(
{
ftxui::text("") | ftxui::bold | ftxui::color(static_cast<ftxui::Color::Palette256>(colorIndex)),
ftxui::text(" "),
}
);
});
return hbox(Elements(std::ranges::begin(colorView), std::ranges::end(colorView)));
}
fn get_visual_width(const String& str) -> usize {
return ftxui::string_width(str);
}
fn get_visual_width_sv(const StringView& sview) -> usize {
return ftxui::string_width(String(sview));
}
fn find_max_label_len(const std::vector<RowInfo>& rows) -> usize {
usize maxWidth = 0;
for (const RowInfo& row : rows) maxWidth = std::max(maxWidth, get_visual_width_sv(row.label));
return maxWidth;
};
fn CreateInfoBox(const Config& config, const os::SystemData& data) -> Element {
const String& name = config.general.name;
const Weather& weather = config.weather;
// clang-format off
const auto& [
userIcon,
paletteIcon,
calendarIcon,
hostIcon,
kernelIcon,
osIcon,
memoryIcon,
weatherIcon,
musicIcon,
diskIcon,
shellIcon,
packageIcon,
deIcon,
wmIcon
] = ui::ICON_TYPE;
// clang-format on
std::vector<RowInfo> initialRows; // Date, Weather
std::vector<RowInfo> systemInfoRows; // Host, Kernel, OS, RAM, Disk, Shell, Packages
std::vector<RowInfo> envInfoRows; // DE, WM
if (data.date)
initialRows.push_back({ .icon = calendarIcon, .label = "Date", .value = *data.date });
if (weather.enabled && data.weather) {
const weather::WeatherReport& weatherInfo = *data.weather;
String weatherValue = weather.showTownName && weatherInfo.name
? std::format("{}°F in {}", std::lround(weatherInfo.temperature), *weatherInfo.name)
: std::format("{}°F, {}", std::lround(weatherInfo.temperature), weatherInfo.description);
initialRows.push_back({ .icon = weatherIcon, .label = "Weather", .value = std::move(weatherValue) });
} else if (weather.enabled && !data.weather.has_value())
debug_at(data.weather.error());
if (data.host && !data.host->empty())
systemInfoRows.push_back({ .icon = hostIcon, .label = "Host", .value = *data.host });
if (data.osVersion) {
systemInfoRows.push_back({
#ifdef __linux__
.icon = GetDistroIcon(*data.osVersion).value_or(osIcon),
#else
.icon = osIcon,
#endif
.label = "OS",
.value = *data.osVersion,
});
}
if (data.kernelVersion)
systemInfoRows.push_back({ .icon = kernelIcon, .label = "Kernel", .value = *data.kernelVersion });
if (data.memInfo)
systemInfoRows.push_back({ .icon = memoryIcon, .label = "RAM", .value = std::format("{}", BytesToGiB(*data.memInfo)) });
else if (!data.memInfo.has_value())
debug_at(data.memInfo.error());
if (data.diskUsage)
systemInfoRows.push_back(
{
.icon = diskIcon,
.label = "Disk",
.value = std::format("{}/{}", BytesToGiB(data.diskUsage->usedBytes), BytesToGiB(data.diskUsage->totalBytes)),
}
);
if (data.shell)
systemInfoRows.push_back({ .icon = shellIcon, .label = "Shell", .value = *data.shell });
if (data.packageCount) {
if (*data.packageCount > 0)
systemInfoRows.push_back({ .icon = packageIcon, .label = "Packages", .value = std::format("{}", *data.packageCount) });
else
debug_log("Package count is 0, skipping");
}
bool addedDe = false;
if (data.desktopEnv && (!data.windowMgr || *data.desktopEnv != *data.windowMgr)) {
envInfoRows.push_back({ .icon = deIcon, .label = "DE", .value = *data.desktopEnv });
addedDe = true;
}
if (data.windowMgr)
if (!addedDe || (data.desktopEnv && *data.desktopEnv != *data.windowMgr))
envInfoRows.push_back({ .icon = wmIcon, .label = "WM", .value = *data.windowMgr });
bool nowPlayingActive = false;
String npText;
if (config.nowPlaying.enabled && data.nowPlaying) {
const String title = data.nowPlaying->title.value_or("Unknown Title");
const String artist = data.nowPlaying->artist.value_or("Unknown Artist");
npText = artist + " - " + title;
nowPlayingActive = true;
}
usize maxContentWidth = 0;
const usize greetingWidth = get_visual_width_sv(userIcon) + get_visual_width_sv("Hello ") + get_visual_width(name) + get_visual_width_sv("! ");
maxContentWidth = std::max(maxContentWidth, greetingWidth);
const usize paletteWidth = get_visual_width_sv(userIcon) + (16 * (get_visual_width_sv("") + get_visual_width_sv(" ")));
maxContentWidth = std::max(maxContentWidth, paletteWidth);
const usize iconActualWidth = get_visual_width_sv(userIcon);
const usize maxLabelWidthInitial = find_max_label_len(initialRows);
const usize maxLabelWidthSystem = find_max_label_len(systemInfoRows);
const usize maxLabelWidthEnv = find_max_label_len(envInfoRows);
const usize requiredWidthInitialW = iconActualWidth + maxLabelWidthInitial;
const usize requiredWidthSystemW = iconActualWidth + maxLabelWidthSystem;
const usize requiredWidthEnvW = iconActualWidth + maxLabelWidthEnv;
fn calculateRowVisualWidth = [&](const RowInfo& row, const usize requiredLabelVisualWidth) -> usize {
return requiredLabelVisualWidth + get_visual_width(row.value) + get_visual_width_sv(" ");
};
for (const RowInfo& row : initialRows)
maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthInitialW));
for (const RowInfo& row : systemInfoRows)
maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthSystemW));
for (const RowInfo& row : envInfoRows)
maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthEnvW));
const usize targetBoxWidth = maxContentWidth + 2;
usize npFixedWidthLeft = 0;
usize npFixedWidthRight = 0;
if (nowPlayingActive) {
npFixedWidthLeft = get_visual_width_sv(musicIcon) + get_visual_width_sv("Playing") + get_visual_width_sv(" ");
npFixedWidthRight = get_visual_width_sv(" ");
}
i32 paragraphLimit = 1;
if (nowPlayingActive) {
i32 availableForParagraph = static_cast<i32>(targetBoxWidth) - static_cast<i32>(npFixedWidthLeft) - static_cast<i32>(npFixedWidthRight);
availableForParagraph -= 2;
paragraphLimit = std::max(1, availableForParagraph);
}
fn createStandardRow = [&](const RowInfo& row, const usize sectionRequiredVisualWidth) {
return hbox(
{
hbox(
{
text(String(row.icon)) | color(ui::DEFAULT_THEME.icon),
text(String(row.label)) | color(ui::DEFAULT_THEME.label),
}
) |
size(WIDTH, EQUAL, static_cast<int>(sectionRequiredVisualWidth)),
filler(),
text(row.value) | color(ui::DEFAULT_THEME.value),
text(" "),
}
);
};
Elements content;
content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan));
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
content.push_back(hbox({ text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles() }));
const bool section1Present = !initialRows.empty();
const bool section2Present = !systemInfoRows.empty();
const bool section3Present = !envInfoRows.empty();
if (section1Present)
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
for (const RowInfo& row : initialRows) content.push_back(createStandardRow(row, requiredWidthInitialW));
if ((section1Present && (section2Present || section3Present)) || (!section1Present && section2Present))
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
for (const RowInfo& row : systemInfoRows) content.push_back(createStandardRow(row, requiredWidthSystemW));
if (section2Present && section3Present)
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
for (const RowInfo& row : envInfoRows) content.push_back(createStandardRow(row, requiredWidthEnvW));
if ((section1Present || section2Present || section3Present) && nowPlayingActive)
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
if (nowPlayingActive) {
content.push_back(hbox(
{
text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon),
text("Playing") | color(ui::DEFAULT_THEME.label),
text(" "),
filler(),
paragraphAlignRight(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, paragraphLimit),
text(" "),
}
));
}
return vbox(content) | borderRounded | color(Color::White);
}
} // namespace
fn CreateUI(const Config& config, const os::SystemData& data) -> Element {
Element infoBox = CreateInfoBox(config, data);
return hbox({ infoBox, filler() });
}
} // namespace ui

View file

@ -1,47 +0,0 @@
#pragma once
#include <ftxui/dom/elements.hpp> // ftxui::Element
#include <ftxui/screen/color.hpp> // ftxui::Color
#include "Core/SystemData.hpp"
#include "Config/Config.hpp"
#include "Util/Types.hpp"
namespace ui {
struct Theme {
ftxui::Color::Palette16 icon;
ftxui::Color::Palette16 label;
ftxui::Color::Palette16 value;
ftxui::Color::Palette16 border;
};
extern const Theme DEFAULT_THEME;
struct Icons {
util::types::StringView user;
util::types::StringView palette;
util::types::StringView calendar;
util::types::StringView host;
util::types::StringView kernel;
util::types::StringView os;
util::types::StringView memory;
util::types::StringView weather;
util::types::StringView music;
util::types::StringView disk;
util::types::StringView shell;
util::types::StringView package;
util::types::StringView desktop;
util::types::StringView windowManager;
};
extern const Icons ICON_TYPE;
/**
* @brief Creates the main UI element based on system data and configuration.
* @param config The application configuration.
* @param data The collected system data. @return The root ftxui::Element for rendering.
*/
fn CreateUI(const Config& config, const os::SystemData& data) -> ftxui::Element;
} // namespace ui

View file

@ -1,204 +0,0 @@
#pragma once
#include <filesystem> // std::filesystem
#include <fstream> // std::{ifstream, ofstream}
#include <glaze/beve/read.hpp> // glz::read_beve
#include <glaze/beve/write.hpp> // glz::write_beve
#include <glaze/core/context.hpp> // glz::{context, error_code, error_ctx}
#include <iterator> // std::istreambuf_iterator
#include <system_error> // std::error_code
#include <type_traits> // std::decay_t
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
namespace util::cache {
namespace fs = std::filesystem;
using error::DracError, error::DracErrorCode;
using types::Err, types::Exception, types::Result, types::String, types::isize;
/**
* @brief Gets the full path for a cache file based on a unique key.
* @param cache_key A unique identifier for the cache (e.g., "weather", "pkg_count_pacman").
* Should ideally only contain filesystem-safe characters.
* @return Result containing the filesystem path on success, or a DracError on failure.
*/
inline fn GetCachePath(const String& cache_key) -> Result<fs::path> {
if (cache_key.empty())
return Err(DracError(DracErrorCode::InvalidArgument, "Cache key cannot be empty."));
if (cache_key.find_first_of("/\\:*?\"<>|") != String::npos)
return Err(
DracError(DracErrorCode::InvalidArgument, std::format("Cache key '{}' contains invalid characters.", cache_key))
);
std::error_code errc;
const fs::path cacheDir = fs::temp_directory_path(errc) / "draconis++";
if (!fs::exists(cacheDir, errc)) {
if (errc)
return Err(DracError(DracErrorCode::IoError, "Failed to check existence of cache directory: " + errc.message()));
fs::create_directories(cacheDir, errc);
if (errc)
return Err(DracError(DracErrorCode::IoError, "Failed to create cache directory: " + errc.message()));
}
if (errc)
return Err(DracError(DracErrorCode::IoError, "Failed to get system temporary directory: " + errc.message()));
return cacheDir / (cache_key + "_cache.beve");
}
/**
* @brief Reads and deserializes data from a BEVE cache file.
* @tparam T The type of the object to deserialize from the cache. Must be Glaze-compatible.
* @param cache_key The unique identifier for the cache.
* @return Result containing the deserialized object of type T on success, or a DracError on failure.
*/
template <typename T>
fn ReadCache(const String& cache_key) -> Result<T> {
Result<fs::path> cachePathResult = GetCachePath(cache_key);
if (!cachePathResult)
return Err(cachePathResult.error());
const fs::path& cachePath = *cachePathResult;
if (std::error_code existsEc; !fs::exists(cachePath, existsEc) || existsEc) {
if (existsEc)
warn_log("Error checking existence of cache file '{}': {}", cachePath.string(), existsEc.message());
return Err(DracError(DracErrorCode::NotFound, "Cache file not found: " + cachePath.string()));
}
std::ifstream ifs(cachePath, std::ios::binary);
if (!ifs.is_open())
return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string()));
try {
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
ifs.close();
if (content.empty())
return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string()));
static_assert(std::is_default_constructible_v<T>, "Cache type T must be default constructible for Glaze.");
T result {};
if (glz::error_ctx glazeErr = glz::read_beve(result, content); glazeErr.ec != glz::error_code::none) {
return Err(DracError(
DracErrorCode::ParseError,
std::format(
"BEVE parse error reading cache '{}' (code {}): {}",
cachePath.string(),
static_cast<int>(glazeErr.ec),
glz::format_error(glazeErr, content)
)
));
}
return result;
} catch (const std::ios_base::failure& e) {
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what())
));
} catch (const Exception& e) {
return Err(DracError(
DracErrorCode::InternalError,
std::format("Standard exception reading cache file {}: {}", cachePath.string(), e.what())
));
} catch (...) {
return Err(DracError(DracErrorCode::Other, "Unknown error reading cache file: " + cachePath.string()));
}
}
/**
* @brief Serializes and writes data to a BEVE cache file safely.
* @tparam T The type of the object to serialize. Must be Glaze-compatible.
* @param cache_key The unique identifier for the cache.
* @param data The data object of type T to write to the cache.
* @return Result containing void on success, or a DracError on failure.
*/
template <typename T>
fn WriteCache(const String& cache_key, const T& data) -> Result<> {
Result<fs::path> cachePathResult = GetCachePath(cache_key);
if (!cachePathResult)
return Err(cachePathResult.error());
const fs::path& cachePath = *cachePathResult;
fs::path tempPath = cachePath;
tempPath += ".tmp";
try {
String binaryBuffer;
using DecayedT = std::decay_t<T>;
DecayedT dataToSerialize = data;
if (glz::error_ctx glazeErr = glz::write_beve(dataToSerialize, binaryBuffer); glazeErr) {
return Err(DracError(
DracErrorCode::ParseError,
std::format(
"BEVE serialization error writing cache for key '{}' (code {}): {}",
cache_key,
static_cast<int>(glazeErr.ec),
glz::format_error(glazeErr, binaryBuffer)
)
));
}
{
std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc);
if (!ofs.is_open())
return Err(DracError(DracErrorCode::IoError, "Failed to open temporary cache file: " + tempPath.string()));
ofs.write(binaryBuffer.data(), static_cast<isize>(binaryBuffer.size()));
if (!ofs) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DracError(DracErrorCode::IoError, "Failed to write to temporary cache file: " + tempPath.string()));
}
}
std::error_code renameEc;
fs::rename(tempPath, cachePath, renameEc);
if (renameEc) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DracError(
DracErrorCode::IoError,
std::format(
"Failed to replace cache file '{}' with temporary file '{}': {}",
cachePath.string(),
tempPath.string(),
renameEc.message()
)
));
}
return {};
} catch (const std::ios_base::failure& e) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what())
));
} catch (const Exception& e) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DracError(
DracErrorCode::InternalError,
std::format("Standard exception writing cache file {}: {}", tempPath.string(), e.what())
));
} catch (...) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DracError(DracErrorCode::Other, "Unknown error writing cache file: " + tempPath.string()));
}
}
} // namespace util::cache

View file

@ -1,9 +0,0 @@
#pragma once
// Fixes conflict in Windows with <windows.h>
#ifdef _WIN32
#undef ERROR
#endif // _WIN32
/// Macro alias for trailing return type functions.
#define fn auto

View file

@ -1,52 +0,0 @@
#pragma once
#ifdef _WIN32
#include <stdlib.h> // NOLINT(*-deprecated-headers)
#endif
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
namespace util::helpers {
using types::Result, types::String, types::CStr;
/**
* @brief Safely retrieves an environment variable.
* @param name The name of the environment variable to retrieve.
* @return A Result containing the value of the environment variable as a String,
* or an EnvError if an error occurred.
*/
[[nodiscard]] inline fn GetEnv(CStr name) -> Result<String> {
using error::DracError, error::DracErrorCode;
using types::Err;
#ifdef _WIN32
using types::i32, types::usize, types::UniquePointer;
char* rawPtr = nullptr;
usize bufferSize = 0;
// Use _dupenv_s to safely retrieve environment variables on Windows
const i32 err = _dupenv_s(&rawPtr, &bufferSize, name);
const UniquePointer<char, decltype(&free)> ptrManager(rawPtr, free);
if (err != 0)
return Err(DracError(DracErrorCode::PermissionDenied, "Failed to retrieve environment variable"));
if (!ptrManager)
return Err(DracError(DracErrorCode::NotFound, "Environment variable not found"));
return ptrManager.get();
#else
// Use std::getenv to retrieve environment variables on POSIX systems
const CStr value = std::getenv(name);
if (!value)
return Err(DracError(DracErrorCode::NotFound, "Environment variable not found"));
return value;
#endif
}
} // namespace util::helpers

View file

@ -1,138 +0,0 @@
#pragma once
#include <expected> // std::{unexpected, expected}
#include <matchit.hpp> // matchit::{match, is, or_, _}
#include <source_location> // std::source_location
#include <system_error> // std::error_code
#ifdef _WIN32
#include <guiddef.h> // GUID
#include <winerror.h> // error values
#include <winrt/base.h> // winrt::hresult_error
#else
#include <format> // std::format
#endif
#include "Util/Types.hpp"
namespace util {
namespace error {
using types::u8, types::i32, types::String, types::StringView, types::Exception;
/**
* @enum DracErrorCode
* @brief Error codes for general OS-level operations.
*/
enum class DracErrorCode : u8 {
ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime.
InternalError, ///< An error occurred within the application's OS abstraction code logic.
InvalidArgument, ///< An invalid argument was passed to a function or method.
IoError, ///< General I/O error (filesystem, pipes, etc.).
NetworkError, ///< A network-related error occurred (e.g., DNS resolution, connection failure).
NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found.
NotSupported, ///< The requested operation is not supported on this platform, version, or configuration.
Other, ///< A generic or unclassified error originating from the OS or an external library.
OutOfMemory, ///< The system ran out of memory or resources to complete the operation.
ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output).
PermissionDenied, ///< Insufficient permissions to perform the operation.
PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message).
Timeout, ///< An operation timed out (e.g., waiting for IPC reply).
};
/**
* @struct DracError
* @brief Holds structured information about an OS-level error.
*
* Used as the error type in Result for many os:: functions.
*/
struct DracError {
// ReSharper disable CppDFANotInitializedField
String message; ///< A descriptive error message, potentially including platform details.
std::source_location location; ///< The source location where the error occurred (file, line, function).
DracErrorCode code; ///< The general category of the error.
// ReSharper restore CppDFANotInitializedField
DracError(const DracErrorCode errc, String msg, const std::source_location& loc = std::source_location::current())
: message(std::move(msg)), location(loc), code(errc) {}
explicit DracError(const Exception& exc, const std::source_location& loc = std::source_location::current())
: message(exc.what()), location(loc), code(DracErrorCode::InternalError) {}
explicit DracError(const std::error_code& errc, const std::source_location& loc = std::source_location::current())
: message(errc.message()), location(loc) {
using namespace matchit;
using enum DracErrorCode;
using enum std::errc;
code = match(errc)(
is | or_(file_too_large, io_error) = IoError,
is | invalid_argument = InvalidArgument,
is | not_enough_memory = OutOfMemory,
is | or_(address_family_not_supported, operation_not_supported, not_supported) = NotSupported,
is | or_(network_unreachable, network_down, connection_refused) = NetworkError,
is | or_(no_such_file_or_directory, not_a_directory, is_a_directory, file_exists) = NotFound,
is | permission_denied = PermissionDenied,
is | timed_out = Timeout,
is | _ = errc.category() == std::generic_category() ? InternalError : PlatformSpecific
);
}
#ifdef _WIN32
explicit DracError(const winrt::hresult_error& err)
: message(winrt::to_string(err.message())) {
using namespace matchit;
using enum DracErrorCode;
fn fromWin32 = [](const types::u32 win32) -> HRESULT { return HRESULT_FROM_WIN32(win32); };
code = match(err.code())(
is | or_(E_ACCESSDENIED, fromWin32(ERROR_ACCESS_DENIED)) = PermissionDenied,
is | fromWin32(ERROR_FILE_NOT_FOUND) = NotFound,
is | fromWin32(ERROR_PATH_NOT_FOUND) = NotFound,
is | fromWin32(ERROR_SERVICE_NOT_FOUND) = NotFound,
is | fromWin32(ERROR_TIMEOUT) = Timeout,
is | fromWin32(ERROR_SEM_TIMEOUT) = Timeout,
is | fromWin32(ERROR_NOT_SUPPORTED) = NotSupported,
is | _ = PlatformSpecific
);
}
#else
DracError(const String& context, const std::source_location& loc = std::source_location::current())
: message(std::format("{}: {}", context, std::system_category().message(errno))), location(loc) {
using namespace matchit;
using enum DracErrorCode;
code = match(errno)(
is | EACCES = PermissionDenied,
is | ENOENT = NotFound,
is | ETIMEDOUT = Timeout,
is | ENOTSUP = NotSupported,
is | EIO = IoError,
is | or_(ECONNREFUSED, ENETDOWN, ENETUNREACH) = NetworkError,
is | _ = PlatformSpecific
);
}
#endif
};
} // namespace error
namespace types {
/**
* @typedef Result
* @brief Alias for std::expected<Tp, Er>. Represents a value that can either be
* a success value of type Tp or an error value of type Er.
* @tparam Tp The type of the success value.
* @tparam Er The type of the error value.
*/
template <typename Tp = void, typename Er = error::DracError>
using Result = std::expected<Tp, Er>;
/**
* @typedef Err
* @brief Alias for std::unexpected<Er>. Used to construct a Result in an error state.
* @tparam Er The type of the error value.
*/
template <typename Er = error::DracError>
using Err = std::unexpected<Er>;
} // namespace types
} // namespace util

View file

@ -1,325 +0,0 @@
#pragma once
#include <chrono> // std::chrono::{days, floor, seconds, system_clock}
#include <ctime> // localtime_r/s, strftime, time_t, tm
#include <filesystem> // std::filesystem::path
#include <format> // std::format
#include <ftxui/screen/color.hpp> // ftxui::Color
#include <utility> // std::forward
#ifdef __cpp_lib_print
#include <print> // std::print
#else
#include <iostream> // std::cout
#endif
#ifndef NDEBUG
#include <source_location> // std::source_location
#endif
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
namespace util::logging {
using types::usize, types::u8, types::i32, types::i64, types::CStr, types::String, types::StringView, types::Array,
types::Option, types::None, types::Mutex, types::LockGuard;
inline fn GetLogMutex() -> Mutex& {
static Mutex LogMutexInstance;
return LogMutexInstance;
}
struct LogLevelConst {
// clang-format off
static constexpr Array<StringView, 16> COLOR_CODE_LITERALS = {
"\033[38;5;0m", "\033[38;5;1m", "\033[38;5;2m", "\033[38;5;3m",
"\033[38;5;4m", "\033[38;5;5m", "\033[38;5;6m", "\033[38;5;7m",
"\033[38;5;8m", "\033[38;5;9m", "\033[38;5;10m", "\033[38;5;11m",
"\033[38;5;12m", "\033[38;5;13m", "\033[38;5;14m", "\033[38;5;15m",
};
// clang-format on
static constexpr const char* RESET_CODE = "\033[0m";
static constexpr const char* BOLD_START = "\033[1m";
static constexpr const char* BOLD_END = "\033[22m";
static constexpr const char* ITALIC_START = "\033[3m";
static constexpr const char* ITALIC_END = "\033[23m";
static constexpr StringView DEBUG_STR = "DEBUG";
static constexpr StringView INFO_STR = "INFO ";
static constexpr StringView WARN_STR = "WARN ";
static constexpr StringView ERROR_STR = "ERROR";
static constexpr ftxui::Color::Palette16 DEBUG_COLOR = ftxui::Color::Palette16::Cyan;
static constexpr ftxui::Color::Palette16 INFO_COLOR = ftxui::Color::Palette16::Green;
static constexpr ftxui::Color::Palette16 WARN_COLOR = ftxui::Color::Palette16::Yellow;
static constexpr ftxui::Color::Palette16 ERROR_COLOR = ftxui::Color::Palette16::Red;
static constexpr ftxui::Color::Palette16 DEBUG_INFO_COLOR = ftxui::Color::Palette16::GrayLight;
static constexpr CStr TIMESTAMP_FORMAT = "%X";
static constexpr CStr LOG_FORMAT = "{} {} {}";
#ifndef NDEBUG
static constexpr CStr DEBUG_INFO_FORMAT = "{}{}{}\n";
static constexpr CStr FILE_LINE_FORMAT = "{}:{}";
static constexpr CStr DEBUG_LINE_PREFIX = " ╰── ";
#endif
};
/**
* @enum LogLevel
* @brief Represents different log levels.
*/
enum class LogLevel : u8 {
Debug,
Info,
Warn,
Error,
};
inline fn GetRuntimeLogLevel() -> LogLevel& {
static LogLevel RuntimeLogLevel = LogLevel::Info;
return RuntimeLogLevel;
}
inline fn SetRuntimeLogLevel(const LogLevel level) {
GetRuntimeLogLevel() = level;
}
/**
* @brief Directly applies ANSI color codes to text
* @param text The text to colorize
* @param color The FTXUI color
* @return Styled string with ANSI codes
*/
inline fn Colorize(const StringView text, const ftxui::Color::Palette16& color) -> String {
return std::format("{}{}{}", LogLevelConst::COLOR_CODE_LITERALS.at(color), text, LogLevelConst::RESET_CODE);
}
/**
* @brief Make text bold with ANSI codes
* @param text The text to make bold
* @return Bold text
*/
inline fn Bold(const StringView text) -> String {
return std::format("{}{}{}", LogLevelConst::BOLD_START, text, LogLevelConst::BOLD_END);
}
/**
* @brief Make text italic with ANSI codes
* @param text The text to make italic
* @return Italic text
*/
inline fn Italic(const StringView text) -> String {
return std::format("{}{}{}", LogLevelConst::ITALIC_START, text, LogLevelConst::ITALIC_END);
}
/**
* @brief Returns the pre-formatted and styled log level strings.
* @note Uses function-local static for lazy initialization to avoid
* static initialization order issues and CERT-ERR58-CPP warnings.
*/
inline fn GetLevelInfo() -> const Array<String, 4>& {
static const Array<String, 4> LEVEL_INFO_INSTANCE = {
Bold(Colorize(LogLevelConst::DEBUG_STR, LogLevelConst::DEBUG_COLOR)),
Bold(Colorize(LogLevelConst::INFO_STR, LogLevelConst::INFO_COLOR)),
Bold(Colorize(LogLevelConst::WARN_STR, LogLevelConst::WARN_COLOR)),
Bold(Colorize(LogLevelConst::ERROR_STR, LogLevelConst::ERROR_COLOR)),
};
return LEVEL_INFO_INSTANCE;
}
/**
* @brief Returns FTXUI color representation for a log level
* @param level The log level
* @return FTXUI color code
*/
constexpr fn GetLevelColor(const LogLevel level) -> ftxui::Color::Palette16 {
using namespace matchit;
using enum LogLevel;
return match(level)(
is | Debug = LogLevelConst::DEBUG_COLOR,
is | Info = LogLevelConst::INFO_COLOR,
is | Warn = LogLevelConst::WARN_COLOR,
is | Error = LogLevelConst::ERROR_COLOR
);
}
/**
* @brief Returns string representation of a log level
* @param level The log level
* @return String representation
*/
constexpr fn GetLevelString(const LogLevel level) -> StringView {
using namespace matchit;
using enum LogLevel;
return match(level)(
is | Debug = LogLevelConst::DEBUG_STR,
is | Info = LogLevelConst::INFO_STR,
is | Warn = LogLevelConst::WARN_STR,
is | Error = LogLevelConst::ERROR_STR
);
}
// ReSharper disable once CppDoxygenUnresolvedReference
/**
* @brief Logs a message with the specified log level, source location, and format string.
* @tparam Args Parameter pack for format arguments.
* @param level The log level (DEBUG, INFO, WARN, ERROR).
* \ifnot NDEBUG
* @param loc The source location of the log message (only in Debug builds).
* \endif
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn LogImpl(
const LogLevel level,
#ifndef NDEBUG
// ReSharper disable once CppDoxygenUndocumentedParameter
const std::source_location& loc,
#endif
std::format_string<Args...> fmt,
Args&&... args
) {
using namespace std::chrono;
using std::filesystem::path;
if (level < GetRuntimeLogLevel())
return;
const LockGuard lock(GetLogMutex());
const auto nowTp = system_clock::now();
const std::time_t nowTt = system_clock::to_time_t(nowTp);
std::tm localTm {};
String timestamp;
#ifdef _WIN32
if (localtime_s(&localTm, &nowTt) == 0) {
#else
if (localtime_r(&nowTt, &localTm) != nullptr) {
#endif
Array<char, 64> timeBuffer {};
const usize formattedTime =
std::strftime(timeBuffer.data(), sizeof(timeBuffer), LogLevelConst::TIMESTAMP_FORMAT, &localTm);
if (formattedTime > 0) {
timestamp = timeBuffer.data();
} else {
try {
timestamp = std::format("{:%X}", nowTp);
} catch ([[maybe_unused]] const std::format_error& fmtErr) { timestamp = "??:??:??"; }
}
} else
timestamp = "??:??:??";
const String message = std::format(fmt, std::forward<Args>(args)...);
const String mainLogLine = std::format(
LogLevelConst::LOG_FORMAT,
Colorize("[" + timestamp + "]", LogLevelConst::DEBUG_INFO_COLOR),
GetLevelInfo().at(static_cast<usize>(level)),
message
);
#ifdef __cpp_lib_print
std::print("{}", mainLogLine);
#else
std::cout << mainLogLine;
#endif
#ifndef NDEBUG
const String fileLine =
std::format(LogLevelConst::FILE_LINE_FORMAT, path(loc.file_name()).lexically_normal().string(), loc.line());
const String fullDebugLine = std::format("{}{}", LogLevelConst::DEBUG_LINE_PREFIX, fileLine);
#ifdef __cpp_lib_print
std::print("\n{}", Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR)));
#else
std::cout << '\n'
<< Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR));
#endif
#endif
#ifdef __cpp_lib_print
std::println("{}", LogLevelConst::RESET_CODE);
#else
std::cout << LogLevelConst::RESET_CODE << '\n';
#endif
}
template <typename ErrorType>
fn LogError(const LogLevel level, const ErrorType& error_obj) {
using DecayedErrorType = std::decay_t<ErrorType>;
#ifndef NDEBUG
std::source_location logLocation;
#endif
String errorMessagePart;
if constexpr (std::is_same_v<DecayedErrorType, error::DracError>) {
#ifndef NDEBUG
logLocation = error_obj.location;
#endif
errorMessagePart = error_obj.message;
} else {
#ifndef NDEBUG
logLocation = std::source_location::current();
#endif
if constexpr (std::is_base_of_v<std::exception, DecayedErrorType>)
errorMessagePart = error_obj.what();
else if constexpr (requires { error_obj.message; })
errorMessagePart = error_obj.message;
else
errorMessagePart = "Unknown error type logged";
}
#ifndef NDEBUG
LogImpl(level, logLocation, "{}", errorMessagePart);
#else
LogImpl(level, "{}", errorMessagePart);
#endif
}
#ifndef NDEBUG
#define debug_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Debug, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define debug_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Debug, error_obj);
#else
#define debug_log(...) ((void)0)
#define debug_at(...) ((void)0)
#endif
#define info_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Info, error_obj);
#define warn_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Warn, error_obj);
#define error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj);
#ifdef NDEBUG
#define info_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Info, fmt __VA_OPT__(, ) __VA_ARGS__)
#define warn_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Warn, fmt __VA_OPT__(, ) __VA_ARGS__)
#define error_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Error, fmt __VA_OPT__(, ) __VA_ARGS__)
#else
#define info_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Info, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define warn_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Warn, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define error_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Error, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#endif
} // namespace util::logging

View file

@ -1,137 +0,0 @@
#pragma once
#include <array> // std::array (Array)
#include <future> // std::future (Future)
#include <map> // std::map (Map)
#include <memory> // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer)
#include <mutex> // std::mutex and std::lock_guard (Mutex, LockGuard)
#include <optional> // std::optional (Option)
#include <string> // std::string (String, StringView)
#include <string_view> // std::string_view (StringView)
#include <utility> // std::pair (Pair)
#include <vector> // std::vector (Vec)
namespace util::types {
using u8 = std::uint8_t; ///< 8-bit unsigned integer.
using u16 = std::uint16_t; ///< 16-bit unsigned integer.
using u32 = std::uint32_t; ///< 32-bit unsigned integer.
using u64 = std::uint64_t; ///< 64-bit unsigned integer.
using i8 = std::int8_t; ///< 8-bit signed integer.
using i16 = std::int16_t; ///< 16-bit signed integer.
using i32 = std::int32_t; ///< 32-bit signed integer.
using i64 = std::int64_t; ///< 64-bit signed integer.
using f32 = float; ///< 32-bit floating-point number.
using f64 = double; ///< 64-bit floating-point number.
using usize = std::size_t; ///< Unsigned size type (result of sizeof).
using isize = std::ptrdiff_t; ///< Signed size type (result of pointer subtraction).
using String = std::string; ///< Owning, mutable string.
using StringView = std::string_view; ///< Non-owning view of a string.
using CStr = const char*; ///< Pointer to a null-terminated C-style string.
using Exception = std::exception; ///< Standard exception type.
using Mutex = std::mutex; ///< Mutex type for synchronization.
using LockGuard = std::lock_guard<Mutex>; ///< RAII-style lock guard for mutexes.
inline constexpr std::nullopt_t None = std::nullopt; ///< Represents an empty optional value.
/**
* @typedef Option
* @brief Alias for std::optional<Tp>. Represents a value that may or may not be present.
* @tparam Tp The type of the potential value.
*/
template <typename Tp>
using Option = std::optional<Tp>;
/**
* @typedef Array
* @brief Alias for std::array<Tp, sz>. Represents a fixed-size array.
* @tparam Tp The element type.
* @tparam sz The size of the array.
*/
template <typename Tp, usize sz>
using Array = std::array<Tp, sz>;
/**
* @typedef Vec
* @brief Alias for std::vector<Tp>. Represents a dynamic-size array (vector).
* @tparam Tp The element type.
*/
template <typename Tp>
using Vec = std::vector<Tp>;
/**
* @typedef Pair
* @brief Alias for std::pair<T1, T2>. Represents a pair of values.
* @tparam T1 The type of the first element.
* @tparam T2 The type of the second element.
*/
template <typename T1, typename T2>
using Pair = std::pair<T1, T2>;
/**
* @typedef Map
* @brief Alias for std::map<Key, Val>. Represents an ordered map (dictionary).
* @tparam Key The key type.
* @tparam Val The value type.
*/
template <typename Key, typename Val>
using Map = std::map<Key, Val>;
/**
* @typedef SharedPointer
* @brief Alias for std::shared_ptr<Tp>. Manages shared ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
*/
template <typename Tp>
using SharedPointer = std::shared_ptr<Tp>;
/**
* @typedef UniquePointer
* @brief Alias for std::unique_ptr<Tp, Dp>. Manages unique ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
* @tparam Dp The deleter type (defaults to std::default_delete<Tp>).
*/
template <typename Tp, typename Dp = std::default_delete<Tp>>
using UniquePointer = std::unique_ptr<Tp, Dp>;
/**
* @typedef Future
* @brief Alias for std::future<Tp>. Represents a value that will be available in the future.
* @tparam Tp The type of the value.
*/
template <typename Tp>
using Future = std::future<Tp>;
/**
* @struct DiskSpace
* @brief Represents disk usage information.
*
* Used as the success type for os::GetDiskUsage.
*/
struct DiskSpace {
u64 usedBytes; ///< Currently used disk space in bytes.
u64 totalBytes; ///< Total disk space in bytes.
};
/**
* @struct MediaInfo
* @brief Holds structured metadata about currently playing media.
*
* Used as the success type for os::GetNowPlaying.
* Using Option<> for fields that might not always be available.
*/
struct MediaInfo {
Option<String> title; ///< Track title.
Option<String> artist; ///< Track artist(s).
MediaInfo() = default;
MediaInfo(Option<String> title, Option<String> artist)
: title(std::move(title)), artist(std::move(artist)) {}
};
} // namespace util::types

View file

@ -1,526 +0,0 @@
#pragma once
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
// clang-format off
#include <cstring>
#include <dbus/dbus.h> // DBus Library
#include <utility> // std::exchange, std::forward
#include <format> // std::format
#include <type_traits> // std::is_convertible_v
#include "Util/Definitions.hpp"
#include "Util/Error.hpp"
#include "Util/Types.hpp"
// clang-format on
namespace DBus {
using util::error::DracError, util::error::DracErrorCode;
using util::types::Option, util::types::Result, util::types::Err, util::types::String, util::types::i32,
util::types::u32, util::types::None;
/**
* @brief RAII wrapper for DBusError. Automatically initializes and frees the error.
*/
class Error {
DBusError m_err {}; ///< The D-Bus error object
bool m_isInitialized = false; ///< Flag indicating if the error is initialized
public:
/**
* @brief Constructor
*
* Initializes the D-Bus error object.
*/
Error()
: m_isInitialized(true) {
dbus_error_init(&m_err);
}
/**
* @brief Destructor
*
* Frees the D-Bus error object if it was initialized.
*/
~Error() {
if (m_isInitialized)
dbus_error_free(&m_err);
}
// Non-copyable
Error(const Error&) = delete;
fn operator=(const Error&)->Error& = delete;
/**
* @brief Move constructor
*
* Transfers ownership of the D-Bus error object.
*
* @param other The other Error object to move from
*/
Error(Error&& other) noexcept
: m_err(other.m_err), m_isInitialized(other.m_isInitialized) {
other.m_isInitialized = false;
dbus_error_init(&other.m_err);
}
/**
* @brief Move assignment operator
*
* Transfers ownership of the D-Bus error object.
*
* @param other The other Error object to move from
* @return A reference to this object
*/
fn operator=(Error&& other) noexcept -> Error& {
if (this != &other) {
if (m_isInitialized)
dbus_error_free(&m_err);
m_err = other.m_err;
m_isInitialized = other.m_isInitialized;
other.m_isInitialized = false;
dbus_error_init(&other.m_err);
}
return *this;
}
/**
* @brief Checks if the error is set.
* @return True if the error is set and initialized, false otherwise.
*/
[[nodiscard]] fn isSet() const -> bool {
return m_isInitialized && dbus_error_is_set(&m_err);
}
/**
* @brief Gets the error message.
* @return The error message string, or "" if not set or not initialized.
*/
[[nodiscard]] fn message() const -> const char* {
return isSet() ? m_err.message : "";
}
/**
* @brief Gets the error name.
* @return The error name string (e.g., "org.freedesktop.DBus.Error.Failed"), or "" if not set or not initialized.
*/
[[nodiscard]] fn name() const -> const char* {
return isSet() ? m_err.name : "";
}
/**
* @brief Gets a pointer to the underlying DBusError. Use with caution.
* @return Pointer to the DBusError struct.
*/
[[nodiscard]] fn get() -> DBusError* {
return &m_err;
}
/**
* @brief Gets a const pointer to the underlying DBusError.
* @return Const pointer to the DBusError struct.
*/
[[nodiscard]] fn get() const -> const DBusError* {
return &m_err;
}
/**
* @brief Converts the D-Bus error to a DracError.
* @param code The DracErrorCode to use if the D-Bus error is set.
* @return A DracError representing the D-Bus error, or an internal error if called when no D-Bus error is set.
*/
[[nodiscard]] fn toDracError(const DracErrorCode code = DracErrorCode::PlatformSpecific) const -> DracError {
if (isSet())
return { code, std::format("D-Bus Error: {} ({})", message(), name()) };
return { DracErrorCode::InternalError, "Attempted to convert non-set ErrorGuard" };
}
};
/**
* @brief RAII wrapper for DBusMessageIter. Automatically frees the iterator.
*
* This class provides a convenient way to manage the lifetime of a D-Bus message iterator.
*/
class MessageIter {
DBusMessageIter m_iter {}; ///< The D-Bus message iterator
bool m_isValid = false; ///< Flag indicating if the iterator is valid
// Allows the Message class to access private members of this class.
friend class Message;
/**
* @brief Constructor
*
* Initializes the D-Bus message iterator.
*
* @param iter The D-Bus message iterator to wrap
* @param isValid Flag indicating if the iterator is valid
*/
explicit MessageIter(const DBusMessageIter& iter, const bool isValid)
: m_iter(iter), m_isValid(isValid) {}
/**
* @brief Destructor
*
* Frees the D-Bus message iterator if it was initialized.
*
* @param value Pointer to the value to be freed
*
* @note This function is unsafe and should not be called directly.
*/
fn getBasic(void* value) -> void {
if (m_isValid)
dbus_message_iter_get_basic(&m_iter, value);
}
public:
// Non-copyable
MessageIter(const MessageIter&) = delete;
fn operator=(const MessageIter&)->MessageIter& = delete;
// Non-movable
MessageIter(MessageIter&&) = delete;
fn operator=(MessageIter&&)->MessageIter& = delete;
// Destructor
~MessageIter() = default;
/**
* @brief Checks if the iterator is validly initialized.
*/
[[nodiscard]] fn isValid() const -> bool {
return m_isValid;
}
/**
* @brief Gets the D-Bus type code of the current argument.
* @return The D-Bus type code, or DBUS_TYPE_INVALID otherwise.
*/
[[nodiscard]] fn getArgType() -> int {
return m_isValid ? dbus_message_iter_get_arg_type(&m_iter) : DBUS_TYPE_INVALID;
}
/**
* @brief Gets the element type of the container pointed to by the iterator.
* Only valid if the iterator points to an ARRAY or VARIANT.
* @return The D-Bus type code of the elements, or DBUS_TYPE_INVALID otherwise.
*/
[[nodiscard]] fn getElementType() -> int {
return m_isValid ? dbus_message_iter_get_element_type(&m_iter) : DBUS_TYPE_INVALID;
}
/**
* @brief Advances the iterator to the next argument.
* @return True if successful (moved to a next element), false if at the end or iterator is invalid.
*/
fn next() -> bool {
return m_isValid && dbus_message_iter_next(&m_iter);
}
/**
* @brief Recurses into a container-type argument (e.g., array, struct, variant).
* @return A new MessageIterGuard for the sub-container. The returned iterator might be invalid
* if the current element is not a container or the main iterator is invalid.
*/
[[nodiscard]] fn recurse() -> MessageIter {
if (!m_isValid)
return MessageIter({}, false);
DBusMessageIter subIter;
dbus_message_iter_recurse(&m_iter, &subIter);
return MessageIter(subIter, true);
}
/**
* @brief Helper to safely get a string argument from the iterator.
* @return An Option containing the string value if the current arg is a valid string, or None otherwise.
*/
[[nodiscard]] fn getString() -> Option<String> {
if (m_isValid && getArgType() == DBUS_TYPE_STRING) {
const char* strPtr = nullptr;
// ReSharper disable once CppRedundantCastExpression
getBasic(static_cast<void*>(&strPtr));
if (strPtr)
return String(strPtr);
}
return None;
}
};
/**
* @brief RAII wrapper for DBusMessage. Automatically unrefs.
*/
class Message {
DBusMessage* m_msg = nullptr; ///< The D-Bus message object
public:
/**
* @brief Constructor
*
* Initializes the D-Bus message object.
*
* @param msg The D-Bus message object to wrap
*/
explicit Message(DBusMessage* msg = nullptr)
: m_msg(msg) {}
/**
* @brief Destructor
*
* Frees the D-Bus message object if it was initialized.
*/
~Message() {
if (m_msg)
dbus_message_unref(m_msg);
}
// Non-copyable
Message(const Message&) = delete;
fn operator=(const Message&)->Message& = delete;
/**
* @brief Move constructor
*
* Transfers ownership of the D-Bus message object.
*
* @param other The other Message object to move from
*/
Message(Message&& other) noexcept
: m_msg(std::exchange(other.m_msg, nullptr)) {}
/**
* @brief Move assignment operator
*
* Transfers ownership of the D-Bus message object.
*
* @param other The other Message object to move from
* @return A reference to this object
*/
fn operator=(Message&& other) noexcept -> Message& {
if (this != &other) {
if (m_msg)
dbus_message_unref(m_msg);
m_msg = std::exchange(other.m_msg, nullptr);
}
return *this;
}
/**
* @brief Gets the underlying DBusMessage pointer. Use with caution.
* @return The raw DBusMessage pointer, or nullptr if not holding a message.
*/
[[nodiscard]] fn get() const -> DBusMessage* {
return m_msg;
}
/**
* @brief Initializes a message iterator for reading arguments from this message.
* @return A MessageIterGuard. Check iter.isValid() before use.
*/
[[nodiscard]] fn iterInit() const -> MessageIter {
if (!m_msg)
return MessageIter({}, false);
DBusMessageIter iter;
const bool isValid = dbus_message_iter_init(m_msg, &iter);
return MessageIter(iter, isValid);
}
/**
* @brief Appends arguments of basic types to the message.
* @tparam Args Types of the arguments to append.
* @param args The arguments to append.
* @return True if all arguments were appended successfully, false otherwise (e.g., allocation error).
*/
template <typename... Args>
[[nodiscard]] fn appendArgs(Args&&... args) -> bool {
if (!m_msg)
return false;
DBusMessageIter iter;
dbus_message_iter_init_append(m_msg, &iter);
bool success = true;
((success = success && appendArgInternal(iter, std::forward<Args>(args))), ...); // NOLINT
return success;
}
/**
* @brief Creates a new D-Bus method call message.
* @param destination Service name (e.g., "org.freedesktop.Notifications"). Can be null.
* @param path Object path (e.g., "/org/freedesktop/Notifications"). Must not be null.
* @param interface Interface name (e.g., "org.freedesktop.Notifications"). Can be null.
* @param method Method name (e.g., "Notify"). Must not be null.
* @return Result containing a MessageGuard on success, or DracError on failure.
*/
static fn newMethodCall(const char* destination, const char* path, const char* interface, const char* method)
-> Result<Message> {
DBusMessage* rawMsg = dbus_message_new_method_call(destination, path, interface, method);
if (!rawMsg)
return Err(DracError(DracErrorCode::OutOfMemory, "dbus_message_new_method_call failed (allocation failed?)"));
return Message(rawMsg);
}
private:
/**
* @brief Appends a single argument to the message.
* @tparam T Type of the argument to append.
* @param iter The D-Bus message iterator.
* @param arg The argument to append.
* @return True if the argument was appended successfully, false otherwise (e.g., allocation error).
*/
template <typename T>
fn appendArgInternal(DBusMessageIter& iter, T&& arg) -> bool {
using DecayedT = std::decay_t<T>;
if constexpr (std::is_convertible_v<DecayedT, const char*>) {
const char* valuePtr = static_cast<const char*>(std::forward<T>(arg));
return dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, static_cast<const void*>(&valuePtr));
} else {
static_assert(!sizeof(T*), "Unsupported type passed to appendArgs");
return false;
}
}
};
/**
* @brief RAII wrapper for DBusConnection. Automatically unrefs the connection.
*
* This class provides a convenient way to manage the lifetime of a D-Bus connection.
*/
class Connection {
DBusConnection* m_conn = nullptr; ///< The D-Bus connection object
public:
/**
* @brief Constructor
*
* Initializes the D-Bus connection object.
*
* @param conn The D-Bus connection object to wrap
*/
explicit Connection(DBusConnection* conn = nullptr)
: m_conn(conn) {}
/**
* @brief Destructor
*
* Frees the D-Bus connection object if it was initialized.
*/
~Connection() {
if (m_conn)
dbus_connection_unref(m_conn);
}
// Non-copyable
Connection(const Connection&) = delete;
fn operator=(const Connection&)->Connection& = delete;
/**
* @brief Move constructor
*
* Transfers ownership of the D-Bus connection object.
*
* @param other The other Connection object to move from
*/
Connection(Connection&& other) noexcept
: m_conn(std::exchange(other.m_conn, nullptr)) {}
/**
* @brief Move assignment operator
*
* Transfers ownership of the D-Bus connection object.
*
* @param other The other Connection object to move from
* @return A reference to this object
*/
fn operator=(Connection&& other) noexcept -> Connection& {
if (this != &other) {
if (m_conn)
dbus_connection_unref(m_conn);
m_conn = std::exchange(other.m_conn, nullptr);
}
return *this;
}
/**
* @brief Gets the underlying DBusConnection pointer. Use with caution.
* @return The raw DBusConnection pointer, or nullptr if not holding a connection.
*/
[[nodiscard]] fn get() const -> DBusConnection* {
return m_conn;
}
/**
* @brief Sends a message and waits for a reply, blocking execution.
* @param message The D-Bus message guard to send.
* @param timeout_milliseconds Timeout duration in milliseconds.
* @return Result containing the reply MessageGuard on success, or DracError on failure.
*/
[[nodiscard]] fn sendWithReplyAndBlock(const Message& message, const i32 timeout_milliseconds = 1000) const
-> Result<Message> {
if (!m_conn || !message.get())
return Err(
DracError(DracErrorCode::InvalidArgument, "Invalid connection or message provided to sendWithReplyAndBlock")
);
Error err;
DBusMessage* rawReply =
dbus_connection_send_with_reply_and_block(m_conn, message.get(), timeout_milliseconds, err.get());
if (err.isSet()) {
if (const char* errName = err.name()) {
if (strcmp(errName, DBUS_ERROR_TIMEOUT) == 0 || strcmp(errName, DBUS_ERROR_NO_REPLY) == 0)
return Err(err.toDracError(DracErrorCode::Timeout));
if (strcmp(errName, DBUS_ERROR_SERVICE_UNKNOWN) == 0)
return Err(err.toDracError(DracErrorCode::NotFound));
if (strcmp(errName, DBUS_ERROR_ACCESS_DENIED) == 0)
return Err(err.toDracError(DracErrorCode::PermissionDenied));
}
return Err(err.toDracError(DracErrorCode::PlatformSpecific));
}
if (!rawReply)
return Err(DracError(
DracErrorCode::ApiUnavailable,
"dbus_connection_send_with_reply_and_block returned null without setting error (likely timeout or "
"disconnected)"
));
return Message(rawReply);
}
/**
* @brief Connects to a D-Bus bus type (Session or System).
* @param bus_type The type of bus (DBUS_BUS_SESSION or DBUS_BUS_SYSTEM).
* @return Result containing a ConnectionGuard on success, or DracError on failure.
*/
static fn busGet(const DBusBusType bus_type) -> Result<Connection> {
Error err;
DBusConnection* rawConn = dbus_bus_get(bus_type, err.get());
if (err.isSet())
return Err(err.toDracError(DracErrorCode::ApiUnavailable));
if (!rawConn)
return Err(DracError(DracErrorCode::ApiUnavailable, "dbus_bus_get returned null without setting error"));
return Connection(rawConn);
}
};
} // namespace DBus
#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__

View file

@ -1,179 +0,0 @@
#pragma once
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
// clang-format off
#include <wayland-client.h> // Wayland client library
#include "Util/Definitions.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
// clang-format on
namespace Wayland {
using util::types::i32, util::types::CStr, util::types::None;
using Display = wl_display;
/**
* @brief Connect to a Wayland display
*
* This function establishes a connection to a Wayland display. It takes a
* display name as an argument.
*
* @param name The name of the display to connect to (or nullptr for default)
* @return A pointer to the Wayland display object
*/
inline fn Connect(CStr name) -> Display* {
return wl_display_connect(name);
}
/**
* @brief Disconnect from a Wayland display
*
* This function disconnects from a Wayland display.
*
* @param display The Wayland display object to disconnect from
* @return void
*/
inline fn Disconnect(Display* display) -> void {
wl_display_disconnect(display);
}
/**
* @brief Get the file descriptor for a Wayland display
*
* This function retrieves the file descriptor for a Wayland display.
*
* @param display The Wayland display object
* @return The file descriptor for the Wayland display
*/
inline fn GetFd(Display* display) -> i32 {
return wl_display_get_fd(display);
}
/**
* @brief RAII wrapper for Wayland display connections
*
* This class manages the connection to a Wayland display. It automatically
* handles resource acquisition and cleanup.
*/
class DisplayGuard {
Display* m_display; ///< The Wayland display object
public:
/**
* @brief Constructor
*
* This constructor sets up a custom logging handler for Wayland and
* establishes a connection to the Wayland display.
*/
DisplayGuard() {
wl_log_set_handler_client([](CStr fmt, va_list args) -> void {
using util::types::i32, util::types::StringView;
va_list argsCopy;
va_copy(argsCopy, args);
i32 size = std::vsnprintf(nullptr, 0, fmt, argsCopy);
va_end(argsCopy);
if (size < 0) {
error_log("Wayland: Internal log formatting error (vsnprintf size check failed).");
return;
}
std::vector<char> buffer(static_cast<size_t>(size) + 1);
i32 writeSize = std::vsnprintf(buffer.data(), buffer.size(), fmt, args);
if (writeSize < 0 || writeSize >= static_cast<int>(buffer.size())) {
error_log("Wayland: Internal log formatting error (vsnprintf write failed).");
return;
}
StringView msgView(buffer.data(), static_cast<size_t>(writeSize));
if (!msgView.empty() && msgView.back() == '\n')
msgView.remove_suffix(1);
debug_log("Wayland {}", msgView);
});
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) - needs to come after wl_log_set_handler_client
m_display = Connect(nullptr);
}
/**
* @brief Destructor
*
* This destructor disconnects from the Wayland display if it is valid.
*/
~DisplayGuard() {
if (m_display)
Disconnect(m_display);
}
// Non-copyable
DisplayGuard(const DisplayGuard&) = delete;
fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable
DisplayGuard(DisplayGuard&& other) noexcept
: m_display(std::exchange(other.m_display, nullptr)) {}
/**
* @brief Move assignment operator
*
* This operator transfers ownership of the Wayland display connection.
*
* @param other The other DisplayGuard object to move from
* @return A reference to this object
*/
fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) {
if (m_display)
Disconnect(m_display);
m_display = std::exchange(other.m_display, nullptr);
}
return *this;
}
/**
* @brief Check if the display guard is valid
*
* This function checks if the display guard is valid (i.e., if it holds a
* valid Wayland display connection).
*
* @return True if the display guard is valid, false otherwise
*/
[[nodiscard]] explicit operator bool() const {
return m_display != nullptr;
}
/**
* @brief Get the Wayland display connection
*
* This function retrieves the underlying Wayland display connection.
*
* @return The Wayland display connection
*/
[[nodiscard]] fn get() const -> Display* {
return m_display;
}
/**
* @brief Get the file descriptor for the Wayland display
*
* This function retrieves the file descriptor for the Wayland display.
*
* @return The file descriptor for the Wayland display
*/
[[nodiscard]] fn fd() const -> i32 {
return GetFd(m_display);
}
};
} // namespace Wayland
#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__

View file

@ -1,339 +0,0 @@
#pragma once
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
// clang-format off
#include <xcb/xcb.h> // XCB library
#include "Util/Definitions.hpp"
#include "Util/Types.hpp"
// clang-format on
namespace XCB {
using util::types::u8, util::types::u16, util::types::i32, util::types::u32, util::types::CStr, util::types::None;
using Connection = xcb_connection_t;
using Setup = xcb_setup_t;
using Screen = xcb_screen_t;
using Window = xcb_window_t;
using Atom = xcb_atom_t;
using GenericError = xcb_generic_error_t;
using IntAtomCookie = xcb_intern_atom_cookie_t;
using IntAtomReply = xcb_intern_atom_reply_t;
using GetPropCookie = xcb_get_property_cookie_t;
using GetPropReply = xcb_get_property_reply_t;
constexpr Atom ATOM_WINDOW = XCB_ATOM_WINDOW; ///< Window atom
/**
* @brief Enum representing different types of connection errors
*
* This enum defines the possible types of errors that can occur when
* establishing or maintaining an XCB connection. Each error type
* corresponds to a specific error code defined in the XCB library.
*/
enum ConnError : u8 {
Generic = XCB_CONN_ERROR, ///< Generic connection error
ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED, ///< Extension not supported
MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT, ///< Memory insufficient
ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED, ///< Request length exceed
ParseErr = XCB_CONN_CLOSED_PARSE_ERR, ///< Parse error
InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN, ///< Invalid screen
FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED, ///< FD passing failed
};
/**
* @brief Connect to an XCB display
*
* This function establishes a connection to an XCB display. It takes a
* display name and a pointer to an integer that will store the screen
* number.
*
* @param displayname The name of the display to connect to
* @param screenp Pointer to an integer that will store the screen number
* @return A pointer to the connection object
*/
inline fn Connect(CStr displayname, i32* screenp) -> Connection* {
return xcb_connect(displayname, screenp);
}
/**
* @brief Disconnect from an XCB display
*
* This function disconnects from an XCB display. It takes a pointer to
* the connection object.
*
* @param conn The connection object to disconnect from
*/
inline fn Disconnect(Connection* conn) -> void {
xcb_disconnect(conn);
}
/**
* @brief Check if a connection has an error
*
* This function checks if a connection has an error. It takes a pointer
* to the connection object.
*
* @param conn The connection object to check
* @return 1 if the connection has an error, 0 otherwise
*/
inline fn ConnectionHasError(Connection* conn) -> i32 {
return xcb_connection_has_error(conn);
}
/**
* @brief Intern an atom
*
* This function interns an atom. It takes a connection object, a flag
*
* @param conn The connection object to intern the atom on
* @param only_if_exists The flag to check if the atom exists
* @param name_len The length of the atom name
* @param name The name of the atom
* @return The cookie for the atom
*/
inline fn InternAtom(Connection* conn, const u8 only_if_exists, const u16 name_len, CStr name) -> IntAtomCookie {
return xcb_intern_atom(conn, only_if_exists, name_len, name);
}
/**
* @brief Get the reply for an interned atom
*
* This function gets the reply for an interned atom. It takes a connection
* object, a cookie, and a pointer to a generic error.
*
* @param conn The connection object
* @param cookie The cookie for the atom
* @param err The pointer to the generic error
* @return The reply for the atom
*/
inline fn InternAtomReply(Connection* conn, const IntAtomCookie cookie, GenericError** err) -> IntAtomReply* {
return xcb_intern_atom_reply(conn, cookie, err);
}
/**
* @brief Get a property
*
* This function gets a property. It takes a connection object, a flag,
* a window, a property, a type, a long offset, and a long length.
*
* @param conn The connection object
* @param _delete The flag
* @param window The window
* @param property The property
* @param type The type
*/
inline fn GetProperty(
Connection* conn,
const u8 _delete,
const Window window,
const Atom property,
const Atom type,
const u32 long_offset,
const u32 long_length
) -> GetPropCookie {
return xcb_get_property(conn, _delete, window, property, type, long_offset, long_length);
}
/**
* @brief Get the reply for a property
*
* This function gets the reply for a property. It takes a connection
* object, a cookie, and a pointer to a generic error.
*
* @param conn The connection object
* @param cookie The cookie for the property
* @param err The pointer to the generic error
* @return The reply for the property
*/
inline fn GetPropertyReply(Connection* conn, const GetPropCookie cookie, GenericError** err) -> GetPropReply* {
return xcb_get_property_reply(conn, cookie, err);
}
/**
* @brief Get the value length for a property
*
* @param reply The reply for the property
* @return The value length for the property
*/
inline fn GetPropertyValueLength(const GetPropReply* reply) -> i32 {
return xcb_get_property_value_length(reply);
}
/**
* @brief Get the value for a property
*
* @param reply The reply for the property
* @return The value for the property
*/
inline fn GetPropertyValue(const GetPropReply* reply) -> void* {
return xcb_get_property_value(reply);
}
/**
* RAII wrapper for X11 Display connections
* Automatically handles resource acquisition and cleanup
*/
class DisplayGuard {
Connection* m_connection = nullptr; ///< The connection to the display
public:
/**
* Opens an XCB connection
* @param name Display name (nullptr for default)
*/
explicit DisplayGuard(const CStr name = nullptr)
: m_connection(Connect(name, nullptr)) {}
~DisplayGuard() {
if (m_connection)
Disconnect(m_connection);
}
// Non-copyable
DisplayGuard(const DisplayGuard&) = delete;
fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable
DisplayGuard(DisplayGuard&& other) noexcept
: m_connection(std::exchange(other.m_connection, nullptr)) {}
/**
* Move assignment operator
* @param other The other display guard
* @return The moved display guard
*/
fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) {
if (m_connection)
Disconnect(m_connection);
m_connection = std::exchange(other.m_connection, nullptr);
}
return *this;
}
/**
* @brief Check if the display guard is valid
* @return True if the display guard is valid, false otherwise
*/
[[nodiscard]] explicit operator bool() const {
return m_connection && !ConnectionHasError(m_connection);
}
/**
* @brief Get the connection to the display
* @return The connection to the display
*/
[[nodiscard]] fn get() const -> Connection* {
return m_connection;
}
/**
* @brief Get the setup for the display
* @return The setup for the display
*/
[[nodiscard]] fn setup() const -> const Setup* {
return m_connection ? xcb_get_setup(m_connection) : nullptr;
}
/**
* @brief Get the root screen for the display
* @return The root screen for the display
*/
[[nodiscard]] fn rootScreen() const -> Screen* {
const Setup* setup = this->setup();
return setup ? xcb_setup_roots_iterator(setup).data : nullptr;
}
};
/**
* RAII wrapper for XCB replies
* Handles automatic cleanup of various XCB reply objects
*/
template <typename T>
class ReplyGuard {
T* m_reply = nullptr; ///< The reply to the XCB request
public:
/**
* @brief Default constructor
*/
ReplyGuard() = default;
/**
* @brief Constructor with a reply
* @param reply The reply to the XCB request
*/
explicit ReplyGuard(T* reply)
: m_reply(reply) {}
/**
* @brief Destructor
*/
~ReplyGuard() {
if (m_reply)
free(m_reply);
}
// Non-copyable
ReplyGuard(const ReplyGuard&) = delete;
fn operator=(const ReplyGuard&)->ReplyGuard& = delete;
// Movable
ReplyGuard(ReplyGuard&& other) noexcept
: m_reply(std::exchange(other.m_reply, nullptr)) {}
/**
* @brief Move assignment operator
* @param other The other reply guard
* @return The moved reply guard
*/
fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& {
if (this != &other) {
if (m_reply)
free(m_reply);
m_reply = std::exchange(other.m_reply, nullptr);
}
return *this;
}
/**
* @brief Check if the reply guard is valid
* @return True if the reply guard is valid, false otherwise
*/
[[nodiscard]] explicit operator bool() const {
return m_reply != nullptr;
}
/**
* @brief Get the reply
* @return The reply
*/
[[nodiscard]] fn get() const -> T* {
return m_reply;
}
/**
* @brief Get the reply
* @return The reply
*/
[[nodiscard]] fn operator->() const->T* {
return m_reply;
}
/**
* @brief Dereference the reply
* @return The reply
*/
[[nodiscard]] fn operator*() const->T& {
return *m_reply;
}
};
} // namespace XCB
#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__

View file

@ -1,103 +0,0 @@
#include <argparse.hpp> // argparse::ArgumentParser
#include <cstdlib> // EXIT_FAILURE, EXIT_SUCCESS
#include <ftxui/dom/elements.hpp> // ftxui::{Element, hbox, vbox, text, separator, filler, etc.}
#include <ftxui/dom/node.hpp> // ftxui::{Render}
#include <ftxui/screen/screen.hpp> // ftxui::{Screen, Dimension::Full}
#ifdef __cpp_lib_print
#include <print> // std::print
#else
#include <iostream> // std::cout
#endif
#include "Config/Config.hpp"
#include "Core/SystemData.hpp"
#include "UI/UI.hpp"
#include "Util/Definitions.hpp"
#include "Util/Logging.hpp"
#include "Util/Types.hpp"
using util::types::i32, util::types::Exception;
fn main(const i32 argc, char* argv[]) -> i32 try {
#ifdef _WIN32
winrt::init_apartment();
#endif
{
using argparse::ArgumentParser;
ArgumentParser parser("draconis", "0.1.0");
parser
.add_argument("--log-level")
.help("Set the log level")
.default_value("info")
.choices("debug", "info", "warn", "error");
parser
.add_argument("-V", "--verbose")
.help("Enable verbose logging. Overrides --log-level.")
.flag();
if (Result<> result = parser.parse_args(argc, argv); !result) {
error_at(result.error());
return EXIT_FAILURE;
}
{
using matchit::match, matchit::is, matchit::_;
using util::logging::LogLevel;
using enum util::logging::LogLevel;
bool verbose = parser.get<bool>("-V").value_or(false) || parser.get<bool>("--verbose").value_or(false);
Result<String> logLevelStr = verbose ? "debug" : parser.get<String>("--log-level");
LogLevel minLevel = match(logLevelStr)(
is | "debug" = Debug,
is | "info" = Info,
is | "warn" = Warn,
is | "error" = Error,
#ifndef NDEBUG
is | _ = Debug
#else
is | _ = Info
#endif
);
SetRuntimeLogLevel(minLevel);
}
}
{
const Config& config = Config::getInstance();
const os::SystemData data = os::SystemData(config);
using ftxui::Element, ftxui::Screen, ftxui::Render;
using ftxui::Dimension::Full, ftxui::Dimension::Fit;
Element document = ui::CreateUI(config, data);
Screen screen = Screen::Create(Full(), Fit(document));
Render(screen, document);
screen.Print();
}
// Running the program as part of the shell's startup will cut
// off the last line of output, so we need to add a newline here.
#ifdef __cpp_lib_print
std::println();
#else
std::cout << '\n';
#endif
return EXIT_SUCCESS;
} catch (const DracError& e) {
error_at(e);
return EXIT_FAILURE;
} catch (const Exception& e) {
error_at(e);
return EXIT_FAILURE;
}

View file

@ -1,13 +0,0 @@
[wrap-file]
directory = curl-8.10.1
source_url = https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.1-1/curl-8.10.1.tar.xz
source_filename = curl-8.10.1.tar.xz
source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee
patch_filename = curl_8.10.1-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.1-1/get_patch
patch_hash = 707c28f35fc9b0e8d68c0c2800712007612f922a31da9637ce706a2159f3ddd8
wrapdb_version = 8.10.1-1
[provide]
dependency_names = libcurl

View file

@ -1,15 +0,0 @@
[wrap-file]
directory = FTXUI-5.0.0
source_url = https://github.com/ArthurSonzogni/FTXUI/archive/refs/tags/v5.0.0.tar.gz
source_filename = FTXUI-5.0.0.tar.gz
source_hash = a2991cb222c944aee14397965d9f6b050245da849d8c5da7c72d112de2786b5b
patch_filename = ftxui_5.0.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/ftxui_5.0.0-1/get_patch
patch_hash = 21c654e82739b90b95bd98c1d321264608d37c50d29fbcc3487f790fd5412909
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/ftxui_5.0.0-1/FTXUI-5.0.0.tar.gz
wrapdb_version = 5.0.0-1
[provide]
ftxui-screen = screen_dep
ftxui-dom = dom_dep
ftxui-component = component_dep

View file

@ -1,5 +0,0 @@
[wrap-file]
source_url = https://github.com/stephenberry/glaze/archive/refs/tags/v5.1.1.tar.gz
source_filename = glaze-5.1.1.tar.gz
source_hash = 7fed59aae4c09b27761c6c94e1e450ed30ddc4d7303ddc70591ec268d90512f5
directory = glaze-5.1.1

View file

@ -1,15 +0,0 @@
[wrap-file]
directory = openssl-3.0.8
source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz
source_filename = openssl-3.0.8.tar.gz
source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e
patch_filename = openssl_3.0.8-3_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch
patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz
wrapdb_version = 3.0.8-3
[provide]
libcrypto = libcrypto_dep
libssl = libssl_dep
openssl = openssl_dep

View file

@ -1,7 +0,0 @@
[wrap-git]
url = https://github.com/SRombauts/SQLiteCpp.git
revision = HEAD
directory = SQLiteCpp
[provide]
sqlitecpp = sqlitecpp_dep

View file

@ -1,10 +0,0 @@
[wrap-file]
directory = tomlplusplus-3.4.0
source_url = https://github.com/marzer/tomlplusplus/archive/v3.4.0.tar.gz
source_filename = tomlplusplus-3.4.0.tar.gz
source_hash = 8517f65938a4faae9ccf8ebb36631a38c1cadfb5efa85d9a72e15b9e97d25155
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/tomlplusplus_3.4.0-1/tomlplusplus-3.4.0.tar.gz
wrapdb_version = 3.4.0-1
[provide]
dependency_names = tomlplusplus

23
util.h Normal file
View file

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
// Unsigned integers
using u8 = std::uint8_t;
using u16 = std::uint16_t;
using u32 = std::uint32_t;
using u64 = std::uint64_t;
// Signed integers
using i8 = std::int8_t;
using i16 = std::int16_t;
using i32 = std::int32_t;
using i64 = std::int64_t;
// Floating-points
using f32 = float;
using f64 = double;
// Size types
using usize = std::size_t;
using isize = std::ptrdiff_t;

12
vcpkg.json Normal file
View file

@ -0,0 +1,12 @@
{
"name" : "draconispp",
"version-string" : "1.0.0",
"builtin-baseline" : "14b91796a68c87bc8d5cb35911b39287ccb7bd95",
"dependencies" : [ {
"name" : "fmt",
"version>=" : "10.2.1#2"
}, {
"name" : "cppwinrt",
"version>=" : "2.0.240111.5"
} ]
}