├── .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 | [](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 | Reconnect
10 | {{ site.description }}
11 |
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
--------------------------------------------------------------------------------