fixed interactive terminal through proxyAgent
This commit is contained in:
parent
7472e2384e
commit
a03b419b43
230
main.go
230
main.go
|
@ -12,6 +12,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
@ -40,8 +41,8 @@ var (
|
|||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
//go:embed templates
|
||||
var templateFiles embed.FS
|
||||
// //go:embed templates
|
||||
// var templateFiles embed.FS
|
||||
|
||||
type Config struct {
|
||||
Database struct {
|
||||
|
@ -68,8 +69,20 @@ func processError(err error) {
|
|||
func init() {
|
||||
tmpl, _ = template.ParseGlob("templates/*.html")
|
||||
|
||||
// var err error
|
||||
// tmpl, err = template.ParseFS(
|
||||
// templateFiles,
|
||||
// "templates/*.html",
|
||||
// "templates/partials/*.html",
|
||||
// )
|
||||
|
||||
// if err != nil {
|
||||
// log.Fatalf("Failed to parse embedded templates: %v", err)
|
||||
// }
|
||||
|
||||
// Sqlite3
|
||||
err := logger.InitDB("/tmp/gontrol_logs.db")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -78,6 +91,19 @@ func init() {
|
|||
db = database.InitSQLiteDB("/tmp/gontrol_agents.db")
|
||||
}
|
||||
|
||||
// func renderTemplate(w http.ResponseWriter, tmplPath string, data interface{}) {
|
||||
// t := tmpl.Lookup(strings.TrimPrefix(tmplPath, "templates/"))
|
||||
// if t == nil {
|
||||
// log.Printf("Template %s not found", tmplPath)
|
||||
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if err := t.Execute(w, data); err != nil {
|
||||
// log.Printf("Failed to render template %s: %v", tmplPath, err)
|
||||
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
// }
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
|
||||
t, err := template.ParseFiles(tmpl)
|
||||
if err != nil {
|
||||
|
@ -109,6 +135,7 @@ func agentsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
} else {
|
||||
agent, _ := api.GetAgent(db, w, r, agentId)
|
||||
renderTemplate(w, "templates/partials/agent_detail.html", agent)
|
||||
// renderTemplate(w, "agent_detail.html", agent)
|
||||
}
|
||||
case http.MethodPost:
|
||||
api.CreateAgent(db, w, r)
|
||||
|
@ -155,6 +182,7 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
renderTemplate(w, "templates/partials/agent_list.html", agents)
|
||||
// renderTemplate(w, "agent_list.html", agents)
|
||||
}
|
||||
|
||||
func getAgentsStatus() []string {
|
||||
|
@ -218,167 +246,67 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
|
||||
renderTemplate(w, "templates/partials/logs_partial.html", logs)
|
||||
// renderTemplate(w, "logs_partial.html", logs)
|
||||
}
|
||||
|
||||
// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
|
||||
// if agentIP == "" {
|
||||
// http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// agentPort := r.URL.Query().Get("port")
|
||||
// if agentIP == "" {
|
||||
// http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Construct the URL to proxy to
|
||||
// agentURL := "http://" + agentIP + ":" + agentPort
|
||||
|
||||
// // Send request to agent server
|
||||
// resp, err := http.Get(agentURL)
|
||||
// if err != nil {
|
||||
// http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
|
||||
// return
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
|
||||
// // Parse the HTML from the agent's response
|
||||
// doc, err := html.Parse(resp.Body)
|
||||
// if err != nil {
|
||||
// http.Error(w, "Failed to parse agent response: "+err.Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Extract all <link> elements for stylesheets
|
||||
// var stylesheets []string
|
||||
// var extractStylesheets func(*html.Node)
|
||||
// extractStylesheets = func(n *html.Node) {
|
||||
// if n.Type == html.ElementNode && n.Data == "link" {
|
||||
// for _, attr := range n.Attr {
|
||||
// if attr.Key == "rel" && attr.Val == "stylesheet" {
|
||||
// for _, attr := range n.Attr {
|
||||
// if attr.Key == "href" {
|
||||
// stylesheets = append(stylesheets, attr.Val)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
// extractStylesheets(c)
|
||||
// }
|
||||
// }
|
||||
// extractStylesheets(doc)
|
||||
|
||||
// // Return the HTML and inject the stylesheets in the <head> section
|
||||
// w.Header().Set("Content-Type", "text/html")
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
|
||||
// // Inject the stylesheets into the <head> section of your page
|
||||
// fmt.Fprintf(w, "<html><head>")
|
||||
// for _, stylesheet := range stylesheets {
|
||||
// // Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
|
||||
// fmt.Fprintf(w, `<link rel="stylesheet" href="%s">`, stylesheet)
|
||||
// }
|
||||
// fmt.Fprintf(w, "</head><body>")
|
||||
|
||||
// // Now, serve the HTML content of the agent web app (or an iframe)
|
||||
// // Output the rest of the HTML (including the agent's content inside iframe)
|
||||
// fmt.Fprintf(w, `
|
||||
// <iframe src="%s" width="100%" height="800px" style="border:none;">
|
||||
// Your browser does not support iframes.
|
||||
// </iframe>
|
||||
// `, agentURL)
|
||||
|
||||
// fmt.Fprintf(w, "</body></html>")
|
||||
// }
|
||||
|
||||
// proxyAgentHandler proxies requests to the agent server specified by IP and port query parameters.
|
||||
// It strips the "/proxyAgent" prefix from the request path before forwarding.
|
||||
// proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
|
||||
// selected by ?ip=…&port=… . It keeps the path *after* /proxyAgent,
|
||||
// removes the two query parameters, disables HTTP/2 (mandatory for WS),
|
||||
// and streams with no buffering.
|
||||
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
agentIP := r.URL.Query().Get("ip")
|
||||
if agentIP == "" {
|
||||
http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
agentPort := r.URL.Query().Get("port")
|
||||
if agentPort == "" {
|
||||
http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ip := r.URL.Query().Get("ip")
|
||||
port := r.URL.Query().Get("port")
|
||||
if ip == "" || port == "" {
|
||||
http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
agentBaseURL := "http://" + agentIP + ":" + agentPort
|
||||
// We leave the scheme "http" even for WebSockets – the Upgrade
|
||||
// header does the rest. (Only cosmetic to change it to "ws”.)
|
||||
target, err := url.Parse("http://" + ip + ":" + port)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := url.Parse(agentBaseURL)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid agent URL: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
// Point to the agent
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
||||
// Trim the first "/proxyAgent" prefix
|
||||
if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path = "/"
|
||||
}
|
||||
}
|
||||
|
||||
// Modify the Director function to rewrite the request URL before sending to agent
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
// Scrub ip/port from downstream query
|
||||
q := req.URL.Query()
|
||||
q.Del("ip")
|
||||
q.Del("port")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// Strip "/proxyAgent" prefix from the path
|
||||
const prefix = "/proxyAgent"
|
||||
if strings.HasPrefix(req.URL.Path, prefix) {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path = "/"
|
||||
}
|
||||
}
|
||||
// Preserve Host (many CLI-style servers care)
|
||||
req.Host = target.Host
|
||||
},
|
||||
|
||||
// Preserve original query parameters except ip and port (remove those for backend)
|
||||
query := req.URL.Query()
|
||||
query.Del("ip")
|
||||
query.Del("port")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
// Critical tweaks for WebSockets
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
ForceAttemptHTTP2: false, // MUST be HTTP/1.1 for WS
|
||||
ResponseHeaderTimeout: 0,
|
||||
},
|
||||
|
||||
// Optional: set the Host header to the target host
|
||||
req.Host = targetURL.Host
|
||||
}
|
||||
FlushInterval: -1, // stream bytes immediately
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// agentIP := r.URL.Query().Get("ip")
|
||||
// if agentIP == "" {
|
||||
// http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// agentPort := r.URL.Query().Get("port")
|
||||
// if agentPort == "" {
|
||||
// http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// agentURL := "http://" + agentIP + ":" + agentPort + "/"
|
||||
|
||||
// resp, err := http.Get(agentURL)
|
||||
// if err != nil {
|
||||
// http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
|
||||
// return
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
|
||||
// // Copy headers from agent response (you might want to filter some)
|
||||
// for k, v := range resp.Header {
|
||||
// for _, vv := range v {
|
||||
// w.Header().Add(k, vv)
|
||||
// }
|
||||
// }
|
||||
// w.WriteHeader(resp.StatusCode)
|
||||
|
||||
// // Stream the entire response body directly to the client
|
||||
// io.Copy(w, resp.Body)
|
||||
// }
|
||||
|
||||
|
||||
func main() {
|
||||
|
||||
// sqlite3 has been initialized in init()
|
||||
|
@ -405,8 +333,8 @@ func main() {
|
|||
webMux.HandleFunc("/logs", logsHandler)
|
||||
webMux.HandleFunc("/logs/{level}", logsHandler)
|
||||
webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
|
||||
webMux.Handle("/templates/", http.FileServer(http.FS(templateFiles)))
|
||||
webMux.HandleFunc("/proxyAgent", proxyAgentHandler)
|
||||
webMux.HandleFunc("/proxyAgent/", proxyAgentHandler)
|
||||
|
||||
// db := database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
|
||||
// defer db.Close()
|
||||
|
|
|
@ -10,6 +10,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (!interactiveMode && event.key === "Enter") {
|
||||
const command = input.value.trim();
|
||||
if (command === "start-interactive") {
|
||||
|
||||
startInteractiveSession();
|
||||
input.value = "";
|
||||
event.preventDefault();
|
||||
|
@ -19,6 +20,30 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
});
|
||||
|
||||
// Helper: get ?ip=…&port=… from the current location
|
||||
function getQueryParam(name) {
|
||||
return new URLSearchParams(window.location.search).get(name);
|
||||
}
|
||||
|
||||
function makeWsUrl() {
|
||||
const proxyIp = getQueryParam("ip"); // "10.0.0.42" if you came via /proxyAgent
|
||||
const proxyPort = getQueryParam("port"); // "8080"
|
||||
const usingProxy = proxyIp && proxyPort; // truthy only in that case
|
||||
|
||||
if (usingProxy) {
|
||||
// Build ws(s)://<main-server>/proxyAgent/terminal?ip=…&port=…
|
||||
const u = new URL("/proxyAgent/terminal", window.location);
|
||||
u.searchParams.set("ip", proxyIp);
|
||||
u.searchParams.set("port", proxyPort);
|
||||
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
||||
return u.toString();
|
||||
}
|
||||
// Fallback: open directly on the agent we’re already on
|
||||
const u = new URL("/terminal", window.location);
|
||||
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
function startInteractiveSession() {
|
||||
interactiveMode = true;
|
||||
// Hide the normal terminal and input.
|
||||
|
@ -55,7 +80,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
console.log("Initial fit: container width =", xtermContainer.offsetWidth, "cols =", term.cols);
|
||||
}, 100);
|
||||
|
||||
interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
|
||||
interactiveWS = new WebSocket(makeWsUrl());
|
||||
// interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
|
||||
interactiveWS.binaryType = "arraybuffer";
|
||||
|
||||
interactiveWS.onopen = function () {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=xterm-addon-fit.js.map
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* 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 {
|
||||
cursor: text;
|
||||
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.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;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -13,6 +13,11 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.23/dist/cytoscape.min.js"></script>
|
||||
<script type="text/javascript" src="static/agents-graph.js"></script>
|
||||
<script type="text/javascript" src="static/gontrol-helper.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="static/xterm.css" />
|
||||
<script rel="text/javascript" src="static/xterm.js"></script>
|
||||
<script rel="text/javascript" src="static/xterm-addon-fit.js"></script>
|
||||
|
||||
<title>g2: gommand & gontrol</title>
|
||||
</head>
|
||||
<!-- <body class="bg-dark text-light" data-bs-theme="dark"> -->
|
||||
|
|
Loading…
Reference in New Issue