diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 00000000..058a593e
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,10 @@
+/* eslint-env node */
+module.exports = {
+ extends: ['plugin:@typescript-eslint/recommended'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint'],
+ root: true,
+ rules: {
+ "@typescript-eslint/ban-ts-comment": "off"
+ }
+};
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 722947c4..1cdec8d7 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Node setup
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: latest
- name: Install yarn
run: npm install --global yarn
- name: Install dependecies
@@ -31,7 +31,10 @@ jobs:
- name: Unit Tests
run: yarn test
- name: Acceptance Tests
- run: PEERBOOK_REF="${{ github.event.inputs.pbRef }}" WEBEXEC_REF="${{ github.event.inputs.weRef }}" bash -x aatp/run
+ run: >
+ PEERBOOK_REF="${{ github.event.inputs.pbRef }}"
+ WEBEXEC_REF="${{ github.event.inputs.weRef }}"
+ bash -x aatp/run
- name: Upload acceptance test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index e21c4d8d..aec034ec 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -1,17 +1,17 @@
name: Validate
-on: push
+on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Node setup
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 20.8.1
- name: Install yarn
run: npm install --global yarn
- name: Install dependecies
diff --git a/.gitignore b/.gitignore
index 260a4da3..1b798851 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,6 @@ aatp/**/authorized_fingerprints
# Local Netlify folder
.netlify
+
+# idea
+.idea/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63eae69e..4fe4045c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,22 @@ will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.8.1] - 2023/9/14
+
+### Fixed
+
+- PeerBook status indicator is always an emoji, no kanji chars
+
+## [1.8.0] - 2023/9/10
+
+### Added
+
+- dashed underline to link on the map - just like in TWR
+- Progressive Web App support
+- synchronizing layout across all connected client
+- the login command for web clients
+- peerbook spinner and indicator
+
## 1.7.1 - 2023/8/27
### Fixed
diff --git a/__mocks__/@revenuecat/purchases-capacitor.ts b/__mocks__/@revenuecat/purchases-capacitor.ts
new file mode 100644
index 00000000..e5472179
--- /dev/null
+++ b/__mocks__/@revenuecat/purchases-capacitor.ts
@@ -0,0 +1,7 @@
+import { vi } from "vitest"
+
+export const configure = vi.fn().mockResolvedValue(undefined);
+export const setMockWebResults = vi.fn().mockResolvedValue(undefined);
+export const getOfferings = vi.fn().mockResolvedValue({ offerings: [] });
+export const getProducts = vi.fn().mockResolvedValue({ products: [] });
+// ...mock other methods as needed
diff --git a/__mocks__/xterm.ts b/__mocks__/xterm.ts
index 12387eb0..29daed19 100644
--- a/__mocks__/xterm.ts
+++ b/__mocks__/xterm.ts
@@ -9,6 +9,10 @@ export class Terminal {
}
element = {
addEventListener: vi.fn(),
+ parentElement: {
+ clientHeight: 480,
+ clientWidth: 640
+ }
}
constructor (props) {
this.out = ""
@@ -62,5 +66,18 @@ export class Terminal {
const ev = new KeyboardEvent("keydown", { key })
this.keyHandler( { domEvent: ev } )
}
+ resize:(columns: number, rows: number) => void = vi.fn();
+ _core = {
+ _renderService: {
+ dimensions: {
+ css: {
+ cell: {
+ width: 5,
+ height: 11
+ }
+ }
+ }
+ }
+ }
}
diff --git a/aatp/http_webrtc/http_webrtc.spec.ts b/aatp/http_webrtc/http_webrtc.spec.ts
index 256dd238..ebf276ce 100644
--- a/aatp/http_webrtc/http_webrtc.spec.ts
+++ b/aatp/http_webrtc/http_webrtc.spec.ts
@@ -100,8 +100,8 @@ pinch_max_y_velocity = 0.1`
await page.screenshot({ path: `/result/2.png` })
await expect(page.locator('.pane')).toHaveCount(2)
})
- test('a pane can be close', async() => {
- sleep(500)
+ test('a pane can be closed', async() => {
+ await sleep(500)
const exitState = await page.evaluate(() => {
try {
window.terminal7.activeG.activeW.activeP.d.send("exit\n")
@@ -120,7 +120,7 @@ pinch_max_y_velocity = 0.1`
await page.screenshot({ path: `/result/second.png` })
const lines = await page.evaluate(() =>
window.terminal7.activeG.activeW.activeP.t.buffer.active.length)
- expect(lines).toEqual(38)
+ await expect(lines).toEqual(38)
await page.evaluate(async() => {
const gate = window.terminal7.activeG
gate.disengage().then(() => {
diff --git a/aatp/infra/webexec/docker_entry.sh b/aatp/infra/webexec/docker_entry.sh
index ac775f82..5dfd6797 100644
--- a/aatp/infra/webexec/docker_entry.sh
+++ b/aatp/infra/webexec/docker_entry.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -x
-EXE="/usr/local/bin/webexec"
+
HOME=/home/runner
CONF=/conf
@@ -19,8 +19,7 @@ if [[ $PEERBOOK == "1" ]]
then
/scripts/wait-for-it.sh -h peerbook -p 17777
fi
-su -c "$EXE start --debug" runner
while true
do
- sleep 1
+ su - runner -c '/usr/local/bin/webexec start --debug'
done
diff --git a/aatp/peerbook_webrtc/admin.spec.ts b/aatp/peerbook_webrtc/admin.spec.ts
index 40691ed8..8ab4f1ee 100644
--- a/aatp/peerbook_webrtc/admin.spec.ts
+++ b/aatp/peerbook_webrtc/admin.spec.ts
@@ -80,7 +80,7 @@ test.describe('peerbook administration', () => {
expect(pbOpen).toBeFalsy()
})
test('purchase update with an active subscription and bad otp', async () => {
- await sleep(500)
+ await page.keyboard.press("Enter")
await redisClient.set("tempid:$ValidBearer", "1")
await sleep(1500)
await page.evaluate(async () => {
diff --git a/aatp/peerbook_webrtc/lab.yaml b/aatp/peerbook_webrtc/lab.yaml
index 234f72f2..48e7de51 100644
--- a/aatp/peerbook_webrtc/lab.yaml
+++ b/aatp/peerbook_webrtc/lab.yaml
@@ -54,7 +54,6 @@ services:
PB_HOME_URL: http://peerbook:17777
redis:
image: "redis:alpine"
- command: [sh, -c, "rm -f /data/dump.rdb && redis-server"] # disable persistence
revenuecat:
image: "mockserver/mockserver"
environment:
@@ -65,8 +64,7 @@ services:
volumes:
- ./aatp/peerbook_webrtc:/config
smtp:
- image: mailhog:local
- build: https://github.com/mailhog/MailHog.git
+ image: jcalonso/mailhog
expose:
- 1025
- 8025
diff --git a/aatp/peerbook_webrtc/session.spec.ts b/aatp/peerbook_webrtc/session.spec.ts
index b6ae081b..4ead6234 100644
--- a/aatp/peerbook_webrtc/session.spec.ts
+++ b/aatp/peerbook_webrtc/session.spec.ts
@@ -144,32 +144,27 @@ insecure = true
await expect(page.locator('.pane')).toHaveCount(1)
})
test('disengage and reconnect', async() => {
- await page.evaluate(async() => {
- const gate = window.terminal7.activeG
- gate.activeW.activeP.d.send("seq 10; sleep 1; seq 10 20\n")
+ await page.evaluate(async() =>
+ await window.terminal7.activeG.activeW.activeP.d.send(
+ "seq 10; sleep 1; seq 10 20\n"))
+ await page.screenshot({ path: `/result/first.png` })
+ const y1 = await page.evaluate(async() => {
+ const ret = window.terminal7.activeG.activeW.activeP.t.buffer.active.cursorY
+ window.terminal7.onAppStateChange({isActive: false})
+ return ret
})
- await sleep(100)
await page.screenshot({ path: `/result/second.png` })
- const lines1 = await page.evaluate(async() => {
- const gate = window.terminal7.activeG
- await gate.disengage().then(() => {
- window.terminal7.clearTimeouts()
- window.terminal7.activeG.session = null
- })
- return gate.activeW.activeP.t.buffer.active.length
- })
await sleep(1000)
await page.screenshot({ path: `/result/third.png` })
- await page.evaluate(async() => {
- window.terminal7.activeG.connect()
- })
+ await page.evaluate(async() =>
+ window.terminal7.onAppStateChange({isActive: true}))
await expect(page.locator('.pane')).toHaveCount(1)
await sleep(1500)
- const lines2 = await page.evaluate(() =>
- window.terminal7.activeG.activeW.activeP.t.buffer.active.length)
+ const y2 = await page.evaluate(() =>
+ window.terminal7.activeG.activeW.activeP.t.buffer.active.cursorY)
await page.screenshot({ path: `/result/fourth.png` })
- console.log(lines1, lines2)
- expect(lines2-lines1).toEqual(11)
+ console.log(y1, y2)
+ expect(y2-y1).toEqual(11)
})
test('after disengage & reconnect, a a pane can be close', async() => {
await page.screenshot({ path: `/result/fifth.png` })
diff --git a/aatp/ui/ui.spec.ts b/aatp/ui/ui.spec.ts
index 891f9246..3bc0717b 100644
--- a/aatp/ui/ui.spec.ts
+++ b/aatp/ui/ui.spec.ts
@@ -103,7 +103,7 @@ insecure=true`)
await page.screenshot({ path: `/result/2.png` })
await page.locator('.tabbar .reset').click()
await expect(page.locator('#t0')).toBeVisible()
- sleep(20)
+ await sleep(20)
await page.keyboard.press('Enter')
await expect(page.locator('#t0')).toBeHidden()
await expect(page.locator('.pane')).toHaveCount(1)
diff --git a/android/.idea/kotlinc.xml b/android/.idea/kotlinc.xml
index 0e65ceac..69e86158 100644
--- a/android/.idea/kotlinc.xml
+++ b/android/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
index c986c771..89efa49f 100644
--- a/android/.idea/misc.xml
+++ b/android/.idea/misc.xml
@@ -1,9 +1,12 @@
-
+
+
+
+
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5be69bf2..347b6ffa 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -8,8 +8,8 @@ android {
applicationId "dev.terminal7"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 5
- versionName "1.7.1"
+ versionCode 6
+ versionName "1.8.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index d3591c12..4ad47812 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -18,7 +18,7 @@ dependencies {
implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-status-bar')
- implementation project(':capgo-capacitor-purchases')
+ implementation project(':revenuecat-purchases-capacitor')
implementation project(':capacitor-native-biometric')
implementation project(':capacitor-rate-app')
implementation project(':capacitor-ssh-plugin')
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index e9aa8c0c..4657a022 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -36,8 +36,8 @@
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
},
{
- "pkg": "@capgo/capacitor-purchases",
- "classpath": "ee.forgr.plugin.capacitor_purchases.CapacitorPurchasesPlugin"
+ "pkg": "@revenuecat/purchases-capacitor",
+ "classpath": "com.revenuecat.purchases.capacitor.PurchasesPlugin"
},
{
"pkg": "capacitor-native-biometric",
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index f8a1cd8d..d94843c7 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -29,8 +29,8 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
-include ':capgo-capacitor-purchases'
-project(':capgo-capacitor-purchases').projectDir = new File('../node_modules/@capgo/capacitor-purchases/android')
+include ':revenuecat-purchases-capacitor'
+project(':revenuecat-purchases-capacitor').projectDir = new File('../node_modules/@revenuecat/purchases-capacitor/android')
include ':capacitor-native-biometric'
project(':capacitor-native-biometric').projectDir = new File('../node_modules/capacitor-native-biometric/android')
diff --git a/css/terminal7.css b/css/terminal7.css
index 2c305248..1d6711d6 100644
--- a/css/terminal7.css
+++ b/css/terminal7.css
@@ -367,7 +367,6 @@ button.rename-close {
position: absolute;
top: 128px;
left: 20px;
- max-width: 35vw;
padding: 10px;
border: 1px dotted;
border-radius: 4px;
@@ -375,9 +374,20 @@ button.rename-close {
line-height: 12px;
}
#version a {
- text-decoration: underline;
+ text-decoration-style: dashed;
+ text-decoration-line: underline;
cursor: pointer;
}
+#version ul {
+ list-style: none;
+ text-align: left;
+ padding-left: 0;
+
+}
+#version li .status {
+ float: right;
+ padding-left: 0;
+}
#version footer {
font-size: 10px;
}
@@ -475,10 +485,30 @@ label {
height: 100%;
width: 100%;
}
+@media (min-width: 600px) {
+ #title-short {
+ display: none;
+ }
+ #title-long {
+ display: block;
+ }
+ #version {
+ max-width: 35vw;
+ }
+}
@media (max-width: 600px) {
#log {
width: 90vw;
}
+ #title-short {
+ display: block;
+ }
+ #title-long {
+ display: none;
+ }
+ #version {
+ max-width: 35vw;
+ }
}
.search-container {
margin: 0 auto;
@@ -787,17 +817,8 @@ div.boarding {
text-decoration: none;
}
#title-short {
- display: none;
letter-spacing: 0.1em;
}
-@media (max-width: 600px) {
- #title-short {
- display: block;
- }
- #title-long {
- display: none;
- }
-}
.expand-gate {
display: block;
position: absolute;
diff --git a/index.html b/index.html
index 3e40d2cd..c3d32ab8 100644
--- a/index.html
+++ b/index.html
@@ -13,7 +13,8 @@
-
+
+
@@ -179,16 +190,20 @@ Network is down
@@ -255,11 +270,11 @@ Error
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index 015c388b..1f2031e7 100644
--- a/ios/App/App.xcodeproj/project.pbxproj
+++ b/ios/App/App.xcodeproj/project.pbxproj
@@ -375,7 +375,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = VB5MV6CCSY;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Terminal7;
@@ -385,7 +385,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7.1;
+ MARKETING_VERSION = 1.8.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = dev.terminal7;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -406,7 +406,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = VB5MV6CCSY;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Terminal7;
@@ -416,7 +416,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7.1;
+ MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.terminal7;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/ios/App/Podfile b/ios/App/Podfile
index 63096e57..47c4d0b7 100644
--- a/ios/App/Podfile
+++ b/ios/App/Podfile
@@ -1,4 +1,15 @@
-require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
+def assertDeploymentTarget(installer)
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ # ensure IPHONEOS_DEPLOYMENT_TARGET is at least 13.0
+ deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f
+ should_upgrade = deployment_target < 13.0 && deployment_target != 0.0
+ if should_upgrade
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
+ end
+ end
+ end
+end
platform :ios, '15.4'
use_frameworks!
@@ -19,7 +30,7 @@ def capacitor_pods
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
- pod 'CapgoCapacitorPurchases', :path => '../../node_modules/@capgo/capacitor-purchases'
+ pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor'
pod 'CapacitorNativeBiometric', :path => '../../node_modules/capacitor-native-biometric'
pod 'CapacitorRateApp', :path => '../../node_modules/capacitor-rate-app'
pod 'CapacitorSshPlugin', :path => '../../node_modules/capacitor-ssh-plugin'
diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock
index 39d23dd3..0bd8c381 100644
--- a/ios/App/Podfile.lock
+++ b/ios/App/Podfile.lock
@@ -1,15 +1,15 @@
PODS:
- - Capacitor (5.2.2):
+ - Capacitor (5.3.0):
- CapacitorCordova
- CapacitorApp (5.0.6):
- Capacitor
- CapacitorBrowser (5.0.6):
- Capacitor
- - CapacitorCamera (5.0.6):
+ - CapacitorCamera (5.0.7):
- Capacitor
- CapacitorClipboard (5.0.6):
- Capacitor
- - CapacitorCordova (5.2.2)
+ - CapacitorCordova (5.3.0)
- CapacitorDevice (5.0.6):
- Capacitor
- CapacitorKeyboard (5.0.6):
@@ -27,14 +27,13 @@ PODS:
- NMSSHT7 (~> 2.9)
- CapacitorStatusBar (5.0.6):
- Capacitor
- - CapgoCapacitorPurchases (5.0.8):
- - Capacitor
- - PurchasesHybridCommon (= 4.8.0)
- - RevenueCat (>= 4.16.0)
- NMSSHT7 (2.9.1)
- - PurchasesHybridCommon (4.8.0):
- - RevenueCat (= 4.16.0)
- - RevenueCat (4.16.0)
+ - PurchasesHybridCommon (7.0.0):
+ - RevenueCat (= 4.27.0)
+ - RevenueCat (4.27.0)
+ - RevenuecatPurchasesCapacitor (7.0.0):
+ - Capacitor
+ - PurchasesHybridCommon (= 7.0.0)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
@@ -51,7 +50,7 @@ DEPENDENCIES:
- CapacitorRateApp (from `../../node_modules/capacitor-rate-app`)
- CapacitorSshPlugin (from `../../node_modules/capacitor-ssh-plugin`)
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- - "CapgoCapacitorPurchases (from `../../node_modules/@capgo/capacitor-purchases`)"
+ - "RevenuecatPurchasesCapacitor (from `../../node_modules/@revenuecat/purchases-capacitor`)"
SPEC REPOS:
trunk:
@@ -88,16 +87,16 @@ EXTERNAL SOURCES:
:path: "../../node_modules/capacitor-ssh-plugin"
CapacitorStatusBar:
:path: "../../node_modules/@capacitor/status-bar"
- CapgoCapacitorPurchases:
- :path: "../../node_modules/@capgo/capacitor-purchases"
+ RevenuecatPurchasesCapacitor:
+ :path: "../../node_modules/@revenuecat/purchases-capacitor"
SPEC CHECKSUMS:
- Capacitor: 070b18988e0f566728ae9a5eb3a7a974595f1626
+ Capacitor: 1ac9165943bc4f2137642d218c5ba05df811de69
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
CapacitorBrowser: 6192948e0ce804fd72aaf77f4114a3ad2e08c760
- CapacitorCamera: 4a95204d13a05b0b726bf9086b44124349ab1952
+ CapacitorCamera: 084b0b228bba7d00587910337b1c89e93b1d32ab
CapacitorClipboard: 77edf49827ea21da2a9c05c690a4a6a4d07199c4
- CapacitorCordova: 3773395d5331add072300ff6041ca2cf7b93cb0b
+ CapacitorCordova: b9374d68e63ce29e96ab5db994cf14fbefd722c9
CapacitorDevice: 2c968f98a1ec4d22357418c1521e7ddc46c675e6
CapacitorKeyboard: b978154b024a5f65e044908e37d15b7de58b9d12
CapacitorNativeBiometric: b47637a8cd349bdac014424bb4ddcae9ee5d4919
@@ -106,11 +105,11 @@ SPEC CHECKSUMS:
CapacitorRateApp: 9c63b7a25f281ce24bac929e8e8ed29a5c25252a
CapacitorSshPlugin: 79f67cb26d40bd0c07cec6dda93d93d5faf7b614
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309
- CapgoCapacitorPurchases: 8351f236aaa2329c1a172d8f762d7f6faddaaea9
NMSSHT7: 67f9d8f43b40b997728761b4cf2aa7ea660ba7c2
- PurchasesHybridCommon: bd1e1d3476afd3f95332b0ae160e4ce1bd1efd99
- RevenueCat: c12e14f5e3dc5732db6d5095ac4ce6ff08d7eeb0
+ PurchasesHybridCommon: af3b2413f9cb999bc1fdca44770bdaf39dfb89fa
+ RevenueCat: 84fbe2eb9bbf63e1abf346ccd3ff9ee45d633e3b
+ RevenuecatPurchasesCapacitor: c0de310959a58b3217acd7ec5ce981175bb050b1
-PODFILE CHECKSUM: ade1f161fe406ec0b829d2febee88fb9b125b5ab
+PODFILE CHECKSUM: 4e9654adb0765b7ee27622f314473ebfafd489a8
-COCOAPODS: 1.12.1
+COCOAPODS: 1.11.3
diff --git a/main.js b/main.ts
similarity index 89%
rename from main.js
rename to main.ts
index cb75b7c7..2609809d 100644
--- a/main.js
+++ b/main.ts
@@ -3,7 +3,7 @@ import "./css/xterm.css"
import "./css/framework7-icons.css"
import "./css/codemirror.css"
import "./css/dialog.css"
-import { Terminal7 } from "./src/terminal7.js"
+import { Terminal7 } from "./src/terminal7"
import { registerSW } from "virtual:pwa-register";
import { StatusBar, Style } from '@capacitor/status-bar';
@@ -22,8 +22,8 @@ document.addEventListener("DOMContentLoaded", async () => {
await StatusBar.show();
await StatusBar.setOverlaysWebView({ overlay: true });
} catch(e) {}
- window.terminal7 = new Terminal7()
- terminal7.open()
+ terminal7 = new Terminal7()
+ await terminal7.open()
}
})
window.addEventListener('beforeinstallprompt', e => {
diff --git a/package.json b/package.json
index baa468c8..15703ada 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "terminal7",
- "version": "1.7.1",
+ "version": "1.8.1",
"description": "A touchable terminal emulator & multiplexer tablet app",
"main": "src/index.js",
"homepage": "https://terminal7.dev",
@@ -30,8 +30,8 @@
"@capacitor/network": "^5.0.0",
"@capacitor/preferences": "^5.0.0",
"@capacitor/status-bar": "^5.0.0",
- "@capgo/capacitor-purchases": "^5.0.0",
"@liveconfig/xterm-webfont": "^2.1.0",
+ "@revenuecat/purchases-capacitor": "^7.0.0",
"@tuzig/codemirror": "5.65.5-mods",
"@tuzig/toml": "^3.0.0-browser",
"@types/marked": "^4.0.7",
@@ -54,6 +54,8 @@
"devDependencies": {
"@capacitor/assets": "^2.0.4",
"@playwright/test": "^1.20.1",
+ "@types/hammerjs": "^2.0.42",
+ "@types/xterm": "^3.0.0",
"@typescript-eslint/eslint-plugin": "<6",
"@typescript-eslint/parser": "<6",
"eslint": "<9",
@@ -76,7 +78,7 @@
"build": "vite build -c vite.dev.config.js",
"clean": "rm -rf dist ios/App/App/public/*",
"aatp": "bash ./aatp/run",
- "lint": "npx eslint aatp src --ext .js,.jsx,.ts,.tsx",
+ "lint": "npx eslint aatp src --ext .js,.ts && npx tsc --noEmit --target es2022 --moduleResolution node src/*.ts",
"capacitor:copy:before": "npm run build"
},
"repository": {
diff --git a/public/virtual-webgl2.js b/public/virtual-webgl2.js
new file mode 100644
index 00000000..accfdce5
--- /dev/null
+++ b/public/virtual-webgl2.js
@@ -0,0 +1,1408 @@
+/*
+ * Copyright 2018, Gregg Tavares.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Gregg Tavares. nor the names of his
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+/* eslint-env browser */
+
+(function() {
+ const settings = {
+ disableWebGL1: false,
+ compositorCreator() {
+ },
+ };
+ const canvasToVirtualContextMap = new Map();
+ const extensionInfo = {};
+ const extensionSaveRestoreHelpersArray = [];
+ const extensionSaveRestoreHelpers = {};
+
+ let currentVirtualContext = null;
+ let someContextsNeedRendering;
+
+ const sharedWebGLContext = document.createElement('canvas').getContext('webgl2');
+ const numAttribs = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_VERTEX_ATTRIBS);
+ const numTextureUnits = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
+ const numUniformBufferBindings = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_UNIFORM_BUFFER_BINDINGS);
+ const baseState = makeDefaultState(sharedWebGLContext, 300, 150);
+
+ const INT = 0x1404
+ const UNSIGNED_INT = 0x1405;
+ const FLOAT = 0x1406;
+
+ const vs = `
+ attribute vec4 position;
+ varying vec2 v_texcoord;
+ void main() {
+ gl_Position = position;
+ v_texcoord = position.xy * .5 + .5;
+ }
+ `;
+
+ const fs = `
+ precision mediump float;
+ varying vec2 v_texcoord;
+ uniform sampler2D u_tex;
+ void main() {
+ gl_FragColor = texture2D(u_tex, v_texcoord);
+ }
+ `;
+
+ const fs2 = `
+ precision mediump float;
+ varying vec2 v_texcoord;
+ uniform sampler2D u_tex;
+ void main() {
+ gl_FragColor = texture2D(u_tex, v_texcoord);
+ gl_FragColor.rgb *= gl_FragColor.a;
+ }
+ `;
+
+ const premultiplyAlphaTrueProgram = createProgram(sharedWebGLContext, [vs, fs]);
+ const premultiplyAlphaFalseProgram = createProgram(sharedWebGLContext, [vs, fs2]);
+
+ {
+ const gl = sharedWebGLContext;
+ const positionLoc = 0; // hard coded in createProgram
+
+ const buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ -1, -1,
+ 1, -1,
+ -1, 1,
+ -1, 1,
+ 1, -1,
+ 1, 1,
+ ]), gl.STATIC_DRAW);
+
+ gl.enableVertexAttribArray(positionLoc);
+ gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
+
+ }
+
+ saveAllState(baseState);
+
+ HTMLCanvasElement.prototype.getContext = (function(origFn) {
+ return function(type, contextAttributes) {
+ if (type === 'webgl' || type === 'experimental-webgl') {
+ return createOrGetVirtualWebGLContext(this, type, contextAttributes);
+ } else if (type === 'webgl2') {
+ return createOrGetVirtualWebGLContext(this, type, contextAttributes);
+ }
+ return origFn.call(this, type, contextAttributes);
+ };
+
+ }(HTMLCanvasElement.prototype.getContext));
+
+ function valueOrDefault(value, defaultValue) {
+ return value === undefined ? defaultValue : value;
+ }
+
+ function errorDisposedContext(fnName) {
+ return function() {
+ throw new Error(`tried to call ${fnName} on disposed context`);
+ };
+ }
+
+ class DefaultCompositor {
+ constructor(canvas) {
+ this._ctx = canvas.getContext('2d');
+ }
+ composite(gl, texture, canvas, contextAttributes) {
+ // note: not entirely sure what to do here. We need this canvas to be at least as large
+ // as the canvas we're drawing to. Resizing a canvas is slow so I think just making
+ // sure we never get smaller than the largest canvas. At the moment though I'm too lazy
+ // to make it smaller.
+ const ctx = this._ctx;
+ const width = canvas.width;
+ const height = canvas.height;
+ const maxWidth = Math.max(gl.canvas.width, width);
+ const maxHeight = Math.max(gl.canvas.height, height);
+ if (gl.canvas.width !== maxWidth || gl.canvas.height !== maxHeight) {
+ gl.canvas.width = maxWidth;
+ gl.canvas.height = maxHeight;
+ }
+
+ gl.viewport(0, 0, width, height);
+
+ gl.useProgram(contextAttributes.premultipliedAlpha ? premultiplyAlphaTrueProgram : premultiplyAlphaFalseProgram);
+
+ // draw the drawingbuffer's texture to the offscreen canvas
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+ // copy it to target canvas
+ ctx.globalCompositeOperation = 'copy';
+ ctx.drawImage(
+ gl.canvas,
+ 0, maxHeight - height, width, height, // src rect
+ 0, 0, width, height); // dest rect
+ }
+ dispose() {
+ }
+ }
+
+ function virtualGLConstruct(canvas, contextAttributes = {}, compositor, disposeHelper) {
+ const gl = sharedWebGLContext;
+ this._canvas = canvas;
+ // Should use Symbols or something to hide these variables from the outside.
+
+ this._compositor = compositor;
+ this._disposeHelper = disposeHelper;
+ this._extensions = {};
+ // based on context attributes and canvas.width, canvas.height
+ // create a texture and framebuffer
+ this._drawingbufferTexture = gl.createTexture();
+ this._drawingbufferFramebuffer = gl.createFramebuffer();
+ this._contextAttributes = {
+ alpha: valueOrDefault(contextAttributes.alpha, true),
+ antialias: false,
+ depth: valueOrDefault(contextAttributes.depth, true),
+ failIfMajorPerformanceCaveat: false,
+ premultipliedAlpha: valueOrDefault(contextAttributes.premultipliedAlpha, true),
+ stencil: valueOrDefault(contextAttributes.stencil, false),
+ };
+ this._preserveDrawingbuffer = valueOrDefault(contextAttributes.preserveDrawingBuffer, false);
+
+ const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
+ const oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
+
+ gl.bindTexture(gl.TEXTURE_2D, this._drawingbufferTexture);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ // this._drawingbufferTexture.id = canvas.id;
+ // this._drawingbufferFramebuffer.id = canvas.id;
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._drawingbufferFramebuffer);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._drawingbufferTexture, 0);
+
+ if (this._contextAttributes.depth) {
+ const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
+ this._depthRenderbuffer = gl.createRenderbuffer();
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this._depthRenderbuffer);
+ const attachmentPoint = this._contextAttributes.stencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachmentPoint, gl.RENDERBUFFER, this._depthRenderbuffer);
+ gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
+ }
+
+ gl.bindTexture(gl.TEXTURE_2D, oldTexture);
+ gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
+
+ // remember all WebGL state (default bindings, default texture units,
+ // default attributes and/or vertex shade object, default program,
+ // default blend, stencil, zBuffer, culling, viewport etc... state
+ this._state = makeDefaultState(gl, canvas.width, canvas.height);
+ this._state.readFramebuffer = this._drawingbufferFramebuffer;
+ this._state.drawFramebuffer = this._drawingbufferFramebuffer;
+ this._state.readBuffer = gl.COLOR_ATTACHMENT0;
+
+ this._state.vertexArray = gl.createVertexArray();
+ this._defaultVertexArray = this._state.vertexArray;
+ }
+
+ function virtualGLDispose() {
+ this._disposeHelper();
+ const gl = sharedWebGLContext;
+ gl.deleteFramebuffer(this._drawingbufferFramebuffer);
+ gl.deleteTexture(this._drawingbufferTexture);
+ if (this._depthRenderbuffer) {
+ gl.deleteRenderbuffer(this._depthRenderbuffer);
+ }
+ if (this._compositor.dispose) {
+ this._compositor.dispose();
+ }
+ for (const [key, value] of Object.entries(this)) {
+ if (typeof value === 'function') {
+ this[key] = errorDisposedContext(key);
+ }
+ }
+ for (const [key, value] of Object.entries(this.prototype)) {
+ if (typeof value === 'function') {
+ this[key] = errorDisposedContext(key);
+ }
+ }
+ }
+
+ function virtualGLComposite(gl) {
+ this._compositor.composite(gl, this._drawingbufferTexture, this.canvas, this._contextAttributes);
+ if (!this._preserveDrawingbuffer) {
+ this._needClear = true;
+ }
+ }
+
+ // Base exists so VirtualWebGLContext has a base class we can replace
+ // because otherwise it's base is Object which we can't replace.
+ class Base {}
+ class VirtualWebGLContext extends Base {
+ constructor(canvas, contextAttributes = {}, compositor, disposeHelper) {
+ super();
+ this.dispose = virtualGLDispose;
+ this.composite = virtualGLComposite;
+ virtualGLConstruct.call(this, canvas, contextAttributes, compositor, disposeHelper);
+ }
+ get canvas() {
+ return this._canvas;
+ }
+ get drawingBufferWidth() {
+ return this.canvas.width;
+ }
+ get drawingBufferHeight() {
+ return this.canvas.height;
+ }
+ }
+ class Base2 {}
+ class VirtualWebGL2Context extends Base2 {
+ constructor(canvas, contextAttributes = {}, compositor, disposeHelper) {
+ super();
+ this.dispose = virtualGLDispose;
+ this.composite = virtualGLComposite;
+ virtualGLConstruct.call(this, canvas, contextAttributes, compositor, disposeHelper);
+ }
+ get canvas() {
+ return this._canvas;
+ }
+ get drawingBufferWidth() {
+ return this.canvas.width;
+ }
+ get drawingBufferHeight() {
+ return this.canvas.height;
+ }
+ }
+
+ // Replace the prototype with WebGL2RenderingContext so that someCtx instanceof WebGL2RenderingContext returns true
+ Object.setPrototypeOf(Object.getPrototypeOf(VirtualWebGLContext.prototype), WebGLRenderingContext.prototype);
+ Object.setPrototypeOf(Object.getPrototypeOf(VirtualWebGL2Context.prototype), WebGL2RenderingContext.prototype);
+
+ function makeDefaultState(gl, width, height) {
+ const vao = gl.createVertexArray();
+ gl.bindVertexArray(vao);
+
+ const state = {
+ arrayBuffer: null,
+ renderbuffer: null,
+ drawFramebuffer: null,
+ readFramebuffer: null,
+ copyReadBuffer: null,
+ copyWriteBuffer: null,
+ pixelPackBuffer: null,
+ pixelUnpackBuffer: null,
+ transformFeedbackBuffer: null,
+ uniformBuffer: null,
+
+ readBuffer: gl.BACK,
+
+ enable: new Map([
+ [ gl.BLEND, false ],
+ [ gl.CULL_FACE, false ],
+ [ gl.DEPTH_TEST, false ],
+ [ gl.DITHER, false ],
+ [ gl.POLYGON_OFFSET_FILL, false ],
+ [ gl.RASTERIZER_DISCARD, false ],
+ [ gl.SAMPLE_ALPHA_TO_COVERAGE, false ],
+ [ gl.SAMPLE_COVERAGE, false ],
+ [ gl.SCISSOR_TEST, false ],
+ [ gl.STENCIL_TEST, false ],
+ ]),
+
+ // This is a place the spec gets wrong! This data should have been part of a VertexArray
+ attribValues: new Array(numAttribs).fill(0).map(() => {
+ return {
+ type: gl.FLOAT,
+ value: [0, 0, 0, 1],
+ };
+ }),
+
+ vertexArray: vao,
+ activeTexture: gl.TEXTURE0,
+ transformFeedback: null,
+
+ pack: new Map([
+ [ gl.PACK_ALIGNMENT, 4],
+ [ gl.UNPACK_ALIGNMENT, 4],
+ [ gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.BROWSER_DEFAULT_WEBGL],
+ [ gl.UNPACK_FLIP_Y_WEBGL, 0],
+ [ gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0],
+ [ gl.UNPACK_ROW_LENGTH , 0],
+ [ gl.UNPACK_SKIP_ROWS , 0],
+ [ gl.UNPACK_SKIP_PIXELS , 0],
+ [ gl.UNPACK_SKIP_IMAGES , 0],
+ [ gl.UNPACK_IMAGE_HEIGHT , 0],
+ [ gl.PACK_ROW_LENGTH , 0],
+ [ gl.PACK_SKIP_ROWS , 0],
+ [ gl.PACK_SKIP_PIXELS , 0],
+ ]),
+
+ currentProgram: null,
+ viewport: [0, 0, width, height],
+ scissor: [0, 0, width, height],
+ blendSrcRgb: gl.ONE,
+ blendDstRgb: gl.ZERO,
+ blendSrcAlpha: gl.ONE,
+ blendDstAlpha: gl.ZERO,
+ blendEquationRgb: gl.FUNC_ADD,
+ blendEquationAlpha: gl.FUNC_ADD,
+ blendColor: [0, 0, 0, 0],
+ clearColor: [0, 0, 0, 0],
+ colorMask: [true, true, true, true],
+ cullFaceMode: gl.BACK,
+ clearDepth: 1,
+ depthFunc: gl.LESS,
+ depthRange: [0, 1],
+ depthMask: true,
+ frontFace: gl.CCW,
+ generateMipmapHint: gl.DONT_CARE,
+ lineWidth: 1,
+ polygonOffsetFactor: 0,
+ polygonOffsetUnits: 0,
+ sampleCoverageValue: 1,
+ sampleCoverageUnits: false,
+ stencilFront: {
+ fail: gl.KEEP,
+ func: gl.ALWAYS,
+ depthFail: gl.KEEP,
+ depthPass: gl.KEEP,
+ ref: 0,
+ valueMask: 0xFFFFFFFF,
+ writeMask: 0xFFFFFFFF,
+ },
+ stencilBack: {
+ fail: gl.KEEP,
+ func: gl.ALWAYS,
+ depthFail: gl.KEEP,
+ depthPass: gl.KEEP,
+ ref: 0,
+ valueMask: 0xFFFFFFFF,
+ writeMask: 0xFFFFFFFF,
+ },
+ stencilClearValue: 0,
+
+ textureUnits: new Array(numTextureUnits).fill(0).map(() => {
+ return {
+ texture2D: null,
+ textureCubemap: null,
+ texture2DArray: null,
+ texture3D: null,
+ sampler: null,
+ };
+ }),
+ uniformBufferBindings: new Array(numUniformBufferBindings).fill(0).map(() => {
+ return {
+ buffer: null,
+ size: 0,
+ start: 0,
+ };
+ }),
+ };
+
+ return state;
+ }
+
+ function isFramebufferBindingNull(vCtx) {
+ return vCtx._state.drawFramebuffer === vCtx._drawingbufferFramebuffer;
+ }
+
+ function createWrapper(origFn/*, name*/) {
+ // lots of optimization could happen here depending on specific functions
+ return function(...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ return origFn.call(sharedWebGLContext, ...args);
+ };
+ }
+
+ function clearIfNeeded(vCtx) {
+ if (vCtx._needClear) {
+ vCtx._needClear = false;
+ const gl = sharedWebGLContext;
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vCtx._drawingbufferFramebuffer);
+ gl.disable(gl.SCISSOR_TEST);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+ enableDisable(gl, gl.SCISSOR_TEST, vCtx._state.scissorTest);
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, vCtx._state.drawFramebuffer);
+ }
+ }
+
+ function beforeDraw(vCtx) {
+ makeCurrentContext(vCtx);
+ resizeCanvasIfChanged(vCtx);
+ clearIfNeeded(vCtx);
+ }
+
+ function afterDraw(vCtx) {
+ if (isFramebufferBindingNull(vCtx)) {
+ vCtx._needComposite = true;
+ if (!someContextsNeedRendering) {
+ someContextsNeedRendering = true;
+ setTimeout(renderAllDirtyVirtualCanvases, 0);
+ }
+ }
+ }
+
+ function createDrawWrapper(origFn) {
+ return function(...args) {
+ // a rendering function was called so we need to copy are drawingBuffer
+ // to the canvas for this context after the current event.
+ beforeDraw(this);
+ const result = origFn.call(sharedWebGLContext, ...args);
+ afterDraw(this);
+ return result;
+ };
+ }
+
+ function createStateArgsSaverFn(fnName) {
+ return function(...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl[fnName](...args);
+ const v = this._state[fnName];
+ for (let i = 0; i < args.length; ++i) {
+ v[i] = args[i];
+ }
+ };
+ }
+
+ function createSaveStateNamedArgs(fnName, argsToStateProps) {
+ return function(...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl[fnName](...args);
+ for (let i = 0; i < argsToStateProps.length; ++i) {
+ this._state[argsToStateProps[i]] = args[i];
+ }
+ };
+ }
+
+ function createVertexAttribWrapper(origFn, fn) {
+ return function(loc, ...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ origFn.call(gl, loc, ...args);
+ const [type, value] = fn(args);
+ this._state.attribValues[loc] = {type, value};
+ };
+ }
+
+ function saveStencilMaskImpl(st, mask) {
+ st.writeMask = mask;
+ }
+
+ function saveStencilMask(state, face, mask) {
+ if (face === sharedWebGLContext.FRONT || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilMaskImpl(state.stencilFront, mask);
+ }
+ if (face === sharedWebGLContext.BACK || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilMaskImpl(state.stencilBack, mask);
+ }
+ }
+
+ function saveStencilFuncImpl(st, func, ref, mask) {
+ st.func = func;
+ st.ref = ref;
+ st.valueMask = mask;
+ }
+
+ function saveStencilFunc(state, face, func, ref, mask) {
+ if (face === sharedWebGLContext.FRONT || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilFuncImpl(state.stencilFront, func, ref, mask);
+ }
+ if (face === sharedWebGLContext.BACK || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilFuncImpl(state.stencilBack, func, ref, mask);
+ }
+ }
+
+ function saveStencilOpImpl(st, fail, zfail, zpass) {
+ st.fail = fail;
+ st.depthFail = zfail;
+ st.depthPass = zpass;
+ }
+
+ function saveStencilOp(state, face, fail, zfail, zpass) {
+ if (face === sharedWebGLContext.FRONT || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilOpImpl(state.stencilFront, fail, zfail, zpass);
+ }
+ if (face === sharedWebGLContext.BACK || face === sharedWebGLContext.FRONT_AND_BACK) {
+ saveStencilOpImpl(state.stencilBack, fail, zfail, zpass);
+ }
+ }
+
+ const virtualFns = {
+ getExtension(extensionName) {
+ extensionName = extensionName.toLowerCase();
+ // just like the real context each extension needs a virtual class because each use
+ // of the extension might be modified (as in people adding properties to it)
+ const existingExt = this._extensions[extensionName];
+ if (existingExt) {
+ return existingExt;
+ }
+
+ const ext = sharedWebGLContext.getExtension(extensionName);
+ if (!ext) {
+ return null;
+ }
+ const wrapperInfo = extensionInfo[extensionName] || {};
+ const wrapperFnMakerFn = wrapperInfo.wrapperFnMakerFn || (() => {
+ console.log('trying to get extension:', extensionName);
+ });
+ const saveRestoreHelper = extensionSaveRestoreHelpers[extensionName];
+ if (!saveRestoreHelper) {
+ const saveRestoreMakerFn = wrapperInfo.saveRestoreMakerFn;
+ if (saveRestoreMakerFn) {
+ const saveRestore = saveRestoreMakerFn(ext);
+ extensionSaveRestoreHelpers[extensionName] = saveRestore;
+ extensionSaveRestoreHelpersArray.push(saveRestore);
+ }
+ }
+
+ const wrapper = {
+ _context: this,
+ };
+ for (const key in ext) {
+ let value = ext[key];
+ if (typeof value === 'function') {
+ value = wrapperFnMakerFn(ext, value, extensionName);
+ }
+ wrapper[key] = value;
+ }
+
+ this._extensions[extensionName] = wrapper;
+
+ return wrapper;
+ },
+ activeTexture(unit) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.activeTexture(unit);
+ this._state.activeTexture = unit;
+ },
+ enable(pname) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.enable(pname);
+ this._state.enable.set(pname, true);
+ },
+ disable(pname) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.disable(pname);
+ this._state.enable.set(pname, false);
+ },
+ viewport: createStateArgsSaverFn('viewport'),
+ scissor: createStateArgsSaverFn('scissor'),
+ blendColor: createStateArgsSaverFn('blendColor'),
+ clearColor: createStateArgsSaverFn('clearColor'),
+ colorMask: createStateArgsSaverFn('colorMask'),
+ depthRange: createStateArgsSaverFn('depthRange'),
+ bindBuffer(target, buffer) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindBuffer(target, buffer);
+ switch (gl.target) {
+ case gl.ARRAY_BUFFER:
+ this._state.arrayBuffer = buffer;
+ break;
+ case gl.COPY_READ_BUFFER:
+ this._state.copyReadBuffer = buffer;
+ break;
+ case gl.COPY_WRITE_BUFFER:
+ this._state.copyWriteBuffer = buffer;
+ break;
+ case gl.PIXEL_PACK_BUFFER:
+ this._state.pixelPackBuffer = buffer;
+ break;
+ case gl.PIXEL_UNPACK_BUFFER:
+ this._state.pixelUnpackBuffer = buffer;
+ break;
+ case gl.TRANSFORM_FEEDBACK_BUFFER:
+ this._state.transformFeedbackBuffer = buffer;
+ break;
+ case gl.UNIFORM_BUFFER:
+ this._state.uniformBuffer = buffer;
+ }
+ },
+ bindBufferBase(target, index, buffer) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindBufferBase(target, index, buffer);
+ switch (target) {
+ case gl.UNIFORM_BUFFER: {
+ const ub = this._state.uniformBufferBindings[index];
+ ub.buffer = buffer;
+ ub.size = 0;
+ ub.start = 0;
+ break;
+ }
+ default:
+ break;
+ }
+ },
+ bindBufferRange(target, index, buffer, offset, size) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindBufferRange(target, index, buffer, offset, size);
+ switch (target) {
+ case gl.UNIFORM_BUFFER: {
+ const ub = this._state.uniformBufferBindings[index];
+ ub.buffer = buffer;
+ ub.size = size;
+ ub.start = offset;
+ break;
+ }
+ default:
+ break;
+ }
+ },
+ bindTexture(target, texture) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindTexture(target, texture);
+ const unit = this._state.textureUnits[this._state.activeTexture - gl.TEXTURE0];
+ switch (target) {
+ case gl.TEXTURE_2D:
+ unit.texture2D = texture;
+ break;
+ case gl.TEXTURE_CUBE_MAP:
+ unit.textureCubemap = texture;
+ break;
+ case gl.TEXTURE_2D_ARRAY:
+ unit.texture2DArray = texture;
+ break;
+ case gl.TEXTURE_3D:
+ unit.texture3D = texture;
+ break;
+ }
+ },
+ bindRenderbuffer(target, renderbuffer) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindRenderbuffer(target, renderbuffer);
+ this._state.renderbuffer = renderbuffer;
+ },
+ bindSampler(unit, sampler) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindSampler(unit, sampler);
+ this._state.textureUnits[unit].sampler = sampler;
+ },
+ bindVertexArray(va) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ if (va === null) {
+ va = this._defaultVertexArray;
+ }
+ gl.bindVertexArray(va);
+ this._state.vertexArray = va;
+ },
+ getContextAttributes() {
+ return this._contextAttributes;
+ },
+ readPixels(...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ clearIfNeeded(this);
+ const gl = sharedWebGLContext;
+ return gl.readPixels(...args);
+ },
+ getParameter(pname) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ const value = gl.getParameter(pname);
+ switch (pname) {
+ case gl.FRAMEBUFFER_BINDING:
+ if (value === this._drawingbufferFramebuffer) {
+ return null;
+ }
+ break;
+ case gl.DRAW_BUFFER0:
+ if (isFramebufferBindingNull(this)) {
+ if (value === gl.COLOR_ATTACHMENT0) {
+ return gl.BACK;
+ }
+ }
+ break;
+ case gl.READ_BUFFER:
+ if (isFramebufferBindingNull(this)) {
+ if (value === gl.COLOR_ATTACHMENT0) {
+ return gl.BACK;
+ }
+ }
+ break;
+ case gl.VERTEX_ARRAY_BINDING:
+ if (value === this._defaultVertexArray) {
+ return null;
+ }
+ break;
+ }
+ return value;
+ },
+ blendFunc(sfactor, dfactor) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.blendFunc(sfactor, dfactor);
+ this._state.blendSrcRgb = sfactor;
+ this._state.blendSrcAlpha = sfactor;
+ this._state.blendDstRgb = dfactor;
+ this._state.blendDstAlpha = dfactor;
+ },
+ blendEquation(mode) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.blendEquation(mode);
+ this._state.blendEquationRgb = mode;
+ this._state.blendEquationAlpha = mode;
+ },
+ blendFuncSeparate: createSaveStateNamedArgs('blendFuncSeparate', ['blendSrcRgb', 'blendDstRgb', 'blendSrcAlpha', 'blendDstAlpha']),
+ blendEquationSeparate: createSaveStateNamedArgs('blendEquationSeparate', ['blendEquationRgb', 'blendEquationAlpha']),
+ cullFace: createSaveStateNamedArgs('cullFace', ['cullFaceMode']),
+ clearDepth: createSaveStateNamedArgs('clearDepth', ['clearDepth']),
+ depthFunc: createSaveStateNamedArgs('depthFunc', ['depthFunc']),
+ depthMask: createSaveStateNamedArgs('depthMask', ['depthMask']),
+ frontFace: createSaveStateNamedArgs('frontFace', ['frontFace']),
+ lineWidth: createSaveStateNamedArgs('lineWidth', ['lineWidth']),
+ polygonOffset: createSaveStateNamedArgs('polygonOffset', ['polygonOffsetFactor', 'polygonOffsetUnits']),
+ sampleCoverage: createSaveStateNamedArgs('sampleCoverage', ['sampleCoverageValue', 'sampleCoverageUnits']),
+ clearStencil: createSaveStateNamedArgs('clearStencil', ['clearStencilValue']),
+ hint(pname, value) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.hint(pname, value);
+ this._state.generateMipmapHint = value;
+ },
+ bindFramebuffer(bindPoint, framebuffer) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ if (framebuffer === null) {
+ // bind our drawingBuffer
+ framebuffer = this._drawingbufferFramebuffer;
+ }
+ gl.bindFramebuffer(bindPoint, framebuffer);
+ switch (bindPoint) {
+ case gl.FRAMEBUFFER:
+ this._state.readFramebuffer = framebuffer;
+ this._state.drawFramebuffer = framebuffer;
+ break;
+ case gl.DRAW_FRAMEBUFFER:
+ this._state.drawFramebuffer = framebuffer;
+ break;
+ case gl.READ_FRAMEBUFFER:
+ this._state.readFramebuffer = framebuffer;
+ break;
+ }
+ },
+ drawBuffers: (function() {
+ const gl = sharedWebGLContext;
+ const backBuffer = [gl.COLOR_ATTACHMENT0];
+
+ return function(drawingBuffers) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ // if the virtual context is bound to canvas then fake it
+ if (isFramebufferBindingNull(this)) {
+ // this really isn't checking everything
+ // for example if the user passed in array.length != 1
+ // then we are supposed to generate an error
+ if (drawingBuffers[0] === gl.BACK) {
+ drawingBuffers = backBuffer;
+ }
+ }
+
+ gl.drawBuffers(drawingBuffers);
+ };
+ }()),
+ clear: createDrawWrapper(WebGL2RenderingContext.prototype.clear),
+ drawArrays: createDrawWrapper(WebGL2RenderingContext.prototype.drawArrays),
+ drawElements: createDrawWrapper(WebGL2RenderingContext.prototype.drawElements),
+ drawArraysInstanced: createDrawWrapper(WebGL2RenderingContext.prototype.drawArraysInstanced),
+ drawElementsInstanced: createDrawWrapper(WebGL2RenderingContext.prototype.drawElementsInstanced),
+ drawRangeElements: createDrawWrapper(WebGL2RenderingContext.prototype.drawRangeElements),
+ useProgram(program) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.useProgram(program);
+ this._state.currentProgram = program;
+ },
+ bindTransformFeedback(target, tb) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.bindTransformFeedback(target, tb);
+ this._state.transformFeedback = tb;
+ },
+ readBuffer(src) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ if (src === gl.BACK) {
+ src = gl.COLOR_ATTACHMENT0;
+ }
+ gl.readBuffer(src);
+ this._state.readBuffer = src;
+ },
+ stencilFunc(func, ref, mask) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilFunc(func, ref, mask);
+ saveStencilFunc(this._state, gl.FRONT_AND_BACK, func, ref, mask);
+ },
+ stencilFuncSeparate(face, func, ref, mask) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilFuncSeparate(face, func, ref, mask);
+ saveStencilFunc(this._state, face, func, ref, mask);
+ },
+ stencilMask(mask) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilMask(mask);
+ saveStencilMask(this._state, gl.FRONT_AND_BACK, mask);
+ },
+ stencilMaskSeparate(face, mask) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilMaskSeparate(face, mask);
+ saveStencilMask(this._state, face, mask);
+ },
+ stencilOp(fail, zfail, zpass) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilOp(fail, zfail, zpass);
+ saveStencilOp(this._state, gl.FRONT_AND_BACK, fail, zfail, zpass);
+ },
+ stencilOpSeparate(face, fail, zfail, zpass) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ gl.stencilOpSeparate(face, fail, zfail, zpass);
+ saveStencilOp(this._state, face, fail, zfail, zpass);
+ },
+ vertexAttrib1f: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1f, ([x]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2f: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1f, ([x, y]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3f: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1f, ([x, y, z]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4f: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1f, ([x, y, z, w]) => [FLOAT, [x, y, z, w]]),
+ vertexAttrib1fv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1fv, ([[x]]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2fv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1fv, ([[x, y]]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3fv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1fv, ([[x, y, z]]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4fv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1fv, ([[x, y, z, w]]) => [FLOAT, [x, y, z, w]]),
+ vertexAttrib1i: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1i, ([x]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2i: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1i, ([x, y]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3i: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1i, ([x, y, z]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4i: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1i, ([x, y, z, w]) => [FLOAT, [x, y, z, w]]),
+ vertexAttrib1iv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1iv, ([[x]]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2iv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1iv, ([[x, y]]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3iv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1iv, ([[x, y, z]]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4iv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1iv, ([[x, y, z, w]]) => [FLOAT, [x, y, z, w]]),
+ vertexAttrib1ui: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1ui, ([x]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2ui: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1ui, ([x, y]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3ui: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1ui, ([x, y, z]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4ui: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1ui, ([x, y, z, w]) => [FLOAT, [x, y, z, w]]),
+ vertexAttrib1uiv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1uiv, ([[x]]) => [FLOAT, [x, 0, 0, 1]]),
+ vertexAttrib2uiv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1uiv, ([[x, y]]) => [FLOAT, [x, y, 0, 1]]),
+ vertexAttrib3uiv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1uiv, ([[x, y, z]]) => [FLOAT, [x, y, z, 1]]),
+ vertexAttrib4uiv: createVertexAttribWrapper(WebGL2RenderingContext.prototype.vertexAttrib1uiv, ([[x, y, z, w]]) => [FLOAT, [x, y, z, w]]),
+ };
+
+ const webgl1Extensions = {
+ oes_texture_float: {
+ fn() {
+ return {};
+ },
+ },
+ oes_vertex_array_object: {
+ fn(vCtx) {
+ return {
+ VERTEX_ARRAY_BINDING_OES: sharedWebGLContext.VERTEX_ARRAY_BINDING,
+ createVertexArrayOES() {
+ return sharedWebGLContext.createVertexArray();
+ },
+ deleteVertexArrayOES(va) {
+ sharedWebGLContext.deleteVertexArray(va);
+ },
+ bindVertexArrayOES(va) {
+ virtualFns.bindVertexArray.call(vCtx, va);
+ },
+ isVertexArrayOES(va) {
+ return sharedWebGLContext.isVertexArray(va);
+ },
+ };
+ },
+ },
+ angle_instanced_arrays: {
+ fn(vCtx) {
+ return {
+ VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE: 0x88FE,
+ drawArraysInstancedANGLE(...args) {
+ virtualFns.drawArraysInstanced.call(vCtx, ...args);
+ },
+ drawElementsInstancedANGLE(...args) {
+ virtualFns.drawElementsInstanced.call(vCtx, ...args);
+ },
+ vertexAttribDivisorANGLE(...args) {
+ sharedWebGLContext.vertexAttribDivisor(...args);
+ },
+ };
+ },
+ },
+ // We can't easily support WebGL_draw_buffers because WebGL2 does not
+ // support gl_FragData. Instead, it requires you to write your shaders
+ // in GLSL ES 3.0 if you want to use multiple color attachments. To support
+ // that correctly would require writing a GLSL parser. We need to change
+ // 'attribute' -> 'in', 'varying' -> 'out' in vertex shaders, 'varying' to 'in'
+ // in fragment shader, 'texture2D' to 'texture' and declare outputs. That sounds
+ // simple but it quickly becomes complicated.
+ //
+ // * 'texture' is a valid identifier in GLSL ES 1.0 but a reserved word in
+ // GLSL ES 3.0 so we'd have to rename identifiers
+ //
+ // * Changing the fragment shader means it's on longer compatible with
+ // GLSL ES 1.0 vertex shaders. But, back in WebGL1, we could easily use
+ // the same vertex shader with and without WEBGL_draw_buffers. That means
+ // we now need 2 versions of every vertex shader (ES 1.0 and ES 3.0), OR
+ // we need to translate ALL shaders to GLSL ES 3.0
+ //
+ // * GLSL 1.0 shaders support dynamically indexing an array of samplers.
+ // GLSL 3.0 does not. So we'd have to emit an emulation function.
+ //
+ // The point is it's not a trivial amount of work.
+ /*
+ WEBGL_draw_buffers: {
+ fn(vCtx) {
+ return {
+ COLOR_ATTACHMENT0_WEBGL : 0x8CE0,
+ COLOR_ATTACHMENT1_WEBGL : 0x8CE1,
+ COLOR_ATTACHMENT2_WEBGL : 0x8CE2,
+ COLOR_ATTACHMENT3_WEBGL : 0x8CE3,
+ COLOR_ATTACHMENT4_WEBGL : 0x8CE4,
+ COLOR_ATTACHMENT5_WEBGL : 0x8CE5,
+ COLOR_ATTACHMENT6_WEBGL : 0x8CE6,
+ COLOR_ATTACHMENT7_WEBGL : 0x8CE7,
+ COLOR_ATTACHMENT8_WEBGL : 0x8CE8,
+ COLOR_ATTACHMENT9_WEBGL : 0x8CE9,
+ COLOR_ATTACHMENT10_WEBGL : 0x8CEA,
+ COLOR_ATTACHMENT11_WEBGL : 0x8CEB,
+ COLOR_ATTACHMENT12_WEBGL : 0x8CEC,
+ COLOR_ATTACHMENT13_WEBGL : 0x8CED,
+ COLOR_ATTACHMENT14_WEBGL : 0x8CEE,
+ COLOR_ATTACHMENT15_WEBGL : 0x8CEF,
+
+ DRAW_BUFFER0_WEBGL : 0x8825,
+ DRAW_BUFFER1_WEBGL : 0x8826,
+ DRAW_BUFFER2_WEBGL : 0x8827,
+ DRAW_BUFFER3_WEBGL : 0x8828,
+ DRAW_BUFFER4_WEBGL : 0x8829,
+ DRAW_BUFFER5_WEBGL : 0x882A,
+ DRAW_BUFFER6_WEBGL : 0x882B,
+ DRAW_BUFFER7_WEBGL : 0x882C,
+ DRAW_BUFFER8_WEBGL : 0x882D,
+ DRAW_BUFFER9_WEBGL : 0x882E,
+ DRAW_BUFFER10_WEBGL : 0x882F,
+ DRAW_BUFFER11_WEBGL : 0x8830,
+ DRAW_BUFFER12_WEBGL : 0x8831,
+ DRAW_BUFFER13_WEBGL : 0x8832,
+ DRAW_BUFFER14_WEBGL : 0x8833,
+ DRAW_BUFFER15_WEBGL : 0x8834,
+
+ MAX_COLOR_ATTACHMENTS_WEBGL : 0x8CDF,
+ MAX_DRAW_BUFFERS_WEBGL : 0x8824,
+
+ drawBuffersWEBGL(buffers) {
+ virtualFns.drawBuffers.call(vCtx, buffers);
+ },
+ };
+ },
+ },
+ */
+ };
+
+ const texImage2DArgParersMap = new Map([
+ [9, function([target, level, internalFormat, width, height, , format, type]) {
+ return {target, level, internalFormat, width, height, format, type};
+ }, ],
+ [6, function([target, level, internalFormat, format, type, texImageSource]) {
+ return {target, level, internalFormat, width: texImageSource.width, height: texImageSource.height, format, type};
+ }, ],
+ [10, function([target, level, internalFormat, width, height, , format, type]) {
+ return {target, level, internalFormat, width, height, format, type};
+ }, ],
+ ]);
+
+ const webgl1Fns = {
+ getExtension(name) {
+ name = name.toLowerCase();
+ const existingExt = this._extensions[name];
+ if (existingExt) {
+ return existingExt;
+ }
+
+ const info = webgl1Extensions[name];
+ if (!info) {
+ return virtualFns.getExtension.call(this, name);
+ }
+
+ return info.fn(this);
+ },
+ texImage2D(...args) {
+ makeCurrentContext(this);
+ resizeCanvasIfChanged(this);
+ const gl = sharedWebGLContext;
+ const fn = texImage2DArgParersMap.get(args.length);
+ const {internalFormat, type} = fn(args);
+ if (type === sharedWebGLContext.FLOAT) {
+ switch (internalFormat) {
+ case gl.RGBA: args[2] = gl.RGBA32F; break;
+ case gl.RGB: args[2] = gl.RGB32F; break;
+ }
+ }
+ gl.texImage2D(...args);
+ },
+ getSupportedExtensions: function() {
+ return [
+ ...sharedWebGLContext.getSupportedExtensions(),
+ 'OES_texture_float',
+ 'WEBGL_depth_texture',
+ 'OES_vertex_array_object',
+ // "WEBGL_draw_buffers", // See other comment
+ ];
+ },
+ };
+
+ // copy all WebGL constants and functions to the prototype of
+ // VirtualWebGLContext
+ function copyProperties(keys, VirtualClass, overrideFns) {
+ for (const key of keys) {
+ const propDesc = Object.getOwnPropertyDescriptor(WebGL2RenderingContext.prototype, key);
+ if (propDesc.get) {
+ // it's a getter/setter ?
+ const virtualPropDesc = Object.getOwnPropertyDescriptor(VirtualClass.prototype, key);
+ if (!virtualPropDesc) {
+ console.warn(`WebGL2RenderingContext.${key} is not supported`);
+ }
+ continue;
+ }
+ switch (key) {
+ default: {
+ const value = WebGL2RenderingContext.prototype[key];
+ let newValue = value;
+ const fn = overrideFns[key] || virtualFns[key];
+ if (fn) {
+ newValue = fn;
+ } else {
+ if (typeof value === 'function') {
+ newValue = createWrapper(value, key);
+ }
+ }
+ VirtualClass.prototype[key] = newValue;
+ break;
+ }
+ }
+ }
+ }
+ copyProperties(Object.keys(WebGLRenderingContext.prototype), VirtualWebGLContext, webgl1Fns);
+ copyProperties(Object.keys(WebGL2RenderingContext.prototype), VirtualWebGL2Context, {});
+
+ function makeCurrentContext(vCtx) {
+ if (currentVirtualContext === vCtx) {
+ return;
+ }
+
+ // save all current WebGL state on the previous current virtual context
+ if (currentVirtualContext) {
+ saveAllState(currentVirtualContext._state, currentVirtualContext);
+ }
+
+ // restore all state for the new context
+ restoreAllState(vCtx._state, vCtx);
+
+ // check if the current state is supposed to be rendering to the canvas.
+ // if so bind vCtx._drawingbuffer
+
+ currentVirtualContext = vCtx;
+ }
+
+ function resizeCanvasIfChanged(vCtx) {
+ const width = vCtx.canvas.width;
+ const height = vCtx.canvas.height;
+
+ if (width !== vCtx._width || height !== vCtx._height) {
+ vCtx._width = width;
+ vCtx._height = height;
+ const gl = sharedWebGLContext;
+ const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
+ const format = vCtx._contextAttributes.alpha ? gl.RGBA : gl.RGB;
+ gl.bindTexture(gl.TEXTURE_2D, vCtx._drawingbufferTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, null);
+ gl.bindTexture(gl.TEXTURE_2D, oldTexture);
+
+ if (vCtx._depthRenderbuffer) {
+ const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
+ const internalFormat = vCtx._contextAttributes.stencil ? gl.DEPTH_STENCIL : gl.DEPTH_COMPONENT16;
+ gl.bindRenderbuffer(gl.RENDERBUFFER, vCtx._depthRenderbuffer);
+ gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, width, height);
+ gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
+ }
+ }
+ }
+
+ function createOrGetVirtualWebGLContext(canvas, type, contextAttributes) {
+ // check if this canvas already has a context
+ const existingVirtualCtx = canvasToVirtualContextMap.get(canvas);
+ if (existingVirtualCtx) {
+ return existingVirtualCtx;
+ }
+
+ const compositor = settings.compositorCreator(canvas, type, contextAttributes) || new DefaultCompositor(canvas, type, contextAttributes);
+ const newVirtualCtx = type === 'webgl2'
+ ? new VirtualWebGL2Context(canvas, contextAttributes, compositor, () => {
+ canvasToVirtualContextMap.delete(canvas);
+ })
+ : new VirtualWebGLContext(canvas, contextAttributes, compositor, () => {
+ canvasToVirtualContextMap.delete(canvas);
+ });
+
+ canvasToVirtualContextMap.set(canvas, newVirtualCtx);
+
+ return newVirtualCtx;
+ }
+
+ function createProgram(gl, shaderSources) {
+ const program = gl.createProgram();
+ [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {
+ const shader = gl.createShader(type);
+ gl.shaderSource(shader, shaderSources[ndx]);
+ gl.compileShader(shader);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ console.error(gl.getShaderInfoLog(shader)); // eslint-disable-line
+ }
+ gl.attachShader(program, shader);
+ });
+ gl.bindAttribLocation(program, 0, 'position');
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error(gl.getProgramInfoLog(program)); // eslint-disable-line
+ }
+
+ return program;
+ }
+
+ function saveAllState(state, vCtx) {
+ // save all WebGL state (current bindings, current texture units,
+ // current attributes and/or vertex shade object, current program,
+ // current blend, stencil, zBuffer, culling, viewport etc... state
+ for (const fns of extensionSaveRestoreHelpersArray) {
+ fns.save(state, vCtx);
+ }
+ }
+
+ function setStencil(gl, face, st) {
+ gl.stencilFuncSeparate(face, st.func, st.ref, st.valueMask);
+ gl.stencilOpSeparate(face, st.fail, st.depthFail, st.depthPass);
+ gl.stencilMaskSeparate(face, st.writeMask);
+ }
+
+ function restoreAllState(state, vCtx) {
+ // restore all WebGL state (current bindings, current texture units,
+ // current attributes and/or vertex shade object, current program,
+ // current blend, stencil, zBuffer, culling, viewport etc... state
+ // save all WebGL state (current bindings, current texture units,
+ // current attributes and/or vertex shade object, current program,
+ // current blend, stencil, zBuffer, culling, viewport etc... state
+ const gl = sharedWebGLContext;
+
+ gl.bindRenderbuffer(gl.RENDERBUFFER, state.renderbuffer);
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, state.readFramebuffer);
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, state.drawFramebuffer);
+
+ // restore attributes
+ gl.bindVertexArray(state.vertexArray);
+ for (let i = 0; i < numAttribs; ++i) {
+ const attr = state.attribValues[i];
+ switch (attr.type) {
+ case gl.FLOAT:
+ gl.vertexAttrib4fv(i, attr.value);
+ break;
+ case gl.INT:
+ gl.vertexAttribI4iv(i, attr.value);
+ break;
+ case gl.UNSIGNED_INT:
+ gl.vertexAttribI4uiv(i, attr.value);
+ break;
+ }
+ }
+
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, state.transformFeedback);
+
+ // restore texture units
+ for (let i = 0; i < numTextureUnits; ++i) {
+ gl.activeTexture(gl.TEXTURE0 + i);
+ const unit = state.textureUnits[i];
+ gl.bindTexture(gl.TEXTURE_2D, unit.texture2D);
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, unit.textureCubemap);
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, unit.texture2DArray);
+ gl.bindTexture(gl.TEXTURE_3D, unit.texture3D);
+ gl.bindSampler(i, unit.sampler);
+ }
+ gl.activeTexture(state.activeTexture);
+
+ // uniform buffer bindings (must be restored before UNIFORM_BUFFER restore)
+ for (let i = 0; i < numUniformBufferBindings; ++i) {
+ const ub = state.uniformBufferBindings[i];
+ if (ub.size || ub.start) {
+ gl.bindBufferRange(gl.UNIFORM_BUFFER, i, ub.buffer, ub.start, ub.size);
+ } else {
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, i, ub.buffer);
+ }
+ }
+
+ // bindings
+ gl.bindBuffer(gl.ARRAY_BUFFER, state.arrayBuffer);
+ gl.bindBuffer(gl.COPY_READ_BUFFER, state.copyReadBuffer);
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, state.copyWriteBuffer);
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, state.pixelPackBuffer);
+ gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, state.pixelUnpackBuffer);
+ gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, state.transformFeedbackBuffer);
+ gl.bindBuffer(gl.UNIFORM_BUFFER, state.uniformBuffer);
+
+ gl.readBuffer(state.readBuffer);
+
+ state.enable.forEach((value, key) => {
+ enableDisable(gl, key, value);
+ });
+
+ state.pack.forEach((value, key) => {
+ gl.pixelStorei(key, value);
+ });
+
+ gl.useProgram(state.currentProgram);
+
+ gl.viewport(...state.viewport);
+ gl.scissor(...state.scissor);
+ gl.blendFuncSeparate(state.blendSrcRgb, state.blendDstRgb, state.blendSrcAlpha, state.blendDstAlpha);
+ gl.blendEquationSeparate(state.blendEquationRgb, state.blendEquationAlpha);
+ gl.blendColor(...state.blendColor);
+ gl.clearColor(...state.clearColor);
+ gl.colorMask(...state.colorMask);
+ gl.cullFace(state.cullFaceMode);
+ gl.clearDepth(state.clearDepth);
+ gl.depthFunc(state.depthFunc);
+ gl.depthRange(...state.depthRange);
+ gl.depthMask(state.depthMask);
+ gl.frontFace(state.frontFace);
+ gl.hint(gl.GENERATE_MIPMAP_HINT, state.generateMipmapHint);
+ gl.lineWidth(state.lineWidth);
+ gl.polygonOffset(state.polygonOffsetFactor, state.polygonOffsetUnits);
+ gl.sampleCoverage(state.sampleCoverageValue, state.sampleCoverageUnits);
+
+ setStencil(gl, gl.BACK, state.stencilBack);
+ setStencil(gl, gl.FRONT, state.stencilFront);
+
+ gl.clearStencil(state.stencilClearValue);
+
+ for (const fns of extensionSaveRestoreHelpersArray) {
+ fns.restore(state, vCtx);
+ }
+ }
+
+ function enableDisable(gl, feature, enable) {
+ if (enable) {
+ gl.enable(feature);
+ } else {
+ gl.disable(feature);
+ }
+ }
+
+ function renderAllDirtyVirtualCanvases() {
+ if (!someContextsNeedRendering) {
+ return;
+ }
+ someContextsNeedRendering = false;
+
+ // save all current WebGL state on the previous current virtual context
+ if (currentVirtualContext) {
+ saveAllState(currentVirtualContext._state, currentVirtualContext);
+ currentVirtualContext = null;
+ }
+
+ // set the state back to the one for drawing the canvas
+ restoreAllState(baseState);
+
+ for (const vCtx of canvasToVirtualContextMap.values()) {
+ if (!vCtx._needComposite) {
+ continue;
+ }
+
+ vCtx._needComposite = false;
+ vCtx.composite(sharedWebGLContext);
+ }
+ }
+
+ window.requestAnimationFrame = (function(origFn) {
+ return function(callback) {
+ return origFn.call(window, (time) => {
+ const result = callback(time);
+ renderAllDirtyVirtualCanvases();
+ return result;
+ });
+ };
+
+ }(window.requestAnimationFrame));
+
+ function setup(options) {
+ Object.assign(settings, options);
+ }
+
+ window.virtualWebGL = {
+ setup,
+ };
+
+}());
diff --git a/src/__mocks__/ssh_session.ts b/src/__mocks__/ssh_session.ts
index d8cadb67..32ca4d81 100644
--- a/src/__mocks__/ssh_session.ts
+++ b/src/__mocks__/ssh_session.ts
@@ -23,7 +23,7 @@ export class SSHSession implements Session {
static wrongPassword = false
onStateChange: (state: State) => void
- onPayloadUpdate: (payload: string) => void
+ onCMD: (payload: string) => void
constructor(address: string, username: string, password: string, port?=22) {
console.log("New mocked SSH seesion", address, username, password, port)
}
@@ -52,7 +52,7 @@ export class SSHSession implements Session {
export class HybridSession implements Session {
onStateChange: (state: State) => void
- onPayloadUpdate: (payload: string) => void
+ onCMD: (payload: string) => void
static fail = false
static wrongPassword = false
constructor(address: string, username: string, password: string, port?=22) {
diff --git a/src/__mocks__/webrtc_session.ts b/src/__mocks__/webrtc_session.ts
index d547e710..9f378cbe 100644
--- a/src/__mocks__/webrtc_session.ts
+++ b/src/__mocks__/webrtc_session.ts
@@ -18,7 +18,7 @@ export class MockChannel implements Channel {
export class HTTPWebRTCSession implements Session {
onStateChange: (state: State) => void
- onPayloadUpdate: (payload: string) => void
+ onCMD: (payload: string) => void
static fail = false
constructor(address: string, username: string, password: string, port?=22) {
console.log("New seesion", address, username, password, port)
diff --git a/src/bell.js b/src/bell.ts
similarity index 100%
rename from src/bell.js
rename to src/bell.ts
diff --git a/src/cell.js b/src/cell.js
deleted file mode 100644
index d53691fe..00000000
--- a/src/cell.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/*! Terminal 7 Cell - a class used as super for both Pane & Layout
- *
- * Copyright: (c) 2021 Benny A. Daon - benny@tuzig.com
- * License: GPLv3
- */
-import * as Hammer from 'hammerjs'
-const ABIT = 10,
- FOCUSED_BORDER_COLOR = "#F4DB53",
- UNFOCUSED_BORDER_COLOR = "#373702"
-
-export class Cell {
- constructor(props) {
- this.gate = props.gate || null
- this.w = props.w
- this.id = props.id || undefined
- this.layout = props.layout || null
- this.createElement(props.className)
- this.sx = props.sx || 0.8
- this.sy = props.sy || 0.8
- this.xoff = props.xoff || 0
- this.yoff = props.yoff || 0
- this.zoomed = false
- this.t7 = window.terminal7
- }
- /*
- * Creates the HTML elment that will store our dimensions and content
- * get an optional className to be added to the element
- */
- createElement(className) {
- // creates the div element that will hold the term
- this.e = document.createElement("div")
- this.e.cell = this
- this.e.classList = "cell"
- if (typeof className == "string")
- this.e.classList.add(className)
- this.w.e.appendChild(this.e)
- return this.e
- }
-
- /*
- * Set the focus on the cell
- */
- focus() {
- if (this.w.activeP !== null) {
- this.w.activeP.e.style.borderColor = UNFOCUSED_BORDER_COLOR
- }
- this.w.activeP = this
- this.e.style.borderColor = FOCUSED_BORDER_COLOR
- this.w.updateDivideButtons()
- setTimeout(() => window.location.href = `#pane-${this.id}`)
- this.w.nameE.setAttribute("href", `#pane-${this.id}`)
- }
-
- /*
- * Catches gestures on an elment using hammerjs.
- * If an element is not passed in, `this.e` is used
- */
- catchFingers(elem) {
- let e = (typeof elem == 'undefined')?this.e:elem,
- h = new Hammer.Manager(e, {}),
- // h.options.domEvents=true; // enable dom events
- singleTap = new Hammer.Tap({event: "tap"}),
- doubleTap = new Hammer.Tap({event: "doubletap", taps: 2}),
- pinch = new Hammer.Pinch({event: "pinch"})
-
- h.add([singleTap,
- doubleTap,
- pinch,
- new Hammer.Tap({event: "twofingerstap", pointers: 2})])
-
- h.on('tap', () => {
- if (this.w.activeP != this) {
- this.focus()
- this.gate.sendState()
- }
- })
- h.on('twofingerstap', () => {
- this.toggleZoom()
- })
- h.on('doubletap', () => {
- this.toggleZoom()
- })
-
- h.on('pinch', e => {
- this.t7.log(e.additionalEvent, e.distance, e.velocityX, e.velocityY, e.direction, e.isFinal)
- if (e.deltaTime < this.lastEventT)
- this.lastEventT = 0
- if ((e.deltaTime - this.lastEventT < 200) ||
- (e.velocityY > this.t7.conf.ui.pinchMaxYVelocity))
- return
- this.lastEventT = e.deltaTime
- if (e.additionalEvent == "pinchout")
- this.scale(1)
- else
- this.scale(-1)
- })
- this.mc = h
- }
- get sx(){
- return parseFloat(this.e.style.width.slice(0,-1)) / 100.0
- }
- set sx(val) {
- if (val > 1.0)
- val = 1.0
- this.e.style.width = String(val*100) + "%"
- }
- get sy() {
- return parseFloat(this.e.style.height.slice(0,-1)) / 100.0
- }
- set sy(val) {
- this.e.style.height = String(val*100) + "%"
- }
- get xoff() {
- return parseFloat(this.e.style.left.slice(0,-1)) / 100.0
- }
- set xoff(val) {
- this.e.style.left = String(val*100) + "%"
- }
- get yoff() {
- return parseFloat(this.e.style.top.slice(0,-1)) / 100.0
- }
- set yoff(val) {
- this.e.style.top = String(val*100) + "%"
- }
- /*
- * Cell.close removes a cell's elment and removes itself from the window
- */
- close() {
- // zero canvas dimension to free it
- this.e.querySelectorAll("canvas").forEach(canvas => {
- canvas.height = 0;
- canvas.width = 0;
- })
- if (this.zoomed)
- this.unzoom()
- this.e.remove()
- if (this.layout)
- this.layout.onClose(this)
- this.gate.sendState()
- }
- styleZoomed(e) {
- e = e || this.t7.zoomedE.querySelector(".pane")
- const se = this.gate.e.querySelector(".search-box")
- let style
- if (se.classList.contains("show"))
- style = `${document.querySelector('.windows-container').offsetHeight - 22}px`
- else
- style = `${document.body.offsetHeight - 36}px`
- e.style.height = style
- e.style.top = "0px"
- e.style.width = "100%"
- this.fit()
- }
- toggleZoom() {
- if (this.zoomed)
- this.unzoom()
- else
- this.zoom()
- this.gate.sendState()
- this.t7.run(() => this.focus(), ABIT)
- }
- zoom() {
- let c = document.createElement('div'),
- e = document.createElement('div'),
- te = this.e.removeChild(this.e.children[0])
- c.classList.add("zoomed")
- e.classList.add("pane", "focused")
- e.style.borderColor = FOCUSED_BORDER_COLOR
- e.appendChild(te)
- c.appendChild(e)
- this.catchFingers(e)
- document.body.appendChild(c)
- this.t7.zoomedE = c
- this.w.e.classList.add("hidden")
- this.resizeObserver = new window.ResizeObserver(() => this.styleZoomed(e))
- this.resizeObserver.observe(e)
- this.zoomed = true
- }
- unzoom() {
- if (this.resizeObserver != null) {
- this.resizeObserver.disconnect()
- this.resizeObserver = null
- }
- let te = this.t7.zoomedE.children[0].children[0]
- this.e.appendChild(te)
- document.body.removeChild(this.t7.zoomedE)
- this.t7.zoomedE = null
- this.w.e.classList.remove("hidden")
- this.zoomed = false
- }
-}
diff --git a/src/cell.ts b/src/cell.ts
new file mode 100644
index 00000000..d3042b0c
--- /dev/null
+++ b/src/cell.ts
@@ -0,0 +1,110 @@
+/*! Terminal 7 Cell - a class used as super for both Pane & Layout
+ *
+ * Copyright: (c) 2021 Benny A. Daon - benny@tuzig.com
+ * License: GPLv3
+ */
+import { Gate } from "./gate"
+import { Window } from "./window"
+import { Layout } from "./layout"
+import { Terminal7 } from "./terminal7"
+import { Pane } from "./pane"
+
+const FOCUSED_BORDER_COLOR = "#F4DB53",
+ UNFOCUSED_BORDER_COLOR = "#373702"
+
+export abstract class Cell {
+ gate?: Gate
+ w: Window
+ id?: number
+ layout?: Layout
+ t7: Terminal7
+ e: HTMLDivElement & {cell?: Cell}
+ lastEventT: number
+ protected constructor(props) {
+ this.gate = props.gate || null
+ this.w = props.w
+ this.id = props.id || undefined
+ this.layout = props.layout || null
+ this.createElement(props.className)
+ this.sx = props.sx || 0.8
+ this.sy = props.sy || 0.8
+ this.xoff = props.xoff || 0
+ this.yoff = props.yoff || 0
+ this.t7 = terminal7
+ }
+ /*
+ * Creates the HTML elment that will store our dimensions and content
+ * get an optional className to be added to the element
+ */
+ createElement(className) {
+ // creates the div element that will hold the term
+ this.e = document.createElement("div") as HTMLDivElement & {cell?: Cell}
+ this.e.cell = this
+ this.e.className = "cell"
+ if (typeof className == "string")
+ this.e.classList.add(className)
+ this.w.e.appendChild(this.e)
+ return this.e
+ }
+
+ /*
+ * Set the focus on the cell
+ */
+ focus() {
+ if (this.w.activeP !== null) {
+ this.w.activeP.e.style.borderColor = UNFOCUSED_BORDER_COLOR
+ }
+ this.w.activeP = this as unknown as Pane
+ this.e.style.borderColor = FOCUSED_BORDER_COLOR
+ this.w.updateDivideButtons()
+ setTimeout(() => window.location.href = `#pane-${this.id}`)
+ this.w.nameE.setAttribute("href", `#pane-${this.id}`)
+ }
+
+ abstract dump()
+
+ abstract refreshDividers()
+
+ abstract fit()
+
+ get sx(){
+ return parseFloat(this.e.style.width.slice(0,-1)) / 100.0
+ }
+ set sx(val) {
+ if (val > 1.0)
+ val = 1.0
+ this.e.style.width = String(val*100) + "%"
+ }
+ get sy() {
+ return parseFloat(this.e.style.height.slice(0,-1)) / 100.0
+ }
+ set sy(val) {
+ this.e.style.height = String(val*100) + "%"
+ }
+ get xoff() {
+ return parseFloat(this.e.style.left.slice(0,-1)) / 100.0
+ }
+ set xoff(val) {
+ this.e.style.left = String(val*100) + "%"
+ }
+ get yoff() {
+ return parseFloat(this.e.style.top.slice(0,-1)) / 100.0
+ }
+ set yoff(val) {
+ this.e.style.top = String(val*100) + "%"
+ }
+ /*
+ * Cell.close removes a cell's elment and removes itself from the window
+ */
+ close() {
+ // zero canvas dimension to free it
+ this.e.querySelectorAll("canvas").forEach(canvas => {
+ canvas.height = 0
+ canvas.width = 0
+ })
+ this.e.remove()
+ if (this.layout)
+ this.layout.onClose(this)
+ this.gate.sendState()
+ }
+}
diff --git a/src/commands.ts b/src/commands.ts
index e9ad944a..c70ba586 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -1,17 +1,16 @@
-import { CapacitorPurchases } from '@capgo/capacitor-purchases'
+import { Purchases } from '@revenuecat/purchases-capacitor'
import { Clipboard } from '@capacitor/clipboard'
import { Shell } from "./shell"
import { Preferences } from "@capacitor/preferences"
-import { DEFAULT_DOTFILE, Terminal7 } from "./terminal7"
+import { DEFAULT_DOTFILE } from "./terminal7"
import { Fields } from "./form"
+//@ts-ignore
import fortuneURL from "../resources/fortune.txt"
import { Gate } from './gate'
-import { Capacitor } from '@capacitor/core'
import { SSHSession, SSHChannel } from './ssh_session'
import { Failure } from './session'
import { NativeBiometric } from 'capacitor-native-biometric'
-
-declare const terminal7 : Terminal7
+import { Capacitor } from "@capacitor/core"
export type Command = {
name: string
@@ -55,7 +54,7 @@ export function loadCommands(shell: Shell): Map {
name: "copykey",
help: "Copy the public key",
usage: "copy[key]",
- execute: async args => copyKeyCMD(shell, args)
+ execute: async () => copyKeyCMD(shell)
},
edit: {
name: "edit",
@@ -349,6 +348,8 @@ async function resetCMD(shell: Shell, args: string[]) {
{ prompt: "Reset connection & Layout" },
{ prompt: "\x1B[31mFactory reset\x1B[0m" },
]
+ if (gate.session && !gate.fitScreen)
+ fields.splice(0, 0, { prompt: "Fit my screen" })
if (!gate.onlySSH)
// Add the connection reset option for webrtc
fields.splice(0, 0, { prompt: "Reset connection" })
@@ -361,9 +362,15 @@ async function resetCMD(shell: Shell, args: string[]) {
}
switch (choice) {
+ case "Fit my screen":
+ gate.setFitScreen()
+ gate.map.showLog(false)
+ return
+
case "Reset connection":
// TODO: simplify
if (gate.session) {
+ gate.session.onStateChange = undefined
gate.session.close()
gate.session = null
}
@@ -414,7 +421,7 @@ async function resetCMD(shell: Shell, args: string[]) {
shell.t.writeln("dotfile back to default")
break
case "Fingerprint":
- await CapacitorPurchases.logOut()
+ await Purchases.logOut()
await terminal7.deleteFingerprint()
shell.t.writeln("Cleared fingerprint and disconnected from PeerBook")
terminal7.pbClose()
@@ -517,6 +524,7 @@ async function editCMD(shell:Shell, args: string[]) {
shell.t.writeln("Failed to delete host")
return
}
+ shell.t.writeln("Gate deleted")
}
gate.delete()
break
@@ -569,7 +577,7 @@ async function copyKeyCMD(shell: Shell) {
return shell.t.writeln(`${publicKey}\n☝️ copied to 📋`)
}
async function subscribeCMD(shell: Shell) {
- const { customerInfo } = await CapacitorPurchases.getCustomerInfo()
+ const { customerInfo } = await Purchases.getCustomerInfo()
if (Capacitor.isNativePlatform() && !customerInfo.entitlements.active.peerbook) {
shell.t.writeln("Join PeerBook subscribers and enjoy:")
shell.t.writeln("")
@@ -587,11 +595,10 @@ async function subscribeCMD(shell: Shell) {
"SIX_MONTH": "6 Months",
"ANNUAL": "Year",
}
- const { offerings } = await CapacitorPurchases.getOfferings(),
+ const offerings = await Purchases.getOfferings(),
offer = offerings.current
- const products = offer.availablePackages.map(p => {
- const identifier = p.identifier,
- price = p.product.priceString,
+ const packages = offer.availablePackages.map(p => {
+ const price = p.product.priceString,
period = TYPES[p.packageType],
introPrice = p.product.introPrice
let prompt = `${price} / ${period}`
@@ -601,9 +608,9 @@ async function subscribeCMD(shell: Shell) {
period = (introPrice.periodNumberOfUnits == 1)?unit:`${introPrice.periodNumberOfUnits} ${unit}s`
prompt += ` 🎁 ${price} for the first ${period} 🎁`
}
- return { identifier, prompt }
+ return { prompt, p }
})
- const fields: Fields = products.map(p => ({ prompt: p.prompt }))
+ const fields: Fields = packages.map(p => ({ prompt: p.prompt }))
fields.push({ prompt: "Restore Purchases" })
fields.push({ prompt: "Cancel" })
let choice
@@ -622,14 +629,14 @@ async function subscribeCMD(shell: Shell) {
throw e
})
try {
- await CapacitorPurchases.restorePurchases()
+ await Purchases.restorePurchases()
} catch(e) {
shell.stopWatchdog()
shell.t.writeln("Error restoring purchases, please try again or `support`")
return
}
shell.stopWatchdog()
- const { customerInfo } = await CapacitorPurchases.getCustomerInfo()
+ const { customerInfo } = await Purchases.getCustomerInfo()
if (!customerInfo.entitlements.active.peerbook) {
shell.t.writeln("Sorry, no active subscription found")
return
@@ -637,7 +644,7 @@ async function subscribeCMD(shell: Shell) {
shell.t.writeln("Subscription restored")
}
} else {
- const product = products.find(p => p.prompt == choice)
+ const p = packages.find(p => p.prompt == choice)
shell.t.writeln("Thank you! directing you to the store")
shell.startWatchdog(120000).catch(e => {
shell.t.writeln("Sorry, subscribe command timed out")
@@ -646,7 +653,7 @@ async function subscribeCMD(shell: Shell) {
})
terminal7.ignoreAppEvents = true
try {
- await terminal7.pb.purchase(product.identifier, offer.identifier)
+ await terminal7.pb.purchase(p.p)
shell.stopWatchdog()
} catch(e) {
shell.stopWatchdog()
@@ -662,6 +669,7 @@ async function subscribeCMD(shell: Shell) {
shell.t.writeln("If you are already subscribed, please `login`")
return
}
+ terminal7.pb.startSpinner()
try {
await terminal7.pb.connect(customerInfo.originalAppUserId)
} catch(e) {
@@ -679,9 +687,11 @@ async function subscribeCMD(shell: Shell) {
shell.t.writeln("Please try again and if persists, `support`")
return
}
+ } finally {
+ terminal7.pb.stopSpinner()
}
- } else
- shell.t.writeln("You are already subscribed and registered")
+ }
+ shell.t.writeln("You are subscribed and registered 🙏")
const uid = await terminal7.pb.getUID()
const answer = await shell.askValue(`Copy user id to the clipboard? (y/N)`, "n")
if (answer.toLowerCase() == "y") {
@@ -730,7 +740,7 @@ export async function installCMD(shell: Shell, args: string[]) {
}
} else {
gate = terminal7.activeG
- if (!gate) {
+ if (!gate && native) {
if (terminal7.gates.length == 1) {
gate = terminal7.gates[0]
shell.t.writeln(`Installing on the only server: ${gate.name}`)
@@ -771,9 +781,10 @@ export async function installCMD(shell: Shell, args: string[]) {
]
if (native)
fields.unshift({ prompt: "Connect & send command" })
- shell.t.writeln("To Enjoy WebRTC you need Terminal7 agent installed & running")
+ shell.t.writeln("To download and install the agent's binary run:")
+ shell.t.writeln("")
+ shell.t.writeln(`\t\x1B[1m${cmd}\x1B[0m`)
shell.t.writeln("")
- shell.t.writeln(`$ \x1B[1m${cmd}\x1B[0m`)
const choice= await shell.runForm(fields, "menu")
if (choice == "Cancel")
return
@@ -803,10 +814,6 @@ export async function installCMD(shell: Shell, args: string[]) {
const session = new SSHSession(gate.addr, gate.username)
- session.onClose = () => {
- // TODO: handle close without installation
- terminal7.log("Install SSH session closed")
- }
session.onStateChange = async (state, failure?: Failure) => {
let channel: SSHChannel
terminal7.log("Install SSH session got state", state, failure)
@@ -901,7 +908,10 @@ export async function installCMD(shell: Shell, args: string[]) {
.catch(() => timedOut = true)
while (!gate.fp && ! timedOut)
await (new Promise(r => setTimeout(r, 100)))
- shell.t.writeln(`Gate is installed & verified, use \`connect ${gate.name}\``)
+ if (gate.fp)
+ shell.t.writeln(`Gate is installed & verified, use \`connect ${gate.name}\``)
+ else
+ shell.t.writeln("Install failed, please try again or `support`")
shell.stopWatchdog()
} else {
shell.t.writeln("Install failed")
@@ -917,15 +927,16 @@ async function configCMD(shell: Shell) {
await shell.openConfig()
}
async function supportCMD(shell: Shell) {
- shell.t.writeln("https://discord.gg/Puu2afdUtr")
- shell.t.writeln("☝️ Please click to join and get help")
+ shell.t.write("Sorry things are not working well. Please ")
+ shell.t.writeln("\x1B]8;;https://github.com/tuzig/terminal7/issues/new?template=bug_report.md\x07report a bug\x1B]8;;\x07")
+ shell.t.writeln("or talk to us on our \x1B]8;;https://discord.gg/Puu2afdUtr\x07discord server\x1B]8;;\x07")
}
async function loginCMD(shell: Shell) {
if (terminal7.pb.isOpen()) {
shell.t.writeln("You are already logged in")
return
}
- const { customerInfo } = await CapacitorPurchases.getCustomerInfo()
+ const { customerInfo } = await Purchases.getCustomerInfo()
if (customerInfo.entitlements.active.peerbook) {
shell.t.writeln("You are already subscribed, please `subscribe` to login")
return
diff --git a/src/cyclic.js b/src/cyclic.js
deleted file mode 100644
index 782ba6e1..00000000
--- a/src/cyclic.js
+++ /dev/null
@@ -1,50 +0,0 @@
-export function CyclicArray(n) {
- if (!(this instanceof CyclicArray)) return new CyclicArray(n)
-
- this.head = 0
- this.tail = 0
- this.capacity = n
- this.list = new Array(n)
- this.length = 0
-}
-
-CyclicArray.prototype.get = function (i) {
- if (i >= this.length) return
- var j = (this.head + i) % this.capacity
- return this.list[j]
-}
-
-CyclicArray.prototype.push = function () {
- for (var i = 0; i < arguments.length; i++) {
- this.list[this.tail] = arguments[i]
- this.tail = (this.tail + 1) % this.capacity
- if (this.length == this.capacity)
- this.head = this.tail
- else
- this.length++
- }
-}
-
-CyclicArray.prototype.pop = function () {
- if (!this.length) return
- var tail = (this.tail - 1 + this.capacity) % this.capacity
- var item = this.list[tail]
- this.tail = tail
- this.length--
- return item
-}
-
-CyclicArray.prototype.shift = function () {
- if (!this.length) return
- var item = this.list[this.head]
- this.head = (this.head + 1) % this.capacity
- this.length--
- return item
-}
-
-CyclicArray.prototype.forEach = function (fn, context) {
- for (var i = 0; i < this.length; i++) {
- var j = (this.head + i) % this.capacity
- fn.call(context, this.list[j], i, this)
- }
-}
diff --git a/src/cyclic.ts b/src/cyclic.ts
new file mode 100644
index 00000000..3b946ba8
--- /dev/null
+++ b/src/cyclic.ts
@@ -0,0 +1,47 @@
+export default class CyclicArray {
+ head = 0
+ tail = 0
+ list: unknown[]
+ length = 0
+
+ constructor(public capacity: number) {
+ this.list = new Array(capacity)
+ }
+
+ get(i) {
+ if (i >= this.length) return
+ const j = (this.head + i) % this.capacity
+ return this.list[j]
+ }
+ push(...args) {
+ for (let i = 0; i < args.length; i++) {
+ this.list[this.tail] = args[i]
+ this.tail = (this.tail + 1) % this.capacity
+ if (this.length == this.capacity)
+ this.head = this.tail
+ else
+ this.length++
+ }
+ }
+ pop() {
+ if (!this.length) return
+ const tail = (this.tail - 1 + this.capacity) % this.capacity
+ const item = this.list[tail]
+ this.tail = tail
+ this.length--
+ return item
+ }
+ shift() {
+ if (!this.length) return
+ const item = this.list[this.head]
+ this.head = (this.head + 1) % this.capacity
+ this.length--
+ return item
+ }
+ forEach(fn, context) {
+ for (let i = 0; i < this.length; i++) {
+ const j = (this.head + i) % this.capacity
+ fn.call(context, this.list[j], i, this)
+ }
+ }
+}
diff --git a/src/gate.ts b/src/gate.ts
index 04b7a339..4bc05ffa 100644
--- a/src/gate.ts
+++ b/src/gate.ts
@@ -7,7 +7,7 @@
*/
import { Clipboard } from '@capacitor/clipboard'
-import { Pane } from './pane.js'
+import { Pane } from './pane'
import { T7Map } from './map'
import { Failure, Session } from './session'
import { PB } from './peerbook'
@@ -15,12 +15,21 @@ import { SSHSession } from './ssh_session'
import { Terminal7 } from './terminal7'
import { Capacitor } from '@capacitor/core'
-import { HTTPWebRTCSession, PeerbookSession } from './webrtc_session'
-import { Window } from './window.js'
+import { HTTPWebRTCSession, PeerbookSession, WebRTCSession } from './webrtc_session'
+import { SerializedWindow, Window } from './window'
import { Preferences } from '@capacitor/preferences'
const FAILED_COLOR = "red"// ashort period of time, in milli
+const TOOLBAR_HEIGHT = 135
+
+export interface ServerPayload {
+ height: number
+ width: number
+ windows: SerializedWindow[]
+ active?: boolean
+}
+
/*
* The gate class abstracts a host connection
*/
@@ -28,12 +37,12 @@ export class Gate {
activeW: Window
addr: string
boarding: boolean
- e: Element
+ e: HTMLDivElement
id: string
marker: number
name: string
secret: string
- session: Session | null
+ session: PeerbookSession | SSHSession | HTTPWebRTCSession | WebRTCSession | Session | null
user: string
username: string
nameE: Element
@@ -49,6 +58,14 @@ export class Gate {
firstConnection: boolean
keyRejected: boolean
connectionFailed: boolean
+ layoutWidth: number
+ layoutHeight: number
+ fontScale: number
+ fitScreen: boolean
+ windows: Window[]
+ breadcrumbs: Window[]
+ sendStateTask?: number = null
+ lastDisconnect?: number
constructor (props) {
// given properties
this.id = props.id
@@ -64,19 +81,17 @@ export class Gate {
//
this.windows = []
this.boarding = false
- this.lastMsgId = 0
- // a mapping of refrence number to function called on received ack
this.breadcrumbs = []
- this.sendStateTask = null
- this.timeoutID = null
this.fp = props.fp
// TODO: move t7 & map into props
- this.t7 = window.terminal7
+ this.t7 = terminal7
this.map = this.t7.map
this.session = null
this.onlySSH = props.onlySSH || false
this.onFailure = Function.prototype()
this.firstConnection = props.firstConnection || false
+ this.fontScale = props.fontScale || 1
+ this.fitScreen = true
}
/*
@@ -86,13 +101,13 @@ export class Gate {
// create the gate element - holding the tabs, windows and tab bar
this.e = document.createElement('div')
this.e.className = "gate hidden"
- this.e.style.zIndex = 2
+ this.e.style.zIndex = "2"
this.e.id = `gate-${this.id}`
e.appendChild(this.e)
// add the tab bar
- let t = document.getElementById("gate-template")
+ let t = document.getElementById("gate-template") as HTMLTemplateElement
if (t) {
- t = t.content.cloneNode(true)
+ t = t.content.cloneNode(true) as HTMLTemplateElement
t.querySelector(".reset").addEventListener('click', ev => {
this.t7.map.shell.runCommand("reset", [this.name])
ev.preventDefault()
@@ -119,6 +134,8 @@ export class Gate {
*/
this.e.appendChild(t)
}
+ this.layoutWidth = document.body.clientWidth
+ this.layoutHeight = document.body.clientHeight - TOOLBAR_HEIGHT
}
// deletes removes the gate from terminal7 and the map
delete() {
@@ -147,8 +164,8 @@ export class Gate {
this.updateNameE()
}
setIndicatorColor(color) {
- this.e.querySelector(".tabbar-names").style.setProperty(
- "--indicator-color", color)
+ const e = this.e.querySelector(".tabbar-names") as HTMLElement
+ e.style.setProperty("--indicator-color", color)
}
/*
* onSessionState(state) is called when the connection
@@ -173,10 +190,9 @@ export class Gate {
this.lastDisconnect = Date.now()
// TODO: start the rain
this.setIndicatorColor(FAILED_COLOR)
- if (terminal7.recovering) {
- this.session.msgHandlers.forEach(v => {
- v[1]("Disconnected")
- })
+ if (terminal7.recovering) {
+ const session = this.session as WebRTCSession
+ session.msgHandlers.forEach(v => v[1]("Disconnected"))
setTimeout(() => this.reconnect(), 10)
return
}
@@ -212,11 +228,12 @@ export class Gate {
this.notify("Sorry, wrong password")
try {
password = await this.map.shell.askPass()
- } catch (e) {
+ } catch (e) {
this.onFailure(failure)
return
}
- this.session.passConnect(this.marker, password)
+ const s = this.session as SSHSession
+ s.passConnect(this.marker, password)
return
case Failure.BadRemoteDescription:
this.session.close()
@@ -251,7 +268,7 @@ export class Gate {
terminal7.log("Cleaned session as failure on recovering")
return
}
- this.notify(failure?"Lost Data Channel":"Lost Connection")
+ this.notify(failure?"Lost Data Channel":"Lost Connection" + ", please try `reset`")
break
case Failure.KeyRejected:
@@ -266,13 +283,12 @@ export class Gate {
firstGate = (await Preferences.get({key: "first_gate"})).value
if (firstGate)
terminal7.ignoreAppEvents = true
- this.session.passConnect(this.marker, password)
+ const session = this.session as SSHSession
+ session.passConnect(this.marker, password)
return
case Failure.FailedToConnect:
this.notify("Failed to connect")
- // SSH failed, don't offer install
- await this.map.shell.onDisconnect(this)
- return
+ break
case Failure.TimedOut:
this.connectionFailed = true
@@ -295,7 +311,7 @@ export class Gate {
return new Promise((resolve, reject) => {
if (!isSSH && !isNative) {
this.session.reconnect(this.marker).then(layout => {
- this.setLayout(layout)
+ this.setLayout(layout as ServerPayload)
resolve()
}).catch(() => {
if (this.session) {
@@ -307,25 +323,24 @@ export class Gate {
})
return
}
+ const closeSessionAndDisconnect = () => {
+ if (this.session && !this.session.isSSH) {
+ this.session.close()
+ this.session = null
+ }
+ this.map.shell.onDisconnect(this, isSSH).then(resolve).catch(reject)
+ }
this.t7.readId().then(({publicKey, privateKey}) => {
this.session.reconnect(this.marker, publicKey, privateKey).then(layout => {
- this.setLayout(layout)
+ this.setLayout(layout as ServerPayload)
resolve()
}).catch(e => {
- if (this.session && !this.session.isSSH) {
- this.session.close()
- this.session = null
- }
- terminal7.log("reconnect failed, calling the shell to handle it", isSSH, e)
- this.map.shell.onDisconnect(this, isSSH).then(resolve).catch(reject)
+ closeSessionAndDisconnect()
+ this.t7.log("reconnect failed, calling the shell to handle it", isSSH, e)
})
}).catch((e) => {
this.t7.log("failed to read id", e)
- if (this.session && !this.session.isSSH) {
- this.session.close()
- this.session = null
- }
- this.map.shell.onDisconnect(this, isSSH).then(resolve).catch(reject)
+ closeSessionAndDisconnect()
resolve()
})
})
@@ -361,7 +376,7 @@ export class Gate {
/*
* returns an array of panes
*/
- panes() {
+ panes(): Pane[] {
const r = []
this.t7.cells.forEach(c => {
if (c instanceof Pane && (c.gate == this))
@@ -373,9 +388,11 @@ export class Gate {
reset() {
this.t7.map.shell.runCommand("reset", [this.name])
}
- setLayout(state: object) {
+ setLayout(state: ServerPayload = null, fromPresenter = false) {
console.log("in setLayout", state)
const winLen = this.windows.length
+ if(this.fitScreen)
+ this.fontScale = 1
// got an empty state
if ((state == null) || !(state.windows instanceof Array) || (state.windows.length == 0)) {
// create the first window and pane
@@ -383,17 +400,22 @@ export class Gate {
this.clear()
this.activeW = this.addWindow("", true)
} else if (winLen > 0) {
- // TODO: validate the current layout is like the state
- this.t7.log("Restoring with marker, opening channel")
- this.panes().forEach(p => {
- if (p.d)
- p.openChannel({id: p.d.id})
- })
+ this.t7.log("Restoring to an existing layout")
+ if (this.activeW && this.activeW.activeP.zoomed)
+ this.activeW.activeP.unzoom()
+ this.syncLayout(state)
+ this.panes().forEach(p => p.openChannel({id: p.channelID}))
} else {
this.t7.log("Setting layout: ", state)
this.clear()
+ if (this.layoutWidth != state.width || this.layoutHeight != state.height) {
+ this.layoutWidth = state.width
+ this.layoutHeight = state.height
+ if (fromPresenter) this.fitScreen = false
+ this.scaleContainer()
+ }
state.windows.forEach(w => {
- const win = this.addWindow(w.name)
+ const win = this.addWindow(w.name, false, w.id)
if (w.active)
this.activeW = win
win.restoreLayout(w.layout, w.active)
@@ -408,7 +430,7 @@ export class Gate {
let foundNull = false
this.panes().forEach((p, i) => {
if (p.d) {
- if (p.needsResize) {
+ if (p.needsResize && this.fitScreen) {
// TODO: fix webexec so there's no need for this
this.t7.run(() => p.d.resize(p.t.cols, p.t.rows), i*10)
p.needsResize = false
@@ -423,13 +445,80 @@ export class Gate {
}, 400)
this.focus()
}
+ scaleContainer() {
+ const container = this.e.querySelector(".windows-container") as HTMLDivElement
+ let scale
+
+ if (this.fitScreen) {
+ container.style.width = "100%"
+ container.style.removeProperty("height")
+ scale = 1
+ container.style.left = "0"
+ container.style.top = "22px"
+ container.style.removeProperty("transform")
+ } else {
+
+ const width = this.layoutWidth,
+ height = this.layoutHeight
+ if (!width || !height)
+ return
+ const maxWidth = document.body.clientWidth,
+ maxHeight = document.body.clientHeight - TOOLBAR_HEIGHT
+ const sx = maxWidth / width,
+ sy = maxHeight / height
+ scale = Math.min(sx, sy, 1)
+ const scaledWidth = width * scale,
+ scaledHeight = height * scale
+ container.style.width = `${scaledWidth}px`
+ container.style.height = `${scaledHeight}px`
+ container.style.left = "50%"
+ container.style.top = "calc(50% - 45px)"
+ container.style.transform = `translate(-50%, -50%)`
+ container.style.transformOrigin = "top left"
+ }
+ this.panes().forEach(p => {
+ // NOTE: the step of changing the font size is 0.5, there is no visual change when doing smaller steps
+ const fontSize = p.fontSize * scale
+ p.t.options.fontSize = Math.floor(fontSize) + (String(fontSize).includes('.') ? .5 : 0)
+ })
+ this.fontScale = scale
+ }
+ syncLayout(state: ServerPayload) {
+ if (state.width != this.layoutWidth || state.height != this.layoutHeight) {
+ this.layoutWidth = state.width
+ this.layoutHeight = state.height
+ this.scaleContainer()
+ console.log("setting fitScreen to false")
+ this.fitScreen = false
+ }
+ state.windows.forEach(w => {
+ const win = this.windows.find(win => win.id == w.id)
+ if (!win) {
+ // Add window
+ this.t7.log(`Adding window ${w.name}`)
+ const newW = this.addWindow(w.name, false, w.id)
+ newW.restoreLayout(w.layout, w.active)
+ if (w.active)
+ newW.focus()
+ return
+ }
+ if (win.name != w.name) {
+ win.name = w.name
+ win.nameE.innerHTML = w.name
+ }
+ win.rootLayout = win.syncLayout(w.layout)
+ win.nameE?.setAttribute("href", `#pane-${win.activeP?.id}`)
+ if (w.active)
+ win.focus()
+ })
+ }
/*
* Adds a window, opens it and returns it
*/
- addWindow(name, createPane) {
+ addWindow(name, createPane?, id?) {
this.t7.log(`adding Window: ${name}`)
- const id = this.windows.length,
- w = new Window({name:name, gate: this, id: id})
+ id = id || this.windows.length
+ const w = new Window({name:name, gate: this, id: id})
this.windows.push(w)
if (this.windows.length >= this.t7.conf.ui.max_tabs)
this.e.querySelector(".add-tab").classList.add("off")
@@ -452,11 +541,13 @@ export class Gate {
this.e.querySelector(".tabbar-names").innerHTML = ""
this.e.querySelectorAll(".window").forEach(e => e.remove())
this.e.querySelectorAll(".modal").forEach(e => e.classList.add("hidden"))
+ this.e.querySelector(".windows-container").removeAttribute("style")
if (this.activeW && this.activeW.activeP.zoomed)
this.activeW.activeP.unzoom()
this.windows = []
this.breadcrumbs = []
- this.msgs = {}
+ this.layoutWidth = 0
+ this.layoutHeight = 0
this.t7.cells.forEach((c, i, cells) => {
if (c instanceof Pane && (c.gate == this))
cells.splice(i, 1)
@@ -465,18 +556,23 @@ export class Gate {
/*
* dump dumps the host to a state object
* */
- dump() {
- const wins = []
+ dump(): ServerPayload {
+ const windows = []
this.windows.forEach(w => {
- const win = {
+ const win: SerializedWindow = {
name: w.name,
- layout: w.dump()
+ id: w.id,
+ layout: w.dump(),
}
if (w == this.activeW)
win.active = true
- wins.push(win)
+ windows.push(win)
})
- return { windows: wins }
+ if (!this.fitScreen)
+ return {windows, width: this.layoutWidth, height: this.layoutHeight}
+ const width = document.body.clientWidth,
+ height = document.body.clientHeight - TOOLBAR_HEIGHT
+ return { windows, width, height }
}
storeState() {
/* TODO: restore the restore to last state
@@ -494,23 +590,26 @@ export class Gate {
}
sendState() {
- if (this.sendStateTask != null)
+ if ((this.sendStateTask != null) || !this.session)
return
+ // @ts-ignore
+ this.sendStateTask = setTimeout(() => {
- this.storeState()
- // send the state only when all panes have a channel
- if (this.session && (this.panes().every(p => p.d != null)))
- this.sendStateTask = setTimeout(() => {
- this.sendStateTask = null
- if (!this.session)
- return
+ this.sendStateTask = null
+
+ if (!this.session)
+ return
+
+ if (this.panes().every(p => p.channelID))
this.session.setPayload(this.dump()).then(() => {
if ((this.windows.length == 0) && (this.session != null)) {
this.t7.log("Closing gate after updating to empty state")
this.close()
}
})
- }, 100)
+ else
+ this.sendState()
+ }, 100)// TODO: make it run when the update is done and all channels opened
}
async onPaneConnected() {
// hide notifications
@@ -537,7 +636,7 @@ export class Gate {
* It first sends a mark request and on it's ack store the restore marker
* and closes the peer connection.
*/
- disengage() {
+ disengage(): Promise {
return new Promise((resolve, reject) => {
this.t7.log(`disengaging. boarding is ${this.boarding}`)
if (!this.session) {
@@ -545,7 +644,7 @@ export class Gate {
return
}
return this.session.disconnect().then(marker => {
- this.marker = marker
+ this.marker = marker as number
resolve()
}).catch(() => {
resolve()
@@ -563,6 +662,7 @@ export class Gate {
}
}
async copyFingerprint() {
+ const fp = await this.t7.getFingerprint()
const cmd = `echo "${fp}" >> ~/.config/webexec/authorized_fingerprints`
const fpForm = [{
prompt: `\n ${this.addr} refused our fingerprint.
@@ -580,15 +680,15 @@ export class Gate {
this.connect(this.onConnected)
}
}
- async completeConnect(): void {
+ async completeConnect(): Promise {
this.keyRejected = false
const isNative = Capacitor.isNativePlatform()
const overPB = this.fp && !this.onlySSH && this.online
if (overPB) {
this.notify("🎌 PeerBook")
- if (!terminal7.pb.isOpen())
+ if (!terminal7.pb.isOpen())
await terminal7.pbConnect()
- this.session = new PeerbookSession(this.fp, this.t7.pb)
+ this.session = new PeerbookSession(this.fp)
} else {
if (isNative) {
this.session = new SSHSession(this.addr, this.username)
@@ -599,14 +699,15 @@ export class Gate {
}
}
this.session.onStateChange = (state, failure?) => this.onSessionState(state, failure)
- this.session.onPayloadUpdate = layout => {
- this.notify("TBD: update new layout")
- this.t7.log("TBD: update layout", layout)
+ this.session.onCMD = msg => {
+ if (msg.type == "set_payload") {
+ this.setLayout(msg.args.payload, true)
+ }
}
this.t7.log("opening session")
if (overPB) {
try {
- this.session.connect(this.marker)
+ await this.session.connect(this.marker)
} catch(e) {
this.t7.log("error connecting", e)
this.notify(`${PB} Connection failed: ${e}`)
@@ -618,20 +719,22 @@ export class Gate {
const firstGate = (await Preferences.get({key: "first_gate"})).value
if (firstGate)
terminal7.ignoreAppEvents = true
- this.session.connect(this.marker, publicKey, privateKey)
+
+ const session = this.session as SSHSession
+ await session.connect(this.marker, publicKey, privateKey)
} catch(e) {
terminal7.log("error connecting with keys", e)
this.handleFailure(Failure.KeyRejected)
}
} else
- this.session.connect(this.marker)
+ await this.session.connect(this.marker)
}
}
load() {
this.t7.log("loading gate")
this.session.getPayload().then(layout => {
console.log("got payload", layout)
- this.setLayout(layout)
+ this.setLayout(layout as ServerPayload)
})
document.getElementById("map").classList.add("hidden")
}
@@ -670,5 +773,12 @@ export class Gate {
this.session = null
}
}
-
+ setFitScreen() {
+ this.layoutWidth = document.body.clientWidth
+ this.layoutHeight = document.body.clientHeight - TOOLBAR_HEIGHT
+ this.fitScreen = true
+ this.scaleContainer()
+ this.fit()
+ this.sendState()
+ }
}
diff --git a/src/layout.js b/src/layout.ts
similarity index 82%
rename from src/layout.js
rename to src/layout.ts
index bd62e62d..4f807fe2 100644
--- a/src/layout.js
+++ b/src/layout.ts
@@ -5,14 +5,27 @@
* Copyright: (c) 2021 Benny A. Daon - benny@tuzig.com
* License: GPLv3
*/
-import { Cell } from './cell.js'
-import { Pane } from './pane.js'
+import { Cell } from './cell'
+import { Pane } from './pane'
const ABIT = 10
+export interface SerializedLayout {
+ dir: string,
+ sx: number,
+ sy: number,
+ xoff: number,
+ yoff: number,
+ cells: Cell[],
+ active?: boolean
+}
+
export class Layout extends Cell {
+ cells?: Cell[]
+ dir: "TBD" | "topbottom" | "rightleft"
+ active?: boolean
/*
- * Layout contructor creates a `Layout` object based on a cell.
+ * Layout constructor creates a `Layout` object based on a cell.
* The new object wraps the `basedOn` cell and makes it his first son
*/
constructor(dir, basedOn) {
@@ -56,8 +69,8 @@ export class Layout extends Cell {
/*
* On a cell going away, resize the other elements
*/
- onClose(c) {
- if (c.zoomed)
+ onClose(c: Cell) {
+ if (c instanceof Pane && c.zoomed)
c.unzoom()
this.t7.cells.splice(this.t7.cells.indexOf(c), 1)
// if this is the only pane in the layout, close the layout
@@ -70,7 +83,7 @@ export class Layout extends Cell {
}
this.e.remove()
} else {
- let i = this.cells.indexOf(c),
+ const i = this.cells.indexOf(c),
p = (i > 0)?this.cells[i-1]:this.cells[1]
// if no peer it means we're removing the last pane in the window
if (p === undefined) {
@@ -102,16 +115,17 @@ export class Layout extends Cell {
/*
* Adds a new pane. If the gate is connected the pane will open a
* new data channel.
+ * If index is given, the pane is replacing the one at that index
*/
- addPane(props) {
- // CONGRATS! a new pane is born. props must include at keast sx & sy
- let p = props || {}
+ addPane(props, index = null) {
+ // CONGRATS! a new pane is born. props must include at least sx & sy
+ const p = props || {}
p.w = this.w
p.gate = this.gate
p.layout = this
- p.channel_id = props.channel_id
+ p.channelID = props.channelID
p.id = this.t7.cells.length
- let pane = new Pane(p)
+ const pane = new Pane(p)
this.t7.cells.push(pane)
if (props.parent instanceof Cell) {
@@ -119,10 +133,13 @@ export class Layout extends Cell {
this.cells.splice(this.cells.indexOf(props.parent)+1, 0, pane)
if (props.parent && props.parent.d)
parent = props.parent.d.id
- pane.openTerminal(parent, props.channel_id)
+ pane.openTerminal(parent, props.channelID)
} else {
- this.cells.push(pane)
- pane.openTerminal(null, props.channel_id)
+ if (typeof index == "number")
+ this.cells.splice(index, 1, pane)
+ else
+ this.cells.push(pane)
+ pane.openTerminal(null, props.channelID)
}
// opening the terminal and the datachannel are heavy so we wait
@@ -155,19 +172,19 @@ export class Layout extends Cell {
if (c == this)
this.t7.log("ERROR: layout shouldn't have `this` in his cells")
// TODO: remove this workaround - `c != this`
- if ((c != this) && (typeof c.toText == "function"))
+ if ((c != this) && c instanceof Layout && (typeof c.toText == "function"))
r += c.toText()
else
- r += `,${c.id || c.d.id}`
+ r += `,${c.id || (c as Pane).d.id}`
})
r += (this.dir=="rightleft")?"]":"}"
return r
}
// Layout.dump dumps the layout to an object
- dump() {
+ dump(): SerializedLayout {
// r is the text the function returns
- let d = {
+ const d: SerializedLayout = {
dir: this.dir,
sx: this.sx,
sy: this.sy,
@@ -188,7 +205,7 @@ export class Layout extends Cell {
* the layout's direction.
*/
set sx(val) {
- let oldS = this.sx,
+ const oldS = this.sx,
r = val/oldS
this.e.style.width = String(val * 100) + "%"
if (isNaN(r) || this.cells == undefined || this.cells.length == 0)
@@ -196,7 +213,7 @@ export class Layout extends Cell {
let off = this.cells[0].xoff
this.cells.forEach((c) => {
if (this.dir == "topbottom") {
- let oldS = c.sx,
+ const oldS = c.sx,
s = oldS * r
c.xoff = off
c.sx = s
@@ -212,7 +229,7 @@ export class Layout extends Cell {
* the layout's direction.
*/
set sy(val) {
- let oldS = this.sy,
+ const oldS = this.sy,
r = val/oldS
this.e.style.height = String(val * 100) + "%"
if (isNaN(r) || this.cells == undefined || this.cells.length == 0)
@@ -220,7 +237,7 @@ export class Layout extends Cell {
let off = this.cells[0].yoff
this.cells.forEach((c) => {
if (this.dir == "rightleft") {
- let oldS = c.sy,
+ const oldS = c.sy,
s = oldS * r
c.yoff = off
c.sy = s
@@ -267,18 +284,18 @@ export class Layout extends Cell {
})
}
prevCell(c) {
- var i = this.cells.indexOf(c) - 1
+ const i = this.cells.indexOf(c) - 1
return (i >= 0)?this.cells[i]:null
}
nextCell(c) {
- var i = this.cells.indexOf(c) + 1
+ const i = this.cells.indexOf(c) + 1
return (i < this.cells.length)?this.cells[i]:null
}
/*
* Layout.moveBorder moves a pane's border
*/
moveBorder(pane, border, dest) {
- var s, off
+ let s, off
let p0 = null,
p1 = null
// first, check if it's a horizontal or vertical border we're moving
@@ -304,10 +321,10 @@ export class Layout extends Cell {
this.layout && this.layout.moveBorder(this, border, dest)
return
}
- let max = this.findNext(p1)
+ const max = this.findNext(p1)
dest = Math.max(dest, p0[off] + 0.02)
dest = Math.min(dest, (max?.[off] || 1) - 0.02)
- let by = p1[off] - dest
+ const by = p1[off] - dest
p0[s] -= by
p1[s] += by
p1[off] = dest
@@ -318,9 +335,20 @@ export class Layout extends Cell {
findNext(c) {
if (this.nextCell(c))
return this.nextCell(c)
- let root = this.layout?.layout
+ const root = this.layout?.layout
if (root)
return root.findNext(this.layout)
return null
}
+ // Layout.allCells returns all the cells in the layout
+ allCells() {
+ let cells = []
+ this.cells.forEach((c) => {
+ if (c instanceof Layout)
+ cells = cells.concat(c.allCells())
+ else
+ cells.push(c)
+ })
+ return cells
+ }
}
diff --git a/src/map.ts b/src/map.ts
index 2071b10a..bfce9634 100644
--- a/src/map.ts
+++ b/src/map.ts
@@ -12,18 +12,23 @@ import { Gate } from './gate'
import { WebLinksAddon } from 'xterm-addon-web-links'
import { FitAddon } from "xterm-addon-fit"
import { WebglAddon } from 'xterm-addon-webgl'
-import { ImageAddon } from 'xterm-addon-image';
+import { ImageAddon } from 'xterm-addon-image'
import XtermWebfont from '@liveconfig/xterm-webfont'
import { Shell } from './shell'
import { Capacitor } from '@capacitor/core'
+import { WebRTCSession } from "./webrtc_session"
+
+export declare interface TerminalWithAddons extends Terminal {
+ loadWebfontAndOpen(element): Promise
+}
export class T7Map {
- t0: Terminal
+ t0: TerminalWithAddons
ttyWait: number
shell: Shell
fitAddon: FitAddon
- open() {
+ open(): Promise {
return new Promise(resolve => {
this.t0 = new Terminal({
cursorBlink: true,
@@ -31,7 +36,6 @@ export class T7Map {
theme: window.terminal7?.conf.theme,
fontFamily: "FiraCode",
fontSize: 14,
- rendererType: "canvas",
convertEol: true,
rows: 20,
cols: 55,
@@ -40,7 +44,7 @@ export class T7Map {
window.open(url, "_blank", "noopener")
}
}
- })
+ }) as TerminalWithAddons
this.shell = new Shell(this)
const e = document.getElementById("t0")
this.fitAddon = new FitAddon()
@@ -94,7 +98,7 @@ export class T7Map {
this.t0.loadWebfontAndOpen(e).then(() => {
if (Capacitor.getPlatform() === "android") {
// hack for android spacebar & virtual keyboard
- this.t0.element.addEventListener("input", ev => {
+ this.t0.element.addEventListener("input", (ev: Event & {data?}) => {
if (ev.data)
this.shell.keyHandler(ev.data)
})
@@ -124,7 +128,7 @@ export class T7Map {
})
}
add(g: Gate): Element {
- const d = document.createElement('div')
+ const d = (document.createElement('div') as HTMLDivElement & {gate: Gate})
const container = document.createElement('div')
d.className = "gate-pad"
if (g.fp)
@@ -167,12 +171,11 @@ export class T7Map {
const edit = b.children[1]
edit.innerHTML = `pencil`
if (peerbook) {
+ const extraClass = offline? "offline" : ""
if (unverified)
- nameE.innerHTML += `lock_shield`
+ nameE.innerHTML += ``
else
- nameE.innerHTML += `peerbook`
- if (offline)
- nameE.classList.add("offline")
+ nameE.innerHTML += ``
}
// there's nothing more to update for static hosts
if (boarding)
@@ -201,8 +204,8 @@ export class T7Map {
async updateStats() {
terminal7.gates.forEach(async (g: Gate) => {
let html = ""
- if (g && g.session && g.session.getStats) {
- const stats = await g.session.getStats()
+ if (g && g.session && (g.session as WebRTCSession).getStats) {
+ const stats = await (g.session as WebRTCSession).getStats()
if (!stats)
return
@@ -226,9 +229,9 @@ export class T7Map {
}
/*
* showLog display or hides the notifications.
- * if the parameters in udefined the function toggles the displays
+ * if the parameters in undefined the function toggles the displays
*/
- showLog(show) {
+ showLog(show = undefined) {
const log = document.getElementById("log")
const minimized = document.getElementById("log-minimized")
if (show === undefined)
diff --git a/src/pane.js b/src/pane.ts
similarity index 78%
rename from src/pane.js
rename to src/pane.ts
index bdd920da..50e73fb5 100644
--- a/src/pane.js
+++ b/src/pane.ts
@@ -4,25 +4,29 @@
* Copyright: (c) 2021 Benny A. Daon - benny@tuzig.com
* License: GPLv3
*/
-import { Cell } from './cell.js'
-import { Terminal } from 'xterm'
+import { Cell } from './cell'
+import { ITheme, Terminal } from 'xterm'
import { Clipboard } from '@capacitor/clipboard'
import { Preferences } from '@capacitor/preferences'
import { FitAddon } from 'xterm-addon-fit'
import { SearchAddon } from 'xterm-addon-search'
import { WebglAddon } from 'xterm-addon-webgl'
import { WebLinksAddon } from 'xterm-addon-web-links'
-import { ImageAddon } from 'xterm-addon-image';
+import { ImageAddon } from 'xterm-addon-image'
import { Camera } from '@capacitor/camera'
/* restore the bell. commented as it silences all background audio
-import { BELL_SOUND } from './bell.js'
+import { BELL_SOUND } from './bell'
*/
-import { Failure } from './session'
+import { Channel, Failure } from './session'
import XtermWebfont from '@liveconfig/xterm-webfont'
+import * as Hammer from "hammerjs"
+import { Manager } from "hammerjs"
+import { TerminalWithAddons } from "./map"
-const REGEX_SEARCH = false,
+const ABIT = 10,
+ REGEX_SEARCH = false,
COPYMODE_BORDER_COLOR = "#F952F9",
FOCUSED_BORDER_COLOR = "#F4DB53",
SEARCH_OPTS = {
@@ -32,30 +36,62 @@ const REGEX_SEARCH = false,
caseSensitive: true,
}
+export interface SerializedPane {
+ sx: number,
+ sy: number,
+ xoff: number,
+ yoff: number,
+ fontSize: number,
+ channelID: number,
+ active: boolean,
+ zoomed: boolean,
+ rows: number,
+ cols: number
+}
export class Pane extends Cell {
+ active = false
+ aLeader = false
+ buffer = []
+ channelID: number
+ cmAtEnd?: boolean
+ cmCursor?: {x: number, y:number}
+ cmDecorations = []
+ cmMarking = false
+ cmSelection: {
+ startRow: number,
+ startColumn: number,
+ endRow: number,
+ endColumn: number
+ }
+ copyMode = false
+ d?: Channel = null
+ dividers = []
+ flashTimer? = null
+ fitAddon: FitAddon
+ fontSize: number
+ imageAddon: ImageAddon
+ lastKey = ''
+ needsResize = false
+ searchAddon: SearchAddon
+ searchDown = false
+ searchTerm = ''
+ t: TerminalWithAddons
+ theme: ITheme
+ repetition = 0
+ retries = 0
+ WebLinksAddon: WebLinksAddon
+ resizeObserver: ResizeObserver
+ zoomed = false
+
constructor(props) {
props.className = "pane"
super(props)
this.catchFingers()
- this.d = null
- this.active = false
- this.fontSize = props.fontSize || 12
+ this.fontSize = (props.fontSize || 12 ) * this.gate.fontScale
this.theme = props.theme || this.t7.conf.theme
- this.copyMode = false
- this.cmAtEnd = null
- this.cmCursor = null
- this.cmMarking = false
- this.cmSelection = null
- this.cmDecorations = []
- this.dividers = []
- this.flashTimer = null
- this.aLeader = false
- this.retries = 0
- this.lastKey = ''
- this.repetition = 0
this.resizeObserver = new window.ResizeObserver(() => this.fit())
- this.needsResize = false
+ this.channelID = props.channelID
}
/*
@@ -69,13 +105,13 @@ export class Pane extends Cell {
* Pane.openTerminal opens an xtermjs terminal on our element
*/
openTerminal(parentID, channelID) {
- console.log("in OpenTerminal")
- var con = document.createElement("div")
+ if (channelID)
+ this.channelID = channelID
+ const con = document.createElement("div")
this.t = new Terminal({
convertEol: false,
fontFamily: "FiraCode",
- fontSize: this.fontSize,
- rendererType: "canvas",
+ fontSize: this.fontSize * this.gate.fontScale,
theme: this.theme,
rows:24,
cols:80,
@@ -83,7 +119,7 @@ export class Pane extends Cell {
/* TODO: restore this. commented because it silences spotify
bellStyle: "sound",
bellSound: BELL_SOUND, */
- })
+ }) as TerminalWithAddons
this.fitAddon = new FitAddon()
this.searchAddon = new SearchAddon()
this.WebLinksAddon = new WebLinksAddon((MouseEvent, url) => {
@@ -113,7 +149,7 @@ export class Pane extends Cell {
this.t.loadAddon(webGLAddon)
this.t.textarea.tabIndex = -1
this.t.attachCustomKeyEventHandler(ev => {
- var toDo = true
+ let toDo = true
// ctrl c is a special case
if (ev.ctrlKey && (ev.key == "c") && (this.d != null)) {
this.d.send(String.fromCharCode(3))
@@ -158,7 +194,7 @@ export class Pane extends Cell {
this.t.clearSelection()
}
})
- this.resizeObserver.observe(this.e);
+ this.resizeObserver.observe(this.e)
this.fit(pane => {
if (pane != null)
pane.openChannel({parent: parentID, id: channelID})
@@ -178,23 +214,114 @@ export class Pane extends Cell {
this.fontSize += by
if (this.fontSize < 6) this.fontSize = 6
else if (this.fontSize > 30) this.fontSize = 30
- this.t.options.fontSize = this.fontSize
+ const fontSize = this.fontSize * this.gate.fontScale
+ this.t.options.fontSize = Math.floor(fontSize) + (String(fontSize).includes('.') ? .5 : 0)
this.fit()
+ this.gate.sendState()
}
+ /*
+ * Catches gestures on an element using hammerjs.
+ * If an element is not passed in, `this.e` is used
+ */
+ catchFingers(elem = undefined) {
+ const e = (typeof elem == 'undefined')?this.e:elem,
+ h = new Manager(e, {}),
+ // h.options.domEvents=true; // enable dom events
+ singleTap = new Hammer.Tap({event: "tap"}),
+ doubleTap = new Hammer.Tap({event: "doubletap", taps: 2}),
+ pinch = new Hammer.Pinch({event: "pinch"})
+
+ h.add([singleTap,
+ doubleTap,
+ pinch,
+ new Hammer.Tap({event: "twofingerstap", pointers: 2})])
+
+ h.on('tap', () => {
+ if (this.w.activeP != this as unknown as Pane) {
+ this.focus()
+ this.gate.sendState()
+ }
+ })
+ h.on('twofingerstap', () => {
+ this.toggleZoom()
+ })
+ h.on('doubletap', () => {
+ this.toggleZoom()
+ })
+
+ h.on('pinch', (e: HammerInput) => {
+ // @ts-ignore additionalEvent is not in the .d.ts
+ this.t7.log(e.additionalEvent, e.distance, e.velocityX, e.velocityY, e.direction, e.isFinal)
+ if (e.deltaTime < this.lastEventT)
+ this.lastEventT = 0
+ if ((e.deltaTime - this.lastEventT < 200) ||
+ (e.velocityY > this.t7.conf.ui.pinchMaxYVelocity))
+ return
+ this.lastEventT = e.deltaTime
+
+ // @ts-ignore additionalEvent is not in the .d.ts
+ if (e.additionalEvent == "pinchout")
+ this.scale(1)
+ else
+ this.scale(-1)
+ })
+ }
+
+ zoom() {
+ const c = document.createElement('div'),
+ e = document.createElement('div'),
+ te = this.e.removeChild(this.e.children[0])
+ c.classList.add("zoomed")
+ e.classList.add("pane", "focused")
+ e.style.borderColor = FOCUSED_BORDER_COLOR
+ e.appendChild(te)
+ c.appendChild(e)
+ this.catchFingers(e)
+ document.body.appendChild(c)
+ this.t7.zoomedE = c
+ this.w.e.classList.add("hidden")
+ this.resizeObserver = new window.ResizeObserver(() => this.styleZoomed(e))
+ this.resizeObserver.observe(e)
+ this.zoomed = true
+ }
+ unzoom() {
+ if (this.resizeObserver != null) {
+ this.resizeObserver.disconnect()
+ this.resizeObserver = null
+ }
+ const te = this.t7.zoomedE.children[0].children[0]
+ this.e.appendChild(te)
+ document.body.removeChild(this.t7.zoomedE)
+ this.t7.zoomedE = null
+ this.w.e.classList.remove("hidden")
+ this.zoomed = false
+ }
+
+ toggleZoom() {
+ if (this.zoomed)
+ this.unzoom()
+ else
+ this.zoom()
+ this.gate.sendState()
+ this.t7.run(() => this.focus(), ABIT)
+
+ this.fit()
+ }
// fit a pane to the display area. If it was resized, the server is updated.
// returns true is size was changed
// TODO: make it async
- fit(cb) {
- if (!this.t) {
- if (cb instanceof Function) cb(this)
+ fit(cb = null) {
+ if (!this.t || !this.gate?.fitScreen) {
+ if (cb instanceof Function)
+ cb(this)
return
}
- let oldr = this.t.rows
- let oldc = this.t.cols
+ const oldr = this.t.rows
+ const oldc = this.t.cols
// there's no point in fitting when in the middle of a restore
- // it happens in the eend anyway
+ // it happens in the end anyway
try {
this.fitAddon.fit()
} catch (e) {
@@ -207,7 +334,7 @@ export class Pane extends Cell {
}
this.refreshDividers()
if (this.t.rows != oldr || this.t.cols != oldc) {
- if (this.d)
+ if (this.d && this.gate.fitScreen)
this.d.resize(this.t.cols, this.t.rows)
else
this.needsResize = true
@@ -229,12 +356,10 @@ export class Pane extends Cell {
* and the relative size (0-1) of the area left for us.
* Returns the new pane.
*/
- split(dir, s) {
+ split(dir, s = 0.5) {
if (!this.isSplittable(dir)) return
- var sx, sy, xoff, yoff, l
+ let sx, sy, xoff, yoff, l
// if the current dir is `TBD` we can swing it our way
- if (typeof s == "undefined")
- s = 0.5
if ((this.layout.dir == "TBD") || (this.layout.cells.length == 1))
this.layout.dir = dir
// if we need to create a new layout do it and add us and new pane as cells
@@ -251,7 +376,7 @@ export class Pane extends Cell {
this.sy -= sy
yoff = this.yoff + this.sy
}
- else {
+ else {
sy = this.sy
sx = this.sx * (1 - s)
yoff = this.yoff
@@ -261,16 +386,22 @@ export class Pane extends Cell {
this.fit()
// add the new pane
- let p = l.addPane({sx: sx, sy: sy,
+ const p = l.addPane({sx: sx, sy: sy,
xoff: xoff, yoff: yoff,
parent: this})
p.focus()
- this.gate.sendState()
return p
}
onChannelConnected(channel) {
- const reconnect = this.d != null
+ const reconnect = typeof this.channelID == "number"
+ if (this.d) {
+ this.d.onMessage = undefined
+ this.d.onClose = undefined
+ this.d.close()
+ }
+
this.d = channel
+ this.channelID = channel.id
this.d.onMessage = m => this.onChannelMessage(m)
this.d.onClose = () => {
this.d = null
@@ -290,13 +421,13 @@ export class Pane extends Cell {
this.buffer = []
if (opts.id) {
this.gate.session.openChannel(opts.id)
- .then((channel, id) =>this.onChannelConnected(channel, id))
+ .then((channel) =>this.onChannelConnected(channel))
.then(resolve)
.catch(m => console.log(m))
} else {
this.gate.session.openChannel(
this.t7.conf.exec.shell, opts.parent, this.t.cols, this.t.rows)
- .then((channel, id) =>this.onChannelConnected(channel, id))
+ .then((channel) =>this.onChannelConnected(channel))
.then(resolve)
.catch(m => console.log(m))
}
@@ -316,10 +447,6 @@ export class Pane extends Cell {
this.flashIndicator()
this.write(m)
}
- toggleZoom() {
- super.toggleZoom()
- this.fit()
- }
toggleSearch() {
const se = this.gate.e.querySelector(".search-box")
if (!se.classList.contains("show"))
@@ -330,15 +457,15 @@ export class Pane extends Cell {
}
}
- showSearch(searchDown) {
+ showSearch(searchDown = false) {
// show the search field
- this.searchDown = searchDown || false
+ this.searchDown = searchDown
const se = this.gate.e.querySelector(".search-box")
se.classList.add("show")
se.classList.remove("hidden")
document.getElementById("search-button").classList.add("on")
// TODO: restore regex search
- let i = se.querySelector("input[name='search-term']")
+ const i = se.querySelector("input[name='search-term']") as HTMLInputElement
this.disableSearchButtons()
i.setAttribute("placeholder", "search string here")
if (this.searchTerm)
@@ -363,7 +490,20 @@ export class Pane extends Cell {
})
i.focus()
}
- enterCopyMode(marking) {
+ styleZoomed(e = null) {
+ e = e || this.t7.zoomedE.querySelector(".pane")
+ const se = this.gate.e.querySelector(".search-box")
+ let style
+ if (se.classList.contains("show"))
+ style = `${(document.querySelector('.windows-container') as HTMLDivElement).offsetHeight - 22}px`
+ else
+ style = `${document.body.offsetHeight - 36}px`
+ e.style.height = style
+ e.style.top = "0px"
+ e.style.width = "100%"
+ this.fit()
+ }
+ enterCopyMode(marking = false) {
if (marking)
this.cmMarking = true
if (!this.copyMode) {
@@ -371,7 +511,7 @@ export class Pane extends Cell {
this.cmInitCursor()
this.cmAtEnd = null
if (this.zoomed)
- this.t7.zoomedE.children[0].style.borderColor = COPYMODE_BORDER_COLOR
+ (this.t7.zoomedE.children[0] as HTMLElement).style.borderColor = COPYMODE_BORDER_COLOR
else
this.e.style.borderColor = COPYMODE_BORDER_COLOR
Preferences.get({key: "first_copymode"}).then(v => {
@@ -392,7 +532,7 @@ export class Pane extends Cell {
this.t.clearSelection()
this.t.scrollToBottom()
if (this.zoomed)
- this.t7.zoomedE.children[0].style.borderColor = FOCUSED_BORDER_COLOR
+ (this.t7.zoomedE.children[0] as HTMLElement).style.borderColor = FOCUSED_BORDER_COLOR
else
this.e.style.borderColor = FOCUSED_BORDER_COLOR
this.focus()
@@ -407,56 +547,55 @@ export class Pane extends Cell {
this.styleZoomed()
}
exitSearch() {
- this.hideSearch();
- this.exitCopyMode();
+ this.hideSearch()
+ this.exitCopyMode()
}
handleMetaKey(ev) {
- var f = null
- this.t7.log(`Handling meta key ${ev.key}`)
- switch (ev.key) {
- case "c":
+ let f = null
+ this.t7.log(`Handling meta key ${ev.code}`)
+ switch (ev.code) {
+ case "KeyC":
this.copySelection()
break
- case "z":
+ case "KeyZ":
f = () => this.toggleZoom()
break
- case ",":
+ case "Comma":
f = () => this.w.rename()
break
- case "d":
+ case "KeyD":
f = () => this.close()
break
- case "0":
+ case "Digit0":
f = () => this.scale(12 - this.fontSize)
break
- case "=":
+ case "Equal":
f = () => this.scale(1)
break
- case "-":
+ case "Minus":
f = () => this.scale(-1)
break
- case "\\":
+ case "Backslash":
f = () => this.split("topbottom")
break
- case "'":
+ case "Quote":
f = () => this.split("rightleft")
break
- case "[":
-
+ case "BracketLeft":
f = () => this.enterCopyMode()
break
- case "f":
+ case "KeyF":
f = () => this.showSearch()
break
// next two keys are on the gate level
- case "t":
+ case "KeyT":
f = () => this.gate.newTab()
break
- case "r":
+ case "KeyR":
f = () => this.gate.reset()
break
// this key is at terminal level
- case "l":
+ case "KeyL":
f = () => this.t7.map.showLog()
break
case "ArrowLeft":
@@ -471,14 +610,17 @@ export class Pane extends Cell {
case "ArrowDown":
f = () => this.w.moveFocus("down")
break
- case "p":
+ case "KeyP":
f = () => this.t7.dumpLog()
break
default:
- if (ev.key >= "1" && ev.key <= "9") {
+ if (ev.code >= "Digit1" && ev.code <= "Digit9") {
const win = this.gate.windows[ev.key - 1]
+ if (this.zoomed)
+ this.toggleZoom()
if (win)
win.focus()
+ ev.preventDefault()
}
break
}
@@ -489,25 +631,13 @@ export class Pane extends Cell {
}
return true
}
- findNext(searchTerm) {
- const notFound = this.gate.e.querySelector(".not-found")
- if (searchTerm) {
- this.cmAtEnd = null
- // this.t.options.selectionStyle = "plain"
- this.searchTerm = searchTerm
- }
-
- if (this.searchTerm) {
- if (!this.searchAddon.findNext(this.searchTerm, SEARCH_OPTS))
- notFound.classList.remove("hidden")
- else {
- notFound.classList.add("hidden")
- this.enterCopyMode(true)
- this.markSelection()
- }
- }
+ findNext(searchTerm = '') {
+ this.find(searchTerm, (st) => this.searchAddon.findNext(st, SEARCH_OPTS))
}
- findPrev(searchTerm) {
+ findPrev(searchTerm = '') {
+ this.find(searchTerm, (st) => this.searchAddon.findPrevious(st, SEARCH_OPTS))
+ }
+ private find(searchTerm: string, findFunc: (string) => boolean): void {
const notFound = this.gate.e.querySelector(".not-found")
if (searchTerm) {
this.cmAtEnd = null
@@ -516,7 +646,7 @@ export class Pane extends Cell {
}
if (this.searchTerm) {
- if (!this.searchAddon.findPrevious(this.searchTerm, SEARCH_OPTS))
+ if (!findFunc(this.searchTerm))
notFound.classList.remove("hidden")
else {
notFound.classList.add("hidden")
@@ -540,13 +670,13 @@ export class Pane extends Cell {
* */
createDividers() {
// create the dividers
- var t = document.getElementById("divider-template")
+ const t = document.getElementById("divider-template") as HTMLTemplateElement
if (t) {
- var d = [t.content.cloneNode(true),
+ const d = [t.content.cloneNode(true),
t.content.cloneNode(true)]
- d.forEach((e, i) => {
+ d.forEach((e: HTMLElement & {pane?: Pane}, i) => {
this.w.e.prepend(e)
- e = this.w.e.children[0]
+ e = this.w.e.children[0] as HTMLElement
e.classList.add((i==0)?"left-divider":"top-divider")
e.pane = this
this.dividers.push(e)
@@ -558,10 +688,10 @@ export class Pane extends Cell {
* moved or resized
*/
refreshDividers() {
- var W = this.w.e.offsetWidth,
- H = this.w.e.offsetHeight,
- d = this.dividers[0]
- if (this.xoff > 0.001 & this.sy * H > 50) {
+ const W = this.w.e.offsetWidth,
+ H = this.w.e.offsetHeight
+ let d = this.dividers[0]
+ if (this.xoff > 0.001 && this.sy * H > 50) {
// refresh left divider position
d.style.left = `${this.xoff * W - 4 - 20 }px`
d.style.top = `${(this.yoff + this.sy/2)* H - 22 - 40}px`
@@ -569,7 +699,7 @@ export class Pane extends Cell {
} else
d.classList.add("hidden")
d = this.dividers[1]
- if (this.yoff > 0.001 & this.sx * W > 50) {
+ if (this.yoff > 0.001 && this.sx * W > 50) {
// refresh top divider position
d.style.top = `${this.yoff * H - 25 - 20 }px`
d.style.left = `${(this.xoff + this.sx/2)* W - 22 - 40}px`
@@ -579,25 +709,31 @@ export class Pane extends Cell {
}
close() {
try {
- this.resizeObserver.unobserve(this.e);
+ this.resizeObserver.unobserve(this.e)
} catch (e) {}
if (this.d)
this.d.close()
this.dividers.forEach(d => d.classList.add("hidden"))
document.querySelector('.add-tab').classList.remove("off")
+ if (this.zoomed)
+ this.unzoom()
super.close()
}
- dump() {
- var cell = {
+ dump(): SerializedPane {
+ const cell = {
sx: this.sx,
sy: this.sy,
xoff: this.xoff,
yoff: this.yoff,
- fontSize: this.fontSize
+ fontSize: this.fontSize,
+ channelID: null,
+ active: false,
+ zoomed: false,
+ rows: this.t.rows,
+ cols: this.t.cols
}
- if (this.d)
- cell.channel_id = this.d.id
+ cell.channelID = this.channelID
if (this.w.activeP && this == this.w.activeP)
cell.active = true
if (this.zoomed)
@@ -627,7 +763,7 @@ export class Pane extends Cell {
return Clipboard.write({string: lines.join('\n')})
}
handleCMKey(key) {
- var x, y, newX, newY,
+ let x, y, newX, newY,
selection = this.cmSelection,
line
// chose the x & y we're going to change
@@ -636,7 +772,7 @@ export class Pane extends Cell {
if (!this.cmCursor)
this.cmInitCursor()
x = this.cmCursor.x
- y = this.cmCursor.y;
+ y = this.cmCursor.y
selection = {
startColumn: x,
endColumn: x,
@@ -646,11 +782,11 @@ export class Pane extends Cell {
}
else if (this.cmAtEnd) {
x = selection.endColumn
- y = selection.endRow;
+ y = selection.endRow
}
else {
x = selection.startColumn
- y = selection.startRow;
+ y = selection.startRow
}
newX = x
newY = y
@@ -658,7 +794,7 @@ export class Pane extends Cell {
if (key.match(/\d/))
this.repetition = 10 * this.repetition + parseInt(key)
else {
- let temp = this.repetition
+ const temp = this.repetition
this.repetition = 0
for (let i = 0; i < temp; i++) {
this.handleCMKey(key)
@@ -726,7 +862,7 @@ export class Pane extends Cell {
break
case "Enter":
this.copySelection()
- this.exitCopyMode();
+ this.exitCopyMode()
break
case '/':
this.showSearch(true)
@@ -860,7 +996,7 @@ export class Pane extends Cell {
if ((newY != y) || (newX != x)) {
if (!this.cmMarking) {
this.cmCursor.x = newX
- this.cmCursor.y = newY;
+ this.cmCursor.y = newY
}
else if (this.cmAtEnd) {
if ((newY < selection.startRow) ||
@@ -893,8 +1029,8 @@ export class Pane extends Cell {
this.cmSelectionUpdate(selection)
if ((newY >= this.t.buffer.active.viewportY + this.t.rows) ||
(newY < this.t.buffer.active.viewportY)) {
- let scroll = newY - this.t.buffer.active.viewportY
- this.t.scrollLines(scroll, true)
+ const scroll = newY - this.t.buffer.active.viewportY
+ this.t.scrollLines(scroll)
}
}
}
@@ -993,14 +1129,14 @@ export class Pane extends Cell {
}
enableSearchButtons() {
const se = this.gate.e.querySelector(".search-box")
- let up = se.querySelector(".search-up"),
+ const up = se.querySelector(".search-up"),
down = se.querySelector(".search-down")
up.classList.remove("off")
down.classList.remove("off")
}
disableSearchButtons() {
const se = this.gate.e.querySelector(".search-box")
- let up = se.querySelector(".search-up"),
+ const up = se.querySelector(".search-up"),
down = se.querySelector(".search-down")
up.classList.add("off")
down.classList.add("off")
@@ -1009,7 +1145,7 @@ export class Pane extends Cell {
startIndex = startIndex || 0
let match = -1
str.replace(regex, (...args) => {
- let i = args.find(x => typeof(x) == "number")
+ const i = args.find(x => typeof(x) == "number")
if (match == -1 && i > startIndex)
match = i
})
@@ -1017,7 +1153,7 @@ export class Pane extends Cell {
}
// showVideo replace the terminal with a video and vice versa
// if `show` is undefined the video is toggled
- showVideo(show) {
+ showVideo(show = undefined) {
const video = document.querySelector("video")
if (show === undefined)
show = video === null
@@ -1029,7 +1165,7 @@ export class Pane extends Cell {
if (show) {
// first remove all videos
button.classList.add("on")
- const v = document.createElement("video");
+ const v = document.createElement("video")
this.e.querySelector("div").classList.add("hidden")
this.e.prepend(v)
Camera.checkPermissions().then(result => {
diff --git a/src/peerbook.ts b/src/peerbook.ts
index 5b3ff503..b92f8306 100644
--- a/src/peerbook.ts
+++ b/src/peerbook.ts
@@ -7,15 +7,35 @@
* License: GPLv3
*/
-export const PB = "\uD83D\uDCD6"
+import { CustomerInfo } from "@revenuecat/purchases-typescript-internal-esm"
-import { Device } from '@capacitor/device';
-import { CapacitorPurchases } from '@capgo/capacitor-purchases'
-import { Failure } from './session'
+export const PB = "\uD83D\uDCD6"
+import { Device } from '@capacitor/device'
+import { Purchases } from '@revenuecat/purchases-capacitor'
import { HTTPWebRTCSession } from './webrtc_session'
import { Gate } from './gate'
import { Shell } from './shell'
-import { Capacitor } from '@capacitor/core';
+import { Capacitor } from '@capacitor/core'
+import {ERROR_HTML_SYMBOL, CLOSED_HTML_SYMBOL, OPEN_HTML_SYMBOL} from './terminal7'
+
+interface PeerbookProps {
+ fp: string,
+ host: string,
+ insecure: boolean,
+ shell: Shell
+}
+
+interface Peer {
+ name: string
+ user: string
+ kind: string
+ verified: boolean
+ created_on: number
+ verified_on: number
+ last_connected: number
+ online: boolean
+ auth_token?: string
+}
export class PeerbookConnection {
ws: WebSocket = null
@@ -30,8 +50,10 @@ export class PeerbookConnection {
shell: Shell
uid: string
updatingStore = false
+ spinnerInterval = null
+ headers: Map
- constructor(props:Map) {
+ constructor(props:PeerbookProps) {
// copy all props to this
Object.assign(this, props)
this.pending = []
@@ -40,7 +62,7 @@ export class PeerbookConnection {
this.uid = ""
}
- async adminCommand(cmd: string, ...args: string[]) {
+ async adminCommand(cmd: string, ...args: string[]): Promise {
const c = args?[cmd, ...args]:[cmd]
if (!this.session) {
console.log("Admin command with no session")
@@ -48,7 +70,7 @@ export class PeerbookConnection {
await this.connect()
} catch (e) {
console.log("Failed to connect to peerbook", e)
- throw new Failure("Failed to connect")
+ throw new Error("Failed to connect")
}
}
@@ -60,7 +82,7 @@ export class PeerbookConnection {
terminal7.log(`cmd ${cmd} ${args} closed with: ${ret}`)
resolve(ret)
}
- channel.onMessage = (data) => {
+ channel.onMessage = (data: Uint8Array) => {
reply.push(...data)
}
}).catch(reject)
@@ -109,10 +131,10 @@ export class PeerbookConnection {
this.echo("and use it to generate a One Time Password")
// verify ourselves - it's the first time and we were approved thanks
// to the revenuecat's user id
- this.shell.startWatchdog().catch(() => {
+ this.shell.startWatchdog(3000).catch(() => {
this.shell.t.writeln("Timed out waiting for OTP")
this.shell.printPrompt()
- }, 3000)
+ })
try {
fp = await terminal7.getFingerprint()
} catch (e) {
@@ -130,7 +152,7 @@ export class PeerbookConnection {
} finally {
this.shell.stopWatchdog()
}
- await CapacitorPurchases.logIn({ appUserID: uid })
+ await Purchases.logIn({ appUserID: uid })
this.shell.t.writeln("Validated! Use `install` to install on a server")
try {
await this.wsConnect()
@@ -142,6 +164,7 @@ export class PeerbookConnection {
}
async startPurchases() {
console.log("Starting purchases")
+ await Purchases.setMockWebResults({ shouldMockWebResults: true })
const keys = {
ios: 'appl_qKHwbgKuoVXokCTMuLRwvukoqkd',
android: 'goog_ncGFZWWmIsdzdfkyMRtPqqyNlsx'
@@ -151,8 +174,7 @@ export class PeerbookConnection {
}
try {
- await CapacitorPurchases.setDebugLogsEnabled({ enabled: true })
- await CapacitorPurchases.setup(props)
+ await Purchases.configure(props)
} catch (e) {
terminal7.log("Failed to setup purchases", e)
return
@@ -163,9 +185,11 @@ export class PeerbookConnection {
* gets customer info from revenuecat and act on it
*/
async updateCustomerInfo() {
- let data: CapacitorPurchases.PurchasesUpdatedPurchaserInfo
+ let data: {
+ customerInfo: CustomerInfo;
+ }
try {
- data = await CapacitorPurchases.getCustomerInfo()
+ data = await Purchases.getCustomerInfo()
} catch (e) {
terminal7.log("Failed to get customer info", e)
return
@@ -183,14 +207,6 @@ export class PeerbookConnection {
const active = data.customerInfo.entitlements.active
this.close()
if (!active.peerbook) {
- // log out to clear the cache
- /*
- try {
- CapacitorPurchases.logOut()
- } catch (e) {
- terminal7.log("Failed to log out", e)
- }
- */
this.updatingStore = false
return
}
@@ -253,7 +269,7 @@ export class PeerbookConnection {
terminal7.log("Got TBD as uid")
reject("Unregistered")
} else {
- CapacitorPurchases.logIn({ appUserID: uid })
+ Purchases.logIn({ appUserID: uid })
this.wsConnect().then(resolve).catch(reject)
}
}).catch(e => {
@@ -264,6 +280,7 @@ export class PeerbookConnection {
return
}
else if (state == 'failed') {
+ this.stopSpinner()
this.session = null
console.log("PB webrtc connection failed", failure)
if (this.uid == "TBD")
@@ -296,19 +313,22 @@ export class PeerbookConnection {
}
}
const schema = this.insecure?"ws":"wss",
- url = encodeURI(`${schema}://${this.host}/ws?fp=${this.fp}`)
- const ws = new WebSocket(url)
+ url = encodeURI(`${schema}://${this.host}/ws?fp=${this.fp}`),
+ statusE = document.getElementById("peerbook-status"),
+ ws = new WebSocket(url)
this.ws = ws
ws.onmessage = ev => {
const m = JSON.parse(ev.data)
if (m.code >= 400) {
console.log("peerbook connect got code", m.code)
- reject(`PeerBook connection error ${m.code}`)
+ this.stopSpinner()
+ statusE.innerHTML = ERROR_HTML_SYMBOL
+ // reject(`PeerBook connection error ${m.code}`)
return
}
if (firstMessage) {
+ this.stopSpinner()
firstMessage = false
- terminal7.notify(`Connected to ${PB} PeerBook ${PB}`)
resolve()
}
if (this.onUpdate)
@@ -317,13 +337,18 @@ export class PeerbookConnection {
terminal7.log("got ws message but no onUpdate", m)
}
ws.onerror = ev => {
- window.terminal7.log("peerbook ws error", ev.toString())
- reject(ev.toString())
+ terminal7.log("peerbook ws error", ev.toString())
+ this.ws = null
+ this.stopSpinner()
+ statusE.innerHTML = ERROR_HTML_SYMBOL
+ reject()
}
ws.onclose = (ev) => {
- window.terminal7.log("peerbook ws closed", ev)
- window.terminal7.notify(`${PB} Disconnected. Please \`sub\` to reconnect`)
+ terminal7.log("peerbook ws closed", ev)
this.ws = null
+ this.stopSpinner()
+ if (statusE.innerHTML != ERROR_HTML_SYMBOL)
+ statusE.innerHTML = CLOSED_HTML_SYMBOL
}
ws.onopen = () => {
console.log("peerbook ws open")
@@ -394,7 +419,7 @@ export class PeerbookConnection {
})
return ret
}
- async verifyFP(fp: string, prompt: string) {
+ async verifyFP(fp: string, prompt?: string) {
let validated = false
// TODO:gAdd biometrics verification
while (!validated) {
@@ -423,13 +448,10 @@ export class PeerbookConnection {
this.echo("Invalid OTP, please try again")
}
}
- purchase(id, offeringId): Promise {
+ purchase(aPackage): Promise {
return new Promise((resolve, reject) => {
// ensure there's only one listener
- CapacitorPurchases.purchasePackage({
- identifier: id,
- offeringIdentifier: offeringId,
- }).then(customerInfo => {
+ Purchases.purchasePackage({ aPackage }).then(customerInfo => {
this.onPurchasesUpdate(customerInfo).then(resolve).catch(reject)
}).catch(e => {
console.log("purchase failed", e)
@@ -437,4 +459,28 @@ export class PeerbookConnection {
})
})
}
+ stopSpinner() {
+ const statusE = document.getElementById("peerbook-status") as HTMLElement
+ statusE.style.opacity = "1"
+ if (this.spinnerInterval) {
+ clearInterval(this.spinnerInterval)
+ this.spinnerInterval = null
+ }
+ }
+ startSpinner() {
+ const statusE = document.getElementById("peerbook-status")
+ let i = 0.1, change = 0.1
+ if (this.spinnerInterval)
+ return
+ this.spinnerInterval = setInterval(() => {
+ i = i + change
+ if (i > 1 || i < 0) {
+ change = -change
+ i = i + change
+ }
+ statusE.style.opacity = String(i)
+ }, 200)
+ statusE.innerHTML = OPEN_HTML_SYMBOL
+ statusE.style.opacity = "0"
+ }
}
diff --git a/src/session.ts b/src/session.ts
index 88e258a1..72044f6b 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -2,7 +2,8 @@ import { Terminal7 } from "./terminal7"
export type CallbackType = (e: unknown) => void
export type ChannelID = number
-export type State = "new" | "connecting" | "connected" | "reconnected" | "disconnected" | "failed" | "unauthorized" | "wrong password"
+export type State = "new" | "connecting" | "connected" | "reconnected" | "disconnected" | "failed" |
+ "unauthorized" | "wrong password" | "closed"
// possible reasons for a failure
export enum Failure {
@@ -19,6 +20,7 @@ export enum Failure {
WrongAddress='Wrong Address',
DataChannelLost="Data Channel Lost",
FailedToConnect="Failed To Connect",
+ Overrun='Overrun',
}
export interface Event {
@@ -31,43 +33,41 @@ export interface Channel {
id?: ChannelID
onClose : CallbackType
onMessage : CallbackType
- close(): Promise
- send(data: string): void
+ close(): void
+ send(data: ArrayBuffer | string): void
resize(sx: number, sy: number): Promise
- get readyState(): string
+ readonly readyState: string
}
export interface Session {
readonly isSSH: boolean
onStateChange : (state: State, failure?: Failure) => void
- onPayloadUpdate: (payload: string) => void
+ onCMD: (payload: unknown) => void
// for reconnect
- openChannel(id: ChannelID): Promise
- // for new channel
- openChannel(cmd: string | string[], parent?: ChannelID, sx?: number, sy?: number):
- Promise
+ openChannel(id: ChannelID | string | string[], parent?: ChannelID, sx?: number, sy?: number): Promise
close(): void
- getPayload(): Promise
- setPayload(payload: string): Promise
- reconnect(marker?: number, publicKey?: string, privateKey?: string): Promise
- disconnect(): Promise
- connect(marker?:number | null, publicKey?: string, privateKey?: string): void
+ getPayload(): Promise
+ setPayload(payload: unknown): Promise
+ reconnect(marker?: number, publicKey?: string, privateKey?: string): Promise
+ disconnect(): Promise
+ connect(marker?: number, publicKey?: string, privateKey?: string): Promise
+ connect(marker?: number, noCDC?: boolean): Promise
fail(failure?: Failure): void
}
export abstract class BaseChannel implements Channel {
id?: ChannelID
- t7: object
+ t7: Terminal7
onClose : CallbackType
onMessage : CallbackType
- abstract close(): Promise
- abstract send(data: string): void
+ abstract close(): void
+ abstract send(data: ArrayBuffer): void
abstract resize(sx: number, sy: number): Promise
constructor() {
this.onMessage = () => void 0
this.onClose = () => void 0
- this.t7 = window.terminal7
+ this.t7 = terminal7
}
get readyState(): string {
@@ -78,25 +78,26 @@ export abstract class BaseSession implements Session {
t7: Terminal7
watchdog: number
onStateChange : (state: State, failure?: Failure) => void
- onPayloadUpdate: (payload: string) => void
- constructor() {
- this.t7 = window.terminal7
+ onCMD: (payload: string) => void
+ protected constructor() {
+ this.t7 = terminal7
}
get isSSH(): boolean {
throw new Error("Not implemented")
}
- async getPayload(): Promise {
+ async getPayload(): Promise {
return null
}
// TODO: get it to throw "Not Implemented"
async setPayload(payload) {
console.log(`ignoring set payload: ${JSON.stringify(payload)}`)
}
- async reconnect(): Promise {
+ async reconnect(marker?: number, publicKey?: string, privateKey?: string): Promise {
throw "Not Implemented"
}
+
// base disconnect is rejected as it's not supported
- disconnect(): Promise{
+ disconnect(): Promise {
return new Promise((resolve, reject) => {
reject()
})
@@ -107,9 +108,11 @@ export abstract class BaseSession implements Session {
this.onStateChange("failed", failure)
}
abstract close(): void
+
// for reconnect
- abstract openChannel(id: ChannelID): Promise
- abstract openChannel(cmd: string | ChannelID, parent?: ChannelID, sx?: number, sy?: number):
- Promise
- abstract connect(): void
+ abstract openChannel(id: number | string | string[], parent?: number, sx?: number, sy?: number): Promise
+
+ abstract connect(marker?: number, publicKey?: string, privateKey?: string): Promise
+ abstract connect(marker?: number, noCDC?: boolean): Promise
+
}
diff --git a/src/shell.ts b/src/shell.ts
index a5dd20fd..f47da96d 100644
--- a/src/shell.ts
+++ b/src/shell.ts
@@ -1,14 +1,13 @@
-import { Channel } from "./session"
+import { Channel, Failure } from "./session"
import { Clipboard } from "@capacitor/clipboard"
import { Terminal } from 'xterm'
import { Command, loadCommands } from './commands'
import { Fields, Form } from './form'
import { Gate } from "./gate"
import { T7Map } from './map'
-import { Failure } from "./session"
import CodeMirror from '@tuzig/codemirror/src/codemirror.js'
import { vimMode } from '@tuzig/codemirror/keymap/vim.js'
-import { tomlMode} from '@tuzig/codemirror/mode/toml/toml.js'
+import { tomlMode } from '@tuzig/codemirror/mode/toml/toml.js'
import { dialogAddOn } from '@tuzig/codemirror/addon/dialog/dialog.js'
export class Shell {
@@ -31,7 +30,7 @@ export class Shell {
historyIndex = 0
confEditor: CodeMirror.EditorFromTextArea
exitConf: () => void
- lineAboveForm: 0
+ lineAboveForm = 0
constructor(map: T7Map) {
this.map = map
@@ -201,7 +200,7 @@ export class Shell {
if (!this.watchdog) return
this.stopWatchdog()
if (terminal7.activeG)
- terminal7.activeG.onFailure("Overrun")
+ terminal7.activeG.onFailure(Failure.Overrun)
await new Promise(r => setTimeout(r, 100))
this.printPrompt()
}
@@ -339,7 +338,7 @@ export class Shell {
async openConfig() {
const modal = document.getElementById("settings"),
button = document.getElementById("dotfile-button"),
- area = document.getElementById("edit-conf"),
+ area = document.getElementById("edit-conf") as HTMLTextAreaElement,
conf = await terminal7.getDotfile()
area.value = conf
@@ -371,7 +370,7 @@ export class Shell {
}
closeConfig(save = false) {
- const area = document.getElementById("edit-conf")
+ const area = document.getElementById("edit-conf") as HTMLTextAreaElement
document.getElementById("dotfile-button").classList.remove("on")
if (save) {
this.confEditor.save()
diff --git a/src/ssh_session.ts b/src/ssh_session.ts
index 07d3ca63..6b773b8f 100644
--- a/src/ssh_session.ts
+++ b/src/ssh_session.ts
@@ -1,5 +1,5 @@
import { SSH, SSHSessionID, StartByPasswd, StartByKey} from 'capacitor-ssh-plugin'
-import { Channel, BaseChannel, BaseSession, Failure, Session, State, ChannelID } from './session'
+import { BaseChannel, BaseSession, Failure, Session, ChannelID } from './session'
import { WebRTCSession } from './webrtc_session'
const ACCEPT_CMD = "/usr/local/bin/webexec accept"
@@ -8,7 +8,9 @@ export class SSHChannel extends BaseChannel {
async close(): Promise {
return SSH.closeChannel({channel: this.id})
}
- send(data: string): void {
+ send(data: string | ArrayBuffer): void {
+ //TODO: remove next line when fixed - https://github.com/tuzig/capacitor-ssh-plugin/issues/15
+ //@ts-ignore the field is actually called "message" not "s"
SSH.writeToChannel({channel: this.id, message: data})
.catch(e => console.log("error from writeToChannel", e))
}
@@ -26,14 +28,12 @@ export class SSHChannel extends BaseChannel {
}
}
}
-// SSHSession is an implmentation of a real time session over ssh
+// SSHSession is an implementation of a real time session over ssh
export class SSHSession extends BaseSession {
id: SSHSessionID
address: string
username: string
port: number
- onStateChange : (state: State, failure?: Failure) => void
- onPayloadUpdate: (payload: string) => void
constructor(address: string, username: string, port=22) {
super()
this.username = username
@@ -45,12 +45,13 @@ export class SSHSession extends BaseSession {
this.id = session
this.onStateChange("connected")
}
- async connect(marker?:number, publicKey: string, privateKey:string) {
+ async connect(marker?:number, publicKey?: string | boolean, privateKey?: string): Promise {
terminal7.log("Connecting using SSH", this.address, this.username, this.port)
SSH.startSessionByKey({
address: this.address,
port: this.port,
username: this.username,
+ // @ts-ignore
publicKey: publicKey,
privateKey: privateKey
}).then(args => {
@@ -75,6 +76,7 @@ export class SSHSession extends BaseSession {
}
SSH.startSessionByPasswd(args)
.then(args => {
+ //@ts-ignore bug in .d.ts?
this.onSSHSession(args.session)
}).catch(e => {
const msg = e.toString()
@@ -90,21 +92,21 @@ export class SSHSession extends BaseSession {
}
openChannel(cmd: unknown, parent?: ChannelID, sx?: number, sy?: number):
- Promise {
+ Promise {
return new Promise((resolve, reject) => {
const channel = new SSHChannel()
SSH.newChannel({session: this.id})
.then(({ id }) => {
console.log("got new channel with id ", id)
channel.id = id
- SSH.startShell({channel: id, command: cmd}, m => channel.handleData(m))
+ SSH.startShell({channel: id, command: cmd as string}, m => channel.handleData(m))
.then(callbackID => {
console.log("got from startShell: ", callbackID)
- resolve(channel, id)
+ resolve(channel)
SSH.setPtySize({channel: id, width: sx, height: sy})
.then(() => {
console.log("after setting size")
- resolve(channel, id)
+ resolve(channel)
})
}).catch(e => {
@@ -138,10 +140,8 @@ export class SSHSession extends BaseSession {
// over SSH
export class HybridSession extends SSHSession {
candidate: string
- webrtcSession: Session
+ webrtcSession: WebRTCSession
sentMessages: Array
- onStateChange : (state: State, failure?: Failure) => void
- onPayloadUpdate: (payload: string) => void
gotREADY: boolean
constructor(address: string, username: string, port=22) {
super(address, username, port)
@@ -153,11 +153,13 @@ export class HybridSession extends SSHSession {
* connect must recieve either password or tag to choose
* whether to use password or identity key based authentication
*/
- async connect(marker?:number, publicKey: string, privateKey:string) {
+ async connect(marker?:number, publicKey?: string, privateKey?:string) {
+
const args: StartByKey = {
address: this.address,
port: this.port,
username: this.username,
+ // @ts-ignore
publicKey: publicKey,
privateKey: privateKey
}
@@ -180,6 +182,7 @@ export class HybridSession extends SSHSession {
password: password,
}
SSH.startSessionByPasswd(args)
+ //@ts-ignore bug in .d.ts?
.then(async ({ session }) => {
this.t7.log("Got ssh session", session)
this.id = session
@@ -213,7 +216,6 @@ export class HybridSession extends SSHSession {
.split("\r\n")
.filter(line => line.length > 0)
.forEach(async line => {
- let c = {}
this.t7.log("line webexec accept: ", line)
if (line.startsWith("READY")) {
try {
@@ -228,6 +230,7 @@ export class HybridSession extends SSHSession {
}
this.candidate += line
// ignore echo
+ let c: unknown = {}
if (this.sentMessages.indexOf(this.candidate) != -1) {
this.t7.log("igonring message: "+this.candidate)
this.candidate = ""
@@ -240,7 +243,7 @@ export class HybridSession extends SSHSession {
this.candidate = ""
if (c == null)
return
- if (c.candidate)
+ if ((c as {candidate?}).candidate)
try {
await this.webrtcSession.pc.addIceCandidate(c)
} catch(e) {
@@ -249,7 +252,7 @@ export class HybridSession extends SSHSession {
}
else
try {
- await this.webrtcSession.pc.setRemoteDescription(c)
+ await this.webrtcSession.pc.setRemoteDescription(c as RTCSessionDescriptionInit)
} catch(e) { this.t7.log("got error setting remote desc:", e.message, c) }
})
}
@@ -273,27 +276,29 @@ export class HybridSession extends SSHSession {
this.webrtcSession.onIceCandidate = e => {
const candidate = JSON.stringify(e.candidate)
this.sentMessages.push(candidate)
+ //@ts-ignore bug in the .d.ts
SSH.writeToChannel({channel: channelId, message: candidate + "\n"})
}
this.webrtcSession.onNegotiationNeeded = () => {
- this.t7.log("on negotiation needed")
+ this.t7.log("on negotiation needed");
this.webrtcSession.pc.createOffer().then(d => {
- const offer = JSON.stringify(d)
+ const offer = JSON.stringify(d);
this.webrtcSession.pc.setLocalDescription(d)
this.sentMessages.push(offer)
+ //@ts-ignore a .d.ts bug
SSH.writeToChannel({channel: channelId, message: offer + "\n"})
})
}
this.webrtcSession.connect(marker)
})
}
- async openChannel(cmd: unknown, parent?: ChannelID, sx?: number, sy?: number) {
+ async openChannel(cmd: number | string | string[], parent?: number, sx?: number, sy?: number): Promise {
if (!this.webrtcSession) {
return super.openChannel(cmd, parent, sx, sy)
} else
// start webrtc data channel
- return this.webrtcSession.openChannel(cmd, parent, sx, sy)
+ return this.webrtcSession.openChannel(cmd, parent, sx, sy) as unknown as SSHChannel
}
async reconnect(marker?: number, publicKey?: string, privateKey?: string) {
@@ -307,7 +312,7 @@ export class HybridSession extends SSHSession {
return this.webrtcSession.close()
}
- getPayload(): Promise {
+ getPayload(): Promise {
if (this.webrtcSession)
return this.webrtcSession.getPayload()
else
@@ -319,7 +324,7 @@ export class HybridSession extends SSHSession {
else
return super.setPayload(payload)
}
- disconnect(): Promise {
+ disconnect(): Promise {
if (this.webrtcSession)
return this.webrtcSession.disconnect()
else
diff --git a/src/terminal7.js b/src/terminal7.ts
similarity index 80%
rename from src/terminal7.js
rename to src/terminal7.ts
index d09b1ac3..1cef7e38 100644
--- a/src/terminal7.js
+++ b/src/terminal7.ts
@@ -6,16 +6,17 @@
* Copyright: (c) 2020 Benny A. Daon - benny@tuzig.com
* License: GPLv3
*/
-import { Gate } from './gate.ts'
-import { T7Map } from './map.ts'
-import { CyclicArray } from './cyclic.js'
+import { Gate } from './gate'
+import { T7Map } from './map'
+import CyclicArray from './cyclic'
import * as TOML from '@tuzig/toml'
-import { formatDate } from './utils.js'
+import { formatDate } from './utils'
import { openDB } from 'idb'
import { marked } from 'marked'
+// @ts-ignore
import changelogURL from '../CHANGELOG.md?url'
-import ssh from 'ed25519-keygen/ssh';
-import { randomBytes } from 'ed25519-keygen/utils';
+import ssh from 'ed25519-keygen/ssh'
+import { randomBytes } from 'ed25519-keygen/utils'
import { Capacitor } from '@capacitor/core'
import { App } from '@capacitor/app'
@@ -28,26 +29,51 @@ import { RateApp } from 'capacitor-rate-app'
import { PeerbookConnection, PB } from './peerbook'
-import { Failure } from './session';
+import { Failure } from './session'
+import { Cell } from "./cell"
+import { Pane } from "./pane"
+import { PeerbookSession } from "./webrtc_session"
+declare type NavType = {
+ standalone?: boolean
+ getInstalledRelatedApps(): Promise<{
+ id?: string,
+ platform: "chrome_web_store" | "play" | "chromeos_play" | "webapp" | "windows" | "f-droid" | "amazon",
+ url?: string,
+ version?: string,
+ }[]>
+} & Navigator
+
+declare let window: {
+ navigator: NavType
+} & Window
+
+declare let navigator: NavType
+
+export const OPEN_HTML_SYMBOL = "📡"
+export const ERROR_HTML_SYMBOL = "🤕"
+export const CLOSED_HTML_SYMBOL = "🙁"
+export const LOCK_HTML_SYMBOL = "🔒"
const WELCOME=` 🖖 Greetings & Salutations 🖖
Thanks for choosing Terminal7. This is TWR, a local
-terminal used to control the terminal and log messages.`
-const WELCOME_NATIVE=WELCOME+`
-Type \`help\`, \`add\` or \`hide\` if you're ready to board.
-For WebRTC 🍯 please \`subscribe\` to our online service.
+terminal used to control the terminal and log messages.
+Most buttons launch a TWR command so you don't need to
+use \`help\`, just \`hide\`.
+If some characters looks off try CTRL-l.`
+const WELCOME_FOOTER=`
Enjoy!
-
+PS - Found a bug? Missing a feature? Please use \`support\`
`
+const WELCOME_NATIVE=WELCOME+`
+For WebRTC 🍯 please \`subscribe\` to our PeerBook service.
+` + WELCOME_FOOTER
const WELCOME_OTHER=WELCOME+`
-Type \`help\`, \`install\` or \`hide\` if you're ready to board.
-If you are one of our PeerBook subscribers, please \`login\`.
-
-Enjoy!
-
-`
+Type \`install\` for instruction on how to install the agent.
+If you are a PeerBook subscriber, please \`login\`.
+(Sorry, no way to subscribe from here yet)
+` + WELCOME_FOOTER
export const DEFAULT_DOTFILE = `# Terminal7's configurations file
[theme]
@@ -87,13 +113,75 @@ function compactCert(cert) {
const ret = cert.getFingerprints()[0].value.toUpperCase().replaceAll(":", "")
return ret
}
+
+declare global {
+ let terminal7: Terminal7
+ interface Window {
+ terminal7: Terminal7
+ }
+}
+
+export interface IceServers {
+ credential: string,
+ credentialType: "password" | string,
+ urls: string[],
+ username?: string
+}
+
export class Terminal7 {
+ gates: Gate[]
+ cells: Cell[]
+ timeouts: number[]
+ activeG?: Gate
+ scrollLingers4: number
+ shortestLongPress: number
+ borderHotSpotSize: number
+ certificates?: RTCCertificate[] = null
+ netConnected = true
+ logBuffer: CyclicArray
+ zoomedE?: HTMLDivElement
+ pendingPanes
+ pb?: PeerbookConnection = null
+ ignoreAppEvents = false
+ purchasesStarted = false
+ iceServers?: IceServers[]
+ recovering?: boolean
+ metaPressStart: number
+ map: T7Map
+ lastActiveState: boolean
+ e: HTMLDivElement
+ conf:{
+ theme
+ exec
+ net
+ ui
+ peerbook?
+ retries?: number
+ }
+ longPressGate: number
+ gesture?: {
+ where: "left" | "top",
+ pane: Pane
+ }
+ pointer0: number
+ firstPointer: {
+ pageX: number,
+ pageY: number
+ }
+ lastIdVerify: number
+ keys: {publicKey: string, privateKey: string}
+
DEFAULT_KEY_TAG = "dev.terminal7.keys.default"
/*
* Terminal7 constructor, all properties should be initiated here
*/
- constructor(settings) {
- settings = settings || {}
+ constructor(settings: {
+ scrollLingers4?: number,
+ shortestLongPress?: number,
+ borderHotSpotSize?: number,
+ logLines?: number,
+ iceServers?: IceServers[]
+ } = {}) {
this.gates = []
this.cells = []
this.timeouts = []
@@ -102,16 +190,11 @@ export class Terminal7 {
this.scrollLingers4 = settings.scrollLingers4 || 2000
this.shortestLongPress = settings.shortestLongPress || 1000
this.borderHotSpotSize = settings.borderHotSpotSize || 30
- this.certificates = null
- this.confEditor = null
- this.flashTimer = null
- this.netConnected = true
- this.logBuffer = CyclicArray(settings.logLines || 101)
+
+ this.logBuffer = new CyclicArray(settings.logLines || 101)
this.zoomedE = null
this.pendingPanes = {}
- this.pb = null
- this.ignoreAppEvents = false
- this.purchasesStarted = false
+
this.iceServers = settings.iceServers || null
}
showKeyHelp () {
@@ -122,15 +205,40 @@ export class Terminal7 {
document.getElementById('keys-help').classList.remove('hidden')
}
}
+ onAppStateChange(state) {
+ const active = state.isActive
+ if (this.lastActiveState == active) {
+ this.log("app state event on unchanged state ignored")
+ return
+ }
+ this.lastActiveState = active
+ this.log("app state changed", this.ignoreAppEvents)
+ if (!active) {
+ if (this.ignoreAppEvents) {
+ terminal7.log("ignoring benched app event")
+ return
+ }
+ this.updateNetworkStatus({connected: false}, false)
+ } else {
+ // We're back! puts us in recovery mode so that it'll
+ // quietly reconnect to the active gate on failure
+ if (this.ignoreAppEvents) {
+ this.ignoreAppEvents = false
+ return
+ }
+ this.clearTimeouts()
+ Network.getStatus().then(s => this.updateNetworkStatus(s))
+ }
+ }
/*
* Terminal7.open opens terminal on the given DOM element,
* loads the gates from local storage and redirects to home
*/
async open() {
- let e = document.getElementById('terminal7')
+ const e = document.getElementById('terminal7')
this.log("in open")
this.lastActiveState = true
- this.e = e
+ this.e = e as HTMLDivElement
await Preferences.migrate()
// reading conf
let d = {},
@@ -146,7 +254,7 @@ export class Terminal7 {
this.run(() =>
this.notify(
`Using default conf as parsing the dotfile failed:\n ${err}`,
- 10))
+ true), 10)
}
this.loadConf(d)
@@ -192,6 +300,11 @@ export class Terminal7 {
ev.stopPropagation()
ev.preventDefault()
})
+ document.getElementById('peerbook-legend').addEventListener(
+ 'click', async (ev) => {
+ setTimeout(() => this.map.shell.runCommand('subscribe', []), 50)
+ ev.stopPropagation()
+ })
// hide the modal on xmark click
// Handle network events for the indicator
Network.addListener('networkStatusChange', s =>
@@ -222,31 +335,8 @@ export class Terminal7 {
if (Capacitor.isNativePlatform()) {
// this is a hack as some operation, like bio verification
// fire two events
- App.addListener('appStateChange', state => {
- const active = state.isActive
- if (this.lastActiveState == active) {
- this.log("app state event on unchanged state ignored")
- return
- }
- this.lastActiveState = active
- console.log("app state changed", this.ignoreAppEvents)
- if (!active) {
- if (this.ignoreAppEvents) {
- terminal7.log("ignoring benched app event")
- return
- }
- this.updateNetworkStatus({connected: false}, false)
- } else {
- // We're back! puts us in recovery mode so that it'll
- // quietly reconnect to the active gate on failure
- if (this.ignoreAppEvents) {
- this.ignoreAppEvents = false
- return
- }
- this.clearTimeouts()
- Network.getStatus().then(s => this.updateNetworkStatus(s))
- }
- })
+ App.addListener('appStateChange', state => this.onAppStateChange(state))
+
}
e.addEventListener("click", e => {
@@ -282,12 +372,17 @@ export class Terminal7 {
}
})
)
+ const resizeObserver = new ResizeObserver(() => {
+ if (this.activeG)
+ this.activeG.setFitScreen()
+ })
+ resizeObserver.observe(document.body)
}
/*
* restoreState is a future feature that uses local storage to restore
* terminal7 to it's last state
*/
- restoreState() {
+ restoreState(): Promise {
return new Promise((resolve, reject) => {
if (!this.conf.ui.autoRestore) {
reject()
@@ -298,7 +393,7 @@ export class Terminal7 {
reject()
else {
const state = JSON.parse(value)
- let gate = this.gates[state.gateId]
+ const gate = this.gates[state.gateId]
if (!gate) {
console.log("Invalid restore state. Starting fresh", state)
this.notify("Invalid restore state. Starting fresh")
@@ -319,33 +414,57 @@ export class Terminal7 {
this.pb.close()
}
}
- async pbConnect() {
+ async pbConnect(): Promise {
+ const statusE = document.getElementById("peerbook-status") as HTMLSpanElement
return new Promise((resolve, reject) => {
+ function callResolve() {
+ if (terminal7.pb)
+ terminal7.pb.stopSpinner()
+ statusE.style.opacity = "1"
+ resolve()
+ }
+ function callReject(e, symbol = "") {
+ if (terminal7.pb)
+ terminal7.pb.stopSpinner()
+ statusE.style.opacity = "1"
+ statusE.innerHTML = symbol
+ reject(e)
+ }
const catchConnect = e => {
+ let symbol = "⛔︎"
if (e =="Unregistered")
- this.notify(`${PB} You are unregistered, please \`subscribe\``)
+ this.notify(Capacitor.isNativePlatform()?
+ `${PB} You need to register, please \`subscribe\``:
+ `${PB} You need to regisrer, please \`subscribe\` on your tablet`)
+
else if (e == Failure.NotSupported)
// TODO: this should be changed to a notification
// after we upgrade peerbook
console.log("PB not supported")
else if (e != "Unauthorized") {
terminal7.log("PB connect failed", e)
- this.notify(`${PB} Failed to connect, please try \`sub\``)
+ this.notify(Capacitor.isNativePlatform()?
+ `${PB} Failed to connect, please try \`subscribe\``:
+ `${PB} Failed to connect, please try \`login\``)
this.notify("If the problem persists, `support`")
- }
- reject(e)
+ symbol = ERROR_HTML_SYMBOL
+ } else
+ symbol = LOCK_HTML_SYMBOL
+
+ callReject(e, symbol)
}
// do nothing when no subscription or already connected
if (this.pb) {
+ this.pb.startSpinner()
if ((this.pb.uid != "TBD") && (this.pb.uid != "")) {
- this.pb.wsConnect().then(resolve).catch(reject)
+ this.pb.wsConnect().then(callResolve).catch(callReject)
return
}
if (this.pb.isOpen())
- resolve()
+ callResolve()
else
- this.pb.connect().then(resolve).catch(catchConnect)
+ this.pb.connect().then(callResolve).catch(catchConnect)
return
}
this.getFingerprint().then(fp => {
@@ -355,21 +474,22 @@ export class Terminal7 {
insecure: this.conf.peerbook && this.conf.peerbook.insecure,
shell: this.map.shell
})
+ this.pb.startSpinner()
this.pb.onUpdate = (m) => this.onPBMessage(m)
if (!this.purchasesStarted) {
this.pb.startPurchases().then(() =>
- this.pb.connect().then(resolve).catch(catchConnect)
- // this.pb.updateCustomerInfo().then(resolve).catch(reject)
- ).catch(reject).finally(() => this.purchasesStarted = true)
+ this.pb.connect().then(callResolve).catch(catchConnect)
+ // this.pb.updateCustomerInfo().then(callResolve).catch(callReject)
+ ).catch(callReject).finally(() => this.purchasesStarted = true)
} else
- this.pb.connect().then(resolve).catch(catchConnect)
+ this.pb.connect().then(callResolve).catch(catchConnect)
})
})
}
catchFingers() {
this.e.addEventListener("pointerdown", ev => this.onPointerDown(ev))
this.e.addEventListener("pointerup", ev => this.onPointerUp(ev))
- this.e.addEventListener("pointercancel", ev => this.onPointerCancel(ev))
+ this.e.addEventListener("pointercancel", () => this.onPointerCancel())
this.e.addEventListener("pointermove", ev => this.onPointerMove(ev))
}
/*
@@ -379,10 +499,10 @@ export class Terminal7 {
*/
// TOFO: add onMap to props
addGate(props, onMap = true) {
- let p = props || {}
+ const p = props || {}
// add the id
p.id = p.fp || p.name
- let g = new Gate(p)
+ const g = new Gate(p)
g.onlySSH = p.onlySSH
this.gates.push(g)
g.open(this.e)
@@ -393,10 +513,10 @@ export class Terminal7 {
return g
}
async storeGates() {
- let out = []
+ const out = []
this.gates.forEach(g => {
if (g.store) {
- let ws = []
+ const ws = []
g.windows.forEach((w) => ws.push(w.id))
out.push({id: g.id, addr: g.addr, user: g.user, secret: g.secret,
name:g.name, windows: ws, store: true, verified: g.verified,
@@ -419,7 +539,7 @@ export class Terminal7 {
await this.map.shell.escapeActiveForm()
}
async goHome() {
- Preferences.remove({key: "last_state"})
+ await Preferences.remove({key: "last_state"})
const s = document.getElementById('map-button')
s.classList.add('off')
if (this.activeG) {
@@ -461,7 +581,7 @@ export class Terminal7 {
this.map.showLog(true)
}
run(cb, delay) {
- var i = this.timeouts.length,
+ const i = this.timeouts.length,
r = window.setTimeout(ev => {
this.timeouts.splice(i, 1)
cb(ev)
@@ -476,10 +596,10 @@ export class Terminal7 {
/*
* disengage gets each active gate to disengae
*/
- disengage() {
+ disengage(): Promise {
return new Promise(resolve => {
this.pbClose()
- var count = 0
+ let count = 0
if (this.activeG && this.activeG.boarding)
this.notify("🌜 Benched", true)
if (this.gates.length > 0) {
@@ -492,7 +612,7 @@ export class Terminal7 {
}
})
}
- let callCB = () => terminal7.run(() => {
+ const callCB = () => terminal7.run(() => {
if (count == 0)
resolve()
else
@@ -502,7 +622,7 @@ export class Terminal7 {
})
}
async updateNetworkStatus (status, updateNetPopup = true) {
- let off = document.getElementById("offline").classList
+ const off = document.getElementById("offline").classList
if (this.netConnected == status.connected) {
if (updateNetPopup) {
if (this.netConnected)
@@ -527,7 +647,9 @@ export class Terminal7 {
if (this.pb.isOpen())
gate.notify("Timed out")
else
- this.notify(`${PB} timed out, please try \`subscribe\``)
+ this.notify(Capacitor.isNativePlatform()?
+ `${PB} timed out, please retry with \`subscribe\``:
+ `${PB} timed out, please retry with \`login\``)
gate.stopBoarding()
})
} else
@@ -598,7 +720,7 @@ export class Terminal7 {
}
// gets the will formatted fingerprint from the current certificate
- getFingerprint() {
+ getFingerprint(): Promise {
// gets the certificate from indexDB. If they are not there, create them
return new Promise((resolve, reject) => {
if (this.certificates) {
@@ -611,7 +733,7 @@ export class Terminal7 {
autoIncrement: true})
},
}).then(db => {
- let tx = db.transaction("certificates"),
+ const tx = db.transaction("certificates"),
store = tx.objectStore("certificates")
store.getAll().then(certificates => {
if (certificates.length == 0) {
@@ -643,6 +765,7 @@ export class Terminal7 {
this.log('generating the certificate')
RTCPeerConnection.generateCertificate({
name: "ECDSA",
+ // @ts-ignore
namedCurve: "P-256",
expires: 31536000000
}).then(cert => {
@@ -657,7 +780,7 @@ export class Terminal7 {
})
})
}
- storeCertificate() {
+ storeCertificate(): Promise {
return new Promise((resolve, reject) => {
openDB("t7", 1, {
upgrade(db) {
@@ -665,9 +788,9 @@ export class Terminal7 {
autoIncrement: true})
},
}).then(db => {
- let tx = db.transaction("certificates", "readwrite"),
+ const tx = db.transaction("certificates", "readwrite"),
store = tx.objectStore("certificates"),
- c = this.certificates[0]
+ c = this.certificates[0] as RTCCertificate & {id:number}
c.id = 1
store.add(c).then(() => {
db.close()
@@ -685,7 +808,7 @@ export class Terminal7 {
// var helpId = (this.activeG)? "help-gate":"help-home",
// var helpId = (this.activeG && this.activeG.activeW.activeP.copyMode)?
// "help-copymode":"help-gate",
- var helpId = "help-gate",
+ const helpId = "help-gate",
ecl = document.getElementById(helpId).classList,
bcl = document.getElementById("help-button").classList
@@ -697,12 +820,14 @@ export class Terminal7 {
}
// handle incomming peerbook messages (coming over sebsocket)
async onPBMessage(m) {
+ const statusE = document.getElementById("peerbook-status")
this.log("got pb message", m)
if (m["code"] !== undefined) {
if (m["code"] == 200) {
- this.notify("\uD83D\uDCD6 Logged in")
+ statusE.innerHTML = OPEN_HTML_SYMBOL
this.pb.uid = m["text"]
} else
+ // TODO: update statusE
this.notify(`\uD83D\uDCD6 ${m["text"]}`)
return
}
@@ -739,35 +864,36 @@ export class Terminal7 {
g.online = m.peer_update.online
g.verified = m.peer_update.verified
g.fp = m.source_fp
- g.updateNameE()
+ await g.updateNameE()
return
}
if (!g.session) {
console.log("session is close ignoring message", m)
return
}
+ const session = g.session as PeerbookSession
if (m.candidate !== undefined) {
- g.session.peerCandidate(m.candidate)
+ session.peerCandidate(m.candidate)
return
}
if (m.answer !== undefined ) {
- var answer = JSON.parse(atob(m.answer))
- g.session.peerAnswer(answer)
+ const answer = JSON.parse(atob(m.answer))
+ session.peerAnswer(answer)
return
}
}
log (...args) {
- var line = ""
+ let line = ""
args.forEach(a => line += JSON.stringify(a) + " ")
console.log(line)
this.logBuffer.push(line)
}
async dumpLog() {
- var data = ""
+ let data = ""
while (this.logBuffer.length > 0) {
data += this.logBuffer.shift() + "\n"
}
- Clipboard.write({string: data})
+ await Clipboard.write({string: data})
this.notify("Log copied to clipboard")
/* TODO: wwould be nice to store log to file, problme is
* Preferences pluging failes
@@ -785,7 +911,6 @@ export class Terminal7 {
onPointerCancel() {
this.pointer0 = null
this.firstPointer = null
- this.lastT = null
this.gesture = null
if (this.longPressGate) {
clearTimeout(this.longPressGate)
@@ -827,7 +952,7 @@ export class Terminal7 {
this.log(`identified: ${this.gesture}`)
}
onPointerMove(ev) {
- let x = ev.pageX,
+ const x = ev.pageX,
y = ev.pageY
/*
@@ -836,9 +961,9 @@ export class Terminal7 {
*/
if (this.gesture) {
- let where = this.gesture.where,
+ const where = this.gesture.where,
dest = Math.min(1.0, (where == "top")
- ? y / document.querySelector('.windows-container').offsetHeight
+ ? y / (document.querySelector('.windows-container') as HTMLDivElement).offsetHeight
: x / document.body.offsetWidth)
this.gesture.pane.layout.moveBorder(this.gesture.pane, where, dest)
ev.stopPropagation()
@@ -846,7 +971,7 @@ export class Terminal7 {
}
}
async onPointerUp(ev) {
- let e = ev.target,
+ const e = ev.target,
gatePad = e.closest(".gate-pad")
if (!this.pointer0)
@@ -857,7 +982,7 @@ export class Terminal7 {
if (!gate)
return
else {
- let deltaT = Date.now() - this.pointer0
+ const deltaT = Date.now() - this.pointer0
clearTimeout(this.longPressGate)
this.longPressGate = null
if (deltaT < this.conf.ui.quickest_press) {
@@ -872,9 +997,10 @@ export class Terminal7 {
ev.stopPropagation()
ev.preventDefault()
} else if (this.gesture) {
- this.activeG.sendState()
+ if (this.activeG && this.activeG.fitScreen)
+ this.activeG.sendState()
} else if (this.firstPointer) {
- let deltaT = Date.now() - this.pointer0,
+ const deltaT = Date.now() - this.pointer0,
x = ev.pageX,
y = ev.pageY,
dx = this.firstPointer.pageX - x,
@@ -887,7 +1013,7 @@ export class Terminal7 {
const minS = (dx > dy)?this.conf.ui.cutMinSpeedY:this.conf.ui.cutMinSpeedX
if (s > minS) {
// it's a cut!!
- let cell = ev.target.closest(".cell"),
+ const cell = ev.target.closest(".cell"),
pane = (cell != null)?cell.cell:undefined
if (pane && !pane.zoomed) {
if (r < 1.0)
@@ -910,7 +1036,7 @@ export class Terminal7 {
async showGreetings() {
const greeted = (await Preferences.get({key: 'greeted'})).value
if (!greeted) {
- Preferences.set({key: "greeted", value: "yep"})
+ await Preferences.set({key: "greeted", value: "yep"})
if (Capacitor.isNativePlatform())
this.map.tty(WELCOME_NATIVE)
else
@@ -922,7 +1048,7 @@ export class Terminal7 {
|| window.navigator.standalone
|| (Capacitor.getPlatform() != "web")
|| document.referrer.includes('android-app://')))
- if (navigator.getInstalledRelatedApps)
+ if (navigator.getInstalledRelatedApps)
navigator.getInstalledRelatedApps().then(relatedApps => {
if (relatedApps.length > 0)
this.map.tty("PWA installed, better use it\n")
@@ -957,7 +1083,7 @@ export class Terminal7 {
}
async deleteFingerprint() {
const db = await openDB("t7", 1)
- let tx = db.transaction("certificates", "readwrite"),
+ const tx = db.transaction("certificates", "readwrite"),
store = tx.objectStore("certificates")
await store.clear()
}
@@ -981,7 +1107,7 @@ export class Terminal7 {
e.innerHTML = marked.parse(changelog)
// add prefix to all ids to avoid conflicts
e.querySelectorAll("[id]").forEach(e => e.id = "changelog-" + e.id)
- document.querySelectorAll("a[href]").forEach(e => {
+ document.querySelectorAll("a[href]").forEach((e: HTMLAnchorElement) => {
e.addEventListener("click", ev => {
ev.stopPropagation()
ev.preventDefault()
@@ -990,7 +1116,7 @@ export class Terminal7 {
})
}
// if show is undefined the change log view state is toggled
- showChangelog(show) {
+ showChangelog(show = undefined) {
const e = document.getElementById("changelog")
if (show === undefined)
// if show is undefined toggle current state
@@ -1004,7 +1130,7 @@ export class Terminal7 {
/*
* collects the default id and returns a { publicKet, privateKey
*/
- async readId() {
+ async readId(): Promise<{publicKey: string, privateKey: string}> {
const now = Date.now()
if (this.keys && (now - this.lastIdVerify < this.conf.ui.verificationTTL))
return this.keys
@@ -1055,14 +1181,17 @@ export class Terminal7 {
}
saveDotfile(text) {
this.cells.forEach(c => {
- if (typeof(c.setTheme) == "function")
+ if (c instanceof Pane)
c.setTheme(this.conf.theme)
})
terminal7.loadConf(TOML.parse(text))
if (this.pb &&
- ((this.pb.host != this.conf.net.peerbook)
+ ((this.pb.host != this.conf.net.peerbook)
+ // TODO: is bug?
+ // @ts-ignore
|| (this.pb.peerName != this.conf.peerbook.peer_name)
|| (this.pb.insecure != this.conf.peerbook.insecure)
+ // @ts-ignore
|| (this.pb.email != this.conf.peerbook.email))) {
this.pbClose()
this.pb = null
@@ -1072,7 +1201,7 @@ export class Terminal7 {
}
async pbVerify() {
const fp = await this.getFingerprint()
- const schema = this.insecure?"http":"https"
+ const schema = this.pb.insecure?"http":"https"
let response
try {
response = await fetch(`${schema}://${this.conf.net.peerbook}/verify`, {
diff --git a/src/utils.js b/src/utils.ts
similarity index 66%
rename from src/utils.js
rename to src/utils.ts
index e8acd631..a03d0b34 100644
--- a/src/utils.js
+++ b/src/utils.ts
@@ -3,78 +3,77 @@
* copied from : https://stackoverflow.com/a/14638191/66595
* used as in: `x.innerHTML = formatDate(d, "dddd h:mmtt d MMM yyyy")`
*/
-export function formatDate(date, format, utc) {
- var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
- var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
- var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
-
- function ii(i, len) {
- var s = i + ""
- len = len || 2
+export function formatDate(date, format, utc = false) {
+ const MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
+ const MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ const dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
+ const ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
+
+ function ii(i, len = 2) {
+ let s = i + ""
while (s.length < len) s = "0" + s
return s
}
- var y = utc ? date.getUTCFullYear() : date.getFullYear()
+ const y = utc ? date.getUTCFullYear() : date.getFullYear()
format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y)
format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2))
format = format.replace(/(^|[^\\])y/g, "$1" + y)
- var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1
+ const M = (utc ? date.getUTCMonth() : date.getMonth()) + 1
format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0])
format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0])
format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M))
format = format.replace(/(^|[^\\])M/g, "$1" + M)
- var d = utc ? date.getUTCDate() : date.getDate()
+ const d = utc ? date.getUTCDate() : date.getDate()
format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0])
format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0])
format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d))
format = format.replace(/(^|[^\\])d/g, "$1" + d)
- var H = utc ? date.getUTCHours() : date.getHours()
+ const H = utc ? date.getUTCHours() : date.getHours()
format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H))
format = format.replace(/(^|[^\\])H/g, "$1" + H)
- var h = H > 12 ? H - 12 : H == 0 ? 12 : H
+ const h = H > 12 ? H - 12 : H == 0 ? 12 : H
format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h))
format = format.replace(/(^|[^\\])h/g, "$1" + h)
- var m = utc ? date.getUTCMinutes() : date.getMinutes()
+ const m = utc ? date.getUTCMinutes() : date.getMinutes()
format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m))
format = format.replace(/(^|[^\\])m/g, "$1" + m)
- var s = utc ? date.getUTCSeconds() : date.getSeconds()
+ const s = utc ? date.getUTCSeconds() : date.getSeconds()
format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s))
format = format.replace(/(^|[^\\])s/g, "$1" + s)
- var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds()
+ let f = utc ? date.getUTCMilliseconds() : date.getMilliseconds()
format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3))
f = Math.round(f / 10)
format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f))
f = Math.round(f / 10)
format = format.replace(/(^|[^\\])f/g, "$1" + f)
- var T = H < 12 ? "AM" : "PM"
+ const T = H < 12 ? "AM" : "PM"
format = format.replace(/(^|[^\\])TT+/g, "$1" + T)
format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0))
- var t = T.toLowerCase()
+ const t = T.toLowerCase()
format = format.replace(/(^|[^\\])tt+/g, "$1" + t)
format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0))
- var tz = -date.getTimezoneOffset()
- var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"
+ let tz = -date.getTimezoneOffset()
+ let K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"
if (!utc) {
tz = Math.abs(tz)
- var tzHrs = Math.floor(tz / 60)
- var tzMin = tz % 60
+ const tzHrs = Math.floor(tz / 60)
+ const tzMin = tz % 60
K += ii(tzHrs) + ":" + ii(tzMin)
}
format = format.replace(/(^|[^\\])K/g, "$1" + K)
- var day = (utc ? date.getUTCDay() : date.getDay()) + 1
+ const day = (utc ? date.getUTCDay() : date.getDay()) + 1
format = format.replace(new RegExp(dddd[0], "g"), dddd[day])
format = format.replace(new RegExp(ddd[0], "g"), ddd[day])
diff --git a/src/webrtc_session.ts b/src/webrtc_session.ts
index 11310514..50b3703a 100644
--- a/src/webrtc_session.ts
+++ b/src/webrtc_session.ts
@@ -1,5 +1,7 @@
-import { CapacitorHttp } from '@capacitor/core';
-import { BaseChannel, BaseSession, CallbackType, Channel, ChannelID, Failure } from './session';
+import { CapacitorHttp, HttpHeaders } from '@capacitor/core';
+import { BaseChannel, BaseSession, Channel, ChannelID, Failure } from './session';
+import { IceServers } from "./terminal7"
+import { ServerPayload } from "./gate"
type ChannelOpenedCB = (channel: Channel, id: ChannelID) => void
type RTCStats = {
@@ -12,17 +14,13 @@ type RTCStats = {
export class WebRTCChannel extends BaseChannel {
dataChannel: RTCDataChannel
session: WebRTCSession
- id: number
- createdOn: number
- onMessage : CallbackType
- constructor(session: BaseSession,
+ constructor(session: WebRTCSession,
id: number,
dc: RTCDataChannel) {
super()
this.id = id
this.session = session
this.dataChannel = dc
- this.createdOn = session.lastMarker
}
// readyState can be one of the four underlying states:
// "connecting", "open", "closing", closed"
@@ -72,7 +70,7 @@ export class WebRTCSession extends BaseSession {
channels: Map
pendingCDCMsgs: Array