Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c3c0ff9
Build with CMake
dhh Nov 11, 2026
9b50e8c
Read slide theme colors the way the window does
dhh Nov 11, 2026
3c03f9c
Unset OMARCHY_PATH after the theme test when it was unset before
omarchybot Nov 11, 2026
85499b7
Give the tests fonts, and leave the trial tools out of ctest
omarchybot Nov 11, 2026
063d2c6
Let CMAKE_PREFIX_PATH choose Qt over the one under build/qt
omarchybot Nov 11, 2026
2b409a5
Test with Noto fonts, as Omarchy installs them
omarchybot Nov 11, 2026
56e2023
Link the tests with LTO too, and depend on qt6-wayland
omarchybot Nov 11, 2026
0585885
Record a chosen theme's colors in the order the theme lists them
omarchybot Nov 11, 2026
aefe90b
Exit after saving from the command line
omarchybot Nov 11, 2026
53f0542
Release the window when the file chooser's portal goes away
omarchybot Nov 11, 2026
d888f2e
Take the corrected comment in the shared bin/test
omarchybot Nov 11, 2026
c2a0726
Keep hype render from overwriting its presentation or a half-written …
omarchybot Nov 11, 2026
88a939e
Quit with Ctrl+Q
omarchybot Nov 11, 2026
fb2ad2a
Write a rendered slide beside its target and rename it over
omarchybot Nov 11, 2026
772af76
Leave packaging to omarchy-pkgs and take its dependencies in CI
omarchybot Nov 11, 2026
91b81bc
Test that a save from the command line that can't be made fails
omarchybot Nov 11, 2026
9b61314
Also release the window when the portal is replaced
omarchybot Nov 11, 2026
bdc417e
Leave packaging to omarchy-pkgs
omarchybot Nov 11, 2026
af353b9
Merge remote-tracking branch 'origin/slide-theme-colors' into HEAD
omarchybot Nov 11, 2026
3343fc6
Merge remote-tracking branch 'origin/save-and-exit' into HEAD
omarchybot Nov 11, 2026
2992b04
Merge remote-tracking branch 'origin/portal-vanishes' into HEAD
omarchybot Nov 11, 2026
b1d8581
Merge remote-tracking branch 'origin/render-output-safety' into HEAD
omarchybot Nov 11, 2026
665365c
Merge remote-tracking branch 'origin/quit-shortcut' into HEAD
omarchybot Nov 11, 2026
3f964f2
Proof only: read dependencies from omarchy-pkgs#2573
omarchybot Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Test

on:
pull_request:
push:
branches: [main, master]

jobs:
test:
name: Build and test
runs-on: ubuntu-latest
# The app ships as an Arch package built against Arch's Qt, so it's tested there.
container: archlinux:latest
defaults:
run:
shell: bash
steps:
- name: Install git for the checkout
run: pacman -Syu --noconfirm --needed git

- uses: actions/checkout@v4

- name: Install the package's dependencies
# The package lives in omarchy-pkgs, so its PKGBUILD is the one list of what the app needs and CI reads it rather than keeping a copy.
run: |
curl -fsSL "https://raw.githubusercontent.com/omacom/omarchy-pkgs/8be89db82f2cdba1d6538fadf6de81929e7c12f4/pkgbuilds/${{ github.event.repository.name }}/PKGBUILD" -o "$RUNNER_TEMP/PKGBUILD"
source "$RUNNER_TEMP/PKGBUILD"
deps=("${depends[@]}" "${makedepends[@]}" "${checkdepends[@]}")
pacman -S --noconfirm --needed base-devel cmake dbus "${deps[@]%%[<>=]*}"

- name: Set up a user and a locale like a desktop's
# Root can read any file, so tests about unreadable ones need an ordinary user, with a second group for tests that change a file's group; and the container generates no locale.
run: |
useradd -m tester
groupadd other
usermod -aG other tester
chown -R tester .
sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
locale-gen

- name: Build
run: su tester -c bin/build

