diff --git a/src/js/components/Plugin.js b/src/js/components/Plugin.js
index ee68a4d..e5d6c3f 100644
--- a/src/js/components/Plugin.js
+++ b/src/js/components/Plugin.js
@@ -1,14 +1,21 @@
export default class Plugin {
- constructor(name = '', version = '1.0.0') {
- this.name = name;
- this.version = version;
- }
+ static displayName = '';
+ static version = '1.0.0';
+ static defaultData = '';
- load = () => {};
+ static commands = {};
+ static descriptions = {};
- afterLoad = () => {};
+ static defaultConfig = {};
- getPublicMethods = () => ({});
+ constructor(api, config = Plugin.defaultConfig) {
+ this.api = api;
+ this.config = config;
+ this.commands = {};
+ this.descriptions = {};
- readStdOut = () => true;
+ this.updateApi = newApi => (this.api = newApi);
+ this.getPublicMethods = () => ({});
+ this.readStdOut = () => true;
+ }
}
diff --git a/src/js/components/Tabs.js b/src/js/components/Tabs.js
new file mode 100644
index 0000000..fdec31c
--- /dev/null
+++ b/src/js/components/Tabs.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react'; // eslint-disable-line
+import PropTypes from 'prop-types';
+
+function last(arr, pre = '') {
+ let base = arr.length > 2 ? `${arr[arr.length - 2]}` : '';
+ if (base.indexOf(`${pre}> `) !== 0) {
+ base = 'bash';
+ }
+ return base.replace(`${pre}> `, '').split(' ')[0];
+}
+
+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,
+ };
+
+ state = {
+ showingPlus: false,
+ mouseOver: false,
+ };
+
+ handleBarClick = (e) => {
+ e.stopPropagation();
+ this.props.createTab();
+ };
+
+ // handle clicking a tab
+ handleTabClick = (e, index) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.setActiveTab(index);
+ };
+
+ // handle remove clicked
+ handleRemoveClick = (e, index, instance) => {
+ e.preventDefault();
+ e.stopPropagation();
+ 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(({ 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)}
+ title={title}
+ tabIndex={0}
+ >
+ {this.context.instances.length > 1 && (
+
this.handleRemoveClick(e, index, instance)}
+ >x
+ ) }
+ {title}
+
+ );
+ });
+
+ return (
+
+ );
+ }
+}
+
+export default Tabs;
diff --git a/src/js/components/Terminal.js b/src/js/components/Terminal.js
index a2f9df8..de7ad5a 100644
--- a/src/js/components/Terminal.js
+++ b/src/js/components/Terminal.js
@@ -8,21 +8,42 @@ import { handleLogging, getOs } from '../utils';
import {
TerminalPropTypes,
TerminalContextTypes,
- TerminalDefaultProps
+ TerminalDefaultProps,
} from './types';
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
+ const v = c === 'x' ? r : (r & 0x3 | 0x8); // eslint-disable-line
+ return v.toString(16);
+ });
+}
const os = getOs();
function getShortcuts(shortcuts, obj) {
- Object.keys(obj).forEach(key => {
+ Object.keys(obj).forEach((key) => {
const split = key.toLowerCase().replace(/\s/g, '').split(',');
- split.forEach(osName => {
+ split.forEach((osName) => {
if (osName === os) {
shortcuts = {
...shortcuts,
- ...obj[key]
+ ...obj[key],
};
}
});
@@ -30,6 +51,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';
@@ -42,67 +100,78 @@ class Terminal extends Component {
constructor(props) {
super(props);
- this.pluginMethods = {};
+ 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',
description: 'the line you want to edit. -1 is the last line',
init: value => parseInt(value, 10),
- defaultValue: -1
- }
- ]
- }
+ defaultValue: -1,
+ },
+ ],
+ },
};
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 = {
+ 'win, linux, darwin': {
+ 'alt + t': this.createTab,
+ },
'win, linux': {
- 'ctrl + l': 'clear'
+ 'ctrl + l': 'clear',
},
darwin: {
- 'cmd + k': 'clear'
- }
+ 'cmd + k': 'clear',
+ },
};
- }
- state = {
- prompt: '>',
- promptPrefix: '',
- summary: [],
- commands: {},
- descriptions: {},
- history: [],
- historyCounter: 0,
- show: true,
- minimise: false,
- maximise: false,
- input: [],
- shortcuts: {},
- keyInputs: []
- };
+ this.state = {
+ prompt: '>',
+ commands: {},
+ descriptions: {},
+ show: props.startState !== 'closed',
+ minimise: props.startState === 'minimised',
+ maximise: props.startState === 'maximised',
+ shortcuts: {},
+ activeTab: '',
+ tabs: [],
+ instances: [],
+ };
+ }
getChildContext() {
return {
- symbol: this.state.promptPrefix + this.state.prompt,
+ instances: this.state.instances,
+ symbol: this.state.prompt,
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'),
@@ -111,28 +180,63 @@ class Terminal extends Component {
unmaximiseWindow: this.setFalse('maximise'),
toggleShow: this.toggleState('show'),
toggleMaximise: this.toggleState('maximise'),
- toggleMinimize: this.toggleState('minimise')
+ toggleMinimize: this.toggleState('minimise'),
};
}
// Prepare the symbol
componentWillMount = () => {
- this.setState({ prompt: this.props.promptSymbol });
- };
-
- // Load everything!
- componentDidMount = () => {
this.loadPlugins();
this.assembleCommands();
this.setDescriptions();
this.setShortcuts();
- this.showMsg();
+ this.createTab();
+ this.setState({ prompt: this.props.promptSymbol });
+ };
+
+ // Load everything!
+ componentDidMount = () => {
if (this.props.watchConsoleLogging) {
this.watchConsoleLogging();
}
};
+ // Tab creation
+ createTab = () => {
+ 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
getAppContent = () => {
const { show, minimise } = this.state;
@@ -147,49 +251,55 @@ class Terminal extends Component {
// Shows the full window (normal window)
getContent = () => {
- const { backgroundColor, color, style, barColor, prompt } = this.props;
+ const {
+ color,
+ style,
+ barColor,
+ showActions,
+ hideTopBar,
+ allowTabs,
+ actionHandlers,
+ } = this.props;
+ const { activeTab, tabs } = this.state;
- const inputStyles = { backgroundColor, color };
- const promptStyles = { color: prompt };
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 (
-
-
+ {!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 (
-
+
);
};
// Show msg (on window close)
- getNote = () =>
+ getNote = () => (
OOPS! You closed the window.
Click on the icon to reopen.
- ;
+
+ );
+
+ // 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 = {
...this.defaultDesciptions,
- ...this.props.descriptions
+ ...this.props.descriptions,
};
- this.props.plugins.forEach(plugin => {
+ pluginMap(this.props.plugins, (plugin) => {
if (plugin.descriptions) {
descriptions = {
...descriptions,
- ...plugin.descriptions
+ ...plugin.descriptions,
};
}
});
@@ -226,8 +343,14 @@ class Terminal extends Component {
this.setState({ shortcuts });
};
- setPromptPrefix = promptPrefix => {
- this.setState({ promptPrefix });
+ // Setter to change the prefix of the input prompt
+ setPromptPrefix = (instance, promptPrefix) => {
+ instance.setState({ promptPrefix });
+ };
+
+ // Set the currently active tab
+ setActiveTab = (activeTab) => {
+ this.setState({ activeTab });
};
// Hide window
@@ -240,74 +363,111 @@ 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 = (index, instance) => {
+ const { instances } = this.state;
+ const pluginInstances = {};
+ const pluginMethods = {};
+
+ const old = instances.find(i => i.index === index);
+
+ pluginMap(this.props.plugins, (PluginClass, config) => {
+ 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;
+ if (old) {
+ old.pluginInstances[PluginClass.displayName].updateApi(api);
+ } else {
+ plugin = new PluginClass(api, config);
+ 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
+ }
+ });
+
+ const data = {
+ index,
+ instance,
+ pluginMethods: old ? old.pluginMethods : pluginMethods,
+ pluginInstances: old ? old.pluginInstances : pluginInstances,
+ };
+
+ if (old) {
+ const realIndex = instances.indexOf(old);
+ instances[realIndex] = data;
+ } else {
+ instances.push(data);
+ }
+
+ this.setState({ instances });
+
+ return () => {
+ const insts = this.state.instances;
+ this.setState({
+ instances: insts.filter(i => !isEqual(i.instance, instance)),
+ });
+ };
+ }
+
+ // Toggle a state boolean
toggleState = name => () => this.setState({ [name]: !this.state[name] });
// Prepare the built-in commands
assembleCommands = () => {
let commands = {
...this.defaultCommands,
- ...this.props.commands
+ ...this.props.commands,
};
- this.props.plugins.forEach(plugin => {
+ pluginMap(this.props.plugins, (plugin) => {
if (plugin.commands) {
commands = {
...commands,
- ...plugin.commands
+ ...plugin.commands,
};
}
});
- Object.keys(commands).forEach(name => {
- 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;
- }
-
- commands[name] = {
- parse,
- method
- };
- });
- this.setState({ commands });
+ this.setState({ commands: modCommands(commands) });
};
/**
* autocomplete with the command the have the best match
* @param {object} input reference
*/
- autocompleteValue = inputRef => {
+ autocompleteValue = (inputRef) => {
const { descriptions } = this.state;
const keysToCheck = Object.keys(descriptions).filter(
- key => descriptions[key] !== false
+ key => descriptions[key] !== false,
);
const { bestMatch } = stringSimilarity.findBestMatch(
inputRef.value,
- keysToCheck
+ keysToCheck,
);
if (bestMatch.rating >= 0.5) {
@@ -318,15 +478,15 @@ class Terminal extends Component {
};
// Refresh or clear the screen
- clearScreen = () => {
- this.setState({ summary: [] });
+ clearScreen = (args, printLine, runCommand, instance) => {
+ instance.setState({ summary: [] });
};
// Method to check for shortcut and invoking commands
- checkShortcuts = key => {
+ checkShortcuts = (instance, key, e) => {
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
@@ -343,61 +503,68 @@ 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: [] });
+ 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) {
- this.setState({ keyInputs: [] });
+ instance.setState({ keyInputs: [] });
}
}
};
// edit-line command
- 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;
+ if (index < 0) {
+ index = summary.length === 0 ? 0 : summary.length - index;
}
summary[index] = args._.join(' ');
- this.setState({ summary });
+ instance.setState({ summary });
};
// 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;
+ if (typeof e.dontShowCommand === 'undefined') {
+ this.printLine.bind(this, instance)(
+ `${promptPrefix}${this.state.prompt} ${e.target.value}`,
+ false,
+ );
+ }
- const res = this.runCommand(
- `${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(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
+ this.printLine.bind(this, instance)(
+ `${promptPrefix}${this.state.prompt} ${e.target.value}`,
+ false,
);
- const { input } = this.state;
- const history = [...this.state.history, e.target.value];
- this.setState({
+ const newHistory = [...history, e.target.value];
+ instance.setState({
input: [...input, e.target.value],
- history,
- historyCounter: history.length
+ history: newHistory,
+ historyCounter: newHistory.length,
});
e.target.value = ''; // eslint-disable-line no-param-reassign
}
@@ -408,16 +575,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);
@@ -427,83 +594,88 @@ class Terminal extends Component {
break;
}
}
- this.checkShortcuts(key);
- };
+ this.checkShortcuts(instance, key, e);
+ }
// Plugins
loadPlugins = () => {
- if (this.props.plugins) {
- this.props.plugins.forEach(plugin => {
- try {
- plugin.load({
- printLine: this.printLine,
- runCommand: this.runCommand,
- setPromptPrefix: this.setPromptPrefix,
- 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();
- } catch (e) {
- // Do nothing
- }
- });
- }
+ const pluginData = {};
+ pluginMap(this.props.plugins, (plugin) => {
+ try {
+ pluginData[plugin.displayName] = plugin.defaultData;
+ } catch (e) {
+ console.error(`Error loading plugin ${plugin.displayName}`, e); // eslint-disable-line no-console
+ }
+ });
+ this.pluginData = 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.state.instances.find(i => isEqual(i.instance, instance));
+ if (instanceData) {
+ if (instanceData.pluginMethods[name]) {
+ if (instanceData.pluginMethods[name][method]) {
+ return instanceData.pluginMethods[name][method];
+ }
+ console.log(instanceData.pluginMethods[name]);
+ throw new Error(
+ `No method with name ${method} 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 = (inp, std = true) => {
+ 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.state.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
+ }
}
}
}
if (print !== false) {
- const summary = this.state.summary;
+ const summary = instance.state.summary;
summary.push(inp);
- this.setState({ summary });
+ instance.setState({ summary });
}
};
+ // 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 = inputText => {
+ runCommand = (instance, inputText) => {
const inputArray = inputText.split(' ');
const input = inputArray[0];
const args = inputArray; // Undefined for function call
- const command = this.state.commands[input];
+ const instanceData = this.state.instances.find(i => isEqual(i.instance, instance));
+ let commands = { ...this.state.commands };
+ if (instanceData) {
+ Object.values(instanceData.pluginInstances).forEach((i) => {
+ commands = {
+ ...commands,
+ ...modCommands(i.commands),
+ };
+ });
+ }
+ const command = commands[input];
let res;
if (input === '') {
@@ -512,47 +684,75 @@ class Terminal extends Component {
if (typeof this.props.commandPassThrough === 'function') {
res = this.props.commandPassThrough(
inputArray,
- this.printLine,
- this.runCommand
+ 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);
+ const type = typeof parsedArgs;
+ if (type !== 'object' || (type === 'object' && !parsedArgs.help)) {
+ res = command.method(
+ parsedArgs,
+ this.printLine.bind(this, instance),
+ this.runCommand.bind(this, instance),
+ command.needsInstance === true ? instance : undefined,
+ );
}
}
return res;
};
+ // Print to active instance
+ printToActive = (...args) => {
+ const data = this.state.instances.find(i => i.index === 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 = () => {
- handleLogging('log', this.printLine);
- handleLogging('info', this.printLine);
- // handleLogging('warn', this.printLine);
- // handleLogging('error', this.printLine);
+ handleLogging('log', this.printToActive);
+ handleLogging('info', this.printToActive);
+ // handleLogging('warn', this.printToActive);
+ // handleLogging('error', this.printToActive);
};
// List all the commands (state + user defined)
- showHelp = () => {
- 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.state.instances.find(i => isEqual(i.instance, instance));
+ if (instanceData) {
+ Object.values(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) {
- this.printLine(`${option} - ${descriptions[option]}`);
+ printLine(`${option} - ${descriptions[option]}`);
}
}
};
// Show the msg (prop msg)
- showMsg = () => {
- this.printLine(this.props.msg);
+ showMsg = (args, printLine) => {
+ if (this.props.msg && this.props.msg.length > 0) {
+ printLine(this.props.msg);
+ }
};
render() {
@@ -567,4 +767,4 @@ class Terminal extends Component {
}
}
-export default Terminal;
\ No newline at end of file
+export default Terminal;
diff --git a/src/js/components/types.js b/src/js/components/types.js
index d64209c..598e9db 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
@@ -32,13 +36,19 @@ 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.oneOfType([
+ PropTypes.func,
+ PropTypes.shape({
+ class: PropTypes.func,
+ config: PropTypes.object,
+ }),
+ ])),
shortcuts: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
+ actionHandlers: PropTypes.shape({
+ handleClose: PropTypes.func,
+ handleMinimise: PropTypes.func,
+ handleMaximise: PropTypes.func,
+ }),
};
// shortcuts example
@@ -52,6 +62,10 @@ export const TerminalPropTypes = {
// }
export const TerminalContextTypes = {
+ barShowing: PropTypes.bool,
+ tabsShowing: PropTypes.bool,
+ activeTab: PropTypes.string,
+ instances: PropTypes.array,
symbol: PropTypes.string,
show: PropTypes.bool,
minimise: PropTypes.bool,
@@ -68,6 +82,10 @@ export const TerminalContextTypes = {
};
export const TerminalDefaultProps = {
+ startState: 'open',
+ hideTopBar: false,
+ allowTabs: true,
+ showActions: true,
msg: '',
color: 'green',
style: {},
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,
+};
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
new file mode 100644
index 0000000..b1d13c1
--- /dev/null
+++ b/src/styles/Tabs.scss
@@ -0,0 +1,82 @@
+.terminal-tab-bar {
+ height: 30px;
+ max-width: 600px;
+ transition: all 0.4s ease-out;
+ background: #222;
+ display: flex;
+ 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;
+ 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;
+}
+
+.terminal-tab-bar-empty {
+ display: inline-block;
+ min-width: 25px;
+ height: 100%;
+ flex: 1;
+}
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 {
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";
diff --git a/starter/App.js b/starter/App.js
index bfc41ae..d13ab92 100644
--- a/starter/App.js
+++ b/starter/App.js
@@ -1,14 +1,17 @@
-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
+// 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 = () => (
{
@@ -31,18 +34,27 @@ 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,
+ // },
+ // },
+ // ]}
/>
);
diff --git a/todo.md b/todo.md
index 94b0ac1..dd21754 100644
--- a/todo.md
+++ b/todo.md
@@ -5,4 +5,4 @@
- [x] Media support
- [ ] Upgrades to the plugin system
- [ ] Bash to js ast
-- [ ] Tabs
+- [x] Tabs
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',