Skip to content

Commit a95775a

Browse files
committed
chore: merge dev into main (Linux build, AppImage, self-update)
2 parents 4302740 + 871ea27 commit a95775a

40 files changed

Lines changed: 2002 additions & 465 deletions

.gitattributes

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Allow Git to detect text files normally so diffs are generated and tools that
2+
# rely on textual patches (including PR automation) function correctly.
3+
* text=auto
4+
5+
# AppImage launcher runs on Linux; a CRLF checkout would break its shebang.
6+
installer/appimage/AppRun text eol=lf
7+
installer/appimage/*.desktop text eol=lf

.github/workflows/build.yml

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,160 @@ jobs:
191191
path: |
192192
dist/PortkeyDrop_macOS_*.zip
193193
194+
build-linux:
195+
name: Linux
196+
needs: prepare
197+
if: needs.prepare.outputs.should_build == 'true'
198+
runs-on: ubuntu-latest
199+
timeout-minutes: 75
200+
steps:
201+
- uses: actions/checkout@v6
202+
with:
203+
ref: ${{ needs.prepare.outputs.is_nightly == 'true' && 'dev' || github.ref }}
204+
fetch-depth: 0
205+
206+
- uses: actions/setup-python@v6
207+
with:
208+
python-version: ${{ env.PYTHON_VERSION }}
209+
cache: pip
210+
211+
- name: Restore Nuitka cache
212+
uses: actions/cache/restore@v5
213+
with:
214+
path: .nuitka-cache
215+
key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }}
216+
restore-keys: |
217+
nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-
218+
nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-
219+
220+
- name: Install system dependencies
221+
run: |
222+
sudo apt-get update
223+
sudo apt-get install -y --no-install-recommends \
224+
ccache \
225+
libgtk-3-dev \
226+
libnotify-dev \
227+
libsdl2-dev \
228+
libwebkit2gtk-4.1-dev \
229+
freeglut3-dev \
230+
xvfb
231+
232+
- name: Install dependencies
233+
run: |
234+
WX_PYTHON_VERSION=4.2.5
235+
PY_TAG=$(python -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")')
236+
pip install \
237+
"https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-24.04/wxpython-${WX_PYTHON_VERSION}-${PY_TAG}-${PY_TAG}-linux_x86_64.whl"
238+
pip install -e .[dev,build]
239+
240+
- name: Build
241+
timeout-minutes: 55
242+
run: |
243+
python scripts/generate_build_meta.py ${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }}
244+
python installer/build_nuitka.py --tag "${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }}"
245+
246+
- name: Smoke test packaged app
247+
env:
248+
XDG_DATA_HOME: ${{ runner.temp }}/portkeydrop-data
249+
XDG_CONFIG_HOME: ${{ runner.temp }}/portkeydrop-config
250+
run: |
251+
set +e
252+
timeout 25s xvfb-run -a dist/PortkeyDrop/PortkeyDrop > linux-smoke.log 2>&1
253+
status=$?
254+
set -e
255+
cat linux-smoke.log
256+
if grep -E "cannot register existing type 'G(Task|AsyncResult|InputStream|Seekable|PollableInputStream)'|GLib-GObject-CRITICAL" linux-smoke.log; then
257+
echo "Packaged Linux app hit the GLib/GIO duplicate type-registration crash."
258+
exit 1
259+
fi
260+
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
261+
echo "Packaged Linux app exited unexpectedly with status $status."
262+
exit "$status"
263+
fi
264+
265+
- name: Package AppImage
266+
timeout-minutes: 15
267+
run: python installer/build_appimage.py
268+
269+
- name: Smoke test AppImage on Fedora
270+
timeout-minutes: 15
271+
run: |
272+
cat > fedora-smoke.sh <<'EOF'
273+
set -e
274+
dnf install -y -q gtk3 libnotify alsa-lib xorg-x11-server-Xvfb dejavu-sans-fonts >/dev/null
275+
cd /work
276+
appimage=$(find dist -maxdepth 1 -name 'PortkeyDrop_Linux_*.AppImage' -print -quit)
277+
if [ -z "$appimage" ]; then
278+
echo "AppImage was not created."
279+
exit 1
280+
fi
281+
chmod +x "$appimage"
282+
export XDG_DATA_HOME=/tmp/pkd-data XDG_CONFIG_HOME=/tmp/pkd-config
283+
set +e
284+
timeout 25s xvfb-run -a "$appimage" --appimage-extract-and-run > fedora-smoke.log 2>&1
285+
status=$?
286+
set -e
287+
cat fedora-smoke.log
288+
if grep -E "error while loading shared libraries|cannot open shared object file" fedora-smoke.log; then
289+
echo "AppImage is missing a shared library on Fedora."
290+
exit 1
291+
fi
292+
if grep -E "cannot register existing type 'G(Task|AsyncResult|InputStream|Seekable|PollableInputStream)'|GLib-GObject-CRITICAL" fedora-smoke.log; then
293+
echo "AppImage hit the GLib/GIO duplicate type-registration crash on Fedora."
294+
exit 1
295+
fi
296+
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
297+
echo "AppImage exited unexpectedly with status $status on Fedora."
298+
exit "$status"
299+
fi
300+
echo "Fedora smoke test passed."
301+
EOF
302+
docker run --rm -v "$PWD:/work" fedora:43 bash /work/fedora-smoke.sh
303+
304+
- name: Verify Linux tarball avoids bundled OpenSSL
305+
run: |
306+
tarball=$(find dist -maxdepth 1 -name 'PortkeyDrop_Linux_*.tar.gz' -print -quit)
307+
if [ -z "$tarball" ]; then
308+
echo "Linux tarball was not created."
309+
exit 1
310+
fi
311+
if tar -tzf "$tarball" | grep -E '(^|/)lib(ssl|crypto)\.so'; then
312+
echo "Linux tarball must not bundle OpenSSL libraries; use the target system OpenSSL instead."
313+
exit 1
314+
fi
315+
316+
- name: Verify Linux tarball includes sound_lib x64 libraries
317+
run: |
318+
tarball=$(find dist -maxdepth 1 -name 'PortkeyDrop_Linux_*.tar.gz' -print -quit)
319+
if [ -z "$tarball" ]; then
320+
echo "Linux tarball was not created."
321+
exit 1
322+
fi
323+
for lib in libbass.so libbassmix.so libbassopus.so libtags.so; do
324+
if ! tar -tzf "$tarball" | grep -qx "PortkeyDrop/sound_lib/lib/x64/$lib"; then
325+
echo "Linux tarball is missing sound_lib/lib/x64/$lib."
326+
exit 1
327+
fi
328+
done
329+
330+
- name: Save Nuitka cache
331+
if: always()
332+
uses: actions/cache/save@v5
333+
with:
334+
path: .nuitka-cache
335+
key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }}
336+
337+
- uses: actions/upload-artifact@v7
338+
with:
339+
name: linux
340+
path: |
341+
dist/PortkeyDrop_Linux_*.tar.gz
342+
dist/PortkeyDrop_Linux_*.AppImage
343+
194344
release:
195345
name: Release
196-
needs: [prepare, build-windows, build-macos]
197-
if: always() && needs.prepare.outputs.should_build == 'true' && (needs.build-windows.result == 'success' || needs.build-macos.result == 'success')
346+
needs: [prepare, build-windows, build-macos, build-linux]
347+
if: always() && needs.prepare.outputs.should_build == 'true' && (needs.build-windows.result == 'success' || needs.build-macos.result == 'success' || needs.build-linux.result == 'success')
198348
runs-on: ubuntu-latest
199349
steps:
200350
- uses: actions/checkout@v6
@@ -220,20 +370,24 @@ jobs:
220370
TIMESTAMP=$(date -u +%Y%m%d)
221371
222372
# Rename assets based on release type
223-
for f in dist/*.exe dist/*.zip; do
373+
for f in dist/*.exe dist/*.zip dist/*.tar.gz dist/*.AppImage; do
224374
[ -f "$f" ] || continue
225375
name=$(basename "$f")
226376
if [ "$IS_NIGHTLY" = "true" ]; then
227377
case "$name" in
228378
*Setup*.exe) mv "$f" "assets/PortkeyDrop-nightly-${TIMESTAMP}-windows-setup.exe" ;;
229379
*Portable*.zip) mv "$f" "assets/PortkeyDrop-nightly-${TIMESTAMP}-windows-portable.zip" ;;
230380
*macOS*.zip) mv "$f" "assets/PortkeyDrop-nightly-${TIMESTAMP}-macOS.zip" ;;
381+
*Linux*.tar.gz) mv "$f" "assets/PortkeyDrop-nightly-${TIMESTAMP}-linux.tar.gz" ;;
382+
*Linux*.AppImage) mv "$f" "assets/PortkeyDrop-nightly-${TIMESTAMP}-linux-x86_64.AppImage" ;;
231383
esac
232384
else
233385
case "$name" in
234386
*Setup*.exe) mv "$f" "assets/PortkeyDrop-${VERSION}-windows-setup.exe" ;;
235387
*Portable*.zip) mv "$f" "assets/PortkeyDrop-${VERSION}-windows-portable.zip" ;;
236388
*macOS*.zip) mv "$f" "assets/PortkeyDrop-${VERSION}-macOS.zip" ;;
389+
*Linux*.tar.gz) mv "$f" "assets/PortkeyDrop-${VERSION}-linux.tar.gz" ;;
390+
*Linux*.AppImage) mv "$f" "assets/PortkeyDrop-${VERSION}-linux-x86_64.AppImage" ;;
237391
esac
238392
fi
239393
done

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
### Added
8+
- Linux builds: releases and nightlies now include a Linux tarball and an AppImage. The AppImage bundles the libraries missing on non-Ubuntu systems, so it runs on Fedora, Arch, and openSUSE too, while screen reader support, themes, and TLS keep using your distro's own libraries.
9+
- Linux AppImage installs can update themselves: the updater downloads the new AppImage, verifies its checksum, swaps it in place, and relaunches. Tarball runs get told where the verified download was saved instead of hitting a dead end.
10+
11+
### Fixed
12+
- Pressing Delete or F2 while editing a text field no longer triggers file delete or rename — those keys now act only inside the file panes, and file operations tell you to focus a pane first instead of guessing (and sometimes targeting a remote file you never touched).
13+
- Background transfer completions no longer jump your position in the file lists back to the top; deletes and renames keep you on the nearest neighbouring file, and sorting or toggling hidden files keeps your selection.
14+
- The "Announce file count" setting now actually speaks the count when entering a directory; empty folders and empty filter results are announced instead of silent.
15+
- The Speech settings tab (rate, volume, verbosity) now controls the built-in speech output instead of doing nothing; verbosity also controls transfer progress announcements.
16+
- The "Verify host keys: ask" setting now really asks: connecting to an unknown SSH host shows an accessible dialog to reject or accept the key once or permanently, instead of silently trusting it.
17+
- The Cancel button on the update download progress dialog now actually cancels the download.
18+
- Upload, download, and transfer with nothing selected or no connection announce why nothing happened instead of doing nothing silently.
19+
- Restoring the window from the notification area and closing the transfer queue now place keyboard focus in the file list rather than on the window frame.
20+
- Removing a site in the Site Manager now asks for confirmation, announces the removal, and clears the form after the last site is deleted.
21+
- Repeated connect presses while a connection attempt is in progress no longer start duplicate attempts (and duplicate error dialogs). Starting a connection is now announced for screen readers.
22+
23+
### Changed
24+
- The transfer queue window no longer opens (and steals focus) automatically every time a transfer is queued; announcements cover the feedback and Ctrl+Shift+T opens it on demand. Queue buttons disable without a selection, and the selection follows the same transfer when rows are removed.
25+
- Added Help > Keyboard Shortcuts listing every binding, including the previously undocumented F6, Ctrl+L, and Ctrl+1/2/3 pane shortcuts.
26+
- Duplicate menu and dialog access keys (Alt+letter) were reassigned so each letter activates its control directly across the menus, Site Manager, Settings, transfer queue, sound pack, and import dialogs.
27+
- The Settings Audio tab now scrolls so all mute checkboxes stay visible when focused; the file lists have proper accessible names for VoiceOver; keyboard-opened context menus appear at the focused file instead of the mouse pointer; switching protocol keeps a hand-typed port; the activity log no longer moves your reading position when new entries arrive; and a startup notice appears when no speech backend is available.
28+
29+
### Changed
30+
- Pressing Escape in the quick connect bar while connected now hides the bar and returns focus to where you were.
31+
- Choosing Connect with an empty host, username, or required password now moves focus to the field that needs input and announces it, instead of showing a dead-end error dialog.
32+
- Quick Connect (Ctrl+N) now focuses the quick connect bar (revealing it while connected) instead of opening a separate dialog with the same fields.
33+
- Pressing Enter in any quick connect field now starts the connection, matching the old dialog behavior.
34+
- Removed the redundant File > Connect menu item; connecting lives in the quick connect bar and the Sites menu. Ctrl+Enter still connects.
35+
- Moved Disconnect from the File menu to the Sites menu, so connecting, disconnecting, and site management share one menu.
36+
- An invalid port in the quick connect bar now focuses the port field with an announcement instead of crashing the connect action.
37+
- Development builds now report the next 0.5.2 development version after the 0.5.1 hotfix release.
38+
739
## [0.5.1] - 2026-05-31
840

941
### Fixed

PRD.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Planned:
108108
- `Ctrl+Shift+N`: New directory
109109
- `Backspace`: Parent directory
110110
- `Enter`: Open directory / download file
111-
- `Ctrl+Q`: Disconnect
111+
- `Ctrl+Enter`: Connect using the quick connect bar
112112
- `Ctrl+I`: File properties
113113
- `Ctrl+T`: Transfer queue
114114
- `/` or `Ctrl+F`: Filter/search files

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Portkey Drop includes a built-in default sound pack with short cues for transfer
3131
| Ctrl+I | File properties |
3232
| Ctrl+Shift+N | New directory |
3333
| Ctrl+Shift+T | Transfer queue |
34-
| Ctrl+Q | Disconnect |
34+
| Ctrl+Enter | Connect using the quick connect bar |
3535
| Enter | Open directory / download file |
3636
| Backspace | Parent directory |
3737
| Delete | Delete selected |
@@ -57,7 +57,9 @@ Saved connection passwords are stored in your system's secure keyring (Windows C
5757

5858
## Install
5959

60-
Portkey Drop currently supports Python 3.11 and 3.12.
60+
Packaged builds for Windows (installer and portable ZIP), macOS, and Linux (tarball and AppImage) are published on the [releases page](https://github.com/Orinks/PortkeyDrop/releases). On Linux, download the AppImage, mark it executable (`chmod +x PortkeyDrop-*.AppImage`), and run it.
61+
62+
To run from source instead, Portkey Drop currently supports Python 3.11 and 3.12.
6163

6264
**Windows:**
6365
```powershell

installer/appimage/AppRun

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
# AppImage entry point. Bundled libraries live in usr/lib; the Nuitka dist
3+
# keeps its own layout under opt/portkeydrop with rpaths patched by
4+
# linuxdeploy to reach usr/lib.
5+
HERE="$(dirname "$(readlink -f "$0")")"
6+
export LD_LIBRARY_PATH="$HERE/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
7+
exec "$HERE/opt/portkeydrop/PortkeyDrop" "$@"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[Desktop Entry]
2+
Type=Application
3+
Name=PortkeyDrop
4+
Comment=Keyboard-first file transfer client for SFTP, FTP, FTPS, and WebDAV
5+
Exec=PortkeyDrop
6+
Icon=portkeydrop
7+
Categories=Network;FileTransfer;
8+
Terminal=false

installer/appimage/portkeydrop.png

1.4 KB
Loading

installer/build.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import shutil
2525
import subprocess
2626
import sys
27+
import tarfile
2728
from pathlib import Path
2829

2930
if hasattr(sys.stdout, "reconfigure"):
@@ -433,10 +434,6 @@ def create_portable_zip() -> bool:
433434

434435
zip_path = DIST_DIR / zip_name
435436

436-
# Remove existing zip
437-
if Path(f"{zip_path}.zip").exists():
438-
Path(f"{zip_path}.zip").unlink()
439-
440437
if IS_WINDOWS:
441438
(source_dir / ".portable").write_text("1\n", encoding="utf-8")
442439
(source_dir / "data").mkdir(exist_ok=True)
@@ -447,9 +444,22 @@ def create_portable_zip() -> bool:
447444
print(f"Error: {exc}")
448445
return False
449446

450-
shutil.make_archive(str(zip_path), "zip", source_dir.parent, source_dir.name)
447+
if IS_LINUX:
448+
# tar.gz keeps the executable bit on the launcher, unlike zip.
449+
archive_file = Path(f"{zip_path}.tar.gz")
450+
if archive_file.exists():
451+
archive_file.unlink()
452+
453+
with tarfile.open(archive_file, "w:gz") as archive:
454+
archive.add(source_dir, arcname=source_dir.name)
455+
else:
456+
archive_file = Path(f"{zip_path}.zip")
457+
if archive_file.exists():
458+
archive_file.unlink()
459+
460+
shutil.make_archive(str(zip_path), "zip", source_dir.parent, source_dir.name)
451461

452-
print(f"\n✓ Portable ZIP created: {zip_path}.zip")
462+
print(f"\n✓ Portable archive created: {archive_file}")
453463
return True
454464

455465

0 commit comments

Comments
 (0)