initial commit

This commit is contained in:
sawka 2022-06-07 17:25:35 -07:00
commit 751914196b
12 changed files with 4927 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
dist/
node_modules/
*~
*.log

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "sh2",
"version": "1.0.0",
"main": "index.js",
"license": "Proprietary",
"dependencies": {
"autobind-decorator": "^2.4.0",
"classnames": "^2.3.1",
"dayjs": "^1.11.3",
"mobx": "^6.6.0",
"mobx-react": "^7.5.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"sprintf-js": "^1.1.2",
"tsx-control-statements": "^4.1.1",
"xterm": "^4.18.0"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/core": "^7.18.2",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/plugin-transform-react-jsx": "^7.17.12",
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@types/classnames": "^2.3.1",
"@types/node": "^17.0.40",
"@types/react": "^18.0.12",
"@types/uuid": "^8.3.4",
"babel-loader": "^8.2.5",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"http-server": "^14.1.1",
"less": "^4.1.2",
"less-loader": "^11.0.0",
"lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.6.0",
"style-loader": "^3.3.1",
"typescript": "^4.7.3",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.1",
"webpack-merge": "^5.8.0"
}
}

13
scripthaus.md Normal file
View File

@ -0,0 +1,13 @@
# SH2 Commands
```bash
# @scripthaus command webpack-watch
# @scripthaus cd :current
node_modules/.bin/webpack --watch --config webpack.dev.js
```
```bash
# @scripthaus command devserver
# @scripthaus cd :current
node_modules/.bin/webpack-dev-server --config webpack.dev.js --host 0.0.0.0
```

220
src/main.tsx Normal file
View File

@ -0,0 +1,220 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {Terminal} from 'xterm';
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
import * as dayjs from 'dayjs'
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames"
type LineType = {
lineid : number,
ts : number,
userid : string,
linetype : string,
text : string,
cmdid : string,
cmdtext : string,
};
@mobxReact.observer
class LineMeta extends React.Component<{line : LineType}, {}> {
render() {
let line = this.props.line;
return (
<div className="meta">
<div className="lineid">{line.lineid}</div>
<div className="user">{line.userid}</div>
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
</div>
);
}
}
@mobxReact.observer
class LineText extends React.Component<{line : LineType}, {}> {
render() {
let line = this.props.line;
return (
<div className="line line-text">
<div className="avatar">
M
</div>
<div className="line-content">
<div className="meta">
<div className="user">{line.userid}</div>
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
</div>
<div className="text">
{line.text}
</div>
</div>
</div>
);
}
}
function loadPtyOut(term : Terminal, sessionId : string, cmdId : string) {
let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", sessionId, cmdId);
fetch(url).then((resp) => {
if (!resp.ok) {
throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText));
}
return resp.text()
}).then((resptext) => {
console.log(resptext);
term.write(resptext);
});
}
@mobxReact.observer
class LineCmd extends React.Component<{line : LineType}, {}> {
terminal : Terminal;
focus : mobx.IObservableValue<boolean> = mobx.observable.box(false, {name: "focus"});
componentDidMount() {
let {line, sessionid} = this.props;
console.log("load terminal", sessionid, line.cmdid);
this.terminal = new Terminal();
this.terminal.open(document.getElementById(this.getId()));
loadPtyOut(this.terminal, sessionid, line.cmdid);
console.log(this.terminal, this.terminal.element);
this.terminal.textarea.addEventListener("focus", () => {
mobx.action(() => {
this.focus.set(true);
})();
});
this.terminal.textarea.addEventListener("blur", () => {
mobx.action(() => {
this.focus.set(false);
})();
});
}
getId() : string {
let {line} = this.props;
return "cmd-" + line.lineid + "-" + line.cmdid;
}
render() {
let {line} = this.props;
let lineid = line.lineid.toString();
let running = false;
return (
<div className="line line-cmd">
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running})}>
{lineid}
</div>
<div className="line-content">
<div className="meta">
<div className="user">{line.userid}</div>
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
<div className="cmdtext">&gt; {line.cmdtext}</div>
</div>
<div className={cn("terminal-wrapper", {"focus": this.focus.get()})}>
<div className="terminal" id={this.getId()}></div>
</div>
</div>
</div>
);
}
}
@mobxReact.observer
class Line extends React.Component<{line : LineType}, {}> {
render() {
let line = this.props.line;
if (line.linetype == "text") {
return <LineText {...this.props}/>;
}
if (line.linetype == "cmd") {
return <LineCmd {...this.props}/>;
}
return <div className="line line-invalid">[invalid line type '{line.linetype}']</div>;
}
}
@mobxReact.observer
class CmdInput extends React.Component<{line : LineType}, {}> {
curLine : mobx.IObservableValue<string> = mobx.observable("", {name: "command-line"});
@mobx.action @boundMethod
onKeyDown(e : any) {
mobx.action(() => {
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
if (e.code == "Enter" && !ctrlMod) {
let cmdLine = this.curLine.get();
this.curLine.set("");
console.log("START COMMAND", cmdLine);
e.preventDefault();
return;
}
console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
})();
}
@boundMethod
onChange(e : any) {
mobx.action(() => {
this.curLine.set(e.target.value);
})();
}
render() {
return (
<div className="box cmd-input has-background-black">
<div className="cmd-input-context">
<div className="has-text-white">
<span className="bold term-blue">[ mike@imac27 master ~/work/gopath/src/github.com/sawka/darktile-termutil ]</span>
</div>
</div>
<div className="cmd-input-field field has-addons">
<div className="control cmd-quick-context">
<div className="button is-static">mike@local</div>
</div>
<div className="control cmd-input-control is-expanded">
<textarea value={this.curLine.get()} onKeyDown={this.onKeyDown} onChange={this.onChange} className="input" type="text"></textarea>
</div>
<div className="control cmd-exec">
<div className="button">
<span className="icon">
<i className="fa fa-rocket"/>
</span>
</div>
</div>
</div>
</div>
);
}
}
@mobxReact.observer
class Main extends React.Component<{sessionid : string}, {}> {
render() {
let lines = [
{lineid: 1, userid: "sawka", ts: 1654631122000, linetype: "text", text: "hello"},
{lineid: 2, userid: "sawka", ts: 1654631125000, linetype: "text", text: "again"},
{lineid: 3, userid: "sawka", ts: 1654631125000, linetype: "??", text: "again"},
{lineid: 4, userid: "sawka", ts: 1654631125000, linetype: "cmd", cmdid: "47445c53-cfcf-4943-8339-2c04447f20a1", cmdtext: "ls -l"},
{lineid: 5, userid: "sawka", ts: 1654631135000, linetype: "cmd", cmdid: "792a66ab-577c-4fe1-88f4-862703bdb42d", cmdtext: "ls -l | grep go"},
];
return (
<div className="main">
<h1 className="title scripthaus-logo-small">
<div className="title-cursor">&#9608;</div>
ScriptHaus
</h1>
<div className="lines">
<For each="line" of={lines}>
<Line key={line.lineid} line={line} sessionid={this.props.sessionid}/>
</For>
</div>
<CmdInput/>
</div>
);
}
}
export {Main};

