├── .github ├── FUNDING.yml └── workflows │ └── build.yaml ├── .gitignore ├── .gitmodules ├── .tool-versions ├── ExportOptions.plist ├── LICENSE ├── README.md ├── Reconnect.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Reconnect.xcscheme │ └── ScreenshotSIS.xcscheme ├── Reconnect ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── Agenda16.imageset │ │ ├── Contents.json │ │ └── agenda.png │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── Contents.json │ ├── Data16.imageset │ │ ├── Contents.json │ │ └── data.png │ ├── Disconnected.imageset │ │ ├── Contents.json │ │ ├── disconnected32.png │ │ └── disconnected@2x.png │ ├── Disk16.imageset │ │ ├── Contents.json │ │ └── Disk32.png │ ├── Drive16.imageset │ │ ├── Contents.json │ │ └── Drive32.png │ ├── FileUnknown16.imageset │ │ ├── Contents.json │ │ └── Unknown32.png │ ├── Folder16.imageset │ │ ├── Contents.json │ │ └── Folder32.png │ ├── Icon.imageset │ │ ├── Contents.json │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── Jotter16.imageset │ │ ├── Contents.json │ │ └── jotter.png │ ├── OPL16.imageset │ │ ├── Contents.json │ │ └── opl.png │ ├── Record16.imageset │ │ ├── Contents.json │ │ └── record.png │ ├── Sheet16.imageset │ │ ├── Contents.json │ │ └── sheet.png │ ├── Sketch16.imageset │ │ ├── Contents.json │ │ └── sketch.png │ └── Word16.imageset │ │ ├── Contents.json │ │ └── word.png ├── Commands │ ├── BrowserCommands.swift │ ├── HelpCommands.swift │ └── SparkleCommands.swift ├── Extensions │ └── Licensable.swift ├── Info.plist ├── Licenses │ ├── apache-2.0-license │ ├── plptools-license │ ├── reconnect-license │ └── sparkle-license ├── Model │ ├── ApplicationModel.swift │ ├── BrowserModel.swift │ ├── CheckForUpdatesViewModel.swift │ ├── EditableTextModel.swift │ ├── FileConverter.swift │ ├── FileReference.swift │ ├── Transfer.swift │ └── TransfersModel.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Reconnect-Bridging-Header.h ├── Reconnect.entitlements ├── ReconnectApp.swift ├── Utilities │ └── NavigationStack.swift ├── Views │ ├── BrowserDetailView.swift │ ├── BrowserView.swift │ ├── CheckForUpdatesView.swift │ ├── EditableText.swift │ ├── FileTypePopover.swift │ ├── HistoryItemView.swift │ ├── PixelImage.swift │ ├── Sidebar.swift │ ├── ThumbnailView.swift │ ├── TransferRow.swift │ └── TransfersView.swift └── Windows │ ├── BrowserWindow.swift │ └── TransfersWindow.swift ├── ReconnectCore ├── .gitignore ├── Package.swift ├── Sources │ └── ReconnectCore │ │ ├── Extensions │ │ ├── Array.swift │ │ ├── DirectoryEntry.swift │ │ ├── DriveInfo.swift │ │ ├── FileManager.swift │ │ ├── PsiLuaEnv.swift │ │ ├── String.swift │ │ ├── UInt32.swift │ │ ├── URL.swift │ │ └── rfsv.errs.swift │ │ ├── Model │ │ ├── FileType.swift │ │ └── ReconnectError.swift │ │ ├── PLP │ │ ├── FileServer.swift │ │ ├── PsionClient.swift │ │ ├── RemoteCommandServicesClient.swift │ │ └── Server.swift │ │ └── Utilities │ │ ├── Graphics.swift │ │ └── SerialDeviceMonitor.swift └── Tests │ └── ReconnectCoreTests │ └── ReconnectCoreTests.swift ├── ReconnectMenu ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x@2x.png │ ├── Contents.json │ ├── StatusConnected.imageset │ │ ├── Contents.json │ │ ├── StatusConnected.png │ │ └── StatusConnectedDark.png │ └── StatusDisconnected.imageset │ │ ├── Contents.json │ │ ├── StatusDisconnected.png │ │ └── StatusDisconnectedDark.png ├── Model │ └── ApplicationModel.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ReconnectMenu.entitlements ├── ReconnectMenuApp.swift └── Views │ └── MainMenu.swift ├── ReconnectTests ├── NavigationStackTests.swift ├── ReconnectTests.swift └── WindowsPathTests.swift ├── ReconnectUITests ├── ReconnectUITests.swift └── ReconnectUITestsLaunchTests.swift ├── ScreenshotSIS └── Command.swift ├── dependencies └── plptools │ └── Package.swift ├── docs ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _includes │ ├── footer.html │ ├── navigation.html │ └── scripts.html ├── _layouts │ └── default.html ├── css │ └── style.css ├── images │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── screenshot-default-dark@2x.png │ └── screenshot-default@2x.png ├── index.md ├── js │ └── rewrite-external-links.js ├── license │ └── index.md └── privacy-policy │ └── index.md ├── graphics ├── app-icon │ ├── SF Symbols Icon.symbolic │ └── Series 5 Icon.sketch └── assets │ ├── agenda │ └── agenda.png │ ├── data │ └── data.png │ ├── disconnected │ ├── disconnected32.png │ └── disconnected32@2x.png │ ├── disk │ └── Disk32.png │ ├── drives │ └── Drive32.png │ ├── folder │ ├── Directory16.png │ ├── Directory24.png │ ├── Directory32.acorn │ ├── Directory32.png │ └── Folder32.png │ ├── jotter │ └── jotter.png │ ├── opl │ └── opl.png │ ├── record │ └── record.png │ ├── series-5 │ ├── series5.png │ └── series5large.png │ ├── sheet │ └── sheet.png │ ├── sketch │ └── sketch.png │ ├── status │ ├── StatusConnected.png │ ├── StatusConnectedDark.png │ ├── StatusDisconnected.png │ └── StatusDisconnectedDark.png │ ├── unknown │ ├── Unknown32.acorn │ ├── Unknown32.png │ └── Unknown32_PsiWin.png │ └── word │ └── word.png ├── images └── screenshot@2x.png ├── profiles ├── Reconnect_Developer_ID_Profile.provisionprofile └── Reconnect_Menu_Developer_ID_Profile.provisionprofile ├── scripts ├── build-website.sh ├── build.sh ├── environment.sh ├── install-dependencies.sh ├── release-notes.html ├── release-notes.md ├── release.sh └── update-release-notes.sh └── utilities └── screenshot.exe /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jbmorley] 2 | buy_me_a_coffee: jbmorley 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 9 * * *' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | macos-build: 15 | 16 | runs-on: inseven-macos-14 17 | 18 | steps: 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | fetch-depth: 0 25 | 26 | - name: Install dependencies 27 | run: scripts/install-dependencies.sh 28 | 29 | - name: Build and test 30 | env: 31 | DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: ${{ secrets.PERSONAL_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }} 32 | DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.PERSONAL_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }} 33 | 34 | APPLE_API_KEY_BASE64: ${{ secrets.PERSONAL_APPLE_API_KEY_BASE64 }} 35 | APPLE_API_KEY_ISSUER_ID: ${{ secrets.PERSONAL_APPLE_API_KEY_ISSUER_ID }} 36 | APPLE_API_KEY_ID: ${{ secrets.PERSONAL_APPLE_API_KEY_ID }} 37 | 38 | SPARKLE_PRIVATE_KEY_BASE64: ${{ secrets.SPARKLE_PRIVATE_KEY_BASE64 }} 39 | 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | RELEASE: ${{ github.ref == 'refs/heads/main' }} 42 | 43 | run: | 44 | scripts/build.sh 45 | 46 | - name: Archive the binary 47 | uses: actions/upload-artifact@v4 48 | with: 49 | path: build/build-*.zip 50 | if-no-files-found: error 51 | 52 | website-build: 53 | 54 | needs: [macos-build] 55 | 56 | runs-on: ubuntu-latest 57 | 58 | steps: 59 | 60 | - name: Checkout repository 61 | uses: actions/checkout@v4 62 | with: 63 | fetch-depth: 0 64 | 65 | - name: Checkout required submodules 66 | run: | 67 | git submodule update --init --depth 1 scripts/build-tools 68 | git submodule update --init --depth 1 scripts/changes 69 | 70 | - name: Install the tool dependencies 71 | uses: jdx/mise-action@v2 72 | 73 | - name: Install dependencies 74 | run: scripts/install-dependencies.sh 75 | 76 | - name: Build website 77 | run: | 78 | scripts/build-website.sh 79 | chmod -v -R +rX "_site/" 80 | 81 | - name: Upload Pages artifact 82 | uses: actions/upload-pages-artifact@v3 83 | 84 | website-deploy: 85 | 86 | needs: website-build 87 | if: ${{ github.ref == 'refs/heads/main' }} 88 | 89 | permissions: 90 | pages: write 91 | id-token: write 92 | 93 | environment: 94 | name: github-pages 95 | url: ${{ steps.deployment.outputs.page_url }} 96 | 97 | runs-on: ubuntu-latest 98 | 99 | steps: 100 | - name: Deploy to GitHub Pages 101 | id: deployment 102 | uses: actions/deploy-pages@v4 103 | 104 | sparkle-update: 105 | needs: macos-build 106 | if: ${{ github.ref == 'refs/heads/main' }} 107 | 108 | runs-on: ubuntu-latest 109 | steps: 110 | 111 | - name: Update Sparkle archives 112 | uses: peter-evans/repository-dispatch@v3 113 | with: 114 | token: ${{ secrets._GITHUB_ACCESS_TOKEN }} 115 | repository: inseven/sparkle-archives 116 | event-type: build 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | .swiftpm 3 | /_site 4 | /.local 5 | /archives 6 | /build 7 | /docs/_site 8 | /docs/.jekyll-cache 9 | /docs/releases 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scripts/changes"] 2 | path = scripts/changes 3 | url = https://github.com/jbmorley/changes.git 4 | [submodule "scripts/build-tools"] 5 | path = scripts/build-tools 6 | url = https://github.com/jbmorley/build-tools.git 7 | [submodule "dependencies/diligence"] 8 | path = dependencies/diligence 9 | url = https://github.com/inseven/diligence.git 10 | [submodule "dependencies/plptools"] 11 | path = dependencies/plptools/plptools 12 | url = https://github.com/jbmorley/plptools.git 13 | [submodule "dependencies/interact"] 14 | path = dependencies/interact 15 | url = https://github.com/inseven/interact.git 16 | [submodule "scripts/Sparkle"] 17 | path = scripts/Sparkle 18 | url = https://github.com/sparkle-project/Sparkle 19 | [submodule "dependencies/opolua/opolua"] 20 | path = dependencies/opolua 21 | url = https://github.com/inseven/opolua.git 22 | [submodule "dependencies/PsionSoftwareIndexSwift"] 23 | path = dependencies/PsionSoftwareIndexSwift 24 | url = git@github.com:inseven/PsionSoftwareIndexSwift.git 25 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.12.1 2 | ruby 3.1.2 3 | -------------------------------------------------------------------------------- /ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | method 8 | developer-id 9 | provisioningProfiles 10 | 11 | uk.co.jbmorley.reconnect.apps.apple 12 | Reconnect Developer ID Profile 13 | uk.co.jbmorley.reconnect.apps.apple.menu 14 | Reconnect Menu Developer ID Profile 15 | 16 | signingCertificate 17 | Developer ID Application 18 | signingStyle 19 | manual 20 | teamID 21 | QS82QFHKWB 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reconnect 2 | 3 | [![build](https://github.com/inseven/PsiMac/actions/workflows/build.yaml/badge.svg)](https://github.com/inseven/PsiMac/actions/workflows/build.yaml) 4 | 5 | Psion connectivity for macOS. 6 | 7 | 8 | 9 | Reconnect is an attempt to recreate the original Psion PsiMac and MacConnect functionality and UI on modern macOS. It makes use of [plptools](https://github.com/rrthomas/plptools/) for both the PLP (Psion Link Protocol) session layer (NCP) and presentation layers (file server, etc). The plan is to contribute back to plptools where appropriate during development. 10 | 11 | The rationale behind creating a new app is that the existing approach taken by plptools, using [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) to expose the Psion files to the Mac, isn't practical (or always possible) on modern macOS. Reconnect aims to make it possible to connect a Psion to modern macOS without any development experience or additional software. 12 | 13 | ## Development 14 | 15 | Debugging Reconnect is a little more awkward than normal since plptools uses signals internally which are trapped by Xcode and lldb by default. Disable this automatic behavior by adding the following line to '~/.lldbinit-Xcode': 16 | 17 | ``` 18 | process handle SIGUSR1 -n true -p true -s false 19 | ``` 20 | 21 | ## References 22 | 23 | - [Psion Link Protocol](https://thoukydides.github.io/riscos-psifs/plp.html) 24 | 25 | ## License 26 | 27 | Reconnect is licensed under the GNU General Public License (GPL) version 2 (see [LICENSE](LICENSE)). It depends on the following separately licensed third-party libraries and components: 28 | 29 | - [Diligence](https://github.com/inseven/diligence), MIT License 30 | - [Interact](https://github.com/inseven/interact), MIT License 31 | - [Licensable](https://github.com/inseven/licensable), MIT License 32 | - [Lua](https://www.lua.org), MIT License 33 | - [OpoLua](https://github.com/inseven/opolua), MIT License 34 | - [plptools](https://github.com/rrthomas/plptools), GPL 2.0 License 35 | - [Sparkle](https://github.com/sparkle-project/Sparkle), Sparkle License 36 | - [Swift Algorithms](https://github.com/apple/swift-algorithms), Apache 2.0 License 37 | - [Swift Argument Parser](https://github.com/apple/swift-argument-parser), Apache 2.0 License 38 | - [Swift Numerics](https://github.com/apple/swift-numerics), Apache 2.0 License 39 | -------------------------------------------------------------------------------- /Reconnect.xcodeproj/xcshareddata/xcschemes/Reconnect.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Reconnect.xcodeproj/xcshareddata/xcschemes/ScreenshotSIS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 58 | 59 | 62 | 63 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Agenda16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "agenda.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Agenda16.imageset/agenda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Agenda16.imageset/agenda.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Data16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "data.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Data16.imageset/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Data16.imageset/data.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Disconnected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "disconnected32.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "disconnected@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Disconnected.imageset/disconnected32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Disconnected.imageset/disconnected32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Disconnected.imageset/disconnected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Disconnected.imageset/disconnected@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Disk16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Disk32.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Disk16.imageset/Disk32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Disk16.imageset/Disk32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Drive16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Drive32.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Drive16.imageset/Drive32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Drive16.imageset/Drive32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/FileUnknown16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Unknown32.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/FileUnknown16.imageset/Unknown32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/FileUnknown16.imageset/Unknown32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Folder16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Folder32.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Folder16.imageset/Folder32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Folder16.imageset/Folder32.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_512x512.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon_512x512@2x@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Icon.imageset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Icon.imageset/icon_512x512.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Icon.imageset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Icon.imageset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Jotter16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "jotter.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Jotter16.imageset/jotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Jotter16.imageset/jotter.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/OPL16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "opl.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/OPL16.imageset/opl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/OPL16.imageset/opl.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Record16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "record.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Record16.imageset/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Record16.imageset/record.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Sheet16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "sheet.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Sheet16.imageset/sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Sheet16.imageset/sheet.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Sketch16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "sketch.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Sketch16.imageset/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Sketch16.imageset/sketch.png -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Word16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "word.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reconnect/Assets.xcassets/Word16.imageset/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/Reconnect/Assets.xcassets/Word16.imageset/word.png -------------------------------------------------------------------------------- /Reconnect/Commands/BrowserCommands.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | public struct BrowserCommands: Commands { 22 | 23 | let browserModel: BrowserModel 24 | 25 | public var body: some Commands { 26 | 27 | CommandGroup(replacing: .newItem) { 28 | Button("New Folder") { 29 | browserModel.newFolder() 30 | } 31 | .keyboardShortcut("N", modifiers: [.command, .shift]) 32 | } 33 | 34 | CommandGroup(before: .newItem) { 35 | Button("Refresh") { 36 | browserModel.refresh() 37 | } 38 | .keyboardShortcut("R") 39 | Divider() 40 | } 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Reconnect/Commands/HelpCommands.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | public struct HelpCommands: Commands { 22 | 23 | @Environment(\.openURL) private var openURL 24 | 25 | public var body: some Commands { 26 | 27 | CommandGroup(replacing: .help) { 28 | Button("Donate") { 29 | openURL(.donate) 30 | } 31 | Button("More Software by Jason Morley") { 32 | openURL(.software) 33 | } 34 | } 35 | 36 | CommandGroup(before: .help) { 37 | Button("GitHub") { 38 | openURL(.gitHub) 39 | } 40 | Button("Discord") { 41 | openURL(.discord) 42 | } 43 | Button("Support") { 44 | openURL(.support) 45 | } 46 | Divider() 47 | } 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Reconnect/Commands/SparkleCommands.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | public struct SparkleCommands: Commands { 22 | 23 | let applicationModel: ApplicationModel 24 | 25 | public var body: some Commands { 26 | CommandGroup(before: .appSettings) { 27 | CheckForUpdatesView(updater: applicationModel.updaterController.updater) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Reconnect/Extensions/Licensable.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import Interact 22 | import Licensable 23 | import OpoLua 24 | 25 | fileprivate let plptoolsLicense = License(id: "https://github.com/rrthomas/plptools", 26 | name: "plptools", 27 | author: "plptools Authors", 28 | text: String(contentsOfResource: "plptools-license"), 29 | attributes: [ 30 | .url(URL(string: "https://github.com/rrthomas/plptools")!, title: "GitHub"), 31 | ]) 32 | 33 | fileprivate let sparkleLicense = License(id: "https://github.com/sparkle-project/Sparkle", 34 | name: "Sparkle", 35 | author: "Sparkle Project", 36 | text: String(contentsOfResource: "sparkle-license"), 37 | attributes: [ 38 | .url(URL(string: "https://github.com/sparkle-project/Sparkle")!, title: "GitHub"), 39 | .url(URL(string: "https://sparkle-project.org")!, title: "Website"), 40 | ]) 41 | 42 | fileprivate let swiftAlgorithmsLicense = License(id: "https://github.com/apple/swift-algorithms", 43 | name: "Swift Algorithms", 44 | author: "Apple Inc. and the Swift Project Authors", 45 | text: String(contentsOfResource: "apache-2.0-license"), 46 | attributes: [ 47 | .url(URL(string: "https://github.com/apple/swift-algorithms")!, title: "GitHub"), 48 | ], 49 | licenses: [ 50 | swiftNumericsLicense, 51 | ]) 52 | 53 | fileprivate let swiftArgumentParserLicense = License(id: "https://github.com/apple/swift-argument-parser", 54 | name: "Swift Argument Parser", 55 | author: "Apple Inc. and the Swift Project Authors", 56 | text: String(contentsOfResource: "apache-2.0-license"), 57 | attributes: [ 58 | .url(URL(string: "https://github.com/apple/swift-argument-parser")!, title: "GitHub"), 59 | ]) 60 | 61 | fileprivate let swiftNumericsLicense = License(id: "https://github.com/apple/swift-numerics", 62 | name: "Swift Numerics", 63 | author: "Apple Inc. and the Swift Numerics Project Authors", 64 | text: String(contentsOfResource: "apache-2.0-license"), 65 | attributes: [ 66 | .url(URL(string: "https://github.com/apple/swift-numerics")!, title: "GitHub"), 67 | ]) 68 | 69 | extension Licensable where Self == License { 70 | 71 | fileprivate static var plptools: License { plptoolsLicense } 72 | fileprivate static var sparkle: License { sparkleLicense } 73 | fileprivate static var swiftAlgorithms: License { swiftAlgorithmsLicense } 74 | fileprivate static var swiftArgumentParser: License { swiftArgumentParserLicense } 75 | fileprivate static var swiftNumerics: License { swiftNumericsLicense } 76 | 77 | } 78 | 79 | fileprivate let reconnectLicense = License(id: "https://github.com/inseven/reconnect", 80 | name: "Reconnect", 81 | author: "Jason Morley", 82 | text: String(contentsOfResource: "reconnect-license"), 83 | attributes: [ 84 | .url(URL(string: "https://github.com/inseven/reconnect")!, title: "GitHub"), 85 | ], 86 | licenses: [ 87 | .interact, 88 | .licensable, 89 | .opolua, 90 | .plptools, 91 | .sparkle, 92 | .swiftAlgorithms, 93 | .swiftArgumentParser, 94 | ]) 95 | 96 | extension Licensable where Self == License { 97 | 98 | public static var reconnect: License { reconnectLicense } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Reconnect/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLName 11 | uk.co.jbmorley.reconnect.types.url 12 | CFBundleURLSchemes 13 | 14 | x-reconnect 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | SUFeedURL 21 | https://sparkle.jbmorley.co.uk/inseven/reconnect/appcast.xml 22 | SUPublicEDKey 23 | 3EjNyhaYtMraXwX9DnIazXbJe4worl2Bktkt8VrZXZk= 24 | 25 | 26 | -------------------------------------------------------------------------------- /Reconnect/Licenses/sparkle-license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2013 Andy Matuschak. 2 | Copyright (c) 2009-2013 Elgato Systems GmbH. 3 | Copyright (c) 2011-2014 Kornel Lesiński. 4 | Copyright (c) 2015-2017 Mayur Pawashe. 5 | Copyright (c) 2014 C.W. Betts. 6 | Copyright (c) 2014 Petroules Corporation. 7 | Copyright (c) 2014 Big Nerd Ranch. 8 | All rights reserved. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | this software and associated documentation files (the "Software"), to deal in 12 | the Software without restriction, including without limitation the rights to 13 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | the Software, and to permit persons to whom the Software is furnished to do so, 15 | subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 23 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 24 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | ================= 28 | EXTERNAL LICENSES 29 | ================= 30 | 31 | bspatch.c and bsdiff.c, from bsdiff 4.3 : 32 | 33 | Copyright 2003-2005 Colin Percival 34 | All rights reserved 35 | 36 | Redistribution and use in source and binary forms, with or without 37 | modification, are permitted providing that the following conditions 38 | are met: 39 | 1. Redistributions of source code must retain the above copyright 40 | notice, this list of conditions and the following disclaimer. 41 | 2. Redistributions in binary form must reproduce the above copyright 42 | notice, this list of conditions and the following disclaimer in the 43 | documentation and/or other materials provided with the distribution. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 46 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 47 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 48 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 49 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 50 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 51 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 52 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 53 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 54 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 55 | POSSIBILITY OF SUCH DAMAGE. 56 | 57 | -- 58 | 59 | sais.c and sais.c, from sais-lite (2010/08/07) : 60 | 61 | The sais-lite copyright is as follows: 62 | 63 | Copyright (c) 2008-2010 Yuta Mori All Rights Reserved. 64 | 65 | Permission is hereby granted, free of charge, to any person 66 | obtaining a copy of this software and associated documentation 67 | files (the "Software"), to deal in the Software without 68 | restriction, including without limitation the rights to use, 69 | copy, modify, merge, publish, distribute, sublicense, and/or sell 70 | copies of the Software, and to permit persons to whom the 71 | Software is furnished to do so, subject to the following 72 | conditions: 73 | 74 | The above copyright notice and this permission notice shall be 75 | included in all copies or substantial portions of the Software. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 78 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 79 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 80 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 81 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 82 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 83 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 84 | OTHER DEALINGS IN THE SOFTWARE. 85 | 86 | -- 87 | 88 | Portable C implementation of Ed25519, from https://github.com/orlp/ed25519 89 | 90 | Copyright (c) 2015 Orson Peters 91 | 92 | This software is provided 'as-is', without any express or implied warranty. In no event will the 93 | authors be held liable for any damages arising from the use of this software. 94 | 95 | Permission is granted to anyone to use this software for any purpose, including commercial 96 | applications, and to alter it and redistribute it freely, subject to the following restrictions: 97 | 98 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the 99 | original software. If you use this software in a product, an acknowledgment in the product 100 | documentation would be appreciated but is not required. 101 | 102 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as 103 | being the original software. 104 | 105 | 3. This notice may not be removed or altered from any source distribution. 106 | 107 | -- 108 | 109 | SUSignatureVerifier.m: 110 | 111 | Copyright (c) 2011 Mark Hamlin. 112 | 113 | All rights reserved. 114 | 115 | Redistribution and use in source and binary forms, with or without 116 | modification, are permitted providing that the following conditions 117 | are met: 118 | 1. Redistributions of source code must retain the above copyright 119 | notice, this list of conditions and the following disclaimer. 120 | 2. Redistributions in binary form must reproduce the above copyright 121 | notice, this list of conditions and the following disclaimer in the 122 | documentation and/or other materials provided with the distribution. 123 | 124 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 125 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 126 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 127 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 128 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 129 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 130 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 131 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 132 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 133 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 134 | POSSIBILITY OF SUCH DAMAGE. 135 | -------------------------------------------------------------------------------- /Reconnect/Model/ApplicationModel.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | import Sparkle 23 | 24 | @MainActor @Observable 25 | class ApplicationModel: NSObject { 26 | 27 | struct SerialDevice: Identifiable { 28 | 29 | var id: String { 30 | return path 31 | } 32 | 33 | var path: String 34 | var available: Bool 35 | var enabled: Binding 36 | } 37 | 38 | enum SettingsKey: String { 39 | case selectedDevices 40 | case convertFiles 41 | } 42 | 43 | var convertFiles: Bool { 44 | didSet { 45 | keyedDefaults.set(convertFiles, forKey: .convertFiles) 46 | } 47 | } 48 | 49 | let updaterController = SPUStandardUpdaterController(startingUpdater: false, 50 | updaterDelegate: nil, 51 | userDriverDelegate: nil) 52 | 53 | private let keyedDefaults = KeyedDefaults() 54 | 55 | override init() { 56 | convertFiles = keyedDefaults.bool(forKey: .convertFiles, default: true) 57 | super.init() 58 | openMenuApplication() 59 | updaterController.startUpdater() 60 | } 61 | 62 | func openMenuApplication() { 63 | guard let embeddedAppURL = Bundle.main.url(forResource: "Reconnect Menu", withExtension: "app") else { 64 | return 65 | } 66 | let openConfiguraiton = NSWorkspace.OpenConfiguration() 67 | NSWorkspace.shared.openApplication(at: embeddedAppURL, configuration: openConfiguraiton) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Reconnect/Model/CheckForUpdatesViewModel.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Sparkle 22 | 23 | // This view model class publishes when new updates can be checked by the user 24 | final class CheckForUpdatesViewModel: ObservableObject { 25 | @Published var canCheckForUpdates = false 26 | 27 | init(updater: SPUUpdater) { 28 | updater.publisher(for: \.canCheckForUpdates) 29 | .assign(to: &$canCheckForUpdates) 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Reconnect/Model/EditableTextModel.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Combine 20 | import SwiftUI 21 | 22 | import Interact 23 | 24 | class EditableTextModel: ObservableObject, Runnable { 25 | 26 | @Published var text: String = "" 27 | 28 | private let initialValue: String 29 | private let completion: (String) -> Void 30 | 31 | private var cancellables: Set = [] 32 | 33 | init(initialValue: String, completion: @escaping (String) -> Void) { 34 | self.initialValue = initialValue 35 | self.completion = completion 36 | self.text = initialValue 37 | } 38 | 39 | func start() { 40 | $text 41 | .debounce(for: 1.0, scheduler: DispatchQueue.main) 42 | .sink { text in 43 | dispatchPrecondition(condition: .onQueue(.main)) 44 | guard text != self.initialValue else { 45 | return 46 | } 47 | self.completion(text) 48 | } 49 | .store(in: &cancellables) 50 | } 51 | 52 | func stop() { 53 | cancellables.removeAll() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Reconnect/Model/FileConverter.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import OpoLua 22 | 23 | import ReconnectCore 24 | 25 | // This is expected to grow into some kind of engine / model for managing file conversions and giving in the moment 26 | // answers about conversions based on the users choices and enabled conversions. 27 | class FileConverter { 28 | 29 | struct Conversion { 30 | let matches: (FileServer.DirectoryEntry) -> Bool 31 | let filename: (FileServer.DirectoryEntry) -> String 32 | let perform: (URL, URL) throws -> URL 33 | } 34 | 35 | static let converters: [Conversion] = [ 36 | 37 | // MBM 38 | .init { directoryEntry in 39 | return directoryEntry.fileType == .mbm || directoryEntry.pathExtension.lowercased() == "mbm" 40 | } filename: { directoryEntry in 41 | return directoryEntry.name 42 | .deletingPathExtension 43 | .appendingPathExtension("tiff")! 44 | } perform: { sourceURL, destinationURL in 45 | // TODO: Generate a temporary file? Should this be done in the outer? 46 | try PsiLuaEnv().convertMultiBitmap(at: sourceURL, to: destinationURL) 47 | try FileManager.default.removeItem(at: sourceURL) 48 | return destinationURL // TODO: this is uuuugly 49 | } 50 | 51 | ] 52 | 53 | static func converter(for directoryEntry: FileServer.DirectoryEntry) -> Conversion? { 54 | return converters.first { 55 | $0.matches(directoryEntry) 56 | } 57 | } 58 | 59 | static func targetFilename(for directoryEntry: FileServer.DirectoryEntry) -> String { 60 | return converter(for: directoryEntry)?.filename(directoryEntry) ?? directoryEntry.name 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Reconnect/Model/FileReference.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import ReconnectCore 22 | 23 | enum FileReference: Equatable { 24 | 25 | case local(URL) 26 | case remote(FileServer.DirectoryEntry) 27 | 28 | } 29 | 30 | extension FileReference { 31 | 32 | var name: String { 33 | switch self { 34 | case .local(let url): 35 | return url.lastPathComponent 36 | case .remote(let directoryEntry): 37 | return directoryEntry.name 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Reconnect/Model/Transfer.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | @MainActor @Observable 22 | class Transfer: Identifiable { 23 | 24 | struct FileDetails: Equatable { 25 | let reference: FileReference 26 | let size: UInt64 27 | } 28 | 29 | enum Status: Equatable { 30 | 31 | static func == (lhs: Transfer.Status, rhs: Transfer.Status) -> Bool { 32 | switch lhs { 33 | case .waiting: 34 | if case .waiting = rhs { 35 | return true 36 | } 37 | return false 38 | case .active(let lhsProgress, let lhsSize): 39 | if case let .active(rhsProgress, rhsSize) = rhs { 40 | return lhsProgress == rhsProgress && lhsSize == rhsSize 41 | } 42 | return false 43 | case .complete(let lhsDetails): 44 | if case let .complete(rhsDetails) = rhs { 45 | return lhsDetails == rhsDetails 46 | } 47 | return false 48 | case .cancelled: 49 | if case .complete = rhs { 50 | return true 51 | } 52 | return false 53 | case .failed(_): 54 | if case .failed = rhs { 55 | return true 56 | } 57 | return false 58 | } 59 | } 60 | 61 | case waiting 62 | case active(UInt32, UInt32) 63 | case complete(FileDetails?) 64 | case cancelled 65 | case failed(Error) 66 | } 67 | 68 | var isCancelled: Bool { 69 | return lock.withLock { 70 | return _isCancelled 71 | } 72 | } 73 | 74 | let id = UUID() 75 | let item: FileReference 76 | let action: (Transfer) async throws -> FileReference 77 | 78 | var status: Status 79 | 80 | private var task: Task? = nil 81 | private var lock = NSLock() 82 | private var _isCancelled: Bool = false 83 | 84 | var isActive: Bool { 85 | switch status { 86 | case .waiting, .active: 87 | return true 88 | case .complete, .cancelled, .failed: 89 | return false 90 | } 91 | } 92 | 93 | init(item: FileReference, 94 | status: Status = .waiting, 95 | action: @escaping ((Transfer) async throws -> FileReference)) { 96 | self.item = item 97 | self.status = status 98 | self.action = action 99 | } 100 | 101 | func run() async throws -> FileReference { 102 | let task = Task { 103 | do { 104 | return try await action(self) 105 | } catch { 106 | print("Failed with error \(error).") 107 | self.setStatus(.failed(error)) 108 | throw error 109 | } 110 | } 111 | self.task = task 112 | return try await task.value 113 | } 114 | 115 | func setStatus(_ status: Status) { 116 | DispatchQueue.main.async { 117 | self.status = status 118 | } 119 | } 120 | 121 | func cancel() { 122 | task?.cancel() 123 | lock.withLock { 124 | _isCancelled = true 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /Reconnect/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reconnect/Reconnect-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "ncpd.h" 6 | -------------------------------------------------------------------------------- /Reconnect/Reconnect.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.serial 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reconnect/ReconnectApp.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Diligence 22 | 23 | @main @MainActor 24 | struct ReconnectApp: App { 25 | 26 | static let title = "Reconnect Support (\(Bundle.main.extendedVersion ?? "Unknown Version"))" 27 | 28 | @State var transfersModel = TransfersModel() 29 | @State var applicationModel = ApplicationModel() 30 | 31 | var body: some Scene { 32 | 33 | BrowserWindow(applicationModel: applicationModel, transfersModel: transfersModel) 34 | 35 | TransfersWindow() 36 | .environment(applicationModel) 37 | .environment(transfersModel) 38 | 39 | About(repository: "inseven/reconnect", copyright: "Copyright © 2024-2025 Jason Morley") { 40 | Action("GitHub", url: .gitHub) 41 | Action("Discord", url: .discord) 42 | Action("Support", url: .support) 43 | } acknowledgements: { 44 | Acknowledgements("Developers") { 45 | Credit("Jason Morley", url: URL(string: "https://jbmorley.co.uk")) 46 | } 47 | Acknowledgements("Thanks") { 48 | Credit("Alex Brown") 49 | Credit("Fabrice Cappaert") 50 | Credit("George Wright") 51 | Credit("Lukas Fittl") 52 | Credit("Sarah Barbour") 53 | Credit("Tom Sutcliffe") 54 | } 55 | } licenses: { 56 | (.reconnect) 57 | } 58 | .handlesExternalEvents(matching: [.about]) 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Reconnect/Utilities/NavigationStack.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | struct NavigationStack { 22 | 23 | struct Item: Identifiable, Equatable { 24 | let id = UUID() 25 | let path: String 26 | } 27 | 28 | var path: String? { 29 | guard let index else { 30 | return nil 31 | } 32 | return items[index].path 33 | } 34 | 35 | var previousItems: [Item] { 36 | guard let index else { 37 | return [] 38 | } 39 | return Array(items[0.. Bool { 68 | guard let index else { 69 | return false 70 | } 71 | return index > 0 72 | } 73 | 74 | func canGoForward() -> Bool { 75 | guard let index else { 76 | return false 77 | } 78 | return index < items.count - 1 79 | } 80 | 81 | mutating func navigate(_ path: String) { 82 | guard let index else { 83 | assert(items.count == 0) 84 | index = 0 85 | self.items = [Item(path: path)] 86 | return 87 | } 88 | assert(index < items.count) 89 | self.items = items[0...index] + [Item(path: path)] 90 | self.index = index + 1 91 | } 92 | 93 | mutating func navigate(_ item: Item) { 94 | guard let index = items.firstIndex(where: { $0 == item }) else { 95 | return 96 | } 97 | self.index = index 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Reconnect/Views/BrowserDetailView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import ReconnectCore 22 | 23 | struct BrowserDetailView: View { 24 | 25 | @Environment(ApplicationModel.self) var applicationModel 26 | 27 | @State var isTargeted = false 28 | 29 | var browserModel: BrowserModel 30 | 31 | func itemProvider(for file: FileServer.DirectoryEntry) -> NSItemProvider? { 32 | if file.isDirectory { 33 | return nil 34 | } else { 35 | let provider = NSItemProvider() 36 | provider.suggestedName = FileConverter.targetFilename(for: file) 37 | provider.registerFileRepresentation(for: .data) { completion in 38 | Task { 39 | do { 40 | let url = try await self.browserModel.download(file.id, convertFiles: true) 41 | completion(url, false, nil) 42 | } catch { 43 | print("Failed to download dragged file with error \(error).") 44 | completion(nil, false, error) 45 | } 46 | } 47 | return nil 48 | } 49 | return provider 50 | } 51 | } 52 | 53 | var body: some View { 54 | @Bindable var browserModel = browserModel 55 | ZStack { 56 | Table(of: FileServer.DirectoryEntry.self, selection: $browserModel.fileSelection) { 57 | TableColumn("") { file in 58 | Image(file.fileType.image) 59 | } 60 | .width(16.0) 61 | TableColumn("Name") { file in 62 | EditableText(initialValue: file.name) { text in 63 | browserModel.rename(file: file, to: text) 64 | } 65 | } 66 | TableColumn("Date Modified") { file in 67 | Text(file.modificationDate.formatted(date: .long, time: .shortened)) 68 | .foregroundStyle(.secondary) 69 | } 70 | TableColumn("Size") { file in 71 | if file.isDirectory { 72 | Text("--") 73 | .foregroundStyle(.secondary) 74 | } else { 75 | Text(file.size.formatted(.byteCount(style: .file))) 76 | .foregroundStyle(.secondary) 77 | } 78 | } 79 | TableColumn("Type") { file in 80 | FileTypePopover(file: file) 81 | .foregroundStyle(.secondary) 82 | } 83 | } rows: { 84 | ForEach(browserModel.files) { file in 85 | TableRow(file) 86 | .itemProvider { 87 | itemProvider(for: file) 88 | } 89 | } 90 | } 91 | .contextMenu(forSelectionType: FileServer.DirectoryEntry.ID.self) { items in 92 | 93 | Button("Open") { 94 | browserModel.navigate(to: items.first!) 95 | } 96 | .disabled(items.count != 1 || !(items.first?.isWindowsDirectory ?? false)) 97 | 98 | Divider() 99 | 100 | Button("Download") { 101 | browserModel.download(items, 102 | to: FileManager.default.downloadsDirectory, 103 | convertFiles: applicationModel.convertFiles) 104 | } 105 | 106 | Divider() 107 | 108 | Button("Delete") { 109 | browserModel.delete(items) 110 | } 111 | 112 | } primaryAction: { items in 113 | guard 114 | items.count == 1, 115 | let item = items.first, 116 | item.isWindowsDirectory 117 | else { 118 | return 119 | } 120 | browserModel.navigate(to: item) 121 | } 122 | .onDeleteCommand { 123 | browserModel.delete() 124 | } 125 | .contextMenu { 126 | Button("New Folder") { 127 | browserModel.newFolder() 128 | } 129 | } 130 | if isTargeted { 131 | Rectangle() 132 | .stroke(.blue, lineWidth: 4) 133 | } 134 | } 135 | .onDrop(of: [.fileURL], isTargeted: $isTargeted) { providers in 136 | for provider in providers { 137 | _ = provider.loadObject(ofClass: URL.self) { url, _ in 138 | guard let url = url else { 139 | return 140 | } 141 | DispatchQueue.main.sync { 142 | browserModel.upload(url: url) 143 | } 144 | } 145 | } 146 | return true 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /Reconnect/Views/BrowserView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import PsionSoftwareIndex 22 | 23 | @MainActor 24 | struct BrowserView: View { 25 | 26 | enum SheetType: Identifiable { 27 | 28 | var id: Self { 29 | return self 30 | } 31 | 32 | case install 33 | } 34 | 35 | @Environment(\.openWindow) private var openWindow 36 | 37 | @Environment(ApplicationModel.self) private var applicationModel 38 | 39 | @State private var sheet: SheetType? = nil 40 | 41 | private var browserModel: BrowserModel 42 | 43 | init(browserModel: BrowserModel) { 44 | self.browserModel = browserModel 45 | } 46 | 47 | var body: some View { 48 | 49 | @Bindable var browserModel = browserModel 50 | 51 | NavigationSplitView { 52 | Sidebar(model: browserModel) 53 | } detail: { 54 | BrowserDetailView(browserModel: browserModel) 55 | } 56 | .toolbar(id: "main") { 57 | ToolbarItem(id: "navigation", placement: .navigation) { 58 | HStack(spacing: 8) { 59 | 60 | Menu { 61 | ForEach(browserModel.previousItems) { item in 62 | Button { 63 | browserModel.navigate(to: item) 64 | } label: { 65 | HistoryItemView(item: item) 66 | } 67 | } 68 | } label: { 69 | Label("Back", systemImage: "chevron.backward") 70 | } primaryAction: { 71 | browserModel.back() 72 | } 73 | .menuIndicator(.hidden) 74 | .disabled(!browserModel.canGoBack()) 75 | 76 | Menu { 77 | ForEach(browserModel.nextItems) { item in 78 | Button { 79 | browserModel.navigate(to: item) 80 | } label: { 81 | HistoryItemView(item: item) 82 | } 83 | } 84 | } label: { 85 | Label("Forward", systemImage: "chevron.forward") 86 | } primaryAction: { 87 | browserModel.forward() 88 | } 89 | .menuIndicator(.hidden) 90 | .disabled(!browserModel.canGoForward()) 91 | 92 | } 93 | .help("See folders you viewed previously") 94 | } 95 | 96 | ToolbarItem(id: "new-folder") { 97 | Button { 98 | browserModel.newFolder() 99 | } label: { 100 | Label("New Folder", systemImage: "folder.badge.plus") 101 | } 102 | } 103 | 104 | ToolbarItem(id: "download") { 105 | Button { 106 | browserModel.download(to: FileManager.default.downloadsDirectory, 107 | convertFiles: applicationModel.convertFiles) 108 | } label: { 109 | Label("New Folder", systemImage: "square.and.arrow.down") 110 | } 111 | .disabled(browserModel.isSelectionEmpty) 112 | } 113 | 114 | ToolbarItem(id: "delete") { 115 | Button { 116 | browserModel.delete() 117 | } label: { 118 | Label("Delete", systemImage: "trash") 119 | } 120 | .disabled(browserModel.isSelectionEmpty) 121 | } 122 | 123 | ToolbarItem(id: "action") { 124 | Menu { 125 | 126 | Button("New Folder") { 127 | browserModel.newFolder() 128 | } 129 | 130 | Divider() 131 | 132 | Button("Download") { 133 | browserModel.download(convertFiles: applicationModel.convertFiles) 134 | } 135 | .disabled(browserModel.isSelectionEmpty) 136 | 137 | Divider() 138 | 139 | Button("Delete") { 140 | browserModel.delete() 141 | } 142 | .disabled(browserModel.isSelectionEmpty) 143 | 144 | } label: { 145 | Label("Action", systemImage: "ellipsis.circle") 146 | } 147 | } 148 | 149 | ToolbarItem(id: "spacer") { 150 | Spacer() 151 | } 152 | 153 | ToolbarItem(id: "refresh") { 154 | Button { 155 | browserModel.refresh() 156 | } label: { 157 | Label("Refresh", systemImage: "arrow.clockwise") 158 | } 159 | } 160 | 161 | ToolbarItem(id: "spacer") { 162 | Spacer() 163 | } 164 | 165 | ToolbarItem(id: "add") { 166 | Button { 167 | sheet = .install 168 | } label: { 169 | Label("Add", systemImage: "plus") 170 | } 171 | } 172 | 173 | } 174 | .navigationTitle(browserModel.navigationTitle ?? "My Psion") 175 | .sheet(item: $sheet) { sheet in 176 | switch sheet { 177 | case .install: 178 | SoftwareIndexView { release in 179 | return release.kind == .installer && release.hasDownload /* && release.tags.contains("opl")*/ 180 | } completion: { url in 181 | guard let url else { 182 | return 183 | } 184 | browserModel.upload(url: url) 185 | } 186 | } 187 | } 188 | .presents($browserModel.lastError) 189 | .onAppear { 190 | browserModel.navigate(to: "C:\\") 191 | } 192 | .task { 193 | await browserModel.start() 194 | } 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /Reconnect/Views/CheckForUpdatesView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Sparkle 22 | 23 | // This is the view for the Check for Updates menu item 24 | // Note this intermediate view is necessary for the disabled state on the menu item to work properly before Monterey. 25 | // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more info 26 | struct CheckForUpdatesView: View { 27 | @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel 28 | private let updater: SPUUpdater 29 | 30 | init(updater: SPUUpdater) { 31 | self.updater = updater 32 | 33 | // Create our view model for our CheckForUpdatesView 34 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) 35 | } 36 | 37 | var body: some View { 38 | Button("Check for Updates…", action: updater.checkForUpdates) 39 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Reconnect/Views/EditableText.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | // This exists as a fairly gnarly workaround to turn SwiftUI's continuous table view text field editing back into 22 | // something that looks vaguely modal. It's possible we'd also get this for free by writing using an NSTextField 23 | // directly, but that's for another day; until then, debouncing edits will have to be sufficient. 24 | struct EditableText: View { 25 | 26 | @StateObject var model: EditableTextModel 27 | 28 | init(initialValue: String, completion: @escaping (String) -> Void) { 29 | _model = StateObject(wrappedValue: EditableTextModel(initialValue: initialValue, completion: completion)) 30 | } 31 | 32 | var body: some View { 33 | TextField("", text: $model.text) 34 | .runs(model) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Reconnect/Views/FileTypePopover.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import ReconnectCore 22 | 23 | struct FileTypePopover: View { 24 | 25 | @State var isPresented: Bool = false 26 | 27 | let file: FileServer.DirectoryEntry 28 | 29 | var body: some View { 30 | Button { 31 | isPresented = true 32 | } label: { 33 | Text(file.fileType.name) 34 | } 35 | .popover(isPresented: $isPresented) { 36 | Grid { 37 | GridRow { 38 | Text("UID1") 39 | Text(String(format: "0x%08X", file.uid1)) 40 | } 41 | GridRow { 42 | Text("UID2") 43 | Text(String(format: "0x%08X", file.uid2)) 44 | } 45 | GridRow { 46 | Text("UID3") 47 | Text(String(format: "0x%08X", file.uid3)) 48 | } 49 | } 50 | .padding() 51 | .textSelection(.enabled) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Reconnect/Views/HistoryItemView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | struct HistoryItemView: View { 22 | 23 | @Environment(BrowserModel.self) var browserModel 24 | 25 | let item: NavigationStack.Item 26 | 27 | var body: some View { 28 | HStack { 29 | Image(browserModel.image(for: item.path)) 30 | Text(browserModel.name(for: item.path) ?? "Unknown") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Reconnect/Views/PixelImage.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | struct PixelImage: View { 24 | 25 | enum Source { 26 | case name(String) 27 | case resource(ImageResource) 28 | } 29 | 30 | let source: Source 31 | 32 | init(_ name: String) { 33 | self.source = .name(name) 34 | } 35 | 36 | init(_ resource: ImageResource) { 37 | self.source = .resource(resource) 38 | } 39 | 40 | var image: Image { 41 | switch source { 42 | case .name(let string): 43 | Image(string) 44 | case .resource(let resource): 45 | Image(resource) 46 | } 47 | } 48 | 49 | var body: some View { 50 | image 51 | .interpolation(.none) 52 | .resizable() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Reconnect/Views/Sidebar.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | struct Sidebar: View { 22 | 23 | var model: BrowserModel 24 | 25 | var body: some View { 26 | @Bindable var model = model 27 | List(selection: $model.driveSelection) { 28 | Section("Drives") { 29 | ForEach(model.drives) { driveInfo in 30 | Label { 31 | Text(driveInfo.displayName) 32 | } icon: { 33 | Image(driveInfo.image) 34 | } 35 | } 36 | } 37 | } 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Reconnect/Views/ThumbnailView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | import QuickLookThumbnailing 21 | 22 | import Interact 23 | 24 | @MainActor 25 | struct ThumbnailView: View { 26 | 27 | let url: URL 28 | let size: CGSize 29 | 30 | @State var image: NSImage? = nil 31 | 32 | var body: some View { 33 | Image(nsImage: image ?? NSWorkspace.shared.icon(forFile: url.path)) 34 | .resizable() 35 | .task { 36 | let thumbnail = try? await QLThumbnailGenerator.shared.thumbnailRepresentation(fileAt: url, 37 | size: size, 38 | scale: 2.0, 39 | iconMode: true) 40 | image = thumbnail?.nsImage 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Reconnect/Views/TransferRow.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | struct TransferRow: View { 24 | 25 | struct LayoutMetrics { 26 | static let iconSize = 32.0 27 | static let horizontalSpacing = 16.0 28 | } 29 | 30 | let transfer: Transfer 31 | 32 | var image: some View { 33 | // We differentiate between complete and incomplete transfers to allow us to show thumbnails that correspond 34 | // with the final state of the tranfer---downloaded files will show their converted thumbnails where 35 | // appropriate, etc. 36 | VStack { 37 | switch transfer.status { 38 | case .complete(let details): 39 | switch transfer.item { 40 | case .local: 41 | PixelImage(.fileUnknown16) 42 | case .remote(let file): 43 | if let details { 44 | switch details.reference { 45 | case .local(let url): 46 | ThumbnailView(url: url, 47 | size: CGSize(width: LayoutMetrics.iconSize, height: LayoutMetrics.iconSize)) 48 | case .remote(let directoryEntry): 49 | PixelImage(directoryEntry.fileType.image) 50 | } 51 | } else { 52 | PixelImage(file.fileType.image) 53 | } 54 | } 55 | default: 56 | switch transfer.item { 57 | case .local: 58 | PixelImage(.fileUnknown16) 59 | case .remote(let file): 60 | PixelImage(file.fileType.image) 61 | } 62 | } 63 | } 64 | .frame(width: LayoutMetrics.iconSize, height: LayoutMetrics.iconSize) 65 | } 66 | 67 | var statusText: String { 68 | switch transfer.status { 69 | case .waiting: 70 | return "Waiting to start…" 71 | case .active(let progress, let size): 72 | return "\(progress.formatted(.byteCount(style: .memory))) of \(size.formatted(.byteCount(style: .memory)))" 73 | case .complete(let details): 74 | if let details { 75 | return details.size.formatted(.byteCount(style: .file)) 76 | } else { 77 | return "Complete" 78 | } 79 | case .cancelled: 80 | return "Cancelled" 81 | case .failed(let error): 82 | return error.localizedDescription 83 | } 84 | 85 | } 86 | 87 | var body: some View { 88 | HStack(spacing: LayoutMetrics.horizontalSpacing) { 89 | 90 | self.image 91 | 92 | VStack(alignment: .leading, spacing: 0) { 93 | 94 | Text(transfer.item.name) 95 | .lineLimit(1) 96 | .truncationMode(.middle) 97 | .horizontalSpace(.trailing) 98 | 99 | switch transfer.status { 100 | case .waiting: 101 | ProgressView(value: 0) 102 | .controlSize(.small) 103 | case .active(let progress, let size): 104 | ProgressView(value: Float(progress) / Float(size)) 105 | .controlSize(.small) 106 | case .complete, .cancelled, .failed: 107 | EmptyView() 108 | } 109 | 110 | Text(statusText) 111 | .lineLimit(1) 112 | .foregroundStyle(.secondary) 113 | .font(.callout) 114 | .help(statusText) 115 | 116 | } 117 | 118 | switch transfer.status { 119 | case .waiting, .active: 120 | Button { 121 | transfer.cancel() 122 | } label: { 123 | Image(systemName: "xmark.circle.fill") 124 | .foregroundColor(.secondary) 125 | } 126 | .buttonStyle(.plain) 127 | case .complete(let details): 128 | if let details { 129 | Button { 130 | switch details.reference { 131 | case .local(let url): 132 | Application.reveal(url) 133 | case .remote(let directoryEntry): 134 | print("Revealing remote files is not currently supported!") 135 | } 136 | } label: { 137 | Image(systemName: "magnifyingglass.circle.fill") 138 | } 139 | .foregroundColor(.secondary) 140 | .buttonStyle(.plain) 141 | } else { 142 | Image(systemName: "checkmark.circle.fill") 143 | .foregroundStyle(.green) 144 | } 145 | case .cancelled, .failed: 146 | Image(systemName: "exclamationmark.circle.fill") 147 | .foregroundStyle(.red) 148 | } 149 | 150 | } 151 | .padding() 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Reconnect/Views/TransfersView.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | struct TransfersView: View { 22 | 23 | private struct LayoutMetrics { 24 | static let width = 360.0 25 | static let minimumHeight = 300.0 26 | static let footerPadding = 8.0 27 | } 28 | 29 | @Environment(\.dismiss) private var dismiss 30 | 31 | @Environment(ApplicationModel.self) private var applicationModel 32 | 33 | private let transfersModel: TransfersModel 34 | 35 | init(transfersModel: TransfersModel) { 36 | self.transfersModel = transfersModel 37 | } 38 | 39 | var body: some View { 40 | @Bindable var applicationModel = applicationModel 41 | @Bindable var transfers = transfersModel 42 | List(selection: $transfers.selection) { 43 | ForEach(transfers.transfers) { transfer in 44 | TransferRow(transfer: transfer) 45 | } 46 | } 47 | .scrollContentBackground(.hidden) 48 | .safeAreaInset(edge: .bottom) { 49 | VStack(spacing: 0) { 50 | Divider() 51 | HStack { 52 | Toggle("Convert Files", isOn: $applicationModel.convertFiles) 53 | 54 | Spacer() 55 | 56 | #if DEBUG 57 | Button("Add Demo Data") { 58 | transfersModel.addDemoData() 59 | } 60 | #endif 61 | 62 | Button("Clear") { 63 | transfersModel.clear() 64 | if transfersModel.transfers.isEmpty { 65 | dismiss() 66 | } 67 | } 68 | .disabled(transfersModel.transfers.isEmpty) 69 | } 70 | .padding(LayoutMetrics.footerPadding) 71 | } 72 | .background(.regularMaterial) 73 | } 74 | .frame(minHeight: LayoutMetrics.minimumHeight) 75 | .frame(width: LayoutMetrics.width) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Reconnect/Windows/BrowserWindow.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | struct BrowserWindow: Scene { 24 | 25 | static let id = "browser" 26 | 27 | @State private var browserModel: BrowserModel 28 | 29 | private let applicationModel: ApplicationModel 30 | private let transfersModel: TransfersModel 31 | 32 | init(applicationModel: ApplicationModel, transfersModel: TransfersModel) { 33 | self.applicationModel = applicationModel 34 | self.transfersModel = transfersModel 35 | _browserModel = State(initialValue: BrowserModel(transfersModel: transfersModel)) 36 | } 37 | 38 | var body: some Scene { 39 | Window("My Psion", id: "browser") { 40 | BrowserView(browserModel: browserModel) 41 | .onOpenURL { url in 42 | guard url == .update else { 43 | print("Unsupported URL \(url).") 44 | return 45 | } 46 | applicationModel.updaterController.updater.checkForUpdates() 47 | } 48 | .handlesExternalEvents(preferring: [.install], allowing: []) 49 | } 50 | .commands { 51 | SparkleCommands(applicationModel: applicationModel) 52 | HelpCommands() 53 | BrowserCommands(browserModel: browserModel) 54 | SidebarCommands() 55 | ToolbarCommands() 56 | } 57 | .environment(applicationModel) 58 | .environment(transfersModel) 59 | .environment(browserModel) 60 | .handlesExternalEvents(matching: [.browser, .update]) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Reconnect/Windows/TransfersWindow.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | struct TransfersWindow: Scene { 24 | 25 | static let id = "transfers" 26 | 27 | @Environment(TransfersModel.self) private var transfersModel 28 | 29 | var body: some Scene { 30 | Window("Transfers", id: Self.id) { 31 | TransfersView(transfersModel: transfersModel) 32 | .onOpenURL { url in 33 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), 34 | let path = components.queryItems?.first(where: { $0.name == "path" })?.value, 35 | let installerURL = URL(string: path), 36 | installerURL.scheme == "file" 37 | else { 38 | return 39 | } 40 | let filename = installerURL.lastPathComponent 41 | Task { 42 | try? await transfersModel.upload(from: installerURL, to: "C:".appendingWindowsPathComponent(filename)) 43 | } 44 | } 45 | .handlesExternalEvents(preferring: [.install], allowing: []) 46 | } 47 | .windowResizability(.contentSize) 48 | .handlesExternalEvents(matching: [.install, .transfers]) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /ReconnectCore/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ReconnectCore/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ReconnectCore", 8 | platforms: [ 9 | .macOS(.v13), 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, making them visible to other packages. 13 | .library( 14 | name: "ReconnectCore", 15 | targets: ["ReconnectCore"]), 16 | ], 17 | dependencies: [ 18 | .package(path: "../dependencies/diligence"), 19 | .package(path: "../dependencies/opolua"), 20 | .package(path: "../dependencies/plptools"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package, defining a module or a test suite. 24 | // Targets can depend on other targets in this package and products from dependencies. 25 | .target( 26 | name: "ReconnectCore", 27 | dependencies: [ 28 | .product(name: "Diligence", package: "diligence"), 29 | .product(name: "OpoLua", package: "opolua"), 30 | .product(name: "ncp", package: "plptools"), 31 | .product(name: "plpftp", package: "plptools"), 32 | ], 33 | swiftSettings: [ 34 | .interoperabilityMode(.Cxx) 35 | ] 36 | ), 37 | .testTarget( 38 | name: "ReconnectCoreTests", 39 | dependencies: ["ReconnectCore"]), 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/Array.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | public extension Array { 22 | 23 | func appending(_ element: Element) -> [Element] { 24 | return self + [element] 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/DirectoryEntry.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | extension FileServer.DirectoryEntry { 22 | 23 | public var fileType: FileType { 24 | if isDirectory { 25 | return .directory 26 | } else { 27 | switch (uid1, uid2, uid3) { 28 | case (.directFileStore, .appDllDoc, .word): 29 | return .word 30 | case (.directFileStore, .appDllDoc, .sheet): 31 | return .sheet 32 | case (.directFileStore, .appDllDoc, .record): 33 | return .record 34 | case (.directFileStore, .appDllDoc, .opl): 35 | return .opl 36 | case (.permanentFileStoreLayout, .appDllDoc, .data): 37 | return .data 38 | case (.permanentFileStoreLayout, .appDllDoc, .agenda): 39 | return .agenda 40 | case (.directFileStore, .appDllDoc, .sketch): 41 | return .sketch 42 | case (.permanentFileStoreLayout, .appDllDoc, .jotter): 43 | return .jotter 44 | case (.directFileStore, .mbm, .none), (.multiBitmapRomImage, .none, .none): 45 | return .mbm 46 | default: 47 | return .unknown 48 | } 49 | } 50 | } 51 | 52 | public var pathExtension: String { 53 | return (self.name as NSString).pathExtension 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/DriveInfo.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | extension FileServer.DriveInfo { 22 | 23 | public var displayName: String { 24 | if let name { 25 | return name 26 | } else { 27 | return "\(drive):" 28 | } 29 | } 30 | 31 | public var image: String { 32 | switch mediaType { 33 | case .notPresent: 34 | return "Drive16" 35 | case .unknown: 36 | return "Drive16" 37 | case .floppy: 38 | return "Drive16" 39 | case .disk: 40 | return "Disk16" 41 | case .compactDisc: 42 | return "Drive16" 43 | case .ram: 44 | return "Drive16" 45 | case .flashDisk: 46 | return "Drive16" 47 | case .rom: 48 | return "Drive16" 49 | case .remote: 50 | return "Drive16" 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/FileManager.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | extension FileManager { 22 | 23 | public var downloadsDirectory: URL { 24 | return urls(for: .downloadsDirectory, in: .userDomainMask)[0] 25 | } 26 | 27 | public func temporaryURL() -> URL { 28 | return temporaryDirectory.appendingPathComponent((UUID().uuidString)) 29 | } 30 | 31 | public func createTemporaryDirectory() throws -> URL { 32 | let temporaryURL = temporaryURL() 33 | try createDirectory(at: temporaryURL, withIntermediateDirectories: true) 34 | return temporaryURL 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/PsiLuaEnv.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import CoreGraphics 20 | import Foundation 21 | 22 | import OpoLua 23 | 24 | extension PsiLuaEnv { 25 | 26 | // TODO: Rename source and destination 27 | public func convertMultiBitmap(at url: URL, to: URL) throws { 28 | let bitmaps = PsiLuaEnv().getMbmBitmaps(path: url.path) ?? [] 29 | let images = bitmaps.map { bitmap in 30 | return CGImage.from(bitmap: bitmap) 31 | } 32 | try CGImageWriteTIFF(destinationURL: to, images: images) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | public extension String { 22 | 23 | static let windowsPathSeparator = "\\" 24 | 25 | var deletingLastWindowsPathComponent: String { 26 | return windowsPathComponents 27 | .dropLast() 28 | .joined(separator: .windowsPathSeparator) 29 | } 30 | 31 | var isRoot: Bool { 32 | return windowsPathComponents.count == 1 33 | } 34 | 35 | var isWindowsDirectory: Bool { 36 | return hasSuffix(.windowsPathSeparator) 37 | } 38 | 39 | var lastWindowsPathComponent: String { 40 | return windowsPathComponents.last ?? "" 41 | } 42 | 43 | var windowsPathComponents: [String] { 44 | return components(separatedBy: "\\").filter { !$0.isEmpty } 45 | } 46 | 47 | init(contentsOfResource resource: String) { 48 | let url = Bundle.main.url(forResource: resource, withExtension: nil)! 49 | try! self.init(contentsOf: url) 50 | } 51 | 52 | func appendingPathExtension(_ pathExtension: String) -> String? { 53 | return (self as NSString).appendingPathExtension(pathExtension) 54 | } 55 | 56 | func appendingWindowsPathComponent(_ component: String, isDirectory: Bool = false) -> String { 57 | return windowsPathComponents 58 | .appending(component) 59 | .joined(separator: .windowsPathSeparator) 60 | .ensuringTrailingWindowsPathSeparator(isPresent: isDirectory) 61 | } 62 | 63 | func ensuringTrailingWindowsPathSeparator(isPresent: Bool = true) -> String { 64 | switch (isWindowsDirectory, isPresent) { 65 | case (true, false): 66 | return windowsPathComponents 67 | .joined(separator: .windowsPathSeparator) 68 | case (false, true): 69 | return self.appending(String.windowsPathSeparator) 70 | case (true, true), (false, false): 71 | return self 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/UInt32.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | public extension UInt32 { 22 | 23 | static let none: Self = 0x00000000 24 | 25 | // UID1 26 | static let directFileStore: Self = 0x10000037 27 | static let permanentFileStoreLayout: Self = 0x10000050 // Database 28 | static let multiBitmapRomImage: Self = 0x10000041 29 | static let dynamicLibraryUid: Self = 0x10000079 // Native app 30 | 31 | // UID2 32 | static let appDllDoc: Self = 0x1000006D 33 | static let mbm: Self = 0x10000042 34 | 35 | // UID3 36 | static let word: Self = 0x1000007F 37 | static let sheet: Self = 0x10000088 38 | static let record: Self = 0x1000007E 39 | static let opl: Self = 0x10000085 40 | static let data: Self = 0x10000086 41 | static let agenda: Self = 0x10000084 42 | static let sketch: Self = 0x1000007D 43 | static let jotter: Self = 0x10000CEA 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Extensions/URL.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import Diligence 22 | 23 | extension URL { 24 | 25 | public static let about = URL(string: "x-reconnect://about")! 26 | public static let browser = URL(string: "x-reconnect://browser")! 27 | public static let install = URL(string: "x-reconnect://install/")! 28 | public static let transfers = URL(string: "x-reconnect://transfers")! 29 | public static let update = URL(string: "x-reconnect://update")! 30 | 31 | public static let discord = URL(string: "https://discord.gg/ZUQDhkZjkK")! 32 | public static let donate = URL(string: "https://jbmorley.co.uk/support")! 33 | public static let gitHub = URL(string: "https://github.com/inseven/reconnect")! 34 | public static let software = URL(string: "https://jbmorley.co.uk/software")! 35 | 36 | public static var support: URL = { 37 | let subject = "Reconnect Support (\(Bundle.main.extendedVersion ?? "Unknown Version"))" 38 | return URL(address: "support@jbmorley.co.uk", subject: subject)! 39 | }() 40 | 41 | public func appendingPathComponents(_ pathComponents: [String]) -> URL { 42 | return pathComponents.reduce(self) { url, pathComponent in 43 | return url.appendingPathComponent(pathComponent) 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Model/FileType.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | public enum FileType { 22 | 23 | case unknown 24 | case directory 25 | case word 26 | case sheet 27 | case record 28 | case opl 29 | case data 30 | case agenda 31 | case sketch 32 | case jotter 33 | case mbm 34 | 35 | } 36 | 37 | extension FileType { 38 | 39 | public var name: String { 40 | switch self { 41 | case .unknown: 42 | return "Unknown" 43 | case .directory: 44 | return "Folder" 45 | case .word: 46 | return "Word" 47 | case .sheet: 48 | return "Sheet" 49 | case .record: 50 | return "Record" 51 | case .opl: 52 | return "OPL" 53 | case .data: 54 | return "Data" 55 | case .agenda: 56 | return "Agenda" 57 | case .sketch: 58 | return "Sketch" 59 | case .jotter: 60 | return "Jotter" 61 | case .mbm: 62 | return "Bitmap" 63 | } 64 | } 65 | 66 | public var image: String { 67 | switch self { 68 | case .unknown: 69 | return "FileUnknown16" 70 | case .directory: 71 | return "Folder16" 72 | case .word: 73 | return "Word16" 74 | case .sheet: 75 | return "Sheet16" 76 | case .record: 77 | return "Record16" 78 | case .opl: 79 | return "OPL16" 80 | case .data: 81 | return "Data16" 82 | case .agenda: 83 | return "Agenda16" 84 | case .sketch: 85 | return "Sketch16" 86 | case .jotter: 87 | return "Jotter16" 88 | case .mbm: 89 | return "FileUnknown16" 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Model/ReconnectError.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import plpftp 22 | 23 | public enum ReconnectError: Error { 24 | case unknown 25 | case rfsvError(rfsv.errs) 26 | case unknownMediaType 27 | case invalidFilePath 28 | case unknownFileSize 29 | case imageSaveError 30 | } 31 | 32 | extension ReconnectError: LocalizedError { 33 | 34 | public var errorDescription: String? { 35 | switch self { 36 | case .unknown: 37 | return "Unknown error." 38 | case .rfsvError(let error): 39 | return error.localizedDescription 40 | case .unknownMediaType: 41 | return "Unknown media type." 42 | case .invalidFilePath: 43 | return "Invalid file path." 44 | case .unknownFileSize: 45 | return "Unknown file size." 46 | case .imageSaveError: 47 | return "Failed to save image." 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/PLP/PsionClient.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | public class PsionClient { 22 | 23 | let fileServer = FileServer() 24 | let remoteCommandServices = RemoteCommandServicesClient() 25 | 26 | public init() { 27 | 28 | } 29 | 30 | public func runProgram(path: String) async throws { 31 | let attributes = try await fileServer.getExtendedAttributes(path: path) 32 | if attributes.uid1 == .dynamicLibraryUid { 33 | try remoteCommandServices.execProgram(program: path) 34 | } else { 35 | try remoteCommandServices.execProgram(program: "Z:\\System\\Apps\\OPL\\OPL.app", args: "A" + path) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/PLP/RemoteCommandServicesClient.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import ncp 22 | import plpftp 23 | 24 | public class RemoteCommandServicesClient { 25 | 26 | private let host: String 27 | private let port: Int32 28 | 29 | private let workQueue = DispatchQueue(label: "RemoteCommandServicesClient.workQueue") 30 | 31 | private var client = RPCSClient() 32 | 33 | public init(host: String = "127.0.0.1", port: Int32 = 7501) { 34 | self.host = host 35 | self.port = port 36 | } 37 | 38 | private func workQueue_connect(perform: (inout RPCSClient) throws -> T) throws -> T { 39 | guard self.client.connect(self.host, self.port) else { 40 | throw ReconnectError.unknown 41 | } 42 | return try perform(&client) 43 | } 44 | 45 | private func withClient(perform: (inout RPCSClient) throws -> T) throws -> T { 46 | dispatchPrecondition(condition: .notOnQueue(workQueue)) 47 | return try workQueue.sync { 48 | return try self.workQueue_connect(perform: perform) 49 | } 50 | } 51 | 52 | public func execProgram(program: String, args: String = "") throws { 53 | return try withClient { client in 54 | try client.execProgram(program, args).check() 55 | } 56 | } 57 | 58 | public func stopPrograms() throws { 59 | return try withClient { client in 60 | try client.stopPrograms().check() 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/PLP/Server.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import ncp 22 | 23 | public protocol ServerDelegate: NSObject { 24 | 25 | func server(server: Server, didChangeConnectionState isConnected: Bool) 26 | 27 | } 28 | 29 | public class Server { 30 | 31 | public weak var delegate: ServerDelegate? = nil 32 | 33 | private var lock = NSLock() 34 | var threadID: pthread_t? = nil // Synchronized with lock. 35 | var devices: [String] = [] // Synchronized with lock. 36 | 37 | func device() -> String { 38 | print("Getting device...") 39 | while true { 40 | let devices = lock.withLock { 41 | return self.devices 42 | } 43 | if let device = devices.first { 44 | return device 45 | } 46 | print("Waiting for devices...") 47 | sleep(1) 48 | } 49 | } 50 | 51 | func threadEntryPoint() { 52 | 53 | setup_signal_handlers() 54 | 55 | // TODO: Maybe this shouldn't be a member? 56 | lock.withLock { 57 | threadID = pthread_self() 58 | } 59 | 60 | let context = Unmanaged.passRetained(self).toOpaque() 61 | let callback: statusCallback_t = { context, status in 62 | guard let context else { 63 | return 64 | } 65 | print("status = \(status)") 66 | let server = Unmanaged.fromOpaque(context).takeUnretainedValue() 67 | DispatchQueue.main.sync { 68 | let isConnected = status == 1 ? true : false 69 | server.delegate?.server(server: server, didChangeConnectionState: isConnected) 70 | } 71 | } 72 | 73 | while true { 74 | let device = self.device() 75 | print("Using device \(device)...") 76 | ncpd(7501, 115200, "127.0.0.1", device, 0x0000, callback, context) 77 | DispatchQueue.main.async { 78 | self.delegate?.server(server: self, didChangeConnectionState: false) 79 | } 80 | print("ncpd ended") 81 | } 82 | } 83 | 84 | public init() { 85 | // Create a new thread and start it 86 | } 87 | 88 | public func start() { 89 | // TODO: ONLY DO THIS ONCE! 90 | let thread = Thread(block: threadEntryPoint) 91 | thread.start() 92 | 93 | // TODO: This should probably block until we're ready?? 94 | } 95 | 96 | public func setDevices(_ devices: [String]) { 97 | // SIGHUP? 98 | guard let threadID = lock.withLock({ 99 | return self.threadID 100 | }) else { 101 | return 102 | } 103 | 104 | print("Updating serial devices \(devices)") 105 | 106 | let needsRestart = lock.withLock { 107 | guard self.devices != devices else { 108 | return false 109 | } 110 | self.devices = devices 111 | return true 112 | } 113 | 114 | guard needsRestart else { 115 | print("Serial devices haven't changed; ignoring.") 116 | return 117 | } 118 | 119 | print("Restarting ncpd...") 120 | pthread_kill(threadID, SIGINT) 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Utilities/Graphics.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | import ImageIO 21 | import UniformTypeIdentifiers 22 | 23 | public func CGImageWriteTIFF(destinationURL: URL, images: [CGImage]) throws { 24 | guard let destination = CGImageDestinationCreateWithURL(destinationURL as CFURL, 25 | UTType.tiff.identifier as CFString, 26 | images.count, 27 | nil) else { 28 | throw ReconnectError.imageSaveError 29 | } 30 | for image in images { 31 | CGImageDestinationAddImage(destination, image, nil) 32 | } 33 | guard CGImageDestinationFinalize(destination) else { 34 | throw ReconnectError.imageSaveError 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ReconnectCore/Sources/ReconnectCore/Utilities/SerialDeviceMonitor.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | import IOKit 21 | import IOKit.serial 22 | 23 | public protocol SerialDeviceMonitorDelegate: NSObject { 24 | 25 | func serialDeviceMonitor(serialDeviceMonitor: SerialDeviceMonitor, didAddDevice device: String) 26 | func serialDeviceMonitor(serialDeviceMonitor: SerialDeviceMonitor, didRemoveDevice device: String) 27 | 28 | } 29 | 30 | public class SerialDeviceMonitor { 31 | 32 | public weak var delegate: SerialDeviceMonitorDelegate? 33 | 34 | public init() { 35 | 36 | } 37 | 38 | public func start() { 39 | let matchingDict = IOServiceMatching(kIOSerialBSDServiceValue) 40 | var notifyPort: IONotificationPortRef? 41 | var addedIterator: io_iterator_t = 0 42 | var removedIterator: io_iterator_t = 0 43 | 44 | notifyPort = IONotificationPortCreate(kIOMainPortDefault) 45 | let runLoopSource = IONotificationPortGetRunLoopSource(notifyPort).takeUnretainedValue() 46 | CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .defaultMode) 47 | 48 | let context = Unmanaged.passRetained(self).toOpaque() 49 | 50 | // TODO: Store this notification and remove it in the future. 51 | IOServiceAddMatchingNotification( 52 | notifyPort!, 53 | kIOMatchedNotification, 54 | matchingDict, 55 | { (context, iterator) in 56 | guard let context else { 57 | return 58 | } 59 | let monitor = Unmanaged.fromOpaque(context).takeUnretainedValue() 60 | while case let service = IOIteratorNext(iterator), service != 0 { 61 | monitor.deviceAdded(service: service) 62 | } 63 | }, 64 | context, 65 | &addedIterator 66 | ) 67 | 68 | // TODO: Store this notification and remove it in the future. 69 | IOServiceAddMatchingNotification( 70 | notifyPort!, 71 | kIOTerminatedNotification, 72 | matchingDict, 73 | { (context, iterator) in 74 | guard let context else { 75 | return 76 | } 77 | let monitor = Unmanaged.fromOpaque(context).takeUnretainedValue() 78 | while case let service = IOIteratorNext(iterator), service != 0 { 79 | monitor.deviceRemoved(service: service) 80 | } 81 | }, 82 | context, 83 | &removedIterator 84 | ) 85 | 86 | // Check the notification iterators for their initial state. We do this for both iterators as it ensures we have 87 | // the correct initial state and is required to arm the notifications. 88 | // https://developer.apple.com/documentation/iokit/1514362-ioserviceaddmatchingnotification 89 | 90 | // Handle existing removals. 91 | while case let service = IOIteratorNext(removedIterator), service != 0 { 92 | deviceRemoved(service: service) 93 | } 94 | 95 | // Handle existing additions. 96 | while case let service = IOIteratorNext(addedIterator), service != 0 { 97 | deviceAdded(service: service) 98 | } 99 | 100 | } 101 | 102 | func stop() { 103 | // TODO: Figure out where this notification is owned and how we call ack to an existing object. 104 | } 105 | 106 | func deviceAdded(service: io_object_t) { 107 | dispatchPrecondition(condition: .onQueue(.main)) 108 | defer { 109 | IOObjectRelease(service) 110 | } 111 | if let deviceName = IORegistryEntryCreateCFProperty(service, 112 | kIOCalloutDeviceKey as CFString, 113 | kCFAllocatorDefault, 0)?.takeUnretainedValue() as? String { 114 | delegate?.serialDeviceMonitor(serialDeviceMonitor: self, didAddDevice: deviceName) 115 | } 116 | } 117 | 118 | func deviceRemoved(service: io_object_t) { 119 | dispatchPrecondition(condition: .onQueue(.main)) 120 | defer { 121 | IOObjectRelease(service) 122 | } 123 | if let deviceName = IORegistryEntryCreateCFProperty(service, 124 | kIOCalloutDeviceKey as CFString, 125 | kCFAllocatorDefault, 0)?.takeUnretainedValue() as? String { 126 | delegate?.serialDeviceMonitor(serialDeviceMonitor: self, didRemoveDevice: deviceName) 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /ReconnectCore/Tests/ReconnectCoreTests/ReconnectCoreTests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | @testable import ReconnectCore 21 | 22 | final class ReconnectCoreTests: XCTestCase { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusConnected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StatusConnected.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "StatusConnectedDark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusConnected.imageset/StatusConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/StatusConnected.imageset/StatusConnected.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusConnected.imageset/StatusConnectedDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/StatusConnected.imageset/StatusConnectedDark.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusDisconnected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StatusDisconnected.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "StatusDisconnectedDark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusDisconnected.imageset/StatusDisconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/StatusDisconnected.imageset/StatusDisconnected.png -------------------------------------------------------------------------------- /ReconnectMenu/Assets.xcassets/StatusDisconnected.imageset/StatusDisconnectedDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/ReconnectMenu/Assets.xcassets/StatusDisconnected.imageset/StatusDisconnectedDark.png -------------------------------------------------------------------------------- /ReconnectMenu/Model/ApplicationModel.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | import ReconnectCore 24 | 25 | @MainActor @Observable 26 | class ApplicationModel: NSObject { 27 | 28 | struct SerialDevice: Identifiable { 29 | 30 | var id: String { 31 | return path 32 | } 33 | 34 | var path: String 35 | var available: Bool 36 | var enabled: Binding 37 | } 38 | 39 | enum SettingsKey: String { 40 | case selectedDevices 41 | } 42 | 43 | var isConnected: Bool = false 44 | 45 | var devices: [SerialDevice] { 46 | return connectedDevices.union(selectedDevices) 47 | .map { device in 48 | let binding: Binding = Binding { 49 | return self.selectedDevices.contains(device) 50 | } set: { newValue in 51 | if newValue { 52 | self.selectedDevices.insert(device) 53 | } else { 54 | self.selectedDevices.remove(device) 55 | } 56 | } 57 | return SerialDevice(path: device, 58 | available: connectedDevices.contains(device), 59 | enabled: binding) 60 | } 61 | .sorted { device1, device2 in 62 | return device1.path.localizedStandardCompare(device2.path) == .orderedAscending 63 | } 64 | } 65 | 66 | private var selectedDevices: Set { 67 | didSet { 68 | keyedDefaults.set(Array(selectedDevices), forKey: .selectedDevices) 69 | update() 70 | } 71 | } 72 | 73 | private var connectedDevices: Set = [] { 74 | didSet { 75 | update() 76 | } 77 | } 78 | 79 | private let keyedDefaults = KeyedDefaults() 80 | private let server: Server = Server() 81 | private let serialDeviceMonitor = SerialDeviceMonitor() 82 | 83 | override init() { 84 | selectedDevices = Set(keyedDefaults.object(forKey: .selectedDevices) as? Array ?? []) 85 | super.init() 86 | server.delegate = self 87 | serialDeviceMonitor.delegate = self 88 | start() 89 | } 90 | 91 | func start() { 92 | server.start() 93 | serialDeviceMonitor.start() 94 | } 95 | 96 | @MainActor func quit() { 97 | NSApplication.shared.terminate(nil) 98 | } 99 | 100 | func update() { 101 | server.setDevices(selectedDevices.intersection(connectedDevices).sorted()) 102 | } 103 | 104 | } 105 | 106 | extension ApplicationModel: ServerDelegate { 107 | 108 | func server(server: Server, didChangeConnectionState isConnected: Bool) { 109 | self.isConnected = isConnected 110 | } 111 | 112 | } 113 | 114 | extension ApplicationModel: SerialDeviceMonitorDelegate { 115 | 116 | func serialDeviceMonitor(serialDeviceMonitor: SerialDeviceMonitor, didAddDevice device: String) { 117 | connectedDevices.insert(device) 118 | 119 | } 120 | 121 | func serialDeviceMonitor(serialDeviceMonitor: SerialDeviceMonitor, didRemoveDevice device: String) { 122 | connectedDevices.remove(device) 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /ReconnectMenu/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ReconnectMenu/ReconnectMenu.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ReconnectMenu/ReconnectMenuApp.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | @main @MainActor 22 | struct ReconnectMenuApp: App { 23 | 24 | @State var applicationModel = ApplicationModel() 25 | 26 | var body: some Scene { 27 | 28 | MenuBarExtra { 29 | MainMenu() 30 | .environment(applicationModel) 31 | } label: { 32 | if applicationModel.isConnected { 33 | Image("StatusConnected") 34 | } else { 35 | Image("StatusDisconnected") 36 | } 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ReconnectMenu/Views/MainMenu.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import SwiftUI 20 | 21 | import Interact 22 | 23 | import ReconnectCore 24 | 25 | struct MainMenu: View { 26 | 27 | @Environment(\.openURL) private var openURL 28 | 29 | @Environment(ApplicationModel.self) var applicationModel 30 | 31 | @ObservedObject var application = Application.shared 32 | 33 | var body: some View { 34 | @Bindable var applicationModel = applicationModel 35 | Button { 36 | openURL(.browser) 37 | } label: { 38 | Text("My Psion...") 39 | } 40 | .disabled(!applicationModel.isConnected) 41 | Divider() 42 | Button { 43 | openURL(.about) 44 | } label: { 45 | Text("About...") 46 | } 47 | Menu("Settings") { 48 | ForEach(applicationModel.devices) { device in 49 | Toggle(isOn: device.enabled) { 50 | Text(device.path) 51 | .foregroundStyle(device.available ? .primary : .secondary) 52 | } 53 | } 54 | Divider() 55 | Toggle("Open at Login", isOn: $application.openAtLogin) 56 | } 57 | Divider() 58 | Button("Check for Updates...") { 59 | openURL(.update) 60 | } 61 | Divider() 62 | Button("Quit") { 63 | applicationModel.quit() 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /ReconnectTests/NavigationStackTests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | @testable import Reconnect 21 | 22 | final class NavigationStackTests: XCTestCase { 23 | 24 | func test() { 25 | var stack = NavigationStack() 26 | XCTAssertNil(stack.path) 27 | XCTAssertFalse(stack.canGoBack()) 28 | XCTAssertFalse(stack.canGoForward()) 29 | 30 | stack.navigate("C:\\") 31 | XCTAssertEqual(stack.path, "C:\\") 32 | XCTAssertFalse(stack.canGoBack()) 33 | XCTAssertFalse(stack.canGoForward()) 34 | 35 | stack.navigate("C:\\Screenshots\\") 36 | XCTAssertEqual(stack.path, "C:\\Screenshots\\") 37 | XCTAssertTrue(stack.canGoBack()) 38 | XCTAssertFalse(stack.canGoForward()) 39 | 40 | stack.back() 41 | XCTAssertEqual(stack.path, "C:\\") 42 | XCTAssertFalse(stack.canGoBack()) 43 | XCTAssertTrue(stack.canGoForward()) 44 | 45 | stack.forward() 46 | XCTAssertEqual(stack.path, "C:\\Screenshots\\") 47 | XCTAssertTrue(stack.canGoBack()) 48 | XCTAssertFalse(stack.canGoForward()) 49 | 50 | stack.back() 51 | XCTAssertEqual(stack.path, "C:\\") 52 | XCTAssertFalse(stack.canGoBack()) 53 | XCTAssertTrue(stack.canGoForward()) 54 | 55 | stack.navigate("C:\\Documents\\") 56 | XCTAssertEqual(stack.path, "C:\\Documents\\") 57 | XCTAssertTrue(stack.canGoBack()) 58 | XCTAssertFalse(stack.canGoForward()) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /ReconnectTests/ReconnectTests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | @testable import Reconnect 21 | 22 | final class ReconnectTests: XCTestCase { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ReconnectTests/WindowsPathTests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | @testable import Reconnect 21 | 22 | final class WindowsPathTests: XCTestCase { 23 | 24 | func testDeletingLastWindowsPathComponent() { 25 | XCTAssertEqual("C:\\Foo\\Bar".deletingLastWindowsPathComponent, "C:\\Foo") 26 | XCTAssertEqual("C:\\Foo\\".deletingLastWindowsPathComponent, "C:") 27 | } 28 | 29 | func testAppendingWindowsPathComponent() { 30 | XCTAssertEqual("C:\\Foo".appendingWindowsPathComponent("Bar"), "C:\\Foo\\Bar") 31 | XCTAssertEqual("C:\\Foo".appendingWindowsPathComponent("Bar", isDirectory: true), "C:\\Foo\\Bar\\") 32 | } 33 | 34 | func testEnsuringTrailingWindowsPathSeparator() { 35 | 36 | XCTAssertEqual("C:\\Foo".ensuringTrailingWindowsPathSeparator(), "C:\\Foo\\") 37 | XCTAssertEqual("C:\\Foo".ensuringTrailingWindowsPathSeparator(isPresent: true), "C:\\Foo\\") 38 | 39 | XCTAssertEqual("C:\\Foo".ensuringTrailingWindowsPathSeparator(isPresent: false), "C:\\Foo") 40 | 41 | XCTAssertEqual("C:\\Foo\\".ensuringTrailingWindowsPathSeparator(), "C:\\Foo\\") 42 | XCTAssertEqual("C:\\Foo\\".ensuringTrailingWindowsPathSeparator(isPresent: true), "C:\\Foo\\") 43 | 44 | XCTAssertEqual("C:\\Foo\\".ensuringTrailingWindowsPathSeparator(isPresent: false), "C:\\Foo") 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ReconnectUITests/ReconnectUITests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | 21 | final class ReconnectUITests: XCTestCase { 22 | 23 | override func setUpWithError() throws { 24 | // Put setup code here. This method is called before the invocation of each test method in the class. 25 | 26 | // In UI tests it is usually best to stop immediately when a failure occurs. 27 | continueAfterFailure = false 28 | 29 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 30 | } 31 | 32 | override func tearDownWithError() throws { 33 | // Put teardown code here. This method is called after the invocation of each test method in the class. 34 | } 35 | 36 | func testExample() throws { 37 | // UI tests must launch the application that they test. 38 | let app = XCUIApplication() 39 | app.launch() 40 | 41 | // Use XCTAssert and related functions to verify your tests produce the correct results. 42 | } 43 | 44 | func testLaunchPerformance() throws { 45 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 46 | // This measures how long it takes to launch your application. 47 | measure(metrics: [XCTApplicationLaunchMetric()]) { 48 | XCUIApplication().launch() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ReconnectUITests/ReconnectUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import XCTest 20 | 21 | final class ReconnectUITestsLaunchTests: XCTestCase { 22 | 23 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 24 | true 25 | } 26 | 27 | override func setUpWithError() throws { 28 | continueAfterFailure = false 29 | } 30 | 31 | func testLaunch() throws { 32 | let app = XCUIApplication() 33 | app.launch() 34 | 35 | // Insert steps here to perform after app launch but before taking a screenshot, 36 | // such as logging into a test account or navigating somewhere in the app 37 | 38 | let attachment = XCTAttachment(screenshot: app.screenshot()) 39 | attachment.name = "Launch Screen" 40 | attachment.lifetime = .keepAlways 41 | add(attachment) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ScreenshotSIS/Command.swift: -------------------------------------------------------------------------------- 1 | // Reconnect -- Psion connectivity for macOS 2 | // 3 | // Copyright (C) 2024-2025 Jason Morley 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation; either version 2 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program; if not, write to the Free Software 17 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 | 19 | import Foundation 20 | 21 | import ArgumentParser 22 | 23 | import ReconnectCore 24 | import OpoLua 25 | 26 | enum Path { 27 | case file(String) 28 | case directory(String) 29 | } 30 | 31 | class Installer { 32 | 33 | let fileServer: FileServer 34 | let interpreter = PsiLuaEnv() 35 | 36 | var paths: [Path] = [] 37 | 38 | init(fileServer: FileServer) { 39 | self.fileServer = fileServer 40 | } 41 | 42 | func install(_ url: URL) async throws { 43 | try interpreter.installSisFile(path: url.path, handler: self) 44 | } 45 | 46 | } 47 | 48 | extension Installer: SisInstallIoHandler { 49 | 50 | func fsop(_ op: Fs.Operation) -> Fs.Result { 51 | switch op.type { 52 | case .write(let data): 53 | let directory = NSTemporaryDirectory() 54 | let fileName = NSUUID().uuidString 55 | let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName])! 56 | do { 57 | let destinationDirectory = op.path.deletingLastWindowsPathComponent 58 | try data.write(to: fullURL) 59 | if !(try fileServer.fileExistsSync(path: destinationDirectory)) { 60 | print("Creating directory '\(destinationDirectory)'...") 61 | try fileServer.mkdirSync(path: destinationDirectory) 62 | paths.append(.directory(destinationDirectory)) 63 | } 64 | print("Writing file '\(op.path)'...") 65 | try fileServer.copyFileSync(fromLocalPath: fullURL.path, toRemotePath: op.path) { progress, size in 66 | let percentage = Float(progress) / Float(size) 67 | print("\(percentage.formatted(.percent))") 68 | return .continue 69 | } 70 | } catch { 71 | print("Failed to write file '\(op.path)' with error '\(error)'.") 72 | return .err(.notReady) 73 | } 74 | paths.append(.file(op.path)) 75 | return .err(.none) 76 | default: 77 | print("unsupported operation '\(op)'") 78 | return .err(.notReady) 79 | } 80 | } 81 | 82 | } 83 | 84 | @main 85 | struct Command: AsyncParsableCommand { 86 | 87 | public static var configuration = CommandConfiguration(version: version()) 88 | 89 | static func version() -> String { 90 | guard let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, 91 | let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String 92 | else { 93 | return "unknown" 94 | } 95 | let components: [String] = [version, buildNumber] 96 | return components.joined(separator: " ") 97 | } 98 | 99 | @Argument(help: "The SIS file to install.") 100 | var installer: String 101 | 102 | @Argument(help: "Path to screenshot utility (screenshot.exe).") 103 | var screenshot: String 104 | 105 | @Argument(help: "Screenshot output directory path.") 106 | var outputDirectory: String 107 | 108 | func sleep(seconds: Double) async throws { 109 | print("Sleeping for \(seconds) seconds...") 110 | try await Task.sleep(for: .seconds(seconds)) 111 | } 112 | 113 | mutating func run() async throws { 114 | 115 | print("Installing '\(installer)'...") 116 | let fileServer = FileServer() 117 | let client = RemoteCommandServicesClient() 118 | let url = URL(filePath: installer) 119 | let installer = Installer(fileServer: fileServer) 120 | let services = PsionClient() 121 | 122 | // Install the SIS file. 123 | try await installer.install(url) 124 | 125 | // Install the screenshot file. 126 | try await fileServer.copyFile(fromLocalPath: screenshot, toRemotePath: "C:\\screenshot.exe") 127 | 128 | // Detect the apps. 129 | let applications = installer.paths.compactMap { (path) -> String? in 130 | switch path { 131 | case .file(let path): 132 | guard path.lowercased().hasSuffix(".app") else { 133 | return nil 134 | } 135 | return path 136 | case .directory(_): 137 | return nil 138 | } 139 | } 140 | 141 | if let application = applications.first { 142 | 143 | // Launch the app. 144 | print("Running app...") 145 | try await services.runProgram(path: application) 146 | 147 | // Wait for the app to start. 148 | try await sleep(seconds: 30) 149 | 150 | // Take a screenshot. 151 | print("Taking screenshot...") 152 | try client.execProgram(program: "C:\\screenshot.exe", args: "") 153 | 154 | // Wait for the screenshot. 155 | try await sleep(seconds: 5) 156 | 157 | // Copy the screenshot. 158 | let outputURL = URL(filePath: outputDirectory).appendingPathComponent("screenshot.mbm") 159 | try await fileServer.copyFile(fromRemotePath: "C:\\screenshot.mbm", toLocalPath: outputURL.path) { progress, size in 160 | print("\(progress) / \(size)") 161 | return .continue 162 | } 163 | try await fileServer.remove(path: "C:\\screenshot.mbm") 164 | try PsiLuaEnv().convertMultiBitmap(at: outputURL, removeSource: true) 165 | 166 | } 167 | 168 | // Close all the running programs. 169 | print("Stopping all programs...") 170 | try client.stopPrograms() 171 | 172 | try await sleep(seconds: 10) 173 | 174 | // Delete the files. 175 | print("Cleaning up files...") 176 | try await fileServer.remove(path: "C:\\screenshot.exe") 177 | for path in installer.paths.reversed() { 178 | switch path { 179 | case .file(let path): 180 | try await fileServer.remove(path: path) 181 | case .directory(let path): 182 | try await fileServer.rmdir(path: path) 183 | } 184 | } 185 | 186 | print("Done.") 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /dependencies/plptools/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "plptools", 7 | platforms: [ 8 | .macOS(.v10_15), 9 | ], 10 | products: [ 11 | // .library( 12 | // name: "plptools", 13 | // targets: [ 14 | // "plptools", 15 | // ] 16 | // ), 17 | .library( 18 | name: "ncp", 19 | targets: [ 20 | "ncp" 21 | ] 22 | ), 23 | .library( 24 | name: "plpftp", 25 | targets: [ 26 | "plpftp" 27 | ] 28 | ) 29 | ], 30 | dependencies: [], 31 | targets: [ 32 | .target( 33 | name: "plptools", 34 | dependencies: [], 35 | path: "plptools", 36 | exclude: [ 37 | "lib/Makefile.am", 38 | ], 39 | sources: [ 40 | "lib", 41 | ], 42 | publicHeadersPath: "lib" 43 | ), 44 | .target( 45 | name: "ncp", 46 | dependencies: [ 47 | "plptools", 48 | ], 49 | path: "plptools", 50 | exclude: [ 51 | // "ncpd/main.cc", 52 | // "ncpd/main.h", 53 | "ncpd/Makefile.am", 54 | ], 55 | sources: [ 56 | "ncpd", 57 | ], 58 | publicHeadersPath: "ncpd", 59 | cSettings: [ 60 | .headerSearchPath("lib"), // TODO: Put the config in an extra directory here? 61 | ] 62 | // swiftSettings: [.interoperabilityMode(.Cxx)] 63 | ), 64 | .target( 65 | name: "plpftp", 66 | dependencies: [ 67 | "plptools", 68 | ], 69 | path: "plptools", 70 | exclude: [ 71 | "plpftp/Makefile.am", 72 | "plpftp/main.cc", 73 | ], 74 | sources: [ 75 | "plpftp" 76 | ], 77 | publicHeadersPath: "plpftp", 78 | cSettings: [ 79 | .headerSearchPath("lib"), // TODO: Put the config in an extra directory here? 80 | ] 81 | ) 82 | ] 83 | ) 84 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 |