- name: Test
run: su tester -c 'LANG=en_US.UTF-8 dbus-run-session -- bin/test'
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
/build/
/trials/
*.user
/pkgbuild/pkg/
/pkgbuild/src/
/pkgbuild/*.pkg.tar.*
__pycache__/
76 changes: 76 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
cmake_minimum_required(VERSION 3.21)
project(hype LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

# Like Qt's own modules, Hype never throws or catches. Without unwinding tables
# and with link-time optimization, the installed binary is about a quarter smaller.
add_compile_options(-fno-exceptions)
add_compile_definitions(QT_NO_EXCEPTIONS)

# A Qt from Qt's installer unpacked under build/qt/ wins over an older system
# Qt, newest first, unless CMAKE_PREFIX_PATH picks another.
if(NOT CMAKE_PREFIX_PATH AND NOT DEFINED ENV{CMAKE_PREFIX_PATH})
file(GLOB bundled_qt LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/build/qt/*/gcc_64)
list(SORT bundled_qt COMPARE NATURAL ORDER DESCENDING)
set(CMAKE_PREFIX_PATH ${bundled_qt})
endif()

find_package(Qt6 6.9 REQUIRED COMPONENTS
Core Gui Qml Quick QuickControls2 Multimedia Concurrent DBus Svg Widgets Test)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WEBP REQUIRED IMPORTED_TARGET libwebp libwebpdemux)
find_package(ZLIB REQUIRED)