217
src/sh2.less Normal file
View File

@ -0,0 +1,217 @@
.main {
.title.scripthaus-logo-small {
padding-left: 10px;
padding-top: 8px;
padding-bottom: 8px;
margin-bottom: 0;
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
background-color: black;
color: rgb(0, 177, 10);
position: relative;
font-size: 1.5rem;
.title-cursor {
position: absolute;
left: 157px;
bottom: 12px;
color: rgb(0, 177, 10);
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
font-size: 1rem;
}
}
}
.line {
margin: 5px;
border-radius: 5px;
padding: 5px;
display: flex;
flex-direction: row;
line-height: 1.25;
.avatar {
height: 38px;
width: 38px;
background-color: #729fcf;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
font-size: 1rem;
margin-right: 15px;
border-radius: 5px;
&.num4 {
font-size: 0.8rem;
}
&.num5 {
font-size: 0.7rem;
}
&.running {
background-color: #cc0000;
}
}
.line-content {
display: flex;
flex-direction: column;
flex-grow: 1;
.meta {
display: flex;
flex-direction: row;
font-size: 1rem;
margin-top: -4px;
.user {
font-weight: bold;
}
.ts {
margin-left: 10px;
margin-top: 4px;
font-size: 0.75rem;
color: #777;
}
.cmdtext {
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
font-size: 0.75rem;
margin-top: 5px;
margin-left: 8px;
overflow: hidden;
}
}
.text {
font-size: 1rem;
}
}
.terminal-wrapper {
background-color: #000;
padding: 5px;
width: calc(100% - 8px);
margin-right: 8px;
margin-top: 2px;
&.focus {
outline: 3px solid blue;
}
}
}
.line.line-cmd {
}
.line.line-invalid {
color: #000;
margin-left: 5px;
}
.lines {
background-color: #f7f7f7;
display: flex;
flex-direction: column;
height: 80vh;
overflow-y: scroll;
padding-bottom: 10px;
padding-top: 10px;
border-top: 2px solid #ddd;
}
.cmd-input {
border-radius: 0;
border-top: 2px solid white;
.cmd-input-context {
color: #fff;
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
}
.cmd-input-field {
textarea {
color: white;
background-color: black;
font-family: 'JetBrains Mono', monospace;
&:active, &:focus {
border-color: white !important;
}
}
.cmd-quick-context .button {
background-color: #000 !important;
color: white;
}
.cmd-exec .button {
background-color: #000 !important;
color: #d3d7cf;
}
}
}
.bold {
font-weight: bold;
}
.term-black {
color: #000;
}
.term-red {
color: #cc0000;
}
.term-green {
color: #4e9a06;
}
.term-yellow {
color: #c4a000;
}
.term-blue {
color: #729fcf;
}
.term-magenta {
color: #75507b;
}
.term-cyan {
color: #06989a;
}
.term-white {
color: #d3d7cf;
}
.term-bright-white {
color: #fff;
}
.monofont-thin {
font-family: 'JetBrains Mono', monospace;
font-weight: 200;
}
.monofont-normal {
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
}
.monofont-bold {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}

