Compare commits
No commits in common. "main" and "windows" have entirely different histories.
56 changed files with 271 additions and 13016 deletions
|
@ -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
|
||||
|
|
20
.clang-tidy
20
.clang-tidy
|
@ -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
1
.envrc
|
@ -1 +0,0 @@
|
|||
use_flake
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
|||
* text eol=lf
|
28
.gitignore
vendored
28
.gitignore
vendored
|
@ -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
6
.gitmodules
vendored
|
@ -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
15
CMakeLists.txt
Normal 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
96
flake.lock
generated
|
@ -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
288
flake.nix
|
@ -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"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
3496
include/argparse.hpp
3496
include/argparse.hpp
File diff suppressed because it is too large
Load diff
1795
include/matchit.hpp
1795
include/matchit.hpp
File diff suppressed because it is too large
Load diff
199
main.cpp
Normal file
199
main.cpp
Normal 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;
|
||||
}
|
234
meson.build
234
meson.build
|
@ -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,
|
||||
)
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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
|
|
@ -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
|
505
src/OS/BSD.cpp
505
src/OS/BSD.cpp
|
@ -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__
|
153
src/OS/Haiku.cpp
153
src/OS/Haiku.cpp
|
@ -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__
|
650
src/OS/Linux.cpp
650
src/OS/Linux.cpp
|
@ -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__
|
|
@ -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
|
|
@ -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__
|
|
@ -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
|
398
src/OS/macOS.cpp
398
src/OS/macOS.cpp
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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}¤t_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;
|
||||
}
|
|
@ -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
|
|
@ -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."));
|
||||
}
|
|
@ -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
|
368
src/UI/UI.cpp
368
src/UI/UI.cpp
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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__
|
|
@ -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__
|
|
@ -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__
|
103
src/main.cpp
103
src/main.cpp
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
[wrap-git]
|
||||
url = https://github.com/SRombauts/SQLiteCpp.git
|
||||
revision = HEAD
|
||||
directory = SQLiteCpp
|
||||
|
||||
[provide]
|
||||
sqlitecpp = sqlitecpp_dep
|
|
@ -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
23
util.h
Normal 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
12
vcpkg.json
Normal 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"
|
||||
} ]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue