chore: use lizardbyte-common in c++ (#158)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-06-30 18:07:43 -04:00
1 parent 27b67b7abd
commit f449b4a909
12 files changed
+115 -198

No files matched your search

-4
View File
@@ -2,10 +2,6 @@
path = third-party/doxyconfig
url = https://github.com/LizardByte/doxyconfig.git
branch = master
[submodule "third-party/googletest"]
path = third-party/googletest
url = https://github.com/google/googletest.git
branch = v1.14.x
[submodule "third-party/lizardbyte-common"]
path = third-party/lizardbyte-common
url = https://github.com/LizardByte/lizardbyte-common.git
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="&quot;file:\\$CMakeCurrentBuildDir$\build\html\index.html&quot;" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="tray" TARGET_NAME="docs" CONFIG_NAME="Release" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
+38 -19
View File
@@ -1,10 +1,23 @@
<div align="center">
<img
src="tray.svg"
alt="tray icon"
width="256"
/>
<h1 align="center">tray</h1>
<h4 align="center">Cross-platform implementation of a system tray icon with a popup menu and notifications.</h4>
</div>
<div align="center">
<a href="https://github.com/LizardByte/tray"><img src="https://img.shields.io/github/stars/lizardbyte/tray.svg?logo=github&style=for-the-badge" alt="GitHub stars"></a>
<a href="https://github.com/LizardByte/tray/actions/workflows/ci.yml?query=branch%3Amaster"><img src="https://img.shields.io/github/actions/workflow/status/lizardbyte/tray/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge" alt="GitHub Workflow Status (CI)"></a>
<a href="https://codecov.io/gh/LizardByte/tray"><img src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fapp.lizardbyte.dev%2Fdashboard%2Fshields%2Fcodecov%2Ftray.json&style=for-the-badge&logo=codecov" alt="Codecov"></a>
<a href="https://sonarcloud.io/project/overview?id=LizardByte_tray"><img src="https://img.shields.io/sonar/quality_gate/LizardByte_tray.svg?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonarqubecloud&label=sonarcloud" alt="SonarCloud"></a>
</div>
# Overview
[![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/tray/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/tray/actions/workflows/ci.yml?query=branch%3Amaster)
[![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/tray?token=HSX66JNEOL&style=for-the-badge&logo=codecov&label=codecov)](https://codecov.io/gh/LizardByte/tray)
[![GitHub stars](https://img.shields.io/github/stars/lizardbyte/tray.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/tray)
## About
## ℹ️ About
Cross-platform, super tiny C99 implementation of a system tray icon with a popup menu and notifications.
@@ -20,23 +33,29 @@ This fork adds the following features:
- refactored code, e.g., moved source code into the `src` directory
- doxygen documentation and readthedocs configuration
## Screenshots
## 🖼️ Screenshots
<div class="tabbed">
- <b class="tab-title">Linux</b>![linux](docs/images/screenshot_linux.png)
- <b class="tab-title">macOS</b>![macOS](docs/images/screenshot_macos.png)
- <b class="tab-title">Windows</b>![windows](docs/images/screenshot_windows.png)
<ul>
<li><b class="tab-title">Linux</b><br>
![linux](docs/images/screenshot_linux.png)
</li>
<li><b class="tab-title">macOS</b><br>
![macOS](docs/images/screenshot_macos.png)
</li>
<li><b class="tab-title">Windows</b><br>
![windows](docs/images/screenshot_windows.png)
</li>
</ul>
</div>
## Supported platforms
## 🖥️ Supported platforms
* Linux/Qt (Qt5 or Qt6 Widgets)
* Windows XP or newer (shellapi.h)
* MacOS (Cocoa/AppKit)
## Prerequisites
## 📋 Prerequisites
* CMake
* [Ninja](https://ninja-build.org/), to have the same build commands on all platforms.
@@ -76,7 +95,7 @@ Install either Qt6 _or_ Qt5 as well as libnotify development packages. The Linux
</div>
## Building
## 🛠️ Building
```bash
mkdir -p build
@@ -84,7 +103,7 @@ cmake -G Ninja -B build -S .
ninja -C build
```
## Python Tooling
## ⚙️ Python Tooling
Install [uv](https://docs.astral.sh/uv/) and initialize the shared tooling submodule:
@@ -94,7 +113,7 @@ uv run --project third-party/lizardbyte-common --locked --only-group lint-c \
python third-party/lizardbyte-common/scripts/update_clang_format.py
```
## Demo
## ▶️ Demo
Execute the `tray_example` application:
@@ -102,7 +121,7 @@ Execute the `tray_example` application:
./build/tray_example
```
## Tests
## ✅ Tests
Execute the `tests` application:
@@ -110,7 +129,7 @@ Execute the `tests` application:
./build/tests/test_tray
```
## API
## 📚 API
Tray structure defines an icon and a menu.
Menu is a NULL-terminated array of items.
@@ -145,7 +164,7 @@ All functions are meant to be called from the UI thread only.
Menu arrays must be terminated with a NULL item, e.g. the last item in the
array must have text field set to NULL.
## License
## 📄 License
This software is distributed under [MIT license](http://www.opensource.org/licenses/mit-license.php),
so feel free to integrate it in your commercial products.
+3 -1
View File
@@ -24,7 +24,9 @@
# project metadata
DOCSET_BUNDLE_ID = dev.lizardbyte.tray
DOCSET_PUBLISHER_ID = dev.lizardbyte.tray.documentation
PROJECT_BRIEF = "Cross-platform, super tiny C99 implementation of a system tray icon with a popup menu and notifications."
PROJECT_BRIEF = "Cross-platform implementation of a system tray icon with a popup menu and notifications."
PROJECT_ICON = ../tray.svg
PROJECT_LOGO = ../tray.svg
PROJECT_NAME = tray
# project specific settings
+9 -2
View File
@@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.13)
project(test_tray)
# Add GoogleTest directory to the project
set(GTEST_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/googletest")
set(GTEST_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/lizardbyte-common/third-party/googletest")
# For Windows: prevent overriding parent compiler/linker runtime settings.
if(WIN32)
@@ -16,6 +16,13 @@ set(INSTALL_GMOCK OFF)
add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest")
include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}")
set(LIZARDBYTE_COMMON_BUILD_TEST_SUPPORT ON CACHE BOOL "Build lizardbyte-common GoogleTest support helpers" FORCE)
if(NOT TARGET lizardbyte::common)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third-party/lizardbyte-common"
"${CMAKE_CURRENT_BINARY_DIR}/lizardbyte-common")
elseif(NOT TARGET lizardbyte::test_support)
message(FATAL_ERROR "tray tests require lizardbyte::test_support")
endif()
# extra libraries for tests
if (APPLE)
set(TEST_LIBS "-framework Cocoa")
@@ -25,7 +32,6 @@ endif()
file(GLOB_RECURSE TEST_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/conftest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/utils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/screenshot_utils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/unit/test_*.cpp"
)
@@ -40,6 +46,7 @@ target_include_directories(${PROJECT_NAME}
target_link_libraries(${PROJECT_NAME}
${TEST_LIBS}
tray::tray
lizardbyte::test_support
gtest
gtest_main # if we use this we don't need our own main function
)
+47 -134
View File
@@ -1,15 +1,14 @@
// standard includes
#include <array>
#include <filesystem>
#include <mutex>
// lib includes
#include <gtest/gtest.h>
#define LIZARDBYTE_COMMON_TESTING_KEEP_GTEST_TEST
#define LIZARDBYTE_COMMON_TESTING_NO_GLOBAL_ALIASES
#include <lizardbyte/common/testing.h>
// test includes
#include "tests/screenshot_utils.h"
#include "tests/utils.h"
// Undefine the original TEST macro
#undef TEST
@@ -20,141 +19,56 @@
/**
* @brief Base class for tests.
*
* This class provides a base test fixture for all tests.
*
* ``cout``, ``stderr``, and ``stdout`` are redirected to a buffer, and the buffer is printed if the test fails.
*
* @todo Retain the color of the original output.
* This class provides a base test fixture for all tests and adds tray-specific helpers.
*/
class BaseTest: public ::testing::Test {
class BaseTest: public ::lizardbyte::common::testing::BaseTest {
protected:
// https://stackoverflow.com/a/58369622/11214013
// we can possibly use some internal googletest functions to capture stdout and stderr, but I have not tested this
// https://stackoverflow.com/a/33186201/11214013
BaseTest() = default;
~BaseTest() override = default;
void SetUp() override {
::lizardbyte::common::testing::BaseTest::SetUp();
// todo: only run this one time, instead of every time a test is run
// see: https://stackoverflow.com/questions/2435277/googletest-accessing-the-environment-from-a-test
// get command line args from the test executable
testArgs = ::testing::internal::GetArgvs();
testArgs_ = getArgs();
// then get the directory of the test executable
// std::string path = ::testing::internal::GetArgvs()[0];
testBinary = testArgs[0];
testBinary_ = testArgs_[0];
// get the directory of the test executable
testBinaryDir = std::filesystem::path(testBinary).parent_path();
testBinaryDir_ = std::filesystem::path(testBinary_).parent_path();
// If testBinaryDir is empty or `.` then set it to the current directory
// maybe some better options here: https://stackoverflow.com/questions/875249/how-to-get-current-directory
if (testBinaryDir.empty() || testBinaryDir.string() == ".") {
testBinaryDir = std::filesystem::current_path();
if (testBinaryDir_.empty() || testBinaryDir_.string() == ".") {
testBinaryDir_ = std::filesystem::current_path();
}
initializeScreenshotsOnce();
sbuf = std::cout.rdbuf(); // save cout buffer (std::cout)
std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout)
}
void TearDown() override {
std::cout.rdbuf(sbuf); // restore cout buffer
// get test info
const ::testing::TestInfo *const test_info = ::testing::UnitTest::GetInstance()->current_test_info();
if (test_info->result()->Failed()) {
std::cout << std::endl
<< "Test failed: " << test_info->name() << std::endl
<< std::endl
<< "Captured cout:" << std::endl
<< cout_buffer.str() << std::endl
<< "Captured stdout:" << std::endl
<< stdout_buffer.str() << std::endl
<< "Captured stderr:" << std::endl
<< stderr_buffer.str() << std::endl;
}
sbuf = nullptr; // clear sbuf
if (pipe_stdout) {
pclose(pipe_stdout);
pipe_stdout = nullptr;
}
if (pipe_stderr) {
pclose(pipe_stderr);
pipe_stderr = nullptr;
}
}
// functions and variables
std::vector<std::string> testArgs; // CLI arguments used
std::filesystem::path testBinary; // full path of this binary
std::filesystem::path testBinaryDir; // full directory of this binary
std::stringstream cout_buffer; // declare cout_buffer
std::stringstream stdout_buffer; // declare stdout_buffer
std::stringstream stderr_buffer; // declare stderr_buffer
std::streambuf *sbuf {nullptr};
FILE *pipe_stdout {nullptr};
FILE *pipe_stderr {nullptr};
bool screenshotsReady {false};
void initializeScreenshotsOnce() {
static std::once_flag screenshotInitFlag;
std::call_once(screenshotInitFlag, [this]() {
auto root = testBinaryDir;
if (!root.empty()) {
std::error_code ec;
std::filesystem::remove_all(root / "screenshots", ec);
}
screenshot::initialize(root);
});
}
int exec(const char *cmd) {
std::array<char, 128> buffer {};
pipe_stdout = popen((std::string(cmd) + " 2>&1").c_str(), "r");
pipe_stderr = popen((std::string(cmd) + " 2>&1").c_str(), "r");
if (!pipe_stdout || !pipe_stderr) {
throw std::runtime_error("popen() failed!");
}
while (fgets(buffer.data(), buffer.size(), pipe_stdout) != nullptr) {
stdout_buffer << buffer.data();
}
while (fgets(buffer.data(), buffer.size(), pipe_stderr) != nullptr) {
stderr_buffer << buffer.data();
}
int returnCode = pclose(pipe_stdout);
pipe_stdout = nullptr;
if (returnCode != 0) {
std::cout << "Error: " << stderr_buffer.str() << std::endl
<< "Return code: " << returnCode << std::endl;
}
return returnCode;
}
bool ensureScreenshotReady() {
if (screenshotsReady) {
if (screenshotsReady_) {
return true;
}
if (std::string reason; !screenshot::is_available(&reason)) {
screenshotUnavailableReason = reason;
screenshotUnavailableReason_ = reason;
return false;
}
if (const auto root = screenshot::output_root(); root.empty()) {
screenshotUnavailableReason = "Screenshot output directory not initialized";
screenshotUnavailableReason_ = "Screenshot output directory not initialized";
return false;
}
screenshotsReady = true;
screenshotsReady_ = true;
return true;
}
bool captureScreenshot(const std::string &name) {
if (!screenshotsReady) {
bool captureScreenshot(const std::string &name) const {
if (!screenshotsReady_) {
return false;
}
bool ok = screenshot::capture(name);
@@ -168,35 +82,34 @@ protected:
return screenshot::output_root();
}
std::string screenshotUnavailableReason;
[[nodiscard]] const std::filesystem::path &testBinaryDir() const {
return testBinaryDir_;
}
[[nodiscard]] const std::string &screenshotUnavailableReason() const {
return screenshotUnavailableReason_;
}
private:
void initializeScreenshotsOnce() const {
static std::once_flag screenshotInitFlag;
std::call_once(screenshotInitFlag, [this]() {
auto root = testBinaryDir_;
if (!root.empty()) {
std::error_code ec;
std::filesystem::remove_all(root / "screenshots", ec);
}
screenshot::initialize(root);
});
}
std::vector<std::string> testArgs_; // CLI arguments used
std::filesystem::path testBinary_; // full path of this binary
std::filesystem::path testBinaryDir_; // full directory of this binary
bool screenshotsReady_ {false};
std::string screenshotUnavailableReason_;
};
class LinuxTest: public BaseTest {
protected:
void SetUp() override {
#ifndef __linux__
GTEST_SKIP_("Skipping, this test is for Linux only.");
#endif
BaseTest::SetUp();
}
};
class MacOSTest: public BaseTest {
protected:
void SetUp() override {
#if !defined(__APPLE__) || !defined(__MACH__)
GTEST_SKIP_("Skipping, this test is for macOS only.");
#endif
BaseTest::SetUp();
}
};
class WindowsTest: public BaseTest {
protected:
void SetUp() override { // NOSONAR(cpp:S1185) - contains platform skip logic, not a trivial override
#ifndef _WIN32
GTEST_SKIP_("Skipping, this test is for Windows only.");
#endif
BaseTest::SetUp();
}
};
using LinuxTest = ::lizardbyte::common::testing::LinuxTest;
using MacOSTest = ::lizardbyte::common::testing::MacOSTest;
using WindowsTest = ::lizardbyte::common::testing::WindowsTest;
+6 -6
View File
@@ -99,7 +99,7 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
}
// Dismisses the open menu from a background thread.
void closeMenu() {
void closeMenu() const {
#if defined(TRAY_WINAPI)
PostMessage(tray_get_hwnd(), WM_CANCELMODE, 0, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -115,7 +115,7 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
}
// Capture a screenshot while the tray menu is open, then dismiss and exit.
void captureMenuStateAndExit(const char *screenshotName) {
void captureMenuStateAndExit(const char *screenshotName) const {
std::atomic_bool exitRequested {false};
std::thread capture_thread([this, screenshotName, &exitRequested]() { // NOSONAR(cpp:S6168) - std::jthread is unavailable on AppleClang 17/libc++ used in CI
EXPECT_TRUE(captureScreenshot(screenshotName));
@@ -172,14 +172,14 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
// Skip tests if screenshot tooling is not available
if (!ensureScreenshotReady()) {
GTEST_SKIP() << "Screenshot tooling missing: " << screenshotUnavailableReason;
GTEST_SKIP() << "Screenshot tooling missing: " << screenshotUnavailableReason();
}
if (screenshot::output_root().empty()) {
GTEST_SKIP() << "Screenshot output path not initialized";
}
// Ensure icon files exist in test binary directory
std::filesystem::path projectRoot = testBinaryDir.parent_path();
std::filesystem::path projectRoot = testBinaryDir().parent_path();
auto ensureIconInTestDir = [&projectRoot, this](const char *iconName) {
std::filesystem::path iconSource;
@@ -192,7 +192,7 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
}
if (!iconSource.empty()) {
std::filesystem::path iconDest = testBinaryDir / iconName;
std::filesystem::path iconDest = testBinaryDir() / iconName;
if (!std::filesystem::exists(iconDest)) {
std::error_code ec;
std::filesystem::copy_file(iconSource, iconDest, ec);
@@ -226,7 +226,7 @@ protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access
// Process pending events to allow tray icon to appear.
// Call this ONLY before screenshots to ensure the icon is visible.
void WaitForTrayReady() {
void WaitForTrayReady() const {
#if defined(TRAY_QT)
for (int i = 0; i < 100; i++) {
tray_loop(0);
-20
View File
@@ -1,20 +0,0 @@
/**
* @file utils.cpp
* @brief Utility functions
*/
// test includes
#include "utils.h"
/**
* @brief Set an environment variable.
* @param name Name of the environment variable
* @param value Value of the environment variable
* @return 0 on success, non-zero error code on failure
*/
int setEnv(const std::string &name, const std::string &value) {
#ifdef _WIN32
return _putenv_s(name.c_str(), value.c_str());
#else
return setenv(name.c_str(), value.c_str(), 1);
#endif
}
-10
View File
@@ -1,10 +0,0 @@
/**
* @file utils.h
* @brief Reusable functions for tests.
*/
#pragma once
// standard includes
#include <string>
int setEnv(const std::string &name, const std::string &value);
Submodule third-party/googletest deleted from f8d7d77c06.
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" rx="12" fill="#2f6fed"/>
<path d="M18 34h8l6 8 14-20h8L32 52z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 204 B