Compare commits

..
Author SHA1 Message Date
anthropic-code-agent[bot] 5ad46585b6 Fractional scaling 2026-03-22 19:58:16 +01:00
anthropic-code-agent[bot]andashaduri 7346db784a Add unit tests for fractional DPI scaling
- Create gui/tests directory structure with CMakeLists.txt
- Add test_fractional_scaling.cpp with comprehensive tests for:
  - 125%, 150%, 175%, 225%, 250% fractional scaling calculations
  - Correction ratio computation (system_scale / integer_ui_scale)
  - DPI detection logic from various DPI values
  - Edge cases: 100% and 200% integer scaling (no fractional)
- Tests verify the critical 250% case that was previously broken
- Integrate gui_tests into test_all executable

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ashaduri/gsmartcontrol/sessions/eb9277d5-ef45-4aee-8d6c-383739f37613
2026-03-22 17:06:49 +00:00
anthropic-code-agent[bot]andashaduri 9f62577161 Fix fractional DPI scaling to store full percent and compute correction ratio
- Store full scale percent (e.g., 250 for 250%) in g_windows_fractional_scaling_percent
- Compute correction = system_scale / integer_ui_scale for proper >=200% scaling
- Update API docs to reflect full percent return value (not fractional remainder)

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ashaduri/gsmartcontrol/sessions/d146d9d4-4974-4ee0-bd7b-12a84ce237ea
2026-03-22 17:00:17 +00:00
anthropic-code-agent[bot]andashaduri a79907bd05 Address PR review comments
- Add forward declaration for Gtk::Window in gsc_init.h to fix compilation
- Use std::lround() instead of truncation for proper rounding in scaling
- Update include comments to reference app_apply_fractional_scaling_to_default_size()

Changes:
1. Forward declare Gtk::Window namespace to avoid requiring Gtkmm headers
2. Replace truncating cast with std::lround() for accurate scaling (e.g., 850*1.25 = 1062.5 → 1063 instead of 1062)
3. Fix include comments in gsc_text_window.h and gsc_info_window.cpp

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-06 14:38:18 +00:00
anthropic-code-agent[bot]andashaduri b574294a51 Refactor: Extract fractional scaling logic to separate function
Extract the duplicated fractional scaling logic into a reusable function
`app_apply_fractional_scaling_to_default_size()` in gsc_init.cpp.

This eliminates code duplication across GscMainWindow, GscInfoWindow,
GscExecutorLogWindow, and GscTextWindow constructors.

The new function:
- Takes window pointer and configured sizes as parameters
- Handles all scaling logic in one place
- Makes it easier to maintain and extend

Addresses review feedback from @ashaduri.

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-06 14:26:09 +00:00
anthropic-code-agent[bot]andashaduri f1fd685441 Adjust default window sizes for fractional HiDPI scaling on Windows
When fractional HiDPI scaling is used on Windows (e.g., 125%, 150%, 250%),
GTK3 only applies integer scaling to the UI, but fonts are scaled to match
the actual DPI. This causes windows to appear smaller than they should be.

This fix detects the fractional scaling percentage during app initialization
and applies it to the default window sizes for all major windows:
- Main window (560x450 → scaled by fraction)
- Info window (850x600 → scaled by fraction)
- Executor log window (900x650 → scaled by fraction)
- Text window (800x600 → scaled by fraction)

The scaling is only applied when:
1. Fractional scaling is detected on Windows
2. No custom window size has been configured by the user

