40 Commits
Author SHA1 Message Date
ksmithandClaude Sonnet 5 6c8146e79c docs: close out Milestone 9 and document Windows/macOS packaging
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:04:59 -06:00
ksmithandClaude Sonnet 5 aa812b0da7 macOS: fix launch crash, missing dylibs, and dmg volume icon
The app crashed at launch with "cannot be opened because of a
problem" because macdeployqt invalidates the code signature when it
rewrites library load paths; re-sign (ad hoc) after macdeployqt runs.

Also fixes the underlying cause of a Library-missing crash: orbithub's
INSTALL_RPATH used the Linux linker token $ORIGIN, which dyld does not
understand, and the bundled FreeRDP/WinPR/KodoTerm dylibs installed to
a sibling lib/orbithub/ directory outside OrbitHub.app rather than
Contents/Frameworks, so they were never copied into the dmg at all.
Both are now APPLE-specific, matching macdeployqt's own layout
(Contents/Frameworks, @executable_path/../Frameworks).

build-dmg.sh now also sets a custom volume icon (via SetFile or the
fileicon brew formula, whichever is available) instead of the
generic disk image icon, degrading gracefully if neither is present.

Also includes the StartupWMClass desktop-file fix from the prior
commit's message, which was never actually staged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:00:46 -06:00
ksmithandClaude Sonnet 5 3840ea9f62 Linux: fix taskbar pin matching and launcher icon mismatch
Add StartupWMClass=OrbitHub to the desktop file so panels (GNOME
Shell, Cinnamon, KDE) can match the running window to the pinned
launcher instead of creating a duplicate taskbar entry.

Replace the hand-authored, stale launcher SVG with PNG icons
rendered directly from createOrbitHubAppIcon() at each hicolor
theme size, so the pinned/menu icon matches the actual app icon
shown in the running window, Windows .ico, and macOS .icns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:50:25 -06:00
ksmithandClaude Sonnet 5 7ce7260305 packaging: rename app ID to org.darksingularity.OrbitHub
The reverse-DNS app ID should reflect a domain we actually control.
Renames the desktop file, AppStream metainfo, icon, Flatpak manifest,
and macOS bundle identifier from io.orbithub.OrbitHub to
org.darksingularity.OrbitHub. Also fixes a stale homepage URL in the
metainfo file pointing at an old git host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:39:06 -06:00
ksmithandClaude Sonnet 5 48adcf33ee packaging: add macOS dmg build script
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:29:35 -06:00
ksmithandClaude Sonnet 5 21e7a39c53 docs: add screenshots section to README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:23:32 -06:00
ksmithandClaude Sonnet 5 f126f4b55c Add Windows installer via Inno Setup
New packaging/windows/orbithub.iss packages the build directory's
top-level contents directly (orbithub.exe, *.dll, platforms/,
sqldrivers/) rather than going through cmake --install: vcpkg's own
applocal DLL deployment already drops every required runtime dependency
flat into build/ on every build (confirmed repeatedly this session), and
the existing private-runtime-lib install() rule uses an RPATH-style Unix
convention (lib/orbithub/) that doesn't work on Windows, where DLLs must
sit next to the exe. Produces a single OrbitHub-Setup-<version>.exe with
a Start Menu entry, optional desktop shortcut, and uninstaller.

build-installer.ps1 reads the project version from build/CMakeCache.txt
(same approach build-deb.sh already uses) and drives ISCC.exe.