# Everything but main.cpp, shared by the app and the tests, so each source
# compiles once and a new file in src/ needs no line here.
file(GLOB hype_sources CONFIGURE_DEPENDS src/*.cpp src/*.h)
list(REMOVE_ITEM hype_sources ${CMAKE_SOURCE_DIR}/src/main.cpp)
add_library(hype_core STATIC ${hype_sources})
target_include_directories(hype_core PUBLIC src)
target_link_libraries(hype_core PUBLIC
Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Multimedia Qt6::Concurrent
Qt6::DBus Qt6::Svg PkgConfig::WEBP ZLIB::ZLIB)
target_compile_options(hype_core PUBLIC -Wall -Wextra)
set_target_properties(hype_core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)

add_executable(hype src/main.cpp src/resources.qrc)
target_link_libraries(hype PRIVATE hype_core)
set_target_properties(hype PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)

enable_testing()

# The tests read exports back with Qt Pdf, which ships in qt6-webengine, so
# building the app alone doesn't need it; testing without it fails, not skips.
find_package(Qt6 6.9 COMPONENTS Pdf)
if(TARGET Qt6::Pdf)
# The window-level tests load the same QML as the app, so they embed it too.
add_executable(hype_tests tests/tests.cpp src/resources.qrc)
target_link_libraries(hype_tests PRIVATE hype_core Qt6::Widgets Qt6::Pdf Qt6::Test)
set_target_properties(hype_tests PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)
add_test(NAME hype_tests COMMAND hype_tests)
else()
message(WARNING "Qt Pdf was not found, so the tests can't be built; install qt6-webengine.")
add_test(NAME hype_tests COMMAND sh -c "echo 'hype_tests needs Qt Pdf, from qt6-webengine' >&2; exit 1")
endif()
# Hype draws its own controls, so the tests keep Qt's platform theme and style out of it.
set_tests_properties(hype_tests PROPERTIES ENVIRONMENT
"QT_QPA_PLATFORM=offscreen;QT_QPA_PLATFORMTHEME=generic;QT_STYLE_OVERRIDE=Fusion")

# The commands, exports and shutdown, driven through build/hype as an agent would.
# test_trials covers the trial import tools, which need their own Python packages.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
add_test(NAME hype_commands
COMMAND ${Python3_EXECUTABLE} -m unittest test_cli test_export test_shutdown
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests)
else()
add_test(NAME hype_commands COMMAND sh -c "echo 'hype_commands needs Python 3' >&2; exit 1")
endif()
set_tests_properties(hype_commands PROPERTIES ENVIRONMENT QT_QPA_PLATFORM=offscreen)
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ Slide navigation and selection shortcuts apply when the sidebar or preview has f
| --- | --- |
| Ctrl+N / Ctrl+O | New presentation / open file |
| Ctrl+S / Ctrl+Shift+S | Save / save as |
| Ctrl+Q | Quit |
| Ctrl+E / Ctrl+Shift+E | Export as PDF / PowerPoint |
| Ctrl+M | Overview on / off |
| Ctrl+. | Markdown source on / off |
Expand Down Expand Up @@ -244,22 +245,21 @@ The mouse wheel over the sidebar selects the previous or next slide. Home/End ju

## Run from source

To build Hype yourself, install a C++17 compiler, make, Qt 6.9 or newer, FFmpeg, and GNU source-highlight; see [the package definition](pkgbuild/PKGBUILD) for dependencies. Then:
To build Hype yourself, install a C++17 compiler, CMake, Qt 6.9 or newer, FFmpeg, and GNU source-highlight; see [the package definition in omarchy-pkgs](https://github.com/omacom/omarchy-pkgs/blob/master/pkgbuilds/hype/PKGBUILD) for dependencies. Then:

```sh
./bin/build
./build/hype open examples/welcome.md
```

`bin/build` uses a Qt toolchain under `build/qt/` when present, then falls back to `qmake6` on your PATH. It checks for Qt 6.9 or newer before compiling. Set `HYPE_QMAKE=/path/to/qmake6` to select a different toolchain.
The build uses a Qt toolchain from Qt's installer under `build/qt/` when present, newest first, then the system Qt, and checks for Qt 6.9 or newer before compiling. Set `CMAKE_PREFIX_PATH=/path/to/Qt/6.9.0/gcc_64` to select a different toolchain, and delete `build/CMakeCache.txt` when switching, since CMake remembers the Qt it found.

For a launcher entry that rebuilds this checkout when opened, run `./bin/install-dev` and choose **Hype (Development)**.

Run `./bin/test` for the regression suite. After building the tests, the presenter
check can also run with two virtual displays:
Run `./bin/test` for the regression suite: the C++ tests (`hype_tests`, which read exported PDFs back with Qt Pdf from `qt6-webengine`) and the command-line tests (`hype_commands`, in Python). Arguments go to `ctest`, so `./bin/test -R hype_commands` runs only those. Set `HYPE_GUI_TESTS=0` to skip the C++ window-level tests on a machine without multimedia. After building the tests, the presenter check can also run with two virtual displays:

```sh
HYPE_GUI_TESTS=1 QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion \
QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion \
QT_QPA_PLATFORM="offscreen:configfile=$PWD/tests/fixtures/presenter-screens.json" \
./build/tests/hype-tests presenterViewUsesSeparateScreens
./build/hype_tests presenterViewUsesSeparateScreens
```
46 changes: 5 additions & 41 deletions bin/build
Original file line number Diff line number Diff line change
@@ -1,42 +1,6 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
mkdir -p "$ROOT/build"
cd "$ROOT/build"
#!/bin/bash
set -euo pipefail

if [ -n "${HYPE_QMAKE:-}" ]; then
build_qmake=$HYPE_QMAKE
else
# Qt's installer names this executable qmake; distro packages also use qmake6.
# Version sorting keeps 6.10 ahead of 6.9 when both toolchains are installed.
build_qmake=$(
for candidate in "$ROOT"/build/qt/*/gcc_64/bin/qmake "$ROOT"/build/qt/*/gcc_64/bin/qmake6; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
fi
done | sort -V | tail -n 1
)
if [ -z "$build_qmake" ]; then
build_qmake=$(command -v qmake6 || true)
fi
fi
if [ -z "$build_qmake" ]; then
printf 'Qt 6.9 or newer is required; qmake6 was not found.\n' >&2
exit 1
fi
qt_version=$("$build_qmake" -query QT_VERSION)
qt_major=${qt_version%%.*}
qt_minor=${qt_version#*.}
qt_minor=${qt_minor%%.*}
if [ "$qt_major" -lt 6 ] || { [ "$qt_major" -eq 6 ] && [ "$qt_minor" -lt 9 ]; }; then
printf 'Qt 6.9 or newer is required; %s reports Qt %s.\n' "$build_qmake" "$qt_version" >&2
exit 1
fi

"$build_qmake" "$ROOT/hype.pro"
# Objects built with another Qt version cannot be safely reused.
if [ -f Makefile ] && [ "$(cat .qmake-used 2>/dev/null || true)" != "$build_qmake" ]; then
make clean
fi
make -j"$(nproc)"
printf '%s\n' "$build_qmake" > .qmake-used
root="$(cd "$(dirname "$0")/.." && pwd)"
cmake -S "$root" -B "$root/build" -DCMAKE_BUILD_TYPE=Release >/dev/null
cmake --build "$root/build" -j"$(nproc)"
5 changes: 0 additions & 5 deletions bin/install

This file was deleted.

18 changes: 7 additions & 11 deletions bin/test
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
mkdir -p "$ROOT/build/tests"
cd "$ROOT/build/tests"
qmake6 "$ROOT/tests/tests.pro"
make -j"$(nproc)"
# The window-level tests catch the regressions unit tests miss, so they run by default.
# Set HYPE_GUI_TESTS=0 to skip them, e.g. on a machine without multimedia.
if [ "${HYPE_GUI_TESTS-1}" = 0 ]; then unset HYPE_GUI_TESTS; else export HYPE_GUI_TESTS=1; fi
QT_QPA_PLATFORM=offscreen QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion ./hype-tests "$@"
#!/bin/bash
set -euo pipefail

# Arguments go to ctest, so `bin/test -V` shows every Qt test function as it runs.
root="$(cd "$(dirname "$0")/.." && pwd)"
"$root/bin/build"
QT_QPA_PLATFORM=offscreen ctest --test-dir "$root/build" --output-on-failure "$@"
28 changes: 0 additions & 28 deletions hype.pro

This file was deleted.

21 changes: 0 additions & 21 deletions pkgbuild/PKGBUILD

This file was deleted.

3 changes: 2 additions & 1 deletion src/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ ApplicationWindow {
Shortcut { enabled: !win.popupOpen && !deck.compressingImage; sequences: [StandardKey.Open]; onActivated: deck.openDialog() }
Shortcut { enabled: !win.popupOpen && !deck.compressingImage; sequences: [StandardKey.Save]; onActivated: deck.save() }
Shortcut { enabled: !win.popupOpen && !deck.compressingImage; sequences: [StandardKey.SaveAs]; onActivated: deck.saveAs() }
Shortcut { enabled: !win.popupOpen && !deck.compressingImage; sequences: [StandardKey.Quit]; context: Qt.ApplicationShortcut; onActivated: win.close() }
Shortcut { sequence: "Ctrl+E"; enabled: !win.popupOpen && !deck.compressingImage && !win.presenting && !deck.exporting; onActivated: deck.exportDialog("pdf") }
Shortcut { sequence: "Ctrl+Shift+E"; enabled: !win.popupOpen && !deck.compressingImage && !win.presenting && !deck.exporting; onActivated: deck.exportDialog("pptx") }
Shortcut { sequences: ["?", "Shift+?"]; enabled: !win.popupOpen && !deck.compressingImage && !win.presenting && !slideEditor.activeFocus && !sourceEditor.activeFocus; onActivated: shortcutsOverlay.open() }
Expand Down Expand Up @@ -1120,7 +1121,7 @@ ApplicationWindow {
background: Rectangle { color: Qt.alpha(win.ui.panel, 0.92); border.color: win.ui.windowBorder; border.width: 2; radius: win.rounding }
readonly property var groups: [
{ title: "Presentation", keys: [
["Ctrl+N", "New presentation"], ["Ctrl+O", "Open"], ["Ctrl+S", "Save"], ["Ctrl+Shift+S", "Save as"],
["Ctrl+N", "New presentation"], ["Ctrl+O", "Open"], ["Ctrl+S", "Save"], ["Ctrl+Shift+S", "Save as"], ["Ctrl+Q", "Quit"],
["Ctrl+E", "Export as PDF"], ["Ctrl+Shift+E", "Export as PowerPoint"], ["Ctrl+Space / F5", "Present"], ["Shift+F5", "Present from first slide"], ["Esc", "Stop presenting"],
["Space", "Play or pause video while presenting"] ] },
{ title: "View", keys: [
Expand Down
28 changes: 17 additions & 11 deletions src/apptheme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ static QColor contrastInk(const QColor &color) {
.0722 * linear(color.blueF());
return luminance > .179 ? QColor("#111111") : QColor("#ffffff");
}
QList<QPair<QString, QString>> readThemeColors(const QString &path) {
QList<QPair<QString, QString>> colors;
QFile file(path);
if (path.isEmpty() || !file.open(QIODevice::ReadOnly))
return colors;
// Quoted or bare hex with an optional note. Quotes must pair; comments need no gap.
static const QRegularExpression entry(
R"re(^\s*([a-z_]+)\s*=\s*(["']?)(#[0-9a-fA-F]{6})\2\s*(?:#.*)?\s*$)re");
while (!file.atEnd()) {
const auto match = entry.match(QString::fromUtf8(file.readLine()));
if (match.hasMatch())
colors.append({match.captured(1), match.captured(3)});
}
return colors;
}
AppTheme::AppTheme(QObject *parent)
: AppTheme(qEnvironmentVariable("XDG_STATE_HOME", QDir::homePath() + "/.local/state") +
"/omarchy/current",
Expand All @@ -35,17 +50,8 @@ AppTheme::AppTheme(const QString &currentDirectory, QObject *parent)
void AppTheme::reload() {
const QString theme = m_currentDirectory + "/theme", path = theme + "/colors.toml";
QMap<QString, QColor> values;
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
// Quoted or bare hex with an optional note. Quotes must pair; comments need no gap.
const QRegularExpression entry(
R"re(^\s*([a-z_]+)\s*=\s*(["']?)(#[0-9a-fA-F]{6})\2\s*(?:#.*)?\s*$)re");
while (!file.atEnd()) {
const auto match = entry.match(QString::fromUtf8(file.readLine()));
if (match.hasMatch())
values[match.captured(1)] = QColor(match.captured(3));
}
}
for (const auto &[key, value] : readThemeColors(path))
values[key] = QColor(value);
const QColor bg = values.value("background", QColor("#1a1b26"));
const QColor fg = values.value("foreground", QColor("#c0caf5"));
const QColor accent = values.value("accent", QColor("#7aa2f7"));
Expand Down
6 changes: 6 additions & 0 deletions src/apptheme.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
#pragma once
#include <QFileSystemWatcher>
#include <QList>
#include <QObject>
#include <QPair>
#include <QTimer>
#include <QVariantMap>

// The hex colors in an Omarchy colors.toml in file order, as written, quoted
// or bare, with or without a trailing note.
QList<QPair<QString, QString>> readThemeColors(const QString &path);

class AppTheme : public QObject {
Q_OBJECT
Q_PROPERTY(QVariantMap colors READ colors NOTIFY changed)
Expand Down
8 changes: 7 additions & 1 deletion src/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,18 @@ int render(const QStringList &arguments) {
output = name;
else if (QFileInfo(output).isDir())
output = QDir(output).filePath(name);
const QString target = QFileInfo(output).canonicalFilePath();
if (!target.isEmpty() && target == QFileInfo(deck.path()).canonicalFilePath())
return fail("Choose an output other than the presentation itself.");
QDir().mkpath(QFileInfo(output).absolutePath());
// A slide with problems still renders, with the problems on a banner, so it can be looked at.
const auto problems = slideProblemsAt(deck, parsed, number - 1);
QString warning;
const QImage image = paint(deck, number - 1, width, &warning);
if (!image.save(output, "PNG"))
// Written beside and renamed over, so an existing file, or a hard link to the
// presentation, is replaced rather than overwritten in place.
QSaveFile file(output);
if (!file.open(QIODevice::WriteOnly) || !image.save(&file, "PNG") || !file.commit())
return fail("Could not save " + output);
int errors = 0;
QJsonArray list;
Expand Down
Loading
Loading