From 311762b74d42e0b476d9c05b191d12c513830f6f Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 19 Jul 2017 00:14:52 -0500 Subject: [PATCH 01/19] basis of instance based control --- src/js/components/Content.js | 56 +++++++++---- src/js/components/Terminal.js | 144 +++++++++++++++++++--------------- starter/App.js | 8 +- webpack/webpack.dev.config.js | 4 + 4 files changed, 131 insertions(+), 81 deletions(-) diff --git a/src/js/components/Content.js b/src/js/components/Content.js index c262649..33cd561 100644 --- a/src/js/components/Content.js +++ b/src/js/components/Content.js @@ -6,9 +6,9 @@ class Content extends Component { static propTypes = { backgroundColor: PropTypes.objectOf(PropTypes.string), - output: PropTypes.arrayOf(PropTypes.element), prompt: PropTypes.objectOf(PropTypes.string), inputStyles: PropTypes.objectOf(PropTypes.string), + register: PropTypes.func, handleChange: PropTypes.func, handlerKeyPress: PropTypes.func.isRequired, }; @@ -18,30 +18,56 @@ class Content extends Component { maximise: PropTypes.bool, }; + state = { + summary: [], + promptPrefix: '', + history: [], + historyCounter: 0, + input: [], + keyInputs: [], + }; + componentDidMount = () => { this.focusInput(); + this.unregister = this.props.register(this); }; // Adjust scrolling componentDidUpdate = () => { - if (this.inputWrapper !== null) this.inputWrapper.scrollIntoView(false); + if (this.inputWrapper !== null) { + this.inputWrapper.scrollIntoView(false); + } }; + componentWillUnmount() { + this.unregister(); + } + focusInput = () => { - if (this.com !== null) this.com.focus(); + if (this.com !== null) { + this.com.focus(); + } }; + handleChange = (e) => { + this.props.handleChange(this, e); + } + + handleKeyPress = (e) => { + this.props.handlerKeyPress(this, e, this.com); + } + render() { - const { - output, - prompt, - inputStyles, - handleChange, - backgroundColor, - handlerKeyPress, - } = this.props; + const { prompt, inputStyles, backgroundColor } = this.props; const { symbol, maximise } = this.context; + const output = this.state.summary.map((content, i) => { + if (typeof content === 'string' && content.length === 0) { + return
 
; + } + return
{content}
; + }); + return (
(this.inputWrapper = elm)} > - {symbol} + + {this.state.promptPrefix + symbol} + (this.com = com)} - onKeyPress={handleChange} - onKeyDown={e => handlerKeyPress(e, this.com)} + onKeyPress={this.handleChange} + onKeyDown={this.handleKeyPress} />
diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index fca29de..ad64e8d 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -44,7 +44,7 @@ class Terminal extends Component { show: this.showMsg, clear: this.clearScreen, help: this.showHelp, - echo: (input) => { console.log(...input.slice(1)); }, + echo: input => input.slice(1).join(' '), 'edit-line': { method: this.editLine, options: [ @@ -78,23 +78,18 @@ class Terminal extends Component { state = { prompt: '>', - promptPrefix: '', - summary: [], commands: {}, descriptions: {}, - history: [], - historyCounter: 0, show: true, minimise: false, maximise: false, - input: [], shortcuts: {}, - keyInputs: [], + instances: [], }; getChildContext() { return { - symbol: this.state.promptPrefix + this.state.prompt, + symbol: this.state.prompt, show: this.state.show, minimise: this.state.minimise, maximise: this.state.maximise, @@ -121,7 +116,7 @@ class Terminal extends Component { this.assembleCommands(); this.setDescriptions(); this.setShortcuts(); - this.showMsg(); + this.showMsg({}, this.printLine.bind(this, this.state.instances[0])); if (this.props.watchConsoleLogging) { this.watchConsoleLogging(); @@ -149,13 +144,6 @@ class Terminal extends Component { const barColorStyles = { backgroundColor: barColor }; const backgroundColorStyles = { backgroundColor }; - const output = this.state.summary.map((content, i) => { - if (typeof content === 'string' && content.length === 0) { - return
 
; - } - return
{content}
; - }); - return (
{ this.setState({ promptPrefix }); }; @@ -243,14 +232,26 @@ class Terminal extends Component { * set the input value with the possible history value * @param {number} next position on the history */ - setValueWithHistory = (position, inputRef) => { - const { history } = this.state; + setValueWithHistory = (instance, position, inputRef) => { + const { history } = instance.state; if (history[position]) { - this.setState({ historyCounter: position }); + instance.setState({ historyCounter: position }); inputRef.value = history[position]; } }; + // Used to keep track of all instances + registerInstance = (instance) => { + const { instances } = this.state; + instances.push(instance); + this.setState({ instances }); + return () => { + this.setState({ + instances: this.state.instances.filter(i => !isEqual(i, instance)), + }); + }; + } + toggleState = name => () => this.setState({ [name]: !this.state[name] }); // Prepare the built-in commands @@ -315,15 +316,15 @@ class Terminal extends Component { } // Refresh or clear the screen - clearScreen = () => { + clearScreen = function clearScreen() { this.setState({ summary: [] }); }; // Method to check for shortcut and invoking commands - checkShortcuts = (key) => { + checkShortcuts = (instance, key) => { const shortcuts = Object.keys(this.state.shortcuts); if (shortcuts.length > 0) { - const { keyInputs } = this.state; + const { keyInputs } = instance.state; let modKey = key; if (key === 'meta') { // eslint-disable-next-line no-nested-ternary @@ -340,17 +341,17 @@ class Terminal extends Component { if (options.length > 0) { if (options.length === 1 && options[0][0].length === len) { const shortcut = shortcuts[options[0][1]]; - this.runCommand(this.state.shortcuts[shortcut]); - this.setState({ keyInputs: [] }); + this.runCommand(instance, this.state.shortcuts[shortcut]); + instance.setState({ keyInputs: [] }); } } else if (keyInputs.length > 0) { - this.setState({ keyInputs: [] }); + instance.setState({ keyInputs: [] }); } } } // edit-line command - editLine = (args) => { + editLine = function editLine(args) { const { summary } = this.state; let index = args.line; if (index === -1) { @@ -361,34 +362,40 @@ class Terminal extends Component { } // Listen for user input - handleChange = (e) => { + handleChange = (instance, e) => { + const { input, promptPrefix, history } = instance.state; if (e.key === 'Enter' && !e.shiftKey) { - this.printLine(`${this.state.promptPrefix}${this.state.prompt} ${e.target.value}`, false); - const { input } = this.state; + this.printLine.bind(this, instance)( + `${promptPrefix}${this.state.prompt} ${e.target.value}`, + false, + ); const res = this.runCommand( + instance, `${input.join('\n')}${input.length > 0 ? '\n' : ''}${e.target.value}`, ); if (typeof res !== 'undefined') { - this.printLine(res); + this.printLine.bind(this, instance)(res); } - const history = [...this.state.history, e.target.value]; - this.setState({ + const newHistory = [...history, e.target.value]; + instance.setState({ input: [], - history, - historyCounter: history.length, + history: newHistory, + historyCounter: newHistory.length, }); e.target.value = ''; // eslint-disable-line no-param-reassign } else if (e.key === 'Enter' && e.shiftKey) { - this.printLine(`${this.state.promptPrefix}${this.state.prompt} ${e.target.value}`, false); - const { input } = this.state; - const history = [...this.state.history, e.target.value]; + this.printLine.bind(this, instance)( + `${promptPrefix}${this.state.prompt} ${e.target.value}`, + false, + ); + const newHistory = [...history, e.target.value]; this.setState({ input: [...input, e.target.value], - history, - historyCounter: history.length, + history: newHistory, + historyCounter: newHistory.length, }); e.target.value = ''; // eslint-disable-line no-param-reassign } @@ -399,16 +406,16 @@ class Terminal extends Component { * with the history * @param {event} event of input */ - handlerKeyPress = (e, inputRef) => { + handlerKeyPress = (instance, e, inputRef) => { const key = whatkey(e).key; - const { historyCounter, keyInputs } = this.state; + const { historyCounter, keyInputs } = instance.state; if (keyInputs.length === 0) { switch (key) { case 'up': - this.setValueWithHistory(historyCounter - 1, inputRef); + this.setValueWithHistory(instance, historyCounter - 1, inputRef); break; case 'down': - this.setValueWithHistory(historyCounter + 1, inputRef); + this.setValueWithHistory(instance, historyCounter + 1, inputRef); break; case 'tab': inputRef.value = this.autocompleteValue(inputRef); @@ -418,7 +425,7 @@ class Terminal extends Component { break; } } - this.checkShortcuts(key); + this.checkShortcuts(instance, key); } // Plugins @@ -427,8 +434,8 @@ class Terminal extends Component { try { plugin.load( { - printLine: this.printLine, - runCommand: this.runCommand, + printLine: this.printLine.bind(this, this), + runCommand: this.runCommand.bind(this, this), setPromptPrefix: this.setPromptPrefix, getPluginMethod: this.getPluginMethod, }, @@ -467,7 +474,7 @@ class Terminal extends Component { } // Print the summary (input -> output) - printLine = (inp, std = true) => { + printLine = (instance, inp, std = true) => { let print = true; if (std) { const { plugins } = this.props; @@ -481,14 +488,14 @@ class Terminal extends Component { } if (print !== false) { - const summary = this.state.summary; + const summary = instance.state.summary; summary.push(inp); - this.setState({ summary }); + instance.setState({ summary }); } }; // Execute the commands - runCommand = (inputText) => { + runCommand = (instance, inputText) => { const inputArray = inputText.split(' '); const input = inputArray[0]; const args = inputArray; // Undefined for function call @@ -499,14 +506,22 @@ class Terminal extends Component { // do nothing } else if (command === undefined) { if (typeof this.props.commandPassThrough === 'function') { - res = this.props.commandPassThrough(inputArray, this.printLine, this.runCommand); + res = this.props.commandPassThrough( + inputArray, + this.printLine.bind(this, instance), + this.runCommand.bind(this, instance), + ); } else { - this.printLine(`-bash:${input}: command not found`); + this.printLine.bind(this, instance)(`-bash:${input}: command not found`); } } else { const parsedArgs = command.parse(args); if (typeof parsedArgs !== 'object' || (typeof parsedArgs === 'object' && !parsedArgs.help)) { - res = command.method(parsedArgs, this.printLine, this.runCommand); + res = command.method.bind(instance)( + parsedArgs, + this.printLine.bind(this, instance), + this.runCommand.bind(this, instance), + ); } } return res; @@ -514,32 +529,35 @@ class Terminal extends Component { // Listen for console logging and pass the input to handler (handleLogging) watchConsoleLogging = () => { - handleLogging('log', this.printLine); - handleLogging('info', this.printLine); - // handleLogging('warn', this.printLine); - // handleLogging('error', this.printLine); + handleLogging('log', this.printLine.bind(this, this.state.instances[0])); + handleLogging('info', this.printLine.bind(this, this.state.instances[0])); + // handleLogging('warn', this.printLine.bind(this, this.state.instances[0])); + // handleLogging('error', this.printLine.bind(this, this.state.instances[0])); }; // List all the commands (state + user defined) - showHelp = () => { + showHelp = (args, printLine) => { const options = Object.keys(this.state.commands); const descriptions = this.state.descriptions; for (const option of options) { // eslint-disable-line no-restricted-syntax if (descriptions[option] !== false) { - this.printLine(`${option} - ${descriptions[option]}`); + printLine(`${option} - ${descriptions[option]}`); } } }; // Show the msg (prop msg) - showMsg = () => { - this.printLine(this.props.msg); + showMsg = (args, printLine) => { + printLine(this.props.msg); }; render() { return ( -
+
{this.getAppContent()}
); diff --git a/starter/App.js b/starter/App.js index a0f4044..c1cd71c 100644 --- a/starter/App.js +++ b/starter/App.js @@ -1,9 +1,9 @@ -import React from 'react'; +import React from 'react'; // eslint-disable-line import { render } from 'react-dom'; import PseudoFileSystem from 'terminal-in-react-pseudo-file-system-plugin'; // eslint-disable-line -// Bundle generated with npm run build:production ('../lib/js/index') or use '../components' -import Terminal from '../lib/js/index'; -import '../lib/css/index.css'; // needed to test prod +// Bundle generated with npm run build:production ('../lib/js/index') or use '../src/js' +import Terminal from '../src/js'; +import '../src/styles/index.scss'; // (../lib/css/index.css) or '../src/styles/index.scss' const App = () => (
diff --git a/webpack/webpack.dev.config.js b/webpack/webpack.dev.config.js index 6aee75b..56e6d2e 100644 --- a/webpack/webpack.dev.config.js +++ b/webpack/webpack.dev.config.js @@ -15,6 +15,10 @@ module.exports = { test: /\.css$/, use: ['style-loader', 'css-loader'], }, + { + test: /\.scss$/, + use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], + }, ], }, target: 'web', From a8e227f2ae8b253a88fe735eba6ad390a5276a6f Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 19 Jul 2017 01:46:31 -0500 Subject: [PATCH 02/19] make plugins instance compatible --- src/js/components/Plugin.js | 13 ++-- src/js/components/Terminal.js | 113 +++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index ee68a4d..4fc86d6 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -1,12 +1,11 @@ export default class Plugin { - constructor(name = '', version = '1.0.0') { - this.name = name; - this.version = version; - } - - load = () => {}; + static defaultData = ''; + static name = ''; + static version = '1.0.0'; - afterLoad = () => {}; + constructor(api) { + this.api = api; + } getPublicMethods = () => ({}); diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index ba3b942..08153b2 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -42,7 +42,7 @@ class Terminal extends Component { constructor(props) { super(props); - this.pluginMethods = {}; + this.instances = []; this.defaultCommands = { // eslint-disable-line react/sort-comp @@ -89,7 +89,7 @@ class Terminal extends Component { minimise: false, maximise: false, shortcuts: {}, - instances: [], + pluginData: {}, }; getChildContext() { @@ -121,7 +121,7 @@ class Terminal extends Component { this.assembleCommands(); this.setDescriptions(); this.setShortcuts(); - this.showMsg({}, this.printLine.bind(this, this.state.instances[0])); + this.showMsg({}, this.printLine.bind(this, this.instances[0].instance)); if (this.props.watchConsoleLogging) { this.watchConsoleLogging(); @@ -240,13 +240,37 @@ class Terminal extends Component { // Used to keep track of all instances registerInstance = (instance) => { - const { instances } = this.state; - instances.push(instance); - this.setState({ instances }); + const pluginInstances = {}; + const pluginMethods = {}; + + this.props.plugins.forEach((PluginClass) => { + try { + const plugin = new PluginClass({ + printLine: this.printLine.bind(this, instance), + runCommand: this.runCommand.bind(this, instance), + setPromptPrefix: this.setPromptPrefix.bind(this, instance), + getPluginMethod: this.getPluginMethod.bind(this, instance), + }); + + pluginMethods[PluginClass.name] = { + ...plugin.getPublicMethods(), + _getName: () => PluginClass.name, + _getVersion: () => PluginClass.version, + }; + pluginInstances[PluginClass.name] = plugin; + } catch (e) { + console.error(`Error instantiating plugin ${PluginClass.name}`, e); // eslint-disable-line no-console + } + }); + + this.instances.push({ + instance, + pluginMethods, + pluginInstances, + }); + return () => { - this.setState({ - instances: this.state.instances.filter(i => !isEqual(i, instance)), - }); + this.instances = this.instances.filter(i => !isEqual(i.instance, instance)); }; } @@ -435,59 +459,48 @@ class Terminal extends Component { // Plugins loadPlugins = () => { // TODO intance plugins + const pluginData = {}; this.props.plugins.forEach((plugin) => { try { - plugin.load({ - printLine: this.printLine.bind(this, this), - runCommand: this.runCommand.bind(this, this), - setPromptPrefix: this.setPromptPrefix.bind(this, this), - getPluginMethod: this.getPluginMethod, - }); - - this.pluginMethods[plugin.name] = { - ...plugin.getPublicMethods(), - _getName: () => plugin.name, - _getVersion: () => plugin.version, - }; - } catch (e) { - console.error(`Error loading plugin ${plugin.name}`); // eslint-disable-line no-console - console.dir(e); - } - }); - - this.props.plugins.forEach((plugin) => { - try { - plugin.afterLoad(); + pluginData[plugin.name] = plugin.defaultData; } catch (e) { - // Do nothing + console.error(`Error loading plugin ${plugin.name}`, e); // eslint-disable-line no-console } }); + this.setState({ pluginData }); }; // Plugin api method to get a public plugin method - getPluginMethod = (name, method) => { - if (this.pluginMethods[name]) { - if (this.pluginMethods[name][method]) { - return this.pluginMethods[name][method]; + getPluginMethod = (instance, name, method) => { + const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + if (instanceData) { + if (instanceData.pluginMethods[name]) { + if (instanceData.pluginMethods[name][method]) { + return instanceData.pluginMethods[name][method]; + } + throw new Error( + `No method with name ${name} has been registered for plugin ${name}`, + ); + } else { + throw new Error(`No plugin with name ${name} has been registered`); } - throw new Error( - `No method with name ${name} has been registered for plugin ${name}`, - ); - } else { - throw new Error(`No plugin with name ${name} has been registered`); } + return null; }; // Print the summary (input -> output) printLine = (instance, inp, std = true) => { let print = true; if (std) { - const { plugins } = this.props; - for (let i = 0; i < plugins.length; i += 1) { - try { - print = plugins[i].readStdOut(inp); - } catch (e) { - // Do nothing + const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + if (instanceData) { + const plugins = instanceData.pluginInstances; + for (let i = 0; i < plugins.length; i += 1) { + try { + print = plugins[i].readStdOut(inp); + } catch (e) { + // Do nothing + } } } } @@ -536,10 +549,10 @@ class Terminal extends Component { // Listen for console logging and pass the input to handler (handleLogging) watchConsoleLogging = () => { // TODO switch to a print to all instances method - handleLogging('log', this.printLine.bind(this, this.state.instances[0])); - handleLogging('info', this.printLine.bind(this, this.state.instances[0])); - // handleLogging('warn', this.printLine.bind(this, this.state.instances[0])); - // handleLogging('error', this.printLine.bind(this, this.state.instances[0])); + handleLogging('log', this.printLine.bind(this, this.instances[0].instance)); + handleLogging('info', this.printLine.bind(this, this.instances[0].instance)); + // handleLogging('warn', this.printLine.bind(this, this.instances[0].instance)); + // handleLogging('error', this.printLine.bind(this, this.instances[0].instance)); }; // List all the commands (state + user defined) From ed88fb1063bca84edff0aac0c0846a5acead9839 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 19 Jul 2017 01:54:50 -0500 Subject: [PATCH 03/19] up verison and export plugin base --- package.json | 2 +- src/js/index.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 97a7778..869325e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "terminal-in-react", - "version": "3.3.2", + "version": "3.4.0", "description": "A component for making a terminal in React", "main": "lib/js/index.js", "module": "src/index.js", diff --git a/src/js/index.js b/src/js/index.js index be18306..a0922d8 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,8 +1,8 @@ import Terminal from './components/Terminal'; -// import BasePlugin from './components/Plugin'; +import PluginBase from './components/Plugin'; export default Terminal; -// export { -// BasePlugin, -// }; +export { + PluginBase, +}; From 21c8b97ae15722be91df1a842cbf9f32e6b5bac3 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 19 Jul 2017 09:51:21 -0500 Subject: [PATCH 04/19] Instance based commands for plugins and static commands from plugins --- src/js/components/Plugin.js | 7 +++- src/js/components/Terminal.js | 74 ++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index 4fc86d6..f91d944 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -1,10 +1,15 @@ export default class Plugin { - static defaultData = ''; static name = ''; static version = '1.0.0'; + static defaultData = ''; + + static commands = {}; + static descriptions = {}; constructor(api) { this.api = api; + this.commands = {}; + this.descriptions = {}; } getPublicMethods = () => ({}); diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 08153b2..28f05e9 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -44,14 +44,23 @@ class Terminal extends Component { this.instances = []; + this.pluginData = {}; + this.defaultCommands = { // eslint-disable-line react/sort-comp show: this.showMsg, - clear: this.clearScreen, - help: this.showHelp, + clear: { + method: this.clearScreen, + needsInstance: true, + }, + help: { + method: this.showHelp, + needsInstance: true, + }, echo: input => input.slice(1).join(' '), 'edit-line': { method: this.editLine, + needsInstance: true, options: [ { name: 'line', @@ -89,7 +98,6 @@ class Terminal extends Component { minimise: false, maximise: false, shortcuts: {}, - pluginData: {}, }; getChildContext() { @@ -191,6 +199,12 @@ class Terminal extends Component { ); + // Plugin data getter + getPluginData = name => this.pluginData[name]; + + // Plugin data setter + setPluginData = (name, data) => (this.pluginData[name] = data); + // Set descriptions of the commands setDescriptions = () => { let descriptions = { @@ -250,6 +264,8 @@ class Terminal extends Component { runCommand: this.runCommand.bind(this, instance), setPromptPrefix: this.setPromptPrefix.bind(this, instance), getPluginMethod: this.getPluginMethod.bind(this, instance), + getData: () => this.getPluginData(PluginClass.name), + setData: data => this.setPluginData(PluginClass.name, data), }); pluginMethods[PluginClass.name] = { @@ -274,6 +290,7 @@ class Terminal extends Component { }; } + // Toggle a state boolean toggleState = name => () => this.setState({ [name]: !this.state[name] }); // Prepare the built-in commands @@ -293,6 +310,7 @@ class Terminal extends Component { }); Object.keys(commands).forEach((name) => { + let needsInstance = false; const definition = commands[name]; let method = definition; let parse = i => i; @@ -312,11 +330,13 @@ class Terminal extends Component { version: false, }); method = definition.method; + needsInstance = definition.needsInstance || false; } commands[name] = { parse, method, + needsInstance, }; }); this.setState({ commands }); @@ -344,8 +364,8 @@ class Terminal extends Component { }; // Refresh or clear the screen - clearScreen = function clearScreen() { - this.setState({ summary: [] }); + clearScreen = (args, printLine, runCommand, instance) => { + instance.setState({ summary: [] }); }; // Method to check for shortcut and invoking commands @@ -379,14 +399,14 @@ class Terminal extends Component { }; // edit-line command - editLine = function editLine(args) { - const { summary } = this.state; + editLine = (args, printLine, runCommand, instance) => { + const { summary } = instance.state; let index = args.line; if (index === -1) { index = summary.length === 0 ? 0 : summary.length - 1; } summary[index] = args._.join(' '); - this.setState({ summary }); + instance.setState({ summary }); }; // Listen for user input @@ -467,7 +487,7 @@ class Terminal extends Component { console.error(`Error loading plugin ${plugin.name}`, e); // eslint-disable-line no-console } }); - this.setState({ pluginData }); + this.pluginData = pluginData; }; // Plugin api method to get a public plugin method @@ -517,7 +537,17 @@ class Terminal extends Component { const inputArray = inputText.split(' '); const input = inputArray[0]; const args = inputArray; // Undefined for function call - const command = this.state.commands[input]; + const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + let commands = { ...this.state.commands }; + if (instanceData) { + instanceData.pluginInstances.forEach((i) => { + commands = { + ...commands, + ...i.commands, + }; + }); + } + const command = commands[input]; let res; if (input === '') { @@ -536,10 +566,11 @@ class Terminal extends Component { const parsedArgs = command.parse(args); const type = typeof parsedArgs; if (type !== 'object' || (type === 'object' && !parsedArgs.help)) { - res = command.method.bind(instance)( + res = command.method( parsedArgs, this.printLine.bind(this, instance), this.runCommand.bind(this, instance), + command.needsInstance === true ? instance : undefined, ); } } @@ -556,9 +587,24 @@ class Terminal extends Component { }; // List all the commands (state + user defined) - showHelp = (args, printLine) => { - const options = Object.keys(this.state.commands); - const descriptions = this.state.descriptions; + showHelp = (args, printLine, runCommand, instance) => { + let commands = { ...this.state.commands }; + let descriptions = { ...this.state.descriptions }; + const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + if (instanceData) { + instanceData.pluginInstances.forEach((i) => { + commands = { + ...commands, + ...i.commands, + }; + descriptions = { + ...descriptions, + ...i.descriptions, + }; + }); + } + const options = Object.keys(commands); + for (const option of options) { // eslint-disable-line no-restricted-syntax if (descriptions[option] !== false) { From fb7cffd511648d453c764d4e3c07ca95401217db Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 19 Jul 2017 09:56:47 -0500 Subject: [PATCH 05/19] update to use displayName attribute --- src/js/components/Plugin.js | 2 +- src/js/components/Terminal.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index f91d944..5c3d9c9 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -1,5 +1,5 @@ export default class Plugin { - static name = ''; + static displayName = ''; static version = '1.0.0'; static defaultData = ''; diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 28f05e9..56599a1 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -264,18 +264,18 @@ class Terminal extends Component { runCommand: this.runCommand.bind(this, instance), setPromptPrefix: this.setPromptPrefix.bind(this, instance), getPluginMethod: this.getPluginMethod.bind(this, instance), - getData: () => this.getPluginData(PluginClass.name), - setData: data => this.setPluginData(PluginClass.name, data), + getData: () => this.getPluginData(PluginClass.displayName), + setData: data => this.setPluginData(PluginClass.displayName, data), }); - pluginMethods[PluginClass.name] = { + pluginMethods[PluginClass.displayName] = { ...plugin.getPublicMethods(), - _getName: () => PluginClass.name, + _getName: () => PluginClass.displayName, _getVersion: () => PluginClass.version, }; - pluginInstances[PluginClass.name] = plugin; + pluginInstances[PluginClass.displayName] = plugin; } catch (e) { - console.error(`Error instantiating plugin ${PluginClass.name}`, e); // eslint-disable-line no-console + console.error(`Error instantiating plugin ${PluginClass.displayName}`, e); // eslint-disable-line no-console } }); @@ -482,9 +482,9 @@ class Terminal extends Component { const pluginData = {}; this.props.plugins.forEach((plugin) => { try { - pluginData[plugin.name] = plugin.defaultData; + pluginData[plugin.displayName] = plugin.defaultData; } catch (e) { - console.error(`Error loading plugin ${plugin.name}`, e); // eslint-disable-line no-console + console.error(`Error loading plugin ${plugin.displayName}`, e); // eslint-disable-line no-console } }); this.pluginData = pluginData; From dd8d98186865da6aebc78bbb8134a0b6a8ad1088 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Thu, 20 Jul 2017 17:43:29 -0500 Subject: [PATCH 06/19] base for tabs --- src/js/components/Content.js | 14 ++- src/js/components/Plugin.js | 2 + src/js/components/Tabs.js | 71 +++++++++++++++ src/js/components/Terminal.js | 163 ++++++++++++++++++++++++---------- src/js/components/types.js | 1 + src/styles/Tabs.scss | 33 +++++++ src/styles/index.scss | 5 +- 7 files changed, 240 insertions(+), 49 deletions(-) create mode 100644 src/js/components/Tabs.js create mode 100644 src/styles/Tabs.scss diff --git a/src/js/components/Content.js b/src/js/components/Content.js index 33cd561..ca2aa3b 100644 --- a/src/js/components/Content.js +++ b/src/js/components/Content.js @@ -5,6 +5,7 @@ class Content extends Component { static displayName = 'Content'; static propTypes = { + oldData: PropTypes.object, // eslint-disable-line backgroundColor: PropTypes.objectOf(PropTypes.string), prompt: PropTypes.objectOf(PropTypes.string), inputStyles: PropTypes.objectOf(PropTypes.string), @@ -13,6 +14,10 @@ class Content extends Component { handlerKeyPress: PropTypes.func.isRequired, }; + static defaultProps = { + oldData: {}, + }; + static contextTypes = { symbol: PropTypes.string, maximise: PropTypes.bool, @@ -27,9 +32,16 @@ class Content extends Component { keyInputs: [], }; + componentWillMount = () => { + this.setState(this.props.oldData); + }; + componentDidMount = () => { this.focusInput(); this.unregister = this.props.register(this); + if (Object.keys(this.props.oldData).length === 0) { + this.handleChange({ target: { value: 'show' }, key: 'Enter', dontShowCommand: true }); + } }; // Adjust scrolling @@ -40,7 +52,7 @@ class Content extends Component { }; componentWillUnmount() { - this.unregister(); + this.unregister(this.state); } focusInput = () => { diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index 5c3d9c9..36ca011 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -12,6 +12,8 @@ export default class Plugin { this.descriptions = {}; } + updateApi = api => (this.api = api); + getPublicMethods = () => ({}); readStdOut = () => true; diff --git a/src/js/components/Tabs.js b/src/js/components/Tabs.js new file mode 100644 index 0000000..bcbf4e2 --- /dev/null +++ b/src/js/components/Tabs.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; // eslint-disable-line +import PropTypes from 'prop-types'; + +function last(arr) { + return arr[arr.length - 1] || 'bash'; +} + +class Tabs extends Component { + static displayName = 'Tabs'; + + static propTypes = { + style: PropTypes.object, // eslint-disable-line + active: PropTypes.string, + setActiveTab: PropTypes.func, + removeTab: PropTypes.func, + createTab: PropTypes.func, + }; + + static defaultProps = { + style: {}, + }; + + static contextTypes = { + instances: PropTypes.array, + maximise: PropTypes.bool, + }; + + handleBarClick = (e) => { + e.stopPropagation(); + this.props.createTab(); + }; + + // handle clicking a tab + handleTabClick = (e, index) => { + e.stopPropagation(); + this.props.setActiveTab(index); + }; + + handleRemoveClick = (e, index) => { + e.stopPropagation(); + this.props.removeTab(index); + }; + + render() { + const { style, active } = this.props; + const tabs = this.context.instances.map(data => ( +
this.handleTabClick(e, data.index)} + > + {(data.instance && data.instance.state) ? last(data.instance.state.summary) : 'bash'} +
+ )); + + return ( +
+ {tabs} +
+ ); + } +} + +export default Tabs; diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 56599a1..cd59494 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -12,6 +12,15 @@ import { } from './types'; import Bar from './Bar'; import Content from './Content'; +import Tabs from './Tabs'; + +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; // eslint-disable-line no-bitwise + const v = c === 'x' ? r : (r & 0x3 | 0x8); // eslint-disable-line + return v.toString(16); + }); +} const os = getOs(); @@ -42,8 +51,6 @@ class Terminal extends Component { constructor(props) { super(props); - this.instances = []; - this.pluginData = {}; this.defaultCommands = { @@ -98,10 +105,14 @@ class Terminal extends Component { minimise: false, maximise: false, shortcuts: {}, + activeTab: '', + tabs: [], + instances: [], }; getChildContext() { return { + instances: this.state.instances, symbol: this.state.prompt, show: this.state.show, minimise: this.state.minimise, @@ -120,22 +131,27 @@ class Terminal extends Component { // Prepare the symbol componentWillMount = () => { - this.setState({ prompt: this.props.promptSymbol }); - }; - - // Load everything! - componentDidMount = () => { this.loadPlugins(); this.assembleCommands(); this.setDescriptions(); this.setShortcuts(); - this.showMsg({}, this.printLine.bind(this, this.instances[0].instance)); + this.createTab(); + this.setState({ prompt: this.props.promptSymbol }); + }; + + // Load everything! + componentDidMount = () => { if (this.props.watchConsoleLogging) { this.watchConsoleLogging(); } }; + // Tab creation + createTab = () => { + this.setState({ activeTab: uuidv4() }); + } + // Show the content on toggling getAppContent = () => { const { show, minimise } = this.state; @@ -150,24 +166,36 @@ class Terminal extends Component { // Shows the full window (normal window) getContent = () => { - const { backgroundColor, color, style, barColor, prompt } = this.props; + const { color, style, barColor, backgroundColor, prompt } = this.props; + const { activeTab, instances } = this.state; + const barColorStyles = { backgroundColor: barColor }; const inputStyles = { backgroundColor, color }; const promptStyles = { color: prompt }; - const barColorStyles = { backgroundColor: barColor }; const backgroundColorStyles = { backgroundColor }; + const data = instances.find(i => i.index === activeTab); + return (
- + {[( + this.registerInstance(activeTab, ...args)} + /> + )]}
); }; @@ -234,6 +262,10 @@ class Terminal extends Component { instance.setState({ promptPrefix }); }; + setActiveTab = (activeTab) => { + this.setState({ activeTab }); + }; + // Hide window setFalse = name => () => this.setState({ [name]: false }); @@ -253,40 +285,70 @@ class Terminal extends Component { }; // Used to keep track of all instances - registerInstance = (instance) => { + registerInstance = (index, instance) => { + const { instances } = this.state; const pluginInstances = {}; const pluginMethods = {}; + const old = instances.find(i => i.index === index); + this.props.plugins.forEach((PluginClass) => { try { - const plugin = new PluginClass({ + const api = { printLine: this.printLine.bind(this, instance), runCommand: this.runCommand.bind(this, instance), setPromptPrefix: this.setPromptPrefix.bind(this, instance), getPluginMethod: this.getPluginMethod.bind(this, instance), getData: () => this.getPluginData(PluginClass.displayName), setData: data => this.setPluginData(PluginClass.displayName, data), - }); - - pluginMethods[PluginClass.displayName] = { - ...plugin.getPublicMethods(), - _getName: () => PluginClass.displayName, - _getVersion: () => PluginClass.version, }; + + let plugin; + if (old) { + old.pluginInstances[PluginClass.displayName].updateApi(api); + } else { + plugin = new PluginClass(api); + + pluginMethods[PluginClass.displayName] = { + ...plugin.getPublicMethods(), + _getName: () => PluginClass.displayName, + _getVersion: () => PluginClass.version, + }; + } + pluginInstances[PluginClass.displayName] = plugin; } catch (e) { console.error(`Error instantiating plugin ${PluginClass.displayName}`, e); // eslint-disable-line no-console } }); - this.instances.push({ + const data = { + index, instance, - pluginMethods, - pluginInstances, - }); + pluginMethods: old ? old.pluginMethods : pluginMethods, + pluginInstances: old ? old.pluginInstances : pluginInstances, + }; - return () => { - this.instances = this.instances.filter(i => !isEqual(i.instance, instance)); + if (old) { + const realIndex = instances.indexOf(old); + instances[realIndex] = data; + } else { + instances.push(data); + } + + this.setState({ instances }); + + return (oldData = {}) => { + const insts = this.state.instances; + this.setState({ + instances: insts.map((i) => { + if (isEqual(i.instance, instance)) { + i.instance = null; + i.oldData = oldData; + } + return i; + }), + }); }; } @@ -413,10 +475,12 @@ class Terminal extends Component { handleChange = (instance, e) => { const { input, promptPrefix, history } = instance.state; if (e.key === 'Enter' && !e.shiftKey) { - this.printLine.bind(this, instance)( - `${promptPrefix}${this.state.prompt} ${e.target.value}`, - false, - ); + if (typeof e.dontShowCommand === 'undefined') { + this.printLine.bind(this, instance)( + `${promptPrefix}${this.state.prompt} ${e.target.value}`, + false, + ); + } const res = this.runCommand( instance, @@ -492,7 +556,7 @@ class Terminal extends Component { // Plugin api method to get a public plugin method getPluginMethod = (instance, name, method) => { - const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + const instanceData = this.state.instances.find(i => isEqual(i.instance, instance)); if (instanceData) { if (instanceData.pluginMethods[name]) { if (instanceData.pluginMethods[name][method]) { @@ -512,7 +576,7 @@ class Terminal extends Component { printLine = (instance, inp, std = true) => { let print = true; if (std) { - const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + const instanceData = this.state.instances.find(i => isEqual(i.instance, instance)); if (instanceData) { const plugins = instanceData.pluginInstances; for (let i = 0; i < plugins.length; i += 1) { @@ -537,10 +601,10 @@ class Terminal extends Component { const inputArray = inputText.split(' '); const input = inputArray[0]; const args = inputArray; // Undefined for function call - const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + const instanceData = this.state.instances.find(i => isEqual(i.instance, instance)); let commands = { ...this.state.commands }; if (instanceData) { - instanceData.pluginInstances.forEach((i) => { + Object.values(instanceData.pluginInstances).forEach((i) => { commands = { ...commands, ...i.commands, @@ -577,22 +641,29 @@ class Terminal extends Component { return res; }; + // Print to active instance + printToActive = (...args) => { + const data = this.state.instances[this.state.activeTab]; + if (data && data.instance !== null) { + this.printLine(data.instance, ...args); + } + } + // Listen for console logging and pass the input to handler (handleLogging) watchConsoleLogging = () => { - // TODO switch to a print to all instances method - handleLogging('log', this.printLine.bind(this, this.instances[0].instance)); - handleLogging('info', this.printLine.bind(this, this.instances[0].instance)); - // handleLogging('warn', this.printLine.bind(this, this.instances[0].instance)); - // handleLogging('error', this.printLine.bind(this, this.instances[0].instance)); + handleLogging('log', this.printToActive); + handleLogging('info', this.printToActive); + // handleLogging('warn', this.printToActive); + // handleLogging('error', this.printToActive); }; // List all the commands (state + user defined) showHelp = (args, printLine, runCommand, instance) => { let commands = { ...this.state.commands }; let descriptions = { ...this.state.descriptions }; - const instanceData = this.instances.find(i => isEqual(i.instance, instance)); + const instanceData = this.state.instances.find(i => isEqual(i.instance, instance)); if (instanceData) { - instanceData.pluginInstances.forEach((i) => { + Object.values(instanceData.pluginInstances).forEach((i) => { commands = { ...commands, ...i.commands, diff --git a/src/js/components/types.js b/src/js/components/types.js index d64209c..8e9891f 100644 --- a/src/js/components/types.js +++ b/src/js/components/types.js @@ -52,6 +52,7 @@ export const TerminalPropTypes = { // } export const TerminalContextTypes = { + instances: PropTypes.array, symbol: PropTypes.string, show: PropTypes.bool, minimise: PropTypes.bool, diff --git a/src/styles/Tabs.scss b/src/styles/Tabs.scss new file mode 100644 index 0000000..e406502 --- /dev/null +++ b/src/styles/Tabs.scss @@ -0,0 +1,33 @@ +.terminal-tab-bar { + height: 30px; + max-width: 600px; + transition: all 0.4s ease-out; + background: #222; +} + +.adjust-tab-bar { + display: block; + margin: 0 auto; +} + +.terminal-tab { + display: inline-block; + vertical-align: top; + height: 30px; + background-color: #333; + border-bottom: 2px solid #333; + text-align: center; + line-height: 30px; + min-width: 50px; + max-width: 150px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 3px; + padding-right: 3px; + cursor: pointer; +} + +.terminal-tab-active { + border-bottom-color: #777; +} diff --git a/src/styles/index.scss b/src/styles/index.scss index aa1d638..0ebea6b 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,2 +1,3 @@ -@import 'Bar'; -@import 'Terminal'; +@import "Bar"; +@import "Tabs"; +@import "Terminal"; From 1e375e2766edb9a2c06686696a508c7082cc75a4 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 19:11:41 -0500 Subject: [PATCH 07/19] tab styles and top bar overrides --- src/js/components/Bar.js | 66 ++++++++++------ src/js/components/Content.js | 31 ++++++-- src/js/components/Tabs.js | 72 +++++++++++++---- src/js/components/Terminal.js | 145 ++++++++++++++++++++++------------ src/js/components/types.js | 16 ++++ src/styles/Bar.scss | 7 +- src/styles/Tabs.scss | 57 +++++++++++-- starter/App.js | 6 +- 8 files changed, 296 insertions(+), 104 deletions(-) diff --git a/src/js/components/Bar.js b/src/js/components/Bar.js index 2a767e7..54eeced 100644 --- a/src/js/components/Bar.js +++ b/src/js/components/Bar.js @@ -5,11 +5,16 @@ class Bar extends Component { static displayName = 'Bar'; static propTypes = { - style: PropTypes.object // eslint-disable-line + style: PropTypes.object, // eslint-disable-line + showActions: PropTypes.bool, + handleMinimise: PropTypes.func, + handleMaximise: PropTypes.func, + handleClose: PropTypes.func, }; static defaultProps = { style: {}, + showActions: true, }; static contextTypes = { @@ -21,45 +26,60 @@ class Bar extends Component { // Close the window handleClose = () => { - this.context.toggleShow(); + if (this.props.handleClose) { + this.props.handleClose(this.context.toggleShow); + } else { + this.context.toggleShow(); + } }; // Minimise the window handleMinimise = () => { - this.context.toggleMinimize(); + if (this.props.handleMinimise) { + this.props.handleMinimise(this.context.toggleMinimize); + } else { + this.context.toggleMinimize(); + } }; // Maximise the window handleMaximise = () => { - this.context.toggleMaximise(); + if (this.props.handleMaximise) { + this.props.handleMaximise(this.context.toggleMaximise); + } else { + this.context.toggleMaximise(); + } }; render() { + const { style, showActions } = this.props; return (
- - - - - + { showActions && ( + + + + + + ) }
); } diff --git a/src/js/components/Content.js b/src/js/components/Content.js index ca2aa3b..7c5c091 100644 --- a/src/js/components/Content.js +++ b/src/js/components/Content.js @@ -5,6 +5,7 @@ class Content extends Component { static displayName = 'Content'; static propTypes = { + id: PropTypes.string, oldData: PropTypes.object, // eslint-disable-line backgroundColor: PropTypes.objectOf(PropTypes.string), prompt: PropTypes.objectOf(PropTypes.string), @@ -21,6 +22,10 @@ class Content extends Component { static contextTypes = { symbol: PropTypes.string, maximise: PropTypes.bool, + instances: PropTypes.array, + activeTab: PropTypes.string, + barShowing: PropTypes.bool, + tabsShowing: PropTypes.bool, }; state = { @@ -33,13 +38,17 @@ class Content extends Component { }; componentWillMount = () => { - this.setState(this.props.oldData); + const data = this.context.instances.find(i => i.index === this.props.id); + if (data) { + this.setState(data.oldData); + } }; componentDidMount = () => { this.focusInput(); + const data = this.context.instances.find(i => i.index === this.props.id); this.unregister = this.props.register(this); - if (Object.keys(this.props.oldData).length === 0) { + if (!data || Object.keys(data.oldData).length === 0) { this.handleChange({ target: { value: 'show' }, key: 'Enter', dontShowCommand: true }); } }; @@ -70,8 +79,12 @@ class Content extends Component { } render() { - const { prompt, inputStyles, backgroundColor } = this.props; - const { symbol, maximise } = this.context; + const { prompt, inputStyles, backgroundColor, id } = this.props; + const { symbol, maximise, activeTab, barShowing, tabsShowing } = this.context; + + if (id !== activeTab) { + return null; + } const output = this.state.summary.map((content, i) => { if (typeof content === 'string' && content.length === 0) { @@ -80,13 +93,21 @@ class Content extends Component { return
{content}
; }); + let toSubtract = 30; + if (!barShowing) { + toSubtract -= 30; + } + if (tabsShowing) { + toSubtract += 30; + } + return (
2 ? `${arr[arr.length - 2]}` : ''; + if (base.indexOf(`${pre}> ` !== 0)) { + base = 'bash'; + } + return base.replace(`${pre}> `, ''); } class Tabs extends Component { @@ -25,6 +29,10 @@ class Tabs extends Component { maximise: PropTypes.bool, }; + state = { + showingPlus: false, + }; + handleBarClick = (e) => { e.stopPropagation(); this.props.createTab(); @@ -32,26 +40,57 @@ class Tabs extends Component { // handle clicking a tab handleTabClick = (e, index) => { + e.preventDefault(); e.stopPropagation(); this.props.setActiveTab(index); }; - handleRemoveClick = (e, index) => { + // handle remove clicked + handleRemoveClick = (e, index, instance) => { + e.preventDefault(); e.stopPropagation(); - this.props.removeTab(index); + this.props.removeTab(index, instance.props.id); + return false; }; + removePlus = () => { + if (this.state.showingPlus) { + this.setState({ showingPlus: false }); + } + } + + showPlus = () => { + if (!this.state.showingPlus) { + this.setState({ showingPlus: true }); + } + } + render() { + const { showingPlus } = this.state; const { style, active } = this.props; - const tabs = this.context.instances.map(data => ( -
this.handleTabClick(e, data.index)} - > - {(data.instance && data.instance.state) ? last(data.instance.state.summary) : 'bash'} -
- )); + const tabs = this.context.instances.map(({ index, instance }) => { + const title = (instance && instance.state) ? last(instance.state.summary, instance.state.promptPrefix) : 'bash'; + return ( +
this.handleTabClick(e, index)} + onFocus={e => this.handleTabClick(e, index)} + onMouseEnter={this.removePlus} + title={title} + tabIndex={0} + > + {this.context.instances.length > 1 && ( +
this.handleRemoveClick(e, index, instance)} + >x
+ ) } + {title} +
+ ); + }); return (
{tabs} +
+
); } diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index cd59494..a407715 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -88,6 +88,9 @@ class Terminal extends Component { }; this.defaultShortcuts = { + 'win, linux, darwin': { + 'alt + t': this.createTab, + }, 'win, linux': { 'ctrl + l': 'clear', }, @@ -95,20 +98,20 @@ class Terminal extends Component { 'cmd + k': 'clear', }, }; - } - state = { - prompt: '>', - commands: {}, - descriptions: {}, - show: true, - minimise: false, - maximise: false, - shortcuts: {}, - activeTab: '', - tabs: [], - instances: [], - }; + this.state = { + prompt: '>', + commands: {}, + descriptions: {}, + show: props.startState !== 'closed', + minimise: props.startState === 'minimised', + maximise: props.startState === 'maximised', + shortcuts: {}, + activeTab: '', + tabs: [], + instances: [], + }; + } getChildContext() { return { @@ -117,6 +120,9 @@ class Terminal extends Component { show: this.state.show, minimise: this.state.minimise, maximise: this.state.maximise, + activeTab: this.state.activeTab, + barShowing: !this.props.hideTopBar, + tabsShowing: this.props.allowTabs, openWindow: this.setTrue('show'), closeWindow: this.setFalse('show'), minimiseWindow: this.setTrue('minimise'), @@ -149,7 +155,37 @@ class Terminal extends Component { // Tab creation createTab = () => { - this.setState({ activeTab: uuidv4() }); + const { color, backgroundColor, prompt, allowTabs } = this.props; + if (allowTabs) { + const { tabs } = this.state; + const id = uuidv4(); + + const inputStyles = { backgroundColor, color }; + const promptStyles = { color: prompt }; + const backgroundColorStyles = { backgroundColor }; + + tabs.push(( + this.registerInstance(id, ...args)} + /> + )); + + this.setState({ activeTab: id, tabs }); + } + } + + // Tab removal + removeTab = (index) => { + const { tabs } = this.state; + tabs.splice(index, 1); + this.setState({ tabs }); } // Show the content on toggling @@ -166,48 +202,49 @@ class Terminal extends Component { // Shows the full window (normal window) getContent = () => { - const { color, style, barColor, backgroundColor, prompt } = this.props; - const { activeTab, instances } = this.state; + const { + color, + style, + barColor, + showActions, + hideTopBar, + allowTabs, + actionHandlers, + } = this.props; + const { activeTab, tabs } = this.state; const barColorStyles = { backgroundColor: barColor }; - const inputStyles = { backgroundColor, color }; - const promptStyles = { color: prompt }; - const backgroundColorStyles = { backgroundColor }; - - const data = instances.find(i => i.index === activeTab); return (
- - - {[( - this.registerInstance(activeTab, ...args)} + {!hideTopBar && ( + + )} + {allowTabs && ( + - )]} + )} + {tabs}
); }; // Show only bar (minimise) getBar = () => { - const { color, barColor, style } = this.props; + const { color, barColor, style, showActions, actionHandlers } = this.props; const barColorStyles = { backgroundColor: barColor }; return (
- +
); }; @@ -262,6 +299,7 @@ class Terminal extends Component { instance.setState({ promptPrefix }); }; + // Set the currently active tab setActiveTab = (activeTab) => { this.setState({ activeTab }); }; @@ -338,16 +376,10 @@ class Terminal extends Component { this.setState({ instances }); - return (oldData = {}) => { + return () => { const insts = this.state.instances; this.setState({ - instances: insts.map((i) => { - if (isEqual(i.instance, instance)) { - i.instance = null; - i.oldData = oldData; - } - return i; - }), + instances: insts.filter(i => !isEqual(i.instance, instance)), }); }; } @@ -431,7 +463,7 @@ class Terminal extends Component { }; // Method to check for shortcut and invoking commands - checkShortcuts = (instance, key) => { + checkShortcuts = (instance, key, e) => { const shortcuts = Object.keys(this.state.shortcuts); if (shortcuts.length > 0) { const { keyInputs } = instance.state; @@ -448,10 +480,19 @@ class Terminal extends Component { .filter(cut => cut[0].length >= keyInputs.length) .filter(cut => isEqual(cut[0].slice(0, len), keyInputs)); + console.log(keyInputs); + if (options.length > 0) { if (options.length === 1 && options[0][0].length === len) { const shortcut = shortcuts[options[0][1]]; - this.runCommand(instance, this.state.shortcuts[shortcut]); + const action = this.state.shortcuts[shortcut]; + if (typeof action === 'string') { + this.runCommand(instance, this.state.shortcuts[shortcut]); + } else if (typeof action === 'function') { + e.preventDefault(); + e.stopPropagation(); + action(); + } instance.setState({ keyInputs: [] }); } } else if (keyInputs.length > 0) { @@ -537,7 +578,7 @@ class Terminal extends Component { break; } } - this.checkShortcuts(instance, key); + this.checkShortcuts(instance, key, e); } // Plugins @@ -643,7 +684,7 @@ class Terminal extends Component { // Print to active instance printToActive = (...args) => { - const data = this.state.instances[this.state.activeTab]; + const data = this.state.instances.find(i => i.index === this.state.activeTab); if (data && data.instance !== null) { this.printLine(data.instance, ...args); } diff --git a/src/js/components/types.js b/src/js/components/types.js index 8e9891f..85beaf5 100644 --- a/src/js/components/types.js +++ b/src/js/components/types.js @@ -18,6 +18,10 @@ export const descriptionsPropType = PropTypes.objectOf(PropTypes.oneOfType([ ])); export const TerminalPropTypes = { + startState: PropTypes.oneOf(['minimised', 'maximised', 'open', 'closed']), + showActions: PropTypes.bool, + hideTopBar: PropTypes.bool, + allowTabs: PropTypes.bool, msg: PropTypes.string, color: PropTypes.string, style: PropTypes.object, // eslint-disable-line @@ -39,6 +43,11 @@ export const TerminalPropTypes = { descriptions: descriptionsPropType, })), shortcuts: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + actionHandlers: PropTypes.shape({ + handleClose: PropTypes.func, + handleMinimise: PropTypes.func, + handleMaximise: PropTypes.func, + }), }; // shortcuts example @@ -52,6 +61,9 @@ export const TerminalPropTypes = { // } export const TerminalContextTypes = { + barShowing: PropTypes.bool, + tabsShowing: PropTypes.bool, + activeTab: PropTypes.string, instances: PropTypes.array, symbol: PropTypes.string, show: PropTypes.bool, @@ -69,6 +81,10 @@ export const TerminalContextTypes = { }; export const TerminalDefaultProps = { + startState: 'open', + hideTopBar: false, + allowTabs: false, + showActions: true, msg: '', color: 'green', style: {}, diff --git a/src/styles/Bar.scss b/src/styles/Bar.scss index 3194f4a..c3492ab 100644 --- a/src/styles/Bar.scss +++ b/src/styles/Bar.scss @@ -3,9 +3,10 @@ max-width: 600px; transition: all 0.4s ease-out; background: black; -} - -.adjust-bar { display: block; margin: 0 auto; + + circle { + cursor: pointer; + } } diff --git a/src/styles/Tabs.scss b/src/styles/Tabs.scss index e406502..e02a4ec 100644 --- a/src/styles/Tabs.scss +++ b/src/styles/Tabs.scss @@ -3,11 +3,13 @@ max-width: 600px; transition: all 0.4s ease-out; background: #222; -} - -.adjust-tab-bar { + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; display: block; margin: 0 auto; + padding-bottom: 15px; + margin-bottom: -14px; } .terminal-tab { @@ -18,16 +20,61 @@ border-bottom: 2px solid #333; text-align: center; line-height: 30px; - min-width: 50px; - max-width: 150px; + width: 100px; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; padding-left: 3px; padding-right: 3px; cursor: pointer; + white-space: pre; + position: relative; } .terminal-tab-active { border-bottom-color: #777; } + +.terminal-tab:focus { + outline: none; +} + +.terminal-tab-close { + position: absolute; + top: 8px; + height: 13px; + line-height: 11px; + right: 3px; + font-size: 11px; + width: 13px; + text-align: center; + color: black; + cursor: pointer; + + &:hover { + color: white; + background-color: black; + border-radius: 50%; + } +} + +.terminal-tab-plus { + display: inline-block; + color: white; + border: 1px solid white; + border-radius: 2px; + width: 13px; + height: 13px; + line-height: 13px; + margin-left: 5px; + margin-top: 8px; + text-align: center; + font-size: 12px; + cursor: pointer; + opacity: 0; + transition: opacity 0.3s; +} + +.terminal-tab-plus-visible { + opacity: 0.7; +} diff --git a/starter/App.js b/starter/App.js index e7c6e72..e58f2e1 100644 --- a/starter/App.js +++ b/starter/App.js @@ -8,6 +8,8 @@ import '../src/styles/index.scss'; // (../lib/css/index.css) or '../src/styles/ const App = () => (
( }, 100 * i); } }, - open: () => window.open('https://www.nitintulswani.surge.sh', '_blank') + open: () => window.open('https://www.nitintulswani.surge.sh', '_blank'), }} descriptions={{ color: 'option for color. For eg - color red', 'type-text': 'Types out input text', - open: 'Open a website' + open: 'Open a website', }} shortcuts={{ 'darwin,win,linux': { From a4e604b2df669cace053a115af119d84dc48d245 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 21:11:08 -0500 Subject: [PATCH 08/19] update docs --- README.md | 46 +++++++++++++++++++++++++++++++++++--- src/js/components/types.js | 2 +- starter/App.js | 1 - todo.md | 2 +- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b5a3482..5080cca 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,14 @@ class App extends Component { render() { return ( -
+

-## Using plugins 🔥 +## Using plugins 🔥 [WIP] We have also developed a plugin system for the `` component which helps you develop custom plugins. Here is one example of plugin which creates a fake file system called [terminal-in-react-pseudo-file-system-plugin](https://github.com/jcgertig/terminal-in-react-pseudo-file-system-plugin). @@ -323,6 +330,31 @@ You can mix and match The value of the shortcut should be a command to run. + +## Override the top bar buttons actionHandlers + +Use the prop `actionHandlers`. + +The object allows for 3 methods `handleClose`, `handleMaximise`, `handleMinimise`; + +Each one is a function and will pass in the default method as the first param. +Any method not passed in will use the default. + +```jsx + { + // do something on close + toggleClose(); + }, + handleMaximise: (toggleMaximise) => { + // do something on maximise + toggleMaximise(); + } + }} +/> +``` + ## Customization Use @@ -331,6 +363,9 @@ Use * prop `backgroundColor` to change the background. * prop `barColor` to change the color of bar. * prop `prompt` to change the prompt (`>`) color. +* prop `showActions` to change if the three circles are shown. +* prop `hideTopBar` to hide the top bar altogether. +* prop `allowTabs` to allow multiple tabs. Follow me on Twitter [@NTulswani](https://twitter.com/NTulswani) for new updates and progress 😄 @@ -351,6 +386,11 @@ Follow me on Twitter [@NTulswani](https://twitter.com/NTulswani) for new updates | **commandPassThrough** | function | null | | **promptSymbol** | string | > | | **plugins** | array | [ { name: '', load: new Plugin(), commands: {} descriptions: {} } ] | +| **startState** | string ['open', 'maximised', 'minimised', 'closed'] | 'open' | +| **showActions** | bool | true | +| **hideTopBar** | bool | false | +| **allowTabs** | bool | true | +| **actionHandlers** | object | undefined | ## Built-in commands @@ -369,7 +409,7 @@ Follow me on Twitter [@NTulswani](https://twitter.com/NTulswani) for new updates ## You want a X feature -Sure! Check our [todolist](./todo.md) or create an issue and I will look into it. +Sure! Check our [todolist](./todo.md) or create an issue and I will look into it. ## Contributing diff --git a/src/js/components/types.js b/src/js/components/types.js index 85beaf5..cf6f208 100644 --- a/src/js/components/types.js +++ b/src/js/components/types.js @@ -83,7 +83,7 @@ export const TerminalContextTypes = { export const TerminalDefaultProps = { startState: 'open', hideTopBar: false, - allowTabs: false, + allowTabs: true, showActions: true, msg: '', color: 'green', diff --git a/starter/App.js b/starter/App.js index e58f2e1..f6d49fe 100644 --- a/starter/App.js +++ b/starter/App.js @@ -9,7 +9,6 @@ const App = () => (
Date: Mon, 24 Jul 2017 21:16:15 -0500 Subject: [PATCH 09/19] remove console log --- src/js/components/Terminal.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index a407715..d74461e 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -480,8 +480,6 @@ class Terminal extends Component { .filter(cut => cut[0].length >= keyInputs.length) .filter(cut => isEqual(cut[0].slice(0, len), keyInputs)); - console.log(keyInputs); - if (options.length > 0) { if (options.length === 1 && options[0][0].length === len) { const shortcut = shortcuts[options[0][1]]; From 792984797e45b688a145c3be6cac4a9a4cd5cdd0 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 21:19:19 -0500 Subject: [PATCH 10/19] fix title --- src/js/components/Tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/Tabs.js b/src/js/components/Tabs.js index 055449e..f9c3f7f 100644 --- a/src/js/components/Tabs.js +++ b/src/js/components/Tabs.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; function last(arr, pre = '') { let base = arr.length > 2 ? `${arr[arr.length - 2]}` : ''; - if (base.indexOf(`${pre}> ` !== 0)) { + if (base.indexOf(`${pre}> `) !== 0) { base = 'bash'; } return base.replace(`${pre}> `, ''); From 2f86d03e320f5647e92d64b421bc185e8bddcfc7 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 21:29:30 -0500 Subject: [PATCH 11/19] update title --- src/js/components/Tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/Tabs.js b/src/js/components/Tabs.js index f9c3f7f..5a9b22e 100644 --- a/src/js/components/Tabs.js +++ b/src/js/components/Tabs.js @@ -6,7 +6,7 @@ function last(arr, pre = '') { if (base.indexOf(`${pre}> `) !== 0) { base = 'bash'; } - return base.replace(`${pre}> `, ''); + return base.replace(`${pre}> `, '').split(' ')[0]; } class Tabs extends Component { From 5ebbed51b3b084197573c24bcb745f01704daecd Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 21:45:11 -0500 Subject: [PATCH 12/19] clean up tab styles --- src/js/components/Tabs.js | 16 ++++++++++------ src/styles/Tabs.scss | 14 ++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/js/components/Tabs.js b/src/js/components/Tabs.js index 5a9b22e..fdec31c 100644 --- a/src/js/components/Tabs.js +++ b/src/js/components/Tabs.js @@ -31,6 +31,7 @@ class Tabs extends Component { state = { showingPlus: false, + mouseOver: false, }; handleBarClick = (e) => { @@ -76,7 +77,6 @@ class Tabs extends Component { className={`terminal-tab${active === index ? ' terminal-tab-active' : ''}`} onClick={e => this.handleTabClick(e, index)} onFocus={e => this.handleTabClick(e, index)} - onMouseEnter={this.removePlus} title={title} tabIndex={0} > @@ -99,14 +99,18 @@ class Tabs extends Component { ...(this.context.maximise ? { maxWidth: '100%' } : {}), }} className="terminal-tab-bar" - onClick={this.handleBarClick} - onMouseEnter={this.showPlus} - onMouseLeave={this.removePlus} > {tabs}
+
+ className="terminal-tab-bar-empty" + onMouseEnter={this.showPlus} + onMouseLeave={this.removePlus} + > +
+
+
); } diff --git a/src/styles/Tabs.scss b/src/styles/Tabs.scss index e02a4ec..b1d13c1 100644 --- a/src/styles/Tabs.scss +++ b/src/styles/Tabs.scss @@ -3,13 +3,8 @@ max-width: 600px; transition: all 0.4s ease-out; background: #222; - overflow-x: auto; - overflow-y: hidden; - white-space: nowrap; - display: block; + display: flex; margin: 0 auto; - padding-bottom: 15px; - margin-bottom: -14px; } .terminal-tab { @@ -78,3 +73,10 @@ .terminal-tab-plus-visible { opacity: 0.7; } + +.terminal-tab-bar-empty { + display: inline-block; + min-width: 25px; + height: 100%; + flex: 1; +} From 7419e44e07ba3296f5936af17ac3852ede09bf25 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Mon, 24 Jul 2017 22:38:48 -0500 Subject: [PATCH 13/19] updates to show msg --- src/js/components/Terminal.js | 4 +++- starter/App.js | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index d74461e..283f630 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -725,7 +725,9 @@ class Terminal extends Component { // Show the msg (prop msg) showMsg = (args, printLine) => { - printLine(this.props.msg); + if (this.props.msg && this.props.msg.length > 0) { + printLine(this.props.msg); + } }; render() { diff --git a/starter/App.js b/starter/App.js index f6d49fe..303927e 100644 --- a/starter/App.js +++ b/starter/App.js @@ -9,7 +9,6 @@ const App = () => (
{ From 4e9726fd7dc0e1581fdc8dd8698b805e0b4c46fb Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Tue, 25 Jul 2017 00:17:57 -0500 Subject: [PATCH 14/19] clean up some plugin issues --- src/js/components/Terminal.js | 71 +++++++++++++++++++---------------- src/js/components/types.js | 7 +--- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 283f630..948d063 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -39,6 +39,43 @@ function getShortcuts(shortcuts, obj) { return shortcuts; } +function modCommands(commands) { + const newCommands = {}; + + Object.keys(commands).forEach((name) => { + let needsInstance = false; + const definition = commands[name]; + let method = definition; + let parse = i => i; + if (typeof definition === 'object') { + const cmd = new Command(); + if (typeof definition.options !== 'undefined') { + try { + cmd.options(definition.options); + } catch (e) { + throw new Error('options for command wrong format'); + } + } + parse = i => + cmd.parse(i, { + name, + help: true, + version: false, + }); + method = definition.method; + needsInstance = definition.needsInstance || false; + } + + newCommands[name] = { + parse, + method, + needsInstance, + }; + }); + + return newCommands; +} + class Terminal extends Component { static displayName = 'Terminal'; @@ -403,37 +440,7 @@ class Terminal extends Component { } }); - Object.keys(commands).forEach((name) => { - let needsInstance = false; - const definition = commands[name]; - let method = definition; - let parse = i => i; - if (typeof definition === 'object') { - const cmd = new Command(); - if (typeof definition.options !== 'undefined') { - try { - cmd.options(definition.options); - } catch (e) { - throw new Error('options for command wrong format'); - } - } - parse = i => - cmd.parse(i, { - name, - help: true, - version: false, - }); - method = definition.method; - needsInstance = definition.needsInstance || false; - } - - commands[name] = { - parse, - method, - needsInstance, - }; - }); - this.setState({ commands }); + this.setState({ commands: modCommands(commands) }); }; /** @@ -646,7 +653,7 @@ class Terminal extends Component { Object.values(instanceData.pluginInstances).forEach((i) => { commands = { ...commands, - ...i.commands, + ...modCommands(i.commands), }; }); } diff --git a/src/js/components/types.js b/src/js/components/types.js index cf6f208..797418e 100644 --- a/src/js/components/types.js +++ b/src/js/components/types.js @@ -36,12 +36,7 @@ export const TerminalPropTypes = { PropTypes.bool, ]), promptSymbol: PropTypes.string, - plugins: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string.isRequired, - load: PropTypes.func, - commands: commandsPropType, - descriptions: descriptionsPropType, - })), + plugins: PropTypes.arrayOf(PropTypes.func), shortcuts: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), actionHandlers: PropTypes.shape({ handleClose: PropTypes.func, From 281e668d9e86780aa0678fe3dd93480a58a3235a Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Wed, 26 Jul 2017 11:05:46 -0500 Subject: [PATCH 15/19] clean up plugin system and multi line input --- src/js/components/Plugin.js | 10 ++++------ src/js/components/Terminal.js | 12 +++++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index 36ca011..0d7e0c0 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -10,11 +10,9 @@ export default class Plugin { this.api = api; this.commands = {}; this.descriptions = {}; - } - - updateApi = api => (this.api = api); - getPublicMethods = () => ({}); - - readStdOut = () => true; + this.updateApi = newApi => (this.api = newApi); + this.getPublicMethods = () => ({}); + this.readStdOut = () => true; + } } diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 948d063..b9ca188 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -383,7 +383,6 @@ class Terminal extends Component { old.pluginInstances[PluginClass.displayName].updateApi(api); } else { plugin = new PluginClass(api); - pluginMethods[PluginClass.displayName] = { ...plugin.getPublicMethods(), _getName: () => PluginClass.displayName, @@ -528,10 +527,8 @@ class Terminal extends Component { ); } - const res = this.runCommand( - instance, - `${input.join('\n')}${input.length > 0 ? '\n' : ''}${e.target.value}`, - ); + input.push(e.target.value); + const res = this.runCommand(instance, `${input.join('\n')}`); if (typeof res !== 'undefined') { this.printLine.bind(this, instance)(res); @@ -550,7 +547,7 @@ class Terminal extends Component { false, ); const newHistory = [...history, e.target.value]; - this.setState({ + instance.setState({ input: [...input, e.target.value], history: newHistory, historyCounter: newHistory.length, @@ -608,8 +605,9 @@ class Terminal extends Component { if (instanceData.pluginMethods[name][method]) { return instanceData.pluginMethods[name][method]; } + console.log(instanceData.pluginMethods[name]); throw new Error( - `No method with name ${name} has been registered for plugin ${name}`, + `No method with name ${method} has been registered for plugin ${name}`, ); } else { throw new Error(`No plugin with name ${name} has been registered`); From 0fd0442a0d4980be39efd4085fc11cf35d7640b5 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Thu, 27 Jul 2017 12:26:39 -0500 Subject: [PATCH 16/19] clean up styles so that text can be selected --- src/js/components/Content.js | 17 ++++++++++++++++- src/js/components/Terminal.js | 1 - src/styles/Terminal.scss | 5 +++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/js/components/Content.js b/src/js/components/Content.js index 7c5c091..b038216 100644 --- a/src/js/components/Content.js +++ b/src/js/components/Content.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; // eslint-disable-line import PropTypes from 'prop-types'; +import whatkey, { unprintableKeys } from 'whatkey'; class Content extends Component { static displayName = 'Content'; @@ -78,6 +79,19 @@ class Content extends Component { this.props.handlerKeyPress(this, e, this.com); } + handleOuterKeypress = (e) => { + const key = whatkey(e).key; + const actionKeys = ['up', 'down', 'left', 'right', 'enter']; + if (unprintableKeys.indexOf(key) < 0) { + if (document.activeElement !== this.com) { + this.com.focus(); + this.com.value += whatkey(e).char; + } + } else if (actionKeys.indexOf(key) > -1) { + this.com.focus(); + } + } + render() { const { prompt, inputStyles, backgroundColor, id } = this.props; const { symbol, maximise, activeTab, barShowing, tabsShowing } = this.context; @@ -110,7 +124,8 @@ class Content extends Component { ? { maxWidth: '100%', maxHeight: `calc(100% - ${toSubtract}px)` } : {}), }} - onClick={this.focusInput} + tabIndex="0" + onKeyUp={this.handleOuterKeypress} >
diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index b9ca188..0c5d891 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -585,7 +585,6 @@ class Terminal extends Component { // Plugins loadPlugins = () => { - // TODO intance plugins const pluginData = {}; this.props.plugins.forEach((plugin) => { try { diff --git a/src/styles/Terminal.scss b/src/styles/Terminal.scss index 0da663e..f0244a8 100644 --- a/src/styles/Terminal.scss +++ b/src/styles/Terminal.scss @@ -72,6 +72,11 @@ max-height: 600px; height: 100%; overflow: scroll; + position: relative; + + &:focus { + outline: none; + } } .terminal-holder { From 104565be9f1c8543cd2470a418ff4b3ecee6c264 Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Thu, 27 Jul 2017 12:54:08 -0500 Subject: [PATCH 17/19] pass config into plugins --- src/js/components/Plugin.js | 5 ++++- src/js/components/Terminal.js | 22 +++++++++++++++++----- src/js/components/types.js | 8 +++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js index 0d7e0c0..e5d6c3f 100644 --- a/src/js/components/Plugin.js +++ b/src/js/components/Plugin.js @@ -6,8 +6,11 @@ export default class Plugin { static commands = {}; static descriptions = {}; - constructor(api) { + static defaultConfig = {}; + + constructor(api, config = Plugin.defaultConfig) { this.api = api; + this.config = config; this.commands = {}; this.descriptions = {}; diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 0c5d891..479b87c 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -14,6 +14,18 @@ import Bar from './Bar'; import Content from './Content'; import Tabs from './Tabs'; +function pluginMap(plugins, eachHandler) { + return plugins.map((plugin) => { + if (typeof plugin === 'function') { + plugin = { + class: plugin, + config: undefined, + }; + } + return plugin; + }).forEach(pluginObj => eachHandler(pluginObj.class, pluginObj.config)); +} + function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; // eslint-disable-line no-bitwise @@ -313,7 +325,7 @@ class Terminal extends Component { ...this.defaultDesciptions, ...this.props.descriptions, }; - this.props.plugins.forEach((plugin) => { + pluginMap(this.props.plugins, (plugin) => { if (plugin.descriptions) { descriptions = { ...descriptions, @@ -367,7 +379,7 @@ class Terminal extends Component { const old = instances.find(i => i.index === index); - this.props.plugins.forEach((PluginClass) => { + pluginMap(this.props.plugins, (PluginClass, config) => { try { const api = { printLine: this.printLine.bind(this, instance), @@ -382,7 +394,7 @@ class Terminal extends Component { if (old) { old.pluginInstances[PluginClass.displayName].updateApi(api); } else { - plugin = new PluginClass(api); + plugin = new PluginClass(api, config); pluginMethods[PluginClass.displayName] = { ...plugin.getPublicMethods(), _getName: () => PluginClass.displayName, @@ -430,7 +442,7 @@ class Terminal extends Component { ...this.props.commands, }; - this.props.plugins.forEach((plugin) => { + pluginMap(this.props.plugins, (plugin) => { if (plugin.commands) { commands = { ...commands, @@ -586,7 +598,7 @@ class Terminal extends Component { // Plugins loadPlugins = () => { const pluginData = {}; - this.props.plugins.forEach((plugin) => { + pluginMap(this.props.plugins, (plugin) => { try { pluginData[plugin.displayName] = plugin.defaultData; } catch (e) { diff --git a/src/js/components/types.js b/src/js/components/types.js index 797418e..598e9db 100644 --- a/src/js/components/types.js +++ b/src/js/components/types.js @@ -36,7 +36,13 @@ export const TerminalPropTypes = { PropTypes.bool, ]), promptSymbol: PropTypes.string, - plugins: PropTypes.arrayOf(PropTypes.func), + plugins: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + class: PropTypes.func, + config: PropTypes.object, + }), + ])), shortcuts: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), actionHandlers: PropTypes.shape({ handleClose: PropTypes.func, From 304805c876802034529ba24fdb9bf53052adc5fc Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Thu, 27 Jul 2017 13:24:01 -0500 Subject: [PATCH 18/19] add removeLine and os to the plugin api --- src/js/components/Terminal.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js index 479b87c..de7ad5a 100644 --- a/src/js/components/Terminal.js +++ b/src/js/components/Terminal.js @@ -129,11 +129,11 @@ class Terminal extends Component { }; this.defaultDesciptions = { - show: 'show the msg', + show: (props.msg && props.msg.length > 0) ? 'show the msg' : false, clear: 'clear the screen', help: 'list all the commands', - echo: 'output the input', - 'edit-line': 'edit the contents of an output line', + echo: false, + 'edit-line': false, }; this.defaultShortcuts = { @@ -383,11 +383,13 @@ class Terminal extends Component { try { const api = { printLine: this.printLine.bind(this, instance), + removeLine: this.removeLine.bind(this, instance), runCommand: this.runCommand.bind(this, instance), setPromptPrefix: this.setPromptPrefix.bind(this, instance), getPluginMethod: this.getPluginMethod.bind(this, instance), getData: () => this.getPluginData(PluginClass.displayName), setData: data => this.setPluginData(PluginClass.displayName, data), + os, }; let plugin; @@ -521,8 +523,8 @@ class Terminal extends Component { editLine = (args, printLine, runCommand, instance) => { const { summary } = instance.state; let index = args.line; - if (index === -1) { - index = summary.length === 0 ? 0 : summary.length - 1; + if (index < 0) { + index = summary.length === 0 ? 0 : summary.length - index; } summary[index] = args._.join(' '); instance.setState({ summary }); @@ -651,6 +653,13 @@ class Terminal extends Component { } }; + // Remove a line from the summary + removeLine = (instance, lineNumber = -1) => { + const summary = instance.state.summary; + summary.splice(lineNumber, 1); + instance.setState({ summary }); + } + // Execute the commands runCommand = (instance, inputText) => { const inputArray = inputText.split(' '); From dd3c00c2093cc07285e90b4dc8a9d012d6c3041b Mon Sep 17 00:00:00 2001 From: Jonathan Gertig Date: Thu, 27 Jul 2017 17:57:58 -0500 Subject: [PATCH 19/19] update the base app --- starter/App.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/starter/App.js b/starter/App.js index 303927e..d13ab92 100644 --- a/starter/App.js +++ b/starter/App.js @@ -1,10 +1,13 @@ import React from 'react'; // eslint-disable-line import { render } from 'react-dom'; -import PseudoFileSystem from 'terminal-in-react-pseudo-file-system-plugin'; // eslint-disable-line +// import pseudoFileSystemPlugin from 'terminal-in-react-pseudo-file-system-plugin'; // eslint-disable-line +// import NodeEvalPlugin from 'terminal-in-react-node-eval-plugin'; // eslint-disable-line // Bundle generated with npm run build:production ('../lib/js/index') or use '../src/js' import Terminal from '../src/js'; import '../src/styles/index.scss'; // (../lib/css/index.css) or '../src/styles/index.scss' +// const FileSystemPlugin = pseudoFileSystemPlugin(); + const App = () => (
( }, 100 * i); } }, - open: () => window.open('https://www.nitintulswani.surge.sh', '_blank'), + open: () => window.open('http://terminal-in-react.surge.sh', '_blank'), }} descriptions={{ color: 'option for color. For eg - color red', 'type-text': 'Types out input text', - open: 'Open a website', + open: 'Open a terminal website', }} shortcuts={{ 'darwin,win,linux': { 'ctrl + a': 'echo whoo', }, }} + // plugins={[ + // FileSystemPlugin, + // { + // class: NodeEvalPlugin, + // config: { + // filesystem: FileSystemPlugin.displayName, + // }, + // }, + // ]} />
);