mirror of
https://github.com/tsl0922/ttyd.git
synced 2026-01-06 02:44:21 +01:00
html: update lint rules
This commit is contained in:
@@ -1,46 +1,51 @@
|
||||
import { Component, h } from 'preact';
|
||||
|
||||
import { ITerminalOptions, ITheme } from 'xterm';
|
||||
import Xterm from './terminal';
|
||||
import { Xterm } from './terminal';
|
||||
|
||||
if ((module as any).hot) {
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
require('preact/debug');
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
require('preact/debug');
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws';
|
||||
const url = [protocol, window.location.host, window.location.pathname, wsPath, window.location.search].join('');
|
||||
const url = [
|
||||
protocol,
|
||||
window.location.host,
|
||||
window.location.pathname,
|
||||
wsPath,
|
||||
window.location.search,
|
||||
].join('');
|
||||
const termOptions = {
|
||||
fontSize: 13,
|
||||
fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
|
||||
theme: {
|
||||
foreground: '#d2d2d2',
|
||||
background: '#2b2b2b',
|
||||
cursor: '#adadad',
|
||||
black: '#000000',
|
||||
red: '#d81e00',
|
||||
green: '#5ea702',
|
||||
yellow: '#cfae00',
|
||||
blue: '#427ab3',
|
||||
magenta: '#89658e',
|
||||
cyan: '#00a7aa',
|
||||
white: '#dbded8',
|
||||
brightBlack: '#686a66',
|
||||
brightRed: '#f54235',
|
||||
brightGreen: '#99e343',
|
||||
brightYellow: '#fdeb61',
|
||||
brightBlue: '#84b0d8',
|
||||
brightMagenta: '#bc94b7',
|
||||
brightCyan: '#37e6e8',
|
||||
brightWhite: '#f1f1f0'
|
||||
} as ITheme
|
||||
fontSize: 13,
|
||||
fontFamily:
|
||||
'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
|
||||
theme: {
|
||||
foreground: '#d2d2d2',
|
||||
background: '#2b2b2b',
|
||||
cursor: '#adadad',
|
||||
black: '#000000',
|
||||
red: '#d81e00',
|
||||
green: '#5ea702',
|
||||
yellow: '#cfae00',
|
||||
blue: '#427ab3',
|
||||
magenta: '#89658e',
|
||||
cyan: '#00a7aa',
|
||||
white: '#dbded8',
|
||||
brightBlack: '#686a66',
|
||||
brightRed: '#f54235',
|
||||
brightGreen: '#99e343',
|
||||
brightYellow: '#fdeb61',
|
||||
brightBlue: '#84b0d8',
|
||||
brightMagenta: '#bc94b7',
|
||||
brightCyan: '#37e6e8',
|
||||
brightWhite: '#f1f1f0',
|
||||
} as ITheme,
|
||||
} as ITerminalOptions;
|
||||
|
||||
export default class App extends Component {
|
||||
public render() {
|
||||
return (
|
||||
<Xterm id='terminal-container' url={url} options={termOptions} />
|
||||
);
|
||||
}
|
||||
export class App extends Component {
|
||||
render() {
|
||||
return <Xterm id="terminal-container" url={url} options={termOptions} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,198 +7,201 @@ import { OverlayAddon } from './overlay';
|
||||
|
||||
import 'xterm/dist/xterm.css';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: Terminal;
|
||||
tty_auth_token?: string;
|
||||
export interface WindowExtended extends Window {
|
||||
term: Terminal;
|
||||
tty_auth_token?: string;
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
declare let window: WindowExtended;
|
||||
|
||||
const enum Command {
|
||||
// server side
|
||||
OUTPUT = '0',
|
||||
SET_WINDOW_TITLE = '1',
|
||||
SET_PREFERENCES = '2',
|
||||
SET_RECONNECT = '3',
|
||||
// server side
|
||||
OUTPUT = '0',
|
||||
SET_WINDOW_TITLE = '1',
|
||||
SET_PREFERENCES = '2',
|
||||
SET_RECONNECT = '3',
|
||||
|
||||
// client side
|
||||
INPUT = '0',
|
||||
RESIZE_TERMINAL = '1'
|
||||
// client side
|
||||
INPUT = '0',
|
||||
RESIZE_TERMINAL = '1',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
url: string;
|
||||
options: ITerminalOptions;
|
||||
id: string;
|
||||
url: string;
|
||||
options: ITerminalOptions;
|
||||
}
|
||||
|
||||
export default class Xterm extends Component<Props> {
|
||||
private textEncoder: TextEncoder;
|
||||
private textDecoder: TextDecoder;
|
||||
private container: HTMLElement;
|
||||
private terminal: Terminal;
|
||||
private fitAddon: FitAddon;
|
||||
private overlayAddon: OverlayAddon;
|
||||
private socket: WebSocket;
|
||||
private title: string;
|
||||
private autoReconnect: number;
|
||||
private resizeTimeout: number;
|
||||
export class Xterm extends Component<Props> {
|
||||
private textEncoder: TextEncoder;
|
||||
private textDecoder: TextDecoder;
|
||||
private container: HTMLElement;
|
||||
private terminal: Terminal;
|
||||
private fitAddon: FitAddon;
|
||||
private overlayAddon: OverlayAddon;
|
||||
private socket: WebSocket;
|
||||
private title: string;
|
||||
private autoReconnect: number;
|
||||
private resizeTimeout: number;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.textEncoder = new TextEncoder();
|
||||
this.textDecoder = new TextDecoder();
|
||||
this.fitAddon = new FitAddon();
|
||||
this.overlayAddon = new OverlayAddon();
|
||||
this.textEncoder = new TextEncoder();
|
||||
this.textDecoder = new TextDecoder();
|
||||
this.fitAddon = new FitAddon();
|
||||
this.overlayAddon = new OverlayAddon();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.openTerminal();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.socket.close();
|
||||
this.terminal.dispose();
|
||||
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.removeEventListener('beforeunload', this.onWindowUnload);
|
||||
}
|
||||
|
||||
render({ id }: Props) {
|
||||
return <div id={id} ref={c => (this.container = c)} />;
|
||||
}
|
||||
|
||||
@bind
|
||||
private onWindowResize() {
|
||||
const { fitAddon } = this;
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
|
||||
}
|
||||
|
||||
private onWindowUnload(event: BeforeUnloadEvent): string {
|
||||
const message = 'Close terminal? this will also terminate the command.';
|
||||
event.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
|
||||
@bind
|
||||
private openTerminal() {
|
||||
if (this.terminal) {
|
||||
this.terminal.dispose();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.openTerminal();
|
||||
this.socket = new WebSocket(this.props.url, ['tty']);
|
||||
this.terminal = new Terminal(this.props.options);
|
||||
const { socket, terminal, container, fitAddon, overlayAddon } = this;
|
||||
window.term = terminal;
|
||||
|
||||
socket.binaryType = 'arraybuffer';
|
||||
socket.onopen = this.onSocketOpen;
|
||||
socket.onmessage = this.onSocketData;
|
||||
socket.onclose = this.onSocketClose;
|
||||
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(overlayAddon);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
|
||||
terminal.onTitleChange(data => {
|
||||
if (data && data !== '') {
|
||||
document.title = data + ' | ' + this.title;
|
||||
}
|
||||
});
|
||||
terminal.onData(this.onTerminalData);
|
||||
terminal.onResize(this.onTerminalResize);
|
||||
if (
|
||||
document.queryCommandSupported &&
|
||||
document.queryCommandSupported('copy')
|
||||
) {
|
||||
terminal.onSelectionChange(() => {
|
||||
overlayAddon.showOverlay('\u2702', 200);
|
||||
document.execCommand('copy');
|
||||
});
|
||||
}
|
||||
terminal.open(container);
|
||||
terminal.focus();
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.socket.close();
|
||||
this.terminal.dispose();
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('beforeunload', this.onWindowUnload);
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.removeEventListener('beforeunload', this.onWindowUnload);
|
||||
@bind
|
||||
private onSocketOpen() {
|
||||
console.log('[ttyd] Websocket connection opened');
|
||||
const { socket, textEncoder, fitAddon } = this;
|
||||
const authToken = window.tty_auth_token;
|
||||
|
||||
socket.send(textEncoder.encode(JSON.stringify({ AuthToken: authToken })));
|
||||
fitAddon.fit();
|
||||
}
|
||||
|
||||
@bind
|
||||
private onSocketClose(event: CloseEvent) {
|
||||
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
|
||||
|
||||
const { overlayAddon, openTerminal, autoReconnect } = this;
|
||||
overlayAddon.showOverlay('Connection Closed', null);
|
||||
window.removeEventListener('beforeunload', this.onWindowUnload);
|
||||
|
||||
// 1008: POLICY_VIOLATION - Auth failure
|
||||
if (event.code === 1008) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
public render({ id }: Props) {
|
||||
return (
|
||||
<div id={id} ref={(c) => this.container = c} />
|
||||
);
|
||||
// 1000: CLOSE_NORMAL
|
||||
if (event.code !== 1000 && autoReconnect > 0) {
|
||||
setTimeout(openTerminal, autoReconnect * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private onWindowResize() {
|
||||
const { fitAddon } = this;
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
|
||||
}
|
||||
@bind
|
||||
private onSocketData(event: MessageEvent) {
|
||||
const { terminal, textDecoder } = this;
|
||||
|
||||
private onWindowUnload(event: BeforeUnloadEvent): string {
|
||||
const message = 'Close terminal? this will also terminate the command.';
|
||||
event.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
const rawData = new Uint8Array(event.data);
|
||||
const cmd = String.fromCharCode(rawData[0]);
|
||||
const data = rawData.slice(1);
|
||||
|
||||
@bind
|
||||
private openTerminal() {
|
||||
if (this.terminal) {
|
||||
this.terminal.dispose();
|
||||
}
|
||||
|
||||
this.socket = new WebSocket(this.props.url, ['tty']);
|
||||
this.terminal = new Terminal(this.props.options);
|
||||
const { socket, terminal, container, fitAddon, overlayAddon } = this;
|
||||
window.term = terminal;
|
||||
|
||||
socket.binaryType = 'arraybuffer';
|
||||
socket.onopen = this.onSocketOpen;
|
||||
socket.onmessage = this.onSocketData;
|
||||
socket.onclose = this.onSocketClose;
|
||||
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(overlayAddon);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
|
||||
terminal.onTitleChange((data) => {
|
||||
if (data && data !== '') {
|
||||
document.title = (data + ' | ' + this.title);
|
||||
}
|
||||
switch (cmd) {
|
||||
case Command.OUTPUT:
|
||||
terminal.writeUtf8(data);
|
||||
break;
|
||||
case Command.SET_WINDOW_TITLE:
|
||||
this.title = textDecoder.decode(data.buffer);
|
||||
document.title = this.title;
|
||||
break;
|
||||
case Command.SET_PREFERENCES:
|
||||
const preferences = JSON.parse(textDecoder.decode(data.buffer));
|
||||
Object.keys(preferences).forEach(key => {
|
||||
console.log(`[ttyd] setting ${key}: ${preferences[key]}`);
|
||||
terminal.setOption(key, preferences[key]);
|
||||
});
|
||||
terminal.onData(this.onTerminalData);
|
||||
terminal.onResize(this.onTerminalResize);
|
||||
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
|
||||
terminal.onSelectionChange(() => {
|
||||
overlayAddon.showOverlay('\u2702', 200);
|
||||
document.execCommand('copy');
|
||||
});
|
||||
}
|
||||
terminal.open(container);
|
||||
terminal.focus();
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('beforeunload', this.onWindowUnload);
|
||||
break;
|
||||
case Command.SET_RECONNECT:
|
||||
this.autoReconnect = Number(textDecoder.decode(data.buffer));
|
||||
console.log(`[ttyd] enabling reconnect: ${this.autoReconnect} seconds`);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ttyd] unknown command: ${cmd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private onSocketOpen() {
|
||||
console.log('[ttyd] Websocket connection opened');
|
||||
const { socket, textEncoder, fitAddon } = this;
|
||||
const authToken = window.tty_auth_token;
|
||||
|
||||
socket.send(textEncoder.encode(JSON.stringify({AuthToken: authToken})));
|
||||
fitAddon.fit();
|
||||
@bind
|
||||
private onTerminalResize(size: { cols: number; rows: number }) {
|
||||
const { overlayAddon, socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({ columns: size.cols, rows: size.rows });
|
||||
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
|
||||
}
|
||||
setTimeout(() => {
|
||||
overlayAddon.showOverlay(`${size.cols}x${size.rows}`);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@bind
|
||||
private onSocketClose(event: CloseEvent) {
|
||||
console.log(`[ttyd] websocket connection closed with code: ${event.code}`);
|
||||
|
||||
const { overlayAddon, openTerminal, autoReconnect } = this;
|
||||
overlayAddon.showOverlay('Connection Closed', null);
|
||||
window.removeEventListener('beforeunload', this.onWindowUnload);
|
||||
|
||||
// 1008: POLICY_VIOLATION - Auth failure
|
||||
if (event.code === 1008) {
|
||||
window.location.reload();
|
||||
}
|
||||
// 1000: CLOSE_NORMAL
|
||||
if (event.code !== 1000 && autoReconnect > 0) {
|
||||
setTimeout(openTerminal, autoReconnect * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private onSocketData(event: MessageEvent) {
|
||||
const { terminal, textDecoder } = this;
|
||||
|
||||
const rawData = new Uint8Array(event.data);
|
||||
const cmd = String.fromCharCode(rawData[0]);
|
||||
const data = rawData.slice(1);
|
||||
|
||||
switch(cmd) {
|
||||
case Command.OUTPUT:
|
||||
terminal.writeUtf8(data);
|
||||
break;
|
||||
case Command.SET_WINDOW_TITLE:
|
||||
this.title = textDecoder.decode(data.buffer);
|
||||
document.title = this.title;
|
||||
break;
|
||||
case Command.SET_PREFERENCES:
|
||||
const preferences = JSON.parse(textDecoder.decode(data.buffer));
|
||||
Object.keys(preferences).forEach((key) => {
|
||||
console.log(`[ttyd] setting ${key}: ${preferences[key]}`);
|
||||
terminal.setOption(key, preferences[key]);
|
||||
});
|
||||
break;
|
||||
case Command.SET_RECONNECT:
|
||||
this.autoReconnect = parseInt(textDecoder.decode(data.buffer));
|
||||
console.log(`[ttyd] enabling reconnect: ${this.autoReconnect} seconds`);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ttyd] unknown command: ${cmd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private onTerminalResize(size: {cols: number, rows: number}) {
|
||||
const { overlayAddon, socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({columns: size.cols, rows: size.rows});
|
||||
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
|
||||
}
|
||||
setTimeout(() => {overlayAddon.showOverlay(`${size.cols}x${size.rows}`)}, 500);
|
||||
}
|
||||
|
||||
@bind
|
||||
private onTerminalData(data: string) {
|
||||
const { socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(textEncoder.encode(Command.INPUT + data));
|
||||
}
|
||||
@bind
|
||||
private onTerminalData(data: string) {
|
||||
const { socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(textEncoder.encode(Command.INPUT + data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
|
||||
export class OverlayAddon implements ITerminalAddon {
|
||||
private terminal: Terminal | undefined;
|
||||
private overlayNode: HTMLElement | null;
|
||||
private overlayTimeout: number | null;
|
||||
private terminal: Terminal | undefined;
|
||||
private overlayNode: HTMLElement | null;
|
||||
private overlayTimeout: number | null;
|
||||
|
||||
constructor() {
|
||||
this.overlayNode = document.createElement('div');
|
||||
this.overlayNode.style.cssText = `border-radius: 15px;
|
||||
constructor() {
|
||||
this.overlayNode = document.createElement('div');
|
||||
this.overlayNode.style.cssText = `border-radius: 15px;
|
||||
font-size: xx-large;
|
||||
opacity: 0.75;
|
||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
@@ -19,53 +19,57 @@ position: absolute;
|
||||
-moz-user-select: none;
|
||||
-moz-transition: opacity 180ms ease-in;`;
|
||||
|
||||
this.overlayNode.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
this.overlayNode.addEventListener(
|
||||
'mousedown',
|
||||
e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
activate(terminal: Terminal): void {
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
|
||||
showOverlay(msg: string, timeout?: number): void {
|
||||
const { terminal, overlayNode } = this;
|
||||
|
||||
overlayNode.style.color = '#101010';
|
||||
overlayNode.style.backgroundColor = '#f0f0f0';
|
||||
overlayNode.textContent = msg;
|
||||
overlayNode.style.opacity = '0.75';
|
||||
|
||||
if (!overlayNode.parentNode) {
|
||||
terminal.element.appendChild(overlayNode);
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this.terminal = terminal;
|
||||
const divSize = terminal.element.getBoundingClientRect();
|
||||
const overlaySize = overlayNode.getBoundingClientRect();
|
||||
|
||||
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
|
||||
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
|
||||
|
||||
if (this.overlayTimeout) {
|
||||
clearTimeout(this.overlayTimeout);
|
||||
}
|
||||
if (timeout === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
public dispose(): void {}
|
||||
|
||||
public showOverlay(msg: string, timeout?: number): void {
|
||||
const {terminal, overlayNode } = this;
|
||||
|
||||
overlayNode.style.color = '#101010';
|
||||
overlayNode.style.backgroundColor = '#f0f0f0';
|
||||
overlayNode.textContent = msg;
|
||||
const self = this;
|
||||
self.overlayTimeout = setTimeout(() => {
|
||||
overlayNode.style.opacity = '0';
|
||||
self.overlayTimeout = setTimeout(() => {
|
||||
if (overlayNode.parentNode) {
|
||||
overlayNode.parentNode.removeChild(overlayNode);
|
||||
}
|
||||
self.overlayTimeout = null;
|
||||
overlayNode.style.opacity = '0.75';
|
||||
|
||||
if (!overlayNode.parentNode) {
|
||||
terminal.element.appendChild(overlayNode);
|
||||
}
|
||||
|
||||
const divSize = terminal.element.getBoundingClientRect();
|
||||
const overlaySize = overlayNode.getBoundingClientRect();
|
||||
|
||||
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
|
||||
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
|
||||
|
||||
if (this.overlayTimeout) {
|
||||
clearTimeout(this.overlayTimeout);
|
||||
}
|
||||
if (timeout === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
self.overlayTimeout = <number><any>setTimeout(() => {
|
||||
overlayNode.style.opacity = '0';
|
||||
self.overlayTimeout = <number><any>setTimeout(() => {
|
||||
if (overlayNode.parentNode) {
|
||||
overlayNode.parentNode.removeChild(overlayNode);
|
||||
}
|
||||
self.overlayTimeout = null;
|
||||
overlayNode.style.opacity = '0.75';
|
||||
}, 200);
|
||||
}, timeout || 1500);
|
||||
}
|
||||
}, 200) as any;
|
||||
}, timeout || 1500) as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, render } from 'preact';
|
||||
import App from './components/app';
|
||||
import { App } from './components/app';
|
||||
import './style/index.scss';
|
||||
|
||||
render(<App />, document.body);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #2b2b2b;
|
||||
.terminal {
|
||||
padding: 5px;
|
||||
}
|
||||
width: auto;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #2b2b2b;
|
||||
.terminal {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user