17
src/sh2.ts Normal file
View File

@ -0,0 +1,17 @@
import * as React from "react";
import {createRoot} from 'react-dom/client';
import {sprintf} from "sprintf-js";
import {Terminal} from 'xterm';
import {Main} from "./main";
let VERSION = __SHVERSION__;
let terminal = null;
document.addEventListener("DOMContentLoaded", () => {
let reactElem = React.createElement(Main, {sessionid: "AQ45MM"}, null);
let elem = document.getElementById("main");
let root = createRoot(elem);
root.render(reactElem);
});
console.log("SCRIPTHAUS", VERSION)

18
static/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script src="/sh2-dev.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@200;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/xterm.css" />
<link rel="stylesheet" href="/sh2.css" />
</head>
<body>
<div id="main"></div>
</body>
</html>

180
static/xterm.css Normal file
View File

@ -0,0 +1,180 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}

65
webpack.common.js Normal file
View File

@ -0,0 +1,65 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require("path");
module.exports = {
mode: "development",
entry: {
sh2: ["./src/sh2.ts", "./src/sh2.less"],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name]-dev.js"
},
module: {
rules: [
{
test: /\.tsx?$/,
// exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: "defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript"],
plugins: [
["@babel/transform-runtime", {"regenerator": true}],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
"babel-plugin-jsx-control-statements",
],
},
},
},
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
],
},
{
test: /\.less$/,
use: [
{loader: MiniCssExtractPlugin.loader},
"css-loader",
"less-loader"
]
},
]
},
plugins: [
new MiniCssExtractPlugin({filename: "[name].css", ignoreOrder: true})
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.wasm', '.json', '.less', '.css']
},
}

32
webpack.dev.js Normal file
View File

@ -0,0 +1,32 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
const VERSION = "v0.1.0";
var merged = merge.merge(common, {
mode: "development",
devtool: "source-map",
devServer: {
static: {
directory: path.join(__dirname, "static"),
},
port: 9000,
headers: {
'Cache-Control': 'no-store',
},
},
watchOptions: {
aggregateTimeout: 200,
},
});
var definePlugin = new webpack.DefinePlugin({
__SHVERSION__: JSON.stringify(VERSION),
__SHBUILD__: JSON.stringify("devbuild"),
});
merged.plugins.push(definePlugin);
module.exports = merged;

48
webpack.prod.js Normal file
View File

@ -0,0 +1,48 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const moment = require("dayjs");
const fs = require("fs");
const VERSION = "v0.1.0";
function makeBuildStr() {
let buildStr = moment().format("YYYYMMDD-HHmmss");
console.log("ScriptHaus " + VERSION + " build " + buildStr);
return buildStr;
}
const BUILD = makeBuildStr();
let BundleAnalyzerPlugin = null;
if (process.env.WEBPACK_ANALYZE) {
BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
}
let merged = merge.merge(common, {
mode: "production",
output: {
path: __dirname,
filename: "build/hibiki/latest/[name]-prod.min.js"
},
devtool: "source-map",
optimization: {
minimize: true,
},
});
merged.plugins.push(new LodashModuleReplacementPlugin());
merged.plugins.push(new MiniCssExtractPlugin({filename: "dist/[name].css", ignoreOrder: true}));
if (BundleAnalyzerPlugin != null) {
merged.plugins.push(new BundleAnalyzerPlugin());
}
merged.plugins.push(new webpack.DefinePlugin({
__SHVERSION__: JSON.stringify(VERSION),
__SHBUILD__: JSON.stringify(BUILD),
}));
module.exports = merged;

4065
yarn.lock Normal file

File diff suppressed because it is too large Load Diff