Not Found

7 |

The page you requested could not be found. 🧐

8 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 4.2.0" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | # gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | gem 'jekyll-environment-variables' 20 | end 21 | 22 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 23 | # and associated library. 24 | platforms :mingw, :x64_mingw, :mswin, :jruby do 25 | gem "tzinfo", "~> 1.2" 26 | gem "tzinfo-data" 27 | end 28 | 29 | # Performance-booster for watching directories on Windows 30 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 31 | 32 | 33 | gem "webrick", "~> 1.7" 34 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.1) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.10) 8 | em-websocket (0.5.3) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0) 11 | eventmachine (1.2.7) 12 | ffi (1.15.5) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.8.0) 15 | i18n (1.12.0) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.2.2) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 2.0) 32 | jekyll-environment-variables (1.0.1) 33 | jekyll (>= 3.0, < 5.x) 34 | jekyll-feed (0.16.0) 35 | jekyll (>= 3.7, < 5.0) 36 | jekyll-sass-converter (2.2.0) 37 | sassc (> 2.0.1, < 3.0) 38 | jekyll-seo-tag (2.8.0) 39 | jekyll (>= 3.8, < 5.0) 40 | jekyll-watch (2.2.1) 41 | listen (~> 3.0) 42 | kramdown (2.4.0) 43 | rexml 44 | kramdown-parser-gfm (1.1.0) 45 | kramdown (~> 2.0) 46 | liquid (4.0.3) 47 | listen (3.7.1) 48 | rb-fsevent (~> 0.10, >= 0.10.3) 49 | rb-inotify (~> 0.9, >= 0.9.10) 50 | mercenary (0.4.0) 51 | minima (2.5.1) 52 | jekyll (>= 3.5, < 5.0) 53 | jekyll-feed (~> 0.9) 54 | jekyll-seo-tag (~> 2.1) 55 | pathutil (0.16.2) 56 | forwardable-extended (~> 2.6) 57 | public_suffix (5.0.0) 58 | rb-fsevent (0.11.1) 59 | rb-inotify (0.10.1) 60 | ffi (~> 1.0) 61 | rexml (3.2.5) 62 | rouge (3.30.0) 63 | safe_yaml (1.0.5) 64 | sassc (2.4.0) 65 | ffi (~> 1.9) 66 | terminal-table (2.0.0) 67 | unicode-display_width (~> 1.1, >= 1.1.1) 68 | unicode-display_width (1.8.0) 69 | webrick (1.7.0) 70 | 71 | PLATFORMS 72 | arm64-darwin-24 73 | x86_64-darwin-20 74 | x86_64-darwin-22 75 | x86_64-linux 76 | 77 | DEPENDENCIES 78 | jekyll (~> 4.2.0) 79 | jekyll-environment-variables 80 | jekyll-feed (~> 0.12) 81 | minima (~> 2.5) 82 | tzinfo (~> 1.2) 83 | tzinfo-data 84 | wdm (~> 0.1.1) 85 | webrick (~> 1.7) 86 | 87 | BUNDLED WITH 88 | 2.2.18 89 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Reconnect 2 | email: support@jbmorley.co.uk 3 | description: Psion connectivity for macOS 4 | baseurl: "" 5 | url: "https://reconnect.jbmorley.co.uk" 6 | 7 | # Build settings 8 | theme: minima 9 | plugins: 10 | - jekyll-feed 11 | - jekyll-environment-variables 12 | destination: ../_site 13 | 14 | defaults: 15 | - values: 16 | layout: "default" 17 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /docs/_includes/navigation.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/_includes/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ site.title }}{% if page.title %} - {{ page.title }}{% endif %} 7 | {% if site.description %}{% endif %} 8 | {% if site.author %}{% endif %} 9 | 10 | 11 | {% if site.description %}{% endif %} 12 | 13 | 14 | {% include navigation.html %} 15 |
16 | {{ content }} 17 |
18 | {% include footer.html %} 19 | {% include scripts.html %} 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --tint-color: #ff9502; 3 | --brand-color: #ff9502; 4 | --primary-foreground-color: #000000; 5 | --secondary-foreground-color: gray; 6 | --background-color: #ffffff; 7 | --navigation-background: rgba(255, 255, 255, 0.7); 8 | --primary-action-background-color: #007aff; 9 | --content-width: 900px; 10 | --vertical-spacing: 1rem; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | 15 | :root { 16 | --primary-foreground-color: #ffffff; 17 | --secondary-foreground-color: gray; 18 | --background-color: #181818; 19 | --navigation-background: rgba(24, 24, 24, 0.7); 20 | --primary-action-background-color: #0a84ff; 21 | } 22 | 23 | } 24 | 25 | body { 26 | font-family: Helvetica, sans-serif; 27 | font-weight: 200; 28 | font-size: 17px; 29 | margin: 0; 30 | background-color: var(--background-color); 31 | color: var(--primary-foreground-color); 32 | } 33 | 34 | a { 35 | text-decoration: underline; 36 | color: var(--primary-foreground-color); 37 | } 38 | 39 | a:hover { 40 | color: var(--brand-color); 41 | } 42 | 43 | h1, h2, h3 { 44 | margin: var(--vertical-spacing) 0 calc(2 * var(--vertical-spacing)) 0; 45 | } 46 | 47 | h1 { 48 | text-align: center; 49 | font-size: 3em; 50 | } 51 | 52 | h1 a, h2 a, h3 a { 53 | text-decoration: none; 54 | } 55 | 56 | p { 57 | margin: var(--vertical-spacing) 0 var(--vertical-spacing) 0; 58 | } 59 | 60 | p.center { 61 | text-align: center; 62 | } 63 | 64 | ul { 65 | margin-bottom: calc(2 * var(--vertical-spacing)); 66 | } 67 | 68 | ul.navigation { 69 | list-style: none; 70 | margin: 0; 71 | padding: 1em; 72 | background-color: white; 73 | position: sticky; 74 | top: 0; 75 | width: 100%; 76 | z-index: 1000; 77 | box-sizing: border-box; 78 | font-weight: 400; 79 | text-align: center; 80 | background: var(--navigation-background); 81 | backdrop-filter: blur(10px); 82 | -webkit-backdrop-filter: blur(10px); 83 | } 84 | 85 | ul.navigation a { 86 | text-decoration: none; 87 | } 88 | 89 | ul.navigation > li { 90 | display: inline-block; 91 | padding: 0.4em; 92 | } 93 | 94 | .header { 95 | text-align: center; 96 | } 97 | 98 | .appname { 99 | font-weight: 800; 100 | text-align: center; 101 | font-size: 3em; 102 | margin-bottom: var(--vertical-spacing); 103 | } 104 | 105 | .tagline { 106 | text-align: center; 107 | font-size: 1.2em; 108 | margin-bottom: var(--vertical-spacing); 109 | } 110 | 111 | .actions { 112 | text-align: center; 113 | } 114 | 115 | .button, .button:hover { 116 | color: white; 117 | background-color: var(--primary-action-background-color); 118 | border-radius: 100vh; 119 | padding: 1rem 1.6rem; 120 | text-decoration: none; 121 | display: inline-block; 122 | } 123 | 124 | .content { 125 | max-width: var(--content-width); 126 | margin: auto; 127 | padding: 0 2em; 128 | } 129 | 130 | footer { 131 | max-width: var(--content-width); 132 | margin: auto; 133 | padding: 2em; 134 | text-align: center; 135 | color: var(--secondary-foreground-color); 136 | font-size: 0.9rem; 137 | } 138 | 139 | footer a { 140 | color: var(--secondary-foreground-color); 141 | } 142 | 143 | footer p { 144 | margin: var(--vertical-spacing) 0 calc(var(--vertical-spacing) / 2) 0; 145 | } 146 | 147 | footer nav ul { 148 | list-style: none; 149 | padding: 0; 150 | margin: 0; 151 | } 152 | 153 | footer nav ul li { 154 | display: inline; 155 | margin-right: 0.4em; 156 | } 157 | 158 | img.hero { 159 | max-width: 100%; 160 | } 161 | -------------------------------------------------------------------------------- /docs/images/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/docs/images/icon_128x128.png -------------------------------------------------------------------------------- /docs/images/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/docs/images/icon_128x128@2x.png -------------------------------------------------------------------------------- /docs/images/screenshot-default-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/docs/images/screenshot-default-dark@2x.png -------------------------------------------------------------------------------- /docs/images/screenshot-default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/docs/images/screenshot-default@2x.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 |

