Internal
Public Access
Add an in-app User Guide and standalone PDF
Adds docs/USER_GUIDE.md, a 10-section end-user guide (getting started, managing/organizing profiles, SSH and RDP connections, session management, settings, troubleshooting). It's embedded into the app binary via a Qt resource file and rendered by a new Help -> User Guide window: a topic sidebar plus content pane, not a single scrolling document, with cross-reference links between sections routed to sidebar selection rather than relying on Qt's Markdown importer's lack of heading anchors. A separate, non-shipped tool (tools/user-guide-pdf/) renders the same source to a standalone PDF via QTextDocument + QPrinter, wrapped by packaging/docs/build-user-guide-pdf.sh. Kept fully outside the main CMake target so Qt6::PrintSupport never becomes a runtime dependency of the shipped app (confirmed via ldd). The PDF itself isn't committed -- generated per release like the platform installers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
/build/
|
||||
/dist/
|
||||
/.flatpak-builder/
|
||||
/build-doc-tool/
|
||||
/docs/USER_GUIDE.pdf
|
||||
|
||||
@@ -81,6 +81,9 @@ add_subdirectory(third_party/FreeRDP EXCLUDE_FROM_ALL)
|
||||
set(ORBITHUB_SOURCES
|
||||
src/about_dialog.cpp
|
||||
src/about_dialog.h
|
||||
src/user_guide_dialog.cpp
|
||||
src/user_guide_dialog.h
|
||||
docs/user_guide.qrc
|
||||
src/app_icon.cpp
|
||||
src/app_icon.h
|
||||
src/main.cpp
|
||||
|
||||
@@ -25,6 +25,9 @@ Progress and milestone details:
|
||||
Latest release (installers for Windows, Linux, and macOS):
|
||||
- [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2)
|
||||
|
||||
User Guide:
|
||||
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
@@ -140,3 +140,19 @@ right-click → **Open** to bypass Gatekeeper's unidentified-developer warning.
|
||||
|
||||
Output path:
|
||||
- `dist/macos/OrbitHub-<version>.dmg`
|
||||
|
||||
## User Guide PDF
|
||||
|
||||
The in-app User Guide (`Help -> User Guide`) is built from
|
||||
`docs/USER_GUIDE.md` and embedded into the app at compile time — no extra
|
||||
step needed for that. A standalone PDF version is generated separately by a
|
||||
small tool (kept out of the main app's dependencies, since it needs
|
||||
`Qt6::PrintSupport`):
|
||||
|
||||
```bash
|
||||
./packaging/docs/build-user-guide-pdf.sh
|
||||
```
|
||||
|
||||
Output path:
|
||||
- `docs/USER_GUIDE.pdf` (not committed to git — a release asset, like the
|
||||
platform installers)
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
## Introduction
|
||||
|
||||
OrbitHub is a native desktop application for organizing connection profiles
|
||||
and launching SSH and RDP sessions from one place, in a single tabbed
|
||||
window. It runs on Windows, Linux, and macOS.
|
||||
|
||||
This guide covers everyday use: creating and organizing profiles,
|
||||
connecting over SSH and RDP, managing active sessions, and what to do when
|
||||
something goes wrong. It does not cover installation or building from
|
||||
source — see the project's `README.md` and `docs/BUILDING.md` for that.
|
||||
|
||||
VNC support is planned but not yet implemented; profiles can be tagged for
|
||||
it, but connecting will show an "unsupported protocol" message for now.
|
||||
|
||||
## Getting Started
|
||||
|
||||
When OrbitHub opens, you land on the **Profiles** tab — a searchable,
|
||||
sortable list of every connection you've saved. Two toolbar controls in
|
||||
the top-right change how that list is presented:
|
||||
|
||||
- **View**: `List` shows every profile in one flat table; `Folders` groups
|
||||
them into the folder tree you've organized them into.
|
||||
- **Sort**: orders the list by Name, Protocol, or Host.
|
||||
|
||||
Along the top of the window, a **Search** box filters by name, host,
|
||||
folder path, or tag as you type, and **Protocol**/**Tag** dropdowns narrow
|
||||
the list further. These filters, plus your last view mode and sort order,
|
||||
are remembered the next time you open OrbitHub.
|
||||
|
||||
Along the bottom of the Profiles tab: **New**, **Edit**, and **Delete**
|
||||
buttons for managing the selected profile. The same actions are available
|
||||
by right-clicking a profile or folder, and from the **File** menu (`New
|
||||
Profile`, `New Folder`).
|
||||
|
||||
To connect, double-click a profile, or select it and press the connect
|
||||
control — this opens a new tab for that session and connects
|
||||
automatically.
|
||||
|
||||
## Managing Profiles
|
||||
|
||||
A profile stores everything needed to reach one remote host. Open **New**
|
||||
(or **Edit** on an existing profile) to fill in:
|
||||
|
||||
- **Name** — a label for the profile; shown on its tab and in the list.
|
||||
- **Host** — hostname or IP address.
|
||||
- **Port** — defaults to `22` for SSH, `3389` for RDP.
|
||||
- **Username** — the account to log in as.
|
||||
- **Domain** — Windows domain for RDP logins (leave blank for local/
|
||||
workgroup accounts or SSH profiles).
|
||||
- **Tags** — free-form, comma-separated labels for filtering and grouping
|
||||
(e.g. `prod, linux, db`).
|
||||
- **Folder** — where the profile lives in the Folders view.
|
||||
- **Protocol** — `SSH`, `RDP`, or `VNC` (VNC is accepted but not yet
|
||||
connectable — see Introduction).
|
||||
|
||||
Fields below Protocol change based on what you pick:
|
||||
|
||||
**SSH**: **Auth Mode** (`Password` or `Private Key`), and if Private Key,
|
||||
a **Private Key** file path with a **Browse** button, plus **Known Hosts**
|
||||
policy — see [Connecting via SSH](#connecting-via-ssh) for what each
|
||||
policy means.
|
||||
|
||||
**RDP**: **RDP Security** and **RDP Performance** — see
|
||||
[Connecting via RDP](#connecting-via-rdp).
|
||||
|
||||
Passwords are never saved in the profile — OrbitHub asks for them each
|
||||
time you connect (unless you've set up private-key SSH auth, which needs
|
||||
no password to be entered per-connection if the key itself has none).
|
||||
|
||||
## Organizing Profiles
|
||||
|
||||
As your profile list grows, two independent tools keep it manageable:
|
||||
|
||||
**Folders.** Switch the Profiles tab to `Folders` view to see profiles
|
||||
grouped into a tree. Create a folder from the **File** menu or by
|
||||
right-clicking in the tree (`New Folder`), and drag any profile onto a
|
||||
folder to move it there. Folders can nest inside other folders.
|
||||
|
||||
**Tags.** Tags are independent of folders — a profile can be in one folder
|
||||
but carry several tags (e.g. `prod`, `linux`, `db` all at once). Use the
|
||||
**Tag** filter dropdown in the toolbar to instantly narrow the list to
|
||||
everything sharing a tag, regardless of which folder it's filed under.
|
||||
|
||||
Combine both with the **Search** box (matches name, host, folder path, or
|
||||
tags) and the **Sort** control (Name / Protocol / Host) to find what you
|
||||
need quickly even with a large profile list.
|
||||
|
||||
## Connecting via SSH
|
||||
|
||||
Double-clicking an SSH profile opens a new tab with an embedded, fully
|
||||
interactive terminal — type directly into it as you would any terminal
|
||||
emulator. A **theme** selector lets you switch between `Dark`, `Light`,
|
||||
and `Solarized Dark`; your choice is remembered for future sessions.
|
||||
|
||||
**Authentication.** Set in the profile itself:
|
||||
- **Password** — OrbitHub prompts for a password each time you connect.
|
||||
It is never stored.
|
||||
- **Private Key** — point at a key file (via the profile's Browse button);
|
||||
no password prompt unless the key itself is passphrase-protected.
|
||||
|
||||
**Known Hosts policy.** This controls how OrbitHub reacts to a server's
|
||||
SSH host key — the mechanism that protects against a different machine
|
||||
silently impersonating a host you've connected to before:
|
||||
|
||||
| Policy | Behavior |
|
||||
|---|---|
|
||||
| `Ask` | Prompts you to confirm trust the first time a host is seen, and on any later change. Recommended default. |
|
||||
| `Accept-new` | Silently trusts a host the first time it's seen, but still stops and warns if a previously-trusted host's key later changes. |
|
||||
| `Strict` | Never trusts an unknown host automatically — the connection fails until you've manually confirmed the host key some other way. |
|
||||
| `Ignore` | Skips host-key checking entirely. Only use this for throwaway/test environments — it removes protection against on-path attacks. |
|
||||
|
||||
Trusted host keys are recorded in your system's normal SSH `known_hosts`
|
||||
file (the same one the `ssh` command line tool uses), so trust decisions
|
||||
made through OrbitHub or a terminal `ssh` session carry over to each
|
||||
other.
|
||||
|
||||
## Connecting via RDP
|
||||
|
||||
RDP sessions render in an embedded display surface inside the tab — no
|
||||
external RDP client window opens. Keyboard and mouse input go straight to
|
||||
the remote desktop while the tab has focus, and resizing the OrbitHub
|
||||
window renegotiates the remote resolution to match. Clipboard content
|
||||
syncs between your machine and the remote session automatically.
|
||||
|
||||
**Authentication** uses the profile's Username/Domain fields; OrbitHub
|
||||
prompts for the password at connect time.
|
||||
|
||||
**RDP Security** controls which transport-security layer is used to
|
||||
negotiate the connection:
|
||||
|
||||
| Mode | Behavior |
|
||||
|---|---|
|
||||
| `Negotiate` | Lets the client and server agree on the strongest mutually-supported option automatically. Recommended default. |
|
||||
| `NLA` | Requires Network Level Authentication (credentials verified before a full session starts) — the modern standard for current Windows versions. |
|
||||
| `TLS` | Requires TLS-only security, without NLA. |
|
||||
| `RDP` | The legacy RDP-native security layer, for older servers that don't support TLS/NLA. |
|
||||
|
||||
**RDP Performance** trades visual fidelity for responsiveness:
|
||||
`Balanced` (default), `Best Quality`, `Best Performance`, or
|
||||
`Auto Detect` (adapts based on the detected connection).
|
||||
|
||||
**Server certificate verification.** The first time you connect to an RDP
|
||||
host, OrbitHub trusts and remembers its TLS certificate — the same
|
||||
trust-on-first-use model SSH uses for host keys. If that certificate ever
|
||||
changes on a later connection, OrbitHub refuses the connection rather than
|
||||
connecting anyway, since a changed certificate can mean either a
|
||||
legitimate server certificate renewal or an active
|
||||
machine-in-the-middle presenting a different one. The event log (see
|
||||
below) shows the specific fingerprints involved. If the change is
|
||||
expected — you rotated the server's certificate yourself — reconnecting
|
||||
after clearing the old entry from FreeRDP's certificate store will trust
|
||||
the new one.
|
||||
|
||||
## Managing Sessions
|
||||
|
||||
Every open connection lives in its own tab in the same window, alongside
|
||||
the Profiles tab. A tab's title and a colored state indicator show
|
||||
whether it's connecting, connected, disconnected, or failed. Closing a
|
||||
tab disconnects that session; opening a profile again starts a fresh one.
|
||||
|
||||
Each session tab includes a collapsible **event log** beneath the
|
||||
connection surface — a timestamped record of connection state changes,
|
||||
warnings, and errors for that session. Controls above the log let you:
|
||||
|
||||
- **Show/Hide Events** — collapse the panel when you don't need it.
|
||||
- **Filter** — a text box to search event text, and an `All` /
|
||||
`Warnings` / `Errors` severity dropdown to narrow what's shown.
|
||||
- **Export Events** — save the current session's full event log to a
|
||||
file, useful when reporting a connection problem.
|
||||
- **Clear Events** — empty the log for that tab.
|
||||
|
||||
## Settings & Preferences
|
||||
|
||||
OrbitHub remembers your preferences across restarts without any separate
|
||||
settings screen — they're saved automatically as you use the app:
|
||||
|
||||
- Profile list: search text, view mode (List/Folders), Protocol/Tag
|
||||
filters, and sort order.
|
||||
- Session tabs: terminal theme choice, and whether the events panel is
|
||||
shown or hidden for new tabs.
|
||||
|
||||
Profile data itself (names, hosts, tags, folder structure, and so on) is
|
||||
stored in a local SQLite database — see the README for its exact path on
|
||||
your platform. Passwords are never part of that stored data.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Host could not be resolved"** — the hostname in the profile can't be
|
||||
looked up by DNS. Check for typos, or try the host's IP address directly
|
||||
to confirm whether it's a DNS problem or something else.
|
||||
|
||||
**SSH connection fails immediately, no prompt** — double-check the
|
||||
profile's Port (default `22`) and that a firewall or network path isn't
|
||||
blocking that port from your machine.
|
||||
|
||||
**RDP: "Authentication failed. Check username and password."** — confirm
|
||||
Username and Domain are correct for the target server; some servers
|
||||
require the domain to be set explicitly even for local accounts.
|
||||
|
||||
**RDP: "RDP security negotiation failed. Try a different RDP security
|
||||
mode."** — the server doesn't support the security mode selected in the
|
||||
profile. Try `Negotiate` first, or a more specific mode if you know what
|
||||
the server requires.
|
||||
|
||||
**RDP: connection refused with a certificate-changed message** — see
|
||||
[Connecting via RDP](#connecting-via-rdp) above; this is expected,
|
||||
protective behavior, not a bug, whenever a previously-trusted server's
|
||||
certificate is replaced.
|
||||
|
||||
**SSH: connection hangs at "Ask" waiting for host-key trust** — check
|
||||
that policy's behavior under
|
||||
[Connecting via SSH](#connecting-via-ssh); switching to `Accept-new` avoids
|
||||
the prompt for genuinely new hosts while still protecting against a later
|
||||
key change.
|
||||
|
||||
If none of this covers what you're seeing, a session's exported event log
|
||||
(see Managing Sessions) is the most useful thing to include when asking
|
||||
for help or filing an issue.
|
||||
|
||||
## About & Support
|
||||
|
||||
OrbitHub is open source under the MIT license. Source code, issue
|
||||
tracking, and releases are hosted at
|
||||
[git.darksingularity.org/DarkSingularity/orbithub](https://git.darksingularity.org/DarkSingularity/orbithub).
|
||||
|
||||
For a list of bundled third-party libraries and their licenses, see
|
||||
**Help → About OrbitHub** inside the app.
|
||||
|
||||
To report a bug or request a feature, open an issue at
|
||||
[git.darksingularity.org/DarkSingularity/orbithub/issues](https://git.darksingularity.org/DarkSingularity/orbithub/issues).
|
||||
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/docs">
|
||||
<file>USER_GUIDE.md</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BUILD_DIR="${1:-$ROOT_DIR/build-doc-tool}"
|
||||
OUTPUT_PATH="${2:-$ROOT_DIR/docs/USER_GUIDE.pdf}"
|
||||
|
||||
cmake -S "$ROOT_DIR/tools/user-guide-pdf" -B "$BUILD_DIR" -G Ninja
|
||||
cmake --build "$BUILD_DIR"
|
||||
|
||||
QT_QPA_PLATFORM=offscreen "$BUILD_DIR/user-guide-pdf" \
|
||||
"$ROOT_DIR/docs/USER_GUIDE.md" \
|
||||
"$OUTPUT_PATH"
|
||||
|
||||
echo "Created $OUTPUT_PATH"
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "about_dialog.h"
|
||||
#include "profiles_window.h"
|
||||
#include "user_guide_dialog.h"
|
||||
#include <QApplication>
|
||||
#include "session_tab.h"
|
||||
|
||||
@@ -182,6 +183,14 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
||||
QAction* userGuideAction = helpMenu->addAction(QStringLiteral("User Guide"));
|
||||
connect(userGuideAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() {
|
||||
UserGuideDialog dialog(this);
|
||||
dialog.exec();
|
||||
});
|
||||
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
|
||||
connect(aboutAction,
|
||||
&QAction::triggered,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "user_guide_dialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFile>
|
||||
#include <QListWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTextBrowser>
|
||||
#include <QTextStream>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QString slugify(const QString& title)
|
||||
{
|
||||
QString slug;
|
||||
slug.reserve(title.size());
|
||||
bool lastWasHyphen = false;
|
||||
for (const QChar& ch : title) {
|
||||
if (ch.isLetterOrNumber()) {
|
||||
slug += ch.toLower();
|
||||
lastWasHyphen = false;
|
||||
} else if (!lastWasHyphen && !slug.isEmpty()) {
|
||||
slug += QLatin1Char('-');
|
||||
lastWasHyphen = true;
|
||||
}
|
||||
}
|
||||
while (slug.endsWith(QLatin1Char('-'))) {
|
||||
slug.chop(1);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UserGuideDialog::UserGuideDialog(QWidget* parent)
|
||||
: QDialog(parent), m_sectionList(nullptr), m_browser(nullptr)
|
||||
{
|
||||
setWindowTitle(QStringLiteral("OrbitHub User Guide"));
|
||||
setWindowIcon(QApplication::windowIcon());
|
||||
resize(900, 640);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(16, 16, 16, 16);
|
||||
layout->setSpacing(12);
|
||||
|
||||
auto* splitter = new QSplitter(Qt::Horizontal, this);
|
||||
|
||||
m_sectionList = new QListWidget(splitter);
|
||||
m_sectionList->setMaximumWidth(220);
|
||||
|
||||
m_browser = new QTextBrowser(splitter);
|
||||
m_browser->setOpenExternalLinks(false);
|
||||
m_browser->setOpenLinks(false);
|
||||
|
||||
splitter->addWidget(m_sectionList);
|
||||
splitter->addWidget(m_browser);
|
||||
splitter->setStretchFactor(0, 0);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
|
||||
layout->addWidget(splitter, 1);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
loadSections();
|
||||
|
||||
connect(m_sectionList,
|
||||
&QListWidget::currentRowChanged,
|
||||
this,
|
||||
[this](int row) { showSection(row); });
|
||||
connect(m_browser, &QTextBrowser::anchorClicked, this, &UserGuideDialog::onAnchorClicked);
|
||||
|
||||
if (!m_sections.isEmpty()) {
|
||||
m_sectionList->setCurrentRow(0);
|
||||
}
|
||||
}
|
||||
|
||||
void UserGuideDialog::loadSections()
|
||||
{
|
||||
QFile file(QStringLiteral(":/docs/USER_GUIDE.md"));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
const QString content = stream.readAll();
|
||||
const QStringList lines = content.split(QLatin1Char('\n'));
|
||||
|
||||
QString currentTitle;
|
||||
QStringList currentLines;
|
||||
|
||||
auto flushSection = [this, ¤tTitle, ¤tLines]() {
|
||||
if (currentTitle.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Section section;
|
||||
section.title = currentTitle;
|
||||
section.anchor = slugify(currentTitle);
|
||||
section.markdown = currentLines.join(QLatin1Char('\n'));
|
||||
m_sections.append(section);
|
||||
};
|
||||
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith(QStringLiteral("## "))) {
|
||||
flushSection();
|
||||
currentTitle = line.mid(3).trimmed();
|
||||
currentLines.clear();
|
||||
}
|
||||
currentLines.append(line);
|
||||
}
|
||||
flushSection();
|
||||
|
||||
for (const Section& section : m_sections) {
|
||||
m_sectionList->addItem(section.title);
|
||||
}
|
||||
}
|
||||
|
||||
void UserGuideDialog::showSection(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_sections.size()) {
|
||||
return;
|
||||
}
|
||||
m_browser->setMarkdown(m_sections[index].markdown);
|
||||
}
|
||||
|
||||
void UserGuideDialog::onAnchorClicked(const QUrl& url)
|
||||
{
|
||||
if (!url.scheme().isEmpty()) {
|
||||
QDesktopServices::openUrl(url);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString fragment = url.fragment();
|
||||
if (fragment.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_sections.size(); ++i) {
|
||||
if (m_sections[i].anchor == fragment) {
|
||||
m_sectionList->setCurrentRow(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef ORBITHUB_USER_GUIDE_DIALOG_H
|
||||
#define ORBITHUB_USER_GUIDE_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class QListWidget;
|
||||
class QTextBrowser;
|
||||
class QUrl;
|
||||
|
||||
class UserGuideDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserGuideDialog(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
struct Section {
|
||||
QString title;
|
||||
QString anchor;
|
||||
QString markdown;
|
||||
};
|
||||
|
||||
void loadSections();
|
||||
void showSection(int index);
|
||||
void onAnchorClicked(const QUrl& url);
|
||||
|
||||
QListWidget* m_sectionList;
|
||||
QTextBrowser* m_browser;
|
||||
QVector<Section> m_sections;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(OrbitHubUserGuidePdf LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets PrintSupport)
|
||||
|
||||
add_executable(user-guide-pdf main.cpp)
|
||||
target_link_libraries(user-guide-pdf PRIVATE Qt6::Widgets Qt6::PrintSupport)
|
||||
@@ -0,0 +1,44 @@
|
||||
#include <QApplication>
|
||||
#include <QFile>
|
||||
#include <QPageSize>
|
||||
#include <QPrinter>
|
||||
#include <QTextDocument>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
if (argc != 3) {
|
||||
std::fprintf(stderr, "Usage: %s <input.md> <output.pdf>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const QString inputPath = QString::fromLocal8Bit(argv[1]);
|
||||
const QString outputPath = QString::fromLocal8Bit(argv[2]);
|
||||
|
||||
QFile input(inputPath);
|
||||
if (!input.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
std::fprintf(stderr, "Could not open %s\n", qPrintable(inputPath));
|
||||
return 1;
|
||||
}
|
||||
|
||||
QTextStream stream(&input);
|
||||
const QString markdown = stream.readAll();
|
||||
|
||||
QTextDocument document;
|
||||
document.setMarkdown(markdown);
|
||||
|
||||
QPrinter printer(QPrinter::HighResolution);
|
||||
printer.setOutputFormat(QPrinter::PdfFormat);
|
||||
printer.setPageSize(QPageSize(QPageSize::Letter));
|
||||
printer.setPageMargins(QMarginsF(50, 50, 50, 50), QPageLayout::Point);
|
||||
printer.setOutputFileName(outputPath);
|
||||
|
||||
document.print(&printer);
|
||||
|
||||
std::printf("Wrote %s\n", qPrintable(outputPath));
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user