This ensures windows scale proportionally with fonts, providing a consistent
user experience on Windows with fractional HiDPI scaling.

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-04 21:58:06 +00:00
anthropic-code-agent[bot] bd30bbe3e5 Initial plan 2026-03-04 21:52:33 +00:00
22 changed files with 359 additions and 455 deletions
-128
View File
@@ -1,128 +0,0 @@
name: Build AppImage
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
BUILD_TYPE: RelWithDebInfo
jobs:
build-appimage:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Update Package Database
run: sudo apt-get update
- name: Install Dependencies
run: sudo apt-get install libgtkmm-3.0-dev gettext wget file libfuse2
- name: Download linuxdeploy
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
run: |
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
sudo mv linuxdeploy-x86_64.AppImage /usr/local/bin/linuxdeploy
- name: Download appimagetool
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool
- name: Create Build Directory
run: cmake -E make_directory build-appimage
- name: Configure CMake for AppImage
shell: bash
working-directory: ${{github.workspace}}/build-appimage
env:
CC: gcc-11
CXX: g++-11
run: >
cmake $GITHUB_WORKSPACE
-DCMAKE_BUILD_TYPE=$BUILD_TYPE
-DCMAKE_INSTALL_PREFIX=/usr
-DAPP_BUILD_APPIMAGE=ON
-DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/toolchains/linux-dev.cmake"
- name: Build
shell: bash
working-directory: ${{github.workspace}}/build-appimage
run: cmake --build . --config $BUILD_TYPE -j$(nproc)
- name: Install to AppDir
shell: bash
working-directory: ${{github.workspace}}/build-appimage
run: DESTDIR=${{github.workspace}}/AppDir cmake --install .
- name: Setup AppDir structure
shell: bash
working-directory: ${{github.workspace}}
run: |
# Copy desktop file and icon to AppDir root
cp AppDir/usr/share/applications/dev.shaduri.gsmartcontrol.desktop AppDir/
cp AppDir/usr/share/icons/hicolor/256x256/apps/gsmartcontrol.png AppDir/
# Create AppRun script
cat > AppDir/AppRun << 'EOF'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PATH="$HERE/usr/bin:${PATH:+:$PATH}"
export GSETTINGS_SCHEMA_DIR="$HERE/usr/share/glib-2.0/schemas:${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}"
export GDK_PIXBUF_MODULEDIR="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
export GDK_PIXBUF_MODULE_FILE="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
exec "$HERE/usr/bin/gsmartcontrol" "$@"
EOF
chmod +x AppDir/AppRun
- name: Bundle dependencies with linuxdeploy
shell: bash
working-directory: ${{github.workspace}}
run: |
linuxdeploy --appdir AppDir \
--executable AppDir/usr/bin/gsmartcontrol \
--desktop-file AppDir/dev.shaduri.gsmartcontrol.desktop \
--icon-file AppDir/gsmartcontrol.png
- name: Update GDK pixbuf cache
shell: bash
working-directory: ${{github.workspace}}
continue-on-error: true
run: |
if [ -d "AppDir/usr/lib/gdk-pixbuf-2.0" ]; then
gdk-pixbuf-query-loaders > AppDir/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache || true
fi
- name: Get version
id: version
shell: bash
run: |
VERSION=$(sed -n 's/.*CMAKE_PROJECT_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' version.txt)
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Create AppImage
shell: bash
working-directory: ${{github.workspace}}
run: |
ARCH=x86_64 appimagetool AppDir GSmartControl-${{ steps.version.outputs.VERSION }}-x86_64.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: GSmartControl AppImage
path: ${{github.workspace}}/GSmartControl-*-x86_64.AppImage
-10
View File
@@ -175,16 +175,6 @@ build_main_workflow:
- x86_64
- i586
# AppImage build (using Ubuntu 22.04 as base)
- name: AppImage
paths:
- target_project: openSUSE:Tools
target_repository: xUbuntu_22.04
- target_project: Ubuntu:22.04
target_repository: universe
architectures:
- x86_64
- set_flags:
flags:
- type: publish
-1
View File
@@ -39,7 +39,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# User-controlled build options
option(APP_BUILD_EXAMPLES "Build examples" OFF)
option(APP_BUILD_TESTS "Build tests" OFF)
option(APP_BUILD_APPIMAGE "Build for AppImage (portable data file paths)" OFF)
# Install documentation
+6 -24
View File
@@ -6,17 +6,8 @@
# Generate files
if (APP_BUILD_APPIMAGE)
set(APPIMAGE_DESKTOP_ID "dev.shaduri.gsmartcontrol")
set(APPIMAGE_EXEC_LINE "Exec=\"${CMAKE_INSTALL_FULL_BINDIR}/gsmartcontrol\"")
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "dev.shaduri.gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
else()
set(APPIMAGE_DESKTOP_ID "gsmartcontrol")
set(APPIMAGE_EXEC_LINE "Exec=\"${CMAKE_INSTALL_FULL_BINDIR}/gsmartcontrol-root\"")
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
endif()
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol-root.in.sh" "gsmartcontrol-root.sh" ESCAPE_QUOTES @ONLY)
configure_file("org.gsmartcontrol.in.policy" "org.gsmartcontrol.policy" ESCAPE_QUOTES @ONLY)
@@ -51,13 +42,8 @@ endif()
# Desktop file
if (NOT WIN32)
if (APP_BUILD_APPIMAGE)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dev.shaduri.gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
else()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
endif()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
# Appdata file
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.appdata.xml"
@@ -69,13 +55,9 @@ if (NOT WIN32)
# Man pages
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
if (NOT APP_BUILD_APPIMAGE)
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" RENAME "gsmartcontrol-root.1")
endif()
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" RENAME "gsmartcontrol-root.1")
# Scripts (this goes to bin, not sbin, as it doesn't require root privileges before running)
if (NOT APP_BUILD_APPIMAGE)
install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol-root.sh" TYPE BIN RENAME "gsmartcontrol-root")
endif()
install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol-root.sh" TYPE BIN RENAME "gsmartcontrol-root")
endif()
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>dev.shaduri.gsmartcontrol</id>
<id>gsmartcontrol</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>GSmartControl</name>
@@ -14,7 +14,7 @@
on it.
</p>
</description>
<launchable type="desktop-id">@APPIMAGE_DESKTOP_ID@.desktop</launchable>
<launchable type="desktop-id">gsmartcontrol.desktop</launchable>
<url type="homepage">https://gsmartcontrol.shaduri.dev</url>
<screenshots>
<screenshot type="default">
+1 -2
View File
@@ -63,5 +63,4 @@ Icon=gsmartcontrol
#X-KDE-RootOnly=true
# Run with root permissions.
# For AppImage builds, execute the binary directly (AppImage does not support embedded sudo scripts)
@APPIMAGE_EXEC_LINE@
Exec="@CMAKE_INSTALL_FULL_BINDIR@/gsmartcontrol-root"
-106
View File
@@ -1,106 +0,0 @@
# GSmartControl AppImage Build
This directory contains scripts and configurations for building GSmartControl as an AppImage.
## What is AppImage?
AppImage is a format for distributing portable software on Linux without requiring installation. AppImages are self-contained applications that can run on most Linux distributions.
## Building the AppImage
### Prerequisites
You need to have the following installed:
- cmake (>= 3.14)
- g++ or clang with C++20 support
- GTK3 and gtkmm-3.0 development packages
- smartmontools (for runtime)
- linuxdeploy and appimagetool (the script will download them if not available)
On Ubuntu/Debian:
```bash
sudo apt-get install cmake g++ libgtkmm-3.0-dev gettext smartmontools
```
On Fedora:
```bash
sudo dnf install cmake gcc-c++ gtkmm30-devel gettext smartmontools
```
### Build Steps
From the repository root directory, run:
```bash
./packaging/appimage/build-appimage.sh
```
The script will:
1. Configure CMake with `-DAPP_BUILD_APPIMAGE=ON`
2. Build the application
3. Create an AppDir with the proper structure
4. Bundle dependencies using linuxdeploy
5. Create the final AppImage file
### Output
The script creates an AppImage file named `GSmartControl-<version>-<arch>.AppImage` in the repository root.
## Running the AppImage
**Important:** GSmartControl requires root privileges to access disk drives. You must run the AppImage with sudo:
```bash
sudo ./GSmartControl-<version>-x86_64.AppImage
```
### Why sudo is required?
- AppImage format does not support embedded privilege escalation scripts
- The binary must be executed directly as root to access `/dev/sd*` and `/dev/nvme*` devices
- This is different from traditional package installations which use the `gsmartcontrol-root` wrapper script
## AppImage-Specific Changes
When building with `-DAPP_BUILD_APPIMAGE=ON`, the following changes are applied:
1. **Data file paths**: Uses relative paths from binary location (`bin/../share/...`)
2. **Desktop file**:
- Uses rDNS format: `dev.shaduri.gsmartcontrol.desktop`
- Executes binary directly without `gsmartcontrol-root` wrapper
3. **Binary location**: Installed to `bin/` instead of `sbin/`
4. **No wrapper script**: `gsmartcontrol-root` is not installed
## Compatibility
The AppImage should work on:
- Fedora Atomic (Silverblue, Kinoite, etc.)
- Most modern Linux distributions with GTK3 support
- Systems with read-only root filesystems
Tested architectures:
- x86_64 (Intel/AMD 64-bit)
## Troubleshooting
### "Permission denied" when running AppImage
Make the AppImage executable:
```bash
chmod +x GSmartControl-*.AppImage
```
### "No drives detected"
Make sure to run with sudo:
```bash
sudo ./GSmartControl-*.AppImage
```
### GTK theme issues
You may need to install GTK3 themes on your system. The AppImage bundles the necessary libraries but uses system themes when available.
## Notes for Developers
- The AppImage build uses the same source code as regular builds
- The `BuildEnv::is_appimage_build()` function can be used to detect AppImage builds at runtime
- Icon and UI file paths are resolved at runtime relative to the binary location
- The AppImage includes all necessary GTK3 and gtkmm libraries
-147
View File
@@ -1,147 +0,0 @@
#!/bin/bash
###############################################################################
# License: BSD Zero Clause License file
# Copyright:
# (C) 2025 Alexander Shaduri <ashaduri@gmail.com>
###############################################################################
# Script to build GSmartControl AppImage
# This script should be run from the repository root directory
#
# Requirements:
# - cmake, wget, file
# - libgtkmm-3.0-dev (or equivalent GTK3 development packages)
# - libfuse2 (required to run linuxdeploy and appimagetool AppImages)
# On Ubuntu/Debian: sudo apt-get install libfuse2
# On Ubuntu 24.04+: sudo apt-get install libfuse2t64
set -e # Exit on error
set -u # Exit on undefined variable
# Configuration
APPDIR="${APPDIR:-AppDir}"
BUILD_DIR="${BUILD_DIR:-build-appimage}"
ARCH="${ARCH:-x86_64}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Building GSmartControl AppImage${NC}"
echo "Architecture: $ARCH"
echo "Build directory: $BUILD_DIR"
echo "AppDir: $APPDIR"
echo
# Check dependencies
if ! command -v cmake &> /dev/null; then
echo -e "${RED}Error: cmake not found. Please install cmake.${NC}"
exit 1
fi
if ! command -v linuxdeploy &> /dev/null; then
echo -e "${YELLOW}Warning: linuxdeploy not found. Will attempt to download it.${NC}"
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
wget -N "$LINUXDEPLOY_URL" -O linuxdeploy
chmod +x linuxdeploy
LINUXDEPLOY="./linuxdeploy"
else
LINUXDEPLOY="linuxdeploy"
fi
if ! command -v appimagetool &> /dev/null; then
echo -e "${YELLOW}Warning: appimagetool not found. Will attempt to download it.${NC}"
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage"
wget -N "$APPIMAGETOOL_URL" -O appimagetool
chmod +x appimagetool
APPIMAGETOOL="./appimagetool"
else
APPIMAGETOOL="appimagetool"
fi
# Clean previous build
echo -e "${GREEN}Cleaning previous build...${NC}"
rm -rf "$BUILD_DIR"
rm -rf "$APPDIR"
# Create build directory
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
# Configure with CMake for AppImage build
echo -e "${GREEN}Configuring CMake for AppImage build...${NC}"
cmake .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr \
-DAPP_BUILD_APPIMAGE=ON \
-DCMAKE_TOOLCHAIN_FILE="../toolchains/linux-dev.cmake"
# Build
echo -e "${GREEN}Building...${NC}"
cmake --build . --config RelWithDebInfo -j$(nproc)
# Install to AppDir
echo -e "${GREEN}Installing to AppDir...${NC}"
DESTDIR="../$APPDIR" cmake --install .
cd ..
# Copy desktop file and icon to AppDir root (required by AppImage)
echo -e "${GREEN}Setting up AppDir structure...${NC}"
cp "$APPDIR/usr/share/applications/dev.shaduri.gsmartcontrol.desktop" "$APPDIR/"
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/gsmartcontrol.png" "$APPDIR/"
# Create AppRun script
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
# AppRun script for GSmartControl AppImage
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
# Export library paths
export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PATH="$HERE/usr/bin:${PATH:+:$PATH}"
# Export GTK settings
export GSETTINGS_SCHEMA_DIR="$HERE/usr/share/glib-2.0/schemas:${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}"
export GDK_PIXBUF_MODULEDIR="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
export GDK_PIXBUF_MODULE_FILE="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
# Run the application
exec "$HERE/usr/bin/gsmartcontrol" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# Use linuxdeploy to bundle dependencies
echo -e "${GREEN}Bundling dependencies with linuxdeploy...${NC}"
$LINUXDEPLOY --appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/gsmartcontrol" \
--desktop-file "$APPDIR/dev.shaduri.gsmartcontrol.desktop" \
--icon-file "$APPDIR/gsmartcontrol.png"
# Update GDK pixbuf cache
if [ -d "$APPDIR/usr/lib/gdk-pixbuf-2.0" ]; then
echo -e "${GREEN}Updating GDK pixbuf cache...${NC}"
gdk-pixbuf-query-loaders > "$APPDIR/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" 2>/dev/null || true
fi
# Get version from version.txt (extract quoted CMAKE_PROJECT_VERSION value)
VERSION=$(sed -n 's/.*CMAKE_PROJECT_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' version.txt | head -n1)
# Create AppImage
echo -e "${GREEN}Creating AppImage...${NC}"
ARCH=$ARCH $APPIMAGETOOL "$APPDIR" "GSmartControl-${VERSION}-${ARCH}.AppImage"
echo
echo -e "${GREEN}AppImage created successfully!${NC}"
echo -e "Output: ${GREEN}GSmartControl-${VERSION}-${ARCH}.AppImage${NC}"
echo
echo -e "${YELLOW}Note: The AppImage must be run with sudo to access disk drives.${NC}"
echo -e "Example: ${GREEN}sudo ./GSmartControl-${VERSION}-${ARCH}.AppImage${NC}"
+55
View File
@@ -11,6 +11,8 @@ Copyright:
#include "gui_utils.h"
#include <cmath>
namespace {
@@ -165,6 +167,59 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
namespace {
/// Storage for Windows fractional scaling percentage (0 if no fractional scaling)
int g_windows_fractional_scaling_percent = 0;
}
int app_get_windows_fractional_scaling_percent()
{
return g_windows_fractional_scaling_percent;
}
void app_set_windows_fractional_scaling_percent(int percent)
{
g_windows_fractional_scaling_percent = percent;
}
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h)
{
if (!window) {
return;
}
int size_w = config_size_w;
int size_h = config_size_h;
// Apply fractional scaling adjustment on Windows if no custom size is configured
// This compensates for GTK3's lack of fractional scaling support
const int full_percent = g_windows_fractional_scaling_percent;
if (full_percent > 0 && size_w == 0 && size_h == 0) {
// Get the default size from glade and scale it
int glade_w = 0, glade_h = 0;
window->get_default_size(glade_w, glade_h);
if (glade_w > 0 && glade_h > 0) {
const double system_scale = static_cast<double>(full_percent) / 100.0;
const int integer_ui_scale = full_percent / 100; // GTK3 uses floor (integer) scaling
const double correction = system_scale / static_cast<double>(integer_ui_scale);
size_w = static_cast<int>(std::lround(glade_w * correction));
size_h = static_cast<int>(std::lround(glade_h * correction));
}
}
if (size_w > 0 && size_h > 0) {
window->set_default_size(size_w, size_h);
}
}
+17
View File
@@ -59,6 +59,23 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Set the fractional scaling percentage detected on Windows.
/// This should only be called once during application initialization.
void app_set_windows_fractional_scaling_percent(int percent);
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
-6
View File
@@ -82,12 +82,6 @@ elseif ("${CMAKE_SYSTEM_NAME}" MATCHES "QNX")
set(CONFIG_KERNEL_QNX TRUE)
endif()
# AppImage build configuration
if (APP_BUILD_APPIMAGE)
set(CONFIG_APPIMAGE_BUILD TRUE)
message(STATUS "AppImage build enabled - using portable data file paths")
endif()
# Additionally, these macros are defined by OS / compilers:
-11
View File
@@ -26,7 +26,6 @@ Copyright:
#cmakedefine CONFIG_KERNEL_SOLARIS
#cmakedefine CONFIG_KERNEL_DARWIN
#cmakedefine CONFIG_KERNEL_QNX
#cmakedefine CONFIG_APPIMAGE_BUILD
#if defined CONFIG_KERNEL_WINDOWS32 || defined CONFIG_KERNEL_WINDOWS64
#define CONFIG_KERNEL_FAMILY_WINDOWS
@@ -184,16 +183,6 @@ struct BuildEnv {
#endif
}
/// Check if this is an AppImage build (portable data file paths).
static constexpr bool is_appimage_build()
{
#ifdef CONFIG_APPIMAGE_BUILD
return true;
#else
return false;
#endif
}
};
+1 -2
View File
@@ -62,12 +62,11 @@ target_link_libraries(gsmartcontrol
if (WIN32)
install(TARGETS gsmartcontrol DESTINATION .)
elseif (APP_BUILD_APPIMAGE)
install(TARGETS gsmartcontrol DESTINATION "${CMAKE_INSTALL_BINDIR}/")
else()
install(TARGETS gsmartcontrol DESTINATION "${CMAKE_INSTALL_SBINDIR}/")
endif()
add_subdirectory(ui)
add_subdirectory(tests)
+3
View File
@@ -31,6 +31,9 @@ Copyright:
GscExecutorLogWindow::GscExecutorLogWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscExecutorLogWindow, false>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* window_close_button = nullptr;
+2 -3
View File
@@ -35,6 +35,7 @@ Copyright:
#include "gsc_info_window.h"
#include "gsc_executor_error_dialog.h"
#include "gsc_startup_settings.h"
#include "gsc_init.h" // app_apply_fractional_scaling_to_default_size()
@@ -166,9 +167,7 @@ GscInfoWindow::GscInfoWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/info_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/info_window/default_size_h");
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
}
// Create missing widgets
+10 -10
View File
@@ -47,6 +47,7 @@
#include "applib/window_instance_manager.h"
#include "applib/gsc_settings.h"
#include "applib/gui_utils.h" // app_set_windows_fractional_scaling_percent()
#include "gsc_main_window.h"
#include "gsc_executor_log_window.h"
#include "gsc_init.h"
@@ -80,6 +81,7 @@ namespace {
return channel;
}
}
@@ -475,13 +477,7 @@ bool app_init_and_loop(int& argc, char**& argv)
hz::data_file_add_search_directory("icons", application_dir / "icons");
hz::data_file_add_search_directory("ui", application_dir / "ui");
hz::data_file_add_search_directory("doc", application_dir / "doc");
} else if constexpr(BuildEnv::is_appimage_build()) {
// AppImage: Use relative paths from binary location (bin/../share/...)
hz::data_file_add_search_directory("icons", application_dir.parent_path() / "share" / BuildEnv::package_name() / "icons");
hz::data_file_add_search_directory("ui", application_dir.parent_path() / "share" / BuildEnv::package_name() / "ui");
hz::data_file_add_search_directory("doc", application_dir.parent_path() / "share" / "doc" / BuildEnv::package_name());
} else {
// Traditional Unix installation: Use absolute paths from BuildEnv
hz::data_file_add_search_directory("icons", hz::fs_path_from_string(BuildEnv::package_pkgdata_dir()) / BuildEnv::package_name() / "icons"); // /usr/share/program_name/icons
hz::data_file_add_search_directory("ui", hz::fs_path_from_string(BuildEnv::package_pkgdata_dir()) / BuildEnv::package_name() / "ui"); // /usr/share/program_name/ui
hz::data_file_add_search_directory("doc", hz::fs_path_from_string(BuildEnv::package_doc_dir())); // /usr/share/doc/[packages/]gsmartcontrol
@@ -525,10 +521,14 @@ bool app_init_and_loop(int& argc, char**& argv)
if (h_ppi > 0) {
const double scale = h_ppi / 96.0;
debug_out_info("app", "Windows system DPI: " << h_ppi << ", scale: " << scale << "\n");
const int fraction_percent = static_cast<int>(std::round(scale * 100)) % 100;
if (fraction_percent != 0) { // fractional scaling
// Increase the font size by fraction, but round down the size to match the Windows behavior (?)
debug_out_dump("app", "Fractional scaling detected, increasing font size by " << fraction_percent << "%.\n");
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100; // 250 -> 200, 150 -> 100
if (full_scale_percent != integer_scale_percent) { // fractional scaling detected
// Store the full scale percent for use in window sizing
app_set_windows_fractional_scaling_percent(full_scale_percent);
// Increase the font size by the fractional amount
const int fraction_percent = full_scale_percent - integer_scale_percent;
debug_out_dump("app", "Fractional scaling detected (" << full_scale_percent << "%), increasing font size by " << fraction_percent << "%.\n");
Gtk::Settings::get_default()->property_gtk_font_name()
.set_value("Segoe UI " + hz::number_to_string_nolocale(static_cast<int>(9 * (1. + fraction_percent/100.))));
}
+19
View File
@@ -15,6 +15,11 @@ Copyright:
#include <string>
namespace Gtk {
class Window;
}
/// Initialize the application and run the main loop
bool app_init_and_loop(int& argc, char**& argv);
@@ -28,6 +33,20 @@ void app_quit();
std::string app_get_debug_buffer_str();
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
#endif
+1 -3
View File
@@ -60,9 +60,7 @@ GscMainWindow::GscMainWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/main_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/main_window/default_size_h");
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
}
// show the window first, scan later
+4
View File
@@ -24,6 +24,7 @@ Copyright:
#include "applib/app_builder_widget.h"
#include "applib/app_gtkmm_tools.h"
#include "applib/gui_utils.h" // app_apply_fractional_scaling_to_default_size()
@@ -49,6 +50,9 @@ class GscTextWindow : public AppBuilderWidget<GscTextWindow<InstanceSwitch>, Ins
GscTextWindow(typename Gtk::Window::BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscTextWindow<InstanceSwitch>, InstanceSwitch::multi_instance>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* save_as_button = nullptr;
+21
View File
@@ -0,0 +1,21 @@
###############################################################################
# License: BSD Zero Clause License file
# Copyright:
# (C) 2026 Alexander Shaduri <ashaduri@gmail.com>
###############################################################################
if (NOT APP_BUILD_TESTS)
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL true)
else()
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL false)
endif()
# Use Object libraries to allow runtime test discovery
add_library(gui_tests OBJECT)
target_sources(gui_tests PRIVATE
test_fractional_scaling.cpp
)
target_link_libraries(gui_tests PRIVATE
Catch2
)
+216
View File
@@ -0,0 +1,216 @@
/******************************************************************************
License: BSD Zero Clause License
Copyright:
(C) 2008 - 2026 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup gui_tests
/// \weakgroup gui_tests
/// @{
#include "catch2/catch.hpp"
#include <cmath>
/// Test the fractional scaling calculation logic
/// This tests the mathematical correctness of the scaling correction ratio
TEST_CASE("FractionalScalingCalculation", "[gui][scaling]")
{
// Test the calculation logic that's used in app_apply_fractional_scaling_to_default_size()
// correction = system_scale / integer_ui_scale
SECTION("125% scaling (fractional)")
{
const int full_percent = 125;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.25
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.25);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.25);
// Test window scaling: 800x600 base should become 1000x750
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1000);
REQUIRE(scaled_h == 750);
}
SECTION("150% scaling (fractional)")
{
const int full_percent = 150;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.5
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.5);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.5);
// Test window scaling: 800x600 base should become 1200x900
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1200);
REQUIRE(scaled_h == 900);
}
SECTION("175% scaling (fractional)")
{
const int full_percent = 175;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.75
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.75);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.75);
// Test window scaling: 800x600 base should become 1400x1050
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1400);
REQUIRE(scaled_h == 1050);
}
SECTION("250% scaling (fractional) - Critical test case")
{
// This is the case that was broken before the fix
const int full_percent = 250;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.5
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.5);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.25); // NOT 1.5 (the old broken behavior)
// Test window scaling: GTK already applies 2x, we need to apply 1.25x more
// For a base of 800x600, GTK makes it 1600x1200, we should scale to 2000x1500
const int gtk_scaled_w = 1600; // After GTK's 2x integer scaling
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 2000); // 2.5x total = 800 * 2.5
REQUIRE(final_h == 1500); // 2.5x total = 600 * 2.5
}
SECTION("225% scaling (fractional)")
{
const int full_percent = 225;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.25
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.25);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.125);
// Test window scaling: 800x600 at 2x GTK = 1600x1200, with correction = 1800x1350
const int gtk_scaled_w = 1600;
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 1800);
REQUIRE(final_h == 1350);
}
SECTION("100% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 100;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
SECTION("200% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 200;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
}
/// Test the DPI detection logic
TEST_CASE("DPIDetectionLogic", "[gui][scaling][dpi]")
{
SECTION("Detect 125% scaling from DPI")
{
const int h_ppi = 120; // 120 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 125);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 150% scaling from DPI")
{
const int h_ppi = 144; // 144 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 150);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 250% scaling from DPI")
{
const int h_ppi = 240; // 240 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 250);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("No fractional scaling at 100% (96 DPI)")
{
const int h_ppi = 96; // 96 DPI (standard)
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 100);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
SECTION("No fractional scaling at 200% (192 DPI)")
{
const int h_ppi = 192; // 192 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 200);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
}
/// @}
+1
View File
@@ -22,6 +22,7 @@ target_link_libraries(test_all PRIVATE
libdebug
applib_tests
hz_tests
gui_tests
Catch2
)