/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
(function() {
"use strict";
var React = require("react");
var Term = require("term");
require("console.css");
/*
* A terminal component that communicates over a cockpit channel.
*
* The only required property is 'channel', which must point to a cockpit
* stream channel.
*
* The size of the terminal can be set with the 'rows' and 'cols'
* properties. If those properties are not given, the terminal will fill
* its container.
*
* If the 'onTitleChanged' callback property is set, it will be called whenever
* the title of the terminal changes.
*
* Call focus() to set the input focus on the terminal.
*/
var Terminal = React.createClass({
propTypes: {
cols: React.PropTypes.number,
rows: React.PropTypes.number,
channel: React.PropTypes.object.isRequired,
onTitleChanged: React.PropTypes.func
},
componentWillMount: function () {
var term = new Term({
cols: this.state.cols || 80,
rows: this.state.rows || 25,
screenKeys: true,
useStyle: true
});
term.on('data', function(data) {
if (this.props.channel.valid)
this.props.channel.send(data);
}.bind(this));
if (this.props.onTitleChanged)
term.on('title', this.props.onTitleChanged);
this.setState({ terminal: term });
},
componentDidMount: function () {
this.state.terminal.open(this.refs.terminal);
this.connectChannel();
if (!this.props.rows) {
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
}
},
componentWillUpdate: function (nextProps, nextState) {
if (nextState.cols !== this.state.cols || nextState.rows !== this.state.rows) {
this.state.terminal.resize(nextState.cols, nextState.rows);
this.props.channel.control({
window: {
rows: nextState.rows,
cols: nextState.cols
}
});
}
if (nextProps.channel !== this.props.channel) {
this.state.terminal.reset();
this.disconnectChannel();
}
},
componentDidUpdate: function (prevProps) {
if (prevProps.channel !== this.props.channel)
this.connectChannel();
},
render: function () {
// ensure react never reuses this div by keying it with the terminal widget
return
;
},
componentWillUnmount: function () {
this.disconnectChannel();
this.state.terminal.destroy();
},
onChannelMessage: function (event, data) {
this.state.terminal.write(data);
},
onChannelClose: function (event, options) {
var term = this.state.terminal;
term.write('\x1b[31m' + (options.problem || 'disconnected') + '\x1b[m\r\n');
term.cursorHidden = true;
term.refresh(term.y, term.y);
},
connectChannel: function () {
var channel = this.props.channel;
if (channel && channel.valid) {
channel.addEventListener('message', this.onChannelMessage.bind(this));
channel.addEventListener('close', this.onChannelClose.bind(this));
}
},
disconnectChannel: function () {
if (this.props.channel) {
this.props.channel.removeEventListener('message', this.onChannelMessage);
this.props.channel.removeEventListener('close', this.onChannelClose);
}
},
focus: function () {
if (this.state.terminal)
this.state.terminal.focus();
},
onWindowResize: function () {
var padding = 2 * 11;
var node = this.getDOMNode();
var terminal = this.refs.terminal.querySelector('.terminal');
var ch = document.createElement('div');
ch.textContent = 'M';
terminal.appendChild(ch);
var height = ch.offsetHeight; // offsetHeight is only correct for block elements
ch.style.display = 'inline';
var width = ch.offsetWidth;
terminal.removeChild(ch);
this.setState({
rows: Math.floor((node.parentElement.clientHeight - padding) / height),
cols: Math.floor((node.parentElement.clientWidth - padding) / width)
});
}
});
module.exports = { Terminal: Terminal };
}());