5 | 9 |

Reconnect
10 |
{{ site.description }}
11 |
12 | Download 13 |
14 |

15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/js/rewrite-external-links.js: -------------------------------------------------------------------------------- 1 | function observe(callback) { 2 | const observer = new MutationObserver(function(mutations, observer) { 3 | mutations.forEach(function(mutation) { 4 | for (let i = 0; i < mutation.addedNodes.length; i++) { 5 | callback(mutation.addedNodes[i]); 6 | } 7 | }); 8 | }); 9 | observer.observe(document.body, { 10 | attributes: true, 11 | childList: true, 12 | subtree: true 13 | }); 14 | callback(document.body); 15 | } 16 | 17 | observe((root) => { 18 | var elements = document.evaluate("//a", root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 19 | for (let i = 0, length = elements.snapshotLength; i < length; ++i) { 20 | var element = elements.snapshotItem(i); 21 | if (element.hasAttribute("href") && 22 | element.getAttribute("href").startsWith("http") && 23 | !element.classList.contains("no-rewrite") && 24 | element.target != "_blank") { 25 | element.target="_blank"; 26 | } 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /docs/license/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: License 3 | --- 4 | 5 | # License 6 | 7 | Reconnect is licensed under the GNU General Public License (GPL) version 2 (see [LICENSE](https://github.com/inseven/reconnect/blob/main/LICENSE)). It depends on the following separately licensed third-party libraries and components: 8 | 9 | --- 10 | 11 | - [Diligence](https://github.com/inseven/diligence), MIT License 12 | - [Interact](https://github.com/inseven/interact), MIT License 13 | - [Licensable](https://github.com/inseven/licensable), MIT License 14 | - [Lua](https://www.lua.org), MIT License 15 | - [OpoLua](https://github.com/inseven/opolua), MIT License 16 | - [plptools](https://github.com/rrthomas/plptools), GPL 2.0 License 17 | - [Sparkle](https://github.com/sparkle-project/Sparkle), Sparkle License 18 | - [Swift Algorithms](https://github.com/apple/swift-algorithms), Apache 2.0 License 19 | - [Swift Argument Parser](https://github.com/apple/swift-argument-parser), Apache 2.0 License 20 | - [Swift Numerics](https://github.com/apple/swift-numerics), Apache 2.0 License 21 | -------------------------------------------------------------------------------- /docs/privacy-policy/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy Policy 3 | --- 4 | 5 | # Privacy Policy 6 | 7 | Reconnect does not collect or store any personal data. 8 | 9 | Should this policy change in the future, we will make all reasonable efforts to inform you and provide mechanisms to opt-out. 10 | -------------------------------------------------------------------------------- /graphics/app-icon/SF Symbols Icon.symbolic: -------------------------------------------------------------------------------- 1 | { 2 | "symbolColor" : { 3 | "red" : 0.9999999403953552, 4 | "green" : 0.9999999403953552, 5 | "blue" : 0.9999999403953552, 6 | "alpha" : 1 7 | }, 8 | "version" : 1, 9 | "topColor" : { 10 | "blue" : 0, 11 | "red" : 1, 12 | "alpha" : 1, 13 | "green" : 0.5763723254 14 | }, 15 | "shadowOpacity" : 0.27266666666666667, 16 | "symbol" : { 17 | "family" : "sf-symbols", 18 | "name" : "cable.connector" 19 | }, 20 | "id" : "24670896-9CE6-40DD-86CB-111B3E0DEB8D", 21 | "shadowHeight" : 0.3, 22 | "bottomColor" : { 23 | "green" : 0.5763723254, 24 | "blue" : 0, 25 | "red" : 1, 26 | "alpha" : 1 27 | }, 28 | "iconOffset" : [ 29 | 0.008556547619047672, 30 | 0 31 | ], 32 | "iconScale" : 0.8198560368342597 33 | } -------------------------------------------------------------------------------- /graphics/app-icon/Series 5 Icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/app-icon/Series 5 Icon.sketch -------------------------------------------------------------------------------- /graphics/assets/agenda/agenda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/agenda/agenda.png -------------------------------------------------------------------------------- /graphics/assets/data/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/data/data.png -------------------------------------------------------------------------------- /graphics/assets/disconnected/disconnected32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/disconnected/disconnected32.png -------------------------------------------------------------------------------- /graphics/assets/disconnected/disconnected32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/disconnected/disconnected32@2x.png -------------------------------------------------------------------------------- /graphics/assets/disk/Disk32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/disk/Disk32.png -------------------------------------------------------------------------------- /graphics/assets/drives/Drive32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/drives/Drive32.png -------------------------------------------------------------------------------- /graphics/assets/folder/Directory16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/folder/Directory16.png -------------------------------------------------------------------------------- /graphics/assets/folder/Directory24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/folder/Directory24.png -------------------------------------------------------------------------------- /graphics/assets/folder/Directory32.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/folder/Directory32.acorn -------------------------------------------------------------------------------- /graphics/assets/folder/Directory32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/folder/Directory32.png -------------------------------------------------------------------------------- /graphics/assets/folder/Folder32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/folder/Folder32.png -------------------------------------------------------------------------------- /graphics/assets/jotter/jotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/jotter/jotter.png -------------------------------------------------------------------------------- /graphics/assets/opl/opl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/opl/opl.png -------------------------------------------------------------------------------- /graphics/assets/record/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/record/record.png -------------------------------------------------------------------------------- /graphics/assets/series-5/series5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/series-5/series5.png -------------------------------------------------------------------------------- /graphics/assets/series-5/series5large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/series-5/series5large.png -------------------------------------------------------------------------------- /graphics/assets/sheet/sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/sheet/sheet.png -------------------------------------------------------------------------------- /graphics/assets/sketch/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/sketch/sketch.png -------------------------------------------------------------------------------- /graphics/assets/status/StatusConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/status/StatusConnected.png -------------------------------------------------------------------------------- /graphics/assets/status/StatusConnectedDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/status/StatusConnectedDark.png -------------------------------------------------------------------------------- /graphics/assets/status/StatusDisconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/status/StatusDisconnected.png -------------------------------------------------------------------------------- /graphics/assets/status/StatusDisconnectedDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/status/StatusDisconnectedDark.png -------------------------------------------------------------------------------- /graphics/assets/unknown/Unknown32.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/unknown/Unknown32.acorn -------------------------------------------------------------------------------- /graphics/assets/unknown/Unknown32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/unknown/Unknown32.png -------------------------------------------------------------------------------- /graphics/assets/unknown/Unknown32_PsiWin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/unknown/Unknown32_PsiWin.png -------------------------------------------------------------------------------- /graphics/assets/word/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/graphics/assets/word/word.png -------------------------------------------------------------------------------- /images/screenshot@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/images/screenshot@2x.png -------------------------------------------------------------------------------- /profiles/Reconnect_Developer_ID_Profile.provisionprofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/profiles/Reconnect_Developer_ID_Profile.provisionprofile -------------------------------------------------------------------------------- /profiles/Reconnect_Menu_Developer_ID_Profile.provisionprofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/profiles/Reconnect_Menu_Developer_ID_Profile.provisionprofile -------------------------------------------------------------------------------- /scripts/build-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reconnect -- Psion connectivity for macOS 4 | # 5 | # Copyright (C) 2024-2025 Jason Morley 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | 21 | set -e 22 | set -o pipefail 23 | set -x 24 | set -u 25 | 26 | SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 27 | 28 | ROOT_DIRECTORY="$SCRIPTS_DIRECTORY/.." 29 | WEBSITE_DIRECTORY="$ROOT_DIRECTORY/docs" 30 | WEBSITE_SIMULATOR_DIRECTORY="$ROOT_DIRECTORY/docs/simulator" 31 | SIMULATOR_WEB_DIRECTORY="$ROOT_DIRECTORY/simulator/web" 32 | 33 | RELEASE_NOTES_TEMPLATE_PATH="$SCRIPTS_DIRECTORY/release-notes.md" 34 | HISTORY_PATH="$SCRIPTS_DIRECTORY/history.yaml" 35 | RELEASE_NOTES_DIRECTORY="$ROOT_DIRECTORY/docs/release-notes" 36 | RELEASE_NOTES_PATH="$RELEASE_NOTES_DIRECTORY/index.markdown" 37 | 38 | source "$SCRIPTS_DIRECTORY/environment.sh" 39 | 40 | cd "$ROOT_DIRECTORY" 41 | if [ -d "$RELEASE_NOTES_DIRECTORY" ]; then 42 | rm -r "$RELEASE_NOTES_DIRECTORY" 43 | fi 44 | "$SCRIPTS_DIRECTORY/update-release-notes.sh" 45 | 46 | # Install the Jekyll dependencies. 47 | export GEM_HOME="${ROOT_DIRECTORY}/.local/ruby" 48 | mkdir -p "$GEM_HOME" 49 | export PATH="${GEM_HOME}/bin":$PATH 50 | gem install bundler 51 | cd "${WEBSITE_DIRECTORY}" 52 | bundle install 53 | 54 | # Get the latest release URL. 55 | if ! DOWNLOAD_URL=$(build-tools latest-github-release inseven reconnect "Reconnect-*.zip"); then 56 | echo >&2 failed 57 | exit 1 58 | fi 59 | # Belt-and-braces check that we managed to get the download URL. 60 | if [[ -z "$DOWNLOAD_URL" ]]; then 61 | echo "Failed to get release download URL." 62 | exit 1 63 | fi 64 | 65 | # Build the website. 66 | cd "${WEBSITE_DIRECTORY}" 67 | export DOWNLOAD_URL 68 | bundle exec jekyll build 69 | -------------------------------------------------------------------------------- /scripts/environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reconnect -- Psion connectivity for macOS 4 | # 5 | # Copyright (C) 2024-2025 Jason Morley 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | 21 | SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 22 | ROOT_DIRECTORY="$SCRIPTS_DIRECTORY/.." 23 | 24 | export LOCAL_TOOLS_PATH="$ROOT_DIRECTORY/.local" 25 | 26 | export PYTHONUSERBASE="$LOCAL_TOOLS_PATH/python" 27 | mkdir -p "$PYTHONUSERBASE" 28 | export PATH="$PYTHONUSERBASE/bin":$PATH 29 | export PYTHONPATH=$PYTHONUSERBASE 30 | 31 | export PATH=$PATH:"$SCRIPTS_DIRECTORY/changes" 32 | export PATH=$PATH:"$SCRIPTS_DIRECTORY/build-tools" 33 | export PATH=$PATH:"$ROOT_DIRECTORY/dependencies/diligence/scripts" 34 | -------------------------------------------------------------------------------- /scripts/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reconnect -- Psion connectivity for macOS 4 | # 5 | # Copyright (C) 2024-2025 Jason Morley 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | 21 | set -e 22 | set -o pipefail 23 | set -x 24 | set -u 25 | 26 | SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 27 | ROOT_DIRECTORY="$SCRIPTS_DIRECTORY/.." 28 | CHANGES_DIRECTORY="$SCRIPTS_DIRECTORY/changes" 29 | BUILD_TOOLS_DIRECTORY="$SCRIPTS_DIRECTORY/build-tools" 30 | 31 | source "$SCRIPTS_DIRECTORY/environment.sh" 32 | 33 | if [ -d "$LOCAL_TOOLS_PATH" ] ; then 34 | rm -r "$LOCAL_TOOLS_PATH" 35 | fi 36 | 37 | python -m pip install --target "$PYTHONUSERBASE" --upgrade pipenv wheel 38 | PIPENV_PIPFILE="$CHANGES_DIRECTORY/Pipfile" pipenv install 39 | PIPENV_PIPFILE="$BUILD_TOOLS_DIRECTORY/Pipfile" pipenv install 40 | -------------------------------------------------------------------------------- /scripts/release-notes.html: -------------------------------------------------------------------------------- 1 | {% for release in releases -%} 2 | {{ release.version }} 3 |
    4 | {% for section in release.sections %}{% for change in section.changes | reverse -%} 5 |
  • {{ change.description }}{% if change.scope %}{{ change.scope }}{% endif %}
  • 6 | {% endfor %}{% endfor %} 7 |
8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /scripts/release-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Releases 3 | --- 4 | 5 | # Releases 6 | 7 | {% for release in releases -%} 8 | ## {% if release.is_released %}{{ release.version }}{% else %}{{ release.version }} (Unreleased){% endif %} 9 | {% for section in release.sections -%} 10 | {% for change in section.changes | reverse -%} 11 | - {{ change.description | regex_replace("\\s+\\(#(\\d+)\\)$", "") }}{% if change.scope %}{{ change.scope }}{% endif %} 12 | {% endfor %}{% endfor %} 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reconnect -- Psion connectivity for macOS 4 | # 5 | # Copyright (C) 2024-2025 Jason Morley 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | 21 | set -e 22 | set -o pipefail 23 | set -x 24 | 25 | # Actually make the release. 26 | gh release create "$CHANGES_TAG" --title "$CHANGES_QUALIFIED_TITLE" --notes-file "$CHANGES_NOTES_FILE" 27 | 28 | # Upload the attachments. 29 | for attachment in "$@" 30 | do 31 | gh release upload "$CHANGES_TAG" "$attachment" 32 | done 33 | -------------------------------------------------------------------------------- /scripts/update-release-notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reconnect -- Psion connectivity for macOS 4 | # 5 | # Copyright (C) 2024-2025 Jason Morley 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | 21 | set -e 22 | set -o pipefail 23 | set -x 24 | set -u 25 | 26 | SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 27 | 28 | ROOT_DIRECTORY="$SCRIPTS_DIRECTORY/.." 29 | RELEASE_NOTES_TEMPLATE_PATH="$SCRIPTS_DIRECTORY/release-notes.md" 30 | RELEASE_NOTES_DIRECTORY="$ROOT_DIRECTORY/docs/releases" 31 | RELEASE_NOTES_PATH="$RELEASE_NOTES_DIRECTORY/index.md" 32 | 33 | source "$SCRIPTS_DIRECTORY/environment.sh" 34 | 35 | cd "$ROOT_DIRECTORY" 36 | 37 | mkdir -p "$RELEASE_NOTES_DIRECTORY" 38 | changes notes --all --template "$RELEASE_NOTES_TEMPLATE_PATH" > "$RELEASE_NOTES_PATH" 39 | -------------------------------------------------------------------------------- /utilities/screenshot.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inseven/reconnect/ab526d45df12a5b7393a99da57d550dd8c6fab00/utilities/screenshot.exe --------------------------------------------------------------------------------