@electerm/electerm-react 2.6.0 → 2.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -242,7 +242,7 @@ export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-fo
242
242
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
243
243
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
244
244
  export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
245
- export const rdpWikiLink = 'https://github.com/electerm/electerm/wiki/RDP-session-known-issues'
245
+ export const rdpWikiLink = 'https://github.com/electerm/electerm/wiki/RDP-limitation'
246
246
  export const vncWikiLink = 'https://github.com/electerm/electerm/wiki/VNC-session-known-issues'
247
247
  export const modals = {
248
248
  hide: 0,
@@ -22,12 +22,12 @@ export default function createTitle (res, hide = true) {
22
22
  return ''
23
23
  }
24
24
  const {
25
- host, port, username, title, type,
25
+ host, port, username, title, type, url,
26
26
  path, connectionHoppings, sshTunnels
27
27
  } = res
28
28
  const h = hide && window.store.config.hideIP ? maskHost(host) : host
29
29
  const fixTitle = `${username || ''}@${h}:${port}`
30
- const extra = host || path ? (path || fixTitle) : ''
30
+ const extra = host || path ? (path || fixTitle) : (url || '')
31
31
  let f = title
32
32
  ? `${title}` + (extra ? ` - ${extra}` : '')
33
33
  : extra
@@ -6,22 +6,22 @@ import ShowItem from '../components/common/show-item'
6
6
  import { chooseSaveDirectory } from './choose-save-folder'
7
7
  import { DownloadOutlined } from '@ant-design/icons'
8
8
 
9
- function downloadForBrowser (filename, text) {
10
- const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
11
- const url = URL.createObjectURL(blob)
12
- const a = document.createElement('a')
13
- a.href = url
14
- a.download = filename
15
- document.body.appendChild(a)
16
- a.click()
17
- document.body.removeChild(a)
18
- URL.revokeObjectURL(url)
19
- }
9
+ // function downloadForBrowser (filename, text) {
10
+ // const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
11
+ // const url = URL.createObjectURL(blob)
12
+ // const a = document.createElement('a')
13
+ // a.href = url
14
+ // a.download = filename
15
+ // document.body.appendChild(a)
16
+ // a.click()
17
+ // document.body.removeChild(a)
18
+ // URL.revokeObjectURL(url)
19
+ // }
20
20
 
21
21
  export default async function download (filename, text) {
22
- if (window.et.isWebApp) {
23
- return downloadForBrowser(filename, text)
24
- }
22
+ // if (window.et.isWebApp) {
23
+ // return downloadForBrowser(filename, text)
24
+ // }
25
25
  const savePath = await chooseSaveDirectory()
26
26
  if (!savePath) {
27
27
  return
@@ -0,0 +1,58 @@
1
+ import React from 'react'
2
+ import InputNumberConfirm from './input-number-confirm'
3
+ import {
4
+ Space
5
+ } from 'antd'
6
+ import {
7
+ MinusCircleOutlined,
8
+ PlusCircleOutlined
9
+ } from '@ant-design/icons'
10
+
11
+ export default function ZoomControl (props) {
12
+ const {
13
+ value,
14
+ onChange,
15
+ min = 0.25,
16
+ max = 5,
17
+ step = 0.25
18
+ } = props
19
+
20
+ const handleChange = (v) => {
21
+ onChange(v / 100)
22
+ }
23
+
24
+ const handleAdd = () => {
25
+ let next = value + step
26
+ if (next > max) next = max
27
+ onChange(next)
28
+ }
29
+
30
+ const handleMinus = () => {
31
+ let next = value - step
32
+ if (next < min) next = min
33
+ onChange(next)
34
+ }
35
+
36
+ return (
37
+ <InputNumberConfirm
38
+ value={Math.round(value * 100)}
39
+ onChange={handleChange}
40
+ step={1}
41
+ min={min * 100}
42
+ max={max * 100}
43
+ suffix='%'
44
+ addonBefore={
45
+ <Space.Compact>
46
+ <PlusCircleOutlined
47
+ onClick={handleAdd}
48
+ className='mg1r pointer font16'
49
+ />
50
+ <MinusCircleOutlined
51
+ onClick={handleMinus}
52
+ className='pointer font16 mg1r'
53
+ />
54
+ </Space.Compact>
55
+ }
56
+ />
57
+ )
58
+ }
@@ -8,10 +8,8 @@ import {
8
8
  } from '../../common/constants'
9
9
  import {
10
10
  Spin,
11
- // Button,
12
11
  Select
13
12
  } from 'antd'
14
- import { notification } from '../common/notification'
15
13
  import {
16
14
  ReloadOutlined,
17
15
  EditOutlined
@@ -22,6 +20,25 @@ import resolutions from './resolutions'
22
20
 
23
21
  const { Option } = Select
24
22
 
23
+ // IronRDP WASM module imports — loaded dynamically
24
+ async function loadWasmModule () {
25
+ if (window.ironRdp) return
26
+ console.debug('[RDP-CLIENT] Loading IronRDP WASM module...')
27
+ const mod = await import('ironrdp-wasm')
28
+ window.ironRdp = {
29
+ wasmInit: mod.default,
30
+ wasmSetup: mod.setup,
31
+ SessionBuilder: mod.SessionBuilder,
32
+ DesktopSize: mod.DesktopSize,
33
+ InputTransaction: mod.InputTransaction,
34
+ DeviceEvent: mod.DeviceEvent,
35
+ Extension: mod.Extension
36
+ }
37
+ await window.ironRdp.wasmInit()
38
+ window.ironRdp.wasmSetup('info')
39
+ console.debug('[RDP-CLIENT] IronRDP WASM module loaded and initialized')
40
+ }
41
+
25
42
  export default class RdpSession extends PureComponent {
26
43
  constructor (props) {
27
44
  const id = `rdp-reso-${props.tab.host}`
@@ -30,10 +47,9 @@ export default class RdpSession extends PureComponent {
30
47
  this.canvasRef = createRef()
31
48
  this.state = {
32
49
  loading: false,
33
- bitmapProps: {},
34
- aspectRatio: 4 / 3,
35
50
  ...resObj
36
51
  }
52
+ this.session = null
37
53
  }
38
54
 
39
55
  componentDidMount () {
@@ -41,14 +57,24 @@ export default class RdpSession extends PureComponent {
41
57
  }
42
58
 
43
59
  componentWillUnmount () {
44
- this.socket && this.socket.close()
45
- delete this.socket
60
+ this.cleanup()
46
61
  }
47
62
 
48
- runInitScript = () => {
49
-
63
+ cleanup = () => {
64
+ console.debug('[RDP-CLIENT] cleanup() called')
65
+ if (this.session) {
66
+ try {
67
+ this.session.shutdown()
68
+ console.debug('[RDP-CLIENT] session.shutdown() called')
69
+ } catch (e) {
70
+ console.debug('[RDP-CLIENT] session.shutdown() error:', e)
71
+ }
72
+ this.session = null
73
+ }
50
74
  }
51
75
 
76
+ runInitScript = () => {}
77
+
52
78
  setStatus = status => {
53
79
  const id = this.props.tab?.id
54
80
  this.props.editTab(id, {
@@ -56,7 +82,7 @@ export default class RdpSession extends PureComponent {
56
82
  })
57
83
  }
58
84
 
59
- remoteInit = async (term = this.term) => {
85
+ remoteInit = async () => {
60
86
  this.setState({
61
87
  loading: true
62
88
  })
@@ -80,237 +106,279 @@ export default class RdpSession extends PureComponent {
80
106
  termType: type,
81
107
  ...tab
82
108
  })
109
+
110
+ console.debug('[RDP-CLIENT] Creating RDP session term, host=', tab.host, 'port=', tab.port)
83
111
  const r = await createTerm(opts)
84
112
  .catch(err => {
85
113
  const text = err.message
86
114
  handleErr({ message: text })
87
115
  })
88
- this.setState({
89
- loading: false
90
- })
91
116
  if (!r) {
117
+ this.setState({ loading: false })
92
118
  this.setStatus(statusMap.error)
119
+ console.error('[RDP-CLIENT] createTerm failed')
93
120
  return
94
121
  }
95
- this.setStatus(statusMap.success)
122
+
96
123
  const {
97
124
  pid, port
98
125
  } = r
99
126
  this.pid = pid
127
+ console.debug('[RDP-CLIENT] Term created, pid=', pid, 'port=', port)
128
+
129
+ // Build the WebSocket proxy address for IronRDP WASM
100
130
  const hs = server
101
131
  ? server.replace(/https?:\/\//, '')
102
132
  : `${host}:${port}`
103
133
  const pre = server.startsWith('https') ? 'wss' : 'ws'
104
134
  const { width, height } = this.state
105
- const wsUrl = `${pre}://${hs}/rdp/${pid}?&token=${tokenElecterm}&width=${width}&height=${height}`
106
- const socket = new WebSocket(wsUrl)
107
- socket.onclose = this.oncloseSocket
108
- socket.onerror = this.onerrorSocket
109
- this.socket = socket
110
- socket.onopen = this.runInitScript
111
- socket.onmessage = this.onData
112
- }
113
-
114
- decompress = (bitmap) => {
115
- let fName = null
116
- switch (bitmap.bitsPerPixel) {
117
- case 15:
118
- fName = 'bitmap_decompress_15'
119
- break
120
- case 16:
121
- fName = 'bitmap_decompress_16'
122
- break
123
- case 24:
124
- fName = 'bitmap_decompress_24'
125
- break
126
- case 32:
127
- fName = 'bitmap_decompress_32'
128
- break
129
- default:
130
- throw new Error('invalid bitmap data format')
135
+ // IronRDP connects to the proxy address, which then proxies via RDCleanPath
136
+ // The WebSocket URL includes the pid and token for auth
137
+ const proxyAddress = `${pre}://${hs}/rdp/${pid}?token=${tokenElecterm}&width=${width}&height=${height}`
138
+ console.debug('[RDP-CLIENT] Proxy address:', proxyAddress)
139
+
140
+ // Load WASM module if not already loaded
141
+ try {
142
+ await loadWasmModule()
143
+ } catch (e) {
144
+ console.error('[RDP-CLIENT] Failed to load WASM module:', e)
145
+ this.setState({ loading: false })
146
+ this.setStatus(statusMap.error)
147
+ return
131
148
  }
132
- const rle = window.Module
133
- const input = new Uint8Array(bitmap.data.data)
134
- const inputPtr = rle._malloc(input.length)
135
- const inputHeap = new Uint8Array(rle.HEAPU8.buffer, inputPtr, input.length)
136
- inputHeap.set(input)
137
-
138
- const outputWidth = bitmap.destRight - bitmap.destLeft + 1
139
- const outputHeight = bitmap.destBottom - bitmap.destTop + 1
140
- const ouputSize = outputWidth * outputHeight * 4
141
- const outputPtr = rle._malloc(ouputSize)
142
-
143
- const outputHeap = new Uint8Array(rle.HEAPU8.buffer, outputPtr, ouputSize)
144
-
145
- rle.ccall(fName,
146
- 'number',
147
- ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
148
- [outputHeap.byteOffset, outputWidth, outputHeight, bitmap.width, bitmap.height, inputHeap.byteOffset, input.length]
149
- )
150
149
 
151
- const output = new Uint8ClampedArray(outputHeap.buffer, outputHeap.byteOffset, ouputSize)
150
+ this.setStatus(statusMap.success)
152
151
 
153
- rle._free(inputPtr)
154
- rle._free(outputPtr)
152
+ // Connect using IronRDP SessionBuilder
153
+ try {
154
+ const canvas = this.canvasRef.current
155
+ if (!canvas) {
156
+ console.error('[RDP-CLIENT] Canvas ref not available')
157
+ this.setState({ loading: false })
158
+ return
159
+ }
155
160
 
156
- return { width: outputWidth, height: outputHeight, arr: output }
157
- }
161
+ const rdpHost = tab.host
162
+ const rdpPort = tab.port || 3389
163
+ const destination = `${rdpHost}:${rdpPort}`
164
+ const username = tab.username || ''
165
+ const password = tab.password || ''
166
+
167
+ console.debug('[RDP-CLIENT] Building IronRDP session...')
168
+ console.debug('[RDP-CLIENT] destination:', destination)
169
+ console.debug('[RDP-CLIENT] username:', username)
170
+ console.debug('[RDP-CLIENT] proxyAddress:', proxyAddress)
171
+ console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
172
+
173
+ const desktopSize = new window.ironRdp.DesktopSize(width, height)
174
+ const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', false)
175
+
176
+ const builder = new window.ironRdp.SessionBuilder()
177
+ builder.username(username)
178
+ builder.password(password)
179
+ builder.destination(destination)
180
+ builder.proxyAddress(proxyAddress)
181
+ builder.authToken('none')
182
+ builder.desktopSize(desktopSize)
183
+ builder.renderCanvas(canvas)
184
+ builder.extension(enableCredsspExt)
185
+
186
+ // Cursor style callback
187
+ builder.setCursorStyleCallbackContext(canvas)
188
+ builder.setCursorStyleCallback(function (style) {
189
+ canvas.style.cursor = style || 'default'
190
+ })
158
191
 
159
- output = (data) => {
160
- if (!data.isCompress) {
161
- return {
162
- width: data.width,
163
- height: data.height,
164
- arr: new Uint8ClampedArray(data.data)
165
- }
166
- }
167
- return this.decompress(data)
168
- }
192
+ console.debug('[RDP-CLIENT] Calling builder.connect()...')
193
+ this.session = await builder.connect()
194
+
195
+ const ds = this.session.desktopSize()
196
+ console.debug('[RDP-CLIENT] Connected! Desktop:', ds.width, 'x', ds.height)
197
+
198
+ // Update canvas size to match actual desktop size
199
+ canvas.width = ds.width
200
+ canvas.height = ds.height
201
+
202
+ this.setState({
203
+ loading: false
204
+ })
169
205
 
170
- getButtonCode (button) {
171
- if (button === 0) {
172
- return 1
173
- } else if (button === 2) {
174
- return 2
175
- } else {
176
- return 0
206
+ canvas.focus()
207
+
208
+ // Run the session event loop (renders frames, handles protocol)
209
+ console.debug('[RDP-CLIENT] Starting session.run() event loop')
210
+ this.session.run().then((info) => {
211
+ console.debug('[RDP-CLIENT] Session ended:', info.reason())
212
+ this.onSessionEnd()
213
+ }).catch((e) => {
214
+ console.error('[RDP-CLIENT] Session error:', this.formatError(e))
215
+ this.onSessionEnd()
216
+ })
217
+ } catch (e) {
218
+ console.error('[RDP-CLIENT] Connection failed:', this.formatError(e))
219
+ this.setState({ loading: false })
220
+ this.setStatus(statusMap.error)
177
221
  }
178
222
  }
179
223
 
180
- getKeyCode (event) {
181
- const { type, button } = event
182
- if (type.startsWith('key')) {
183
- return scanCode(event)
184
- } else if (type === 'mousemove') {
185
- return 0
186
- } else {
187
- return this.getButtonCode(button)
224
+ formatError = (e) => {
225
+ if (e && typeof e === 'object' && '__wbg_ptr' in e) {
226
+ try {
227
+ const kindNames = {
228
+ 0: 'General',
229
+ 1: 'WrongPassword',
230
+ 2: 'LogonFailure',
231
+ 3: 'AccessDenied',
232
+ 4: 'RDCleanPath',
233
+ 5: 'ProxyConnect',
234
+ 6: 'NegotiationFailure'
235
+ }
236
+ const kind = e.kind ? e.kind() : 'Unknown'
237
+ const bt = e.backtrace ? e.backtrace() : ''
238
+ return `[${kindNames[kind] || kind}] ${bt}`
239
+ } catch (_) {}
188
240
  }
241
+ return e?.message || e?.toString() || String(e)
189
242
  }
190
243
 
191
- handleCanvasEvent = e => {
192
- const {
193
- type,
194
- clientX,
195
- clientY
196
- } = e
197
- const {
198
- left,
199
- top
200
- } = e.target.getBoundingClientRect()
201
- const x = clientX - left
202
- const y = clientY - top
203
- const keyCode = this.getKeyCode(e)
204
- const action = type.startsWith('key')
205
- ? 'sendKeyEventScancode'
206
- : type === 'mousewheel'
207
- ? 'sendWheelEvent'
208
- : 'sendPointerEvent'
209
- const pressed = type === 'mousedown' || type === 'keydown'
210
- let params = []
211
- if (type.startsWith('mouse') || type.startsWith('context')) {
212
- params = [x, y, keyCode, pressed]
213
- } else if (type === 'wheel') {
214
- const isHorizontal = false
215
- const delta = isHorizontal ? e.deltaX : e.deltaY
216
- const step = Math.round(Math.abs(delta) * 15 / 8)
217
- params = [x, y, step, delta > 0, isHorizontal]
218
- } else if (type === 'keydown' || type === 'keyup') {
219
- params = [keyCode, pressed]
220
- }
221
- this.socket.send(JSON.stringify({
222
- action,
223
- params
224
- }))
244
+ onSessionEnd = () => {
245
+ console.debug('[RDP-CLIENT] onSessionEnd called')
246
+ this.session = null
247
+ this.setStatus(statusMap.error)
225
248
  }
226
249
 
227
- onData = async (msg) => {
228
- let { data } = msg
229
- data = JSON.parse(data)
230
- if (data.action === 'session-rdp-connected') {
231
- return this.setState({
232
- loading: false
233
- })
234
- }
250
+ setupInputHandlers = () => {
235
251
  const canvas = this.canvasRef.current
236
- const ctx = canvas.getContext('2d')
237
- const {
238
- width,
239
- height,
240
- arr
241
- } = this.output(data)
242
- const imageData = ctx.createImageData(width, height)
243
- imageData.data.set(arr)
244
- ctx.putImageData(imageData, data.destLeft, data.destTop)
245
- }
252
+ if (!canvas || this._inputHandlersSetup) return
253
+ this._inputHandlersSetup = true
254
+ console.debug('[RDP-CLIENT] Setting up input handlers')
255
+
256
+ canvas.addEventListener('keydown', (e) => {
257
+ e.preventDefault()
258
+ e.stopPropagation()
259
+ if (!this.session) return
260
+ const scancode = this.getScancode(e.code)
261
+ if (scancode === null) return
262
+ try {
263
+ const event = window.ironRdp.DeviceEvent.keyPressed(scancode)
264
+ const tx = new window.ironRdp.InputTransaction()
265
+ tx.addEvent(event)
266
+ this.session.applyInputs(tx)
267
+ } catch (err) {
268
+ console.error('[RDP-CLIENT] Key press error:', err)
269
+ }
270
+ })
246
271
 
247
- onerrorSocket = err => {
248
- this.setStatus(statusMap.error)
249
- console.error('socket error', err)
272
+ canvas.addEventListener('keyup', (e) => {
273
+ e.preventDefault()
274
+ e.stopPropagation()
275
+ if (!this.session) return
276
+ const scancode = this.getScancode(e.code)
277
+ if (scancode === null) return
278
+ try {
279
+ const event = window.ironRdp.DeviceEvent.keyReleased(scancode)
280
+ const tx = new window.ironRdp.InputTransaction()
281
+ tx.addEvent(event)
282
+ this.session.applyInputs(tx)
283
+ } catch (err) {
284
+ console.error('[RDP-CLIENT] Key release error:', err)
285
+ }
286
+ })
287
+
288
+ canvas.addEventListener('mousemove', (e) => {
289
+ if (!this.session) return
290
+ try {
291
+ const rect = canvas.getBoundingClientRect()
292
+ const scaleX = canvas.width / rect.width
293
+ const scaleY = canvas.height / rect.height
294
+ const x = Math.round((e.clientX - rect.left) * scaleX)
295
+ const y = Math.round((e.clientY - rect.top) * scaleY)
296
+ const event = window.ironRdp.DeviceEvent.mouseMove(x, y)
297
+ const tx = new window.ironRdp.InputTransaction()
298
+ tx.addEvent(event)
299
+ this.session.applyInputs(tx)
300
+ } catch (err) {
301
+ // suppress frequent mouse errors
302
+ }
303
+ })
304
+
305
+ canvas.addEventListener('mousedown', (e) => {
306
+ e.preventDefault()
307
+ canvas.focus()
308
+ if (!this.session) return
309
+ try {
310
+ const event = window.ironRdp.DeviceEvent.mouseButtonPressed(e.button)
311
+ const tx = new window.ironRdp.InputTransaction()
312
+ tx.addEvent(event)
313
+ this.session.applyInputs(tx)
314
+ } catch (err) {
315
+ console.error('[RDP-CLIENT] Mouse down error:', err)
316
+ }
317
+ })
318
+
319
+ canvas.addEventListener('mouseup', (e) => {
320
+ e.preventDefault()
321
+ if (!this.session) return
322
+ try {
323
+ const event = window.ironRdp.DeviceEvent.mouseButtonReleased(e.button)
324
+ const tx = new window.ironRdp.InputTransaction()
325
+ tx.addEvent(event)
326
+ this.session.applyInputs(tx)
327
+ } catch (err) {
328
+ console.error('[RDP-CLIENT] Mouse up error:', err)
329
+ }
330
+ })
331
+
332
+ canvas.addEventListener('wheel', (e) => {
333
+ e.preventDefault()
334
+ if (!this.session) return
335
+ try {
336
+ if (e.deltaY !== 0) {
337
+ const amount = e.deltaY > 0 ? -1 : 1
338
+ const event = window.ironRdp.DeviceEvent.wheelRotations(true, amount, 1)
339
+ const tx = new window.ironRdp.InputTransaction()
340
+ tx.addEvent(event)
341
+ this.session.applyInputs(tx)
342
+ }
343
+ if (e.deltaX !== 0) {
344
+ const amount = e.deltaX > 0 ? -1 : 1
345
+ const event = window.ironRdp.DeviceEvent.wheelRotations(false, amount, 1)
346
+ const tx = new window.ironRdp.InputTransaction()
347
+ tx.addEvent(event)
348
+ this.session.applyInputs(tx)
349
+ }
350
+ } catch (err) {
351
+ console.error('[RDP-CLIENT] Wheel error:', err)
352
+ }
353
+ }, { passive: false })
354
+
355
+ canvas.addEventListener('contextmenu', (e) => e.preventDefault())
250
356
  }
251
357
 
252
- closeMsg = () => {
253
- notification.destroy(this.warningKey)
358
+ // Get PS/2 scancode from keyboard event code using existing code-scan module
359
+ getScancode = (code) => {
360
+ const sc = scanCode({ code })
361
+ return sc !== undefined ? sc : null
254
362
  }
255
363
 
256
- handleClickClose = () => {
257
- this.closeMsg()
258
- this.handleReInit()
364
+ handleEditResolutions = () => {
365
+ window.store.toggleResolutionEdit()
259
366
  }
260
367
 
261
368
  handleReInit = () => {
262
- this.socket.send(JSON.stringify({
263
- action: 'reload',
264
- params: [
265
- this.state.width,
266
- this.state.height
267
- ]
268
- }))
269
- this.setState({
270
- loading: true
271
- })
369
+ console.debug('[RDP-CLIENT] handleReInit called')
370
+ this.cleanup()
371
+ this.props.reloadTab(
372
+ this.props.tab
373
+ )
272
374
  }
273
375
 
274
376
  handleReload = () => {
275
- this.closeMsg()
276
377
  this.props.reloadTab(
277
378
  this.props.tab
278
379
  )
279
380
  }
280
381
 
281
- handleEditResolutions = () => {
282
- window.store.toggleResolutionEdit()
283
- }
284
-
285
- oncloseSocket = () => {
286
- // this.setStatus(
287
- // statusMap.error
288
- // )
289
- // this.warningKey = `open${Date.now()}`
290
- // notification.warning({
291
- // key: this.warningKey,
292
- // message: e('socketCloseTip'),
293
- // duration: 30,
294
- // description: (
295
- // <div className='pd2y'>
296
- // <Button
297
- // className='mg1r'
298
- // type='primary'
299
- // onClick={this.handleClickClose}
300
- // >
301
- // {m('close')}
302
- // </Button>
303
- // <Button
304
- // icon={<ReloadOutlined />}
305
- // onClick={this.handleReload}
306
- // >
307
- // {m('reload')}
308
- // </Button>
309
- // </div>
310
- // )
311
- // })
312
- }
313
-
314
382
  getAllRes = () => {
315
383
  return [
316
384
  ...this.props.resolutions,
@@ -389,6 +457,11 @@ export default class RdpSession extends PureComponent {
389
457
  )
390
458
  }
391
459
 
460
+ componentDidUpdate () {
461
+ // Set up native input handlers after canvas is rendered
462
+ this.setupInputHandlers()
463
+ }
464
+
392
465
  render () {
393
466
  const { width: w, height: h } = this.props
394
467
  const rdpProps = {
@@ -401,13 +474,6 @@ export default class RdpSession extends PureComponent {
401
474
  const canvasProps = {
402
475
  width,
403
476
  height,
404
- onMouseDown: this.handleCanvasEvent,
405
- onMouseUp: this.handleCanvasEvent,
406
- onMouseMove: this.handleCanvasEvent,
407
- onKeyDown: this.handleCanvasEvent,
408
- onKeyUp: this.handleCanvasEvent,
409
- onWheel: this.handleCanvasEvent,
410
- onContextMenu: this.handleCanvasEvent,
411
477
  tabIndex: 0
412
478
  }
413
479
  return (
@@ -167,7 +167,6 @@
167
167
 
168
168
  .sftp-has-pager
169
169
  .sftp-table-content
170
- position static
171
170
  padding-bottom 42px
172
171
  .pager-wrap
173
172
  position absolute
@@ -175,6 +174,7 @@
175
174
  left 0
176
175
  width 100%
177
176
  background var(--main)
177
+ height 40px
178
178
  border-top 1px solid var(--main-darker)
179
179
 
180
180
  .file-header-context-menu
@@ -3,6 +3,87 @@ import shortcutsDefaultsGen from './shortcuts-defaults.js'
3
3
  import {
4
4
  isMacJs
5
5
  } from '../../common/constants'
6
+ import keyControlPressed from '../../common/key-control-pressed.js'
7
+
8
+ function sendInputData (ctx, data) {
9
+ if (!data) return
10
+ if (ctx.attachAddon && ctx.attachAddon._sendData) {
11
+ ctx.attachAddon._sendData(data)
12
+ }
13
+ // if (!ctx.onData) return
14
+ // if (splitChars) {
15
+ // for (const ch of data) {
16
+ // ctx.onData(ch)
17
+ // }
18
+ // return
19
+ // }
20
+ // ctx.onData(data)
21
+ }
22
+
23
+ function getSelectionReplaceInfo (term) {
24
+ if (!term || !term.hasSelection()) return null
25
+ if (term.buffer?.active?.type === 'alternate') return null
26
+ const getPos = term.getSelectionPosition?.bind(term)
27
+ if (!getPos) return null
28
+ const pos = getPos()
29
+ if (!pos || !pos.start || !pos.end) return null
30
+ const buffer = term.buffer.active
31
+ const cursorY = buffer.cursorY
32
+ if (pos.start.y !== cursorY || pos.end.y !== cursorY) return null
33
+ const startX = Math.min(pos.start.x, pos.end.x)
34
+ const endX = Math.max(pos.start.x, pos.end.x)
35
+ if (startX === endX) return null
36
+ return {
37
+ startX,
38
+ endX,
39
+ cursorX: buffer.cursorX
40
+ }
41
+ }
42
+
43
+ export function handleTerminalSelectionReplace (event, ctx) {
44
+ if (
45
+ !ctx.term ||
46
+ !ctx.term.hasSelection() ||
47
+ keyControlPressed(event)
48
+ ) {
49
+ return false
50
+ }
51
+ const { key } = event
52
+ const isBackspace = key === 'Backspace'
53
+ const isDelete = key === 'Delete'
54
+ const isPrintable = key && key.length === 1
55
+ if (!isBackspace && !isDelete && !isPrintable) return false
56
+
57
+ if (event && event.preventDefault) {
58
+ event.preventDefault()
59
+ }
60
+ if (event && event.stopPropagation) {
61
+ event.stopPropagation()
62
+ }
63
+
64
+ const info = getSelectionReplaceInfo(ctx.term)
65
+ if (!info) return false
66
+
67
+ const { startX, endX, cursorX } = info
68
+ const move = startX - cursorX
69
+ if (move > 0) {
70
+ sendInputData(ctx, '\x1b[C'.repeat(move))
71
+ } else if (move < 0) {
72
+ sendInputData(ctx, '\x1b[D'.repeat(-move))
73
+ }
74
+
75
+ const delCount = endX - startX
76
+ if (delCount > 0) {
77
+ sendInputData(ctx, '\x1b[3~'.repeat(delCount))
78
+ }
79
+
80
+ if (isPrintable) {
81
+ sendInputData(ctx, key)
82
+ }
83
+
84
+ ctx.term.clearSelection()
85
+ return true
86
+ }
6
87
 
7
88
  function buildConfig (config, filter = d => d) {
8
89
  const defs = shortcutsDefaultsGen().filter(filter)
@@ -65,6 +146,9 @@ export function shortcutExtend (Cls) {
65
146
  if (isInAntdInput()) {
66
147
  return
67
148
  }
149
+ if (handleTerminalSelectionReplace(event, this)) {
150
+ return false
151
+ }
68
152
  if (
69
153
  this.term &&
70
154
  key === 'Backspace' &&
@@ -1,38 +1,11 @@
1
- import InputNumberConfirm from '../common/input-number-confirm'
2
- import {
3
- Space
4
- } from 'antd'
5
- import {
6
- MinusCircleOutlined,
7
- PlusCircleOutlined
8
- } from '@ant-design/icons'
1
+ import ZoomControl from '../common/zoom-control'
9
2
 
10
3
  export default function ZoomMenu (props) {
11
4
  const { store } = window
12
- const handleChange = (v) => {
13
- store.zoom(v / 100)
14
- }
15
-
16
5
  return (
17
- <InputNumberConfirm
18
- value={parseInt(props.config.zoom * 100, 10)}
19
- onChange={handleChange}
20
- step={1}
21
- min={25}
22
- max={500}
23
- suffix='%'
24
- addonBefore={
25
- <Space.Addon>
26
- <PlusCircleOutlined
27
- onClick={() => store.zoom(0.25, true)}
28
- className='mg1r pointer font16'
29
- />
30
- <MinusCircleOutlined
31
- onClick={() => store.zoom(-0.25, true)}
32
- className='pointer font16'
33
- />
34
- </Space.Addon>
35
- }
6
+ <ZoomControl
7
+ value={props.config.zoom}
8
+ onChange={(v) => store.zoom(v)}
36
9
  />
37
10
  )
38
11
  }
@@ -971,64 +971,70 @@ class Term extends Component {
971
971
  ?.openSuggestions(cursorPos, data)
972
972
  }
973
973
 
974
+ /**
975
+ * Read current input directly from terminal buffer
976
+ * This is more reliable than tracking character-by-character
977
+ */
974
978
  getCurrentInput = () => {
975
- return this.currentInput
979
+ if (!this.term) return ''
980
+
981
+ const buffer = this.term.buffer.active
982
+ const cursorY = buffer.cursorY
983
+ const cursorX = buffer.cursorX
984
+
985
+ // Get the current line from buffer (baseY + cursorY gives absolute position)
986
+ const absoluteY = buffer.baseY + cursorY
987
+ const line = buffer.getLine(absoluteY)
988
+ if (!line) return ''
989
+
990
+ // Get text from start of line up to cursor position
991
+ const lineText = line.translateToString(true, 0, cursorX)
992
+
993
+ // Try to extract command after prompt
994
+ // Common prompt endings with trailing space
995
+ const promptEndings = ['$ ', '# ', '> ', '% ', '] ', ') ']
996
+
997
+ let commandStart = 0
998
+ for (const ending of promptEndings) {
999
+ const idx = lineText.lastIndexOf(ending)
1000
+ if (idx !== -1 && idx + ending.length > commandStart) {
1001
+ commandStart = idx + ending.length
1002
+ }
1003
+ }
1004
+
1005
+ return lineText.slice(commandStart)
976
1006
  }
977
1007
 
978
1008
  setCurrentInput = (value) => {
979
1009
  this.currentInput = value
980
1010
  }
981
1011
 
982
- updateCurrentInput = (d) => {
983
- // Handle backspace (both \x7f and \b)
984
- if (d === '\x7f' || d === '\b') {
985
- this.currentInput = this.currentInput.slice(0, -1)
986
- return
987
- }
988
- // Handle Ctrl+U (clear line)
989
- if (d === '\x15') {
990
- this.currentInput = ''
991
- return
992
- }
993
- // Handle Ctrl+W (delete word)
994
- if (d === '\x17') {
995
- this.currentInput = this.currentInput.replace(/\S*\s*$/, '')
996
- return
997
- }
998
- // Handle Ctrl+C (cancel)
999
- if (d === '\x03') {
1000
- this.currentInput = ''
1001
- return
1002
- }
1003
- // Handle Enter
1012
+ /**
1013
+ * Handle special input events for command history tracking
1014
+ * The actual input reading is done via getCurrentInput from buffer
1015
+ */
1016
+ handleInputEvent = (d) => {
1017
+ // Handle Enter - add command to history
1004
1018
  if (d === '\r' || d === '\n') {
1005
- // Add to manual command history if shell integration is not available
1006
- if (this.currentInput.trim() && this.shouldUseManualHistory()) {
1007
- this.manualCommandHistory.add(this.currentInput.trim())
1008
- // Also add to global history for suggestions
1009
- window.store.addCmdHistory(this.currentInput.trim())
1019
+ const currentCmd = this.getCurrentInput()
1020
+ if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
1021
+ this.manualCommandHistory.add(currentCmd.trim())
1022
+ window.store.addCmdHistory(currentCmd.trim())
1010
1023
  }
1011
- this.currentInput = ''
1012
- return
1013
- }
1014
- // Handle Escape and other control characters
1015
- if (d.charCodeAt(0) < 32 && d !== '\t') {
1016
- return
1017
- }
1018
- // Handle arrow keys and other escape sequences
1019
- if (d.startsWith('\x1b')) {
1020
- return
1024
+ this.closeSuggestions()
1021
1025
  }
1022
- // Regular character input - append to buffer
1023
- this.currentInput += d
1024
1026
  }
1025
1027
 
1026
1028
  onData = (d) => {
1027
- this.updateCurrentInput(d)
1028
- const data = this.getCurrentInput()
1029
- if (this.props.config.showCmdSuggestions && data) {
1030
- const cursorPos = this.getCursorPosition()
1031
- this.openSuggestions(cursorPos, data)
1029
+ this.handleInputEvent(d)
1030
+ if (this.props.config.showCmdSuggestions) {
1031
+ const data = this.getCurrentInput()
1032
+ if (data && d !== '\r' && d !== '\n') {
1033
+ const cursorPos = this.getCursorPosition()
1034
+ this.openSuggestions(cursorPos, data)
1035
+ } else {
1036
+ this.closeSuggestions()
1037
+ }
1032
1038
  } else {
1033
1039
  this.closeSuggestions()
1034
1040
  }
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  Input,
3
- Tooltip
3
+ Tooltip,
4
+ Dropdown,
5
+ Space
4
6
  } from 'antd'
5
7
  import { copy } from '../../common/clipboard'
6
8
  import {
7
9
  ReloadOutlined,
8
- GlobalOutlined
10
+ GlobalOutlined,
11
+ EllipsisOutlined
9
12
  } from '@ant-design/icons'
13
+ import ZoomControl from '../common/zoom-control'
10
14
 
11
15
  export default function AddressBar (props) {
12
16
  const {
@@ -14,7 +18,9 @@ export default function AddressBar (props) {
14
18
  onReload,
15
19
  onOpen,
16
20
  title,
17
- description
21
+ description,
22
+ zoom,
23
+ onZoom
18
24
  } = props
19
25
  const content = (
20
26
  <>
@@ -25,6 +31,19 @@ export default function AddressBar (props) {
25
31
  function handleClick () {
26
32
  copy(url)
27
33
  }
34
+ const items = [
35
+ {
36
+ key: 'zoom',
37
+ label: (
38
+ <div onClick={e => e.stopPropagation()}>
39
+ <ZoomControl
40
+ value={zoom}
41
+ onChange={onZoom}
42
+ />
43
+ </div>
44
+ )
45
+ }
46
+ ]
28
47
  return (
29
48
  <div className='web-address-bar pd1'>
30
49
  <Tooltip
@@ -39,9 +58,19 @@ export default function AddressBar (props) {
39
58
  />
40
59
  }
41
60
  suffix={
42
- <GlobalOutlined
43
- onClick={onOpen}
44
- />
61
+ <Space>
62
+ <GlobalOutlined
63
+ className='pointer'
64
+ onClick={onOpen}
65
+ title={window.translate('openInDefaultBrowser')}
66
+ />
67
+ <Dropdown
68
+ menu={{ items }}
69
+ trigger={['click']}
70
+ >
71
+ <EllipsisOutlined className='pointer' />
72
+ </Dropdown>
73
+ </Space>
45
74
  }
46
75
  />
47
76
  </Tooltip>
@@ -0,0 +1,69 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { Input, Button } from 'antd'
3
+ import Modal from '../common/modal'
4
+
5
+ export default function WebAuthModal ({ authRequest, onAuthSubmit, onAuthCancel }) {
6
+ const [username, setUsername] = useState('')
7
+ const [password, setPassword] = useState('')
8
+
9
+ const handleSubmit = useCallback(() => {
10
+ onAuthSubmit(username, password)
11
+ setUsername('')
12
+ setPassword('')
13
+ }, [onAuthSubmit, username, password])
14
+
15
+ const handleCancel = useCallback(() => {
16
+ onAuthCancel()
17
+ setUsername('')
18
+ setPassword('')
19
+ }, [onAuthCancel])
20
+
21
+ return (
22
+ <Modal
23
+ open={!!authRequest}
24
+ title='Authentication Required'
25
+ width={400}
26
+ onCancel={handleCancel}
27
+ footer={null}
28
+ >
29
+ <div className='pd1y'>
30
+ <p>
31
+ <b>{authRequest?.host}</b> requires authentication
32
+ {authRequest?.realm ? ` (${authRequest.realm})` : ''}
33
+ </p>
34
+ <div className='pd1b'>
35
+ <div className='pd1b'>Username</div>
36
+ <Input
37
+ value={username}
38
+ onChange={e => setUsername(e.target.value)}
39
+ placeholder='Username'
40
+ autoFocus
41
+ />
42
+ </div>
43
+ <div className='pd1b'>
44
+ <div className='pd1b'>Password</div>
45
+ <Input.Password
46
+ value={password}
47
+ onChange={e => setPassword(e.target.value)}
48
+ placeholder='Password'
49
+ onPressEnter={handleSubmit}
50
+ />
51
+ </div>
52
+ <div className='pd1t alignright'>
53
+ <Button
54
+ className='mg1r'
55
+ onClick={handleCancel}
56
+ >
57
+ Cancel
58
+ </Button>
59
+ <Button
60
+ type='primary'
61
+ onClick={handleSubmit}
62
+ >
63
+ Login
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ </Modal>
68
+ )
69
+ }
@@ -1,5 +1,6 @@
1
1
  import AddressBar from './address-bar'
2
- // import React, { useEffect } from 'react'
2
+ import WebAuthModal from './web-auth-modal'
3
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
3
4
 
4
5
  export default function WebSession (props) {
5
6
  const {
@@ -8,6 +9,9 @@ export default function WebSession (props) {
8
9
  height,
9
10
  reloadTab
10
11
  } = props
12
+ const [zoom, setZoom] = useState(1.0)
13
+ const [authRequest, setAuthRequest] = useState(null)
14
+ const webviewRef = useRef(null)
11
15
  const urlRegex = /^[a-z\d.+-]+:\/\/[^\s/$.?#].[^\s]*$/i
12
16
 
13
17
  const { url = '' } = tab
@@ -15,6 +19,8 @@ export default function WebSession (props) {
15
19
  url,
16
20
  title: tab.title,
17
21
  description: tab.description,
22
+ zoom,
23
+ onZoom: handleZoom,
18
24
  onOpen: () => {
19
25
  window.openLink(tab.url)
20
26
  },
@@ -25,6 +31,50 @@ export default function WebSession (props) {
25
31
  }
26
32
  }
27
33
 
34
+ function handleZoom (v) {
35
+ setZoom(v)
36
+ const el = webviewRef.current
37
+ if (!el) {
38
+ return
39
+ }
40
+ if (el.setZoomFactor) {
41
+ el.setZoomFactor(v)
42
+ } else {
43
+ el.style.zoom = v
44
+ }
45
+ }
46
+
47
+ // Handle HTTP Basic Auth requests from webview
48
+ useEffect(() => {
49
+ if (!window.api || !window.api.onWebviewAuthRequest) {
50
+ return
51
+ }
52
+ const removeListener = window.api.onWebviewAuthRequest((data) => {
53
+ setAuthRequest(data)
54
+ })
55
+ return removeListener
56
+ }, [])
57
+
58
+ const handleAuthSubmit = useCallback((username, password) => {
59
+ if (!authRequest) return
60
+ window.api.sendWebviewAuthResponse({
61
+ id: authRequest.id,
62
+ username,
63
+ password
64
+ })
65
+ setAuthRequest(null)
66
+ }, [authRequest])
67
+
68
+ const handleAuthCancel = useCallback(() => {
69
+ if (!authRequest) return
70
+ window.api.sendWebviewAuthResponse({
71
+ id: authRequest.id,
72
+ username: '',
73
+ password: ''
74
+ })
75
+ setAuthRequest(null)
76
+ }, [authRequest])
77
+
28
78
  // TODO: 支持自定义Header和Cookie
29
79
  // useEffect(() => {
30
80
  // const webview = document.querySelector('webview')
@@ -64,14 +114,15 @@ export default function WebSession (props) {
64
114
  }
65
115
  }
66
116
  return (
67
- <iframe {...iframeProps} />
117
+ <iframe {...iframeProps} ref={webviewRef} />
68
118
  )
69
119
  }
70
120
  const viewProps = {
71
121
  src: url,
72
122
  style: {
73
123
  width: (width - 10) + 'px',
74
- height: (height + hOffset) + 'px'
124
+ height: (height + hOffset) + 'px',
125
+ background: '#fff'
75
126
  },
76
127
  disableblinkfeatures: 'true',
77
128
  disablewebsecurity: 'true',
@@ -79,7 +130,7 @@ export default function WebSession (props) {
79
130
  useragent: tab.useragent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
80
131
  }
81
132
  return (
82
- <webview {...viewProps} />
133
+ <webview {...viewProps} ref={webviewRef} />
83
134
  )
84
135
  }
85
136
 
@@ -91,6 +142,11 @@ export default function WebSession (props) {
91
142
  <div className='pd1'>
92
143
  {renderView()}
93
144
  </div>
145
+ <WebAuthModal
146
+ authRequest={authRequest}
147
+ onAuthSubmit={handleAuthSubmit}
148
+ onAuthCancel={handleAuthCancel}
149
+ />
94
150
  </div>
95
151
  )
96
152
  }
@@ -47,7 +47,6 @@ html
47
47
  img.iblock.logo-filter(src='images/electerm.png', alt='', height=80)
48
48
  script.
49
49
  window.et = !{JSON.stringify(_global)}
50
- - var url1 = '/src/client/entry/rle.js'
51
50
  - var url = '/src/client/entry/basic.js'
52
51
  - if (isDev)
53
52
  //- script(src='/external/react.development.js?' + version)
@@ -65,7 +64,6 @@ html
65
64
  //- script(src='/external/react.production.min.js?' + version)
66
65
  //- script(src='/external/react-dom.production.min.js?' + version)
67
66
  - var url = src='/js/basic-' + version + '.js'
68
- - var url1 = src='/js/rle-' + version + '.js'
69
67
  script(src=url1, type='module')
70
68
  script(src=url, type='module')
71
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.6.0",
3
+ "version": "2.7.8",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,2 +0,0 @@
1
- // This file and rle.wasm is generated by [**Emscripten**](https://github.com/kripken/emscripten) from [rle.c](https://raw.githubusercontent.com/citronneur/mstsc.js/master/obj/rle.c)
2
- var Module=typeof Module!="undefined"?Module:{};window.Module=Module;var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}process.on("uncaughtException",ex=>{if(ex!=="unwind"&&!(ex instanceof ExitStatus)&&!(ex.context instanceof ExitStatus)){throw ex}});quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){var f="rle.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(binaryFile)){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{if(!response["ok"]){throw`failed to load wasm binary file at '${binaryFile}'`}return response["arrayBuffer"]()}).catch(()=>getBinarySync(binaryFile))}else if(readAsync){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),reject)})}}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{"a":wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["b"];updateMemoryViews();addOnInit(wasmExports["c"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);return false}}if(!wasmBinaryFile)wasmBinaryFile=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult);return{}}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.charCodeAt(i);if(u>=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;var UTF8ArrayToString=(heapOrArray,idx,maxBytesToRead)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={"string":str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},"array":arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var wasmImports={a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["c"])();var _bitmap_decompress_15=Module["_bitmap_decompress_15"]=(a0,a1,a2,a3,a4,a5,a6)=>(_bitmap_decompress_15=Module["_bitmap_decompress_15"]=wasmExports["d"])(a0,a1,a2,a3,a4,a5,a6);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["e"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["f"])(a0);var _bitmap_decompress_16=Module["_bitmap_decompress_16"]=(a0,a1,a2,a3,a4,a5,a6)=>(_bitmap_decompress_16=Module["_bitmap_decompress_16"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5,a6);var _bitmap_decompress_24=Module["_bitmap_decompress_24"]=(a0,a1,a2,a3,a4,a5,a6)=>(_bitmap_decompress_24=Module["_bitmap_decompress_24"]=wasmExports["h"])(a0,a1,a2,a3,a4,a5,a6);var _bitmap_decompress_32=Module["_bitmap_decompress_32"]=(a0,a1,a2,a3,a4,a5,a6)=>(_bitmap_decompress_32=Module["_bitmap_decompress_32"]=wasmExports["i"])(a0,a1,a2,a3,a4,a5,a6);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["k"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["l"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["m"])();Module["ccall"]=ccall;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();
Binary file