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"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -40,8 +41,8 @@ var (
|
||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var staticFiles embed.FS
|
var staticFiles embed.FS
|
||||||
|
|
||||||
//go:embed templates
|
// //go:embed templates
|
||||||
var templateFiles embed.FS
|
// var templateFiles embed.FS
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Database struct {
|
Database struct {
|
||||||
|
@ -68,8 +69,20 @@ func processError(err error) {
|
||||||
func init() {
|
func init() {
|
||||||
tmpl, _ = template.ParseGlob("templates/*.html")
|
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
|
// Sqlite3
|
||||||
err := logger.InitDB("/tmp/gontrol_logs.db")
|
err := logger.InitDB("/tmp/gontrol_logs.db")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -78,6 +91,19 @@ func init() {
|
||||||
db = database.InitSQLiteDB("/tmp/gontrol_agents.db")
|
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{}) {
|
func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
|
||||||
t, err := template.ParseFiles(tmpl)
|
t, err := template.ParseFiles(tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -109,6 +135,7 @@ func agentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
agent, _ := api.GetAgent(db, w, r, agentId)
|
agent, _ := api.GetAgent(db, w, r, agentId)
|
||||||
renderTemplate(w, "templates/partials/agent_detail.html", agent)
|
renderTemplate(w, "templates/partials/agent_detail.html", agent)
|
||||||
|
// renderTemplate(w, "agent_detail.html", agent)
|
||||||
}
|
}
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
api.CreateAgent(db, w, r)
|
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, "templates/partials/agent_list.html", agents)
|
||||||
|
// renderTemplate(w, "agent_list.html", agents)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAgentsStatus() []string {
|
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, "templates/partials/logs_partial.html", logs)
|
||||||
|
// renderTemplate(w, "logs_partial.html", logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
// proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
|
||||||
// agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
|
// selected by ?ip=…&port=… . It keeps the path *after* /proxyAgent,
|
||||||
// if agentIP == "" {
|
// removes the two query parameters, disables HTTP/2 (mandatory for WS),
|
||||||
// http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
|
// and streams with no buffering.
|
||||||
// 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.
|
|
||||||
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
agentIP := r.URL.Query().Get("ip")
|
ip := r.URL.Query().Get("ip")
|
||||||
if agentIP == "" {
|
port := r.URL.Query().Get("port")
|
||||||
http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
|
if ip == "" || port == "" {
|
||||||
return
|
http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
|
||||||
}
|
return
|
||||||
agentPort := r.URL.Query().Get("port")
|
}
|
||||||
if agentPort == "" {
|
|
||||||
http.Error(w, "Missing 'port' parameter", 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)
|
proxy := &httputil.ReverseProxy{
|
||||||
if err != nil {
|
Director: func(req *http.Request) {
|
||||||
http.Error(w, "Invalid agent URL: "+err.Error(), http.StatusBadRequest)
|
// Point to the agent
|
||||||
return
|
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
|
// Scrub ip/port from downstream query
|
||||||
originalDirector := proxy.Director
|
q := req.URL.Query()
|
||||||
proxy.Director = func(req *http.Request) {
|
q.Del("ip")
|
||||||
originalDirector(req)
|
q.Del("port")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
// Strip "/proxyAgent" prefix from the path
|
// Preserve Host (many CLI-style servers care)
|
||||||
const prefix = "/proxyAgent"
|
req.Host = target.Host
|
||||||
if strings.HasPrefix(req.URL.Path, prefix) {
|
},
|
||||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
|
|
||||||
if req.URL.Path == "" {
|
|
||||||
req.URL.Path = "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve original query parameters except ip and port (remove those for backend)
|
// Critical tweaks for WebSockets
|
||||||
query := req.URL.Query()
|
Transport: &http.Transport{
|
||||||
query.Del("ip")
|
Proxy: http.ProxyFromEnvironment,
|
||||||
query.Del("port")
|
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||||
req.URL.RawQuery = query.Encode()
|
ForceAttemptHTTP2: false, // MUST be HTTP/1.1 for WS
|
||||||
|
ResponseHeaderTimeout: 0,
|
||||||
|
},
|
||||||
|
|
||||||
// Optional: set the Host header to the target host
|
FlushInterval: -1, // stream bytes immediately
|
||||||
req.Host = targetURL.Host
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
func main() {
|
||||||
|
|
||||||
// sqlite3 has been initialized in init()
|
// sqlite3 has been initialized in init()
|
||||||
|
@ -405,8 +333,8 @@ func main() {
|
||||||
webMux.HandleFunc("/logs", logsHandler)
|
webMux.HandleFunc("/logs", logsHandler)
|
||||||
webMux.HandleFunc("/logs/{level}", logsHandler)
|
webMux.HandleFunc("/logs/{level}", logsHandler)
|
||||||
webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
|
webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
|
||||||
webMux.Handle("/templates/", http.FileServer(http.FS(templateFiles)))
|
|
||||||
webMux.HandleFunc("/proxyAgent", proxyAgentHandler)
|
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)
|
// db := database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
|
||||||
// defer db.Close()
|
// defer db.Close()
|
||||||
|
|
|
@ -10,6 +10,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
if (!interactiveMode && event.key === "Enter") {
|
if (!interactiveMode && event.key === "Enter") {
|
||||||
const command = input.value.trim();
|
const command = input.value.trim();
|
||||||
if (command === "start-interactive") {
|
if (command === "start-interactive") {
|
||||||
|
|
||||||
startInteractiveSession();
|
startInteractiveSession();
|
||||||
input.value = "";
|
input.value = "";
|
||||||
event.preventDefault();
|
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() {
|
function startInteractiveSession() {
|
||||||
interactiveMode = true;
|
interactiveMode = true;
|
||||||
// Hide the normal terminal and input.
|
// 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);
|
console.log("Initial fit: container width =", xtermContainer.offsetWidth, "cols =", term.cols);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
|
interactiveWS = new WebSocket(makeWsUrl());
|
||||||
|
// interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
|
||||||
interactiveWS.binaryType = "arraybuffer";
|
interactiveWS.binaryType = "arraybuffer";
|
||||||
|
|
||||||
interactiveWS.onopen = function () {
|
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 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/agents-graph.js"></script>
|
||||||
<script type="text/javascript" src="static/gontrol-helper.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>
|
<title>g2: gommand & gontrol</title>
|
||||||
</head>
|
</head>
|
||||||
<!-- <body class="bg-dark text-light" data-bs-theme="dark"> -->
|
<!-- <body class="bg-dark text-light" data-bs-theme="dark"> -->
|
||||||
|
|
Loading…
Reference in New Issue