Release: v2.1.6 - Added VaptVupt 2.40

This commit is contained in:
Cristian Cezar Moisés 2026-04-22 03:46:59 -03:00
commit d4660e6539
41 changed files with 4309 additions and 741 deletions

21
gui/LICENSE-GUI Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Cristian Cezar Moisés
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

127
gui/README.md Normal file
View file

@ -0,0 +1,127 @@
# Zupt GUI — Cross-Platform Post-Quantum Backup
Desktop application for [zupt](https://github.com/cristiancmoises/zupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption.
Works on GNU/Linux, BSD, macOS, and Windows.
## Install
### Linux (recommended)
```bash
tar xzf zupt-gui.tar.gz && cd zupt-gui
./zupt-gui # auto-creates venv, installs PySide6
./install.sh --user # adds right-click menu integration
```
After install, right-click any file in Nemo/Nautilus to see "Compress with Zupt".
Double-click any .zupt file to open it in the GUI.
### Windows
**Option A — Installer (recommended):**
Download `ZuptGUI-2.1.6-Setup.exe` and run it. Installs to Program Files, adds Start Menu shortcut, desktop shortcut, right-click context menus, and .zupt file association. Includes uninstaller.
**Option B — Build from source:**
```cmd
cd packaging\windows
build-windows.bat
```
Requires Python 3.9+, NSIS 3.x, and a compiled `zupt.exe`.
**Option C — Run directly:**
```cmd
pip install PySide6
python src\zupt_gui.py
```
### macOS / BSD
```bash
pip3 install PySide6
python3 src/zupt_gui.py
```
### AppImage (universal Linux)
```bash
chmod +x zupt-gui-1.0.0-x86_64.AppImage
./zupt-gui-1.0.0-x86_64.AppImage
```
### Flatpak
```bash
flatpak-builder --install build packaging/flatpak/dev.zupt.gui.yml
flatpak run dev.zupt.gui
```
## Features
| Tab | Function |
|-----|----------|
| Keys | Generate ML-KEM-768 + X25519 hybrid keypairs |
| Compress | All codecs, levels 1-9, dedup, solid, password, PQ keys |
| Extract | Decrypt and extract .zupt archives |
| Verify | Check block checksums, view archive metadata |
| Disk | Full-disk backup and restore |
| About | Version, cryptographic stack, credits |
All tabs support drag-and-drop. Drop a .zupt file anywhere on the window to extract it. Drop any other file to compress it.
## System Integration
### Linux (Nemo / Cinnamon)
After `./install.sh --user`:
- Right-click any file: **Compress with Zupt**
- Right-click .zupt file: **Extract with Zupt**
- Double-click .zupt: opens in Zupt GUI
### Windows (after installer)
- Right-click any file: **Compress with Zupt**
- Right-click any folder: **Compress with Zupt**
- Double-click .zupt: opens in Zupt GUI
- Right-click .zupt: **Verify Integrity**
## Architecture
```
Zupt GUI (PySide6, Python)
|
|-- subprocess.Popen() with streaming stderr
|
v
zupt CLI (Pure C11 binary)
ML-KEM-768 + X25519 + AES-256-CTR
VaptVupt / LZHP / Store codecs
Block deduplication, full-disk backup
```
The GUI calls the zupt CLI binary — all cryptography runs in native C, not Python.
## Packaging
| Platform | Format | Tool |
|----------|--------|------|
| Any | pip | `pip install .` |
| Debian/Ubuntu/Mint | .deb | `packaging/deb/control` |
| Fedora/openSUSE | .rpm | `rpmbuild -ba packaging/rpm/zupt-gui.spec` |
| Universal Linux | .AppImage | `packaging/appimage/build-appimage.sh` |
| Sandboxed Linux | .flatpak | `packaging/flatpak/dev.zupt.gui.yml` |
| Windows | .exe installer | `packaging/windows/build-windows.bat` |
| Windows | NSIS .exe | `packaging/windows/zupt-installer.nsi` |
## Credits
- **zupt** v2.1.6 — Cristian Cezar Moises ([github](https://github.com/cristiancmoises/zupt))
- **libzupt** v1.0.2 — Alessandro de Oliveira Faria ([github](https://github.com/cabelo/libzupt))
## License
MIT

BIN
gui/assets/zupt-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
gui/assets/zupt-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
gui/assets/zupt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

BIN
gui/assets/zupt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

88
gui/install.sh Executable file
View file

@ -0,0 +1,88 @@
#!/bin/bash
# Install Zupt GUI + desktop integration
# Run: sudo ./install.sh (or ./install.sh --user for per-user install)
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
USER_INSTALL=0
[ "$1" = "--user" ] && USER_INSTALL=1
if [ "$USER_INSTALL" -eq 1 ]; then
BIN="$HOME/.local/bin"
APPS="$HOME/.local/share/applications"
NEMO="$HOME/.local/share/nemo/actions"
MIME="$HOME/.local/share/mime"
NAUTILUS="$HOME/.local/share/nautilus/scripts"
else
BIN="/usr/local/bin"
APPS="/usr/share/applications"
NEMO="/usr/share/nemo/actions"
MIME="/usr/share/mime"
NAUTILUS=""
fi
mkdir -p "$BIN" "$APPS"
# ── Install launcher ──
cat > "$BIN/zupt-gui" << LAUNCHER
#!/bin/bash
DIR="$DIR"
VENV="\$DIR/.venv"
PY="\$VENV/bin/python3"
[ ! -x "\$PY" ] && python3 -m venv "\$VENV" && "\$VENV/bin/pip" install PySide6 -q
exec "\$PY" "\$DIR/src/zupt_gui.py" "\$@"
LAUNCHER
chmod +x "$BIN/zupt-gui"
echo "Installed: $BIN/zupt-gui"
# ── Desktop entry ──
cp "$DIR/packaging/zupt-gui.desktop" "$APPS/"
echo "Installed: $APPS/zupt-gui.desktop"
# ── Nemo actions (Linux Mint / Cinnamon) ──
if [ -d "$(dirname "$NEMO")" ] || [ "$USER_INSTALL" -eq 1 ]; then
mkdir -p "$NEMO"
cp "$DIR/packaging/desktop-integration/nemo/"*.nemo_action "$NEMO/" 2>/dev/null && \
echo "Installed: Nemo right-click actions" || true
fi
# ── Nautilus scripts (GNOME) ──
if [ -n "$NAUTILUS" ]; then
mkdir -p "$NAUTILUS"
cat > "$NAUTILUS/Compress with Zupt" << 'NSCRIPT'
#!/bin/bash
zupt-gui --compress $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
NSCRIPT
chmod +x "$NAUTILUS/Compress with Zupt"
echo "Installed: Nautilus script"
fi
# ── MIME type for .zupt files ──
MIME_XML="$MIME/packages/zupt.xml"
if [ ! -f "$MIME_XML" ]; then
mkdir -p "$(dirname "$MIME_XML")"
cat > "$MIME_XML" << 'MIMEXML'
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-zupt">
<comment>Zupt Archive</comment>
<glob pattern="*.zupt"/>
<icon name="package-x-generic"/>
</mime-type>
</mime-info>
MIMEXML
if command -v update-mime-database >/dev/null; then
update-mime-database "$MIME" 2>/dev/null
fi
echo "Registered: .zupt MIME type"
fi
# ── Associate .zupt files with zupt-gui ──
if command -v xdg-mime >/dev/null; then
xdg-mime default zupt-gui.desktop application/x-zupt 2>/dev/null
echo "Associated: .zupt files open with Zupt GUI"
fi
echo ""
echo "Done. Right-click any file in your file manager to see Zupt options."
echo "Double-click any .zupt file to open it in Zupt GUI."

View file

@ -0,0 +1,49 @@
#!/bin/bash
# Build Zupt GUI AppImage
# Requires: appimagetool, python3, pip
set -e
APP="zupt-gui"
VERSION="1.0.0"
APPDIR="${APP}.AppDir"
rm -rf "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/zupt-gui" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps"
# Install Python + deps into AppDir
python3 -m venv "$APPDIR/usr/python"
"$APPDIR/usr/python/bin/pip" install PySide6 --quiet
# Copy app
cp ../../src/zupt_gui.py "$APPDIR/usr/share/zupt-gui/"
cp ../../assets/zupt.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" 2>/dev/null || true
# Create launcher
cat > "$APPDIR/AppRun" << 'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
export PATH="$HERE/usr/bin:$HERE/usr/python/bin:$PATH"
exec python3 "$HERE/usr/share/zupt-gui/zupt_gui.py" "$@"
APPRUN
chmod +x "$APPDIR/AppRun"
# Desktop file
cat > "$APPDIR/${APP}.desktop" << DESKTOP
[Desktop Entry]
Type=Application
Name=Zupt GUI
Comment=Post-Quantum Backup Utility
Exec=zupt-gui
Icon=zupt-gui
Categories=Utility;Archiving;Security;
Terminal=false
DESKTOP
# Build AppImage
if command -v appimagetool >/dev/null; then
ARCH=x86_64 appimagetool "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage"
echo "Built: ${APP}-${VERSION}-x86_64.AppImage"
else
echo "appimagetool not found. Install from https://github.com/AppImage/AppImageKit"
echo "AppDir ready at: $APPDIR/"
fi

13
gui/packaging/deb/control Normal file
View file

@ -0,0 +1,13 @@
Package: zupt-gui
Version: 1.0.0
Section: utils
Priority: optional
Architecture: all
Depends: python3 (>= 3.9), python3-pyside6, zupt (>= 2.1.6)
Maintainer: Cristian Cezar Moises <cristian@zupt.dev>
Homepage: https://github.com/cristiancmoises/zupt
Description: Zupt GUI — Post-Quantum Backup Utility
Cross-platform graphical interface for the zupt backup compression
utility with post-quantum hybrid encryption (ML-KEM-768 + X25519),
hardware-adaptive codecs, block-level deduplication, and full-disk
backup/restore support.

View file

@ -0,0 +1,7 @@
[Nemo Action]
Name=Compress with Zupt
Comment=Create encrypted .zupt archive
Exec=zupt-gui --compress %F
Icon-Name=package-x-generic
Selection=Any
Extensions=any;

View file

@ -0,0 +1,7 @@
[Nemo Action]
Name=Extract with Zupt
Comment=Decrypt and extract .zupt archive
Exec=zupt-gui --extract %F
Icon-Name=package-x-generic
Selection=S
Extensions=zupt;

View file

@ -0,0 +1,37 @@
app-id: dev.zupt.gui
runtime: org.freedesktop.Platform
runtime-version: '24.08'
sdk: org.freedesktop.Sdk
command: zupt-gui
finish-args:
- --share=ipc
- --socket=x11
- --socket=wayland
- --filesystem=home
- --device=all # For disk backup (block devices)
modules:
- name: python3-pyside6
buildsystem: simple
build-commands:
- pip3 install --prefix=/app PySide6
- name: zupt
buildsystem: simple
build-commands:
- make
- install -Dm755 zupt /app/bin/zupt
sources:
- type: git
url: https://github.com/cristiancmoises/zupt
tag: v2.1.6
- name: zupt-gui
buildsystem: simple
build-commands:
- install -Dm755 src/zupt_gui.py /app/bin/zupt-gui
- install -Dm644 packaging/zupt-gui.desktop /app/share/applications/dev.zupt.gui.desktop
sources:
- type: dir
path: .

View file

@ -0,0 +1,43 @@
Name: zupt-gui
Version: 1.0.0
Release: 1%{?dist}
Summary: Zupt GUI Post-Quantum Backup Utility
License: MIT
URL: https://github.com/cristiancmoises/zupt
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
Requires: python3 >= 3.9
Requires: python3-pyside6
Requires: zupt >= 2.1.6
%description
Cross-platform graphical interface for the zupt backup compression
utility with post-quantum hybrid encryption (ML-KEM-768 + X25519),
hardware-adaptive codecs, block-level deduplication, and full-disk
backup/restore support.
%install
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_datadir}/zupt-gui
mkdir -p %{buildroot}%{_datadir}/applications
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m 755 src/zupt_gui.py %{buildroot}%{_datadir}/zupt-gui/zupt_gui.py
install -m 644 packaging/zupt-gui.desktop %{buildroot}%{_datadir}/applications/
cat > %{buildroot}%{_bindir}/zupt-gui << 'EOF'
#!/bin/sh
exec python3 %{_datadir}/zupt-gui/zupt_gui.py "$@"
EOF
chmod 755 %{buildroot}%{_bindir}/zupt-gui
%files
%license LICENSE
%{_bindir}/zupt-gui
%{_datadir}/zupt-gui/
%{_datadir}/applications/zupt-gui.desktop
%changelog
* Mon Apr 21 2026 Cristian Cezar Moises - 1.0.0-1
- Initial release

View file

@ -0,0 +1,92 @@
@echo off
REM ══════════════════════════════════════════════════
REM Zupt GUI — Windows Build Script
REM Creates: ZuptGUI-2.1.6-Setup.exe
REM
REM Prerequisites:
REM 1. Python 3.9+ (python.org)
REM 2. NSIS 3.x (nsis.sourceforge.io)
REM 3. zupt.exe (compiled zupt CLI binary for Windows)
REM
REM Usage:
REM cd packaging\windows
REM build-windows.bat
REM ══════════════════════════════════════════════════
setlocal
echo.
echo Zupt GUI — Windows Build
echo ════════════════════════
echo.
REM ── Step 1: Install Python deps ──
echo [1/4] Installing dependencies...
pip install PySide6 pyinstaller --quiet --upgrade
if errorlevel 1 (
echo ERROR: pip install failed. Is Python in PATH?
pause
exit /b 1
)
REM ── Step 2: Build .exe with PyInstaller ──
echo [2/4] Building ZuptGUI.exe...
if exist dist rmdir /s /q dist
if exist build rmdir /s /q build
pyinstaller --onefile --windowed ^
--name "ZuptGUI" ^
--icon "..\..\assets\zupt.ico" ^
--add-data "..\..\assets\zupt.ico;assets" ^
--add-data "..\..\assets\zupt.png;assets" ^
"..\..\src\zupt_gui.py"
if not exist "dist\ZuptGUI.exe" (
echo ERROR: PyInstaller build failed.
pause
exit /b 1
)
echo Built: dist\ZuptGUI.exe
REM ── Step 3: Check for zupt.exe ──
echo [3/4] Checking for zupt.exe...
if not exist "zupt.exe" (
echo.
echo WARNING: zupt.exe not found in this directory.
echo The installer needs zupt.exe to bundle the CLI tool.
echo Options:
echo a) Copy zupt.exe here and re-run this script
echo b) Build zupt from source with MSYS2/MinGW:
echo pacman -S mingw-w64-x86_64-gcc make
echo cd zupt-2.1.6 ^&^& make
echo cp zupt.exe packaging/windows/
echo.
)
REM ── Step 4: Build NSIS installer ──
echo [4/4] Building installer...
where makensis >nul 2>&1
if errorlevel 1 (
echo.
echo NSIS not found. Install from: https://nsis.sourceforge.io
echo Then run: makensis zupt-installer.nsi
echo.
echo Standalone exe ready at: dist\ZuptGUI.exe
pause
exit /b 0
)
makensis zupt-installer.nsi
if errorlevel 1 (
echo ERROR: NSIS build failed.
pause
exit /b 1
)
echo.
echo ════════════════════════════════════════════
echo Build complete!
echo Standalone: dist\ZuptGUI.exe
echo Installer: ZuptGUI-2.1.6-Setup.exe
echo ════════════════════════════════════════════
echo.
pause

View file

@ -0,0 +1,132 @@
; Zupt GUI NSIS Installer Script
; Builds: ZuptGUI-Setup.exe
;
; Prerequisites on the build machine:
; 1. NSIS 3.x installed (https://nsis.sourceforge.io)
; 2. Run build-windows.bat first to create dist/ZuptGUI.exe
; 3. Place zupt.exe in this directory
; 4. Then: makensis zupt-installer.nsi
!include "MUI2.nsh"
!include "FileFunc.nsh"
; Config
!define APPNAME "Zupt"
!define APPVERSION "2.1.6"
!define GUIVERSION "1.0.0"
!define PUBLISHER "Cristian Cezar Moises"
!define HELPURL "https://github.com/cristiancmoises/zupt"
!define EXE "ZuptGUI.exe"
!define CLI "zupt.exe"
Name "${APPNAME} ${APPVERSION}"
OutFile "ZuptGUI-${APPVERSION}-Setup.exe"
InstallDir "$PROGRAMFILES\${APPNAME}"
InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel admin
; UI
!define MUI_ICON "..\..\assets\zupt.ico"
!define MUI_UNICON "..\..\assets\zupt.ico"
!define MUI_ABORTWARNING
!define MUI_WELCOMEPAGE_TITLE "Install ${APPNAME} ${APPVERSION}"
!define MUI_WELCOMEPAGE_TEXT "Post-quantum backup compression with ML-KEM-768 + X25519 hybrid encryption.$\r$\n$\r$\nThis will install the Zupt GUI and CLI tools."
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "..\..\LICENSE"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; Install
Section "Install"
SetOutPath $INSTDIR
; Copy files
File "dist\${EXE}"
File "${CLI}"
File "..\..\assets\zupt.ico"
File "..\..\LICENSE"
File "..\..\README.md"
; Write uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
; Start Menu
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
; Desktop shortcut
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico"
; Add to PATH (so zupt.exe is available system-wide)
EnVar::AddValue "PATH" "$INSTDIR"
; Register .zupt file association
WriteRegStr HKCR ".zupt" "" "ZuptArchive"
WriteRegStr HKCR "ZuptArchive" "" "Zupt Archive"
WriteRegStr HKCR "ZuptArchive\DefaultIcon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "ZuptArchive\shell\open\command" "" '"$INSTDIR\${EXE}" --extract "%1"'
WriteRegStr HKCR "ZuptArchive\shell\verify\command" "" '"$INSTDIR\${CLI}" test "%1"'
WriteRegStr HKCR "ZuptArchive\shell\verify" "" "Verify Integrity"
; Right-click "Compress with Zupt" on any file
WriteRegStr HKCR "*\shell\ZuptCompress" "" "Compress with Zupt"
WriteRegStr HKCR "*\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "*\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"'
; Right-click on directories
WriteRegStr HKCR "Directory\shell\ZuptCompress" "" "Compress with Zupt"
WriteRegStr HKCR "Directory\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico"
WriteRegStr HKCR "Directory\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"'
; Add/Remove Programs entry
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayName" "${APPNAME} — Post-Quantum Backup"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayIcon" "$INSTDIR\zupt.ico"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "Publisher" "${PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayVersion" "${APPVERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "URLInfoAbout" "${HELPURL}"
; Calculate installed size
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "EstimatedSize" "$0"
SectionEnd
; Uninstall
Section "Uninstall"
; Remove files
Delete "$INSTDIR\${EXE}"
Delete "$INSTDIR\${CLI}"
Delete "$INSTDIR\zupt.ico"
Delete "$INSTDIR\LICENSE"
Delete "$INSTDIR\README.md"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
; Remove shortcuts
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
Delete "$DESKTOP\${APPNAME}.lnk"
; Remove from PATH
EnVar::DeleteValue "PATH" "$INSTDIR"
; Remove file associations
DeleteRegKey HKCR ".zupt"
DeleteRegKey HKCR "ZuptArchive"
DeleteRegKey HKCR "*\shell\ZuptCompress"
DeleteRegKey HKCR "Directory\shell\ZuptCompress"
; Remove Add/Remove Programs entry
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
DeleteRegKey HKLM "Software\${APPNAME}"
SectionEnd

View file

@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=Zupt GUI
GenericName=Post-Quantum Backup
Comment=Compress, encrypt, and backup with quantum-resistant cryptography
Exec=zupt-gui
Icon=zupt-gui
Categories=Utility;Archiving;Security;
Keywords=backup;compress;encrypt;quantum;zupt;
Terminal=false
StartupNotify=true
MimeType=application/x-zupt;

1
gui/requirements.txt Normal file
View file

@ -0,0 +1 @@
PySide6>=6.5

32
gui/setup.py Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
setup(
name="zupt-gui",
version="1.0.0",
description="Zupt GUI — Cross-Platform Post-Quantum Backup Utility",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Cristian Cezar Moisés",
url="https://github.com/cristiancmoises/zupt",
license="MIT",
packages=find_packages(where="src"),
package_dir={"": "src"},
py_modules=["zupt_gui"],
python_requires=">=3.9",
install_requires=["PySide6>=6.5"],
entry_points={
"console_scripts": ["zupt-gui=zupt_gui:main"],
"gui_scripts": ["zupt-gui=zupt_gui:main"],
},
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: Qt",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Security :: Cryptography",
"Topic :: System :: Archiving :: Compression",
],
)

569
gui/src/zupt_gui.py Normal file
View file

@ -0,0 +1,569 @@
#!/usr/bin/env python3
"""Zupt GUI — Cross-Platform Post-Quantum Backup. Requires PySide6."""
import sys, os, subprocess, shutil
from pathlib import Path
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog,
QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox,
QSpinBox, QMessageBox, QStatusBar, QScrollArea
)
from PySide6.QtCore import Qt, Signal, QObject, QThread
from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap
# ── Find zupt binary ──
def _find_zupt():
if os.environ.get("ZUPT_BIN") and os.path.isfile(os.environ["ZUPT_BIN"]):
return os.environ["ZUPT_BIN"]
# Check local project tree FIRST (handles running from zupt-2.1.6/gui/)
here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
for c in [here.parent.parent/"zupt", # zupt-2.1.6/gui/src -> zupt-2.1.6/zupt
here.parent/"zupt", # zupt-2.1.6/gui -> zupt-2.1.6/zupt (shouldn't happen but safe)
here/"zupt", # same dir as script
here.parent.parent/"zupt.exe",
here.parent/"zupt.exe",
here/"zupt.exe"]:
if c.is_file() and os.access(str(c), os.X_OK):
return str(c.resolve())
# Then system PATH
found = shutil.which("zupt")
if found: return found
# Fallback common paths
for c in [Path("/usr/local/bin/zupt"), Path("/usr/bin/zupt")]:
if c.is_file(): return str(c)
return "zupt"
ZUPT = _find_zupt()
# ── Query version ONCE at import (cached) ──
def _get_version():
try:
r = subprocess.run([ZUPT, "version"], capture_output=True, text=True, timeout=5)
if r.returncode == 0:
lines = r.stdout.strip().split("\n")
return lines[0], r.stdout.strip()
except Exception: pass
return "zupt (not found)", ""
ZUPT_VER_SHORT, ZUPT_VER_FULL = _get_version()
# ── Find icon file ──
def _find_icon():
here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
for p in [here/"assets/zupt.ico", here/"assets/zupt-icon.png", here/"assets/zupt.png",
here.parent/"assets/zupt.ico", here.parent/"assets/zupt-icon.png",
Path("/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"),
Path("/usr/share/zupt-gui/assets/zupt-icon.png")]:
if p.is_file(): return str(p)
return None
ICON_PATH = _find_icon()
STYLE = """
* { font-family: "Segoe UI", "Cantarell", "Noto Sans", sans-serif; }
QMainWindow { background: #0a0a0a; }
QTabWidget::pane { background: #0a0a0a; border: none; border-top: 1px solid #1a2a30; }
QTabBar { background: #050a0e; }
QTabBar::tab {
background: #050a0e; color: #5a7a88; padding: 11px 22px;
border: none; border-bottom: 2px solid transparent;
font-weight: 600; font-size: 12px;
}
QTabBar::tab:hover { color: #a0c8d8; background: #0a1018; }
QTabBar::tab:selected { color: #00dde0; border-bottom-color: #00dde0; background: #0c1218; }
QLabel { color: #90acb8; }
QLineEdit, QComboBox, QSpinBox {
background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30;
border-radius: 6px; padding: 9px 12px;
font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 13px;
selection-background-color: #004858;
}
QLineEdit:focus, QComboBox:focus, QSpinBox:focus { border-color: #006878; }
QComboBox::drop-down { border: none; padding-right: 8px; }
QComboBox QAbstractItemView { background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30; selection-background-color: #004858; }
QPushButton {
background: #0a2028; color: #00dde0; border: 1px solid #1a3a40;
border-radius: 6px; padding: 10px 24px; font-weight: 600; font-size: 12px;
}
QPushButton:hover { background: #0e2830; border-color: #00dde0; }
QPushButton:pressed { background: #062020; }
QPushButton:disabled { color: #304048; border-color: #151e22; }
QPushButton#green { color: #00c868; border-color: #0a3020; background: #061a10; }
QPushButton#green:hover { border-color: #00c868; background: #0a2818; }
QPushButton#amber { color: #d0a020; border-color: #2a2010; background: #181200; }
QPushButton#amber:hover { border-color: #d0a020; background: #201800; }
QPushButton#small { padding: 8px 14px; font-size: 11px; }
QTextEdit {
background: #060c10; color: #608898; border: 1px solid #1a2a30;
border-radius: 6px; padding: 10px;
font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 11px;
}
QProgressBar { background: #0e1820; border: none; border-radius: 3px; max-height: 6px; }
QProgressBar::chunk { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #00dde0,stop:1 #008090); border-radius: 3px; }
QCheckBox { color: #6a8898; spacing: 6px; font-size: 13px; }
QCheckBox::indicator { width: 16px; height: 16px; border-radius: 3px; border: 1px solid #1a3a40; background: #0e1820; }
QCheckBox::indicator:checked { background: #004858; border-color: #00dde0; }
QScrollArea { border: none; background: transparent; }
QStatusBar { background: #050a0e; color: #3a5868; font-size: 11px; border-top: 1px solid #1a2a30; }
QFrame#sep { background: #1a2a30; max-height: 1px; }
"""
def run_zupt(args, timeout=30):
try:
r = subprocess.run([ZUPT]+list(args), capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout, r.stderr
except FileNotFoundError: return -1, "", f"zupt not found: {ZUPT}"
except subprocess.TimeoutExpired: return -1, "", "Timed out"
class Worker(QObject):
done = Signal(int, str, str)
log = Signal(str)
def __init__(self, args): super().__init__(); self.args = args
def run(self):
self.log.emit(f"$ zupt {' '.join(self.args)}")
try:
proc = subprocess.Popen([ZUPT]+self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
err_lines = []
for line in proc.stderr:
line = line.rstrip('\n')
if line: err_lines.append(line); self.log.emit(line)
stdout, _ = proc.communicate(timeout=7200)
self.done.emit(proc.returncode, stdout or "", "\n".join(err_lines))
except FileNotFoundError: self.done.emit(-1, "", f"zupt not found: {ZUPT}")
except subprocess.TimeoutExpired: proc.kill(); self.done.emit(-1, "", "Timed out")
# ── Widgets ──
def H(text):
lbl = QLabel(text.upper())
lbl.setStyleSheet("font-size:10px;font-weight:700;color:#3a5868;letter-spacing:1.5px;")
return lbl
def Sep():
f = QFrame(); f.setObjectName("sep"); f.setFixedHeight(1); return f
def Log(h=150):
t = QTextEdit(); t.setReadOnly(True); t.setMaximumHeight(h); return t
def PwField(ph="Optional"):
pw = QLineEdit(); pw.setEchoMode(QLineEdit.Password); pw.setPlaceholderText(ph); return pw
class PathField(QWidget):
def __init__(self, ph="", mode="open", filters="All Files (*)"):
super().__init__()
self.mode, self.filters = mode, filters
self.setAcceptDrops(True)
lay = QHBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(6)
self.edit = QLineEdit(); self.edit.setPlaceholderText(ph)
lay.addWidget(self.edit, 1)
btn = QPushButton("Browse"); btn.setObjectName("small"); btn.setFixedWidth(70)
btn.clicked.connect(self._pick); lay.addWidget(btn)
def _pick(self):
if self.mode == "save": p, _ = QFileDialog.getSaveFileName(self, "Save", "", self.filters)
elif self.mode == "dir": p = QFileDialog.getExistingDirectory(self, "Directory")
elif self.mode == "multi":
ps, _ = QFileDialog.getOpenFileNames(self, "Select", "", self.filters)
p = "|".join(ps) if ps else ""
else: p, _ = QFileDialog.getOpenFileName(self, "Open", "", self.filters)
if p: self.edit.setText(p)
def path(self): return self.edit.text().strip()
def paths(self):
t = self.path(); return t.split("|") if "|" in t else [t] if t else []
def dragEnterEvent(self, e):
if e.mimeData().hasUrls(): e.acceptProposedAction()
def dropEvent(self, e):
ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()]
if ps: self.edit.setText("|".join(ps) if self.mode == "multi" else ps[0])
def scrollable(w):
sa = QScrollArea(); sa.setWidgetResizable(True); sa.setWidget(w); sa.setFrameShape(QFrame.NoFrame); return sa
def run_async(parent, cmd, btn, log, progress=None):
log.clear(); btn.setEnabled(False)
if progress: progress.show()
t = QThread(); w = Worker(cmd); w.moveToThread(t)
w.log.connect(log.append)
def finish(code, out, err):
btn.setEnabled(True)
if progress: progress.hide()
log.append("\nDone." if code == 0 else f"\nFailed (exit {code}).")
t.quit()
w.done.connect(finish)
t.started.connect(w.run); t.start()
parent._thread, parent._worker = t, w
# ── Tabs ──
class KeysTab(QWidget):
def __init__(self):
super().__init__()
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Generate or export ML-KEM-768 + X25519 hybrid keys."))
v.addWidget(Sep())
# Section 1: Generate new keypair
v.addWidget(H("Generate new keypair"))
v.addWidget(QLabel("Creates both private and public key files."))
v.addWidget(H("Private key output"))
self.gen_priv = PathField("e.g. ~/zupt_private.key", "save", "Key (*.key);;All (*)")
v.addWidget(self.gen_priv)
self.gen_btn = QPushButton("Generate Keypair")
self.gen_btn.clicked.connect(self._generate)
v.addWidget(self.gen_btn)
self.gen_log = Log(120); v.addWidget(self.gen_log)
v.addWidget(Sep())
# Section 2: Export public key from existing private key
v.addWidget(H("Export public key from private key"))
v.addWidget(QLabel("Extract the public key from an existing private key file."))
v.addWidget(H("Existing private key"))
self.exp_priv = PathField("Select private key", "open", "Key (*.key);;All (*)")
v.addWidget(self.exp_priv)
v.addWidget(H("Public key output"))
self.exp_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)")
v.addWidget(self.exp_pub)
self.exp_btn = QPushButton("Export Public Key")
self.exp_btn.setObjectName("green")
self.exp_btn.clicked.connect(self._export)
v.addWidget(self.exp_btn)
self.exp_log = Log(100); v.addWidget(self.exp_log)
v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
def _generate(self):
p = self.gen_priv.path() or str(Path.home() / "zupt_private.key")
self.gen_priv.edit.setText(p)
self.gen_log.clear(); self.gen_btn.setEnabled(False)
code, _, err = run_zupt(["keygen", "-o", p])
self.gen_log.append(err.strip())
if code == 0:
pub = p.rsplit(".", 1)[0] + "_public.key" if "." in p else p + ".pub"
c2, _, e2 = run_zupt(["keygen", "--pub", "-o", pub, "-k", p])
self.gen_log.append(e2.strip())
if c2 == 0:
self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}")
else:
self.gen_log.append("\nFailed.")
self.gen_btn.setEnabled(True)
def _export(self):
priv = self.exp_priv.path()
pub = self.exp_pub.path()
if not priv: QMessageBox.warning(self, "Zupt", "Select the private key file."); return
if not pub:
pub = priv.rsplit(".", 1)[0] + "_public.key" if "." in priv else priv + ".pub"
self.exp_pub.edit.setText(pub)
self.exp_log.clear(); self.exp_btn.setEnabled(False)
code, _, err = run_zupt(["keygen", "--pub", "-o", pub, "-k", priv])
self.exp_log.append(err.strip())
if code == 0:
self.exp_log.append(f"\nPublic key: {pub}")
else:
self.exp_log.append("\nFailed.")
self.exp_btn.setEnabled(True)
class CompressTab(QWidget):
def __init__(self, initial=None):
super().__init__()
self._thread = self._worker = None
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Compress files into an encrypted .zupt archive."))
v.addWidget(Sep())
v.addWidget(H("Source files / directory"))
self.src = PathField("Drop files here or browse", "multi"); v.addWidget(self.src)
v.addWidget(H("Output archive"))
self.dst = PathField("e.g. backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.dst)
row = QHBoxLayout(); row.setSpacing(16)
for label, widget in [("Codec", self._mk_codec()), ("Level", self._mk_level())]:
c = QVBoxLayout(); c.addWidget(H(label)); c.addWidget(widget); row.addLayout(c)
c = QVBoxLayout(); c.addWidget(H("Options"))
self.dedup = QCheckBox("Dedup"); self.solid = QCheckBox("Solid")
oh = QHBoxLayout(); oh.addWidget(self.dedup); oh.addWidget(self.solid); oh.addStretch()
c.addLayout(oh); row.addLayout(c)
v.addLayout(row); v.addWidget(Sep())
enc = QHBoxLayout(); enc.setSpacing(16)
pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField("AES-256"); pw.addWidget(self.pw); enc.addLayout(pw)
pq = QVBoxLayout(); pq.addWidget(H("PQ public key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq)
v.addLayout(enc)
self.btn = QPushButton("Compress"); self.btn.clicked.connect(self._run); v.addWidget(self.btn)
self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress)
self.log = Log(140); v.addWidget(self.log); v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
if initial: self.src.edit.setText("|".join(initial))
def _mk_codec(self):
self.codec = QComboBox(); self.codec.addItems(["AUTO","VaptVupt","LZHP","Store"]); return self.codec
def _mk_level(self):
self.level = QSpinBox(); self.level.setRange(1,9); self.level.setValue(7); return self.level
def _run(self):
srcs = self.src.paths()
if not srcs or not srcs[0]: QMessageBox.warning(self, "Zupt", "Select files."); return
dst = self.dst.path() or srcs[0] + ".zupt"; self.dst.edit.setText(dst)
cmd = ["compress", "-l", str(self.level.value())]
cm = {"AUTO": None, "VaptVupt": "--vv", "LZHP": "--lzhp", "Store": "-s"}
if cm.get(self.codec.currentText()): cmd.append(cm[self.codec.currentText()])
if self.dedup.isChecked(): cmd.append("--dedup")
if self.solid.isChecked(): cmd.append("--solid")
if self.pw.text(): cmd += ["-p", self.pw.text()]
if self.pq.path(): cmd += ["--pq", self.pq.path()]
cmd.append(dst); cmd.extend(srcs)
run_async(self, cmd, self.btn, self.log, self.progress)
class ExtractTab(QWidget):
def __init__(self, initial=None):
super().__init__()
self._thread = self._worker = None
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Extract and decrypt a .zupt archive."))
v.addWidget(Sep())
v.addWidget(H("Archive")); self.arc = PathField("Drop .zupt here", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.arc)
v.addWidget(H("Output directory")); self.out = PathField("Same as archive", "dir"); v.addWidget(self.out)
enc = QHBoxLayout(); enc.setSpacing(16)
pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField(); pw.addWidget(self.pw); enc.addLayout(pw)
pq = QVBoxLayout(); pq.addWidget(H("PQ private key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq)
v.addLayout(enc)
self.btn = QPushButton("Extract"); self.btn.setObjectName("green"); self.btn.clicked.connect(self._run); v.addWidget(self.btn)
self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress)
self.log = Log(140); v.addWidget(self.log); v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
if initial: self.arc.edit.setText(initial)
def _run(self):
arc = self.arc.path()
if not arc: QMessageBox.warning(self, "Zupt", "Select an archive."); return
cmd = ["extract"]
if self.out.path(): cmd += ["-o", self.out.path()]
if self.pw.text(): cmd += ["-p", self.pw.text()]
if self.pq.path(): cmd += ["--pq", self.pq.path()]
cmd.append(arc)
run_async(self, cmd, self.btn, self.log, self.progress)
class VerifyTab(QWidget):
def __init__(self):
super().__init__()
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Verify checksums or inspect archive metadata."))
v.addWidget(Sep())
v.addWidget(H("Verify integrity"))
self.varc = PathField("Archive to verify", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.varc)
v.addWidget(H("Password (if encrypted)"))
self.vpw = PwField("Leave empty if not encrypted"); v.addWidget(self.vpw)
self.vbtn = QPushButton("Verify"); self.vbtn.setObjectName("amber"); self.vbtn.clicked.connect(self._verify); v.addWidget(self.vbtn)
self.vlog = Log(120); v.addWidget(self.vlog)
v.addWidget(Sep())
v.addWidget(H("Archive info (no password needed)"))
self.iarc = PathField("Archive to inspect", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.iarc)
self.ibtn = QPushButton("Show Info"); self.ibtn.clicked.connect(self._info); v.addWidget(self.ibtn)
self.ilog = Log(140); v.addWidget(self.ilog); v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
def _verify(self):
arc = self.varc.path()
if not arc: return
cmd = ["test"]
if self.vpw.text(): cmd += ["-p", self.vpw.text()]
cmd.append(arc); self.vlog.clear()
code, out, err = run_zupt(cmd, timeout=600)
self.vlog.append((err + "\n" + out).strip())
self.vlog.append("\nAll checksums passed." if code == 0 else "\nVerification failed.")
def _info(self):
arc = self.iarc.path()
if not arc: return
self.ilog.clear()
code, out, err = run_zupt(["info", arc])
self.ilog.append(out.strip() if out.strip() else err.strip())
class DiskTab(QWidget):
def __init__(self):
super().__init__()
self._thread = self._worker = None
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10)
v.addWidget(QLabel("Full-disk or partition backup and restore."))
v.addWidget(Sep())
v.addWidget(H("Backup — source device or image"))
self.bsrc = PathField("/dev/sdX or disk.img"); v.addWidget(self.bsrc)
v.addWidget(H("Backup — output archive"))
self.bout = PathField("backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.bout)
bopt = QHBoxLayout(); bopt.setSpacing(16)
oc = QVBoxLayout(); oc.addWidget(H("Options")); self.bdedup = QCheckBox("Block deduplication"); oc.addWidget(self.bdedup); bopt.addLayout(oc)
pc = QVBoxLayout(); pc.addWidget(H("Password")); self.bpw = PwField("Optional — AES-256"); pc.addWidget(self.bpw); bopt.addLayout(pc)
v.addLayout(bopt)
self.bbtn = QPushButton("Start Backup"); self.bbtn.clicked.connect(self._backup); v.addWidget(self.bbtn)
self.blog = Log(100); v.addWidget(self.blog)
v.addWidget(Sep())
v.addWidget(H("Restore — archive"))
self.rarc = PathField("backup.zupt", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.rarc)
v.addWidget(H("Restore — target device or file"))
self.rtgt = PathField("/dev/sdX or output.img", "save"); v.addWidget(self.rtgt)
v.addWidget(H("Restore — password"))
self.rpw = PwField("If archive is encrypted"); v.addWidget(self.rpw)
self.rbtn = QPushButton("Start Restore"); self.rbtn.setObjectName("green"); self.rbtn.clicked.connect(self._restore); v.addWidget(self.rbtn)
self.rlog = Log(100); v.addWidget(self.rlog); v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
def _backup(self):
s, o = self.bsrc.path(), self.bout.path()
if not s or not o: QMessageBox.warning(self, "Zupt", "Set source and output."); return
cmd = ["disk", "backup"]
if self.bdedup.isChecked(): cmd.append("--dedup")
if self.bpw.text(): cmd += ["-p", self.bpw.text()]
cmd += [o, s]; run_async(self, cmd, self.bbtn, self.blog)
def _restore(self):
a, t = self.rarc.path(), self.rtgt.path()
if not a or not t: QMessageBox.warning(self, "Zupt", "Set archive and target."); return
if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", QMessageBox.Yes|QMessageBox.Cancel) != QMessageBox.Yes: return
cmd = ["disk", "restore"]
if self.rpw.text(): cmd += ["-p", self.rpw.text()]
cmd += [a, t]; run_async(self, cmd, self.rbtn, self.rlog)
class AboutTab(QWidget):
def __init__(self):
super().__init__()
inner = QWidget()
v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(4)
# Extract version number from cached string
ver_num = ZUPT_VER_SHORT.replace("zupt ", "").strip() if "zupt " in ZUPT_VER_SHORT else ZUPT_VER_SHORT
for text, style in [
("ZUPT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
(ver_num, "color:white;font-size:28px;font-weight:800;font-family:monospace;"),
("", ""),
("Post-quantum backup compression with ML-KEM-768 + X25519", "color:#6a8898;font-size:13px;"),
("hybrid encryption, hardware-adaptive codecs, and block dedup.", "color:#6a8898;font-size:13px;"),
("", ""),
("CRYPTOGRAPHIC STACK", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
("ML-KEM-768 FIPS 203 Post-Quantum KEM", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("X25519 RFC 7748 Elliptic Curve DH", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("AES-256-CTR FIPS 197 Symmetric Cipher", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("HMAC-SHA256 RFC 2104 Authentication", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("PBKDF2 RFC 8018 Key Derivation", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("SHA3/SHAKE FIPS 202 Hash / XOF", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("", ""),
("CREDITS", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
("zupt Cristian Cezar Moises MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("github.com/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
("", ""),
("libzupt Alessandro de Oliveira Faria MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("github.com/cabelo/libzupt", "color:#3a5868;font-size:11px;font-family:monospace;"),
("", ""),
("WEBSITE", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"),
("https://zupt.securityops.co", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("zupt@riseup.net", "color:#5a7a88;font-size:12px;font-family:monospace;"),
("", ""),
(ZUPT_VER_SHORT, "color:#3a5868;font-size:11px;font-family:monospace;"),
]:
lbl = QLabel(text)
if text == "": lbl.setFixedHeight(10)
elif style: lbl.setStyleSheet(style)
v.addWidget(lbl)
v.addStretch()
lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner))
# ── Main ──
class ZuptWindow(QMainWindow):
def __init__(self, compress_files=None, extract_file=None):
super().__init__()
self.setWindowTitle(f"Zupt — {ZUPT_VER_SHORT}")
self.setMinimumSize(720, 500)
self.resize(880, 640)
self.setAcceptDrops(True)
# Window icon
if ICON_PATH:
self.setWindowIcon(QIcon(ICON_PATH))
central = QWidget(); self.setCentralWidget(central)
layout = QVBoxLayout(central); layout.setContentsMargins(0,0,0,0); layout.setSpacing(0)
# Header
hdr = QFrame(); hdr.setStyleSheet("background:#050a0e;border-bottom:1px solid #1a2a30;")
hl = QHBoxLayout(hdr); hl.setContentsMargins(20,10,20,10)
title = QLabel("ZUPT"); title.setStyleSheet("color:white;font-size:15px;font-weight:800;letter-spacing:3px;")
hl.addWidget(title)
sub = QLabel("Post-Quantum Backup"); sub.setStyleSheet("color:#3a5868;font-size:10px;font-weight:600;letter-spacing:1px;margin-left:8px;")
hl.addWidget(sub); hl.addStretch()
ver_num = ZUPT_VER_SHORT.replace("zupt ", "v").strip()
vl = QLabel(ver_num); vl.setStyleSheet("color:#3a5868;font-size:10px;font-family:monospace;background:#0a1018;padding:3px 10px;border-radius:4px;border:1px solid #1a2a30;")
hl.addWidget(vl)
layout.addWidget(hdr)
# Tabs
self.tabs = QTabWidget()
self.compress_tab = CompressTab(initial=compress_files)
self.extract_tab = ExtractTab(initial=extract_file)
self.tabs.addTab(KeysTab(), "Keys")
self.tabs.addTab(self.compress_tab, "Compress")
self.tabs.addTab(self.extract_tab, "Extract")
self.tabs.addTab(VerifyTab(), "Verify")
self.tabs.addTab(DiskTab(), "Disk")
self.tabs.addTab(AboutTab(), "About")
if compress_files: self.tabs.setCurrentIndex(1)
elif extract_file: self.tabs.setCurrentIndex(2)
layout.addWidget(self.tabs)
sb = QStatusBar()
sb.showMessage(f"{ZUPT_VER_SHORT} | {ZUPT}")
self.setStatusBar(sb)
def dragEnterEvent(self, e):
if e.mimeData().hasUrls(): e.acceptProposedAction()
def dropEvent(self, e):
ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()]
if not ps: return
if ps[0].endswith(".zupt"):
self.extract_tab.arc.edit.setText(ps[0]); self.tabs.setCurrentIndex(2)
else:
self.compress_tab.src.edit.setText("|".join(ps)); self.tabs.setCurrentIndex(1)
def main():
compress_files = extract_file = None
args = sys.argv[1:]
if args:
if args[0] == "--compress" and len(args) > 1: compress_files = args[1:]
elif args[0] == "--extract" and len(args) > 1: extract_file = args[1]
elif args[0].endswith(".zupt"): extract_file = args[0]
else: compress_files = args
app = QApplication(sys.argv)
app.setApplicationName("Zupt")
if ICON_PATH: app.setWindowIcon(QIcon(ICON_PATH))
app.setStyle("Fusion")
app.setStyleSheet(STYLE)
pal = QPalette()
for role, c in [(QPalette.Window,"#0a0a0a"),(QPalette.WindowText,"#90acb8"),
(QPalette.Base,"#0e1820"),(QPalette.Text,"#c0dce8"),
(QPalette.Button,"#0a2028"),(QPalette.ButtonText,"#00dde0"),
(QPalette.Highlight,"#004858"),(QPalette.HighlightedText,"#00dde0")]:
pal.setColor(role, QColor(c))
app.setPalette(pal)
win = ZuptWindow(compress_files=compress_files, extract_file=extract_file)
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

46
gui/zupt-gui Executable file
View file

@ -0,0 +1,46 @@
#!/bin/bash
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
VENV="$DIR/.venv"
PY="$VENV/bin/python3"
PIP="$VENV/bin/pip"
GUI="$DIR/src/zupt_gui.py"
# ─── System deps (Qt xcb needs these on Debian/Mint/Ubuntu) ───
NEED_APT=0
for pkg in libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 python3-full; do
dpkg -s "$pkg" >/dev/null 2>&1 || NEED_APT=1
done
if [ "$NEED_APT" -eq 1 ]; then
echo "Installing system dependencies..."
sudo apt-get update -qq
sudo apt-get install -y python3-full python3-venv \
libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 2>/dev/null
fi
# ─── Venv ───
if [ ! -x "$PY" ]; then
rm -rf "$VENV"
python3 -m venv "$VENV"
fi
if ! "$PY" -c "import PySide6" 2>/dev/null; then
echo "Installing PySide6..."
"$PIP" install --upgrade pip -q 2>/dev/null
"$PIP" install PySide6 -q
fi
# ─── Find zupt — local build FIRST, then system ───
if [ -z "$ZUPT_BIN" ]; then
# Check project tree first (gui/ is inside zupt-2.1.6/)
for p in "$DIR/../zupt" "$DIR/../../zupt" "$DIR/zupt"; do
[ -x "$p" ] && export ZUPT_BIN="$(readlink -f "$p")" && break
done
# Then system PATH
if [ -z "$ZUPT_BIN" ]; then
p="$(command -v zupt 2>/dev/null)"
[ -x "$p" ] && export ZUPT_BIN="$p"
fi
fi
exec "$PY" "$GUI" "$@"