Code signing is out of scope for now (no certificate available), so the
installer will trigger a SmartScreen warning until one is purchased.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:57 -06:00
ksmithandClaude Sonnet 5 4daf5cfc65 docs: close out Milestone 7 (cross-platform protocol hardening)
Linux, macOS, and Windows have all been validated against the current
feature set (SSH, RDP display/keyboard/clipboard/cursor, Tab forwarding,
key repeat, profile dialog, single-window UI), with the platform-specific
bugs found along the way fixed and tracked individually on the issue
tracker (#8-#16). VNC validation remains out of scope while Milestone 6
stays deferred.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:50 -06:00
ksmithandClaude Sonnet 5 0990049241 Fix Flatpak build issues: EOL runtime and icon naming mismatch
- Bump the KDE runtime/SDK from 6.8 (now end-of-life per Flathub) to the
  current stable 6.10.
- The .desktop file's Icon=orbithub didn't match the app ID
  (io.orbithub.OrbitHub), so Flatpak's export step skipped it entirely
  ("Icon referenced in desktop file but not exported"). Use the app-ID-
  matching icon name everywhere instead, and drop the now-redundant
  renamed orbithub.svg install() rule that existed only for the old name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:45 -06:00
ksmithandClaude Sonnet 5 6d5133e1bd Switch to date-based version scheme
Release versions now follow v<year>.<month>.<day>, with a .N suffix
appended only if a second release happens the same day (e.g. v2026.9.8,
then v2026.9.8.2 for a same-day follow-up). Replaces the placeholder
0.1.0 the project started with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:33 -06:00
ksmithandClaude Sonnet 5 9d88b74c04 Fix macOS icon showing as unrecognized-file placeholder
The .icns generated via Pillow's ICNS writer wasn't rendering in Finder
(showed the "unavailable" circle-slash placeholder instead), likely due
to Pillow's ICNS encoding not matching what Icon Services expects. Rebuilt
the file by hand instead, using Apple's documented icns binary format
directly (magic header + OSType-tagged PNG chunks: icp4/icp5/icp6 for
16/32/64px, ic07-ic10 for 128/256/512/1024px) with the exact same PNGs
already rendered from createOrbitHubAppIcon(), sidestepping Pillow's
ICNS writer entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 12:41:08 -06:00
ksmithandClaude Sonnet 5 49ee12f5a1 Fix macOS configure error: add BUNDLE DESTINATION for install(TARGETS)
CMake requires an explicit BUNDLE DESTINATION for install(TARGETS) when
the target has MACOSX_BUNDLE set, or configure fails with "install
TARGETS give no BUNDLE DESTINATION for MACOSX_BUNDLE executable target".
The RUNTIME clause still covers Linux/Windows; BUNDLE only applies when
the target actually is a bundle (macOS), so no platform guard is needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 12:37:47 -06:00
ksmithandClaude Sonnet 5 ac3cebc9ae macOS: build a proper .app bundle with an embedded icon
Same root cause as the earlier Windows exe icon fix: add_executable()
never set MACOSX_BUNDLE, so on macOS this produced a bare Mach-O binary
rather than a proper .app bundle -- and without a bundle there's no
Info.plist/.icns mechanism for Finder to show a custom icon at all.

Added packaging/macos/orbithub.icns, rendered at up to 1024px directly
from the same createOrbitHubAppIcon() logic used at runtime (via a
one-off export tool, not committed) so it matches what the app actually
looks like. CMake's MACOSX_BUNDLE keyword and MACOSX_BUNDLE_ICON_FILE
wire it into an auto-generated Info.plist; both keywords are no-ops on
other platforms. The profile database uses QStandardPaths::AppDataLocation,
not the executable's own path, so this doesn't affect where existing
profiles are found.

Build output on macOS changes from build/orbithub to build/orbithub.app
(launch with `open build/orbithub.app`); docs/BUILDING.md updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 12:35:52 -06:00
ksmithandClaude Sonnet 5 8f83b0c8d9 Fix RDP key repeat: forward auto-repeat presses to the remote server
RdpDisplayWidget::keyPressEvent() dropped every event where
QKeyEvent::isAutoRepeat() was true, which discards the entire repeat
stream Qt generates while a key is held -- so holding a key only ever
produced a single keystroke on the remote machine. Auto-repeat presses
need to reach the remote server so it can perform its own typematic
repeat, exactly as a physical keyboard held down would; only release
events should filter isAutoRepeat() (kept as-is), since Qt uses a
synthetic release/press pair purely to normalize platform auto-repeat
quirks, and forwarding that synthetic release would send a spurious
key-up for a key still physically held.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:54:50 -06:00
ksmithandClaude Sonnet 5 ac1deac148 Windows: embed app icon into the executable file
The app's icon only appeared at runtime (window/taskbar), set programmatically
via QApplication::setWindowIcon(). Explorer showed the generic default icon
for orbithub.exe itself, since that requires an icon baked into the PE file
as a Windows resource at build time -- a completely separate mechanism.

Added packaging/windows/orbithub.ico, rendered directly from the same
createOrbitHubAppIcon() logic used at runtime (via a one-off export tool,
not committed) so the file icon matches what the app actually looks like.
A minimal .rc resource file embeds it, compiled in only for WIN32 builds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 09:49:38 -06:00
ksmithandClaude Sonnet 5 b28adf8cbe Fix RDP host resolution failure on Windows: initialize Winsock
Every RDP connect attempt on Windows failed instantly with
ERRCONNECT_DNS_NAME_NOT_FOUND, even for a literal IP address. Root cause:
Winsock requires WSAStartup() to be called once by the process before any
socket/getaddrinfo call will succeed, and nothing in OrbitHub was calling
it (Qt Network isn't used, so Qt never does it either). FreeRDP's own
reference Windows client (wf_client.c) pairs WSAStartup with
freerdp_handle_signals() in its startup init for exactly this reason.
WinPR provides a portable no-op WSAStartup shim on POSIX, so this can be
called unconditionally cross-platform alongside the existing signal-handler
fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 09:37:02 -06:00
ksmithandClaude Sonnet 5 317df68d1f Fix RDP connect crash on Windows: initialize FreeRDP signal handling
Every reference FreeRDP client (X11, Windows, macOS, SDL, Wayland, Android)
calls freerdp_handle_signals() once at startup before connecting. OrbitHub
never did. On Windows this is the only place that initializes a global
CRITICAL_SECTION used internally by freerdp_add_signal_cleanup_handler(),
which freerdp_connect() calls automatically. Skipping it leaves that lock
zero-initialized -- invalid on Windows, but silently tolerated on POSIX,
which is why this never surfaced in Linux testing. The result was a crash
(access violation inside EnterCriticalSection) on every RDP connect attempt
on Windows. Call it once, guarded, before the first connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 09:27:25 -06:00
ksmithandClaude Sonnet 5 fce69b8be9 Forward Tab/Shift+Tab to remote sessions instead of local focus navigation
QWidget's default handling intercepts Tab/Shift+Tab for focus-chain
navigation before they ever reach keyPressEvent(), so pressing Tab in an
RDP session moved focus to the OrbitHub UI (e.g. the Show Events button)
instead of reaching the remote machine. KodoTerm (SSH) already overrides
focusNextPrevChild() to opt out of this; RdpDisplayWidget and TerminalView
did not. Both already handle Key_Tab in their own keyPressEvent, so this
is enough to let it reach them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 08:50:01 -06:00
ksmithandClaude Sonnet 5 f3ea7f12a3 Windows: deploy sqlite3.dll and mark orbithub as a GUI (WIN32) executable
Two more Windows launch issues found alongside the Qt plugin deployment:
vcpkg's qtbase[sql-sqlite] links the QSQLITE driver plugin against vcpkg's
own shared sqlite3 port rather than bundling it, so the plugin was found
but failed to load without sqlite3.dll alongside the executable. Separately,
plain add_executable() defaults to the console subsystem on Windows, so a
command-prompt window appeared behind the GUI on every launch; add_executable
needs the WIN32 keyword to mark it as a GUI app (a no-op on other platforms).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 08:27:50 -06:00
ksmithandClaude Sonnet 5 30f0134748 Deploy Qt platform and SQLite plugins automatically on Windows
Qt loads its platform integration and SQL driver plugins dynamically
(QFactoryLoader) rather than importing them at link time, so they never
show up in orbithub.exe's import table and vcpkg's automatic DLL
deployment never copies them. Without this, the app fails to launch on
Windows with "Could not find the Qt platform plugin" or "can not load
requested driver 'QSQLITE'". Copy them out explicitly via Qt's own
exported plugin targets after each build, plus matching install() rules
for future packaging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 08:20:17 -06:00
ksmithandClaude Sonnet 5 a89748be67 Merge Profiles and Session windows into a single window
ProfilesWindow and SessionWindow were two independent top-level windows,
coordinated through a QPointer and manual show/create-or-reuse logic, with
duplicated Help menus and a path where Quit didn't actually quit if a
SessionWindow happened to be open. ProfilesWindow is now an embedded QWidget
shown as a permanent, unclosable "Profiles" tab inside SessionWindow's tab
widget; SessionWindow is the app's sole top-level window. Double-clicking a
profile opens a session tab in the same window instead of finding-or-creating
a second one, and closing all session tabs falls back to the Profiles tab
rather than closing the app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 15:59:53 -06:00
ksmithandClaude Sonnet 5 4b4b7d68f2 Fix profile dialog form fields collapsing to sizeHint on macOS
QFormLayout without an explicit field growth policy falls back to the
active Qt style's default, which differs per platform: Linux's Fusion/Breeze
styles expand fields to fill the row, but macOS's native style defaults to
FieldsStayAtSizeHint, leaving fields small with empty space beside them.
Set the policy explicitly so the dialog renders consistently everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 15:18:09 -06:00
ksmithandClaude Sonnet 5 793fdd9366 RDP session fixes: correct keyboard scancodes, clipboard sync, cursor shapes
Fix RDP keyboard input using FreeRDP's authoritative X11-keycode-to-scancode
table instead of ad hoc bit math, which misread punctuation keys as unrelated
letter keys (e.g. apostrophe as B) because X11 keycode numbering only
coincidentally overlaps PC/AT scancodes.

Add bidirectional clipboard sync (CF_UNICODETEXT) over the cliprdr channel,
and RDP pointer/cursor shape sync so the local cursor reflects what the
remote OS wants displayed (resize handles, text I-beam, etc.) instead of
staying a static arrow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 14:53:03 -06:00
ksmithandClaude Sonnet 5 27cc3a3bb2 Session UX: inline password prompt and terminal font size controls
Replace the modal RDP/SSH password dialog with an inline prompt bar
embedded in the session tab instead of a separate popup window. Add
per-tab terminal font size controls (increase/decrease/reset/set
exact point size) via the tab context menu, with the chosen size
persisted across sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:02:03 -06:00
ksmith c1c23d115a packaging: add deb/flatpak build flow and desktop metadata 2026-03-03 20:46:03 -07:00
ksmith f54c2e9bcd docs: update Windows build requirements and refresh milestone tag 2026-03-03 20:21:38 -07:00
ksmith ae9928782d docs: close out milestone 8 and set milestone 9 as current 2026-03-03 20:16:43 -07:00
ksmith 2485ffb14f docs: clarify Qt6 LGPLv3 licensing links in README 2026-03-03 20:13:12 -07:00
ksmith eadcdd7f10 Milestone 8 UX: folder tree workflows, about dialog, and app icon polish 2026-03-03 20:07:41 -07:00
ksmith 36006bd4aa Milestone 5: deliver embedded RDP sessions and lifecycle hardening 2026-03-03 18:59:26 -07:00
ksmith 230a401386 Refine roadmap: Milestones 5 and 6 as full RDP/VNC delivery 2026-03-01 11:11:26 -07:00
ksmith e77f2598b9 Document post-M4 roadmap with separate RDP and VNC milestones 2026-03-01 11:06:30 -07:00
ksmith 2b25f805cd Complete Milestone 4 interactive SSH session UX 2026-03-01 11:00:31 -07:00
ksmith 776ddc1a53 Integrate KodoTerm for SSH terminal sessions 2026-03-01 10:36:06 -07:00
ksmith c3369b8e48 Improve terminal theming, cursor UX, and size negotiation 2026-03-01 10:14:43 -07:00
ksmith 20ee48db32 Add ANSI color rendering and terminal themes 2026-03-01 10:08:04 -07:00
ksmith 2b4f498259 Allow direct terminal typing and collapsible session panels 2026-03-01 09:58:21 -07:00
ksmith 614d31fa71 Restore built-in askpass helper for SSH password auth 2026-03-01 09:53:17 -07:00
ksmith ceed19d517 Draft Milestone 4 scope and mark progress in docs 2026-03-01 09:50:08 -07:00
ksmith 2ea712db36 Start Milestone 4 interactive SSH terminal and host-key flow 2026-03-01 09:50:03 -07:00
3475 changed files with 751851 additions and 387 deletions
+2
View File
@@ -1 +1,3 @@
/build/
/dist/
/.flatpak-builder/
+224 -4
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 0.1.0 LANGUAGES CXX)
project(OrbitHub VERSION 2026.9.8 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -10,16 +10,86 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
include(GNUInstallDirs)
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql)
qt_standard_project_setup()
add_executable(orbithub
add_subdirectory(third_party/KodoTerm)
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/CMakeLists.txt")
message(FATAL_ERROR "Vendored FreeRDP source is missing at third_party/FreeRDP")
endif()
# Pin FreeRDP build to the vendored source tree so headers/libs are always from the same revision.
set(FREERDP_UNIFIED_BUILD ON CACHE BOOL "" FORCE)
set(WITH_MANPAGES OFF CACHE BOOL "" FORCE)
set(WITH_SAMPLE OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING_INTERNAL OFF CACHE BOOL "" FORCE)
set(WITH_CLIENT_COMMON ON CACHE BOOL "" FORCE)
set(WITH_CLIENT OFF CACHE BOOL "" FORCE)
set(WITH_CLIENT_SDL OFF CACHE BOOL "" FORCE)
set(WITH_CLIENT_INTERFACE OFF CACHE BOOL "" FORCE)
set(WITH_SERVER OFF CACHE BOOL "" FORCE)
set(WITH_CHANNELS ON CACHE BOOL "" FORCE)
set(WITH_CLIENT_CHANNELS ON CACHE BOOL "" FORCE)
set(WITH_FUSE OFF CACHE BOOL "" FORCE)
set(CHANNEL_DRDYNVC ON CACHE BOOL "" FORCE)
set(CHANNEL_DRDYNVC_CLIENT ON CACHE BOOL "" FORCE)
set(CHANNEL_DISP ON CACHE BOOL "" FORCE)
set(CHANNEL_DISP_CLIENT ON CACHE BOOL "" FORCE)
set(CHANNEL_AINPUT OFF CACHE BOOL "" FORCE)
set(CHANNEL_AUDIN OFF CACHE BOOL "" FORCE)
set(CHANNEL_CLIPRDR ON CACHE BOOL "" FORCE)
set(CHANNEL_CLIPRDR_CLIENT ON CACHE BOOL "" FORCE)
set(CHANNEL_DRIVE OFF CACHE BOOL "" FORCE)
set(CHANNEL_ECHO OFF CACHE BOOL "" FORCE)
set(CHANNEL_ENCOMSP OFF CACHE BOOL "" FORCE)
set(CHANNEL_GEOMETRY OFF CACHE BOOL "" FORCE)
set(CHANNEL_LOCATION OFF CACHE BOOL "" FORCE)
set(CHANNEL_PARALLEL OFF CACHE BOOL "" FORCE)
set(CHANNEL_PRINTER OFF CACHE BOOL "" FORCE)
set(CHANNEL_RAIL OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPDR OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPEAR OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPECAM OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPEI OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPEMSC OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPGFX OFF CACHE BOOL "" FORCE)
set(CHANNEL_RDPSND OFF CACHE BOOL "" FORCE)
set(CHANNEL_REMDESK OFF CACHE BOOL "" FORCE)
set(CHANNEL_SERIAL OFF CACHE BOOL "" FORCE)
set(CHANNEL_SMARTCARD OFF CACHE BOOL "" FORCE)
set(CHANNEL_SSHAGENT OFF CACHE BOOL "" FORCE)
set(CHANNEL_TELEMETRY OFF CACHE BOOL "" FORCE)
set(CHANNEL_URBDRC OFF CACHE BOOL "" FORCE)
set(CHANNEL_VIDEO OFF CACHE BOOL "" FORCE)
set(WITH_FFMPEG OFF CACHE BOOL "" FORCE)
set(WITH_DSP_FFMPEG OFF CACHE BOOL "" FORCE)
set(WITH_VIDEO_FFMPEG OFF CACHE BOOL "" FORCE)
set(WITH_CAIRO OFF CACHE BOOL "" FORCE)
set(WITH_SWSCALE OFF CACHE BOOL "" FORCE)
set(WITH_JPEG OFF CACHE BOOL "" FORCE)
set(WITH_KRB5 OFF CACHE BOOL "" FORCE)
set(WITH_UNICODE_BUILTIN ON CACHE BOOL "" FORCE)
set(WITH_WINPR_TOOLS OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/FreeRDP EXCLUDE_FROM_ALL)
set(ORBITHUB_SOURCES
src/about_dialog.cpp
src/about_dialog.h
src/app_icon.cpp
src/app_icon.h
src/main.cpp
src/profile_dialog.cpp
src/profile_dialog.h
src/profile_repository.cpp
src/profile_repository.h
src/profiles_tree_widget.cpp
src/profiles_tree_widget.h
src/profiles_window.cpp
src/profiles_window.h
src/session_backend.h
@@ -27,14 +97,164 @@ add_executable(orbithub
src/session_backend_factory.h
src/session_tab.cpp
src/session_tab.h
src/rdp_display_widget.cpp
src/rdp_display_widget.h
src/terminal_view.cpp
src/terminal_view.h
src/session_window.cpp
src/session_window.h
src/rdp_session_backend.cpp
src/rdp_session_backend.h
src/ssh_session_backend.cpp
src/ssh_session_backend.h
src/unsupported_session_backend.cpp
src/unsupported_session_backend.h
)
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
if(WIN32)
list(APPEND ORBITHUB_SOURCES packaging/windows/orbithub.rc)
endif()
install(TARGETS orbithub RUNTIME DESTINATION bin)
if(APPLE)
list(APPEND ORBITHUB_SOURCES packaging/macos/orbithub.icns)
set_source_files_properties(packaging/macos/orbithub.icns PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
set(MACOSX_BUNDLE_ICON_FILE orbithub.icns)
set(MACOSX_BUNDLE_BUNDLE_NAME "OrbitHub")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.darksingularity.OrbitHub")
set(MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}")
endif()
add_executable(orbithub WIN32 MACOSX_BUNDLE ${ORBITHUB_SOURCES})
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
if(TARGET freerdp AND TARGET winpr)
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
target_include_directories(orbithub PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/winpr/include
${CMAKE_CURRENT_BINARY_DIR}/third_party/FreeRDP/include
${CMAKE_CURRENT_BINARY_DIR}/third_party/FreeRDP/winpr/include
)
target_link_libraries(orbithub PRIVATE freerdp winpr)
if(TARGET freerdp-client)
target_link_libraries(orbithub PRIVATE freerdp-client)
endif()
else()
message(FATAL_ERROR "Vendored FreeRDP targets were not produced as expected.")
endif()
# Qt loads platform integration and SQL driver plugins dynamically at
# runtime (QFactoryLoader), so they never appear in orbithub.exe's import
# table. vcpkg's automatic DLL deployment only follows the import table, so
# these plugins have to be copied out explicitly or the app fails to start
# with "Could not find the Qt platform plugin" / "can not load requested
# driver 'QSQLITE'".
if(WIN32)
if(TARGET Qt6::QWindowsIntegrationPlugin)
add_custom_command(TARGET orbithub POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:orbithub>/platforms"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:Qt6::QWindowsIntegrationPlugin>"
"$<TARGET_FILE_DIR:orbithub>/platforms/"
COMMENT "Deploying Qt Windows platform plugin"
)
install(FILES "$<TARGET_FILE:Qt6::QWindowsIntegrationPlugin>"
DESTINATION "${CMAKE_INSTALL_BINDIR}/platforms"
)
endif()
if(TARGET Qt6::QSQLiteDriverPlugin)
add_custom_command(TARGET orbithub POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:orbithub>/sqldrivers"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:Qt6::QSQLiteDriverPlugin>"
"$<TARGET_FILE_DIR:orbithub>/sqldrivers/"
COMMENT "Deploying Qt SQLite driver plugin"
)
install(FILES "$<TARGET_FILE:Qt6::QSQLiteDriverPlugin>"
DESTINATION "${CMAKE_INSTALL_BINDIR}/sqldrivers"
)
# vcpkg's qtbase[sql-sqlite] links the plugin against vcpkg's own
# shared sqlite3 port rather than bundling SQLite into the plugin,
# so the plugin fails to load ("can not load requested driver
# 'QSQLITE'") unless sqlite3.dll also ships next to the executable.
find_file(ORBITHUB_SQLITE3_DLL
NAMES sqlite3.dll
PATHS ${CMAKE_PREFIX_PATH}
PATH_SUFFIXES bin
)
if(ORBITHUB_SQLITE3_DLL)
add_custom_command(TARGET orbithub POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${ORBITHUB_SQLITE3_DLL}"
"$<TARGET_FILE_DIR:orbithub>/"
COMMENT "Deploying sqlite3.dll (Qt SQLite plugin dependency)"
)
install(FILES "${ORBITHUB_SQLITE3_DLL}" DESTINATION "${CMAKE_INSTALL_BINDIR}")
endif()
endif()
endif()
if(APPLE)
# Mirrors macdeployqt's own layout for Qt frameworks: dylibs live in
# Contents/Frameworks inside the bundle, found via @executable_path
# (the macOS/dyld equivalent of Linux's $ORIGIN token, which dyld
# does not understand).
set_target_properties(orbithub PROPERTIES
INSTALL_RPATH "@executable_path/../Frameworks"
INSTALL_RPATH_USE_LINK_PATH ON
)
else()
set_target_properties(orbithub PROPERTIES
BUILD_RPATH_USE_ORIGIN ON
INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}/orbithub"
INSTALL_RPATH_USE_LINK_PATH ON
)
endif()
install(TARGETS orbithub
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
BUNDLE DESTINATION .
)
if(APPLE)
set(ORBITHUB_PRIVATE_LIB_DESTINATION "OrbitHub.app/Contents/Frameworks")
else()
set(ORBITHUB_PRIVATE_LIB_DESTINATION "${CMAKE_INSTALL_LIBDIR}/orbithub")
endif()
set(ORBITHUB_RUNTIME_TARGETS KodoTerm freerdp winpr)
if(TARGET freerdp-client)
list(APPEND ORBITHUB_RUNTIME_TARGETS freerdp-client)
endif()
foreach(runtime_target IN LISTS ORBITHUB_RUNTIME_TARGETS)
if(TARGET ${runtime_target})
install(TARGETS ${runtime_target}
RUNTIME DESTINATION ${ORBITHUB_PRIVATE_LIB_DESTINATION}
LIBRARY DESTINATION ${ORBITHUB_PRIVATE_LIB_DESTINATION}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
endif()
endforeach()
install(FILES
packaging/linux/org.darksingularity.OrbitHub.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
)
foreach(icon_size IN ITEMS 16 24 32 48 64 128 256)
install(FILES
packaging/linux/icons/${icon_size}x${icon_size}/apps/org.darksingularity.OrbitHub.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${icon_size}x${icon_size}/apps
)
endforeach()
install(FILES
packaging/linux/org.darksingularity.OrbitHub.metainfo.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo
)
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/orbithub)
+170
View File
@@ -0,0 +1,170 @@
# OrbitHub
OrbitHub is a cross-platform native desktop app for managing and launching remote sessions from one place.
It is implemented in C++17 with Qt6 Widgets and built with CMake.
Supported target platforms:
- Windows
- Linux
- macOS
## Current Status
OrbitHub is in active development.
- Milestones completed: M0-M5, and M7-M9
- Current milestone: Milestone 10 (v1.0 Stabilization)
- Deferred milestone: Milestone 6 (VNC Fully Working)
- Latest checkpoint tag: `v0-m8-done`
- VNC implementation milestone (M6) is currently deferred
Progress and milestone details:
- [docs/PROGRESS.md](docs/PROGRESS.md)
## Screenshots
![Profile list with folders](docs/images/screenshot-profiles.png)
Profiles organized into folders, with protocol, host, and tags shown at a glance. (Sample data shown; not real hosts.)
## Implemented Features
### Profile Management
- SQLite-backed profile storage
- Create, edit, delete profiles
- Protocol-aware profile validation (SSH/RDP/VNC)
- Profile search and sorting
- Tags support
- Folder/subfolder support
- `List` and `Folders` profile views
- Right-click profile tree actions:
- New Folder
- New Connection
- Drag-and-drop profile moves between folders with persistence
### Session Experience
- Multi-tab session window
- Auto-connect on tab open
- Disconnect on tab close
- Session state indicators on tabs
- Timestamped event log with filtering and export
### SSH
- Embedded interactive terminal (in-app typing)
- Theme support (`Dark`, `Light`, `Solarized Dark`)
- Password and private-key auth flows
- Known-hosts policy support
### RDP
- Embedded in-window RDP rendering surface (no external launcher)
- Keyboard/mouse input forwarding
- Resize handling and resolution renegotiation
- Domain-aware authentication support
- RDP security/performance profile options
### App UX
- App icon and themed About dialog
- `File` menu:
- New Profile
- New Folder
- Quit
- `Help` menu:
- About OrbitHub
## Build and Run
Detailed platform instructions:
- [docs/BUILDING.md](docs/BUILDING.md)
Quick start (Linux/macOS with Ninja):
```bash
cmake -S . -B build -G Ninja
cmake --build build
./build/orbithub
```
## Packaging
Detailed packaging instructions for all platforms:
- [docs/BUILDING.md](docs/BUILDING.md)
Linux (`.deb`):
```bash
./packaging/linux/build-deb.sh
```
Linux (Flatpak):
```bash
./packaging/flatpak/build-flatpak.sh
```
Windows (Inno Setup installer):
```powershell
.\packaging\windows\build-installer.ps1
```
macOS (`.dmg`):
```bash
./packaging/macos/build-dmg.sh
```
## Dependencies
Core dependencies:
- Qt 6 (Widgets, SQL)
- CMake 3.21+
- C++17 toolchain
Protocol/runtime dependencies:
- SSH client (`ssh`) available on `PATH` for SSH sessions
Bundled/vendored third-party components:
- KodoTerm
- libvterm
- FreeRDP/WinPR
## Licensing
Project license:
- MIT (see [LICENSE](LICENSE))
License links:
- MIT License: <https://opensource.org/licenses/MIT>
- GNU LGPLv3: <https://www.gnu.org/licenses/lgpl-3.0.html>
- Apache License 2.0: <https://www.apache.org/licenses/LICENSE-2.0>
Important third-party license notes:
- Qt6 is dynamically linked in this project build setup.
- Qt6 is used under LGPLv3 terms in this project build setup.
- KodoTerm and libvterm are MIT-licensed.
- FreeRDP/WinPR is Apache-2.0 licensed.
Repository license files:
- Project: [LICENSE](LICENSE)
- KodoTerm: [third_party/KodoTerm/LICENSE](third_party/KodoTerm/LICENSE)
- FreeRDP: [third_party/FreeRDP/LICENSE](third_party/FreeRDP/LICENSE)
See in-app `Help -> About OrbitHub` for license links and third-party inventory.
## Repository Structure
- `src/` - application source code
- `docs/` - build guide, spec, and progress tracking
- `third_party/` - vendored third-party dependencies
- `build/` - local build output (generated)
## Notes
- Passwords are requested at connect time and are not stored in the profile database.
- This repository currently prioritizes integrated SSH and RDP workflows while VNC implementation is pending.
+98 -15
View File
@@ -2,6 +2,14 @@
Run all commands from the repository root unless noted.
## Requirements
Minimum toolchain requirements on all platforms:
- CMake 3.21+
- C++17 compiler toolchain
- Qt 6.2+ with `Widgets` and `Sql` modules (dynamic linking)
- OpenSSH client available on `PATH` (required for SSH sessions)
## Linux (Ubuntu / Mint)
```bash
@@ -9,9 +17,9 @@ sudo apt update
sudo apt install -y \
build-essential cmake ninja-build git pkg-config \
qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools \
openssh-client
openssh-client libssl-dev zlib1g-dev
cmake -S . -B build -G Ninja
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/orbithub
```
@@ -21,39 +29,114 @@ cmake --build build
```bash
xcode-select --install
brew update
brew install cmake ninja pkg-config qt@6 openssh
brew install cmake ninja pkg-config qt@6 openssh openssl@3
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$(brew --prefix qt@6);$(brew --prefix openssl@3)"
cmake --build build
./build/orbithub
open build/orbithub.app
```
## Windows 11 (PowerShell + MSVC + vcpkg)
Install required software:
```powershell
winget install -e --id Git.Git
winget install -e --id Kitware.CMake
winget install -e --id Ninja-build.Ninja
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
winget install -e --id Microsoft.VisualStudio.2022.BuildTools `
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools"
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows11SDK.22621"
```
Open a new terminal after installs, then:
Install dependencies via vcpkg:
```powershell
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg
C:\dev\vcpkg\bootstrap-vcpkg.bat
C:\dev\vcpkg\vcpkg.exe install qtbase:x64-windows
cmake -S . -B build -G Ninja `
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build
.\build\orbithub.exe
C:\dev\vcpkg\vcpkg.exe install qtbase:x64-windows openssl:x64-windows zlib:x64-windows
```
Open `x64 Native Tools Command Prompt for VS 2022` (or Developer PowerShell), then build:
```powershell
cmake -S . -B build -G Ninja `
-DCMAKE_BUILD_TYPE=Release `
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build
```
Run (ensures DLL paths from vcpkg are present):
```powershell
C:\dev\vcpkg\vcpkg.exe env --triplet x64-windows -- .\build\orbithub.exe
```
If you already have `Qt 6` from the Qt installer and do not want vcpkg Qt, you can point CMake at that Qt install with `-DCMAKE_PREFIX_PATH=...`, but you still need compatible `OpenSSL` and `zlib` development libraries for the embedded FreeRDP build.
## Notes
- OrbitHub currently requires Qt6 Widgets and CMake 3.21+.
- Milestone 3 SSH sessions require an `ssh` client available on `PATH`.
- OrbitHub builds vendored `KodoTerm`, `libvterm`, and `FreeRDP` from `third_party/`.
- If Qt is installed in a custom location, pass `-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/<toolchain>` to CMake.
- Build output executable:
- Linux: `build/orbithub`
- macOS: `build/orbithub.app` (a proper app bundle, launch with `open build/orbithub.app`)
- Windows: `build\\orbithub.exe`
## Linux Packaging
Build a Debian package (`.deb`) from the current Linux build:
```bash
./packaging/linux/build-deb.sh
```
Output path:
- `dist/orbithub_<version>_<arch>.deb`
Build a Flatpak bundle:
```bash
sudo apt-get install -y flatpak flatpak-builder
./packaging/flatpak/build-flatpak.sh
```
Output path:
- `dist/flatpak/org.darksingularity.OrbitHub.flatpak`
## Windows Packaging
Requires [Inno Setup 6](https://jrsoftware.org/isinfo.php) (`winget install -e --id JRSoftware.InnoSetup`).
From a configured and built `build\` directory (see Windows build steps above):
```powershell
.\packaging\windows\build-installer.ps1
```
Output path:
- `dist\windows\OrbitHub-Setup-<version>.exe`
## macOS Packaging
Requires `qt@6` from Homebrew (for `macdeployqt`). For a custom volume icon on
the `.dmg`, also install `SetFile` (via Xcode's "Additional Tools", from
[developer.apple.com/download/all](https://developer.apple.com/download/all))
or `brew install fileicon` — packaging still works without either, just with
the generic disk image icon.
```bash
./packaging/macos/build-dmg.sh
```
This runs `cmake --install`, then `macdeployqt` to bundle Qt frameworks and
plugins into the `.app`, then re-signs the bundle (ad hoc, since there is no
Developer ID certificate) so it passes macOS's launch-time code-integrity
check. The result is unsigned/unnotarized, so first launch requires
right-click → **Open** to bypass Gatekeeper's unidentified-developer warning.
Output path:
- `dist/macos/OrbitHub-<version>.dmg`
+73
View File
@@ -77,3 +77,76 @@ OrbitHub uses a two-window model:
- Per-session timestamped event log and user-friendly error mapping
- Profile schema extended with `private_key_path` and `known_hosts_policy`
- Tag: v0-m3-done
---
## Milestone 4
- Interactive embedded SSH terminal (`KodoTerm` + `libvterm`)
- SSH host-key trust prompt flow for `Ask` policy
- Improved SSH auth flow for password / private key
- Terminal utilities and UX polish (theme, clear, resize/input behavior)
- Session lifecycle UX cleanup (auto-connect, disconnect on close, tab state indicators)
- Tag: v0-m4-done
---
## Milestone 5 - RDP Fully Working
- Implement complete RDP protocol support (replace not-implemented RDP path)
- Deliver a usable in-app RDP session experience consistent with SSH tab UX
- Support RDP connect/disconnect/reconnect lifecycle from OrbitHub
- Add required RDP-specific connect options in profile/session flows
- Normalize event/error reporting with existing SSH behavior
- Planned Tag: v0-m5-done
---
## Milestone 6 - VNC Fully Working
- Implement complete VNC protocol support (replace not-implemented VNC path)
- Deliver a usable in-app VNC session experience consistent with SSH/RDP tab UX
- Support VNC connect/disconnect/reconnect lifecycle from OrbitHub
- Add required VNC-specific connect options in profile/session flows
- Normalize event/error reporting with SSH/RDP behavior
- Planned Tag: v0-m6-done
---
## Milestone 7 - Cross-Platform Protocol Hardening
- Validate SSH/RDP/VNC behavior on Windows, Linux, and macOS
- Resolve platform-specific path/process/auth differences
- Improve diagnostics and failure messaging for common protocol issues
- Add protocol regression checklist and repeatable verification scripts
- Planned Tag: v0-m7-done
---
## Milestone 8 - Profile and Session UX Completion
- Complete profile fields/validation for all protocols
- Add quality-of-life controls for active sessions (tab context actions, defaults, persistence)
- Persist per-user UI/session preferences (theme, panel visibility, terminal defaults)
- Improve session history/event visibility and filtering
- Planned Tag: v0-m8-done
---
## Milestone 9 - Packaging and Distribution
- Produce distributable artifacts for Windows, Linux, and macOS
- Document dependency/runtime requirements per platform
- Add release build scripts for reproducible packaging
- Validate clean install + first-run flow on each platform
- Planned Tag: v0-m9-done
---
## Milestone 10 - v1.0 Stabilization
- End-to-end QA pass across core workflows (profile CRUD + SSH/RDP/VNC session lifecycle)
- Fix blocker/critical defects from validation
- Finalize docs (`BUILDING`, usage notes, known limitations)
- Prepare v1.0 release notes and final acceptance checklist
- Planned Tag: v1.0.0
+146
View File
@@ -60,3 +60,149 @@ Delivered:
Git:
- Tag: `v0-m3-done`
## Milestone 4 - Interactive SSH Session UX
Status: Completed
Delivered:
- Embedded interactive SSH terminal using `KodoTerm` + vendored `libvterm`
- Native in-terminal typing for SSH sessions (no separate input box)
- ANSI/color rendering with selectable terminal themes (`Dark`, `Light`, `Solarized Dark`)
- Cross-platform SSH auth path improvements (`ssh-askpass` handling and host-key policy wiring)
- Session UX simplification: auto-connect on tab open, disconnect on tab close
- Tab-state indicators via tab color and state suffix (`Connecting`, `Connected`, `Disconnected`, `Failed`)
- Right-click tab menu for `Disconnect`, `Reconnect`, `Theme`, and `Clear`
- Collapsible events panel retained as primary diagnostics surface; inline detail/status banners removed
- Terminal behavior polish: better fixed-width font selection, cursor visibility, backspace handling, and terminal-size negotiation stability
Git:
- Tag: `v0-m4-done`
## Milestone 5 - RDP Fully Working
Status: Completed
Delivered:
- Added `RdpSessionBackend` and wired protocol selection so `RDP` no longer routes to unsupported backend
- Pivoted RDP design to embedded-only integration (no external RDP process launches)
- Implemented embedded FreeRDP client thread with connect/disconnect lifecycle and event-loop handling
- Added in-window `RdpDisplayWidget` rendering surface with frame updates from FreeRDP GDI
- Wired direct keyboard/mouse input from the embedded RDP surface to the backend
- Added RDP connect-time password prompt flow and settings wiring (host/port/user/password, desktop size)
- Added explicit profile `Domain` support for RDP auth (with `DOMAIN\username` fallback parsing)
- Updated session tab/context-menu behavior so terminal-only actions are hidden on RDP tabs
- Implemented dynamic in-session RDP resolution renegotiation from viewport resize events
- Enabled minimal FreeRDP client-channel build (`drdynvc` + `disp`) and channel loading for runtime resize support
- Added RDP profile-level security mode and performance profile options, wired into FreeRDP connection settings
- Hardened RDP lifecycle handling for disconnect/reconnect/abort flows to avoid false failure states on user-initiated stops
- Expanded RDP error/disconnect diagnostics with richer FreeRDP code mapping and raw disconnect detail events
- Pulled FreeRDP source for integration planning and API review
Git:
- Tag: `v0-m5-done`
## Milestone 6 - VNC Fully Working
Status: Deferred (temporarily postponed)
Planned Scope:
- Replace current unsupported VNC path with complete VNC implementation
- Deliver usable in-app VNC session behavior aligned to SSH/RDP UX
- Implement VNC connect/disconnect/reconnect lifecycle handling
- Extend profile/session connect options needed by VNC
- Standardize event log and error mapping behavior with SSH/RDP
## Milestone 7 - Cross-Platform Protocol Hardening
Status: Completed
Delivered:
- Validated SSH and RDP workflows on Linux, macOS, and Windows 11 (VNC is out
of scope while Milestone 6 remains deferred)
- Windows: validated the full `docs/BUILDING.md` toolchain end-to-end
(Git/CMake/Ninja/VS Build Tools with the C++ workload/vcpkg for
Qt6-OpenSSL-zlib); documented a VS Build Tools installer gotcha where the
actual compiler is a "recommended", not "required", component of the
VCTools workload
- Fixed RDP keyboard input misreading punctuation keys on Linux (X11 keycode
numbering was being treated as a PC/AT scancode; now uses FreeRDP's
authoritative X11-keycode-to-scancode table)
- Added RDP clipboard sync (bidirectional, plain text) and RDP cursor/pointer
shape sync (resize handles, text I-beam, etc. instead of a static arrow)
- Fixed Tab/Shift+Tab being intercepted by local UI focus navigation instead
of reaching SSH/RDP sessions
- Fixed RDP key auto-repeat being dropped, so holding a key only ever sent a
single keystroke to the remote machine
- Windows-specific RDP fixes: a crash on every connect attempt (FreeRDP's
signal-handling critical section was never initialized) and a host
resolution failure on every connect, including literal IP addresses
(Winsock was never initialized via `WSAStartup`)
- Windows: automatic build-time deployment of Qt's platform/SQL-driver
plugins and their runtime DLL dependencies, and marked the executable as a
GUI (`WIN32`) app to remove a stray console window behind the UI
- Windows and macOS: proper native app icon embedding (Windows `.rc`/`.ico`
resource; macOS `.app` bundle with `.icns`), replacing the generic default
icon previously shown for the built executable/bundle
- macOS: fixed Edit/New Profile dialog form fields collapsing to `sizeHint`
width due to the platform-default `QFormLayout` field growth policy
Git:
- Tag: Pending user approval (`v0-m7-done`)
## Milestone 8 - Profile and Session UX Completion
Status: Completed
Delivered:
- Added profile `tags` field to storage + schema migration and profile editor UX
- Added profile `folder_path` field + nested folder/subfolder profile view mode
- Added profile tree context actions (`New Folder`, `New Connection`) and drag-to-folder profile moves with persistence
- Added `Help -> About OrbitHub` dialog with third-party library inventory and MIT/Apache-2.0 license links
- Extended profile search to include tags/folder path and added profile sort controls (`Name`, `Protocol`, `Host`)
- Persisted profile list UX preferences (`search text`, `view mode`, protocol/tag filters, `sort order`) across app restarts
- Added protocol-aware profile validation/normalization for SSH/RDP/VNC (repository + dialog)
- Improved profile form protocol UX hints and SSH private-key path validation
- Added session events filtering and tab-context actions (`Show/Hide Events`, `Copy Events`, `Clear Events`)
- Added session diagnostics QoL: severity quick-filter (`All/Warnings/Errors`) and `Export Events` action
- Persisted session UI defaults (`terminal theme`, `events panel visibility`) for new tabs/windows
- Added profile quick filters (`Protocol`, `Tag`) with persistence to speed profile browsing
Validation:
- Local build verification passed (`cmake --build build`)
- No automated tests are currently configured in CTest
Git:
- Tag: Pending user approval (`v0-m8-done`)
## Milestone 9 - Packaging and Distribution
Status: Completed
Delivered:
- Linux `.deb` (`packaging/linux/build-deb.sh`) and Flatpak (`packaging/flatpak/build-flatpak.sh`) packages, both verified installed and launched with working SSH/RDP
- Renamed the app's reverse-DNS identity from `io.orbithub.OrbitHub` to `org.darksingularity.OrbitHub` (desktop file, AppStream metainfo, Flatpak manifest, macOS bundle identifier) to reflect a domain actually owned by the project
- Fixed Linux taskbar/panel pin matching (`StartupWMClass`) so a pinned launcher merges with its running window instead of creating a duplicate entry
- Replaced the stale, mismatched hand-authored launcher SVG with PNG icons rendered directly from the app's own `createOrbitHubAppIcon()` at each hicolor theme size
- Windows installer via Inno Setup (`packaging/windows/orbithub.iss`, `packaging/windows/build-installer.ps1`), verified installed silently and launched cleanly with no crash events
- macOS `.dmg` via `cmake --install` + `macdeployqt` + `hdiutil` (`packaging/macos/build-dmg.sh`), verified installed and launched after fixing:
- a launch crash caused by `macdeployqt` invalidating the code signature (fixed with ad hoc re-signing)
- a missing-library crash caused by the Linux-only `$ORIGIN` rpath token and vendored dylibs installing outside the `.app` bundle (fixed with `APPLE`-specific `@executable_path`/`Contents/Frameworks` layout)
- the dmg showing the generic disk icon instead of the app icon
- README and `docs/BUILDING.md` Packaging sections documented for all three platforms
Validation:
- All three installers built, installed, and launched successfully with working SSH/RDP sessions
- Code signing is ad hoc only (no purchased Apple Developer ID or Windows code-signing certificate), so macOS Gatekeeper and Windows SmartScreen still show first-run warnings by design
Git:
- Tag: Pending user approval
## Milestone 10 - v1.0 Stabilization
Status: Planned
Planned Scope:
- Run final regression and acceptance testing across all protocols
- Resolve release-blocking defects
- Finalize docs and publish v1.0 release notes/checklist
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MANIFEST="$ROOT_DIR/packaging/flatpak/org.darksingularity.OrbitHub.yml"
DIST_DIR="${1:-$ROOT_DIR/dist/flatpak}"
BUILD_DIR="$DIST_DIR/build"
REPO_DIR="$DIST_DIR/repo"
BUNDLE="$DIST_DIR/org.darksingularity.OrbitHub.flatpak"
if ! command -v flatpak-builder >/dev/null 2>&1; then
echo "flatpak-builder is required. Install it first:" >&2
echo " sudo apt-get install -y flatpak-builder" >&2
exit 1
fi
mkdir -p "$DIST_DIR"
flatpak-builder \
--force-clean \
--repo="$REPO_DIR" \
"$BUILD_DIR" \
"$MANIFEST"
flatpak build-bundle "$REPO_DIR" "$BUNDLE" org.darksingularity.OrbitHub
echo "Created $BUNDLE"
@@ -0,0 +1,21 @@
app-id: org.darksingularity.OrbitHub
runtime: org.kde.Platform
runtime-version: "6.10"
sdk: org.kde.Sdk
command: orbithub
finish-args:
- --share=network
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --filesystem=home
modules:
- name: orbithub
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DCMAKE_BUILD_TYPE=Release
sources:
- type: dir
path: ../..
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUILD_DIR="${1:-$ROOT_DIR/build}"
DIST_DIR="${2:-$ROOT_DIR/dist}"
STAGE_DIR="$DIST_DIR/deb-staging"
PKG_ROOT="$STAGE_DIR/orbithub"
if [[ ! -f "$BUILD_DIR/CMakeCache.txt" ]]; then
echo "Build directory not configured: $BUILD_DIR" >&2
echo "Run: cmake -S \"$ROOT_DIR\" -B \"$BUILD_DIR\" -G Ninja" >&2
exit 1
fi
mkdir -p "$DIST_DIR"
rm -rf "$STAGE_DIR"
mkdir -p "$PKG_ROOT/DEBIAN"
VERSION="$(sed -n 's/^CMAKE_PROJECT_VERSION:STATIC=//p' "$BUILD_DIR/CMakeCache.txt" | head -n1)"
ARCH="$(dpkg --print-architecture)"
if [[ -z "$VERSION" ]]; then
echo "Unable to determine project version from $BUILD_DIR/CMakeCache.txt" >&2
exit 1
fi
cmake --build "$BUILD_DIR" -j
cmake --install "$BUILD_DIR" --prefix "$PKG_ROOT/usr"
cat > "$PKG_ROOT/DEBIAN/control" <<EOF
Package: orbithub
Version: ${VERSION}
Section: net
Priority: optional
Architecture: ${ARCH}
Maintainer: OrbitHub Maintainers <maintainers@orbithub.local>
Depends: libc6, libstdc++6, libqt6core6, libqt6gui6, libqt6widgets6, libqt6sql6, libssl3, zlib1g, openssh-client
Description: OrbitHub remote session manager
OrbitHub is a native desktop application for managing connection profiles
and opening SSH and RDP sessions in a tabbed interface.
EOF
cat > "$PKG_ROOT/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
fi
EOF
chmod 0755 "$PKG_ROOT/DEBIAN/postinst"
cat > "$PKG_ROOT/DEBIAN/postrm" <<'EOF'
#!/bin/sh
set -e
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
fi
EOF
chmod 0755 "$PKG_ROOT/DEBIAN/postrm"
OUTPUT_DEB="$DIST_DIR/orbithub_${VERSION}_${ARCH}.deb"
fakeroot dpkg-deb --build "$PKG_ROOT" "$OUTPUT_DEB" >/dev/null
echo "Created $OUTPUT_DEB"
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=OrbitHub
GenericName=Remote Session Manager
Comment=Manage SSH and RDP sessions in one native app
Exec=orbithub
Icon=org.darksingularity.OrbitHub
Terminal=false
StartupWMClass=OrbitHub
Categories=Network;RemoteAccess;Utility;
StartupNotify=true
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>org.darksingularity.OrbitHub</id>
<name>OrbitHub</name>
<summary>Unified remote session manager for SSH and RDP</summary>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<description>
<p>OrbitHub is a native desktop app for organizing connection profiles and launching SSH and RDP sessions in one tabbed interface.</p>
</description>
<launchable type="desktop-id">org.darksingularity.OrbitHub.desktop</launchable>
<url type="homepage">https://git.darksingularity.org/DarkSingularity/orbithub</url>
<provides>
<binary>orbithub</binary>
</provides>
</component>
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUILD_DIR="${1:-$ROOT_DIR/build}"
DIST_DIR="${2:-$ROOT_DIR/dist/macos}"
STAGE_DIR="$DIST_DIR/dmg-staging"
INSTALL_PREFIX="$DIST_DIR/install"
APP_BUNDLE="OrbitHub.app"
if [[ ! -f "$BUILD_DIR/CMakeCache.txt" ]]; then
echo "Build directory not configured: $BUILD_DIR" >&2
echo "Run: cmake -S \"$ROOT_DIR\" -B \"$BUILD_DIR\" -G Ninja" >&2
exit 1
fi
VERSION="$(sed -n 's/^CMAKE_PROJECT_VERSION:STATIC=//p' "$BUILD_DIR/CMakeCache.txt" | head -n1)"
if [[ -z "$VERSION" ]]; then
echo "Unable to determine project version from $BUILD_DIR/CMakeCache.txt" >&2
exit 1
fi
MACDEPLOYQT="$(brew --prefix qt@6)/bin/macdeployqt"
if [[ ! -x "$MACDEPLOYQT" ]]; then
echo "macdeployqt not found at $MACDEPLOYQT" >&2
echo "Install it: brew install qt@6" >&2
exit 1
fi
mkdir -p "$DIST_DIR"
rm -rf "$STAGE_DIR" "$INSTALL_PREFIX"
mkdir -p "$STAGE_DIR"
cmake --build "$BUILD_DIR" -j
cmake --install "$BUILD_DIR" --prefix "$INSTALL_PREFIX"
if [[ ! -d "$INSTALL_PREFIX/$APP_BUNDLE" ]]; then
echo "Expected app bundle not found: $INSTALL_PREFIX/$APP_BUNDLE" >&2
exit 1
fi
"$MACDEPLOYQT" "$INSTALL_PREFIX/$APP_BUNDLE"
# macdeployqt rewrites library load paths after bundling Qt, which
# invalidates any prior signature. Re-sign (ad hoc, since we have no
# Developer ID certificate) so the app passes macOS's code-integrity
# check at launch. Without this, launching fails with a generic
# "cannot be opened because of a problem" crash-reporter dialog rather
# than the milder unidentified-developer Gatekeeper prompt.
codesign --force --deep --sign - "$INSTALL_PREFIX/$APP_BUNDLE"
cp -R "$INSTALL_PREFIX/$APP_BUNDLE" "$STAGE_DIR/$APP_BUNDLE"
ln -s /Applications "$STAGE_DIR/Applications"
cp "$ROOT_DIR/packaging/macos/orbithub.icns" "$STAGE_DIR/.VolumeIcon.icns"
OUTPUT_DMG="$DIST_DIR/OrbitHub-${VERSION}.dmg"
RW_DMG="$DIST_DIR/OrbitHub-rw.dmg"
rm -f "$OUTPUT_DMG" "$RW_DMG"
hdiutil create -volname "OrbitHub" -srcfolder "$STAGE_DIR" -ov -format UDRW "$RW_DMG"
# Setting the volume's icon requires flipping a Finder attribute bit,
# which needs SetFile (from Xcode's "Additional Tools", not the base
# Command Line Tools) or the fileicon brew formula. Neither is
# guaranteed to be installed, so this step degrades gracefully: the
# dmg is still produced either way, just without a custom icon if no
# tool is available.
ICON_TOOL=""
if command -v SetFile >/dev/null 2>&1; then
ICON_TOOL="SetFile"
elif [[ -x /Library/Developer/CommandLineTools/usr/bin/SetFile ]]; then
ICON_TOOL="/Library/Developer/CommandLineTools/usr/bin/SetFile"
elif command -v fileicon >/dev/null 2>&1; then
ICON_TOOL="fileicon"
fi
if [[ -n "$ICON_TOOL" ]]; then
MOUNT_DIR="$(mktemp -d)"
hdiutil attach "$RW_DMG" -mountpoint "$MOUNT_DIR" -nobrowse -quiet
if [[ "$ICON_TOOL" == "fileicon" ]]; then
fileicon set "$MOUNT_DIR" "$ROOT_DIR/packaging/macos/orbithub.icns" >/dev/null
else
"$ICON_TOOL" -a V "$MOUNT_DIR/.VolumeIcon.icns"
"$ICON_TOOL" -a C "$MOUNT_DIR"
fi
hdiutil detach "$MOUNT_DIR" -quiet
rmdir "$MOUNT_DIR" 2>/dev/null || true
else
echo "Note: SetFile/fileicon not found, dmg will use the generic disk icon." >&2
echo " Install one to get a custom volume icon:" >&2
echo " brew install fileicon" >&2
echo " or download 'Additional Tools for Xcode' from developer.apple.com/download/all" >&2
fi
hdiutil convert "$RW_DMG" -format UDZO -ov -o "$OUTPUT_DMG"
rm -f "$RW_DMG"
echo "Created $OUTPUT_DMG"
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
param(
[string]$BuildDir = "$PSScriptRoot\..\..\build",
[string]$DistDir = "$PSScriptRoot\..\..\dist\windows"
)
$ErrorActionPreference = "Stop"
$cacheFile = Join-Path $BuildDir "CMakeCache.txt"
if (-not (Test-Path $cacheFile)) {
throw "Build directory not configured: $BuildDir (run cmake -S . -B build first)"
}
$versionLine = Select-String -Path $cacheFile -Pattern '^CMAKE_PROJECT_VERSION:STATIC=(.*)$'
if (-not $versionLine) {
throw "Unable to determine project version from $cacheFile"
}
$version = $versionLine.Matches[0].Groups[1].Value
$isccCandidates = @(
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
"C:\Program Files\Inno Setup 6\ISCC.exe",
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
)
$iscc = $isccCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
throw "Inno Setup compiler (ISCC.exe) not found. Install it: winget install -e --id JRSoftware.InnoSetup"
}
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
& $iscc "/DMyAppVersion=$version" "$PSScriptRoot\orbithub.iss"
if ($LASTEXITCODE -ne 0) {
throw "ISCC.exe failed with exit code $LASTEXITCODE"
}
Write-Output "Created $DistDir\OrbitHub-Setup-$version.exe"
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+46
View File
@@ -0,0 +1,46 @@
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppName "OrbitHub"
#define MyAppPublisher "OrbitHub Maintainers"
#define MyAppExeName "orbithub.exe"
#define MyBuildDir "..\..\build"
[Setup]
AppId={{6B2E0B8F-2C6D-4E7C-9C36-6C6C9C6F6C1A}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
OutputDir=..\..\dist\windows
OutputBaseFilename=OrbitHub-Setup-{#MyAppVersion}
Compression=lzma2
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
SetupIconFile=orbithub.ico
WizardStyle=modern
DisableProgramGroupPage=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription: "Additional icons:"; Flags: unchecked
[Files]
Source: "{#MyBuildDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyBuildDir}\*.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyBuildDir}\platforms\*"; DestDir: "{app}\platforms"; Flags: ignoreversion recursesubdirs
Source: "{#MyBuildDir}\sqldrivers\*"; DestDir: "{app}\sqldrivers"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent
+1
View File
@@ -0,0 +1 @@
1 ICON "orbithub.ico"
+92
View File
@@ -0,0 +1,92 @@
#include "about_dialog.h"
#include <QApplication>
#include <QCoreApplication>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QTextBrowser>
#include <QVBoxLayout>
AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
{
setWindowTitle(QStringLiteral("About OrbitHub"));
setWindowIcon(QApplication::windowIcon());
resize(760, 560);
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(16, 16, 16, 16);
layout->setSpacing(12);
auto* headerRow = new QHBoxLayout();
headerRow->setSpacing(12);
auto* iconLabel = new QLabel(this);
iconLabel->setFixedSize(80, 80);
iconLabel->setPixmap(QApplication::windowIcon().pixmap(80, 80));
iconLabel->setAlignment(Qt::AlignCenter);
auto* titleColumn = new QVBoxLayout();
titleColumn->setSpacing(4);
auto* title = new QLabel(QStringLiteral("<h1 style='margin:0'>OrbitHub</h1>"), this);
auto* subtitle = new QLabel(
QStringLiteral("Unified remote session manager for SSH, RDP, and VNC workflows."),
this);
subtitle->setWordWrap(true);
subtitle->setStyleSheet(QStringLiteral("color: palette(mid);"));
const QString version = QCoreApplication::applicationVersion().trimmed().isEmpty()
? QStringLiteral("Development build")
: QCoreApplication::applicationVersion().trimmed();
auto* buildLine = new QLabel(
QStringLiteral("Version: %1 | Qt runtime linked dynamically").arg(version),
this);
buildLine->setStyleSheet(QStringLiteral("color: palette(mid);"));
titleColumn->addWidget(title);
titleColumn->addWidget(subtitle);
titleColumn->addWidget(buildLine);
titleColumn->addStretch();
headerRow->addWidget(iconLabel, 0, Qt::AlignTop);
headerRow->addLayout(titleColumn, 1);
auto* browser = new QTextBrowser(this);
browser->setOpenExternalLinks(true);
browser->setStyleSheet(QStringLiteral(
"QTextBrowser { border: 1px solid palette(midlight); border-radius: 8px; padding: 8px; }"));
browser->setHtml(QStringLiteral(R"(
<h3 style="margin-top:0">Third-Party Libraries</h3>
<p>OrbitHub uses the following external libraries:</p>
<table cellspacing="0" cellpadding="6" border="1" style="border-collapse:collapse; width:100%;">
<tr><th align="left">Library</th><th align="left">License</th><th align="left">Upstream</th></tr>
<tr><td>Qt 6 (Widgets / SQL)</td><td>LGPLv3 / GPLv3 / Commercial</td><td><a href="https://www.qt.io/licensing">qt.io/licensing</a></td></tr>
<tr><td>KodoTerm</td><td>MIT</td><td><a href="https://github.com/diegoiast/KodoTerm">github.com/diegoiast/KodoTerm</a></td></tr>
<tr><td>libvterm</td><td>MIT</td><td><a href="https://github.com/neovim/libvterm">github.com/neovim/libvterm</a></td></tr>
<tr><td>FreeRDP / WinPR</td><td>Apache License 2.0</td><td><a href="https://github.com/FreeRDP/FreeRDP">github.com/FreeRDP/FreeRDP</a></td></tr>
</table>
<h3>License Links</h3>
<ul>
<li><a href="https://www.gnu.org/licenses/lgpl-3.0.html">GNU LGPLv3 (gnu.org)</a></li>
<li><a href="https://opensource.org/licenses/MIT">MIT License (opensource.org)</a></li>
<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0 (apache.org)</a></li>
</ul>
<h3>License Files In This Repository</h3>
<ul>
<li><code>third_party/KodoTerm/LICENSE</code></li>
<li><code>third_party/FreeRDP/LICENSE</code></li>
<li><code>LICENSE</code> (project license)</li>
</ul>
)"));
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
layout->addLayout(headerRow);
layout->addWidget(browser, 1);
layout->addWidget(buttons);
}
+14
View File
@@ -0,0 +1,14 @@
#ifndef ORBITHUB_ABOUT_DIALOG_H
#define ORBITHUB_ABOUT_DIALOG_H
#include <QDialog>
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget* parent = nullptr);
};
#endif
+101
View File
@@ -0,0 +1,101 @@
#include "app_icon.h"
#include <QBrush>
#include <QColor>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPixmap>
namespace {
QPixmap renderIconPixmap(int size)
{
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
const qreal s = static_cast<qreal>(size);
const QRectF badgeRect(0.06 * s, 0.06 * s, 0.88 * s, 0.88 * s);
const qreal badgeRadius = 0.19 * s;
QLinearGradient badgeGradient(badgeRect.topLeft(), badgeRect.bottomRight());
badgeGradient.setColorAt(0.0, QColor(QStringLiteral("#0B1220")));
badgeGradient.setColorAt(1.0, QColor(QStringLiteral("#111827")));
QPainterPath badgePath;
badgePath.addRoundedRect(badgeRect, badgeRadius, badgeRadius);
painter.fillPath(badgePath, badgeGradient);
const QPointF center(0.5 * s, 0.5 * s);
const QRectF orbitRect(0.14 * s, 0.26 * s, 0.72 * s, 0.44 * s);
// Draw orbit behind monitor first.
QPen orbitBackPen(QColor(QStringLiteral("#38BDF8")));
orbitBackPen.setWidthF(0.06 * s);
orbitBackPen.setCapStyle(Qt::RoundCap);
painter.setPen(orbitBackPen);
painter.setBrush(Qt::NoBrush);
QTransform orbitTransform;
orbitTransform.translate(center.x(), center.y());
orbitTransform.rotate(-20.0);
orbitTransform.translate(-center.x(), -center.y());
painter.setTransform(orbitTransform);
painter.drawEllipse(orbitRect);
painter.resetTransform();
const QRectF monitorRect(0.2 * s, 0.2 * s, 0.6 * s, 0.44 * s);
const QRectF screenRect(0.24 * s, 0.24 * s, 0.52 * s, 0.34 * s);
const QRectF standStemRect(0.46 * s, 0.64 * s, 0.08 * s, 0.1 * s);
const QRectF standBaseRect(0.34 * s, 0.74 * s, 0.32 * s, 0.08 * s);
QPainterPath monitorPath;
monitorPath.addRoundedRect(monitorRect, 0.07 * s, 0.07 * s);
painter.fillPath(monitorPath, QColor(QStringLiteral("#1F2937")));
painter.setPen(QPen(QColor(QStringLiteral("#4B5563")), 0.016 * s));
painter.drawPath(monitorPath);
QLinearGradient screenGradient(screenRect.topLeft(), screenRect.bottomRight());
screenGradient.setColorAt(0.0, QColor(QStringLiteral("#22D3EE")));
screenGradient.setColorAt(0.55, QColor(QStringLiteral("#38BDF8")));
screenGradient.setColorAt(1.0, QColor(QStringLiteral("#0EA5E9")));
painter.fillRect(screenRect, screenGradient);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(QStringLiteral("#9CA3AF")));
painter.drawRoundedRect(standStemRect, 0.02 * s, 0.02 * s);
painter.setBrush(QColor(QStringLiteral("#6B7280")));
painter.drawRoundedRect(standBaseRect, 0.03 * s, 0.03 * s);
// Orbit front segment over monitor for depth.
QPen orbitFrontPen(QColor(QStringLiteral("#A3E635")));
orbitFrontPen.setWidthF(0.06 * s);
orbitFrontPen.setCapStyle(Qt::RoundCap);
painter.setPen(orbitFrontPen);
painter.setBrush(Qt::NoBrush);
painter.setTransform(orbitTransform);
painter.drawArc(orbitRect, 212 * 16, 126 * 16);
painter.drawArc(orbitRect, 6 * 16, 24 * 16);
painter.resetTransform();
// Small indicator star.
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(QStringLiteral("#E5E7EB")));
painter.drawEllipse(QPointF(0.77 * s, 0.2 * s), 0.02 * s, 0.02 * s);
return pixmap;
}
}
QIcon createOrbitHubAppIcon()
{
QIcon icon;
const int sizes[] = {16, 24, 32, 48, 64, 128, 256};
for (const int size : sizes) {
icon.addPixmap(renderIconPixmap(size));
}
return icon;
}
+8
View File
@@ -0,0 +1,8 @@
#ifndef ORBITHUB_APP_ICON_H
#define ORBITHUB_APP_ICON_H
#include <QIcon>
QIcon createOrbitHubAppIcon();
#endif
+9 -3
View File
@@ -1,12 +1,18 @@
#include "profiles_window.h"
#include "app_icon.h"
#include "session_window.h"
#include <QApplication>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
Q_INIT_RESOURCE(KodoTermThemes);
ProfilesWindow window;
QApplication app(argc, argv);
app.setOrganizationName(QStringLiteral("FireBugIT"));
app.setApplicationName(QStringLiteral("OrbitHub"));
app.setWindowIcon(createOrbitHubAppIcon());
SessionWindow window;
window.show();
return app.exec();
+143 -11
View File
@@ -3,6 +3,7 @@
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
@@ -24,6 +25,28 @@ int standardPortForProtocol(const QString& protocol)
}
return 22; // SSH default
}
QString normalizedProtocol(const QString& protocol)
{
if (protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
if (protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("VNC");
}
return QStringLiteral("SSH");
}
QString normalizedAuthMode(const QString& protocol, const QString& authMode)
{
if (protocol != QStringLiteral("SSH")) {
return QStringLiteral("Password");
}
if (authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Private Key");
}
return QStringLiteral("Password");
}
}
ProfileDialog::ProfileDialog(QWidget* parent)
@@ -32,27 +55,44 @@ ProfileDialog::ProfileDialog(QWidget* parent)
m_hostInput(new QLineEdit(this)),
m_portInput(new QSpinBox(this)),
m_usernameInput(new QLineEdit(this)),
m_domainInput(new QLineEdit(this)),
m_tagsInput(new QLineEdit(this)),
m_protocolInput(new QComboBox(this)),
m_authModeInput(new QComboBox(this)),
m_privateKeyPathInput(new QLineEdit(this)),
m_browsePrivateKeyButton(new QPushButton(QStringLiteral("Browse"), this)),
m_knownHostsPolicyInput(new QComboBox(this))
m_knownHostsPolicyInput(new QComboBox(this)),
m_rdpSecurityModeInput(new QComboBox(this)),
m_rdpPerformanceProfileInput(new QComboBox(this)),
m_protocolHint(new QLabel(this)),
m_folderHint(new QLabel(this))
{
resize(520, 340);
resize(560, 360);
auto* layout = new QVBoxLayout(this);
auto* form = new QFormLayout();
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
form->setFormAlignment(Qt::AlignLeft | Qt::AlignTop);
form->setLabelAlignment(Qt::AlignRight);
m_nameInput->setPlaceholderText(QStringLiteral("Production Bastion"));
m_hostInput->setPlaceholderText(QStringLiteral("example.internal"));
m_portInput->setRange(1, 65535);
m_portInput->setValue(22);
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
m_domainInput->setPlaceholderText(QStringLiteral("CONTOSO"));
m_tagsInput->setPlaceholderText(QStringLiteral("prod, linux, db"));
m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")});
m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")});
m_knownHostsPolicyInput->addItems(
{QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
{QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
m_rdpSecurityModeInput->addItems(
{QStringLiteral("Negotiate"), QStringLiteral("NLA"), QStringLiteral("TLS"), QStringLiteral("RDP")});
m_rdpPerformanceProfileInput->addItems({QStringLiteral("Balanced"),
QStringLiteral("Best Quality"),
QStringLiteral("Best Performance"),
QStringLiteral("Auto Detect")});
m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519"));
@@ -80,6 +120,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
this,
[this](const QString& protocol) {
m_portInput->setValue(standardPortForProtocol(protocol));
if (protocol != QStringLiteral("SSH")) {
const QSignalBlocker blocker(m_authModeInput);
m_authModeInput->setCurrentText(QStringLiteral("Password"));
}
refreshAuthFields();
});
@@ -92,21 +136,29 @@ ProfileDialog::ProfileDialog(QWidget* parent)
form->addRow(QStringLiteral("Host"), m_hostInput);
form->addRow(QStringLiteral("Port"), m_portInput);
form->addRow(QStringLiteral("Username"), m_usernameInput);
form->addRow(QStringLiteral("Domain"), m_domainInput);
form->addRow(QStringLiteral("Tags"), m_tagsInput);
form->addRow(QStringLiteral("Protocol"), m_protocolInput);
form->addRow(QStringLiteral("Auth Mode"), m_authModeInput);
form->addRow(QStringLiteral("Private Key"), privateKeyRow);
form->addRow(QStringLiteral("Known Hosts"), m_knownHostsPolicyInput);
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
auto* note = new QLabel(
QStringLiteral("Passwords are requested at connect time and are not stored."),
this);
note->setWordWrap(true);
m_protocolHint->setWordWrap(true);
m_folderHint->setWordWrap(true);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addLayout(form);
layout->addWidget(m_protocolHint);
layout->addWidget(m_folderHint);
layout->addWidget(note);
layout->addWidget(buttons);
@@ -118,12 +170,21 @@ void ProfileDialog::setDialogTitle(const QString& title)
setWindowTitle(title);
}
void ProfileDialog::setDefaultFolderPath(const QString& folderPath)
{
m_defaultFolderPath = folderPath.trimmed();
refreshAuthFields();
}
void ProfileDialog::setProfile(const Profile& profile)
{
m_nameInput->setText(profile.name);
m_hostInput->setText(profile.host);
m_portInput->setValue(profile.port > 0 ? profile.port : 22);
m_usernameInput->setText(profile.username);
m_domainInput->setText(profile.domain);
m_defaultFolderPath = profile.folderPath.trimmed();
m_tagsInput->setText(profile.tags);
m_privateKeyPathInput->setText(profile.privateKeyPath);
const int protocolIndex = m_protocolInput->findText(profile.protocol);
@@ -138,6 +199,12 @@ void ProfileDialog::setProfile(const Profile& profile)
const int knownHostsIndex = m_knownHostsPolicyInput->findText(profile.knownHostsPolicy);
m_knownHostsPolicyInput->setCurrentIndex(knownHostsIndex >= 0 ? knownHostsIndex : 0);
const int securityModeIndex = m_rdpSecurityModeInput->findText(profile.rdpSecurityMode);
m_rdpSecurityModeInput->setCurrentIndex(securityModeIndex >= 0 ? securityModeIndex : 0);
const int performanceProfileIndex =
m_rdpPerformanceProfileInput->findText(profile.rdpPerformanceProfile);
m_rdpPerformanceProfileInput->setCurrentIndex(performanceProfileIndex >= 0 ? performanceProfileIndex
: 0);
refreshAuthFields();
}
@@ -145,15 +212,31 @@ void ProfileDialog::setProfile(const Profile& profile)
Profile ProfileDialog::profile() const
{
Profile profile;
const QString protocol = normalizedProtocol(m_protocolInput->currentText());
const QString authMode = normalizedAuthMode(protocol, m_authModeInput->currentText());
profile.id = -1;
profile.name = m_nameInput->text().trimmed();
profile.host = m_hostInput->text().trimmed();
profile.port = m_portInput->value();
profile.username = m_usernameInput->text().trimmed();
profile.protocol = m_protocolInput->currentText();
profile.authMode = m_authModeInput->currentText();
profile.privateKeyPath = m_privateKeyPathInput->text().trimmed();
profile.knownHostsPolicy = m_knownHostsPolicyInput->currentText();
profile.domain = protocol == QStringLiteral("RDP") ? m_domainInput->text().trimmed() : QString();
profile.folderPath = m_defaultFolderPath.trimmed();
profile.tags = m_tagsInput->text().trimmed();
profile.protocol = protocol;
profile.authMode = authMode;
profile.privateKeyPath = (protocol == QStringLiteral("SSH")
&& authMode == QStringLiteral("Private Key"))
? m_privateKeyPathInput->text().trimmed()
: QString();
profile.knownHostsPolicy = protocol == QStringLiteral("SSH") ? m_knownHostsPolicyInput->currentText()
: QStringLiteral("Ask");
profile.rdpSecurityMode = protocol == QStringLiteral("RDP")
? m_rdpSecurityModeInput->currentText()
: QStringLiteral("Negotiate");
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
? m_rdpPerformanceProfileInput->currentText()
: QStringLiteral("Balanced");
return profile;
}
@@ -173,24 +256,73 @@ void ProfileDialog::accept()
return;
}
if (m_protocolInput->currentText() == QStringLiteral("SSH")
const QString protocol = m_protocolInput->currentText();
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
&& m_usernameInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Username is required for SSH profiles."));
QStringLiteral("Username is required for %1 profiles.").arg(protocol));
return;
}
if (protocol == QStringLiteral("SSH")
&& m_authModeInput->currentText() == QStringLiteral("Private Key")) {
const QString privateKeyPath = m_privateKeyPathInput->text().trimmed();
if (privateKeyPath.isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Private key path is required for SSH private key authentication."));
return;
}
if (!QFileInfo::exists(privateKeyPath)) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Private key file does not exist: %1").arg(privateKeyPath));
return;
}
}
QDialog::accept();
}
void ProfileDialog::refreshAuthFields()
{
const bool isSsh = m_protocolInput->currentText() == QStringLiteral("SSH");
const bool isPrivateKey = m_authModeInput->currentText() == QStringLiteral("Private Key");
const QString protocol = normalizedProtocol(m_protocolInput->currentText());
const bool isSsh = protocol == QStringLiteral("SSH");
const bool isRdp = protocol == QStringLiteral("RDP");
const bool isVnc = protocol == QStringLiteral("VNC");
const QString normalizedMode = normalizedAuthMode(protocol, m_authModeInput->currentText());
if (normalizedMode != m_authModeInput->currentText()) {
const QSignalBlocker blocker(m_authModeInput);
m_authModeInput->setCurrentText(normalizedMode);
}
const bool isPrivateKey = normalizedMode == QStringLiteral("Private Key");
m_authModeInput->setEnabled(isSsh);
m_privateKeyPathInput->setEnabled(isSsh && isPrivateKey);
m_browsePrivateKeyButton->setEnabled(isSsh && isPrivateKey);
m_knownHostsPolicyInput->setEnabled(isSsh);
m_domainInput->setEnabled(isRdp);
m_rdpSecurityModeInput->setEnabled(isRdp);
m_rdpPerformanceProfileInput->setEnabled(isRdp);
if (isSsh) {
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
m_protocolHint->setText(
QStringLiteral("SSH: username is required. Choose Password or Private Key auth."));
} else if (isRdp) {
m_usernameInput->setPlaceholderText(QStringLiteral("Administrator"));
m_protocolHint->setText(
QStringLiteral("RDP: username and password are required. Domain is optional."));
} else if (isVnc) {
m_usernameInput->setPlaceholderText(QStringLiteral("optional"));
m_protocolHint->setText(
QStringLiteral("VNC: host and port are required. Username/domain are optional and ignored by most servers."));
}
m_folderHint->setText(m_defaultFolderPath.isEmpty()
? QStringLiteral("Target folder: root")
: QStringLiteral("Target folder: %1").arg(m_defaultFolderPath));
}
+9
View File
@@ -6,6 +6,7 @@
#include <QDialog>
class QComboBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QSpinBox;
@@ -18,6 +19,7 @@ public:
explicit ProfileDialog(QWidget* parent = nullptr);
void setDialogTitle(const QString& title);
void setDefaultFolderPath(const QString& folderPath);
void setProfile(const Profile& profile);
Profile profile() const;
@@ -29,11 +31,18 @@ private:
QLineEdit* m_hostInput;
QSpinBox* m_portInput;
QLineEdit* m_usernameInput;
QLineEdit* m_domainInput;
QLineEdit* m_tagsInput;
QComboBox* m_protocolInput;
QComboBox* m_authModeInput;
QLineEdit* m_privateKeyPathInput;
QPushButton* m_browsePrivateKeyButton;
QComboBox* m_knownHostsPolicyInput;
QComboBox* m_rdpSecurityModeInput;
QComboBox* m_rdpPerformanceProfileInput;
QLabel* m_protocolHint;
QLabel* m_folderHint;
QString m_defaultFolderPath;
void refreshAuthFields();
};
+315 -37
View File
@@ -7,6 +7,7 @@
#include <QSqlQuery>
#include <QStandardPaths>
#include <QVariant>
#include <QStringList>
namespace {
QString buildDatabasePath()
@@ -22,18 +23,160 @@ QString buildDatabasePath()
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
}
QString normalizedRdpSecurityMode(const QString& value)
{
const QString mode = value.trimmed();
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("NLA");
}
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("TLS");
}
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
return QStringLiteral("Negotiate");
}
QString normalizedRdpPerformanceProfile(const QString& value)
{
const QString profile = value.trimmed();
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Quality");
}
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Performance");
}
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Auto Detect");
}
return QStringLiteral("Balanced");
}
QString normalizedProtocol(const QString& value)
{
const QString protocol = value.trimmed();
if (protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
if (protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("VNC");
}
return QStringLiteral("SSH");
}
QString normalizedAuthMode(const QString& protocol, const QString& value)
{
if (protocol != QStringLiteral("SSH")) {
return QStringLiteral("Password");
}
const QString authMode = value.trimmed();
if (authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Private Key");
}
return QStringLiteral("Password");
}
QString normalizedKnownHostsPolicy(const QString& value)
{
const QString policy = value.trimmed();
if (policy.compare(QStringLiteral("Strict"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Strict");
}
if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Accept New");
}
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Ignore");
}
return QStringLiteral("Ask");
}
QString normalizedFolderPath(const QString& value)
{
QString path = value.trimmed();
path.replace(QChar::fromLatin1('\\'), QChar::fromLatin1('/'));
const QStringList rawParts = path.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
QStringList normalized;
for (const QString& rawPart : rawParts) {
const QString part = rawPart.trimmed();
if (!part.isEmpty()) {
normalized.push_back(part);
}
}
return normalized.join(QStringLiteral("/"));
}
QString normalizedTags(const QString& value)
{
const QStringList rawTokens = value.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
QStringList normalized;
QSet<QString> dedupe;
for (const QString& token : rawTokens) {
const QString trimmed = token.trimmed();
if (trimmed.isEmpty()) {
continue;
}
const QString key = trimmed.toLower();
if (dedupe.contains(key)) {
continue;
}
dedupe.insert(key);
normalized.push_back(trimmed);
}
return normalized.join(QStringLiteral(", "));
}
QString orderByClause(ProfileSortOrder sortOrder)
{
switch (sortOrder) {
case ProfileSortOrder::ProtocolAsc:
return QStringLiteral("ORDER BY lower(protocol) ASC, lower(name) ASC, id ASC");
case ProfileSortOrder::HostAsc:
return QStringLiteral("ORDER BY lower(host) ASC, lower(name) ASC, id ASC");
case ProfileSortOrder::NameAsc:
default:
return QStringLiteral("ORDER BY lower(name) ASC, id ASC");
}
}
QString nonNullTrimmed(const QString& value)
{
const QString trimmed = value.trimmed();
return trimmed.isNull() ? QStringLiteral("") : trimmed;
}
void bindProfileFields(QSqlQuery& query, const Profile& profile)
{
query.addBindValue(profile.name.trimmed());
query.addBindValue(profile.host.trimmed());
const QString protocol = normalizedProtocol(profile.protocol);
const QString authMode = normalizedAuthMode(protocol, profile.authMode);
const bool isSsh = protocol == QStringLiteral("SSH");
const bool isRdp = protocol == QStringLiteral("RDP");
query.addBindValue(nonNullTrimmed(profile.name));
query.addBindValue(nonNullTrimmed(profile.host));
query.addBindValue(profile.port);
query.addBindValue(profile.username.trimmed());
query.addBindValue(profile.protocol.trimmed());
query.addBindValue(profile.authMode.trimmed());
query.addBindValue(profile.privateKeyPath.trimmed());
query.addBindValue(profile.knownHostsPolicy.trimmed().isEmpty()
? QStringLiteral("Strict")
: profile.knownHostsPolicy.trimmed());
query.addBindValue(nonNullTrimmed(profile.username));
query.addBindValue(isRdp ? nonNullTrimmed(profile.domain) : QStringLiteral(""));
query.addBindValue(nonNullTrimmed(normalizedFolderPath(profile.folderPath)));
query.addBindValue(protocol);
query.addBindValue(authMode);
query.addBindValue((isSsh && authMode == QStringLiteral("Private Key"))
? nonNullTrimmed(profile.privateKeyPath)
: QStringLiteral(""));
query.addBindValue(isSsh ? normalizedKnownHostsPolicy(profile.knownHostsPolicy)
: QStringLiteral("Ask"));
query.addBindValue(isRdp ? normalizedRdpSecurityMode(profile.rdpSecurityMode)
: QStringLiteral("Negotiate"));
query.addBindValue(isRdp ? normalizedRdpPerformanceProfile(profile.rdpPerformanceProfile)
: QStringLiteral("Balanced"));
query.addBindValue(normalizedTags(profile.tags));
}
Profile profileFromQuery(const QSqlQuery& query)
@@ -44,20 +187,68 @@ Profile profileFromQuery(const QSqlQuery& query)
profile.host = query.value(2).toString();
profile.port = query.value(3).toInt();
profile.username = query.value(4).toString();
profile.protocol = query.value(5).toString();
profile.authMode = query.value(6).toString();
profile.privateKeyPath = query.value(7).toString();
profile.knownHostsPolicy = query.value(8).toString();
if (profile.knownHostsPolicy.isEmpty()) {
profile.knownHostsPolicy = QStringLiteral("Strict");
}
profile.domain = query.value(5).toString();
profile.folderPath = normalizedFolderPath(query.value(6).toString());
profile.protocol = normalizedProtocol(query.value(7).toString());
profile.authMode = normalizedAuthMode(profile.protocol, query.value(8).toString());
profile.privateKeyPath = profile.authMode == QStringLiteral("Private Key")
? query.value(9).toString().trimmed()
: QString();
profile.knownHostsPolicy = profile.protocol == QStringLiteral("SSH")
? normalizedKnownHostsPolicy(query.value(10).toString())
: QStringLiteral("Ask");
profile.rdpSecurityMode = profile.protocol == QStringLiteral("RDP")
? normalizedRdpSecurityMode(query.value(11).toString())
: QStringLiteral("Negotiate");
profile.rdpPerformanceProfile = profile.protocol == QStringLiteral("RDP")
? normalizedRdpPerformanceProfile(query.value(12).toString())
: QStringLiteral("Balanced");
profile.tags = normalizedTags(query.value(13).toString());
return profile;
}
bool isProfileValid(const Profile& profile)
bool isProfileValid(const Profile& profile, QString* error)
{
return !profile.name.trimmed().isEmpty() && !profile.host.trimmed().isEmpty()
&& profile.port >= 1 && profile.port <= 65535;
if (profile.name.trimmed().isEmpty()) {
if (error != nullptr) {
*error = QStringLiteral("Profile name is required.");
}
return false;
}
if (profile.host.trimmed().isEmpty()) {
if (error != nullptr) {
*error = QStringLiteral("Host is required.");
}
return false;
}
if (profile.port < 1 || profile.port > 65535) {
if (error != nullptr) {
*error = QStringLiteral("Port must be between 1 and 65535.");
}
return false;
}
const QString protocol = normalizedProtocol(profile.protocol);
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
&& profile.username.trimmed().isEmpty()) {
if (error != nullptr) {
*error = QStringLiteral("Username is required for %1 profiles.").arg(protocol);
}
return false;
}
const QString authMode = normalizedAuthMode(protocol, profile.authMode);
if (protocol == QStringLiteral("SSH") && authMode == QStringLiteral("Private Key")
&& profile.privateKeyPath.trimmed().isEmpty()) {
if (error != nullptr) {
*error = QStringLiteral("Private key path is required for SSH private key authentication.");
}
return false;
}
return true;
}
}
@@ -89,7 +280,60 @@ QString ProfileRepository::lastError() const
return m_lastError;
}
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
std::vector<QString> ProfileRepository::listFolders() const
{
std::vector<QString> result;
if (!QSqlDatabase::contains(m_connectionName)) {
return result;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("SELECT path FROM profile_folders ORDER BY lower(path) ASC"));
if (!query.exec()) {
setLastError(query.lastError().text());
return result;
}
while (query.next()) {
const QString normalized = normalizedFolderPath(query.value(0).toString());
if (!normalized.isEmpty()) {
result.push_back(normalized);
}
}
return result;
}
bool ProfileRepository::createFolder(const QString& folderPath) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return false;
}
const QString normalized = normalizedFolderPath(folderPath);
if (normalized.isEmpty()) {
setLastError(QStringLiteral("Folder path is required."));
return false;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("INSERT OR IGNORE INTO profile_folders(path) VALUES (?)"));
query.addBindValue(normalized);
if (!query.exec()) {
setLastError(query.lastError().text());
return false;
}
return true;
}
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery,
ProfileSortOrder sortOrder) const
{
std::vector<Profile> result;
@@ -100,20 +344,23 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
const QString orderBy = orderByClause(sortOrder);
if (searchQuery.trimmed().isEmpty()) {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"FROM profiles "
"ORDER BY lower(name) ASC, id ASC"));
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
"FROM profiles ")
+ orderBy);
} else {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
"ORDER BY lower(name) ASC, id ASC"));
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
"FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) OR lower(tags) LIKE lower(?) OR lower(folder_path) LIKE lower(?) ")
+ orderBy);
const QString search = QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%");
query.addBindValue(search);
query.addBindValue(search);
query.addBindValue(search);
query.addBindValue(search);
}
if (!query.exec()) {
@@ -138,7 +385,7 @@ std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
"FROM profiles WHERE id = ?"));
query.addBindValue(id);
@@ -162,15 +409,16 @@ std::optional<Profile> ProfileRepository::createProfile(const Profile& profile)
setLastError(QString());
if (!isProfileValid(profile)) {
setLastError(QStringLiteral("Name, host, and a valid port are required."));
QString validationError;
if (!isProfileValid(profile, &validationError)) {
setLastError(validationError);
return std::nullopt;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
"INSERT INTO profiles(name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
bindProfileFields(query, profile);
if (!query.exec()) {
@@ -191,15 +439,17 @@ bool ProfileRepository::updateProfile(const Profile& profile) const
setLastError(QString());
if (profile.id < 0 || !isProfileValid(profile)) {
setLastError(QStringLiteral("Invalid profile data."));
QString validationError;
if (profile.id < 0 || !isProfileValid(profile, &validationError)) {
setLastError(validationError.isEmpty() ? QStringLiteral("Invalid profile data.")
: validationError);
return false;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"UPDATE profiles "
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ? "
"SET name = ?, host = ?, port = ?, username = ?, domain = ?, folder_path = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ?, rdp_security_mode = ?, rdp_performance_profile = ?, tags = ? "
"WHERE id = ?"));
bindProfileFields(query, profile);
query.addBindValue(profile.id);
@@ -250,10 +500,15 @@ bool ProfileRepository::initializeDatabase()
"host TEXT NOT NULL DEFAULT '',"
"port INTEGER NOT NULL DEFAULT 22,"
"username TEXT NOT NULL DEFAULT '',"
"domain TEXT NOT NULL DEFAULT '',"
"folder_path TEXT NOT NULL DEFAULT '',"
"protocol TEXT NOT NULL DEFAULT 'SSH',"
"auth_mode TEXT NOT NULL DEFAULT 'Password',"
"private_key_path TEXT NOT NULL DEFAULT '',"
"known_hosts_policy TEXT NOT NULL DEFAULT 'Strict'"
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask',"
"rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate',"
"rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced',"
"tags TEXT NOT NULL DEFAULT ''"
")"));
if (!created) {
@@ -261,6 +516,15 @@ bool ProfileRepository::initializeDatabase()
return false;
}
const bool foldersCreated = query.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS profile_folders ("
"path TEXT PRIMARY KEY NOT NULL"
")"));
if (!foldersCreated) {
m_initError = query.lastError().text();
return false;
}
if (!ensureProfileSchema()) {
m_initError = m_lastError;
return false;
@@ -296,10 +560,15 @@ bool ProfileRepository::ensureProfileSchema() const
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("domain"), QStringLiteral("ALTER TABLE profiles ADD COLUMN domain TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("folder_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN folder_path TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")},
{QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Strict'")}};
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")},
{QStringLiteral("rdp_security_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate'")},
{QStringLiteral("rdp_performance_profile"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'")},
{QStringLiteral("tags"), QStringLiteral("ALTER TABLE profiles ADD COLUMN tags TEXT NOT NULL DEFAULT ''")}};
for (const ColumnDef& column : required) {
if (columns.contains(column.name)) {
@@ -313,6 +582,15 @@ bool ProfileRepository::ensureProfileSchema() const
}
}
QSqlQuery ensureFolders(QSqlDatabase::database(m_connectionName));
if (!ensureFolders.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS profile_folders ("
"path TEXT PRIMARY KEY NOT NULL"
")"))) {
setLastError(ensureFolders.lastError().text());
return false;
}
setLastError(QString());
return true;
}
+16 -2
View File
@@ -14,10 +14,21 @@ struct Profile
QString host;
int port = 22;
QString username;
QString domain;
QString folderPath;
QString protocol = QStringLiteral("SSH");
QString authMode = QStringLiteral("Password");
QString privateKeyPath;
QString knownHostsPolicy = QStringLiteral("Strict");
QString knownHostsPolicy = QStringLiteral("Ask");
QString rdpSecurityMode = QStringLiteral("Negotiate");
QString rdpPerformanceProfile = QStringLiteral("Balanced");
QString tags;
};
enum class ProfileSortOrder {
NameAsc,
ProtocolAsc,
HostAsc,
};
class ProfileRepository
@@ -29,7 +40,10 @@ public:
QString initError() const;
QString lastError() const;
std::vector<Profile> listProfiles(const QString& searchQuery = QString()) const;
std::vector<Profile> listProfiles(const QString& searchQuery = QString(),
ProfileSortOrder sortOrder = ProfileSortOrder::NameAsc) const;
std::vector<QString> listFolders() const;
bool createFolder(const QString& folderPath) const;
std::optional<Profile> getProfile(qint64 id) const;
std::optional<Profile> createProfile(const Profile& profile) const;
bool updateProfile(const Profile& profile) const;
+11
View File
@@ -0,0 +1,11 @@
#include "profiles_tree_widget.h"
#include <QDropEvent>
ProfilesTreeWidget::ProfilesTreeWidget(QWidget* parent) : QTreeWidget(parent) {}
void ProfilesTreeWidget::dropEvent(QDropEvent* event)
{
QTreeWidget::dropEvent(event);
emit itemsDropped();
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef ORBITHUB_PROFILES_TREE_WIDGET_H
#define ORBITHUB_PROFILES_TREE_WIDGET_H
#include <QTreeWidget>
class QDropEvent;
class ProfilesTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
explicit ProfilesTreeWidget(QWidget* parent = nullptr);
signals:
void itemsDropped();
protected:
void dropEvent(QDropEvent* event) override;
};
#endif
+720 -73
View File
@@ -2,42 +2,104 @@
#include "profile_dialog.h"
#include "profile_repository.h"
#include "session_window.h"
#include <algorithm>
#include "profiles_tree_widget.h"
#include <QAction>
#include <QAbstractItemView>
#include <QComboBox>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
#include <QStringList>
#include <QStyle>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVariant>
#include <QVBoxLayout>
#include <QWidget>
namespace {
QString formatProfileListItem(const Profile& profile)
constexpr int kProfileIdRole = Qt::UserRole;
constexpr int kFolderPathRole = Qt::UserRole + 1;
QString normalizeFolderPathForView(const QString& value)
{
return QStringLiteral("%1 [%2 %3:%4]")
.arg(profile.name, profile.protocol, profile.host, QString::number(profile.port));
QString path = value.trimmed();
path.replace(QChar::fromLatin1('\\'), QChar::fromLatin1('/'));
const QStringList rawParts = path.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
QStringList normalized;
for (const QString& rawPart : rawParts) {
const QString part = rawPart.trimmed();
if (!part.isEmpty()) {
normalized.push_back(part);
}
}
return normalized.join(QStringLiteral("/"));
}
QString joinFolderPath(const QString& parentFolder, const QString& childName)
{
const QString parent = normalizeFolderPathForView(parentFolder);
const QString child = normalizeFolderPathForView(childName);
if (child.isEmpty()) {
return parent;
}
if (parent.isEmpty()) {
return child;
}
return QStringLiteral("%1/%2").arg(parent, child);
}
QStringList splitFolderPath(const QString& folderPath)
{
const QString normalized = normalizeFolderPathForView(folderPath);
if (normalized.isEmpty()) {
return {};
}
return normalized.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
}
bool profileHasTag(const Profile& profile, const QString& requestedTag)
{
const QString needle = requestedTag.trimmed();
if (needle.isEmpty()) {
return true;
}
const QStringList tags = profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
for (const QString& tag : tags) {
if (tag.trimmed().compare(needle, Qt::CaseInsensitive) == 0) {
return true;
}
}
return false;
}
}
ProfilesWindow::ProfilesWindow(QWidget* parent)
: QMainWindow(parent),
: QWidget(parent),
m_searchBox(nullptr),
m_profilesList(nullptr),
m_viewModeBox(nullptr),
m_sortBox(nullptr),
m_protocolFilterBox(nullptr),
m_tagFilterBox(nullptr),
m_profilesTree(nullptr),
m_newButton(nullptr),
m_editButton(nullptr),
m_deleteButton(nullptr),
m_repository(std::make_unique<ProfileRepository>())
{
setWindowTitle(QStringLiteral("OrbitHub Profiles"));
resize(640, 620);
setupUi();
if (!m_repository->initError().isEmpty()) {
@@ -49,10 +111,11 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
m_editButton->setEnabled(false);
m_deleteButton->setEnabled(false);
m_searchBox->setEnabled(false);
m_profilesList->setEnabled(false);
m_profilesTree->setEnabled(false);
return;
}
loadUiPreferences();
loadProfiles();
}
@@ -60,54 +123,139 @@ ProfilesWindow::~ProfilesWindow() = default;
void ProfilesWindow::setupUi()
{
auto* central = new QWidget(this);
auto* rootLayout = new QVBoxLayout(central);
auto* rootLayout = new QVBoxLayout(this);
auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
m_searchBox = new QLineEdit(central);
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name or host..."));
auto* searchLabel = new QLabel(QStringLiteral("Search"), this);
m_searchBox = new QLineEdit(this);
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name, host, folder, or tags..."));
m_profilesList = new QListWidget(central);
m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection);
auto* viewModeLabel = new QLabel(QStringLiteral("View"), this);
m_viewModeBox = new QComboBox(this);
m_viewModeBox->addItem(QStringLiteral("List"));
m_viewModeBox->addItem(QStringLiteral("Folders"));
auto* sortLabel = new QLabel(QStringLiteral("Sort"), this);
m_sortBox = new QComboBox(this);
m_sortBox->addItem(QStringLiteral("Name"), static_cast<int>(ProfileSortOrder::NameAsc));
m_sortBox->addItem(QStringLiteral("Protocol"), static_cast<int>(ProfileSortOrder::ProtocolAsc));
m_sortBox->addItem(QStringLiteral("Host"), static_cast<int>(ProfileSortOrder::HostAsc));
auto* protocolFilterLabel = new QLabel(QStringLiteral("Protocol"), this);
m_protocolFilterBox = new QComboBox(this);
m_protocolFilterBox->addItem(QStringLiteral("All"));
m_protocolFilterBox->addItem(QStringLiteral("SSH"));
m_protocolFilterBox->addItem(QStringLiteral("RDP"));
m_protocolFilterBox->addItem(QStringLiteral("VNC"));
auto* tagFilterLabel = new QLabel(QStringLiteral("Tag"), this);
m_tagFilterBox = new QComboBox(this);
m_tagFilterBox->addItem(QStringLiteral("All"));
m_profilesTree = new ProfilesTreeWidget(this);
m_profilesTree->setSelectionMode(QAbstractItemView::SingleSelection);
m_profilesTree->setColumnCount(4);
m_profilesTree->setHeaderLabels(
{QStringLiteral("Name"), QStringLiteral("Protocol"), QStringLiteral("Server"), QStringLiteral("Tags")});
m_profilesTree->setRootIsDecorated(true);
m_profilesTree->setDragEnabled(true);
m_profilesTree->setAcceptDrops(true);
m_profilesTree->viewport()->setAcceptDrops(true);
m_profilesTree->setDropIndicatorShown(true);
m_profilesTree->setDragDropMode(QAbstractItemView::InternalMove);
m_profilesTree->setDefaultDropAction(Qt::MoveAction);
m_profilesTree->setContextMenuPolicy(Qt::CustomContextMenu);
m_profilesTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_profilesTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_profilesTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_profilesTree->header()->setSectionResizeMode(3, QHeaderView::Stretch);
auto* buttonRow = new QHBoxLayout();
m_newButton = new QPushButton(QStringLiteral("New"), central);
m_editButton = new QPushButton(QStringLiteral("Edit"), central);
m_deleteButton = new QPushButton(QStringLiteral("Delete"), central);
m_newButton = new QPushButton(QStringLiteral("New"), this);
m_editButton = new QPushButton(QStringLiteral("Edit"), this);
m_deleteButton = new QPushButton(QStringLiteral("Delete"), this);
buttonRow->addWidget(m_newButton);
buttonRow->addWidget(m_editButton);
buttonRow->addWidget(m_deleteButton);
buttonRow->addStretch();
rootLayout->addWidget(searchLabel);
rootLayout->addWidget(m_searchBox);
rootLayout->addWidget(m_profilesList, 1);
rootLayout->addLayout(buttonRow);
auto* filterRow = new QHBoxLayout();
filterRow->addWidget(searchLabel);
filterRow->addWidget(m_searchBox, 1);
filterRow->addWidget(viewModeLabel);
filterRow->addWidget(m_viewModeBox);
filterRow->addWidget(protocolFilterLabel);
filterRow->addWidget(m_protocolFilterBox);
filterRow->addWidget(tagFilterLabel);
filterRow->addWidget(m_tagFilterBox);
filterRow->addWidget(sortLabel);
filterRow->addWidget(m_sortBox);
setCentralWidget(central);
rootLayout->addLayout(filterRow);
rootLayout->addWidget(m_profilesTree, 1);
rootLayout->addLayout(buttonRow);
connect(m_searchBox,
&QLineEdit::textChanged,
this,
[this](const QString& text) { loadProfiles(text); });
connect(m_profilesList,
&QListWidget::itemDoubleClicked,
[this](const QString&) {
saveUiPreferences();
loadProfiles();
});
connect(m_viewModeBox,
&QComboBox::currentIndexChanged,
this,
[this](QListWidgetItem* item) { openSessionForItem(item); });
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_sortBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_protocolFilterBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_tagFilterBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_profilesTree,
&QTreeWidget::itemDoubleClicked,
this,
[this](QTreeWidgetItem* item, int) { openSessionForItem(item); });
connect(m_profilesTree,
&QWidget::customContextMenuRequested,
this,
[this](const QPoint& pos) { showTreeContextMenu(pos); });
connect(m_profilesTree,
&ProfilesTreeWidget::itemsDropped,
this,
[this]() { persistFolderAssignmentsFromTree(); });
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
}
void ProfilesWindow::loadProfiles(const QString& query)
void ProfilesWindow::loadProfiles()
{
m_profilesList->clear();
m_profilesTree->clear();
m_profileCache.clear();
const std::vector<Profile> profiles = m_repository->listProfiles(query);
const QString query = m_searchBox == nullptr ? QString() : m_searchBox->text();
const std::vector<Profile> profiles = m_repository->listProfiles(query, selectedSortOrder());
if (!m_repository->lastError().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Load Profiles"),
@@ -116,28 +264,522 @@ void ProfilesWindow::loadProfiles(const QString& query)
return;
}
updateTagFilterOptions(profiles);
const QString protocolFilter = selectedProtocolFilter();
const QString tagFilter = selectedTagFilter();
const bool folderView = isFolderViewEnabled();
m_profilesTree->setDragEnabled(folderView);
m_profilesTree->setAcceptDrops(folderView);
m_profilesTree->viewport()->setAcceptDrops(folderView);
m_profilesTree->setDropIndicatorShown(folderView);
m_profilesTree->setDragDropMode(folderView ? QAbstractItemView::InternalMove
: QAbstractItemView::NoDragDrop);
std::vector<Profile> filteredProfiles;
filteredProfiles.reserve(profiles.size());
for (const Profile& profile : profiles) {
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5\nKnown Hosts: %6")
.arg(profile.protocol,
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
profile.host,
QString::number(profile.port),
profile.authMode,
profile.knownHostsPolicy));
if (!protocolFilter.isEmpty()
&& profile.protocol.compare(protocolFilter, Qt::CaseInsensitive) != 0) {
continue;
}
if (!tagFilter.isEmpty() && !profileHasTag(profile, tagFilter)) {
continue;
}
filteredProfiles.push_back(profile);
}
if (folderView) {
std::map<QString, QTreeWidgetItem*> folderNodes;
const std::vector<QString> explicitFolders = m_repository->listFolders();
for (const QString& folderPath : explicitFolders) {
if (query.trimmed().isEmpty()
|| folderPath.contains(query.trimmed(), Qt::CaseInsensitive)) {
upsertFolderNode(splitFolderPath(folderPath), folderNodes);
}
}
for (const Profile& profile : filteredProfiles) {
QTreeWidgetItem* parent = nullptr;
const QStringList folderParts = splitFolderPath(profile.folderPath);
if (!folderParts.isEmpty()) {
parent = upsertFolderNode(folderParts, folderNodes);
}
addProfileNode(parent, profile);
}
m_profilesTree->expandAll();
} else {
for (const Profile& profile : filteredProfiles) {
addProfileNode(nullptr, profile);
}
}
}
ProfileSortOrder ProfilesWindow::selectedSortOrder() const
{
if (m_sortBox == nullptr) {
return ProfileSortOrder::NameAsc;
}
const QVariant value = m_sortBox->currentData();
if (!value.isValid()) {
return ProfileSortOrder::NameAsc;
}
const int orderValue = value.toInt();
switch (static_cast<ProfileSortOrder>(orderValue)) {
case ProfileSortOrder::ProtocolAsc:
return ProfileSortOrder::ProtocolAsc;
case ProfileSortOrder::HostAsc:
return ProfileSortOrder::HostAsc;
case ProfileSortOrder::NameAsc:
default:
return ProfileSortOrder::NameAsc;
}
}
bool ProfilesWindow::isFolderViewEnabled() const
{
if (m_viewModeBox == nullptr) {
return false;
}
return m_viewModeBox->currentText().compare(QStringLiteral("Folders"), Qt::CaseInsensitive)
== 0;
}
QString ProfilesWindow::selectedProtocolFilter() const
{
if (m_protocolFilterBox == nullptr) {
return QString();
}
const QString selected = m_protocolFilterBox->currentText().trimmed();
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
return QString();
}
return selected;
}
QString ProfilesWindow::selectedTagFilter() const
{
if (m_tagFilterBox == nullptr) {
return QString();
}
const QString selected = m_tagFilterBox->currentText().trimmed();
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
return QString();
}
return selected;
}
void ProfilesWindow::updateTagFilterOptions(const std::vector<Profile>& profiles)
{
if (m_tagFilterBox == nullptr) {
return;
}
const QString previousSelection = m_tagFilterBox->currentText().trimmed();
QString desiredSelection = previousSelection;
if (!m_pendingTagFilterPreference.trimmed().isEmpty()) {
desiredSelection = m_pendingTagFilterPreference.trimmed();
}
QSet<QString> seen;
QStringList tags;
for (const Profile& profile : profiles) {
const QStringList splitTags =
profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
for (const QString& rawTag : splitTags) {
const QString tag = rawTag.trimmed();
if (tag.isEmpty()) {
continue;
}
const QString dedupeKey = tag.toLower();
if (seen.contains(dedupeKey)) {
continue;
}
seen.insert(dedupeKey);
tags.push_back(tag);
}
}
tags.sort(Qt::CaseInsensitive);
const QSignalBlocker blocker(m_tagFilterBox);
m_tagFilterBox->clear();
m_tagFilterBox->addItem(QStringLiteral("All"));
for (const QString& tag : tags) {
m_tagFilterBox->addItem(tag);
}
int restoredIndex = m_tagFilterBox->findText(desiredSelection, Qt::MatchFixedString);
if (restoredIndex < 0) {
restoredIndex = 0;
}
m_tagFilterBox->setCurrentIndex(restoredIndex);
m_pendingTagFilterPreference.clear();
}
QTreeWidgetItem* ProfilesWindow::upsertFolderNode(const QStringList& folderParts,
std::map<QString, QTreeWidgetItem*>& folderNodes)
{
QString currentPath;
QTreeWidgetItem* parent = nullptr;
for (const QString& rawPart : folderParts) {
const QString part = rawPart.trimmed();
if (part.isEmpty()) {
continue;
}
currentPath = currentPath.isEmpty() ? part
: QStringLiteral("%1/%2").arg(currentPath, part);
const auto existing = folderNodes.find(currentPath);
if (existing != folderNodes.end()) {
parent = existing->second;
continue;
}
auto* folderItem = parent == nullptr ? new QTreeWidgetItem(m_profilesTree)
: new QTreeWidgetItem(parent);
folderItem->setText(0, part);
folderItem->setIcon(0, style()->standardIcon(QStyle::SP_DirIcon));
folderItem->setData(0, kFolderPathRole, currentPath);
folderItem->setFlags((folderItem->flags() | Qt::ItemIsDropEnabled) & ~Qt::ItemIsDragEnabled);
folderNodes.insert_or_assign(currentPath, folderItem);
parent = folderItem;
}
return parent;
}
void ProfilesWindow::addProfileNode(QTreeWidgetItem* parent, const Profile& profile)
{
auto* item = parent == nullptr ? new QTreeWidgetItem(m_profilesTree) : new QTreeWidgetItem(parent);
item->setText(0, profile.name);
item->setText(1, profile.protocol);
item->setText(2, QStringLiteral("%1:%2").arg(profile.host, QString::number(profile.port)));
item->setText(3, profile.tags);
item->setIcon(0, style()->standardIcon(QStyle::SP_ComputerIcon));
item->setData(0, kProfileIdRole, QVariant::fromValue(profile.id));
item->setData(0, kFolderPathRole, normalizeFolderPathForView(profile.folderPath));
item->setFlags((item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable) & ~Qt::ItemIsDropEnabled);
const QString identity = [&profile]() {
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
&& !profile.domain.trimmed().isEmpty()) {
return QStringLiteral("%1\\%2").arg(profile.domain.trimmed(),
profile.username.trimmed().isEmpty()
? QStringLiteral("<none>")
: profile.username.trimmed());
}
return profile.username.trimmed().isEmpty() ? QStringLiteral("<none>")
: profile.username.trimmed();
}();
QString tooltip = QStringLiteral("%1://%2@%3:%4\nAuth: %5")
.arg(profile.protocol,
identity,
profile.host,
QString::number(profile.port),
profile.authMode);
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nKnown Hosts: %1").arg(profile.knownHostsPolicy);
} else if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nRDP Security: %1\nRDP Performance: %2")
.arg(profile.rdpSecurityMode, profile.rdpPerformanceProfile);
}
if (!profile.folderPath.trimmed().isEmpty()) {
tooltip += QStringLiteral("\nFolder: %1").arg(profile.folderPath.trimmed());
}
if (!profile.tags.trimmed().isEmpty()) {
tooltip += QStringLiteral("\nTags: %1").arg(profile.tags);
}
item->setToolTip(0, tooltip);
item->setToolTip(1, tooltip);
item->setToolTip(2, tooltip);
item->setToolTip(3, tooltip);
m_profileCache.insert_or_assign(profile.id, profile);
}
QString ProfilesWindow::folderPathForItem(const QTreeWidgetItem* item) const
{
if (item == nullptr) {
return QString();
}
const QVariant idValue = item->data(0, kProfileIdRole);
if (idValue.isValid()) {
const qint64 id = idValue.toLongLong();
const auto cacheIt = m_profileCache.find(id);
if (cacheIt != m_profileCache.end()) {
return normalizeFolderPathForView(cacheIt->second.folderPath);
}
}
return normalizeFolderPathForView(item->data(0, kFolderPathRole).toString());
}
void ProfilesWindow::showTreeContextMenu(const QPoint& pos)
{
if (m_profilesTree == nullptr) {
return;
}
QTreeWidgetItem* item = m_profilesTree->itemAt(pos);
if (item != nullptr) {
m_profilesTree->setCurrentItem(item);
}
const QString contextFolder = folderPathForItem(item);
const bool isProfileItem = item != nullptr && item->data(0, kProfileIdRole).isValid();
QMenu menu(this);
QAction* newConnectionAction = menu.addAction(QStringLiteral("New Connection"));
QAction* newFolderAction = menu.addAction(QStringLiteral("New Folder"));
QAction* connectAction = nullptr;
QAction* editAction = nullptr;
QAction* deleteAction = nullptr;
if (isProfileItem) {
menu.addSeparator();
connectAction = menu.addAction(QStringLiteral("Connect"));
editAction = menu.addAction(QStringLiteral("Edit"));
deleteAction = menu.addAction(QStringLiteral("Delete"));
}
QAction* chosen = menu.exec(m_profilesTree->viewport()->mapToGlobal(pos));
if (chosen == nullptr) {
return;
}
if (chosen == newConnectionAction) {
createProfile(contextFolder);
return;
}
if (chosen == newFolderAction) {
createFolderInContext(contextFolder);
return;
}
if (isProfileItem && chosen == connectAction) {
openSessionForItem(item);
return;
}
if (isProfileItem && chosen == editAction) {
editSelectedProfile();
return;
}
if (isProfileItem && chosen == deleteAction) {
deleteSelectedProfile();
return;
}
}
void ProfilesWindow::createFolderInContext(const QString& baseFolderPath)
{
bool accepted = false;
const QString folderName = QInputDialog::getText(
this,
QStringLiteral("New Folder"),
QStringLiteral("Folder name"),
QLineEdit::Normal,
QString(),
&accepted);
if (!accepted) {
return;
}
const QString normalized = joinFolderPath(baseFolderPath, folderName);
if (normalized.isEmpty()) {
QMessageBox::information(this,
QStringLiteral("New Folder"),
QStringLiteral("Folder name is required."));
return;
}
if (!m_repository->createFolder(normalized)) {
QMessageBox::warning(this,
QStringLiteral("New Folder"),
QStringLiteral("Failed to create folder: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles();
}
void ProfilesWindow::persistFolderAssignmentsFromTree()
{
if (!isFolderViewEnabled() || m_profilesTree == nullptr) {
return;
}
std::unordered_map<qint64, QString> assignments;
const int topLevelCount = m_profilesTree->topLevelItemCount();
for (int index = 0; index < topLevelCount; ++index) {
collectProfileAssignments(m_profilesTree->topLevelItem(index), QString(), assignments);
}
bool hadErrors = false;
int updatedCount = 0;
for (const auto& [id, newFolderPath] : assignments) {
auto cacheIt = m_profileCache.find(id);
Profile profile;
if (cacheIt != m_profileCache.end()) {
profile = cacheIt->second;
} else {
const std::optional<Profile> fetched = m_repository->getProfile(id);
if (!fetched.has_value()) {
continue;
}
profile = fetched.value();
}
const QString existing = normalizeFolderPathForView(profile.folderPath);
const QString target = normalizeFolderPathForView(newFolderPath);
if (existing == target) {
continue;
}
profile.folderPath = target;
if (!target.isEmpty()) {
m_repository->createFolder(target);
}
if (!m_repository->updateProfile(profile)) {
hadErrors = true;
continue;
}
m_profileCache.insert_or_assign(profile.id, profile);
++updatedCount;
}
if (hadErrors) {
QMessageBox::warning(this,
QStringLiteral("Move Profile"),
QStringLiteral("One or more profile moves could not be saved."));
}
if (updatedCount > 0 || hadErrors) {
loadProfiles();
}
}
void ProfilesWindow::collectProfileAssignments(
const QTreeWidgetItem* item,
const QString& parentFolderPath,
std::unordered_map<qint64, QString>& assignments) const
{
if (item == nullptr) {
return;
}
const QVariant idValue = item->data(0, kProfileIdRole);
if (idValue.isValid()) {
assignments.insert_or_assign(idValue.toLongLong(), normalizeFolderPathForView(parentFolderPath));
return;
}
const QString folderPath = joinFolderPath(parentFolderPath, item->text(0));
const int childCount = item->childCount();
for (int index = 0; index < childCount; ++index) {
collectProfileAssignments(item->child(index), folderPath, assignments);
}
}
void ProfilesWindow::loadUiPreferences()
{
QSettings settings;
const QString search = settings.value(QStringLiteral("profiles/searchText")).toString();
const QString viewMode =
settings.value(QStringLiteral("profiles/viewMode"), QStringLiteral("List")).toString();
const int sortValue = settings
.value(QStringLiteral("profiles/sortOrder"),
static_cast<int>(ProfileSortOrder::NameAsc))
.toInt();
const QString protocolFilter =
settings.value(QStringLiteral("profiles/protocolFilter"), QStringLiteral("All")).toString();
const QString tagFilter =
settings.value(QStringLiteral("profiles/tagFilter"), QStringLiteral("All")).toString();
m_pendingTagFilterPreference = tagFilter.trimmed();
if (m_searchBox != nullptr) {
const QSignalBlocker blocker(m_searchBox);
m_searchBox->setText(search);
}
if (m_viewModeBox != nullptr) {
int found = m_viewModeBox->findText(viewMode, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_viewModeBox);
m_viewModeBox->setCurrentIndex(found);
}
if (m_sortBox != nullptr) {
int found = m_sortBox->findData(sortValue);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_sortBox);
m_sortBox->setCurrentIndex(found);
}
if (m_protocolFilterBox != nullptr) {
int found = m_protocolFilterBox->findText(protocolFilter, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_protocolFilterBox);
m_protocolFilterBox->setCurrentIndex(found);
}
if (m_tagFilterBox != nullptr) {
int found = m_tagFilterBox->findText(tagFilter, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_tagFilterBox);
m_tagFilterBox->setCurrentIndex(found);
}
}
void ProfilesWindow::saveUiPreferences() const
{
QSettings settings;
if (m_searchBox != nullptr) {
settings.setValue(QStringLiteral("profiles/searchText"), m_searchBox->text());
}
if (m_viewModeBox != nullptr) {
settings.setValue(QStringLiteral("profiles/viewMode"), m_viewModeBox->currentText());
}
if (m_sortBox != nullptr) {
settings.setValue(QStringLiteral("profiles/sortOrder"), m_sortBox->currentData());
}
if (m_protocolFilterBox != nullptr) {
settings.setValue(QStringLiteral("profiles/protocolFilter"), m_protocolFilterBox->currentText());
}
if (m_tagFilterBox != nullptr) {
settings.setValue(QStringLiteral("profiles/tagFilter"), m_tagFilterBox->currentText());
}
}
std::optional<Profile> ProfilesWindow::selectedProfile() const
{
QListWidgetItem* item = m_profilesList->currentItem();
QTreeWidgetItem* item = m_profilesTree->currentItem();
if (item == nullptr) {
return std::nullopt;
}
const QVariant value = item->data(Qt::UserRole);
const QVariant value = item->data(0, kProfileIdRole);
if (!value.isValid()) {
return std::nullopt;
}
@@ -151,16 +793,22 @@ std::optional<Profile> ProfilesWindow::selectedProfile() const
return m_repository->getProfile(id);
}
void ProfilesWindow::createProfile()
void ProfilesWindow::createProfile(const QString& defaultFolderPath)
{
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("New Profile"));
dialog.setDefaultFolderPath(defaultFolderPath);
if (dialog.exec() != QDialog::Accepted) {
return;
}
if (!m_repository->createProfile(dialog.profile()).has_value()) {
const Profile newProfile = dialog.profile();
if (!newProfile.folderPath.trimmed().isEmpty()) {
m_repository->createFolder(newProfile.folderPath);
}
if (!m_repository->createProfile(newProfile).has_value()) {
QMessageBox::warning(this,
QStringLiteral("Create Profile"),
QStringLiteral("Failed to create profile: %1")
@@ -170,7 +818,7 @@ void ProfilesWindow::createProfile()
return;
}
loadProfiles(m_searchBox->text());
loadProfiles();
}
void ProfilesWindow::editSelectedProfile()
@@ -193,6 +841,9 @@ void ProfilesWindow::editSelectedProfile()
Profile updated = dialog.profile();
updated.id = selected->id;
if (!updated.folderPath.trimmed().isEmpty()) {
m_repository->createFolder(updated.folderPath);
}
if (!m_repository->updateProfile(updated)) {
QMessageBox::warning(this,
@@ -204,7 +855,7 @@ void ProfilesWindow::editSelectedProfile()
return;
}
loadProfiles(m_searchBox->text());
loadProfiles();
}
void ProfilesWindow::deleteSelectedProfile()
@@ -238,17 +889,18 @@ void ProfilesWindow::deleteSelectedProfile()
return;
}
loadProfiles(m_searchBox->text());
loadProfiles();
}
void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
void ProfilesWindow::openSessionForItem(QTreeWidgetItem* item)
{
if (item == nullptr) {
return;
}
const QVariant value = item->data(Qt::UserRole);
const QVariant value = item->data(0, kProfileIdRole);
if (!value.isValid()) {
item->setExpanded(!item->isExpanded());
return;
}
@@ -264,22 +916,17 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
return;
}
auto* session = new SessionWindow(profile.value());
session->setAttribute(Qt::WA_DeleteOnClose);
m_sessionWindows.emplace_back(session);
connect(session,
&QObject::destroyed,
this,
[this](QObject* object) {
m_sessionWindows.erase(
std::remove_if(m_sessionWindows.begin(),
m_sessionWindows.end(),
[object](const QPointer<SessionWindow>& candidate) {
return candidate.isNull() || candidate.data() == object;
}),
m_sessionWindows.end());
});
session->show();
emit connectRequested(profile.value());
}
void ProfilesWindow::createProfileInCurrentContext()
{
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
createProfile(folderPath);
}
void ProfilesWindow::createFolderInCurrentContext()
{
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
createFolderInContext(folderPath);
}
+42 -12
View File
@@ -3,23 +3,26 @@
#include "profile_repository.h"
#include <QMainWindow>
#include <QWidget>
#include <QString>
#include <QStringList>
#include <QtGlobal>
#include <memory>
#include <map>
#include <optional>
#include <QPointer>
#include <unordered_map>
#include <vector>
#include <unordered_map>
class QListWidget;
class QListWidgetItem;
class QTreeWidget;
class QTreeWidgetItem;
class QLineEdit;
class QPushButton;
class SessionWindow;
class QComboBox;
class QPoint;
class ProfilesTreeWidget;
class ProfilesWindow : public QMainWindow
class ProfilesWindow : public QWidget
{
Q_OBJECT
@@ -27,23 +30,50 @@ public:
explicit ProfilesWindow(QWidget* parent = nullptr);
~ProfilesWindow() override;
void createProfileInCurrentContext();
void createFolderInCurrentContext();
signals:
void connectRequested(const Profile& profile);
private:
QLineEdit* m_searchBox;
QListWidget* m_profilesList;
QComboBox* m_viewModeBox;
QComboBox* m_sortBox;
QComboBox* m_protocolFilterBox;
QComboBox* m_tagFilterBox;
ProfilesTreeWidget* m_profilesTree;
QPushButton* m_newButton;
QPushButton* m_editButton;
QPushButton* m_deleteButton;
std::vector<QPointer<SessionWindow>> m_sessionWindows;
std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;
QString m_pendingTagFilterPreference;
void setupUi();
void loadProfiles(const QString& query = QString());
void loadProfiles();
ProfileSortOrder selectedSortOrder() const;
bool isFolderViewEnabled() const;
QString selectedProtocolFilter() const;
QString selectedTagFilter() const;
void updateTagFilterOptions(const std::vector<Profile>& profiles);
QTreeWidgetItem* upsertFolderNode(const QStringList& folderParts,
std::map<QString, QTreeWidgetItem*>& folderNodes);
void addProfileNode(QTreeWidgetItem* parent, const Profile& profile);
QString folderPathForItem(const QTreeWidgetItem* item) const;
void showTreeContextMenu(const QPoint& pos);
void createFolderInContext(const QString& baseFolderPath);
void persistFolderAssignmentsFromTree();
void collectProfileAssignments(const QTreeWidgetItem* item,
const QString& parentFolderPath,
std::unordered_map<qint64, QString>& assignments) const;
void loadUiPreferences();
void saveUiPreferences() const;
std::optional<Profile> selectedProfile() const;
void createProfile();
void createProfile(const QString& defaultFolderPath = QString());
void editSelectedProfile();
void deleteSelectedProfile();
void openSessionForItem(QListWidgetItem* item);
void openSessionForItem(QTreeWidgetItem* item);
};
#endif
+280
View File
@@ -0,0 +1,280 @@
#include "rdp_display_widget.h"
#include <QCursor>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPixmap>
#include <QResizeEvent>
#include <QTimer>
#include <QWheelEvent>
#include <QtGlobal>
namespace {
QSize sanitizeSize(const QSize& size)
{
return QSize(qMax(1, size.width()), qMax(1, size.height()));
}
}
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
: QWidget(parent), m_remoteSize(1280, 720), m_cursorMode(CursorMode::Default)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
setAutoFillBackground(false);
setMinimumSize(320, 200);
QTimer::singleShot(0, this, [this]() {
const QSize size = sanitizeSize(this->size());
emit viewportSizeChanged(size.width(), size.height());
});
}
void RdpDisplayWidget::setFrame(const QImage& frame)
{
if (frame.isNull()) {
return;
}
m_frame = frame;
m_remoteSize = sanitizeSize(frame.size());
update();
}
void RdpDisplayWidget::setRemoteDesktopSize(int width, int height)
{
if (width < 1 || height < 1) {
return;
}
const QSize nextSize(width, height);
if (m_remoteSize == nextSize) {
return;
}
m_remoteSize = nextSize;
update();
}
void RdpDisplayWidget::clearFrame()
{
m_frame = QImage();
update();
}
void RdpDisplayWidget::setCursorImage(const QImage& image, const QPoint& hotspot)
{
m_cursorImage = image;
m_cursorHotspot = hotspot;
m_cursorMode = CursorMode::Custom;
applyCursor();
}
void RdpDisplayWidget::setCursorHidden()
{
m_cursorMode = CursorMode::Hidden;
applyCursor();
}
void RdpDisplayWidget::setCursorDefault()
{
m_cursorMode = CursorMode::Default;
applyCursor();
}
void RdpDisplayWidget::applyCursor()
{
if (m_cursorMode == CursorMode::Hidden) {
setCursor(Qt::BlankCursor);
return;
}
if (m_cursorMode == CursorMode::Default || m_cursorImage.isNull()) {
unsetCursor();
return;
}
const QSize remote = effectiveRemoteSize();
const QRectF target = renderRect();
if (remote.isEmpty() || target.isEmpty()) {
setCursor(QCursor(QPixmap::fromImage(m_cursorImage),
m_cursorHotspot.x(),
m_cursorHotspot.y()));
return;
}
const qreal scale = target.width() / remote.width();
QImage scaledImage = m_cursorImage;
if (!qFuzzyCompare(scale, 1.0)) {
scaledImage = m_cursorImage.scaled(
qMax(1, qRound(m_cursorImage.width() * scale)),
qMax(1, qRound(m_cursorImage.height() * scale)),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
const int hotX = qBound(0, qRound(m_cursorHotspot.x() * scale), scaledImage.width());
const int hotY = qBound(0, qRound(m_cursorHotspot.y() * scale), scaledImage.height());
setCursor(QCursor(QPixmap::fromImage(scaledImage), hotX, hotY));
}
void RdpDisplayWidget::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), QColor(QStringLiteral("#101214")));
const QRectF target = renderRect();
if (!m_frame.isNull()) {
painter.drawImage(target, m_frame);
} else {
painter.setPen(QColor(QStringLiteral("#b0bec5")));
painter.drawText(rect(),
Qt::AlignCenter,
QStringLiteral("Waiting for remote desktop frame..."));
}
}
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
const QSize size = sanitizeSize(event->size());
emit viewportSizeChanged(size.width(), size.height());
applyCursor();
}
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
{
if (event == nullptr) {
return;
}
// Auto-repeat presses must reach the remote server so it can perform
// its own typematic repeat, exactly as a physical keyboard held down
// would. Only release events filter out isAutoRepeat() (below), since
// Qt uses a synthetic release/press pair purely to normalize platform
// auto-repeat quirks -- forwarding that synthetic release would send a
// spurious key-up for a key that is still physically held.
emit keyInput(event->key(),
event->nativeScanCode(),
event->text(),
true,
static_cast<int>(event->modifiers()));
event->accept();
}
void RdpDisplayWidget::keyReleaseEvent(QKeyEvent* event)
{
if (event == nullptr || event->isAutoRepeat()) {
return;
}
emit keyInput(event->key(),
event->nativeScanCode(),
event->text(),
false,
static_cast<int>(event->modifiers()));
event->accept();
}
bool RdpDisplayWidget::focusNextPrevChild(bool next)
{
Q_UNUSED(next);
// Tab/Shift+Tab must reach keyPressEvent() and be forwarded to the
// remote session instead of moving focus to the next local widget.
return false;
}
void RdpDisplayWidget::mousePressEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
setFocus(Qt::MouseFocusReason);
const QPoint mapped = mapToRemote(event->position());
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), true);
event->accept();
}
void RdpDisplayWidget::mouseReleaseEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), false);
event->accept();
}
void RdpDisplayWidget::mouseMoveEvent(QMouseEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
emit mouseMoveInput(mapped.x(), mapped.y());
event->accept();
}
void RdpDisplayWidget::wheelEvent(QWheelEvent* event)
{
if (event == nullptr) {
return;
}
const QPoint mapped = mapToRemote(event->position());
const QPoint angle = event->angleDelta();
emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y());
event->accept();
}
QRectF RdpDisplayWidget::renderRect() const
{
const QSize remote = effectiveRemoteSize();
const QRectF area = rect();
if (area.isEmpty()) {
return QRectF();
}
const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height());
const qreal drawWidth = remote.width() * scale;
const qreal drawHeight = remote.height() * scale;
const qreal x = area.x() + ((area.width() - drawWidth) * 0.5);
const qreal y = area.y() + ((area.height() - drawHeight) * 0.5);
return QRectF(x, y, drawWidth, drawHeight);
}
QPoint RdpDisplayWidget::mapToRemote(const QPointF& pos) const
{
const QSize remote = effectiveRemoteSize();
const QRectF target = renderRect();
if (target.isEmpty()) {
return QPoint(0, 0);
}
const qreal clampedX = qBound(target.left(), pos.x(), target.right());
const qreal clampedY = qBound(target.top(), pos.y(), target.bottom());
const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width());
const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height());
const int remoteX = qBound(0, static_cast<int>(normalizedX * remote.width()), remote.width() - 1);
const int remoteY = qBound(0, static_cast<int>(normalizedY * remote.height()), remote.height() - 1);
return QPoint(remoteX, remoteY);
}
QSize RdpDisplayWidget::effectiveRemoteSize() const
{
if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) {
return m_remoteSize;
}
if (!m_frame.isNull()) {
return sanitizeSize(m_frame.size());
}
return QSize(1280, 720);
}
+64
View File
@@ -0,0 +1,64 @@
#ifndef ORBITHUB_RDP_DISPLAY_WIDGET_H
#define ORBITHUB_RDP_DISPLAY_WIDGET_H
#include <QImage>
#include <QWidget>
class QKeyEvent;
class QMouseEvent;
class QPaintEvent;
class QResizeEvent;
class QWheelEvent;
class RdpDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit RdpDisplayWidget(QWidget* parent = nullptr);
void setFrame(const QImage& frame);
void setRemoteDesktopSize(int width, int height);
void clearFrame();
void setCursorImage(const QImage& image, const QPoint& hotspot);
void setCursorHidden();
void setCursorDefault();
signals:
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
void mouseMoveInput(int x, int y);
void mouseButtonInput(int x, int y, int button, bool pressed);
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
void viewportSizeChanged(int width, int height);
protected:
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
bool focusNextPrevChild(bool next) override;
private:
enum class CursorMode {
Default,
Custom,
Hidden,
};
QImage m_frame;
QSize m_remoteSize;
QImage m_cursorImage;
QPoint m_cursorHotspot;
CursorMode m_cursorMode;
QRectF renderRect() const;
QPoint mapToRemote(const QPointF& pos) const;
QSize effectiveRemoteSize() const;
void applyCursor();
};
#endif
File diff suppressed because it is too large Load Diff
+119
View File
@@ -0,0 +1,119 @@
#ifndef ORBITHUB_RDP_SESSION_BACKEND_H
#define ORBITHUB_RDP_SESSION_BACKEND_H
#include "session_backend.h"
#include <atomic>
#include <cstdint>
#include <deque>
#include <mutex>
#include <thread>
struct rdp_freerdp;
class RdpSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr);
~RdpSessionBackend() override;
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers) override;
void sendMouseMoveEvent(int x, int y) override;
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
void setClipboardText(const QString& text) override;
private:
enum class InputEventType {
Key,
MouseMove,
MouseButton,
MouseWheel,
Resize,
SetClipboardText,
};
struct InputEvent {
InputEventType type = InputEventType::MouseMove;
int key = 0;
quint32 nativeScanCode = 0;
QString text;
bool pressed = false;
int modifiers = 0;
int x = 0;
int y = 0;
int button = 0;
int deltaX = 0;
int deltaY = 0;
int width = 0;
int height = 0;
};
SessionState m_state;
SessionConnectOptions m_activeOptions;
std::atomic_bool m_userInitiatedDisconnect;
std::atomic_int m_requestedDesktopWidth;
std::atomic_int m_requestedDesktopHeight;
std::thread m_worker;
std::atomic_bool m_workerRunning;
std::atomic_bool m_stopRequested;
std::mutex m_instanceMutex;
rdp_freerdp* m_instance;
std::mutex m_inputMutex;
std::deque<InputEvent> m_inputEvents;
std::mutex m_displayControlMutex;
void* m_displayControlContext;
bool m_displayControlReady;
bool m_resizeFailureLogged;
int m_lastResizeWidth;
int m_lastResizeHeight;
std::mutex m_cliprdrMutex;
void* m_cliprdrContext;
QString m_pendingLocalClipboardText;
void setState(SessionState state, const QString& message);
bool validateProfile(QString& message) const;
void startWorker();
void stopWorker(bool userInitiated);
void workerMain();
void enqueueInputEvent(const InputEvent& event);
void processInputEvents(rdp_freerdp* instance);
bool sendDisplayResize(rdp_freerdp* instance, int width, int height);
void sendClipboardTextToRemote(rdp_freerdp* instance, const QString& text);
public:
void onChannelConnectedEvent(const char* name, void* channelInterface);
void onChannelDisconnectedEvent(const char* name, void* channelInterface);
void onDisplayControlCaps(uint32_t maxNumMonitors,
uint32_t maxMonitorAreaFactorA,
uint32_t maxMonitorAreaFactorB);
void onCliprdrMonitorReady();
void onCliprdrServerFormatList(bool hasUnicodeText);
void onCliprdrServerFormatDataRequest(uint32_t requestedFormatId);
void onCliprdrServerFormatDataResponse(bool success, const uint8_t* data, uint32_t size);
private:
void emitStateAsync(SessionState state, const QString& message);
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
int sanitizeDesktopWidth(int width) const;
int sanitizeDesktopHeight(int height) const;
};
#endif
+49
View File
@@ -3,8 +3,11 @@
#include "profile_repository.h"
#include <QImage>
#include <QObject>
#include <QPoint>
#include <QString>
#include <QtGlobal>
class SessionConnectOptions
{
@@ -41,11 +44,57 @@ public slots:
virtual void connectSession(const SessionConnectOptions& options) = 0;
virtual void disconnectSession() = 0;
virtual void reconnectSession(const SessionConnectOptions& options) = 0;
virtual void sendInput(const QString& input) = 0;
virtual void confirmHostKey(bool trustHost) = 0;
virtual void updateTerminalSize(int columns, int rows) = 0;
virtual void setClipboardText(const QString& text)
{
Q_UNUSED(text);
}
virtual void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers)
{
Q_UNUSED(key);
Q_UNUSED(nativeScanCode);
Q_UNUSED(text);
Q_UNUSED(pressed);
Q_UNUSED(modifiers);
}
virtual void sendMouseMoveEvent(int x, int y)
{
Q_UNUSED(x);
Q_UNUSED(y);
}
virtual void sendMouseButtonEvent(int x, int y, int button, bool pressed)
{
Q_UNUSED(x);
Q_UNUSED(y);
Q_UNUSED(button);
Q_UNUSED(pressed);
}
virtual void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY)
{
Q_UNUSED(x);
Q_UNUSED(y);
Q_UNUSED(deltaX);
Q_UNUSED(deltaY);
}
signals:
void stateChanged(SessionState state, const QString& message);
void eventLogged(const QString& message);
void connectionError(const QString& displayMessage, const QString& rawMessage);
void outputReceived(const QString& text);
void hostKeyConfirmationRequested(const QString& prompt);
void frameUpdated(const QImage& frame);
void remoteDesktopSizeChanged(int width, int height);
void remoteClipboardTextChanged(const QString& text);
void cursorImageChanged(const QImage& image, const QPoint& hotspot);
void cursorHidden();
void cursorReset();
private:
Profile m_profile;
+4
View File
@@ -1,5 +1,6 @@
#include "session_backend_factory.h"
#include "rdp_session_backend.h"
#include "session_backend.h"
#include "ssh_session_backend.h"
#include "unsupported_session_backend.h"
@@ -9,6 +10,9 @@ std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
return std::make_unique<SshSessionBackend>(profile);
}
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return std::make_unique<RdpSessionBackend>(profile);
}
return std::make_unique<UnsupportedSessionBackend>(profile);
}
+1084 -182
View File
File diff suppressed because it is too large Load Diff
+104 -15
View File
@@ -5,63 +5,152 @@
#include "session_backend.h"
#include <QWidget>
#include <QStringList>
#include <QtGlobal>
#include <functional>
#include <optional>
#include <vector>
class QLabel;
class QPlainTextEdit;
class QPushButton;
class QThread;
class SessionBackend;
class TerminalView;
class RdpDisplayWidget;
class QToolButton;
class QLineEdit;
class QComboBox;
class QLabel;
class QPushButton;
class KodoTerm;
struct SessionUiPreferences
{
QString terminalThemeName = QStringLiteral("Dark");
bool eventsPanelExpanded = false;
int terminalFontPointSize = 0;
};
class SessionTab : public QWidget
{
Q_OBJECT
public:
explicit SessionTab(const Profile& profile, QWidget* parent = nullptr);
explicit SessionTab(const Profile& profile,
const SessionUiPreferences& preferences,
QWidget* parent = nullptr);
~SessionTab() override;
QString tabTitle() const;
void connectSession();
void disconnectSession();
void reconnectSession();
void clearTerminal();
void setTerminalThemeName(const QString& themeName);
QString terminalThemeName() const;
bool supportsThemeSelection() const;
bool supportsClearAction() const;
bool supportsZoom() const;
void zoomIn();
void zoomOut();
void resetZoom();
void setTerminalFontPointSize(int pointSize);
int terminalFontPointSize() const;
bool isEventsPanelExpanded() const;
void setEventsPanelExpanded(bool expanded);
void clearEvents();
void copyEvents() const;
void exportEventsToFile();
signals:
void tabTitleChanged(const QString& title);
void tabStateChanged(SessionState state);
void terminalThemeChanged(const QString& themeName);
void terminalFontSizeChanged(int pointSize);
void eventsPanelVisibilityChanged(bool expanded);
void requestConnect(const SessionConnectOptions& options);
void requestDisconnect();
void requestReconnect(const SessionConnectOptions& options);
void requestInput(const QString& input);
void requestHostKeyConfirmation(bool trustHost);
void requestTerminalSize(int columns, int rows);
void requestKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers);
void requestMouseMoveEvent(int x, int y);
void requestMouseButtonEvent(int x, int y, int button, bool pressed);
void requestMouseWheelEvent(int x, int y, int deltaX, int deltaY);
void requestSetClipboardText(const QString& text);
private slots:
void onConnectClicked();
void onDisconnectClicked();
void onReconnectClicked();
void onCopyErrorClicked();
void onBackendStateChanged(SessionState state, const QString& message);
void onBackendEventLogged(const QString& message);
void onBackendConnectionError(const QString& displayMessage, const QString& rawMessage);
void onBackendOutputReceived(const QString& text);
void onBackendHostKeyConfirmationRequested(const QString& prompt);
void onBackendRemoteClipboardTextChanged(const QString& text);
void onSystemClipboardChanged();
private:
Profile m_profile;
QThread* m_backendThread;
SessionBackend* m_backend;
bool m_useKodoTermForSsh;
SessionState m_state;
QString m_lastError;
SessionConnectOptions m_lastConnectOptions;
QString m_terminalThemeName;
int m_terminalFontPointSize;
QString m_lastSyncedClipboardText;
bool m_clipboardSyncSupported;
QLabel* m_statusLabel;
QLabel* m_errorLabel;
KodoTerm* m_sshTerminal;
RdpDisplayWidget* m_rdpDisplay;
TerminalView* m_terminalOutput;
QPlainTextEdit* m_eventLog;
QPushButton* m_connectButton;
QPushButton* m_disconnectButton;
QPushButton* m_reconnectButton;
QPushButton* m_copyErrorButton;
QToolButton* m_toggleEventsButton;
QLineEdit* m_eventFilterInput;
QComboBox* m_eventSeverityFilterInput;
QToolButton* m_clearEventsButton;
QToolButton* m_exportEventsButton;
QWidget* m_eventsPanel;
QWidget* m_passwordPromptBar;
QLabel* m_passwordPromptLabel;
QLineEdit* m_passwordPromptInput;
QPushButton* m_passwordPromptConnectButton;
QPushButton* m_passwordPromptCancelButton;
std::function<void(std::optional<QString>)> m_passwordPromptCallback;
enum class EventSeverity {
Info,
Warning,
Error,
};
struct EventEntry {
QString line;
EventSeverity severity;
};
std::vector<EventEntry> m_eventEntries;
QString m_eventFilter;
EventSeverity m_eventSeverityFilter;
bool m_eventsPanelExpanded;
void setupUi();
std::optional<SessionConnectOptions> buildConnectOptions();
void requestConnectOptions(std::function<void(std::optional<SessionConnectOptions>)> callback);
void showPasswordPrompt(const QString& labelText,
std::function<void(std::optional<QString>)> callback);
void hidePasswordPrompt();
bool validateProfileForConnect();
void appendEvent(const QString& message);
void setState(SessionState state, const QString& message);
QString stateSuffix() const;
void refreshActionButtons();
void setPanelExpanded(QToolButton* button, QWidget* panel, const QString& name, bool expanded);
bool startSshTerminal(const SessionConnectOptions& options);
void applyTerminalTheme(const QString& themeName);
void refreshEventLogView();
static EventSeverity classifyEventSeverity(const QString& message);
};
#endif
+264 -6
View File
@@ -1,14 +1,52 @@
#include "session_window.h"
#include "about_dialog.h"
#include "profiles_window.h"
#include <QApplication>
#include "session_tab.h"
#include <QAction>
#include <QColor>
#include <QInputDialog>
#include <QMenu>
#include <QMenuBar>
#include <QPalette>
#include <QSettings>
#include <QStringList>
#include <QTabBar>
#include <QTabWidget>
SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
: QMainWindow(parent), m_tabs(new QTabWidget(this))
namespace {
QColor tabColorForState(SessionState state, const QPalette& palette)
{
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
switch (state) {
case SessionState::Disconnected:
return palette.color(QPalette::WindowText);
case SessionState::Connecting:
return QColor(QStringLiteral("#9a6700"));
case SessionState::Connected:
return QColor(QStringLiteral("#2e7d32"));
case SessionState::Failed:
return QColor(QStringLiteral("#c62828"));
}
return palette.color(QPalette::WindowText);
}
QStringList terminalThemeNames()
{
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
}
}
SessionWindow::SessionWindow(QWidget* parent)
: QMainWindow(parent), m_tabs(new QTabWidget(this)), m_profilesWidget(nullptr)
{
loadUiPreferences();
setWindowTitle(QStringLiteral("OrbitHub"));
resize(1080, 760);
setWindowIcon(QApplication::windowIcon());
m_tabs->setTabsClosable(true);
connect(m_tabs,
@@ -16,27 +54,215 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
this,
[this](int index) {
QWidget* tab = m_tabs->widget(index);
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
sessionTab->disconnectSession();
}
m_tabs->removeTab(index);
delete tab;
if (m_tabs->count() == 0) {
close();
if (sessionTabCount() == 0) {
setWindowTitle(QStringLiteral("OrbitHub"));
}
});
m_tabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_tabs->tabBar(),
&QWidget::customContextMenuRequested,
this,
[this](const QPoint& pos) {
const int index = m_tabs->tabBar()->tabAt(pos);
if (index < 0) {
return;
}
auto* tab = qobject_cast<SessionTab*>(m_tabs->widget(index));
if (tab == nullptr) {
return;
}
QMenu menu(this);
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
QAction* toggleEventsAction = menu.addAction(
tab->isEventsPanelExpanded() ? QStringLiteral("Hide Events")
: QStringLiteral("Show Events"));
QAction* copyEventsAction = menu.addAction(QStringLiteral("Copy Events"));
QAction* exportEventsAction = menu.addAction(QStringLiteral("Export Events"));
QAction* clearEventsAction = menu.addAction(QStringLiteral("Clear Events"));
QList<QAction*> themeActions;
if (tab->supportsThemeSelection()) {
menu.addSeparator();
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
const QString currentTheme = tab->terminalThemeName();
for (const QString& themeName : terminalThemeNames()) {
QAction* themeAction = themeMenu->addAction(themeName);
themeAction->setCheckable(true);
themeAction->setChecked(
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
themeActions.append(themeAction);
}
}
QAction* clearAction = nullptr;
if (tab->supportsClearAction()) {
clearAction = menu.addAction(QStringLiteral("Clear"));
}
QAction* zoomInAction = nullptr;
QAction* zoomOutAction = nullptr;
QAction* resetZoomAction = nullptr;
QAction* setFontSizeAction = nullptr;
if (tab->supportsZoom()) {
menu.addSeparator();
zoomInAction = menu.addAction(QStringLiteral("Increase Font Size"));
zoomOutAction = menu.addAction(QStringLiteral("Decrease Font Size"));
resetZoomAction = menu.addAction(QStringLiteral("Reset Font Size"));
setFontSizeAction = menu.addAction(QStringLiteral("Set Font Size..."));
}
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
} else if (chosen == reconnectAction) {
tab->reconnectSession();
} else if (chosen == toggleEventsAction) {
tab->setEventsPanelExpanded(!tab->isEventsPanelExpanded());
} else if (chosen == copyEventsAction) {
tab->copyEvents();
} else if (chosen == exportEventsAction) {
tab->exportEventsToFile();
} else if (chosen == clearEventsAction) {
tab->clearEvents();
} else if (clearAction != nullptr && chosen == clearAction) {
tab->clearTerminal();
} else if (zoomInAction != nullptr && chosen == zoomInAction) {
tab->zoomIn();
} else if (zoomOutAction != nullptr && chosen == zoomOutAction) {
tab->zoomOut();
} else if (resetZoomAction != nullptr && chosen == resetZoomAction) {
tab->resetZoom();
} else if (setFontSizeAction != nullptr && chosen == setFontSizeAction) {
bool accepted = false;
const int currentSize =
tab->terminalFontPointSize() > 0 ? tab->terminalFontPointSize() : 10;
const int newSize = QInputDialog::getInt(this,
QStringLiteral("Set Font Size"),
QStringLiteral("Font size (points):"),
currentSize,
6,
72,
1,
&accepted);
if (accepted) {
tab->setTerminalFontPointSize(newSize);
}
} else {
for (QAction* themeAction : themeActions) {
if (chosen == themeAction) {
tab->setTerminalThemeName(themeAction->text());
break;
}
}
}
});
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
fileMenu->addSeparator();
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
connect(newProfileAction,
&QAction::triggered,
this,
[this]() { m_profilesWidget->createProfileInCurrentContext(); });
connect(newFolderAction,
&QAction::triggered,
this,
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
connect(aboutAction,
&QAction::triggered,
this,
[this]() {
AboutDialog dialog(this);
dialog.exec();
});
setCentralWidget(m_tabs);
m_profilesWidget = new ProfilesWindow(this);
const int profilesIndex = m_tabs->addTab(m_profilesWidget, QStringLiteral("Profiles"));
m_tabs->tabBar()->setTabButton(profilesIndex, QTabBar::LeftSide, nullptr);
m_tabs->tabBar()->setTabButton(profilesIndex, QTabBar::RightSide, nullptr);
connect(m_profilesWidget,
&ProfilesWindow::connectRequested,
this,
&SessionWindow::addSessionTab);
}
void SessionWindow::openProfile(const Profile& profile)
{
addSessionTab(profile);
}
void SessionWindow::addSessionTab(const Profile& profile)
{
auto* tab = new SessionTab(profile, this);
auto* tab = new SessionTab(profile, m_preferences, this);
const int index = m_tabs->addTab(tab, tab->tabTitle());
m_tabs->setCurrentIndex(index);
if (sessionTabCount() > 1) {
setWindowTitle(QStringLiteral("OrbitHub Sessions"));
} else {
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
}
m_tabs->tabBar()->setTabTextColor(
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
connect(tab,
&SessionTab::tabTitleChanged,
this,
[this, tab](const QString& title) { updateTabTitle(tab, title); });
connect(tab,
&SessionTab::tabStateChanged,
this,
[this, tab](SessionState state) {
for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) == tab) {
m_tabs->tabBar()->setTabTextColor(
i, tabColorForState(state, m_tabs->palette()));
return;
}
}
});
connect(tab,
&SessionTab::terminalThemeChanged,
this,
[this](const QString& themeName) {
m_preferences.terminalThemeName = themeName.trimmed().isEmpty()
? QStringLiteral("Dark")
: themeName.trimmed();
saveUiPreferences();
});
connect(tab,
&SessionTab::terminalFontSizeChanged,
this,
[this](int pointSize) {
if (pointSize <= 0) {
return;
}
m_preferences.terminalFontPointSize = pointSize;
saveUiPreferences();
});
connect(tab,
&SessionTab::eventsPanelVisibilityChanged,
this,
[this](bool expanded) {
m_preferences.eventsPanelExpanded = expanded;
saveUiPreferences();
});
}
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
@@ -48,3 +274,35 @@ void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
}
}
}
int SessionWindow::sessionTabCount() const
{
return m_tabs->count() - 1;
}
void SessionWindow::loadUiPreferences()
{
QSettings settings;
m_preferences.terminalThemeName =
settings.value(QStringLiteral("session/defaultTerminalTheme"), QStringLiteral("Dark"))
.toString()
.trimmed();
if (m_preferences.terminalThemeName.isEmpty()) {
m_preferences.terminalThemeName = QStringLiteral("Dark");
}
m_preferences.eventsPanelExpanded =
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
m_preferences.terminalFontPointSize =
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
}
void SessionWindow::saveUiPreferences() const
{
QSettings settings;
settings.setValue(QStringLiteral("session/defaultTerminalTheme"),
m_preferences.terminalThemeName);
settings.setValue(QStringLiteral("session/eventsPanelExpanded"),
m_preferences.eventsPanelExpanded);
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
m_preferences.terminalFontPointSize);
}
+9 -2
View File
@@ -2,24 +2,31 @@
#define ORBITHUB_SESSION_WINDOW_H
#include "profile_repository.h"
#include "session_tab.h"
#include <QMainWindow>
class QTabWidget;
class SessionTab;
class ProfilesWindow;
class SessionWindow : public QMainWindow
{
Q_OBJECT
public:
explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
explicit SessionWindow(QWidget* parent = nullptr);
void openProfile(const Profile& profile);
private:
QTabWidget* m_tabs;
ProfilesWindow* m_profilesWidget;
SessionUiPreferences m_preferences;
void addSessionTab(const Profile& profile);
void updateTabTitle(SessionTab* tab, const QString& title);
void loadUiPreferences();
void saveUiPreferences() const;
int sessionTabCount() const;
};
#endif
+134 -25
View File
@@ -14,17 +14,6 @@ QString escapeForShellSingleQuotes(const QString& value)
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
return escaped;
}
QString escapedForWindowsEcho(const QString& value)
{
QString escaped = value;
escaped.replace(QStringLiteral("^"), QStringLiteral("^^"));
escaped.replace(QStringLiteral("&"), QStringLiteral("^&"));
escaped.replace(QStringLiteral("|"), QStringLiteral("^|"));
escaped.replace(QStringLiteral("<"), QStringLiteral("^<"));
escaped.replace(QStringLiteral(">"), QStringLiteral("^>"));
return escaped;
}
}
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
@@ -33,7 +22,12 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
m_connectedProbeTimer(new QTimer(this)),
m_state(SessionState::Disconnected),
m_userInitiatedDisconnect(false),
m_reconnectPending(false)
m_reconnectPending(false),
m_waitingForPasswordPrompt(false),
m_waitingForHostKeyConfirmation(false),
m_passwordSubmitted(false),
m_terminalColumns(0),
m_terminalRows(0)
{
m_connectedProbeTimer->setSingleShot(true);
@@ -46,6 +40,10 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
this,
&SshSessionBackend::onProcessFinished);
connect(m_process,
&QProcess::readyReadStandardOutput,
this,
&SshSessionBackend::onReadyReadStandardOutput);
connect(m_process,
&QProcess::readyReadStandardError,
this,
@@ -75,6 +73,10 @@ void SshSessionBackend::connectSession(const SessionConnectOptions& options)
m_userInitiatedDisconnect = false;
m_reconnectPending = false;
m_lastRawError.clear();
m_activeOptions = options;
m_waitingForPasswordPrompt = false;
m_waitingForHostKeyConfirmation = false;
m_passwordSubmitted = false;
if (!startSshProcess(options)) {
return;
@@ -123,6 +125,45 @@ void SshSessionBackend::reconnectSession(const SessionConnectOptions& options)
m_process->terminate();
}
void SshSessionBackend::sendInput(const QString& input)
{
if (m_process->state() != QProcess::Running) {
emit eventLogged(QStringLiteral("Input ignored: session is not running."));
return;
}
if (input.isEmpty()) {
return;
}
m_process->write(input.toUtf8());
}
void SshSessionBackend::confirmHostKey(bool trustHost)
{
if (m_process->state() != QProcess::Running || !m_waitingForHostKeyConfirmation) {
return;
}
m_waitingForHostKeyConfirmation = false;
const QString response = trustHost ? QStringLiteral("yes\n") : QStringLiteral("no\n");
m_process->write(response.toUtf8());
emit eventLogged(trustHost
? QStringLiteral("Host key accepted by user.")
: QStringLiteral("Host key rejected by user."));
}
void SshSessionBackend::updateTerminalSize(int columns, int rows)
{
m_terminalColumns = columns;
m_terminalRows = rows;
if (m_state == SessionState::Connected) {
applyTerminalSizeIfAvailable();
}
}
void SshSessionBackend::onProcessStarted()
{
emit eventLogged(QStringLiteral("ssh process started."));
@@ -178,6 +219,21 @@ void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus)
setState(SessionState::Disconnected, QStringLiteral("SSH session ended."));
}
void SshSessionBackend::onReadyReadStandardOutput()
{
const QString chunk = QString::fromUtf8(m_process->readAllStandardOutput());
if (chunk.isEmpty()) {
return;
}
emit outputReceived(chunk);
if (m_state == SessionState::Connecting && !m_waitingForHostKeyConfirmation
&& !m_waitingForPasswordPrompt) {
setState(SessionState::Connected, QStringLiteral("SSH session established."));
}
}
void SshSessionBackend::onReadyReadStandardError()
{
const QString chunk = QString::fromUtf8(m_process->readAllStandardError());
@@ -186,10 +242,40 @@ void SshSessionBackend::onReadyReadStandardError()
}
m_lastRawError += chunk;
emit outputReceived(chunk);
const QStringList lines = chunk.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
for (const QString& line : lines) {
emit eventLogged(line.trimmed());
const QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
emit eventLogged(trimmed);
}
if (trimmed.contains(QStringLiteral("Are you sure you want to continue connecting"),
Qt::CaseInsensitive)
&& !m_waitingForHostKeyConfirmation) {
m_waitingForHostKeyConfirmation = true;
emit eventLogged(QStringLiteral("Awaiting host key confirmation from user."));
emit hostKeyConfirmationRequested(trimmed);
continue;
}
if (trimmed.contains(QStringLiteral("password:"), Qt::CaseInsensitive)
&& profile().authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0
&& !m_passwordSubmitted) {
if (m_activeOptions.password.isEmpty()) {
const QString message = QStringLiteral("Password prompt received but no password is available.");
setState(SessionState::Failed, message);
emit connectionError(message, trimmed);
return;
}
m_waitingForPasswordPrompt = false;
m_passwordSubmitted = true;
m_process->write((m_activeOptions.password + QStringLiteral("\n")).toUtf8());
emit eventLogged(QStringLiteral("Password prompt received; credentials submitted."));
continue;
}
}
}
@@ -199,7 +285,8 @@ void SshSessionBackend::onConnectedProbeTimeout()
return;
}
if (m_process->state() == QProcess::Running) {
if (m_process->state() == QProcess::Running && !m_waitingForHostKeyConfirmation
&& !m_waitingForPasswordPrompt) {
setState(SessionState::Connected, QStringLiteral("SSH session established."));
}
}
@@ -209,6 +296,10 @@ void SshSessionBackend::setState(SessionState state, const QString& message)
m_state = state;
emit stateChanged(state, message);
emit eventLogged(message);
if (m_state == SessionState::Connected) {
applyTerminalSizeIfAvailable();
}
}
bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
@@ -229,12 +320,9 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
return false;
}
cleanupAskPassScript();
QStringList args;
args << QStringLiteral("-N") << QStringLiteral("-T") << QStringLiteral("-p")
<< QString::number(p.port) << QStringLiteral("-o")
<< QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(p.port)
<< QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveCountMax=2");
@@ -248,6 +336,8 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
<< QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsFileForNullDevice());
} else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new");
} else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask");
} else {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
}
@@ -265,6 +355,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password")
<< QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no")
<< QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1");
m_waitingForPasswordPrompt = false;
QString askPassError;
if (!configureAskPass(options, environment, askPassError)) {
@@ -346,10 +437,11 @@ bool SshSessionBackend::configureAskPass(const SessionConnectOptions& options,
QTextStream out(&script);
#ifdef Q_OS_WIN
out << "@echo off\r\n";
out << "echo " << escapedForWindowsEcho(options.password) << "\r\n";
out << "echo " << options.password << "\r\n";
#else
const QString escapedPassword = escapeForShellSingleQuotes(options.password);
out << "#!/bin/sh\n";
out << "printf '%s\\n' '" << escapeForShellSingleQuotes(options.password) << "'\n";
out << "printf '%s\\n' '" << escapedPassword << "'\n";
#endif
out.flush();
script.close();
@@ -368,7 +460,6 @@ bool SshSessionBackend::configureAskPass(const SessionConnectOptions& options,
if (!environment.contains(QStringLiteral("DISPLAY"))) {
environment.insert(QStringLiteral("DISPLAY"), QStringLiteral(":0"));
}
return true;
}
@@ -407,11 +498,11 @@ QString SshSessionBackend::mapSshError(const QString& rawError) const
return QStringLiteral("Private key file is not accessible.");
}
if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) {
if (raw.contains(QStringLiteral("ssh-askpass"), Qt::CaseInsensitive)) {
return QStringLiteral("SSH password helper is missing or failed to launch.");
}
return QStringLiteral("Required file was not found.");
}
if (raw.contains(QStringLiteral("Text file busy"), Qt::CaseInsensitive)) {
return QStringLiteral("Credential helper could not start (text file busy). Retry the connection.");
}
if (raw.isEmpty()) {
return QStringLiteral("SSH connection failed for an unknown reason.");
}
@@ -427,3 +518,21 @@ QString SshSessionBackend::knownHostsFileForNullDevice() const
return QStringLiteral("/dev/null");
#endif
}
void SshSessionBackend::applyTerminalSizeIfAvailable()
{
if (m_process->state() != QProcess::Running) {
return;
}
if (m_terminalColumns <= 0 || m_terminalRows <= 0) {
return;
}
const QString command = QStringLiteral("stty cols %1 rows %2\\n")
.arg(m_terminalColumns)
.arg(m_terminalRows);
m_process->write(command.toUtf8());
emit eventLogged(
QStringLiteral("Applied terminal size: %1x%2").arg(m_terminalColumns).arg(m_terminalRows));
}
+11
View File
@@ -19,11 +19,15 @@ public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
private slots:
void onProcessStarted();
void onProcessErrorOccurred(QProcess::ProcessError error);
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onReadyReadStandardOutput();
void onReadyReadStandardError();
void onConnectedProbeTimeout();
@@ -34,8 +38,14 @@ private:
bool m_userInitiatedDisconnect;
bool m_reconnectPending;
SessionConnectOptions m_reconnectOptions;
SessionConnectOptions m_activeOptions;
QString m_lastRawError;
QString m_askPassScriptPath;
bool m_waitingForPasswordPrompt;
bool m_waitingForHostKeyConfirmation;
bool m_passwordSubmitted;
int m_terminalColumns;
int m_terminalRows;
void setState(SessionState state, const QString& message);
bool startSshProcess(const SessionConnectOptions& options);
@@ -45,6 +55,7 @@ private:
void cleanupAskPassScript();
QString mapSshError(const QString& rawError) const;
QString knownHostsFileForNullDevice() const;
void applyTerminalSizeIfAvailable();
};
#endif
+536
View File
@@ -0,0 +1,536 @@
#include "terminal_view.h"
#include <QApplication>
#include <QClipboard>
#include <QColor>
#include <QFocusEvent>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QResizeEvent>
#include <QTimer>
#include <QTextCursor>
#include <algorithm>
namespace {
QString normalizedThemeName(const QString& value)
{
return value.trimmed().toLower();
}
}
TerminalView::TerminalView(QWidget* parent)
: QTextEdit(parent),
m_bold(false),
m_hasFgColor(false),
m_hasBgColor(false)
{
setReadOnly(false);
setUndoRedoEnabled(false);
setAcceptRichText(false);
setLineWrapMode(QTextEdit::NoWrap);
setContextMenuPolicy(Qt::NoContextMenu);
setCursorWidth(2);
document()->setMaximumBlockCount(4000);
applyThemePalette(paletteByName(QStringLiteral("Dark")));
resetSgrState();
QTimer::singleShot(0, this, [this]() {
moveCursor(QTextCursor::End);
emitTerminalSize();
});
}
QStringList TerminalView::themeNames()
{
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
}
void TerminalView::setThemeName(const QString& themeName)
{
applyThemePalette(paletteByName(themeName));
}
void TerminalView::setFontPointSize(int pointSize)
{
QFont updatedFont = font();
updatedFont.setPointSize(pointSize);
setFont(updatedFont);
emitTerminalSize();
}
void TerminalView::appendTerminalData(const QString& data)
{
if (data.isEmpty()) {
return;
}
const QString merged = m_pendingEscape + data;
m_pendingEscape.clear();
QString plainBuffer;
for (int i = 0; i < merged.size();) {
const QChar ch = merged.at(i);
if (ch == QChar::fromLatin1('\x1b')) {
if (!plainBuffer.isEmpty()) {
appendTextChunk(plainBuffer);
plainBuffer.clear();
}
if (i + 1 >= merged.size()) {
m_pendingEscape = merged.mid(i);
break;
}
if (merged.at(i + 1) != QChar::fromLatin1('[')) {
i += 2;
continue;
}
int end = i + 2;
while (end < merged.size()) {
const ushort c = merged.at(end).unicode();
if (c >= 0x40 && c <= 0x7e) {
break;
}
++end;
}
if (end >= merged.size()) {
m_pendingEscape = merged.mid(i);
break;
}
const QChar finalByte = merged.at(end);
const QString params = merged.mid(i + 2, end - (i + 2));
if (finalByte == QChar::fromLatin1('m')) {
handleSgrSequence(params);
} else if (finalByte == QChar::fromLatin1('J')) {
if (params.isEmpty() || params == QStringLiteral("2")) {
clear();
}
}
i = end + 1;
continue;
}
if (ch == QChar::fromLatin1('\r')) {
const bool hasLfAfter = (i + 1 < merged.size() && merged.at(i + 1) == QChar::fromLatin1('\n'));
if (!hasLfAfter) {
plainBuffer.append(QChar::fromLatin1('\n'));
}
++i;
continue;
}
plainBuffer.append(ch);
++i;
}
if (!plainBuffer.isEmpty()) {
appendTextChunk(plainBuffer);
}
}
void TerminalView::keyPressEvent(QKeyEvent* event)
{
if (event == nullptr) {
return;
}
moveCursor(QTextCursor::End);
const Qt::KeyboardModifiers modifiers = event->modifiers();
if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)
&& event->key() == Qt::Key_C) {
const QString selected = textCursor().selectedText();
if (!selected.isEmpty()) {
QApplication::clipboard()->setText(selected);
}
return;
}
if (modifiers == Qt::ControlModifier) {
switch (event->key()) {
case Qt::Key_C:
emit inputGenerated(QStringLiteral("\x03"));
return;
case Qt::Key_D:
emit inputGenerated(QStringLiteral("\x04"));
return;
case Qt::Key_L:
emit inputGenerated(QStringLiteral("\x0c"));
return;
case Qt::Key_V: {
const QString clipboardText = QApplication::clipboard()->text();
if (!clipboardText.isEmpty()) {
emit inputGenerated(clipboardText);
}
return;
}
default:
break;
}
}
switch (event->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
emit inputGenerated(QStringLiteral("\n"));
return;
case Qt::Key_Backspace:
emit inputGenerated(QStringLiteral("\x7f"));
return;
case Qt::Key_Tab:
emit inputGenerated(QStringLiteral("\t"));
return;
case Qt::Key_Left:
emit inputGenerated(QStringLiteral("\x1b[D"));
return;
case Qt::Key_Right:
emit inputGenerated(QStringLiteral("\x1b[C"));
return;
case Qt::Key_Up:
emit inputGenerated(QStringLiteral("\x1b[A"));
return;
case Qt::Key_Down:
emit inputGenerated(QStringLiteral("\x1b[B"));
return;
default:
break;
}
const QString text = event->text();
if (!text.isEmpty()) {
emit inputGenerated(text);
return;
}
}
void TerminalView::focusInEvent(QFocusEvent* event)
{
QTextEdit::focusInEvent(event);
moveCursor(QTextCursor::End);
}
bool TerminalView::focusNextPrevChild(bool next)
{
Q_UNUSED(next);
// Tab/Shift+Tab must reach keyPressEvent() and be forwarded to the
// remote session instead of moving focus to the next local widget.
return false;
}
void TerminalView::resizeEvent(QResizeEvent* event)
{
QTextEdit::resizeEvent(event);
emitTerminalSize();
}
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
{
const QString theme = normalizedThemeName(themeName);
if (theme == QStringLiteral("light")) {
return ThemePalette{QStringLiteral("Light"),
QColor(QStringLiteral("#ececec")),
QColor(QStringLiteral("#000000")),
{QColor(QStringLiteral("#000000")),
QColor(QStringLiteral("#aa0000")),
QColor(QStringLiteral("#008000")),
QColor(QStringLiteral("#7a5f00")),
QColor(QStringLiteral("#0033cc")),
QColor(QStringLiteral("#8a00a8")),
QColor(QStringLiteral("#005f87")),
QColor(QStringLiteral("#333333"))},
{QColor(QStringLiteral("#5c5c5c")),
QColor(QStringLiteral("#d30000")),
QColor(QStringLiteral("#00a000")),
QColor(QStringLiteral("#9a7700")),
QColor(QStringLiteral("#0055ff")),
QColor(QStringLiteral("#b300db")),
QColor(QStringLiteral("#007ea7")),
QColor(QStringLiteral("#111111"))}};
}
if (theme == QStringLiteral("solarized dark")) {
return ThemePalette{QStringLiteral("Solarized Dark"),
QColor(QStringLiteral("#002b36")),
QColor(QStringLiteral("#839496")),
{QColor(QStringLiteral("#073642")),
QColor(QStringLiteral("#dc322f")),
QColor(QStringLiteral("#859900")),
QColor(QStringLiteral("#b58900")),
QColor(QStringLiteral("#268bd2")),
QColor(QStringLiteral("#d33682")),
QColor(QStringLiteral("#2aa198")),
QColor(QStringLiteral("#eee8d5"))},
{QColor(QStringLiteral("#586e75")),
QColor(QStringLiteral("#cb4b16")),
QColor(QStringLiteral("#586e75")),
QColor(QStringLiteral("#657b83")),
QColor(QStringLiteral("#839496")),
QColor(QStringLiteral("#6c71c4")),
QColor(QStringLiteral("#93a1a1")),
QColor(QStringLiteral("#fdf6e3"))}};
}
return ThemePalette{QStringLiteral("Dark"),
QColor(QStringLiteral("#1e1e1e")),
QColor(QStringLiteral("#d4d4d4")),
{QColor(QStringLiteral("#000000")),
QColor(QStringLiteral("#cd3131")),
QColor(QStringLiteral("#0dbc79")),
QColor(QStringLiteral("#e5e510")),
QColor(QStringLiteral("#2472c8")),
QColor(QStringLiteral("#bc3fbc")),
QColor(QStringLiteral("#11a8cd")),
QColor(QStringLiteral("#e5e5e5"))},
{QColor(QStringLiteral("#666666")),
QColor(QStringLiteral("#f14c4c")),
QColor(QStringLiteral("#23d18b")),
QColor(QStringLiteral("#f5f543")),
QColor(QStringLiteral("#3b8eea")),
QColor(QStringLiteral("#d670d6")),
QColor(QStringLiteral("#29b8db")),
QColor(QStringLiteral("#ffffff"))}};
}
QColor TerminalView::colorFrom256Index(int index)
{
if (index < 0) {
index = 0;
}
if (index > 255) {
index = 255;
}
if (index < 16) {
static const std::array<QColor, 16> base = {
QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#800000")),
QColor(QStringLiteral("#008000")), QColor(QStringLiteral("#808000")),
QColor(QStringLiteral("#000080")), QColor(QStringLiteral("#800080")),
QColor(QStringLiteral("#008080")), QColor(QStringLiteral("#c0c0c0")),
QColor(QStringLiteral("#808080")), QColor(QStringLiteral("#ff0000")),
QColor(QStringLiteral("#00ff00")), QColor(QStringLiteral("#ffff00")),
QColor(QStringLiteral("#0000ff")), QColor(QStringLiteral("#ff00ff")),
QColor(QStringLiteral("#00ffff")), QColor(QStringLiteral("#ffffff"))};
return base.at(static_cast<size_t>(index));
}
if (index >= 16 && index <= 231) {
const int c = index - 16;
const int r = c / 36;
const int g = (c / 6) % 6;
const int b = c % 6;
const auto channel = [](int v) { return v == 0 ? 0 : 55 + v * 40; };
return QColor(channel(r), channel(g), channel(b));
}
const int gray = 8 + (index - 232) * 10;
return QColor(gray, gray, gray);
}
void TerminalView::applyThemePalette(const ThemePalette& palette)
{
m_palette = palette;
const QString stylesheet = QStringLiteral("QTextEdit { background: %1; color: %2; }")
.arg(m_palette.background.name(), m_palette.foreground.name());
setStyleSheet(stylesheet);
if (!m_hasFgColor) {
m_fgColor = m_palette.foreground;
}
if (!m_hasBgColor) {
m_bgColor = m_palette.background;
}
applyCurrentFormat();
}
void TerminalView::applyCurrentFormat()
{
m_currentFormat = QTextCharFormat();
m_currentFormat.setForeground(m_hasFgColor ? m_fgColor : m_palette.foreground);
if (m_hasBgColor) {
m_currentFormat.setBackground(m_bgColor);
}
QFont font = currentFont();
font.setBold(m_bold);
m_currentFormat.setFont(font);
}
void TerminalView::resetSgrState()
{
m_bold = false;
m_hasFgColor = false;
m_hasBgColor = false;
m_fgColor = m_palette.foreground;
m_bgColor = m_palette.background;
applyCurrentFormat();
}
void TerminalView::handleSgrSequence(const QString& params)
{
QStringList parts = params.split(QChar::fromLatin1(';'), Qt::KeepEmptyParts);
if (parts.isEmpty()) {
parts.push_back(QStringLiteral("0"));
}
for (int i = 0; i < parts.size(); ++i) {
const QString part = parts.at(i).trimmed();
bool ok = false;
const int code = part.isEmpty() ? 0 : part.toInt(&ok);
if (!ok && !part.isEmpty()) {
continue;
}
if (code == 0) {
resetSgrState();
continue;
}
if (code == 1) {
m_bold = true;
continue;
}
if (code == 22) {
m_bold = false;
continue;
}
if (code == 39) {
m_hasFgColor = false;
continue;
}
if (code == 49) {
m_hasBgColor = false;
continue;
}
if (code >= 30 && code <= 37) {
m_fgColor = paletteColor(false, code - 30, false);
m_hasFgColor = true;
continue;
}
if (code >= 90 && code <= 97) {
m_fgColor = paletteColor(false, code - 90, true);
m_hasFgColor = true;
continue;
}
if (code >= 40 && code <= 47) {
m_bgColor = paletteColor(true, code - 40, false);
m_hasBgColor = true;
continue;
}
if (code >= 100 && code <= 107) {
m_bgColor = paletteColor(true, code - 100, true);
m_hasBgColor = true;
continue;
}
if (code == 38 || code == 48) {
const bool background = (code == 48);
if (i + 1 >= parts.size()) {
continue;
}
const int mode = parts.at(i + 1).toInt(&ok);
if (!ok) {
continue;
}
if (mode == 5 && i + 2 < parts.size()) {
const int index = parts.at(i + 2).toInt(&ok);
if (ok) {
const QColor color = colorFrom256Index(index);
if (background) {
m_bgColor = color;
m_hasBgColor = true;
} else {
m_fgColor = color;
m_hasFgColor = true;
}
}
i += 2;
continue;
}
if (mode == 2 && i + 4 < parts.size()) {
const int r = parts.at(i + 2).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const int g = parts.at(i + 3).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const int b = parts.at(i + 4).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const QColor color(r, g, b);
if (background) {
m_bgColor = color;
m_hasBgColor = true;
} else {
m_fgColor = color;
m_hasFgColor = true;
}
i += 4;
continue;
}
}
}
applyCurrentFormat();
}
void TerminalView::appendTextChunk(const QString& text)
{
if (text.isEmpty()) {
return;
}
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::End);
cursor.insertText(text, m_currentFormat);
setTextCursor(cursor);
ensureCursorVisible();
}
QColor TerminalView::paletteColor(bool, int index, bool bright) const
{
const int safeIndex = std::clamp(index, 0, 7);
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
: m_palette.normal.at(static_cast<size_t>(safeIndex));
}
int TerminalView::terminalColumns() const
{
const QFontMetrics metrics(font());
const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M')));
return std::max(1, viewport()->width() / cellWidth);
}
int TerminalView::terminalRows() const
{
const QFontMetrics metrics(font());
const int cellHeight = std::max(1, metrics.lineSpacing());
return std::max(1, viewport()->height() / cellHeight);
}
void TerminalView::emitTerminalSize()
{
emit terminalSizeChanged(terminalColumns(), terminalRows());
}
+68
View File
@@ -0,0 +1,68 @@
#ifndef ORBITHUB_TERMINAL_VIEW_H
#define ORBITHUB_TERMINAL_VIEW_H
#include <QTextEdit>
#include <array>
class QKeyEvent;
class QFocusEvent;
class QResizeEvent;
class TerminalView : public QTextEdit
{
Q_OBJECT
public:
explicit TerminalView(QWidget* parent = nullptr);
static QStringList themeNames();
void setThemeName(const QString& themeName);
void appendTerminalData(const QString& data);
void setFontPointSize(int pointSize);
signals:
void inputGenerated(const QString& input);
void terminalSizeChanged(int columns, int rows);
protected:
void keyPressEvent(QKeyEvent* event) override;
void focusInEvent(QFocusEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
bool focusNextPrevChild(bool next) override;
private:
struct ThemePalette {
QString name;
QColor background;
QColor foreground;
std::array<QColor, 8> normal;
std::array<QColor, 8> bright;
};
ThemePalette m_palette;
QString m_pendingEscape;
QString m_rawHistory;
bool m_bold;
bool m_hasFgColor;
bool m_hasBgColor;
QColor m_fgColor;
QColor m_bgColor;
QTextCharFormat m_currentFormat;
static ThemePalette paletteByName(const QString& themeName);
static QColor colorFrom256Index(int index);
void applyThemePalette(const ThemePalette& palette);
void applyCurrentFormat();
void resetSgrState();
void handleSgrSequence(const QString& params);
void appendTextChunk(const QString& text);
QColor paletteColor(bool background, int index, bool bright) const;
void processData(const QString& data, bool storeInHistory);
int terminalColumns() const;
int terminalRows() const;
void emitTerminalSize();
};
#endif
+13
View File
@@ -24,3 +24,16 @@ void UnsupportedSessionBackend::reconnectSession(const SessionConnectOptions& op
{
connectSession(options);
}
void UnsupportedSessionBackend::sendInput(const QString&)
{
emit eventLogged(QStringLiteral("Input ignored: protocol backend is not interactive."));
}
void UnsupportedSessionBackend::confirmHostKey(bool)
{
}
void UnsupportedSessionBackend::updateTerminalSize(int, int)
{
}
+3
View File
@@ -14,6 +14,9 @@ public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
};
#endif
+124
View File
@@ -0,0 +1,124 @@
---
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: true
AfterControlStatement: true
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: true
AfterExternBlock: true
BeforeCatch: true
BeforeElse: true
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Allman
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakStringLiterals: true
ColumnLimit: 100
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: false
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: ForIndentation
...
Language: Cpp
Standard: Auto
NamespaceIndentation: All
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
...
Language: ObjC
PointerBindsToType: false
SortIncludes: false
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
...
Language: Java
BreakAfterJavaFieldAnnotations: false
...
Language: JavaScript
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
...
Language: Proto
...
Language: TableGen
...
Language: TextProto
...
+138
View File
@@ -0,0 +1,138 @@
---
Checks: >
-*,
abseil-*,
altera-*,
bugprone-*,
cert-*,
clang-analyzer*,
concurrency-*,
cppcoreguidelines*,
google-*,
hicpp-*,
llvm-*,
modernize-*,
objc-*,
openmp-*,
performance-*,
portability-*,
readability-*,
-altera-id-dependent-backward-branch,
-altera-struct-pack-align,
-altera-unroll-loops,
-cppcoreguidelines-interfaces-global-init,
-bugprone-easily-swappable-parameters,
-bugprone-assignment-in-if-condition,
-bugprone-branch-clone,
-bugprone-macro-parentheses,
-cert-dcl16-c,
-cert-env33-c,
-cert-dcl50-cpp,
-clang-analyzer-webkit.NoUncountedMemberChecker,
-clang-analyzer-optin.performance.Padding,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
-clang-analyzer-security.VAList,
-clang-analyzer-valist.Uninitialized,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-no-malloc,
-cppcoreguidelines-use-enum-class,
-google-readability-braces-around-statements,
-google-readability-todo,
-hicpp-avoid-c-arrays,
-hicpp-braces-around-statements,
-hicpp-no-array-decay,
-hicpp-no-assembler,
-hicpp-multiway-paths-covered,
-hicpp-signed-bitwise,
-hicpp-uppercase-literal-suffix,
-hicpp-vararg,
-hicpp-no-malloc,
-llvm-use-ranges,
-llvm-header-guard,
-llvm-include-order,
-llvm-qualified-auto,
-llvm-else-after-return,
-readability-else-after-return,
-readability-avoid-nested-conditional-operator,
-modernize-use-using,
-modernize-avoid-variadic-functions,
-modernize-use-trailing-return-type,
-modernize-return-braced-init-list,
-modernize-macro-to-enum,
-modernize-pass-by-value,
-modernize-avoid-c-arrays,
-readability-use-anyofallof,
-readability-braces-around-statements,
-readability-convert-member-functions-to-static,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-readability-implicit-bool-conversion,
-readability-magic-numbers,
-readability-math-missing-parentheses,
-readability-misleading-indentation,
-readability-qualified-auto,
-readability-redundant-parentheses,
-readability-suspicious-call-argument,
-readability-string-compare,
-readability-uppercase-literal-suffix,
-readability-use-concise-preprocessor-directives,
-performance-no-int-to-ptr,
-performance-enum-size,
-performance-avoid-endl,
-portability-avoid-pragma-once
WarningsAsErrors: ''
HeaderFilterRegex: ''
FormatStyle: file
User: nin
CheckOptions:
- key: readability-implicit-bool-conversion.AllowIntegerConditions
value: 'true'
- key: llvm-else-after-return.WarnOnConditionVariables
value: 'false'
- key: modernize-loop-convert.MinConfidence
value: reasonable
- key: modernize-replace-auto-ptr.IncludeStyle
value: llvm
- key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons
value: 'false'
- key: google-readability-namespace-comments.ShortNamespaceLines
value: '10'
- key: cert-err33-c.CheckedFunctions
value: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;'
- key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
value: 'false'
- key: cert-dcl16-c.NewSuffixes
value: 'L;LL;LU;LLU'
- key: google-readability-braces-around-statements.ShortStatementLines
value: '1'
- key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: 'true'
- key: google-readability-namespace-comments.SpacesBeforeComments
value: '2'
- key: modernize-loop-convert.MaxCopySize
value: '16'
- key: modernize-pass-by-value.IncludeStyle
value: llvm
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: llvm-qualified-auto.AddConstToQualified
value: 'false'
- key: modernize-loop-convert.NamingStyle
value: CamelCase
- key: llvm-else-after-return.WarnOnUnfixable
value: 'false'
- key: google-readability-function-size.StatementThreshold
value: '800'
...
+3
View File
@@ -0,0 +1,3 @@
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
+28
View File
@@ -0,0 +1,28 @@
## Found a bug? - We would like to help you and smash the bug away.
1. __Please don't "report" questions as bugs.__
* We are reachable via
* Matrix room : #FreeRDP:matrix.org (main)
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
* IRC channel : #freerdp @ irc.oftc.net (bridged)
* We are reachable via mailing list <freerdp-devel@lists.sourceforge.net>
* Try our mailing list for discussions/questions
1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information.
1. If it's a __new__ bug - create a new issue.
1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting
## To save time and help us identify the issue a bug report should at least contain the following:
* a useful description of the bug - "It's not working" isn't good enough - you must try harder ;)
* the steps to reproduce the bug
* command line you have used
* to what system did you connect to? (win8, 2008, ..)
* what did you expect to happen?
* what actually happened?
* freerdp version (e.g. xfreerdp --version) or package version or git commit
* freerdp configuration (e.g. xfreerdp --buildconfig)
* operating System, architecture, distribution e.g. linux, amd64, debian
* if you built it yourself add some notes which branch you have used, also your cmake parameters can help
* extra information helping us to find the bug
## Please remove this text before submitting your issue!
_Thank you for reporting a bug!_
@@ -0,0 +1,7 @@
---
name: Backport
about: Create a issue to request/track a backport
---
Related pull request for master:
@@ -0,0 +1,55 @@
---
name: Bug report
about: Create a report to help us improve
---
**Found a bug? - We would like to help you and smash the bug away.**
1. __Please don't "report" questions as bugs. For these (questions/build instructions/...) please use one of the following means of contact:__
* We are reachable via:
* Matrix room : #FreeRDP:matrix.org (main)
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
* IRC channel : #freerdp @ irc.oftc.net (bridged)
* We are reachable via mailing list <freerdp-devel@lists.sourceforge.net>
* Try our mailing list for discussions/questions
1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information.
1. If it's a __new__ bug - create a new issue.
1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Application details**
* FreeRDP version (`xfreerdp /version`)
* Command line used
* Output of `xfreerdp /buildconfig`
* OS version connecting to (server side)
* If available the log output from a run with `/log-level:trace 2>&1 | tee log.txt`
* If you built it yourself add some notes which tag/commit/branch you have used, also your cmake parameters and
compiler can help
**Environment (please complete the following information):**
- OS: [e.g. Linux/Windows/Android/..]
- Version/Distribution: [e.g. Debian 10, Windows 2008, Android 10]
- Architecture: [amd64, arm]:
**Additional context**
Add any other context about the problem here.
** Please remove this text before submitting your issue!
_Thank you for reporting a bug!_
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+29
View File
@@ -0,0 +1,29 @@
## This is how are pull requests handled by FreeRDP
1. Every new pull request needs to build and pass the unit tests at https://ci.freerdp.com
1. At least 1 (better two) people need to review and test a pull request and agree to accept
## Preparations before creating a pull
* Rebase your branch to current master, no merges allowed!
* Try to clean up your commit history, group changes to commits
* Check your formatting! A _clang-format_ script can be found at ```.clang-format```
* The cmake target ```clangformat``` reformats the whole codebase
* Optional (but higly recommended)
* Run a clang scanbuild before and after your changes to avoid introducing new bugs
* Run your compiler at pedantic level to check for new warnings
## To ease accepting your contribution
* Give the pull request a proper name so people looking at it have an basic idea what it is for
* Add at least a brief description what it does (or should do :) and what it's good for
* Give instructions on how to test your changes
* Ideally add unit tests if adding new features
## What you should be prepared for
* fix issues found during the review phase
* Joining our chat to talk to other developers or help them test your pull might accelerate acceptance
* Matrix room : #FreeRDP:matrix.org (main)
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
* IRC channel : #freerdp @ irc.oftc.net (bridged)
* Joining our mailing list <freerdp-devel@lists.sourceforge.net> may be helpful too.
* Check the pull request builder at https://ci.freerdp.com/job/code-quality-checker/ and fix all warnings affecting your code
## Please remove this text before submitting your pull!
+57
View File
@@ -0,0 +1,57 @@
name: abi-checker
on:
workflow_dispatch:
branches: [ master, stable* ]
inputs:
API_BASE_REF:
description: 'Base revision for ABI compatibility check'
required: true
default: '3.6.0'
pull_request:
branches: [ master, stable* ]
schedule:
- cron: '30 4 * * SUN'
jobs:
build:
runs-on: ubuntu-latest
name: "Run ABI checker on ubuntu-latest"
steps:
- name: "Check out pull request"
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'pull_request' }}
uses: suzuki-shunsuke/get-pr-action@v0.1.0
id: pr
- name: "Check out source"
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{steps.pr.outputs.merge_commit_sha}}
- name: "Prepare environment"
run: |
sudo apt-get update -q -y
sudo apt-get --fix-broken install -q -y
sudo apt-get install -q -y devscripts equivs abigail-tools \
clang \
pylint \
curl
./packaging/scripts/prepare_deb_freerdp-nightly.sh
sudo mk-build-deps -i
- name: "Prepare configuration"
run: |
mkdir -p abi-checker
cp scripts/abi-diff.sh abi-checker/
echo "GITHUB_BASE_REF=$GITHUB_BASE_REF"
echo "GITHUB_HEAD_REF=$GITHUB_HEAD_REF"
echo "API_BASE_REF=${{ inputs.API_BASE_REF || '3.6.0' }}"
echo "HEAD=$(git rev-parse HEAD)"
echo "remotes=$(git remote -v)"
- name: "Run ABI check..."
env:
BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'workflow_dispatch' && inputs.API_BASE_REF || '3.6.0' }}
run: |
echo "BASE_REF=$BASE_REF"
./abi-checker/abi-diff.sh $BASE_REF
@@ -0,0 +1,56 @@
name: '[arm,ppc,ricsv] architecture builds'
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: '30 5 * * SUN'
jobs:
build_job:
runs-on: ubuntu-latest
name: "Test on ${{ matrix.distro }}/${{ matrix.arch }}"
strategy:
fail-fast: false
matrix:
include:
- arch: armv7
distro: bookworm
- arch: aarch64
distro: bookworm
- arch: s390x
distro: bookworm
- arch: ppc64le
distro: bookworm
- arch: riscv64
distro: ubuntu24.04
steps:
- uses: actions/checkout@v4
- uses: uraimo/run-on-arch-action@v3.0.1
name: "Run tests"
id: build
with:
arch: ${{ matrix.arch }}
distro: ${{ matrix.distro }}
githubToken: ${{ github.token }}
env: |
CTEST_OUTPUT_ON_FAILURE: 1
WLOG_LEVEL: 'trace'
install: |
echo "whoami: $(whoami)"
echo "working directory: $(pwd)"
apt-get update -q -y
apt-get install -q -y devscripts clang ninja-build ccache equivs
run: |
echo "whoami: $(whoami)"
echo "working directory: $(pwd)"
find . -name control -exec mk-build-deps -i -t "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y" {} \;
cmake -GNinja \
-C ci/cmake-preloads/config-linux-alt-arch.txt \
-B ci-build \
-S . \
-DCMAKE_INSTALL_PREFIX=/tmp/ci-test \
-DCMAKE_C_COMPILER=/usr/bin/clang \
-DCMAKE_CXX_COMPILER=/usr/bin/clang++
cmake --build ci-build --parallel $(nproc) --target install
cmake --build ci-build --parallel $(nproc) --target test
+26
View File
@@ -0,0 +1,26 @@
name: bash-format
on:
workflow_dispatch:
branches: [ master, stable* ]
pull_request:
branches: [ master, stable* ]
schedule:
- cron: '30 4 * * SUN'
jobs:
build:
runs-on: ubuntu-latest
name: "bash-format"
steps:
- name: "Check out source"
uses: actions/checkout@v4
- name: "Prepare environment"
run: |
sudo apt-get update -q -y
sudo apt-get install -q -y \
shfmt
- name: "Run shfmt..."
run: |
./scripts/bash-format.sh
@@ -0,0 +1,24 @@
name: Post clang-tidy review comments
on:
workflow_run:
workflows: ["clang-tidy-review"]
types:
- completed
permissions:
pull-requests: write
issues: write
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: akallabeth/clang-tidy-review/post@master
# lgtm_comment_body, max_comments, and annotations need to be set on the posting workflow in a split setup
with:
token: ${{ secrets.GITHUB_TOKEN }}
annotations: false
max_comments: 10
+28
View File
@@ -0,0 +1,28 @@
name: clang-tidy-review
on:
pull_request:
branches: [ master, stable* ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Run clang-tidy
- uses: akallabeth/clang-tidy-review@master
id: review
with:
split_workflow: true
clang_tidy_checks: ''
apt_packages: devscripts,equivs
install_commands: 'ln -s packaging/deb/freerdp-nightly debian; mk-build-deps -i -t "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y"'
# CMake command to run in order to generate compile_commands.json
build_dir: tidy
cmake_command: cmake -Btidy -S. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -C ci/cmake-preloads/config-qa.cmake
# Uploads an artefact containing clang_fixes.json
- uses: akallabeth/clang-tidy-review/upload@master
id: upload-review
+26
View File
@@ -0,0 +1,26 @@
name: cmake-format
on:
workflow_dispatch:
branches: [ master, stable* ]
pull_request:
branches: [ master, stable* ]
schedule:
- cron: '30 4 * * SUN'
jobs:
build:
runs-on: ubuntu-latest
name: "cmake-format"
steps:
- name: "Check out source"
uses: actions/checkout@v4
- name: "Prepare environment"
run: |
sudo apt-get update -q -y
sudo apt-get install -q -y \
cmake-format
- name: "Run cmake-format..."
run: |
./scripts/cmake-format.sh
@@ -0,0 +1,93 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: '41 2 * * 2'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: c-cpp
build-mode: manual
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
run: |
sudo apt-get update -q -y
sudo apt-get install -q -y devscripts clang ccache ninja-build equivs
./packaging/scripts/prepare_deb_freerdp-nightly.sh
sudo mk-build-deps -i
mkdir ci-build
cd ci-build
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
export CFLAGS="-Weverything"
export CXXFLAGS="-Weverything"
cmake -GNinja ../ci/cmake-preloads/config-linux-all.txt ..
cmake --build .
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+26
View File
@@ -0,0 +1,26 @@
name: codespell
on:
workflow_dispatch:
branches: [ master, stable* ]
pull_request:
branches: [ master, stable* ]
schedule:
- cron: '30 4 * * SUN'
jobs:
build:
runs-on: ubuntu-latest
name: "codespell"
steps:
- name: "Check out source"
uses: actions/checkout@v4
- name: "Prepare environment"
run: |
sudo apt-get update -q -y
sudo apt-get install -q -y \
codespell
- name: "Run codespell..."
run: |
./scripts/codespell.sh
+59
View File
@@ -0,0 +1,59 @@
name: Coverity
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
branches: [ master, stable* ]
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'FreeRDP' }}
steps:
- uses: actions/checkout@v4
- name: Install apt dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
devscripts \
ninja-build \
equivs \
ccache \
clang
sudo mk-build-deps --install packaging/deb/freerdp-nightly/control
- name: Download Coverity build tool
run: |
wget -c -N https://scan.coverity.com/download/linux64 --post-data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=FreeRDP" -O coverity_tool.tar.gz
mkdir coverity_tool
tar xzf coverity_tool.tar.gz --strip 1 -C coverity_tool
- name: Build with Coverity build tool
run: |
export PATH=`pwd`/coverity_tool/bin:$PATH
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
cov-configure --template --compiler clang --comptype clangcc
# in source build is used to help coverity to determine relative file path
cmake \
-GNinja \
-C ci/cmake-preloads/config-coverity.txt \
-DCOVERITY_BUILD=ON \
-Bcov-build \
-S.
cov-build --dir cov-int cmake --build cov-build
- name: Submit build result to Coverity Scan
run: |
tar czvf cov.tar.gz cov-int
curl --form token=${{ secrets.COVERITY_SCAN_TOKEN }} \
--form email=team+coverity@freerdp.com \
--form file=@cov.tar.gz \
--form version="Commit $GITHUB_SHA" \
--form description="Build submitted via CI" \
https://scan.coverity.com/builds?project=FreeRDP
+71
View File
@@ -0,0 +1,71 @@
name: '[freebsd] architecture builds'
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: '30 5 * * SAT'
jobs:
freebsd_job:
runs-on: ubuntu-latest
name: Build on FreeBSD
steps:
- uses: actions/checkout@v4
- name: Test in FreeBSD
id: test
uses: vmactions/freebsd-vm@v1
with:
usesh: true
copyback: false
prepare: |
pkg install -y \
cmake \
ninja \
krb5-devel \
json-c \
libcjson \
fdk-aac \
libsoxr \
sdl2 \
sdl3 \
sdl2_ttf \
sdl2_image \
opus \
png \
webp \
openjpeg \
libjpeg-turbo \
opensc \
v4l_compat \
libv4l \
uriparser \
ffmpeg \
pulseaudio \
pcsc-lite \
cups \
opencl \
openssl34 \
gsm \
influxpkg-config \
icu \
fusefs-libs3 \
ccache \
opencl-clang-llvm15 \
faac \
faad2 \
opus-tools \
openh264 \
alsa-lib \
cairo \
ocl-icd
run: |
export LD_LIBRARY_PATH=/usr/lib/clang/18/lib/freebsd
export CTEST_OUTPUT_ON_FAILURE=1
cmake -GNinja \
-C ci/cmake-preloads/config-freebsd.txt \
-B ci-build \
-S . \
-DCMAKE_INSTALL_PREFIX=/tmp/ci-test
cmake --build ci-build --parallel $(nproc) --target install
cmake --build ci-build --parallel $(nproc) --target test
+43
View File
@@ -0,0 +1,43 @@
name: Fuzzing testing
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: "0 3 21 * *"
jobs:
fuzzing:
if: github.repository == 'FreeRDP/FreeRDP'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sanitizer: [address, undefined]
steps:
- uses: actions/checkout@v4
- name: Build fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'freerdp'
dry-run: false
sanitizer: ${{ matrix.sanitizer }}
- name: Run fuzzers (${{ matrix.sanitizer }})
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'freerdp'
fuzz-seconds: 600
dry-run: false
sanitizer: ${{ matrix.sanitizer }}
- name: Upload crash
uses: actions/upload-artifact@v4.3.6
if: failure() && steps.build.outcome == 'success'
with:
name: ${{ matrix.sanitizer }}-artifacts
retention-days: 21
path: ./out/artifacts
@@ -0,0 +1,29 @@
name: Close inactive issues
on:
workflow_dispatch:
schedule:
- cron: "33 3 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-stale: 30
days-before-close: 30
operations-per-run: 90
exempt-all-milestones: true
exempt-assignees: true
exempt-issue-labels: "wip,pinned,help-wanted,blocker,feature"
exempt-pr-labels: "wip,pinned,help-wanted,blocker,feature"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
name: macos-builder
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: '30 5 * * SUN'
jobs:
build:
runs-on: macos-latest
name: "Run macos build on mac-latest"
steps:
- name: "Check out source"
uses: actions/checkout@v4
- name: "Prepare environment"
run: |
brew install autoconf automake git libtool meson
- name: "Run mac os build..."
run: |
./scripts/bundle-mac-os.sh
+35
View File
@@ -0,0 +1,35 @@
name: mingw-builder
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: '30 5 * * SUN'
jobs:
build:
runs-on: ubuntu-latest
name: "Run mingw build on ubuntu-latest"
steps:
- name: "Check out source"
uses: actions/checkout@v4
- name: "Prepare environment"
run: |
sudo apt-get update -q -y
sudo apt-get install -q -y \
git \
nasm \
meson \
cmake \
ninja-build \
mingw-w64 \
mingw-w64-tools \
binutils-mingw-w64
- name: "Run mingw [shared] build..."
run: |
./scripts/mingw.sh
- name: "Run mingw [static] build..."
run: |
./scripts/mingw.sh -c -s --clean-first
@@ -0,0 +1,61 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
name: timezone-update
on:
workflow_dispatch:
branches: [ master, stable* ]
schedule:
- cron: "0 5 11 * *"
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Configure CMake
run: cmake -G"Visual Studio 17 2022" -Bbuild -Swinpr\libwinpr\timezone\utils
- name: Restore dependencies
run: dotnet restore build\tzextract.sln
- name: Build & Install CMake
run: cmake --build build --config Release
- name: Update timezones
run: build\Release\tzextract.exe winpr\libwinpr\timezone
- name: Format code
run: |
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/WindowsZones.c
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap.c
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap_static.h
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap.json
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v6
with:
commit-message: Update timezone definitions
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: timezone-patches
branch-suffix: timestamp
delete-branch: true
title: '[timezones] Update definitions'
body: |
Timezone update
- Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
labels: |
automated pr
assignees: akallabeth
reviewers: akallabeth
draft: false
+5
View File
@@ -0,0 +1,5 @@
**/CMakeCache.txt
**/CMakeFiles
build
checker
abi-checker
+98
View File
@@ -0,0 +1,98 @@
# Generate .txt license file for CPack (PackageMaker requires a file extension)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt @ONLY)
# Workaround to remove c++ compiler macros and defines for Eclipse.
# If c++ macros/defines are set __cplusplus is also set which causes
# problems when compiling freerdp/jni. To prevent this problem we set the macros to "".
if(ANDROID AND CMAKE_EXTRA_GENERATOR STREQUAL "Eclipse CDT4")
set(CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS "")
message(STATUS "Disabled CXX system defines for eclipse (workaround).")
endif()
set(CPACK_SOURCE_IGNORE_FILES "/\\\\.git/;/\\\\.gitignore;/CMakeCache.txt")
if(NOT WIN32)
if(APPLE AND (NOT IOS))
if(WITH_SERVER)
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "mfreerdp-server")
endif()
endif()
if(WITH_X11)
set(CPACK_PACKAGE_EXECUTABLES "xfreerdp")
if(WITH_SERVER)
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "xfreerdp-server")
endif()
endif()
endif()
set(CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set(CPACK_TOPLEVEL_TAG "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
string(TOLOWER ${CMAKE_PROJECT_NAME} CMAKE_PROJECT_NAME_lower)
set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}")
set(CPACK_PACKAGE_NAME "FreeRDP")
set(CPACK_PACKAGE_VENDOR "FreeRDP")
set(CPACK_PACKAGE_VERSION ${FREERDP_VERSION_FULL})
set(CPACK_PACKAGE_VERSION_MAJOR ${FREERDP_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${FREERDP_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${FREERDP_VERSION_REVISION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "FreeRDP: A Remote Desktop Protocol Implementation")
set(CPACK_PACKAGE_CONTACT "Marc-Andre Moreau")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "marcandre.moreau@gmail.com")
set(CPACK_DEBIAN_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
set(CPACK_PACKAGE_INSTALL_DIRECTORY "FreeRDP")
set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt")
set(CPACK_NSIS_MODIFY_PATH ON)
set(CPACK_PACKAGE_ICON "${PROJECT_SOURCE_DIR}/resources\\\\FreeRDP_Install.bmp")
set(CPACK_NSIS_MUI_ICON "${PROJECT_SOURCE_DIR}/resources\\\\FreeRDP_Icon_96px.ico")
set(CPACK_NSIS_MUI_UNICON "${PROJECT_SOURCE_DIR}/resource\\\\FreeRDP_Icon_96px.ico")
set(CPACK_COMPONENTS_ALL client server libraries headers symbols tools)
if(MSVC)
string(FIND ${CMAKE_MSVC_RUNTIME_LIBRARY} "DLL" IS_SHARED)
if(NOT IS_SHARED STREQUAL "-1")
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
include(InstallRequiredSystemLibraries)
install(PROGRAMS ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT libraries)
endif()
endif()
set(CPACK_COMPONENT_CLIENT_DISPLAY_NAME "Client")
set(CPACK_COMPONENT_CLIENT_GROUP "Applications")
set(CPACK_COMPONENT_SERVER_DISPLAY_NAME "Server")
set(CPACK_COMPONENT_SERVER_GROUP "Applications")
set(CPACK_COMPONENT_LIBRARIES_DISPLAY_NAME "Libraries")
set(CPACK_COMPONENT_LIBRARIES_GROUP "Runtime")
set(CPACK_COMPONENT_HEADERS_DISPLAY_NAME "Headers")
set(CPACK_COMPONENT_HEADERS_GROUP "Development")
set(CPACK_COMPONENT_SYMBOLS_DISPLAY_NAME "Symbols")
set(CPACK_COMPONENT_SYMBOLS_GROUP "Development")
set(CPACK_COMPONENT_TOOLS_DISPLAY_NAME "Tools")
set(CPACK_COMPONENT_TOOLS_GROUP "Applications")
set(CPACK_COMPONENT_GROUP_RUNTIME_DESCRIPTION "Runtime")
set(CPACK_COMPONENT_GROUP_APPLICATIONS_DESCRIPTION "Applications")
set(CPACK_COMPONENT_GROUP_DEVELOPMENT_DESCRIPTION "Development")
configure_file("${PROJECT_SOURCE_DIR}/CMakeCPackOptions.cmake.in" "${PROJECT_BINARY_DIR}/CMakeCPackOptions.cmake" @ONLY)
set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_BINARY_DIR}/CMakeCPackOptions.cmake")
include(CPack)
+10
View File
@@ -0,0 +1,10 @@
# This file is configured at cmake time, and loaded at cpack time.
# To pass variables to cpack from cmake, they must be configured in this file.
if("${CPACK_GENERATOR}" STREQUAL "PackageMaker")
if(CMAKE_PACKAGE_QTGUI)
set(CPACK_PACKAGE_DEFAULT_LOCATION "/Applications")
else()
set(CPACK_PACKAGE_DEFAULT_LOCATION "/usr")
endif()
endif()
+603
View File
@@ -0,0 +1,603 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2011 O.S. Systems Software Ltda.
# Copyright 2011 Otavio Salvador <otavio@ossystems.com.br>
# Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
# Copyright 2012 HP Development Company, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
cmake_minimum_required(VERSION 3.13)
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW)
endif()
project(FreeRDP LANGUAGES C)
add_custom_target(fuzzers COMMENT "Build fuzzers")
if(NOT PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
# Git auto-ignore out-of-source build directory
file(GENERATE OUTPUT .gitignore CONTENT "*")
endif()
if(NOT DEFINED VENDOR)
set(VENDOR "FreeRDP" CACHE STRING "FreeRDP package vendor")
endif()
if(NOT DEFINED PRODUCT)
set(PRODUCT "FreeRDP" CACHE STRING "FreeRDP package name")
endif()
if(NOT DEFINED PROJECT_URL)
set(PROJECT_URL "https://freerdp.com" CACHE STRING "FreeRDP package url")
endif()
if(NOT DEFINED FREERDP_VENDOR)
set(FREERDP_VENDOR 1)
endif()
if(NOT WIN32 AND NOT ANDROID)
if(APPLE)
set(OPT_DEFAULT_VAL OFF)
else()
set(OPT_DEFAULT_VAL ON)
endif()
option(WITH_X11 "build X11 client/server" ${OPT_DEFAULT_VAL})
endif()
# Enable coverity related pragma definitions
if(COVERITY_BUILD)
add_compile_definitions(COVERITY_BUILD)
endif()
# Include our extra modules
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/)
include(ProjectCStandard)
include(PkgConfigHelpers)
# Check for cmake compatibility (enable/disable features)
include(CheckCmakeCompat)
# Include cmake modules
if(WITH_CLANG_FORMAT)
include(ClangFormat)
endif()
include(CompilerFlags)
include(CheckIncludeFiles)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(CheckStructHasMember)
include(TestBigEndian)
include(CompilerDetect)
include(FindFeature)
include(ShowCMakeVars)
include(ConfigOptions)
include(FeatureSummary)
include(CheckCCompilerFlag)
include(CMakePackageConfigHelpers)
include(InstallFreeRDPMan)
include(GetGitRevisionDescription)
include(SetFreeRDPCMakeInstallDir)
include(Doxygen)
include(GetSysconfDir)
# FreeRDP internal builds should always check results
add_compile_definitions(WINPR_DEFINE_ATTR_NODISCARD)
# Soname versioning
set(BUILD_NUMBER 0)
if($ENV{BUILD_NUMBER})
set(BUILD_NUMBER $ENV{BUILD_NUMBER})
endif()
include(GetProjectVersion)
get_project_version(
FREERDP_VERSION_MAJOR FREERDP_VERSION_MINOR FREERDP_VERSION_REVISION FREERDP_VERSION_SUFFIX GIT_REVISION
)
set(FREERDP_API_VERSION "${FREERDP_VERSION_MAJOR}")
set(FREERDP_VERSION "${FREERDP_VERSION_MAJOR}.${FREERDP_VERSION_MINOR}.${FREERDP_VERSION_REVISION}")
if(FREERDP_VERSION_SUFFIX)
set(FREERDP_VERSION_FULL "${FREERDP_VERSION}-${FREERDP_VERSION_SUFFIX}")
else()
set(FREERDP_VERSION_FULL "${FREERDP_VERSION}")
endif()
message("FREERDP_VERSION=${FREERDP_VERSION_FULL}")
message(STATUS "Git Revision ${GIT_REVISION}")
# MSVC compatibility with system headers
add_compile_definitions(NONAMELESSUNION)
# Make the detected version available as default version for all subprojects
set(FREERDP_DEFAULT_PROJECT_VERSION ${FREERDP_VERSION} CACHE STRING INTERNAL)
set(FREERDP_MAJOR_DIR "freerdp${FREERDP_VERSION_MAJOR}")
set(FREERDP_INCLUDE_DIR "include/${FREERDP_MAJOR_DIR}/")
option(WITH_SMARTCARD_EMULATE "Emulate smartcards instead of redirecting readers" ON)
if(WITH_SMARTCARD_EMULATE)
add_compile_definitions(WITH_SMARTCARD_EMULATE)
find_package(ZLIB REQUIRED)
endif()
option(WITH_FREERDP_DEPRECATED "Build FreeRDP deprecated symbols" OFF)
if(WITH_FREERDP_DEPRECATED)
add_compile_definitions(WITH_FREERDP_DEPRECATED)
endif()
option(WITHOUT_FREERDP_3x_DEPRECATED "Build FreeRDP 3x deprecated symbols" OFF)
if(WITH_FREERDP_3x_DEPRECATED)
message(WARNING "WITH_FREERDP_3x_DEPRECATED has been replaced with WITHOUT_FREERDP_3x_DEPRECATED")
set(WITHOUT_FREERDP_3x_DEPRECATED OFF)
endif()
option(WITH_FREERDP_DEPRECATED_COMMANDLINE "Build FreeRDP deprecated command line options" OFF)
if(WITH_FREERDP_DEPRECATED_COMMANDLINE)
add_compile_definitions(WITH_FREERDP_DEPRECATED_COMMANDLINE)
endif()
# Make paths absolute
if(CMAKE_INSTALL_PREFIX)
get_filename_component(CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" ABSOLUTE)
endif()
if(FREERDP_EXTERNAL_PATH)
get_filename_component(FREERDP_EXTERNAL_PATH "${FREERDP_EXTERNAL_PATH}" ABSOLUTE)
endif()
# Allow to search the host machine for git/ccache
if(CMAKE_CROSSCOMPILING)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH)
endif(CMAKE_CROSSCOMPILING)
find_program(CCACHE ccache)
if(CCACHE AND WITH_CCACHE)
if(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE})
endif(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
endif(CCACHE AND WITH_CCACHE)
if(CMAKE_CROSSCOMPILING)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY)
endif(CMAKE_CROSSCOMPILING)
# /Allow to search the host machine for git/ccache
# Turn on solution folders (2.8.4+)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
option(CTEST_OUTPUT_ON_FAILURE ON "show verbose output on CTest failures")
if(BUILD_TESTING_INTERNAL)
set(EXPORT_ALL_SYMBOLS ON CACHE BOOL "testing default" FORCE)
add_compile_definitions(BUILD_TESTING_INTERNAL)
elseif(BUILD_TESTING)
set(EXPORT_ALL_SYMBOLS OFF CACHE BOOL "testing default" FORCE)
endif()
include(ExportAllSymbols)
set(THREAD_PREFER_PTHREAD_FLAG TRUE)
if(NOT IOS)
find_package(Threads REQUIRED)
endif()
if(WIN32)
add_compile_definitions(UNICODE _UNICODE)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(WIN32_LEAN_AND_MEAN)
add_compile_definitions(_WINSOCK_DEPRECATED_NO_WARNINGS)
set(CMAKE_DL_LIBS "")
set(CMAKE_USE_RELATIVE_PATH ON)
string(TIMESTAMP RC_VERSION_YEAR "%Y")
if(NOT DEFINED CMAKE_WINDOWS_VERSION)
set(CMAKE_WINDOWS_VERSION "WIN7")
endif()
if(CMAKE_WINDOWS_VERSION STREQUAL "WINXP")
add_compile_definitions(WINVER=0x0501 _WIN32_WINNT=0x0501)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN7")
add_compile_definitions(WINVER=0x0601 _WIN32_WINNT=0x0601)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN8")
add_compile_definitions(WINVER=0x0602 _WIN32_WINNT=0x0602)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN10")
add_compile_definitions(WINVER=0x0A00 _WIN32_WINNT=0x0A00)
endif()
# Set product and vendor for dll and exe version information.
set(RC_VERSION_VENDOR ${VENDOR})
set(RC_VERSION_PRODUCT ${PRODUCT})
set(RC_VERSION_PATCH ${BUILD_NUMBER})
set(RC_VERSION_DESCRIPTION
"${FREERDP_VERSION_FULL} ${GIT_REVISION} ${CMAKE_WINDOWS_VERSION} ${CMAKE_SYSTEM_PROCESSOR}"
)
if(FREERDP_EXTERNAL_SSL_PATH)
set(OPENSSL_ROOT_DIR ${FREERDP_EXTERNAL_SSL_PATH})
endif()
endif()
add_compile_definitions(FREERDP_EXPORTS)
# Mac OS X
if(APPLE)
if(IOS)
if(NOT FREERDP_IOS_EXTERNAL_SSL_PATH)
message(
STATUS
"FREERDP_IOS_EXTERNAL_SSL_PATH not set! Required if openssl is not found in the iOS SDK (which usually isn't"
)
endif()
set(CMAKE_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH} ${FREERDP_IOS_EXTERNAL_SSL_PATH})
set_property(GLOBAL PROPERTY XCODE_ATTRIBUTE_SKIP_INSTALL YES)
endif(IOS)
# Temporarily disabled, causes the cmake script to be reexecuted, causing the compilation to fail.
# Workaround: specify the parameter in the command-line
# if(WITH_CLANG)
# set(CMAKE_C_COMPILER "clang")
# endif()
if(WITH_VERBOSE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -v")
endif()
endif(APPLE)
# Android
if(ANDROID)
set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ${ANDROID_LIBRARY_USE_LIB64_PATHS})
if(ANDROID_ABI STREQUAL arm64-v8a)
include(CheckCCompilerFlag)
check_c_compiler_flag("-mfloat-abi=softfp" ABI_SOFTFP_SUPPORTED)
if(ABI_SOFTFP_SUPPORTED)
# https://github.com/android/ndk/issues/910
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfloat-abi=softfp")
endif()
endif()
add_compile_definitions("$<$<CONFIG:Debug>:NDK_DEBUG=1>")
# NOTE: Manually add -gdwarf-3, as newer toolchains default to -gdwarf-4,
# which is not supported by the gdbserver binary shipped with
# the android NDK (tested with r9b)
add_compile_options("$<$<CONFIG:Debug>:-gdwarf-3>")
add_link_options(-llog)
# CMAKE_PREFIX_PATH detection is broken in most Android toolchain files
# Append it to CMAKE_FIND_ROOT_PATH and avoid potential duplicates
list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH})
list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH)
if(NOT FREERDP_EXTERNAL_PATH)
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/external/")
set(FREERDP_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/external/")
else()
message(STATUS "FREERDP_EXTERNAL_PATH not set!")
endif()
endif()
list(APPEND CMAKE_INCLUDE_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/include)
list(APPEND CMAKE_LIBRARY_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
if(WITH_GPROF)
configure_file(
${PROJECT_SOURCE_DIR}/scripts/gprof_generate.sh.in ${PROJECT_BINARY_DIR}/scripts/gprof_generate.sh @ONLY
)
endif(WITH_GPROF)
endif()
if(UNIX OR CYGWIN)
set(WAYLAND_FEATURE_TYPE "RECOMMENDED")
else()
set(WAYLAND_FEATURE_TYPE "DISABLED")
endif()
if(WITH_PCSC_WINPR)
find_package(PCSCWinPR)
endif()
set(WAYLAND_FEATURE_PURPOSE "Wayland")
set(WAYLAND_FEATURE_DESCRIPTION "Wayland client")
set(OPENSSL_FEATURE_TYPE "REQUIRED")
set(OPENSSL_FEATURE_PURPOSE "cryptography")
set(OPENSSL_FEATURE_DESCRIPTION "encryption, certificate validation, hashing functions")
set(MBEDTLS_FEATURE_TYPE "OPTIONAL")
set(MBEDTLS_FEATURE_PURPOSE "cryptography")
set(MBEDTLS_FEATURE_DESCRIPTION "[experimental] encryption, certificate validation, hashing functions")
set(PCSC_FEATURE_TYPE "RECOMMENDED")
set(PCSC_FEATURE_PURPOSE "smart card")
set(PCSC_FEATURE_DESCRIPTION "smart card device redirection")
set(OPENH264_FEATURE_TYPE "OPTIONAL")
set(OPENH264_FEATURE_PURPOSE "codec")
set(OPENH264_FEATURE_DESCRIPTION "use OpenH264 library")
set(GSM_FEATURE_TYPE "OPTIONAL")
set(GSM_FEATURE_PURPOSE "codec")
set(GSM_FEATURE_DESCRIPTION "GSM audio codec library")
set(LAME_FEATURE_TYPE "OPTIONAL")
set(LAME_FEATURE_PURPOSE "codec")
set(LAME_FEATURE_DESCRIPTION "lame MP3 audio codec library")
set(FAAD2_FEATURE_TYPE "OPTIONAL")
set(FAAD2_FEATURE_PURPOSE "codec")
set(FAAD2_FEATURE_DESCRIPTION "FAAD2 AAC audio codec library")
set(FAAC_FEATURE_TYPE "OPTIONAL")
set(FAAC_FEATURE_PURPOSE "codec")
set(FAAC_FEATURE_DESCRIPTION "FAAC AAC audio codec library")
set(SOXR_FEATURE_TYPE "OPTIONAL")
set(SOXR_FEATURE_PURPOSE "codec")
set(SOXR_FEATURE_DESCRIPTION "SOX audio resample library")
if(WIN32)
set(WAYLAND_FEATURE_TYPE "DISABLED")
set(PCSC_FEATURE_TYPE "DISABLED")
endif()
if(APPLE)
set(WAYLAND_FEATURE_TYPE "DISABLED")
if(IOS)
set(PCSC_FEATURE_TYPE "DISABLED")
endif()
endif()
if(ANDROID)
set(WAYLAND_FEATURE_TYPE "DISABLED")
set(PCSC_FEATURE_TYPE "DISABLED")
endif()
if(NOT WITHOUT_FREERDP_3x_DEPRECATED)
find_feature(Wayland ${WAYLAND_FEATURE_TYPE} ${WAYLAND_FEATURE_PURPOSE} ${WAYLAND_FEATURE_DESCRIPTION})
endif()
option(WITH_LIBRESSL "[experimental] build with LibreSSL" OFF)
if(WITH_LIBRESSL)
message(WARNING "-DWITH_LIBRESSL=ON: LibreSSL enabled. Expect incompatibilities.")
message(WARNING "Only OpenSSL is tested on each release, other implementations depend on external contributions")
find_package(LibreSSL REQUIRED)
include_directories(SYSTEM ${LibreSSL_INCLUDE_DIRS})
set(OPENSSL_INCLUDE_DIR ${LIBRESSL_INCLUDE_DIR})
set(OPENSSL_LIBRARIES ${LIBRESSL_LIBRARIES})
set(OPENSSL_CRYPTO_LIBRARIES ${LIBRESSL_LIBRARIES})
set(WITH_OPENSSL ON)
set(OPENSSL_FOUND ON)
add_compile_definitions("WITH_LIBRESSL")
add_compile_definitions("WITH_OPENSSL")
else()
find_feature(OpenSSL ${OPENSSL_FEATURE_TYPE} ${OPENSSL_FEATURE_PURPOSE} ${OPENSSL_FEATURE_DESCRIPTION})
find_feature(MbedTLS ${MBEDTLS_FEATURE_TYPE} ${MBEDTLS_FEATURE_PURPOSE} ${MBEDTLS_FEATURE_DESCRIPTION})
endif()
find_feature(PCSC ${PCSC_FEATURE_TYPE} ${PCSC_FEATURE_PURPOSE} ${PCSC_FEATURE_DESCRIPTION})
if(WITH_DSP_FFMPEG OR WITH_VIDEO_FFMPEG OR WITH_FFMPEG)
find_package(FFmpeg REQUIRED COMPONENTS AVUTIL AVCODEC)
endif()
find_feature(OpenH264 ${OPENH264_FEATURE_TYPE} ${OPENH264_FEATURE_PURPOSE} ${OPENH264_FEATURE_DESCRIPTION})
find_feature(GSM ${GSM_FEATURE_TYPE} ${GSM_FEATURE_PURPOSE} ${GSM_FEATURE_DESCRIPTION})
find_feature(LAME ${LAME_FEATURE_TYPE} ${LAME_FEATURE_PURPOSE} ${LAME_FEATURE_DESCRIPTION})
find_feature(FAAD2 ${FAAD2_FEATURE_TYPE} ${FAAD2_FEATURE_PURPOSE} ${FAAD2_FEATURE_DESCRIPTION})
find_feature(FAAC ${FAAC_FEATURE_TYPE} ${FAAC_FEATURE_PURPOSE} ${FAAC_FEATURE_DESCRIPTION})
find_feature(soxr ${SOXR_FEATURE_TYPE} ${SOXR_FEATURE_PURPOSE} ${SOXR_FEATURE_DESCRIPTION})
option(WITH_OPENCL "[experimental] enable OpenCL support for primitives" OFF)
if(WITH_OPENCL)
find_package(OpenCL REQUIRED)
endif()
if(WITH_OPENH264 AND NOT WITH_OPENH264_LOADING)
option(WITH_OPENH264_LOADING "Use LoadLibrary to load openh264 at runtime" OFF)
endif(WITH_OPENH264 AND NOT WITH_OPENH264_LOADING)
# Version check, if we have detected FFMPEG but the version is too old
# deactivate it as sound backend.
if(WITH_DSP_FFMPEG)
if(AVCODEC_VERSION VERSION_LESS "57.48.101")
message(
WARNING
"FFmpeg version detected (${AVCODEC_VERSION}) is too old. (Require at least 57.48.101 for sound). Deactivating"
)
set(WITH_DSP_FFMPEG OFF)
endif()
endif(WITH_DSP_FFMPEG)
if(WITH_OPENH264 AND NOT OPENH264_FOUND)
message(FATAL_ERROR "OpenH264 support requested but not detected")
endif()
set(WITH_OPENH264 ${OPENH264_FOUND})
if(OPENSSL_FOUND)
add_compile_definitions("WITH_OPENSSL")
message(STATUS "Using OpenSSL Version: ${OPENSSL_VERSION}")
include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
endif()
if(MBEDTLS_FOUND)
message(WARNING "-DWITH_MBEDTLS=ON: Mbed-TLS enabled. Expect incompatibilities")
message(WARNING "Only OpenSSL is tested on each release, other implementations depend on external contributions")
add_compile_definitions("WITH_MBEDTLS")
endif()
if(WITH_OPENH264 OR WITH_MEDIA_FOUNDATION OR WITH_VIDEO_FFMPEG OR WITH_MEDIACODEC)
set(WITH_GFX_H264 ON)
else()
set(WITH_GFX_H264 OFF)
endif()
# Android expects all libraries to be loadable
# without paths.
if(ANDROID OR WIN32 OR MAC_BUNDLE)
set(PLUGIN_ABS_PATHS_DEFAULT OFF)
else()
set(PLUGIN_ABS_PATHS_DEFAULT ON)
endif()
option(WITH_ABSOLUTE_PLUGIN_LOAD_PATHS "Load plugins with absolute paths" ${PLUGIN_ABS_PATHS_DEFAULT})
if(NOT WITH_ABSOLUTE_PLUGIN_LOAD_PATHS)
set(FREERDP_DATA_PATH "share")
if(NOT FREERDP_INSTALL_PREFIX)
set(FREERDP_INSTALL_PREFIX ".")
endif()
set(FREERDP_LIBRARY_PATH ".")
set(FREERDP_PLUGIN_PATH ".")
else()
set(FREERDP_DATA_PATH "${CMAKE_INSTALL_PREFIX}/share/${FREERDP_MAJOR_DIR}")
if(NOT FREERDP_INSTALL_PREFIX)
set(FREERDP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
endif()
set(FREERDP_LIBRARY_PATH "${CMAKE_INSTALL_LIBDIR}")
if(WIN32)
set(FREERDP_PLUGIN_PATH "${CMAKE_INSTALL_BINDIR}/${FREERDP_MAJOR_DIR}")
else()
set(FREERDP_PLUGIN_PATH "${CMAKE_INSTALL_LIBDIR}/${FREERDP_MAJOR_DIR}")
endif()
endif()
set(FREERDP_ADDIN_PATH "${FREERDP_PLUGIN_PATH}")
# Path to put extensions
set(FREERDP_EXTENSION_POSTFIX "${FREERDP_MAJOR_DIR}/extensions")
set(FREERDP_EXTENSION_REL_PATH "${CMAKE_INSTALL_LIBDIR}/${FREERDP_EXTENSION_POSTFIX}")
set(FREERDP_EXTENSION_PATH "${CMAKE_INSTALL_FULL_LIBDIR}/${FREERDP_EXTENSION_POSTFIX}")
# Proxy plugins path
if(NOT DEFINED PROXY_PLUGINDIR)
message("using default plugins location")
set(FREERDP_PROXY_PLUGINDIR "${FREERDP_PLUGIN_PATH}/proxy/")
else()
set(FREERDP_PROXY_PLUGINDIR "${PROXY_PLUGINDIR}")
endif()
# Unit Tests
include(CTest)
if(BUILD_TESTING_INTERNAL OR BUILD_TESTING)
enable_testing()
if(MSVC)
set(TESTING_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
else()
set(TESTING_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Testing")
endif()
endif()
include(CommonConfigOptions)
if(FREERDP_UNIFIED_BUILD)
add_subdirectory(winpr)
if(NOT WITHOUT_FREERDP_3x_DEPRECATED)
if(WITH_WAYLAND)
add_subdirectory(uwac)
endif()
endif()
if(WITH_SERVER)
option(WITH_RDTK "build rdtk toolkit" OFF)
if(WITH_RDTK)
add_subdirectory(rdtk)
endif()
endif()
include_directories(${PROJECT_SOURCE_DIR}/winpr/include)
include_directories(${PROJECT_BINARY_DIR}/winpr/include)
else()
find_package(WinPR 3 REQUIRED)
include_directories(SYSTEM ${WinPR_INCLUDE_DIR})
endif()
option(WITH_AAD "Compile with support for Azure AD authentication" ${WITH_WINPR_JSON})
# Include directories
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_BINARY_DIR}/include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
# Sub-directories
if(WITH_THIRD_PARTY)
add_subdirectory(third-party)
if(NOT "${THIRD_PARTY_INCLUDES}" STREQUAL "")
include_directories(SYSTEM ${THIRD_PARTY_INCLUDES})
endif()
endif()
# used in libfreerdp subfolder as well
setfreerdpcmakeinstalldir(FREERDP_CMAKE_INSTALL_DIR "FreeRDP${FREERDP_VERSION_MAJOR}")
add_subdirectory(libfreerdp)
if(WITH_CHANNELS)
add_subdirectory(channels)
endif()
if(WITH_CLIENT_COMMON OR WITH_CLIENT)
add_subdirectory(client)
endif()
if(WITH_SERVER)
add_subdirectory(server)
endif()
# must be after all targets have been added in libfreerdp and channel
install(EXPORT FreeRDPTargets DESTINATION ${FREERDP_CMAKE_INSTALL_DIR})
# Packaging
set(CMAKE_CPACK_INCLUDE_FILE "CMakeCPack.cmake")
if(NOT (VENDOR MATCHES "FreeRDP"))
if(DEFINED CLIENT_VENDOR_PATH)
if(EXISTS "${PROJECT_SOURCE_DIR}/${CLIENT_VENDOR_PATH}/CMakeCPack.cmake")
set(CMAKE_CPACK_INCLUDE_FILE "${CLIENT_VENDOR_PATH}/CMakeCPack.cmake")
endif()
endif()
endif()
#message("VENDOR: ${VENDOR} CLIENT_VENDOR_PATH: ${CLIENT_VENDOR_PATH} CMAKE_CPACK_INCLUDE_FILE: ${CMAKE_CPACK_INCLUDE_FILE}")
set(FREERDP_BUILD_CONFIG_LIST "")
get_cmake_property(res VARIABLES)
foreach(var ${res})
if(var MATCHES "^WITH_*|^BUILD_TESTING*|^WINPR_HAVE_*")
list(APPEND FREERDP_BUILD_CONFIG_LIST "${var}=${${var}}")
endif()
endforeach()
string(REPLACE ";" " " FREERDP_BUILD_CONFIG "${FREERDP_BUILD_CONFIG_LIST}")
add_subdirectory(include)
include(${CMAKE_CPACK_INCLUDE_FILE})
message(STATUS "Intrinsic path configuration:")
#ShowCMakeVars("^CMAKE_INSTALL_PREFIX")
#ShowCMakeVars("^CMAKE_INSTALL_LIBDIR")
showcmakevars("^FREERDP_INSTALL_PREFIX|^FREERDP_LIBRARY_PATH|^FREERDP_PLUGIN_PATH")
showcmakevars("^FREERDP_ADDIN_PATH|^FREERDP_EXTENSION_PATH|^FREERDP_PROXY_PLUGINDIR")
+1481
View File
File diff suppressed because it is too large Load Diff
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+54
View File
@@ -0,0 +1,54 @@
# FreeRDP: A Remote Desktop Protocol Implementation
FreeRDP is a free implementation of the Remote Desktop Protocol (RDP), released under the Apache license.
Enjoy the freedom of using your software wherever you want, the way you want it, in a world where
interoperability can finally liberate your computing experience.
## Code Quality Status
[![abi-checker](https://github.com/FreeRDP/FreeRDP/actions/workflows/abi-checker.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/abi-checker.yml)
[![clang-tidy-review](https://github.com/FreeRDP/FreeRDP/actions/workflows/clang-tidy.yml/badge.svg?event=pull_request_target)](https://github.com/FreeRDP/FreeRDP/actions/workflows/clang-tidy.yml)
[![CodeQL](https://github.com/FreeRDP/FreeRDP/actions/workflows/codeql-analysis.yml/badge.svg?branch=master)](https://github.com/FreeRDP/FreeRDP/actions/workflows/codeql-analysis.yml)
[![mingw-builder](https://github.com/FreeRDP/FreeRDP/actions/workflows/mingw.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/mingw.yml)
[![macos-builder](https://github.com/FreeRDP/FreeRDP/actions/workflows/macos.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/macos.yml)
[![[arm,ppc,ricsv] architecture builds](https://github.com/FreeRDP/FreeRDP/actions/workflows/alt-architectures.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/alt-architectures.yml)
[![[freebsd] architecture builds](https://github.com/FreeRDP/FreeRDP/actions/workflows/freebsd.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/freebsd.yml)
[![coverity](https://scan.coverity.com/projects/616/badge.svg)](https://scan.coverity.com/projects/freerdp)
## Resources
Project website: https://www.freerdp.com/
Issue tracker: https://github.com/FreeRDP/FreeRDP/issues
Sources: https://github.com/FreeRDP/FreeRDP/
Downloads: https://pub.freerdp.com/releases/
Wiki: https://github.com/FreeRDP/FreeRDP/wiki
API documentation: https://pub.freerdp.com/api/
Security policy: https://github.com/FreeRDP/FreeRDP/security/policy
FAQ: https://github.com/FreeRDP/FreeRDP/wiki/FAQ
### Contact
* Matrix room : `#FreeRDP:matrix.org` (main)
* ~~XMPP channel: `#FreeRDP#matrix.org@matrix.org` (bridged)~~ no longer available
* IRC channel : `#freerdp @ irc.oftc.net` (bridged)
* Mailing list: https://lists.sourceforge.net/lists/listinfo/freerdp-devel
## Microsoft Open Specifications
Information regarding the Microsoft Open Specifications can be found at:
https://www.microsoft.com/openspecifications/
A list of reference documentation is maintained here:
https://github.com/FreeRDP/FreeRDP/wiki/Reference-Documentation
## Compilation
Instructions on how to get started compiling FreeRDP can be found on the wiki:
https://github.com/FreeRDP/FreeRDP/wiki/Compilation
+114
View File
@@ -0,0 +1,114 @@
# FreeRDP Security Policies and Procedures
This document describes the security policy and procedures for the [FreeRDP Project](https://github.com/FreeRDP/FreeRDP).
The following topics are covered:
* [Supported Versions](#supported-versions)
* [Reporting a Vulnerability](#reporting-a-vulnerability)
* [Disclosure Procedure](#disclosure-procedure)
## Supported versions
Security is very important for us therefore we try to provide security updates and support for
the latest stable version as well as for the development branch.
Since our development branch is, like the protocol itself, a moving target we won't request CVEs for issues that are *only* found on the development branch.
The following table shows the currently supported versions:
| Version | Branch | Supported |
| ------- |--------------| ------------------ |
| < 2.0.0 | stable-1.x | :x: |
| 2.x.x | stable-2.0 | :x: |
| 3.x.x | stable-3.0 | :white_check_mark: |
| - | master | :white_check_mark: |
## Reporting a vulnerability
**IMPORTANT**: Please, do not file security vulnerabilities as public issues on GitHub
In advance: **Thank you** for reporting a security vulnerability and making FreeRDP more stable! We really appreciate your effort.
Please let us know who we should give the credit or attributions to.
If you have found a security vulnerability in FreeRDP you can either directly open an [Advisory on GitHub](https://github.com/FreeRDP/FreeRDP/security/advisories/new)[^1] or send us an email to mailto:security@freerdp.com
In case of an email you can use the [FreeRDP security team GPG key](#reporting-gpg-key) for encrypted communication.
Once we receive a report we will review it and respond as soon as possible.
###
## Disclosure procedure
When the FreeRDP team receives a report one of the team members will be assigned as primary contact.
The primary contact will do all further communications and coordinate the fix and release process.
How your report will be handled:
* When a report is received we will acknowledge the reception and review the reported issue(s) as soon as possible.
* Once confirmed we will determine the affected versions. If not reported via GitHub a [security advisory draft on GitHub](https://github.com/FreeRDP/FreeRDP/security/advisories) will be created for any issue. If it applies we will request a CVE.
* On a private branch we will fix the issue and check the code for any potential similar problem.
* After the fix is validated we will create and publish a new release for all supported versions and publish the advisories.
## Reporting GPG key
FreeRDP's security reporting public gpg key https://pub.freerdp.com/FreeRDP-security-team.pub.asc
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGBz+jsBEADaIM94hYfn/xDzncQwXl7/q6+06+ssqO3iUGqFr+0EPS+HxRjD
BeKjVRSkuo0+QLQoZgCwkoltEj1xRWNqCTDMA+oZkZH8L82eqCnUQqgCOyNWAVMH
6u6ValiZH3ruYxergBBHhyR4Ot2ia0xWN8MKTp+emLpzQ7goimGMo0mxR5FiDAdb
QKz1q5bgs3bb2pLpERNF+z13OS10Mzk1zdr++1pov5PWOTBRKmvBtPJKswmDpb0y
jQGeeqBFZwKzx0n6BTzDZtkqzTwvGhbm9Sb+qO0IO66IV8zQhPG/JUfDkByd6mX9
Ykke0gxoRx54XqoRwZGNydOxMN6g3Oj1+ioWisltYLs/SzW20f3AMCoTeYyfjKtf
01refrA3aRfhDctvW5/s2LP0OEG2P/yQYXiGhK6uVxShz3Oa5dhFwiS8G63omZRH
AEqSk46EhAbbT4xfZ/Np209rhis4KW40cMMpI0F+XpyfT05ZQD6ytHTPgWTxv/OF
G9zy2ysT0kq+t+Hb+1RWQUq/2Dz9Lf6xLZPgqtyzg8xiFxZ4i1kf/VDWa3M76zn3
qMcj3SPOxKY//wW70jCxf44yD38NvSa1M2Sz/K/RJKWkRWP/jhV1UHYusbzCmsvm
M9JkknNMJvGIjBDjHEVy6dlTaHQoHDY+Me9gsrEX0ZS9xXgAiB2IupabEwARAQAB
tEJGcmVlUkRQIFNlY3VyaXR5IFRlYW0gKGh0dHBzOi8vZnJlZXJkcC5jb20pIDxz
ZWN1cml0eUBmcmVlcmRwLmNvbT6JAk4EEwEKADgWIQRvuAE0sDt7JnxXu0o3Ibww
YbfjNAUCYHP6OwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA3IbwwYbfj
NPviD/43NLg7YfjAlvj5GipSmgelLwlIA+L/qbrf4NAB+NZ9oqp3bBdj4e5gZmiI
zd6bkANCqk21YiOE31medUfy+nfBQFVvj0oUg1X16C6RaIX5qA3Dwt5qBwKmDkT5
j7JlxUS6Eluiau67ePiDYu2Wbp0qYuAmNUNL+Y2NCO9UJiy0Oq6YVXS971D5lC+a
SX0x9pizmFV3zro+l6/3kHTVbPednfX99yz9SZge64aWXo3MXVN8JD0lR3+92l99
XsFDc+lGeR4azLFIqXC4Cr5Lbk34Hw/VwUC32xxFUaJ2ZmV3pA8bhCtBSxSmxnHS
H3hoBaD1WpuApbW8Psx6qsgoaSUdWsjluA4eQ5afJBf9O2NlT1mim5MAINY4PbWP
o4zq3p1ABVTzuB8tsGA9o6DeYVUUrj7lCv9STdGRhm0472BDkp/gvKMBoPgg3Qez
kvGKK7iVy8R/BOPjh9wP1art5JLVsralXGHA/5Ceid4ojKFzGIC9g3lnAPh+T/eM
duyY9XH4un1r73r6DRqUoczSfHYbxhKxWt0cRNdIadcXXusMPV/w4J4j55WcLrBE
5nopp/prJ5bYegUvRRrwVSFwLDxkE2dh68Zvlh5VWXIPFge0RPEAijYWR5qR2z+/
VHgPYmliOnWFJN1rzekmWjKFtg5A57FkZyk3cp5x0/2xAX+TIbkCDQRgc/o7ARAA
vw53CoVkMzBlisSEETNdEKQMaiQ8BtbC438v/b1mOOeoE0YCfSW7RyflA/TXHOah
db0s3v/Kk2xmbjeMS9IJXlWviKKnOVMrMZvtJdQ4EKfqc5EpxNx7OiEofA/7n7Xs
1YEt6KjYaM/vgANl9HA2UXzqSFiRhkWjj1WA7vhqCWUArpAMGeCDYab2BBfp6Z4f
W9178N2vHH+Hh/uBwGUDnShU38GH8Nstkdcyw5puiJqNQBfZ1Fz9luzutp6zAgHz
WzobeRPZCCXs7CfxcvpkFS0ctOteQtIRIfP+jbDnldMmClQ87UVcKv0pCCJkMLNk
YUCMAb2UC2boCIf0omeeque4+FOphcO4+R/8jc6cYlQpgwUg2/IwBEEnCqtvo3qu
k6uzONhfWZPtUdJd158MGKGTogXVXGzoGzxIrKkZ4W1VuuMiEmhIQZO8e7/4Iz4a
Zp4qQXI8rsmNJN3lB5a7MWgrZ8mjllYRdfiTEvfQ+PiQqnG6PEHZ82om9kp555gs
15UqhjHAqRRtfXzQvZko0ngAxxZNVFPwK8LnxkyEPClRBC5eV3ljI8cvCfnWD01q
rCzSlSafFHCEUEQOhOrf/bBbXPkYTJw2KlumH5w9R6xQWgqneiD/+Qmqdclzdn36
Pgbhyu6uSNZehbx5ptt/EM66JSAW7Q7W6Qnz5PNnHgEAEQEAAYkCNgQYAQoAIBYh
BG+4ATSwO3smfFe7SjchvDBht+M0BQJgc/o7AhsMAAoJEDchvDBht+M0JYUQALlV
dwmk6ZFq5dq0utWgutysL47b30BhYwNMVe0/6UW4h4TYaW6B3f58X7ik7EdYciyR
68eYfwKGhuv/y90QaGXJMU13XHpoInSaHQRhn5M/GkN16DBXdBok70Fh9Gx89Zhs
VKF3qwIVx5AO5CwrVA6F/iOiUEW31xiT7VFkbW1Cfl5H+M6nVXSR1bOdmxTObTz7
CEeJMOVrZs36hVLMWLqZF0igVebO2AsDOY63fy/9MLn8ynCHhnAMvsm9ULWuFzGj
OsJezChduaHqPkopgwihe7jthUn4qWjABbbzKkS6HLBpGAfCzUun+lMpvIEUf+EJ
bpk7gj9xDEP6y96tV/dCeWb4p8N8webR8nVgsRxoEnfIdCkoB80iZGOzKfYYnvdz
ngs8MIL6dC4Nc1/t9ECV4O/w4uwIH65nC1ay0YOK/O/j2SEfnVHQmAuOsgTz+pBn
u6DIA2HsBzFdOCljtf3m4AeAaTbL7MBSDceApqg0lcrhjclqHJo1aJh3M6aVm3gq
yUt7y26Hkh/vYEJwW4gqRho4gb7BvjTZh5LUbrjmRtexFQ1eWM82u23yYS2L+y2Y
ejSKIKmJhXHqsgCVGYw5woZEEMzgpkoIWYG/Eoy+oVuU02QITh/Uc5VRsA9DuwSV
Vw2F8gu/fHiadawxWIhUH+plFVQZc1KwgPcIMW3S
=O0kP
-----END PGP PUBLIC KEY BLOCK-----
```
[^1]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
+292
View File
@@ -0,0 +1,292 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
include(CMakeParseArguments)
include(CMakeDependentOption)
macro(define_channel_options)
set(PREFIX "CHANNEL")
cmake_parse_arguments(
${PREFIX} "" "NAME;TYPE;DESCRIPTION;SPECIFICATIONS;DEFAULT;CLIENT_DEFAULT;SERVER_DEFAULT" "" ${ARGN}
)
string(TOUPPER "CHANNEL_${CHANNEL_NAME}" CHANNEL_OPTION)
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_CLIENT_OPTION)
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" CHANNEL_SERVER_OPTION)
string(TOUPPER "${CHANNEL_TYPE}" CHANNEL_TYPE)
if(CHANNEL_DEFAULT)
set(OPTION_DEFAULT ${CHANNEL_DEFAULT})
elseif(CHANNEL_CLIENT_OPTION OR CHANNEL_SERVER_OPTION)
set(OPTION_DEFAULT "ON")
endif()
set(CHANNEL_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel")
set(CHANNEL_CLIENT_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel client")
set(CHANNEL_SERVER_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel server")
if("${CHANNEL_TYPE}" STREQUAL "DYNAMIC")
cmake_dependent_option(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT} "CHANNEL_DRDYNVC" OFF)
else()
option(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT})
endif()
# If the channel was enabled before the client/server options will stay ensure
# they are deleted if the channel is gone.
if(NOT ${CHANNEL_OPTION})
unset(${CHANNEL_CLIENT_OPTION} CACHE)
unset(${CHANNEL_SERVER_OPTION} CACHE)
endif()
cmake_dependent_option(
${CHANNEL_CLIENT_OPTION} "${CHANNEL_CLIENT_OPTION_DOC}" ${CHANNEL_CLIENT_DEFAULT} "${CHANNEL_OPTION}" OFF
)
cmake_dependent_option(
${CHANNEL_SERVER_OPTION} "${CHANNEL_SERVER_OPTION_DOC}" ${CHANNEL_SERVER_DEFAULT} "${CHANNEL_OPTION}" OFF
)
endmacro(define_channel_options)
macro(define_channel _channel_name)
set(CHANNEL_NAME ${_channel_name})
set(MODULE_NAME ${CHANNEL_NAME})
string(TOUPPER "CHANNEL_${CHANNEL_NAME}" MODULE_PREFIX)
endmacro(define_channel)
macro(define_channel_client _channel_name)
set(CHANNEL_NAME ${_channel_name})
set(MODULE_NAME "${CHANNEL_NAME}-client")
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" MODULE_PREFIX)
endmacro(define_channel_client)
macro(define_channel_server _channel_name)
set(CHANNEL_NAME ${_channel_name})
set(MODULE_NAME "${CHANNEL_NAME}-server")
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" MODULE_PREFIX)
endmacro(define_channel_server)
macro(define_channel_client_subsystem _channel_name _subsystem _type)
set(CHANNEL_NAME ${_channel_name})
set(CHANNEL_SUBSYSTEM ${_subsystem})
string(LENGTH "${_type}" _type_length)
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_PREFIX)
if(_type_length GREATER 0)
set(SUBSYSTEM_TYPE ${_type})
set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}-${SUBSYSTEM_TYPE}")
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}_${SUBSYSTEM_TYPE}" MODULE_PREFIX)
else()
set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}")
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX)
endif()
endmacro(define_channel_client_subsystem)
macro(define_channel_server_subsystem _channel_name _subsystem _type)
set(CHANNEL_NAME ${_channel_name})
set(CHANNEL_SUBSYSTEM ${_subsystem})
set(MODULE_NAME "${CHANNEL_NAME}-server-${CHANNEL_SUBSYSTEM}")
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_server_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX)
endmacro(define_channel_server_subsystem)
macro(add_channel_client _channel_prefix _channel_name)
if(${_channel_prefix}_CLIENT)
add_subdirectory(client)
if(${${_channel_prefix}_CLIENT_STATIC})
set(CHANNEL_STATIC_CLIENT_MODULES ${CHANNEL_STATIC_CLIENT_MODULES} ${_channel_prefix} PARENT_SCOPE)
set(${_channel_prefix}_CLIENT_NAME ${${_channel_prefix}_CLIENT_NAME} PARENT_SCOPE)
set(${_channel_prefix}_CLIENT_CHANNEL ${${_channel_prefix}_CLIENT_CHANNEL} PARENT_SCOPE)
set(${_channel_prefix}_CLIENT_ENTRY ${${_channel_prefix}_CLIENT_ENTRY} PARENT_SCOPE)
set(CHANNEL_STATIC_CLIENT_ENTRIES ${CHANNEL_STATIC_CLIENT_ENTRIES} ${${_channel_prefix}_CLIENT_ENTRY}
PARENT_SCOPE
)
endif()
endif()
endmacro(add_channel_client)
macro(add_channel_server _channel_prefix _channel_name)
if(${_channel_prefix}_SERVER)
add_subdirectory(server)
if(${${_channel_prefix}_SERVER_STATIC})
set(CHANNEL_STATIC_SERVER_MODULES ${CHANNEL_STATIC_SERVER_MODULES} ${_channel_prefix} PARENT_SCOPE)
set(${_channel_prefix}_SERVER_NAME ${${_channel_prefix}_SERVER_NAME} PARENT_SCOPE)
set(${_channel_prefix}_SERVER_CHANNEL ${${_channel_prefix}_SERVER_CHANNEL} PARENT_SCOPE)
set(${_channel_prefix}_SERVER_ENTRY ${${_channel_prefix}_SERVER_ENTRY} PARENT_SCOPE)
set(CHANNEL_STATIC_SERVER_ENTRIES ${CHANNEL_STATIC_SERVER_ENTRIES} ${${_channel_prefix}_SERVER_ENTRY}
PARENT_SCOPE
)
endif()
endif()
endmacro(add_channel_server)
macro(add_channel_client_subsystem _channel_prefix _channel_name _subsystem _type)
add_subdirectory(${_subsystem})
set(_channel_module_name "${_channel_name}-client")
string(LENGTH "${_type}" _type_length)
if(_type_length GREATER 0)
string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}_${_type}" _subsystem_prefix)
else()
string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}" _subsystem_prefix)
endif()
if(${${_subsystem_prefix}_STATIC})
get_target_property(CHANNEL_SUBSYSTEMS ${_channel_module_name} SUBSYSTEMS)
if(_type_length GREATER 0)
set(SUBSYSTEMS ${SUBSYSTEMS} "${_subsystem}-${_type}")
else()
set(SUBSYSTEMS ${SUBSYSTEMS} ${_subsystem})
endif()
set_target_properties(${_channel_module_name} PROPERTIES SUBSYSTEMS "${SUBSYSTEMS}")
endif()
endmacro(add_channel_client_subsystem)
macro(channel_install _targets _destination _export_target)
if(NOT BUILD_SHARED_LIBS)
foreach(_target_name IN ITEMS ${_targets})
target_include_directories(${_target_name} INTERFACE $<INSTALL_INTERFACE:include>)
endforeach()
installwithrpath(TARGETS ${_targets} DESTINATION ${_destination} EXPORT ${_export_target})
endif()
endmacro(channel_install)
macro(server_channel_install _targets _destination)
channel_install(${_targets} ${_destination} "FreeRDP-ServerTargets")
endmacro(server_channel_install)
macro(client_channel_install _targets _destination)
channel_install(${_targets} ${_destination} "FreeRDP-ClientTargets")
endmacro(client_channel_install)
macro(add_channel_client_library _module_prefix _module_name _channel_name _dynamic _entry)
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
if(NOT "${_lnk_dir}" STREQUAL "")
link_directories(${_lnk_dir})
endif()
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE)
set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE)
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client")
if(${_module_prefix}_LIBS)
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
endif()
client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
endmacro(add_channel_client_library)
macro(
add_channel_client_subsystem_library
_module_prefix
_module_name
_channel_name
_type
_dynamic
_entry
)
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
if(NOT "${_lnk_dir}" STREQUAL "")
link_directories(${_lnk_dir})
endif()
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
set(${_module_prefix}_TYPE ${_type} PARENT_SCOPE)
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${_channel_name}/Client/Subsystem/${CHANNEL_SUBSYSTEM}")
if(${_module_prefix}_LIBS)
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
endif()
client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
endmacro(add_channel_client_subsystem_library)
macro(add_channel_server_library _module_prefix _module_name _channel_name _dynamic _entry)
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
if(NOT "${_lnk_dir}" STREQUAL "")
link_directories(${_lnk_dir})
endif()
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE)
set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE)
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server")
if(${_module_prefix}_LIBS)
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
endif()
server_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
endmacro(add_channel_server_library)
set(FILENAME "ChannelOptions.cmake")
file(GLOB FILEPATHS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*/${FILENAME}")
# We need special treatment for drdynvc:
# It needs to be the first entry so that every
# dynamic channel has the dependent options available.
set(DRDYNVC_MATCH "")
foreach(FILEPATH ${FILEPATHS})
if(${FILEPATH} MATCHES "^([^/]*)drdynvc/+${FILENAME}")
set(DRDYNVC_MATCH ${FILEPATH})
endif()
endforeach()
if(NOT "${DRDYNVC_MATCH}" STREQUAL "")
list(REMOVE_ITEM FILEPATHS ${DRDYNVC_MATCH})
list(APPEND FILEPATHS ${DRDYNVC_MATCH})
list(REVERSE FILEPATHS) # list PREPEND is not available on old CMake3
endif()
foreach(FILEPATH ${FILEPATHS})
if(${FILEPATH} MATCHES "^([^/]*)/+${FILENAME}")
string(REGEX REPLACE "^([^/]*)/+${FILENAME}" "\\1" DIR ${FILEPATH})
set(CHANNEL_OPTION)
include(${FILEPATH})
if(${CHANNEL_OPTION})
set(CHANNEL_MESSAGE "Adding ${CHANNEL_TYPE} channel")
if(${CHANNEL_CLIENT_OPTION})
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} client")
endif()
if(${CHANNEL_SERVER_OPTION})
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} server")
endif()
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} \"${CHANNEL_NAME}\"")
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE}: ${CHANNEL_DESCRIPTION}")
message(STATUS "${CHANNEL_MESSAGE}")
add_subdirectory(${DIR})
endif()
endif()
endforeach(FILEPATH)
if(WITH_CHANNELS)
if(WITH_CLIENT_CHANNELS)
add_subdirectory(client)
set(FREERDP_CHANNELS_CLIENT_SRCS ${FREERDP_CHANNELS_CLIENT_SRCS} PARENT_SCOPE)
set(FREERDP_CHANNELS_CLIENT_LIBS ${FREERDP_CHANNELS_CLIENT_LIBS} PARENT_SCOPE)
endif()
if(WITH_SERVER_CHANNELS)
add_subdirectory(server)
set(FREERDP_CHANNELS_SERVER_SRCS ${FREERDP_CHANNELS_SERVER_SRCS} PARENT_SCOPE)
set(FREERDP_CHANNELS_SERVER_LIBS ${FREERDP_CHANNELS_SERVER_LIBS} PARENT_SCOPE)
endif()
endif()
+27
View File
@@ -0,0 +1,27 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2022 Armin Novak <anovak@thincast.com>
# Copyright 2022 Thincast Technologies GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel("ainput")
if(WITH_CLIENT_CHANNELS)
add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME})
endif()
if(WITH_SERVER_CHANNELS)
add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME})
endif()
@@ -0,0 +1,20 @@
set(OPTION_DEFAULT ON)
set(OPTION_CLIENT_DEFAULT ON)
set(OPTION_SERVER_DEFAULT ON)
define_channel_options(
NAME
"ainput"
TYPE
"dynamic"
DESCRIPTION
"Advanced Input Virtual Channel Extension"
SPECIFICATIONS
"[XXXXX]"
DEFAULT
${OPTION_DEFAULT}
CLIENT_DEFAULT
${OPTION_CLIENT_DEFAULT}
SERVER_DEFAULT
${OPTION_SERVER_DEFAULT}
)
@@ -0,0 +1,27 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2022 Armin Novak <anovak@thincast.com>
# Copyright 2022 Thincast Technologies GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel_client("ainput")
set(${MODULE_PREFIX}_SRCS ainput_main.c ainput_main.h)
set(${MODULE_PREFIX}_LIBS winpr)
include_directories(..)
add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry")
+200
View File
@@ -0,0 +1,200 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Advanced Input Virtual Channel Extension
*
* Copyright 2022 Armin Novak <anovak@thincast.com>
* Copyright 2022 Thincast Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <freerdp/config.h>
#include <stdio.h>
#include <stdlib.h>
#include <winpr/crt.h>
#include <winpr/assert.h>
#include <winpr/stream.h>
#include <winpr/sysinfo.h>
#include "ainput_main.h"
#include <freerdp/channels/log.h>
#include <freerdp/client/channels.h>
#include <freerdp/client/ainput.h>
#include <freerdp/channels/ainput.h>
#include "../common/ainput_common.h"
#define TAG CHANNELS_TAG("ainput.client")
typedef struct AINPUT_PLUGIN_ AINPUT_PLUGIN;
struct AINPUT_PLUGIN_
{
GENERIC_DYNVC_PLUGIN base;
AInputClientContext* context;
UINT32 MajorVersion;
UINT32 MinorVersion;
CRITICAL_SECTION lock;
};
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT ainput_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
{
UINT16 type = 0;
AINPUT_PLUGIN* ainput = nullptr;
GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback;
WINPR_ASSERT(callback);
WINPR_ASSERT(data);
ainput = (AINPUT_PLUGIN*)callback->plugin;
WINPR_ASSERT(ainput);
if (!Stream_CheckAndLogRequiredLength(TAG, data, 2))
return ERROR_NO_DATA;
Stream_Read_UINT16(data, type);
switch (type)
{
case MSG_AINPUT_VERSION:
if (!Stream_CheckAndLogRequiredLength(TAG, data, 8))
return ERROR_NO_DATA;
Stream_Read_UINT32(data, ainput->MajorVersion);
Stream_Read_UINT32(data, ainput->MinorVersion);
break;
default:
WLog_WARN(TAG, "Received unsupported message type 0x%04" PRIx16, type);
break;
}
return CHANNEL_RC_OK;
}
static UINT ainput_send_input_event(AInputClientContext* context, UINT64 flags, INT32 x, INT32 y)
{
BYTE buffer[32] = WINPR_C_ARRAY_INIT;
wStream sbuffer = WINPR_C_ARRAY_INIT;
wStream* s = Stream_StaticInit(&sbuffer, buffer, sizeof(buffer));
WINPR_ASSERT(s);
WINPR_ASSERT(context);
const UINT64 time = GetTickCount64();
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)context->handle;
WINPR_ASSERT(ainput);
if (ainput->MajorVersion != AINPUT_VERSION_MAJOR)
{
WLog_WARN(TAG, "Unsupported channel version %" PRIu32 ".%" PRIu32 ", aborting.",
ainput->MajorVersion, ainput->MinorVersion);
return CHANNEL_RC_UNSUPPORTED_VERSION;
}
{
char ebuffer[128] = WINPR_C_ARRAY_INIT;
WLog_VRB(TAG, "sending timestamp=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, time,
ainput_flags_to_string(flags, ebuffer, sizeof(ebuffer)), x, y);
}
/* Message type */
Stream_Write_UINT16(s, MSG_AINPUT_MOUSE);
/* Event data */
Stream_Write_UINT64(s, time);
Stream_Write_UINT64(s, flags);
Stream_Write_INT32(s, x);
Stream_Write_INT32(s, y);
Stream_SealLength(s);
/* ainput back what we have received. AINPUT does not have any message IDs. */
EnterCriticalSection(&ainput->lock);
GENERIC_CHANNEL_CALLBACK* callback = ainput->base.listener_callback->channel_callback;
WINPR_ASSERT(callback);
WINPR_ASSERT(callback->channel);
WINPR_ASSERT(callback->channel->Write);
const UINT rc = callback->channel->Write(callback->channel, (ULONG)Stream_Length(s),
Stream_Buffer(s), nullptr);
LeaveCriticalSection(&ainput->lock);
return rc;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT ainput_on_close(IWTSVirtualChannelCallback* pChannelCallback)
{
GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback;
if (callback)
{
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)callback->plugin;
WINPR_ASSERT(ainput);
/* Lock here to ensure that no ainput_send_input_event is in progress. */
EnterCriticalSection(&ainput->lock);
free(callback);
LeaveCriticalSection(&ainput->lock);
}
return CHANNEL_RC_OK;
}
static UINT init_plugin_cb(GENERIC_DYNVC_PLUGIN* base, WINPR_ATTR_UNUSED rdpContext* rcontext,
WINPR_ATTR_UNUSED rdpSettings* settings)
{
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)base;
AInputClientContext* context = (AInputClientContext*)calloc(1, sizeof(AInputClientContext));
if (!context)
return CHANNEL_RC_NO_MEMORY;
context->handle = (void*)base;
context->AInputSendInputEvent = ainput_send_input_event;
InitializeCriticalSection(&ainput->lock);
EnterCriticalSection(&ainput->lock);
ainput->context = context;
ainput->base.iface.pInterface = context;
LeaveCriticalSection(&ainput->lock);
return CHANNEL_RC_OK;
}
static void terminate_plugin_cb(GENERIC_DYNVC_PLUGIN* base)
{
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)base;
WINPR_ASSERT(ainput);
DeleteCriticalSection(&ainput->lock);
free(ainput->context);
}
static const IWTSVirtualChannelCallback ainput_functions = { ainput_on_data_received,
nullptr, /* Open */
ainput_on_close, nullptr };
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE ainput_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
{
return freerdp_generic_DVCPluginEntry(pEntryPoints, TAG, AINPUT_DVC_CHANNEL_NAME,
sizeof(AINPUT_PLUGIN), sizeof(GENERIC_CHANNEL_CALLBACK),
&ainput_functions, init_plugin_cb, terminate_plugin_cb);
}
@@ -0,0 +1,40 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Advanced Input Virtual Channel Extension
*
* Copyright 2022 Armin Novak <anovak@thincast.com>
* Copyright 2022 Thincast Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H
#define FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H
#include <freerdp/config.h>
#include <freerdp/dvc.h>
#include <freerdp/types.h>
#include <freerdp/addin.h>
#include <freerdp/channels/log.h>
#define DVC_TAG CHANNELS_TAG("ainput.client")
#ifdef WITH_DEBUG_DVC
#define DEBUG_DVC(...) WLog_DBG(DVC_TAG, __VA_ARGS__)
#else
#define DEBUG_DVC(...) \
do \
{ \
} while (0)
#endif
#endif /* FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H */
@@ -0,0 +1,60 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Audio Input Redirection Virtual Channel
*
* Copyright 2022 Armin Novak <anovak@thincast.com>
* Copyright 2022 Thincast Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FREERDP_INT_AINPUT_COMMON_H
#define FREERDP_INT_AINPUT_COMMON_H
#include <winpr/string.h>
#include <freerdp/channels/ainput.h>
WINPR_ATTR_NODISCARD
static inline const char* ainput_flags_to_string(UINT64 flags, char* buffer, size_t size)
{
char number[32] = WINPR_C_ARRAY_INIT;
if (flags & AINPUT_FLAGS_HAVE_REL)
winpr_str_append("AINPUT_FLAGS_HAVE_REL", buffer, size, "|");
if (flags & AINPUT_FLAGS_WHEEL)
winpr_str_append("AINPUT_FLAGS_WHEEL", buffer, size, "|");
if (flags & AINPUT_FLAGS_MOVE)
winpr_str_append("AINPUT_FLAGS_MOVE", buffer, size, "|");
if (flags & AINPUT_FLAGS_DOWN)
winpr_str_append("AINPUT_FLAGS_DOWN", buffer, size, "|");
if (flags & AINPUT_FLAGS_REL)
winpr_str_append("AINPUT_FLAGS_REL", buffer, size, "|");
if (flags & AINPUT_FLAGS_BUTTON1)
winpr_str_append("AINPUT_FLAGS_BUTTON1", buffer, size, "|");
if (flags & AINPUT_FLAGS_BUTTON2)
winpr_str_append("AINPUT_FLAGS_BUTTON2", buffer, size, "|");
if (flags & AINPUT_FLAGS_BUTTON3)
winpr_str_append("AINPUT_FLAGS_BUTTON3", buffer, size, "|");
if (flags & AINPUT_XFLAGS_BUTTON1)
winpr_str_append("AINPUT_XFLAGS_BUTTON1", buffer, size, "|");
if (flags & AINPUT_XFLAGS_BUTTON2)
winpr_str_append("AINPUT_XFLAGS_BUTTON2", buffer, size, "|");
_snprintf(number, sizeof(number), "[0x%08" PRIx64 "]", flags);
winpr_str_append(number, buffer, size, " ");
return buffer;
}
#endif /* FREERDP_INT_AINPUT_COMMON_H */
@@ -0,0 +1,24 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2022 Armin Novak <anovak@thincast.com>
# Copyright 2022 Thincast Technologies GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel_server("ainput")
set(${MODULE_PREFIX}_SRCS ainput_main.c)
set(${MODULE_PREFIX}_LIBS freerdp)
add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry")
+596
View File
@@ -0,0 +1,596 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Advanced Input Virtual Channel Extension
*
* Copyright 2022 Armin Novak <anovak@thincast.com>
* Copyright 2022 Thincast Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <freerdp/config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winpr/crt.h>
#include <winpr/assert.h>
#include <winpr/synch.h>
#include <winpr/thread.h>
#include <winpr/stream.h>
#include <winpr/sysinfo.h>
#include <freerdp/freerdp.h>
#include <freerdp/channels/ainput.h>
#include <freerdp/server/ainput.h>
#include <freerdp/channels/log.h>
#include "../common/ainput_common.h"
#define TAG CHANNELS_TAG("ainput.server")
typedef enum
{
AINPUT_INITIAL,
AINPUT_OPENED,
AINPUT_VERSION_SENT,
} eAInputChannelState;
typedef struct
{
ainput_server_context context;
BOOL opened;
HANDLE stopEvent;
HANDLE thread;
void* ainput_channel;
DWORD SessionId;
BOOL isOpened;
BOOL externalThread;
/* Channel state */
eAInputChannelState state;
wStream* buffer;
} ainput_server;
static UINT ainput_server_context_poll(ainput_server_context* context);
static BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle);
static UINT ainput_server_context_poll_int(ainput_server_context* context);
static BOOL ainput_server_is_open(ainput_server_context* context)
{
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
return ainput->isOpened;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT ainput_server_open_channel(ainput_server* ainput)
{
DWORD Error = 0;
HANDLE hEvent = nullptr;
DWORD StartTick = 0;
DWORD BytesReturned = 0;
PULONG pSessionId = nullptr;
WINPR_ASSERT(ainput);
if (WTSQuerySessionInformationA(ainput->context.vcm, WTS_CURRENT_SESSION, WTSSessionId,
(LPSTR*)&pSessionId, &BytesReturned) == FALSE)
{
WLog_ERR(TAG, "WTSQuerySessionInformationA failed!");
return ERROR_INTERNAL_ERROR;
}
ainput->SessionId = (DWORD)*pSessionId;
WTSFreeMemory(pSessionId);
hEvent = WTSVirtualChannelManagerGetEventHandle(ainput->context.vcm);
StartTick = GetTickCount();
while (ainput->ainput_channel == nullptr)
{
if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED)
{
Error = GetLastError();
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error);
return Error;
}
ainput->ainput_channel = WTSVirtualChannelOpenEx(ainput->SessionId, AINPUT_DVC_CHANNEL_NAME,
WTS_CHANNEL_OPTION_DYNAMIC);
Error = GetLastError();
if (Error == ERROR_NOT_FOUND)
{
WLog_DBG(TAG, "Channel %s not found", AINPUT_DVC_CHANNEL_NAME);
break;
}
if (ainput->ainput_channel)
{
UINT32 channelId = 0;
BOOL status = TRUE;
channelId = WTSChannelGetIdByHandle(ainput->ainput_channel);
IFCALLRET(ainput->context.ChannelIdAssigned, status, &ainput->context, channelId);
if (!status)
{
WLog_ERR(TAG, "context->ChannelIdAssigned failed!");
return ERROR_INTERNAL_ERROR;
}
break;
}
if (GetTickCount() - StartTick > 5000)
{
WLog_WARN(TAG, "Timeout opening channel %s", AINPUT_DVC_CHANNEL_NAME);
break;
}
}
return ainput->ainput_channel ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR;
}
static UINT ainput_server_send_version(ainput_server* ainput)
{
ULONG written = 0;
wStream* s = nullptr;
WINPR_ASSERT(ainput);
s = ainput->buffer;
WINPR_ASSERT(s);
Stream_ResetPosition(s);
if (!Stream_EnsureCapacity(s, 10))
{
WLog_WARN(TAG, "[%s] out of memory", AINPUT_DVC_CHANNEL_NAME);
return ERROR_OUTOFMEMORY;
}
Stream_Write_UINT16(s, MSG_AINPUT_VERSION);
Stream_Write_UINT32(s, AINPUT_VERSION_MAJOR); /* Version (4 bytes) */
Stream_Write_UINT32(s, AINPUT_VERSION_MINOR); /* Version (4 bytes) */
WINPR_ASSERT(Stream_GetPosition(s) <= UINT32_MAX);
if (!WTSVirtualChannelWrite(ainput->ainput_channel, Stream_BufferAs(s, char),
(ULONG)Stream_GetPosition(s), &written))
{
WLog_ERR(TAG, "WTSVirtualChannelWrite failed!");
return ERROR_INTERNAL_ERROR;
}
return CHANNEL_RC_OK;
}
static UINT ainput_server_recv_mouse_event(ainput_server* ainput, wStream* s)
{
UINT error = CHANNEL_RC_OK;
UINT64 flags = 0;
UINT64 time = 0;
INT32 x = 0;
INT32 y = 0;
char buffer[128] = WINPR_C_ARRAY_INIT;
WINPR_ASSERT(ainput);
WINPR_ASSERT(s);
if (!Stream_CheckAndLogRequiredLength(TAG, s, 24))
return ERROR_NO_DATA;
Stream_Read_UINT64(s, time);
Stream_Read_UINT64(s, flags);
Stream_Read_INT32(s, x);
Stream_Read_INT32(s, y);
WLog_VRB(TAG, "received: time=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, time,
ainput_flags_to_string(flags, buffer, sizeof(buffer)), x, y);
IFCALLRET(ainput->context.MouseEvent, error, &ainput->context, time, flags, x, y);
return error;
}
static HANDLE ainput_server_get_channel_handle(ainput_server* ainput)
{
void* buffer = nullptr;
DWORD BytesReturned = 0;
HANDLE ChannelEvent = nullptr;
WINPR_ASSERT(ainput);
if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualEventHandle, &buffer,
&BytesReturned) == TRUE)
{
if (BytesReturned == sizeof(HANDLE))
ChannelEvent = *(HANDLE*)buffer;
WTSFreeMemory(buffer);
}
return ChannelEvent;
}
static DWORD WINAPI ainput_server_thread_func(LPVOID arg)
{
DWORD nCount = 0;
HANDLE events[2] = WINPR_C_ARRAY_INIT;
ainput_server* ainput = (ainput_server*)arg;
UINT error = CHANNEL_RC_OK;
DWORD status = 0;
WINPR_ASSERT(ainput);
nCount = 0;
events[nCount++] = ainput->stopEvent;
while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0))
{
switch (ainput->state)
{
case AINPUT_OPENED:
events[1] = ainput_server_get_channel_handle(ainput);
nCount = 2;
status = WaitForMultipleObjects(nCount, events, FALSE, 100);
switch (status)
{
case WAIT_TIMEOUT:
case WAIT_OBJECT_0 + 1:
case WAIT_OBJECT_0:
error = ainput_server_context_poll_int(&ainput->context);
break;
case WAIT_FAILED:
default:
WLog_WARN(TAG, "[%s] Wait for open failed", AINPUT_DVC_CHANNEL_NAME);
error = ERROR_INTERNAL_ERROR;
break;
}
break;
case AINPUT_VERSION_SENT:
status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE);
switch (status)
{
case WAIT_TIMEOUT:
case WAIT_OBJECT_0 + 1:
case WAIT_OBJECT_0:
error = ainput_server_context_poll_int(&ainput->context);
break;
case WAIT_FAILED:
default:
WLog_WARN(TAG, "[%s] Wait for version failed", AINPUT_DVC_CHANNEL_NAME);
error = ERROR_INTERNAL_ERROR;
break;
}
break;
default:
error = ainput_server_context_poll_int(&ainput->context);
break;
}
}
(void)WTSVirtualChannelClose(ainput->ainput_channel);
ainput->ainput_channel = nullptr;
if (error && ainput->context.rdpcontext)
setChannelError(ainput->context.rdpcontext, error,
"ainput_server_thread_func reported an error");
ExitThread(error);
return error;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT ainput_server_open(ainput_server_context* context)
{
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
if (!ainput->externalThread && (ainput->thread == nullptr))
{
ainput->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!ainput->stopEvent)
{
WLog_ERR(TAG, "CreateEvent failed!");
return ERROR_INTERNAL_ERROR;
}
ainput->thread = CreateThread(nullptr, 0, ainput_server_thread_func, ainput, 0, nullptr);
if (!ainput->thread)
{
WLog_ERR(TAG, "CreateEvent failed!");
(void)CloseHandle(ainput->stopEvent);
ainput->stopEvent = nullptr;
return ERROR_INTERNAL_ERROR;
}
}
ainput->isOpened = TRUE;
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT ainput_server_close(ainput_server_context* context)
{
UINT error = CHANNEL_RC_OK;
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
if (!ainput->externalThread && ainput->thread)
{
(void)SetEvent(ainput->stopEvent);
if (WaitForSingleObject(ainput->thread, INFINITE) == WAIT_FAILED)
{
error = GetLastError();
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
return error;
}
(void)CloseHandle(ainput->thread);
(void)CloseHandle(ainput->stopEvent);
ainput->thread = nullptr;
ainput->stopEvent = nullptr;
}
if (ainput->externalThread)
{
if (ainput->state != AINPUT_INITIAL)
{
(void)WTSVirtualChannelClose(ainput->ainput_channel);
ainput->ainput_channel = nullptr;
ainput->state = AINPUT_INITIAL;
}
}
ainput->isOpened = FALSE;
return error;
}
static UINT ainput_server_initialize(ainput_server_context* context, BOOL externalThread)
{
UINT error = CHANNEL_RC_OK;
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
if (ainput->isOpened)
{
WLog_WARN(TAG, "Application error: AINPUT channel already initialized, calling in this "
"state is not possible!");
return ERROR_INVALID_STATE;
}
ainput->externalThread = externalThread;
return error;
}
ainput_server_context* ainput_server_context_new(HANDLE vcm)
{
ainput_server* ainput = (ainput_server*)calloc(1, sizeof(ainput_server));
if (!ainput)
return nullptr;
ainput->context.vcm = vcm;
ainput->context.Open = ainput_server_open;
ainput->context.IsOpen = ainput_server_is_open;
ainput->context.Close = ainput_server_close;
ainput->context.Initialize = ainput_server_initialize;
ainput->context.Poll = ainput_server_context_poll;
ainput->context.ChannelHandle = ainput_server_context_handle;
ainput->buffer = Stream_New(nullptr, 4096);
if (!ainput->buffer)
goto fail;
return &ainput->context;
fail:
WINPR_PRAGMA_DIAG_PUSH
WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC
ainput_server_context_free(&ainput->context);
WINPR_PRAGMA_DIAG_POP
return nullptr;
}
void ainput_server_context_free(ainput_server_context* context)
{
ainput_server* ainput = (ainput_server*)context;
if (ainput)
{
ainput_server_close(context);
Stream_Free(ainput->buffer, TRUE);
}
free(ainput);
}
static UINT ainput_process_message(ainput_server* ainput)
{
UINT error = ERROR_INTERNAL_ERROR;
ULONG BytesReturned = 0;
ULONG ActualBytesReturned = 0;
WINPR_ASSERT(ainput);
WINPR_ASSERT(ainput->ainput_channel);
wStream* s = ainput->buffer;
WINPR_ASSERT(s);
Stream_ResetPosition(s);
const BOOL rc = WTSVirtualChannelRead(ainput->ainput_channel, 0, nullptr, 0, &BytesReturned);
if (!rc)
goto out;
if (BytesReturned < 2)
{
error = CHANNEL_RC_OK;
goto out;
}
if (!Stream_EnsureRemainingCapacity(s, BytesReturned))
{
WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!");
error = CHANNEL_RC_NO_MEMORY;
goto out;
}
if (WTSVirtualChannelRead(ainput->ainput_channel, 0, Stream_BufferAs(s, char),
(ULONG)Stream_Capacity(s), &ActualBytesReturned) == FALSE)
{
WLog_ERR(TAG, "WTSVirtualChannelRead failed!");
goto out;
}
if (BytesReturned != ActualBytesReturned)
{
WLog_ERR(TAG, "WTSVirtualChannelRead size mismatch %" PRIu32 ", expected %" PRIu32,
ActualBytesReturned, BytesReturned);
goto out;
}
Stream_SetLength(s, ActualBytesReturned);
{
const UINT16 MessageId = Stream_Get_UINT16(s);
switch (MessageId)
{
case MSG_AINPUT_MOUSE:
error = ainput_server_recv_mouse_event(ainput, s);
break;
default:
WLog_ERR(TAG, "audin_server_thread_func: unknown MessageId %" PRIu16 "", MessageId);
break;
}
}
out:
if (error)
WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error);
return error;
}
BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle)
{
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
WINPR_ASSERT(handle);
if (!ainput->externalThread)
{
WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME);
return FALSE;
}
if (ainput->state == AINPUT_INITIAL)
{
WLog_WARN(TAG, "[%s] state fail!", AINPUT_DVC_CHANNEL_NAME);
return FALSE;
}
*handle = ainput_server_get_channel_handle(ainput);
return TRUE;
}
UINT ainput_server_context_poll_int(ainput_server_context* context)
{
ainput_server* ainput = (ainput_server*)context;
UINT error = ERROR_INTERNAL_ERROR;
WINPR_ASSERT(ainput);
switch (ainput->state)
{
case AINPUT_INITIAL:
error = ainput_server_open_channel(ainput);
if (error)
WLog_ERR(TAG, "ainput_server_open_channel failed with error %" PRIu32 "!", error);
else
ainput->state = AINPUT_OPENED;
break;
case AINPUT_OPENED:
{
union
{
BYTE* pb;
void* pv;
} buffer;
DWORD BytesReturned = 0;
buffer.pv = nullptr;
if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualChannelReady, &buffer.pv,
&BytesReturned) != TRUE)
{
WLog_ERR(TAG, "WTSVirtualChannelReady failed,");
}
else
{
if (*buffer.pb != 0)
{
error = ainput_server_send_version(ainput);
if (error)
WLog_ERR(TAG, "audin_server_send_version failed with error %" PRIu32 "!",
error);
else
ainput->state = AINPUT_VERSION_SENT;
}
else
error = CHANNEL_RC_OK;
}
WTSFreeMemory(buffer.pv);
}
break;
case AINPUT_VERSION_SENT:
error = ainput_process_message(ainput);
break;
default:
WLog_ERR(TAG, "AINPUT channel is in invalid state %u", ainput->state);
break;
}
return error;
}
UINT ainput_server_context_poll(ainput_server_context* context)
{
ainput_server* ainput = (ainput_server*)context;
WINPR_ASSERT(ainput);
if (!ainput->externalThread)
{
WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME);
return ERROR_INTERNAL_ERROR;
}
return ainput_server_context_poll_int(context);
}
+26
View File
@@ -0,0 +1,26 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel("audin")
if(WITH_CLIENT_CHANNELS)
add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME})
endif()
if(WITH_SERVER_CHANNELS)
add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME})
endif()
+24
View File
@@ -0,0 +1,24 @@
set(OPTION_DEFAULT ON)
set(OPTION_CLIENT_DEFAULT ON)
set(OPTION_SERVER_DEFAULT ON)
if(ANDROID)
set(OPTION_SERVER_DEFAULT OFF)
endif()
define_channel_options(
NAME
"audin"
TYPE
"dynamic"
DESCRIPTION
"Audio Input Redirection Virtual Channel Extension"
SPECIFICATIONS
"[MS-RDPEAI]"
DEFAULT
${OPTION_DEFAULT}
CLIENT_DEFAULT
${OPTION_CLIENT_DEFAULT}
SERVER_DEFAULT
${OPTION_SERVER_DEFAULT}
)
@@ -0,0 +1,58 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel_client("audin")
set(${MODULE_PREFIX}_SRCS audin_main.c audin_main.h)
set(${MODULE_PREFIX}_LIBS freerdp winpr)
include_directories(..)
add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry")
if(WITH_OSS)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "")
endif()
if(WITH_ALSA)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "")
endif()
if(WITH_PULSE)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "")
endif()
if(WITH_OPENSLES)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "")
endif()
if(WITH_WINMM)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "")
endif()
if(WITH_MACAUDIO)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "")
endif()
if(WITH_SNDIO)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "sndio" "")
endif()
if(WITH_IOSAUDIO)
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ios" "")
endif()
@@ -0,0 +1,31 @@
# FreeRDP: A Remote Desktop Protocol Implementation
# FreeRDP cmake build script
#
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
define_channel_client_subsystem("audin" "alsa" "")
find_package(ALSA REQUIRED)
set(${MODULE_PREFIX}_SRCS audin_alsa.c)
set(${MODULE_PREFIX}_LIBS winpr freerdp ${ALSA_LIBRARIES})
freerdp_client_pc_add_requires_private("alsa")
include_directories(..)
include_directories(SYSTEM ${ALSA_INCLUDE_DIRS})
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
@@ -0,0 +1,466 @@
/**
* FreeRDP: A Remote Desktop Protocol Implementation
* Audio Input Redirection Virtual Channel - ALSA implementation
*
* Copyright 2010-2011 Vic Lee
* Copyright 2015 Thincast Technologies GmbH
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <freerdp/config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winpr/crt.h>
#include <winpr/synch.h>
#include <winpr/thread.h>
#include <winpr/cmdline.h>
#include <winpr/wlog.h>
#include <alsa/asoundlib.h>
#include <freerdp/freerdp.h>
#include <freerdp/addin.h>
#include <freerdp/channels/rdpsnd.h>
#include "audin_main.h"
typedef struct
{
IAudinDevice iface;
char* device_name;
UINT32 frames_per_packet;
AUDIO_FORMAT aformat;
HANDLE thread;
HANDLE stopEvent;
AudinReceive receive;
void* user_data;
rdpContext* rdpcontext;
wLog* log;
size_t bytes_per_frame;
} AudinALSADevice;
static snd_pcm_format_t audin_alsa_format(UINT32 wFormatTag, UINT32 bitPerChannel)
{
switch (wFormatTag)
{
case WAVE_FORMAT_PCM:
switch (bitPerChannel)
{
case 16:
return SND_PCM_FORMAT_S16_LE;
case 8:
return SND_PCM_FORMAT_S8;
default:
return SND_PCM_FORMAT_UNKNOWN;
}
default:
return SND_PCM_FORMAT_UNKNOWN;
}
}
static BOOL audin_alsa_set_params(AudinALSADevice* alsa, snd_pcm_t* capture_handle)
{
int error = 0;
SSIZE_T s = 0;
UINT32 channels = alsa->aformat.nChannels;
snd_pcm_hw_params_t* hw_params = nullptr;
snd_pcm_format_t format =
audin_alsa_format(alsa->aformat.wFormatTag, alsa->aformat.wBitsPerSample);
if ((error = snd_pcm_hw_params_malloc(&hw_params)) < 0)
{
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_hw_params_malloc (%s)", snd_strerror(error));
return FALSE;
}
snd_pcm_hw_params_any(capture_handle, hw_params);
snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(capture_handle, hw_params, format);
snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &alsa->aformat.nSamplesPerSec,
nullptr);
snd_pcm_hw_params_set_channels_near(capture_handle, hw_params, &channels);
snd_pcm_hw_params(capture_handle, hw_params);
snd_pcm_hw_params_free(hw_params);
snd_pcm_prepare(capture_handle);
if (channels > UINT16_MAX)
return FALSE;
s = snd_pcm_format_size(format, 1);
if ((s < 0) || (s > UINT16_MAX))
return FALSE;
alsa->aformat.nChannels = (UINT16)channels;
alsa->bytes_per_frame = (size_t)s * channels;
return TRUE;
}
static DWORD WINAPI audin_alsa_thread_func(LPVOID arg)
{
DWORD error = CHANNEL_RC_OK;
BYTE* buffer = nullptr;
AudinALSADevice* alsa = (AudinALSADevice*)arg;
WINPR_ASSERT(alsa);
WLog_Print(alsa->log, WLOG_DEBUG, "in");
snd_pcm_t* capture_handle = nullptr;
const int rc = snd_pcm_open(&capture_handle, alsa->device_name, SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0)
{
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_open (%s)", snd_strerror(rc));
error = CHANNEL_RC_INITIALIZATION_ERROR;
goto out;
}
if (!audin_alsa_set_params(alsa, capture_handle))
{
WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_set_params failed");
goto out;
}
buffer =
(BYTE*)calloc(alsa->frames_per_packet + alsa->aformat.nBlockAlign, alsa->bytes_per_frame);
if (!buffer)
{
WLog_Print(alsa->log, WLOG_ERROR, "calloc failed!");
error = CHANNEL_RC_NO_MEMORY;
goto out;
}
while (1)
{
size_t frames = alsa->frames_per_packet;
const DWORD status = WaitForSingleObject(alsa->stopEvent, 0);
if (status == WAIT_FAILED)
{
error = GetLastError();
WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "!",
error);
break;
}
if (status == WAIT_OBJECT_0)
{
WLog_Print(alsa->log, WLOG_DEBUG, "alsa->stopEvent requests termination");
break;
}
snd_pcm_sframes_t framesRead = snd_pcm_readi(capture_handle, buffer, frames);
if (framesRead == 0)
continue;
if (framesRead == -EPIPE)
{
const int res = snd_pcm_recover(capture_handle, (int)framesRead, 0);
if (res < 0)
WLog_Print(alsa->log, WLOG_WARN, "snd_pcm_recover (%s)", snd_strerror(res));
continue;
}
else if (framesRead < 0)
{
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_readi (%s)", snd_strerror((int)framesRead));
error = ERROR_INTERNAL_ERROR;
break;
}
error = alsa->receive(&alsa->aformat, buffer,
WINPR_ASSERTING_INT_CAST(size_t, framesRead) * alsa->bytes_per_frame,
alsa->user_data);
if (error)
{
WLog_Print(alsa->log, WLOG_ERROR,
"audin_alsa_thread_receive failed with error %" PRIu32, error);
break;
}
}
free(buffer);
if (capture_handle)
{
const int res = snd_pcm_close(capture_handle);
if (res < 0)
WLog_Print(alsa->log, WLOG_WARN, "snd_pcm_close (%s)", snd_strerror(res));
}
out:
WLog_Print(alsa->log, WLOG_DEBUG, "out");
if (error && alsa->rdpcontext)
setChannelError(alsa->rdpcontext, error, "audin_alsa_thread_func reported an error");
ExitThread(error);
return error;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT audin_alsa_free(IAudinDevice* device)
{
AudinALSADevice* alsa = (AudinALSADevice*)device;
if (alsa)
free(alsa->device_name);
free(alsa);
return CHANNEL_RC_OK;
}
static BOOL audin_alsa_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
{
if (!device || !format)
return FALSE;
switch (format->wFormatTag)
{
case WAVE_FORMAT_PCM:
if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) &&
(format->wBitsPerSample == 8 || format->wBitsPerSample == 16) &&
(format->nChannels == 1 || format->nChannels == 2))
{
return TRUE;
}
break;
default:
return FALSE;
}
return FALSE;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT audin_alsa_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
UINT32 FramesPerPacket)
{
AudinALSADevice* alsa = (AudinALSADevice*)device;
if (!alsa || !format)
return ERROR_INVALID_PARAMETER;
alsa->aformat = *format;
alsa->frames_per_packet = FramesPerPacket;
if (audin_alsa_format(format->wFormatTag, format->wBitsPerSample) == SND_PCM_FORMAT_UNKNOWN)
return ERROR_INTERNAL_ERROR;
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT audin_alsa_open(IAudinDevice* device, AudinReceive receive, void* user_data)
{
AudinALSADevice* alsa = (AudinALSADevice*)device;
if (!device || !receive || !user_data)
return ERROR_INVALID_PARAMETER;
alsa->receive = receive;
alsa->user_data = user_data;
if (!(alsa->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
{
WLog_Print(alsa->log, WLOG_ERROR, "CreateEvent failed!");
goto error_out;
}
if (!(alsa->thread = CreateThread(nullptr, 0, audin_alsa_thread_func, alsa, 0, nullptr)))
{
WLog_Print(alsa->log, WLOG_ERROR, "CreateThread failed!");
goto error_out;
}
return CHANNEL_RC_OK;
error_out:
(void)CloseHandle(alsa->stopEvent);
alsa->stopEvent = nullptr;
return ERROR_INTERNAL_ERROR;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT audin_alsa_close(IAudinDevice* device)
{
UINT error = CHANNEL_RC_OK;
AudinALSADevice* alsa = (AudinALSADevice*)device;
if (!alsa)
return ERROR_INVALID_PARAMETER;
if (alsa->stopEvent)
{
(void)SetEvent(alsa->stopEvent);
if (WaitForSingleObject(alsa->thread, INFINITE) == WAIT_FAILED)
{
error = GetLastError();
WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "",
error);
return error;
}
(void)CloseHandle(alsa->stopEvent);
alsa->stopEvent = nullptr;
(void)CloseHandle(alsa->thread);
alsa->thread = nullptr;
}
alsa->receive = nullptr;
alsa->user_data = nullptr;
return error;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
static UINT audin_alsa_parse_addin_args(AudinALSADevice* device, const ADDIN_ARGV* args)
{
int status = 0;
DWORD flags = 0;
const COMMAND_LINE_ARGUMENT_A* arg = nullptr;
AudinALSADevice* alsa = device;
COMMAND_LINE_ARGUMENT_A audin_alsa_args[] = {
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
"audio device name" },
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
};
flags =
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_alsa_args, flags, alsa,
nullptr, nullptr);
if (status < 0)
return ERROR_INVALID_PARAMETER;
arg = audin_alsa_args;
do
{
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
continue;
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
{
alsa->device_name = _strdup(arg->Value);
if (!alsa->device_name)
{
WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!");
return CHANNEL_RC_NO_MEMORY;
}
}
CommandLineSwitchEnd(arg)
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
return CHANNEL_RC_OK;
}
/**
* Function description
*
* @return 0 on success, otherwise a Win32 error code
*/
FREERDP_ENTRY_POINT(UINT VCAPITYPE alsa_freerdp_audin_client_subsystem_entry(
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
{
const ADDIN_ARGV* args = nullptr;
AudinALSADevice* alsa = nullptr;
UINT error = 0;
alsa = (AudinALSADevice*)calloc(1, sizeof(AudinALSADevice));
if (!alsa)
{
WLog_ERR(TAG, "calloc failed!");
return CHANNEL_RC_NO_MEMORY;
}
alsa->log = WLog_Get(TAG);
alsa->iface.Open = audin_alsa_open;
alsa->iface.FormatSupported = audin_alsa_format_supported;
alsa->iface.SetFormat = audin_alsa_set_format;
alsa->iface.Close = audin_alsa_close;
alsa->iface.Free = audin_alsa_free;
alsa->rdpcontext = pEntryPoints->rdpcontext;
args = pEntryPoints->args;
if ((error = audin_alsa_parse_addin_args(alsa, args)))
{
WLog_Print(alsa->log, WLOG_ERROR,
"audin_alsa_parse_addin_args failed with errorcode %" PRIu32 "!", error);
goto error_out;
}
if (!alsa->device_name)
{
alsa->device_name = _strdup("default");
if (!alsa->device_name)
{
WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!");
error = CHANNEL_RC_NO_MEMORY;
goto error_out;
}
}
alsa->frames_per_packet = 128;
alsa->aformat.nChannels = 2;
alsa->aformat.wBitsPerSample = 16;
alsa->aformat.wFormatTag = WAVE_FORMAT_PCM;
alsa->aformat.nSamplesPerSec = 44100;
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)alsa)))
{
WLog_Print(alsa->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!",
error);
goto error_out;
}
return CHANNEL_RC_OK;
error_out:
free(alsa->device_name);
free(alsa);
return error;
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More