github.com/aitjcize/Overlord@v0.0.0-20240314041920-104a804cf5e8/overlord/app/common/js/TerminalWindow.jsx (about)

     1  // Copyright 2015 The Chromium OS Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  //
     5  // External dependencies:
     6  // - term.js: https://github.com/chjj/term.js
     7  //
     8  // Internal dependencies:
     9  // - UploadProgressWidget
    10  //
    11  // View for TerminalWindow:
    12  // - TerminalWindow
    13  //   props:
    14  //     id: DOM id
    15  //     path: path to websocket
    16  //     uploadRequestPath: path to upload the file (without terminal sid)
    17  //     title: window title
    18  //     enableMinimize: a boolean value for enable minimize button
    19  //     enableCopy: a boolean value for enable the copy icon, which allow
    20  //      copying of terminal buffer
    21  //     progressBars: an react reference to UploadProgressWidget instance
    22  //     onOpen: callback for connection open
    23  //     onClose: callback for connection close
    24  //     onError: callback for connection error
    25  //     onMessage: callback for message (binary)
    26  //     onMessage: callback for control message (JSON)
    27  //     onCloseClicked: callback for close button clicked
    28  //     onMinimizeClicked: callback for mininize button clicked
    29  
    30  // Terminal control sequence identifier
    31  var CONTROL_START = 128;
    32  var CONTROL_END = 129;
    33  
    34  
    35  Terminal.prototype.CopyAll = function () {
    36    var term = this;
    37    var textarea = term.getCopyTextarea();
    38    var text = term.grabText(
    39      0, term.cols - 1,
    40      0, term.lines.length - 1);
    41    term.emit("copy", text);
    42    textarea.focus();
    43    textarea.textContent = text;
    44    textarea.value = text;
    45    textarea.setSelectionRange(0, text.length);
    46    document.execCommand("Copy");
    47    setTimeout(function () {
    48      term.element.focus();
    49      term.focus();
    50    }, 1);
    51  };
    52  
    53  
    54  var TerminalWindow = React.createClass({
    55    mixins: [BaseWindow],
    56    getInitialState: function () {
    57      return {sid: "", maximized: false, window_params: undefined};
    58    },
    59    resizeWindowToVisualSize: function (visualWidth, visualHeight) {
    60      // We use CONTROL_START and CONTROL_END to specify the control buffer
    61      // region.  Ghost can use the 2 characters to know the control string.
    62      // format:
    63      // CONTROL_START ControlString_in_JSON CONTROL_END
    64      var term = this.term;
    65  
    66      if (visualWidth == 0 || visualHeight == 0) {
    67        return;
    68      }
    69  
    70      var widthToColsFactor = term.cols / term.element.clientWidth;
    71      var heightToRowsFactor = term.rows / term.element.clientHeight;
    72  
    73      var newCols = Math.floor(visualWidth * widthToColsFactor);
    74      var newRows = Math.floor(visualHeight * heightToRowsFactor);
    75  
    76      // Change visual size
    77      term.element.width = visualWidth;
    78      term.element.height = visualHeight;
    79  
    80      if (newCols != term.cols || newRows != term.rows) {
    81        var msg = {
    82          command: "resize",
    83          params: [newRows, newCols]
    84        }
    85        term.resize(newCols, newRows);
    86        term.refresh(0, term.rows - 1);
    87  
    88        // Send terminal control sequence
    89        this.sock.send((new Uint8Array([CONTROL_START])).buffer);
    90        this.sock.send(JSON.stringify(msg));
    91        this.sock.send((new Uint8Array([CONTROL_END])).buffer);
    92      }
    93    },
    94    componentDidMount: function () {
    95      var el = this.refs.window;
    96      var url = "ws" + ((window.location.protocol == "https:")? "s": "" ) +
    97                "://" + window.location.host + this.props.path;
    98      var sock = new WebSocket(url);
    99  
   100      var $el = $(el);
   101  
   102      this.sock = sock;
   103  
   104      this.makeDraggable(".terminal");
   105      this.bringToFront();
   106  
   107      sock.onerror = function (event) {
   108        var callback = this.props.onError;
   109        if (typeof(callback) != "undefined") {
   110          (callback.bind(this))(event);
   111        }
   112      }.bind(this);
   113  
   114      var term = new Terminal({
   115        cols: 80,
   116        rows: 24,
   117        scrollback: 10000,
   118        useStyle: true,
   119        screenKeys: false
   120      });
   121      this.term = term;
   122  
   123      var bindDisconnectEvent = function () {
   124        var overlay = $el.find(".terminal-disconnected-overlay");
   125        overlay.on("click", function (event) {
   126          overlay.css("display", "none");
   127        })
   128      }
   129  
   130      var bindDragAndDropEvents = function () {
   131        var termDom = $el.find(".terminal");
   132        var overlay = $el.find(".terminal-drop-overlay");
   133  
   134        termDom.on("dragenter", function (event) {
   135          event.preventDefault();
   136          event.stopPropagation();
   137          overlay.css("display", "block");
   138        }.bind(this));
   139  
   140        overlay.on("dragenter", function (event) {
   141          event.preventDefault();
   142          event.stopPropagation();
   143        });
   144  
   145        overlay.on("dragover", function (event) {
   146          event.preventDefault();
   147          event.stopPropagation();
   148        });
   149  
   150        overlay.on("dragleave", function (event) {
   151          event.preventDefault();
   152          event.stopPropagation();
   153          overlay.css("display", "none");
   154        });
   155  
   156        overlay.on("drop", function (event) {
   157          event.preventDefault();
   158          event.stopPropagation();
   159          var files = event.originalEvent.dataTransfer.files;
   160  
   161          for (var i = 0; i < files.length; i++) {
   162            this.props.progressBars.upload(this.props.uploadRequestPath, files[i],
   163                                           undefined, this.state.sid);
   164          }
   165          $el.find(".terminal-drop-overlay").css("display", "none");
   166        }.bind(this));
   167      }.bind(this);
   168  
   169      sock.onopen = function (event) {
   170        term.open(el);
   171        term.on("title", function (title) {
   172          $el.find(".app-window-title").text(title);
   173        });
   174  
   175        term.on("data", function (data) {
   176          if (sock.readyState == 1) { // OPEN
   177            sock.send(data);
   178          }
   179        });
   180  
   181        sock.onmessage = function (msg) {
   182          if (msg.data instanceof Blob) {
   183            var callback = this.props.onMessage;
   184            ReadBlobAsText(msg.data, function (text) {
   185              term.write(text);
   186              if (typeof(callback) != "undefined") {
   187                (callback.bind(this))(text);
   188              }
   189            }.bind(this));
   190          // In Javacscript, a string literal is not a instance of String.
   191          // We check both cases here.
   192          } else if (msg.data instanceof String || typeof(msg.data) == "string") {
   193            var control = JSON.parse(msg.data);
   194            if (control.type == "sid") {
   195              this.setState({sid: control.data})
   196            }
   197            var callback = this.props.onControl;
   198            if (typeof(callback) != "undefined") {
   199              (callback.bind(this))(control);
   200            }
   201          }
   202        }.bind(this);
   203  
   204        sock.onclose = function (event) {
   205          term.write("\r\nConnection lost.");
   206          $el.find(".terminal-disconnected-overlay").css("display", "block");
   207  
   208          var callback = this.props.onClose;
   209          if (typeof(callback) != "undefined") {
   210            (callback.bind(this))(event);
   211          }
   212        }.bind(this);
   213  
   214        // Bind events
   215        bindDisconnectEvent();
   216  
   217        // Only bind drag and drop event if uploadRequestPath is provided
   218        if (typeof(this.props.uploadRequestPath) != "undefined") {
   219          bindDragAndDropEvents();
   220        }
   221  
   222        var callback = this.props.onOpen;
   223        if (typeof(callback) != "undefined") {
   224          (callback.bind(this))(event);
   225        }
   226      }.bind(this);
   227    },
   228    onCloseMouseUp2: function (event) {
   229      this.onCloseMouseUp();
   230      this.sock.close();
   231    },
   232    onMaximizeMouseUp: function (event) {
   233      var el = this.refs.window;
   234      if (!this.state.maximized) {
   235        var window_params = {
   236          left: el.offsetLeft,
   237          top: el.offsetTop,
   238          width: this.term.element.clientWidth,
   239          height: this.term.element.clientHeight,
   240        };
   241        var offsetWidth = el.offsetWidth - this.term.element.clientWidth;
   242        var offsetHeight = el.offsetHeight - this.term.element.clientHeight;
   243        this.disableDraggable();
   244        this.resizeWindowToVisualSize(window.innerWidth - offsetWidth,
   245                                      window.innerHeight - offsetHeight);
   246        this.setState(function (state, props) {
   247          state.maximized = true;
   248          state.window_params = window_params;
   249        });
   250      } else {
   251        var params = this.state.window_params;
   252        this.enableDraggable();
   253        $(el).css({
   254          top: params.top,
   255          left: params.left,
   256          position: "fixed",
   257        });
   258        this.resizeWindowToVisualSize(params.width, params.height);
   259        this.setState(function (state, props) {
   260          state.maximized = false;
   261          state.window_params = undefined;
   262        });
   263      }
   264    },
   265    onCopyMouseUp: function (event) {
   266      this.term.CopyAll();
   267    },
   268    render: function () {
   269      var minimize_icon_node = "",
   270          maximize_icon_node = "",
   271          copy_icon_node = "",
   272          app_window_class = "";
   273      if (this.props.enableCopy) {
   274        copy_icon_node = (
   275            <div className="app-window-icon app-window-copy"
   276             onMouseUp={this.onCopyMouseUp}></div>
   277        );
   278      }
   279      if (this.props.enableMinimize) {
   280        minimize_icon_node = (
   281            <div className="app-window-icon app-window-minimize"
   282             onMouseUp={this.onMinimizeMouseUp}></div>
   283        );
   284      }
   285      if (this.props.enableMaximize) {
   286        maximize_icon_node = (
   287            <div className="app-window-icon app-window-maximize"
   288             onMouseUp={this.onMaximizeMouseUp}></div>
   289        );
   290      }
   291      if (this.state.maximized) {
   292        app_window_class = "app-window-maximized";
   293      }
   294      return (
   295        <div className={"app-window " + app_window_class} ref="window"
   296            onMouseDown={this.onWindowMouseDown}>
   297          <div className="app-window-title">{this.props.title}</div>
   298          <div className="app-window-control">
   299            {copy_icon_node}
   300            {minimize_icon_node}
   301            {maximize_icon_node}
   302            <div className="app-window-icon app-window-close"
   303             onMouseUp={this.onCloseMouseUp2}></div>
   304          </div>
   305          <div className="terminal-overlay terminal-drop-overlay">
   306            Drop files here to upload
   307          </div>
   308          <div className="terminal-overlay terminal-disconnected-overlay">
   309            Connection lost
   310          </div>
   311        </div>
   312      );
   313    }
   314  });