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 });