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 overlay = document.getElementById('overlay'); |
||||
export const terminal = document.getElementById('terminal'); |
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 { |
Copy text selection to clipboard on double click or select |
||||
if (window.clipboardData?.setData) { |
@param event - the event this function is bound to eg mouseup |
||||
window.clipboardData.setData('Text', text); |
@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; |
return true; |
||||
} |
} |
||||
if ( |
if ( |
@ -1,11 +1,11 @@ |
|||||
import { isNull, isUndefined } from '../../web_modules/lodash.js'; |
import _ from 'lodash'; |
||||
import { verifyPrompt } from '../shared/verify.js'; |
import { verifyPrompt } from '../shared/verify.js'; |
||||
import { overlay } from '../shared/elements.js'; |
import { overlay } from '../shared/elements.js'; |
||||
|
|
||||
export function disconnect(reason: string): void { |
export function disconnect(reason: string): void { |
||||
if (isNull(overlay)) return; |
if (_.isNull(overlay)) return; |
||||
overlay.style.display = 'block'; |
overlay.style.display = 'block'; |
||||
const msg = document.getElementById('msg'); |
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); |
window.removeEventListener('beforeunload', verifyPrompt, false); |
||||
} |
} |
@ -1,8 +1,8 @@ |
|||||
import { isNull } from '../../web_modules/lodash.js'; |
import _ from 'lodash'; |
||||
|
|
||||
export function mobileKeyboard(): void { |
export function mobileKeyboard(): void { |
||||
const [screen] = document.getElementsByClassName('xterm-screen'); |
const [screen] = document.getElementsByClassName('xterm-screen'); |
||||
if (isNull(screen)) return; |
if (_.isNull(screen)) return; |
||||
screen.setAttribute('contenteditable', 'true'); |
screen.setAttribute('contenteditable', 'true'); |
||||
screen.setAttribute('spellcheck', 'false'); |
screen.setAttribute('spellcheck', 'false'); |
||||
screen.setAttribute('autocorrect', '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/[^/]+$'); |
const userRegex = new RegExp('ssh/[^/]+$'); |
||||
export const trim = (str: string): string => str.replace(/\/*$/, ''); |
export const trim = (str: string): string => str.replace(/\/*$/, ''); |
@ -0,0 +1 @@ |
|||||
|
export const isDev = process.env.NODE_ENV === 'development'; |
Loading…
Reference in new issue