29 changed files with 271 additions and 250 deletions
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -1,12 +0,0 @@ |
|||
import { isUndefined } from '../../web_modules/lodash.js'; |
|||
|
|||
export function loadOptions(): object { |
|||
const defaultOptions = { fontSize: 14 }; |
|||
try { |
|||
return isUndefined(localStorage.options) |
|||
? defaultOptions |
|||
: JSON.parse(localStorage.options); |
|||
} catch { |
|||
return defaultOptions; |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
caches.keys().then(cacheNames => { |
|||
cacheNames.forEach(cacheName => { |
|||
caches.delete(cacheName); |
|||
}); |
|||
}); |
@ -1,161 +0,0 @@ |
|||
import { Terminal } from '../web_modules/xterm.js'; |
|||
import { isNull } from '../web_modules/lodash.js'; |
|||
import { FitAddon } from '../web_modules/xterm-addon-fit.js'; |
|||
import { |
|||
dom, |
|||
library, |
|||
} from '../web_modules/@fortawesome/fontawesome-svg-core.js'; |
|||
import { faCogs } from '../web_modules/@fortawesome/free-solid-svg-icons.js'; |
|||
import Toastify from '../web_modules/toastify-js.js'; |
|||
import fileType from '../web_modules/file-type.js'; |
|||
|
|||
import { FileDownloader } from './client/download.js'; |
|||
import { copySelected, copyShortcut } from './client/copyToClipboard.js'; |
|||
import { disconnect } from './client/disconnect.js'; |
|||
import { loadOptions } from './client/options.js'; |
|||
import { mobileKeyboard } from './client/mobile.js'; |
|||
import { overlay, terminal } from './shared/elements.js'; |
|||
import { socket } from './client/socket.js'; |
|||
import { verifyPrompt } from './shared/verify.js'; |
|||
|
|||
// Setup for fontawesome
|
|||
library.add(faCogs); |
|||
dom.watch(); |
|||
|
|||
socket.on('connect', () => { |
|||
const term = new Terminal(); |
|||
if (isNull(terminal)) return; |
|||
const fitAddon = new FitAddon(); |
|||
term.loadAddon(fitAddon); |
|||
term.open(terminal); |
|||
const resize = (): void => { |
|||
fitAddon.fit(); |
|||
socket.emit('resize', { cols: term.cols, rows: term.rows }); |
|||
}; |
|||
|
|||
const options = loadOptions(); |
|||
Object.entries(options).forEach(([key, value]) => { |
|||
term.setOption(key, value); |
|||
}); |
|||
const code = JSON.stringify(options, null, 2); |
|||
const editor = document.querySelector('#options .editor'); |
|||
if (!isNull(editor)) { |
|||
editor.value = code; |
|||
editor.addEventListener('keyup', () => { |
|||
try { |
|||
const updated = JSON.parse(editor.value); |
|||
const updatedCode = JSON.stringify(updated, null, 2); |
|||
editor.value = updatedCode; |
|||
editor.classList.remove('error'); |
|||
localStorage.options = updatedCode; |
|||
Object.keys(updated).forEach(key => { |
|||
const value = updated[key]; |
|||
term.setOption(key, value); |
|||
}); |
|||
resize(); |
|||
} catch { |
|||
// skip
|
|||
editor.classList.add('error'); |
|||
} |
|||
}); |
|||
const toggle = document.querySelector('#options .toggler'); |
|||
const optionsElem = document.getElementById('options'); |
|||
if (!isNull(toggle) && !isNull(optionsElem)) { |
|||
toggle.addEventListener('click', e => { |
|||
optionsElem.classList.toggle('opened'); |
|||
e.preventDefault(); |
|||
}); |
|||
} |
|||
} |
|||
if (!isNull(overlay)) overlay.style.display = 'none'; |
|||
window.addEventListener('beforeunload', verifyPrompt, false); |
|||
|
|||
term.attachCustomKeyEventHandler(copyShortcut); |
|||
|
|||
document.addEventListener( |
|||
'mouseup', |
|||
() => { |
|||
if (term.hasSelection()) copySelected(term.getSelection()); |
|||
}, |
|||
false, |
|||
); |
|||
|
|||
window.onresize = resize; |
|||
resize(); |
|||
term.focus(); |
|||
mobileKeyboard(); |
|||
|
|||
const fileDownloader = new FileDownloader((bufferCharacters: string) => { |
|||
let fileCharacters = bufferCharacters; |
|||
// Try to decode it as base64, if it fails we assume it's not base64
|
|||
try { |
|||
fileCharacters = window.atob(fileCharacters); |
|||
} catch (err) { |
|||
// Assuming it's not base64...
|
|||
} |
|||
|
|||
const bytes = new Uint8Array(fileCharacters.length); |
|||
for (let i = 0; i < fileCharacters.length; i += 1) { |
|||
bytes[i] = fileCharacters.charCodeAt(i); |
|||
} |
|||
|
|||
let mimeType = 'application/octet-stream'; |
|||
let fileExt = ''; |
|||
const typeData = fileType(bytes); |
|||
if (typeData) { |
|||
mimeType = typeData.mime; |
|||
fileExt = typeData.ext; |
|||
} |
|||
// Check if the buffer is ASCII
|
|||
// Ref: https://stackoverflow.com/a/14313213
|
|||
// eslint-disable-next-line no-control-regex
|
|||
else if (/^[\x00-\xFF]*$/.test(fileCharacters)) { |
|||
mimeType = 'text/plain'; |
|||
fileExt = 'txt'; |
|||
} |
|||
const fileName = `file-${new Date() |
|||
.toISOString() |
|||
.split('.')[0] |
|||
.replace(/-/g, '') |
|||
.replace('T', '') |
|||
.replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
|
|||
|
|||
const blob = new Blob([new Uint8Array(bytes.buffer)], { |
|||
type: mimeType, |
|||
}); |
|||
const blobUrl = URL.createObjectURL(blob); |
|||
|
|||
Toastify({ |
|||
text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`, |
|||
duration: 10000, |
|||
newWindow: true, |
|||
gravity: 'bottom', |
|||
position: 'right', |
|||
backgroundColor: '#fff', |
|||
stopOnFocus: true, |
|||
}).showToast(); |
|||
}); |
|||
|
|||
term.onData((data: string) => { |
|||
socket.emit('input', data); |
|||
}); |
|||
term.onResize((size: string) => { |
|||
socket.emit('resize', size); |
|||
}); |
|||
socket |
|||
.on('data', (data: string) => { |
|||
const remainingData = fileDownloader.buffer(data); |
|||
if (remainingData) { |
|||
term.write(remainingData); |
|||
} |
|||
}) |
|||
.on('login', () => { |
|||
term.writeln(''); |
|||
resize(); |
|||
}) |
|||
.on('logout', disconnect) |
|||
.on('disconnect', disconnect) |
|||
.on('error', (err: string | null) => { |
|||
if (err) disconnect(err); |
|||
}); |
|||
}); |
@ -1,2 +1,5 @@ |
|||
export const overlay = document.getElementById('overlay'); |
|||
export const terminal = document.getElementById('terminal'); |
|||
export const editor = document.querySelector( |
|||
'#options .editor', |
|||
) as HTMLInputElement; |
|||
|
@ -0,0 +1,74 @@ |
|||
import _ from 'lodash'; |
|||
import { Terminal } from 'xterm'; |
|||
import { FitAddon } from 'xterm-addon-fit'; |
|||
import { dom, library } from '@fortawesome/fontawesome-svg-core'; |
|||
import { faCogs } from '@fortawesome/free-solid-svg-icons'; |
|||
|
|||
import { FileDownloader } from './wetty/download.js'; |
|||
import { copySelected, copyShortcut } from './wetty/clipboard.js'; |
|||
import { disconnect } from './wetty/disconnect.js'; |
|||
import { configureTerm } from './wetty/options.js'; |
|||
import { mobileKeyboard } from './wetty/mobile.js'; |
|||
import { overlay, terminal } from './shared/elements.js'; |
|||
import { socket } from './wetty/socket.js'; |
|||
import { verifyPrompt } from './shared/verify.js'; |
|||
|
|||
// Setup for fontawesome
|
|||
library.add(faCogs); |
|||
dom.watch(); |
|||
|
|||
socket.on('connect', () => { |
|||
const term = new Terminal(); |
|||
if (_.isNull(terminal)) return; |
|||
const fitAddon = new FitAddon(); |
|||
term.loadAddon(fitAddon); |
|||
term.open(terminal); |
|||
const resize = (): void => { |
|||
fitAddon.fit(); |
|||
socket.emit('resize', { cols: term.cols, rows: term.rows }); |
|||
}; |
|||
|
|||
configureTerm(term, resize); |
|||
|
|||
if (!_.isNull(overlay)) overlay.style.display = 'none'; |
|||
window.addEventListener('beforeunload', verifyPrompt, false); |
|||
|
|||
term.attachCustomKeyEventHandler(copyShortcut); |
|||
|
|||
document.addEventListener( |
|||
'mouseup', |
|||
event => { |
|||
if (term.hasSelection()) copySelected(event, term.getSelection()); |
|||
}, |
|||
false, |
|||
); |
|||
|
|||
window.onresize = resize; |
|||
resize(); |
|||
term.focus(); |
|||
mobileKeyboard(); |
|||
const fileDownloader = new FileDownloader(); |
|||
|
|||
term.onData((data: string) => { |
|||
socket.emit('input', data); |
|||
}); |
|||
term.onResize((size: { cols: number; rows: number }) => { |
|||
socket.emit('resize', size); |
|||
}); |
|||
socket |
|||
.on('data', (data: string) => { |
|||
const remainingData = fileDownloader.buffer(data); |
|||
if (remainingData) { |
|||
term.write(remainingData); |
|||
} |
|||
}) |
|||
.on('login', () => { |
|||
term.writeln(''); |
|||
resize(); |
|||
}) |
|||
.on('logout', disconnect) |
|||
.on('disconnect', disconnect) |
|||
.on('error', (err: string | null) => { |
|||
if (err) disconnect(err); |
|||
}); |
|||
}); |
@ -1,7 +1,12 @@ |
|||
// NOTE text selection on double click or select
|
|||
export function copySelected(text: string): boolean { |
|||
if (window.clipboardData?.setData) { |
|||
window.clipboardData.setData('Text', text); |
|||
/** |
|||
Copy text selection to clipboard on double click or select |
|||
@param event - the event this function is bound to eg mouseup |
|||
@param text - the selected text to copy |
|||
@returns boolean to indicate success or failure |
|||
*/ |
|||
export function copySelected(event: Event, text: string): boolean { |
|||
if (event.clipboardData?.setData) { |
|||
event.clipboardData.setData('Text', text); |
|||
return true; |
|||
} |
|||
if ( |
@ -1,11 +1,11 @@ |
|||
import { isNull, isUndefined } from '../../web_modules/lodash.js'; |
|||
import _ from 'lodash'; |
|||
import { verifyPrompt } from '../shared/verify.js'; |
|||
import { overlay } from '../shared/elements.js'; |
|||
|
|||
export function disconnect(reason: string): void { |
|||
if (isNull(overlay)) return; |
|||
if (_.isNull(overlay)) return; |
|||
overlay.style.display = 'block'; |
|||
const msg = document.getElementById('msg'); |
|||
if (!isUndefined(reason) && !isNull(msg)) msg.innerHTML = reason; |
|||
if (!_.isUndefined(reason) && !_.isNull(msg)) msg.innerHTML = reason; |
|||
window.removeEventListener('beforeunload', verifyPrompt, false); |
|||
} |
@ -1,8 +1,8 @@ |
|||
import { isNull } from '../../web_modules/lodash.js'; |
|||
import _ from 'lodash'; |
|||
|
|||
export function mobileKeyboard(): void { |
|||
const [screen] = document.getElementsByClassName('xterm-screen'); |
|||
if (isNull(screen)) return; |
|||
if (_.isNull(screen)) return; |
|||
screen.setAttribute('contenteditable', 'true'); |
|||
screen.setAttribute('spellcheck', 'false'); |
|||
screen.setAttribute('autocorrect', 'false'); |
@ -0,0 +1,51 @@ |
|||
import _ from 'lodash'; |
|||
import type { Terminal } from 'xterm'; |
|||
|
|||
import { editor } from '../shared/elements.js'; |
|||
|
|||
function loadOptions(): object { |
|||
const defaultOptions = { fontSize: 14 }; |
|||
try { |
|||
return _.isUndefined(localStorage.options) |
|||
? defaultOptions |
|||
: JSON.parse(localStorage.options); |
|||
} catch { |
|||
return defaultOptions; |
|||
} |
|||
} |
|||
|
|||
export function configureTerm(term: Terminal, resize: Function): void { |
|||
const options = loadOptions(); |
|||
Object.entries(options).forEach(([key, value]) => { |
|||
term.setOption(key, value); |
|||
}); |
|||
const config = JSON.stringify(options, null, 2); |
|||
if (!_.isNull(editor)) { |
|||
editor.value = config; |
|||
editor.addEventListener('keyup', () => { |
|||
try { |
|||
const updated = JSON.parse(editor.value); |
|||
const updatedConf = JSON.stringify(updated, null, 2); |
|||
editor.value = updatedConf; |
|||
editor.classList.remove('error'); |
|||
localStorage.options = updatedConf; |
|||
Object.keys(updated).forEach(key => { |
|||
const value = updated[key]; |
|||
term.setOption(key, value); |
|||
}); |
|||
resize(); |
|||
} catch { |
|||
// skip
|
|||
editor.classList.add('error'); |
|||
} |
|||
}); |
|||
const toggle = document.querySelector('#options .toggler'); |
|||
const optionsElem = document.getElementById('options'); |
|||
if (!_.isNull(toggle) && !_.isNull(optionsElem)) { |
|||
toggle.addEventListener('click', e => { |
|||
optionsElem.classList.toggle('opened'); |
|||
e.preventDefault(); |
|||
}); |
|||
} |
|||
} |
|||
} |
@ -1,4 +1,4 @@ |
|||
import io from '../../web_modules/socket.io-client.js'; |
|||
import io from 'socket.io-client'; |
|||
|
|||
const userRegex = new RegExp('ssh/[^/]+$'); |
|||
export const trim = (str: string): string => str.replace(/\/*$/, ''); |
@ -0,0 +1 @@ |
|||
export const isDev = process.env.NODE_ENV === 'development'; |
Loading…
Reference in new issue