define(function(require, exports, module) { main.consumes = [ "Editor", "editors", "commands", "menus", "layout", "util", "settings", "ui", "proc", "c9", "preferences", "tabManager", "dialog.error", "dialog.question", "dialog.alert", "installer" ]; main.provides = ["terminal"]; return main; function main(options, imports, register) { var c9 = imports.c9; var Editor = imports.Editor; var editors = imports.editors; var layout = imports.layout; var proc = imports.proc; var util = imports.util; var ui = imports.ui; var commands = imports.commands; var prefs = imports.preferences; var menus = imports.menus; var tabs = imports.tabManager; var settings = imports.settings; var installer = imports.installer; var question = imports["dialog.question"]; var showError = imports["dialog.error"].show; var hideError = imports["dialog.error"].hide; var alert = imports["dialog.alert"]; // Disabled: bad performance, openshift specific, possibly unreliable // var Monitor = require("./monitor.js"); var markup = require("text!./terminal.xml"); var markupMenu = require("text!./menu.xml"); var Aceterm = require("./aceterm/aceterm"); var libterm = require("./aceterm/libterm"); // Needed to clear ace var EditSession = require("ace/edit_session").EditSession; var dummySession = new EditSession(""); var extensions = []; // Set up the generic handle var handle = editors.register("terminal", "Terminal", Terminal, extensions); var handleEmit = handle.getEmitter(); handleEmit.setMaxListeners(1000); var TMUX = options.tmux || "~/.c9/bin/tmux"; var VFSROOT = options.root || "~"; var TMPDIR = options.tmpdir; var tmuxConnection = require("./tmux_connection")(c9, proc, options.installPath, options.shell); var mnuTerminal; var lastEditor; var lastTerminal; var shownDotsHelp; var installPrompted; var defaults = { "flat-light" : ["#eaf0f7", "#000000", "#bed1e3", false], "flat-dark" : ["#153649", "#FFFFFF", "#515D77", true], "light" : ["rgb(248, 248, 231)", "#000000", "rgb(137, 193, 253)", false], "light-gray" : ["rgb(248, 248, 231)", "#000000", "rgb(137, 193, 253)", false], "dark" : ["#153649", "#FFFFFF", "#515D77", true], "dark-gray" : ["#153649", "#FFFFFF", "#515D77", true] }; var themeName; if (options.defaults) { for (themeName in options.defaults) { defaults[themeName] = options.defaults[themeName]; } } // Import the CSS ui.insertCss(require("text!./style.css"), options.staticPrefix, handle); handle.on("load", function(){ commands.addCommand({ name: "openterminal", group: "Terminal", hint: "Opens a new terminal window", msg: "opening terminal.", bindKey: { mac: "Option-T", win: "Alt-T" }, exec: function (editor) { var pane = tabs.focussedTab && tabs.focussedTab.pane; if (tabs.getTabs(tabs.container).length === 0) pane = null; tabs.open({ editorType: "terminal", focus: true, pane: pane }, function(){}); } }, handle); commands.addCommand({ name: "switchterminal", group: "Terminal", hint: "Switch between Editor and Terminal", msg: "switching.", bindKey: { mac: "Option-S", win: "Alt-S" }, isAvailable: function() { return tabs.focussedTab; }, exec: function (editor) { if (!tabs.focussedTab) return; if (tabs.focussedTab.editorType !== "terminal") lastTerminal && tabs.focusTab(lastTerminal); else lastEditor && tabs.focusTab(lastEditor); } }, handle); commands.addCommand({ name: "clearterm", group: "Terminal", hint: "Clears the terminal buffer", isAvailable: function(editor) { return editor && editor.type == "terminal"; }, exec: function (editor) { tabs.focussedTab.editor.clear(); } }, handle); var meta = '\x1b'; [ ["close_term_pane", "x", "x"], ["split_term_pane", '"', '"'], ["layout_term_hor_even", "Meta-1", meta + "1"], ["layout_term_ver_even", "Meta-2", meta + "2"], ["layout_term_hor_main", "Meta-3", meta + "3"], ["layout_term_ver_main", "Meta-4", meta + "4"], ["move_term_paneup", "Up", '\x1b[A'], ["move_term_panedown", "Down", '\x1b[B'], ["move_term_paneright", "Right", '\x1b[C'], ["move_term_paneleft", "Left", '\x1b[D'], ["term_help", "?", '?'], ["term_restart", "", ":kill-server\r"], ["term_detach", "", ":detach -a\r"], ["toggle_term_status", "", ":set-option status on\r"] ].forEach(function(iter) { commands.addCommand({ name: iter[0], group: "Terminal", bindKey: { mac: "", //Ctrl-B " + iter[1].replace(/Meta/, "Command"), win: "" //Ctrl-B " + iter[1] }, isAvailable: function(editor, e) { var type = editor && editor.type; return (type == "terminal" || type == "output") && e.source == "click"; }, exec: function (editor) { if (iter[0] == "toggle_term_status") { var session = editor.activeDocument.getSession(); session.status = !(session.status || 0); editor.write(String.fromCharCode(2) + iter[2].replace(/on\r/, session.status ? "on\r" : "off\r")); } else { editor.write(String.fromCharCode(2) + iter[2]); } } }, handle); }); var menu = tabs.getElement("mnuEditors"); var ctxItem = menus.addItemToMenu(menu, new ui.item({ caption: "New Terminal", hotkey: "{commands.commandManager.openterminal}", onclick: function(e) { tabs.open({ active: true, pane: this.parentNode.pane, editorType: "terminal" }, function(){}); } }), 200, handle); menus.addItemByPath("Window/New Terminal", new ui.item({ command: "openterminal" }), 30, handle); menus.addItemByPath("Window/Navigation/~", new ui.divider(), 1500, handle); menus.addItemByPath("Window/Navigation/Switch Between Editor and Terminal", new ui.item({ command: "switchterminal" }), 1550, handle); function setSettings(){ libterm.cursorBlink = settings.getBool("user/terminal/@blinking"); libterm.scrollback = settings.getNumber("user/terminal/@scrollback") || 1000; var cname = ".c9terminal .c9terminalcontainer .terminal"; var sname = ".c9terminal .c9terminalcontainer"; var fsize = settings.getNumber("user/terminal/@fontsize"); var fstyle = settings.getBool("user/terminal/@antialiasedfonts"); var fcolor = settings.get("user/terminal/@foregroundColor"); var bcolor = settings.get("user/terminal/@backgroundColor"); var scolor = settings.get("user/terminal/@selectionColor"); [ [cname, "fontFamily", settings.get("user/terminal/@fontfamily") || "Ubuntu Mono, Menlo, Consolas, monospace"], [cname, "fontSize", fsize ? fsize + "px" : "10px"], [cname, "WebkitFontSmoothing", fstyle ? "antialiased" : "auto"], [cname, "MozOSXFontSmoothing", fstyle ? "grayscale" : "auto"], [cname, "color", fcolor || "rgb(255,255,255)"], [sname, "backgroundColor", bcolor || "rgb(25, 34, 39)"], [cname + " .ace_selection", "backgroundColor", scolor || "rgb(81, 93, 119)"] ].forEach(function(i) { ui.setStyleRule(i[0], i[1], i[2]); }); // Small hack until we have terminal themes var colors; if (bcolor == "#eaf0f7") { colors = [ // dark: '#eaf0f7', // background wrong link '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', // bright: '#555753', // grey '#ef2929', // red '#579818', // green '#C3A613', // yellow '#5183B8', // blue '#ad7fa8', // purple '#20C7C7', // mint '#BBBBBB' // light grey ]; } libterm.setColors(fcolor, bcolor, colors); handleEmit("settingsUpdate"); } // Terminal settings.on("read", function(e) { var skin = settings.get("user/general/@skin"); var colors = defaults[skin] || defaults["dark"]; settings.setDefaults("user/terminal", [ ["backgroundColor", colors[0]], ["foregroundColor", colors[1]], ["selectionColor", colors[2]], ["antialiasedfonts", colors[3]], ["fontfamily", "Ubuntu Mono, Menlo, Consolas, monospace"], // Monaco, ["fontsize", "12"], ["blinking", "false"], ["scrollback", 1000] ]); setSettings(); }, handle); settings.on("user/terminal", setSettings); layout.on("themeChange", function(e) { setSettings(); var skin = e.oldTheme; if (!(settings.get("user/terminal/@backgroundColor") == defaults[skin][0] && settings.get("user/terminal/@foregroundColor") == defaults[skin][1] && settings.get("user/terminal/@selectionColor") == defaults[skin][2] && settings.get("user/terminal/@antialiasedfonts") == defaults[skin][3])) return false; }); layout.on("themeDefaults", function(e) { var skin = e.theme; settings.set("user/terminal/@backgroundColor", defaults[skin][0]); settings.set("user/terminal/@foregroundColor", defaults[skin][1]); settings.set("user/terminal/@selectionColor", defaults[skin][2]); settings.set("user/terminal/@antialiasedfonts", defaults[skin][3]); }, handle); // Settings UI prefs.add({ "Editors" : { "Terminal" : { position: 100, "Text Color" : { type: "colorbox", path: "user/terminal/@foregroundColor", position: 10100 }, "Background Color" : { type: "colorbox", path: "user/terminal/@backgroundColor", position: 10200 }, "Selection Color" : { type: "colorbox", path: "user/terminal/@selectionColor", position: 10250 }, "Font Family" : { type: "textbox", path: "user/terminal/@fontfamily", position: 10300 }, "Font Size" : { type: "spinner", path: "user/terminal/@fontsize", min: "1", max: "72", position: 11000 }, "Antialiased Fonts" : { type: "checkbox", path: "user/terminal/@antialiasedfonts", position: 12000 }, "Blinking Cursor" : { type: "checkbox", path: "user/terminal/@blinking", position: 12000 }, "Scrollback" : { type: "spinner", path: "user/terminal/@scrollback", min: "1", max: "100000", position: 13000 } } } }, handle); // Offline c9.on("stateChange", function(e) { // Online if (e.state & c9.NETWORK) { ctxItem && ctxItem.enable(); ui.setStyleRule(".terminal .ace_content", "opacity", ""); } // Offline else { ctxItem && ctxItem.disable(); ui.setStyleRule(".terminal .ace_content", "opacity", "0.5"); } }); }); handle.on("unload", function(){ mnuTerminal = null; lastEditor = null; lastTerminal = null; shownDotsHelp = null; installPrompted = null; }); handle.draw = function(){ ui.insertMarkup(null, markupMenu, handle); mnuTerminal = handle.getElement("mnuTerminal"); if (c9.platform == "win32") { var nodes = mnuTerminal.childNodes; while (nodes[6]) { mnuTerminal.removeChild(nodes[6]); } } handle.draw = function(){}; }; handle.Terminal = Terminal; handle.VFSROOT = VFSROOT; var counter = 0; /***** Initialization *****/ function Terminal(isOutputTerminal) { var plugin = new Editor("Ajax.org", main.consumes, extensions); var emit = plugin.getEmitter(); var container, barTerminal, currentSession, currentDocument, aceterm; plugin.on("draw", function(e) { // Create UI elements ui.insertMarkup(e.tab, markup, plugin); barTerminal = plugin.getElement("barTerminal"); // Draw menu handle.draw(); // Set context menu barTerminal.setAttribute("contextmenu", mnuTerminal); // Fetch Reference to the HTML Element container = barTerminal.firstChild.$ext; // todo do we need barTerminal or e.htmlNode aceterm = Aceterm.createEditor(null, "ace/theme/idle_fingers"); aceterm.container.style.position = "absolute"; aceterm.container.style.left = "0px"; aceterm.container.style.right = "0px"; aceterm.container.style.top = "0px"; aceterm.container.style.bottom = "0px"; // e.htmlNode container.appendChild(aceterm.container); aceterm.on("focus", function() { barTerminal.setAttribute("class", "c9terminal c9terminalFocus"); }); aceterm.on("blur", function() { barTerminal.setAttribute("class", "c9terminal"); }); handle.on("settingsUpdate", function(){ aceterm.renderer.updateFull(); }, plugin); var cm = commands; // TODO find better way for terminal and ace commands to coexist aceterm.commands.addCommands(cm.getExceptionList()); cm.on("update", function() { aceterm.commands.addCommands(cm.getExceptionList()); }, plugin); aceterm.commands.exec = function(command) { return cm.exec(command); }; plugin.on("unload", function(){ aceterm.destroy(); container.innerHTML = ""; aceterm = null; container = null; }); aceterm.on("focus", function(){ if (currentSession && !currentSession.connected && currentSession.reconnect) currentSession.reconnect(); }); handleEmit.sticky("create", { editor: plugin }, plugin); }); /***** Methods *****/ function write(data) { if (currentSession) { if (currentSession.connected) currentSession.pty.write(data); else { var session = currentSession; plugin.on("connect", function wait(e) { if (e.tab == session.tab) { currentSession.pty.write(data); plugin.off("connect", wait); } }); } } } function focus(){ if (aceterm) aceterm.focus(); } function blur(){ // var cursor = barTerminal.$ext.querySelector(".terminal .reverse-video"); // if (cursor && settings.getBool("user/terminal/blinking")) // cursor.parentNode.removeChild(cursor); barTerminal.setAttribute("class", "c9terminal"); if (aceterm) aceterm.blur(); } var afterAnim; function resize(e) { var renderer = aceterm && aceterm.renderer; if (!renderer || !currentDocument) return; if (e.type == "anim") { var htmlNode = aceterm.container; if (!htmlNode) return; if (e.vertical) { var size = e.current === 0 ? Math.abs(e.delta) - 5 - currentDocument.tab.pane.aml.$buttons.offsetHeight : htmlNode.offsetHeight + e.delta; renderer.onResize(false, null, null, size); } else { renderer.onResize(false, null, htmlNode.offsetWidth + e.delta); } afterAnim = true; } else if (e.type == "afteranim" && afterAnim) { afterAnim = false; } else { afterAnim = false; renderer.$updateSizeAsync(); } } function updateCover(aceSession, add) { if (aceSession.updatingStatus) return; if (!add) { if (!aceSession.term.tmuxDotCover) return; var x = aceSession.term.tmuxDotCover.width; var y = aceSession.term.tmuxDotCover.height; if (isEmpty(aceSession.term, x, y)) return; } aceSession.updatingStatus = true; aceSession.c9session.getStatus({clients:true}, function(e, s) { aceSession.updatingStatus = false; if (e) return console.warn(e); var term = aceSession.term; add = term.rows > s.height || term.cols > s.width; setCover(aceSession, add); if (aceSession.term.tmuxDotCover) { aceSession.term.tmuxDotCover.width = s.width; aceSession.term.tmuxDotCover.height = s.height; aceSession._signal("changeFrontMarker"); } }); } function showTmuxDotsHelp(e) { if (settings.getBool("user/terminal/@collab") || shownDotsHelp) return; var aceSession = e && e.editor && e.editor.session.term; if (aceSession && !aceSession.tmuxDotCover) return; shownDotsHelp = true; alert.show("Collaborative Terminal", "Your Terminal is in Collaborative mode", "When you Share your workspace with others, they can use your " + "Terminal as well.\n\n" + "Some side-effects are that the terminal view will only be as " + "large as the smallest view of all collaborators. The rest " + "of the space is unavailable. Also, you can’t scroll back " + "the history. \n\n" + "(note that this can also happen when the workspace is open " + "in another window; to resolve this, right click on the " + "terminal and choose ‘detach other clients’)", function(){ // Hide settings.set("user/terminal/@collab", alert.dontShow); }, { showDontShow: true }); } function isEmpty(term, x, y) { if (x >= term.cols || y >= term.rows) return false; var lines = term.lines; for (var i = term.ybase; i < lines.length; i++) { if (!lines[i]) continue; for (var j = term.ybase; j < lines.length; j++) { if (lines[i][j] && lines[i][j][1]) return false; } } return true; } function setCover(aceSession, add) { if (Boolean(aceSession.term.tmuxDotCover) === Boolean(add)) return; aceSession.setScrollTop(Number.MAX_VALUE); if (aceSession.term.tmuxDotCover) { aceSession.removeMarker(aceSession.term.tmuxDotCover.id); aceSession.term.tmuxDotCover = null; return; } var marker = {}; marker.update = function(html, markerLayer, session, config) { if (!this.width || !this.height || !session.term) return; var rows = session.term.rows; var cols = session.term.cols; var coverHeight = config.lineHeight * (rows - this.height); var screenBottom = config.height - coverHeight + config.offset + 2; var screenRight = (cols - this.width) * config.characterWidth; html.push("
", "
", "
" ); }; aceSession.addDynamicMarker(marker, true); aceSession.term.tmuxDotCover = marker; if (aceSession.ace) aceSession.ace.on("click", showTmuxDotsHelp); } function isTmuxBorderChar(x){ return !x || x[1] && "\xb7\u2500\u2502\u2518".indexOf(x[1]) != -1 } function clearTmuxBorders(terminal) { var trimmed = false; var lines = terminal.lines; for (var i = Math.max(0, terminal.ybase - 4); i < lines.length; i++) { var line = terminal.lines[i]; if (line) { var col = Math.min(line.length, terminal.cols) - 1; while (col >= 0 && isTmuxBorderChar(line[col])) { if (line[col]) line[col][1] = ''; col--; trimmed = true; } } } updateCover(terminal.aceSession, trimmed); } function loadHistory(session) { if (session.terminal.tmuxDotCover) return; session.getOutputHistory({}, function(e, data) { if (!e && data) { session.terminal.setOutputHistory(data, true); session.getStatus({clients: true}, function(e, status) { if (e) return; if (status.clients && status.clients.length > 0) { var terminal = session.terminal; var rows = terminal.rows; var cols = terminal.cols; session.terminal.resize(status.width, status.height); updateCover(session.terminal.aceSession); terminal.cols = cols; while (terminal.rows < rows) { terminal.lines.push(terminal.blankLine()); terminal.rows++; } terminal.rows = rows; } }); } }); } function createTerminal(session, state) { var queue = ""; var warned = false; var timer = null; var initialConnect = true; function send(data) { if (!(c9.status & c9.NETWORK)) return warnConnection(); emit("input", { data: data, session: session }); queue += data; if (!timer) { timer = setTimeout(function() { timer = null; if (!session.connected) return initialConnect || warnConnection(); // Send data to stdin of tmux process session.pty.write(queue); queue = ""; }, 1); } } function warnConnection() { if (warned) return; warned = true; var error = showError("Terminal was disconnected. Trying to reconnect"); if (!session.connecting && session.reconnect) session.reconnect(); session.once("connected", function() { hideError(error); warned = false; }); } // Create the terminal renderer and monitor var terminal = new Aceterm(0, 0, send); session.terminal = terminal; session.monitor = terminal.monitor; session.aceSession = terminal.aceSession; session.aceSession.c9session = session; session.send = send; // Add method to write to terminal session.write = function(data) { var handled = emit("beforeWrite", { data: data, session: session }); if (!handled) session.terminal.write(data); emit("afterWrite", { data: data, session: session }); }; // Create a container and initialize the terminal in it. session.attach(); // Update the terminal title terminal.on("title", function(title) { emit("title", { title: title }); if (!session.output) { session.doc.title = session.doc.tooltip = title.replace(/^.+?:\d+:/, ""); } }); session.aceSession.resize = session.resize.bind(session); // delay a little until we have correct size aceterm.renderer.once("afterRender", function start(){ if (session.resize() === false) return aceterm.renderer.once("afterRender", start); // Lets get our TMUX process tmuxConnection.init(session, function(err, session) { if (err) emit("connectError", { error: err }); else { emit("connect", { id: session.id, tab: session.tab }); loadHistory(session); initialConnect = false; if (queue) { session.pty.write(queue); queue = ""; } } }); }); // hack to deal with dotted borders drawn by tmux terminal.on("afterWrite", function() { clearTmuxBorders(terminal); }); session.getEmitter().sticky("terminalReady", session); } /***** Lifecycle *****/ plugin.on("documentLoad", function(e) { var doc = e.doc; var session = doc.getSession(); session.__defineGetter__("tab", function(){ return doc.tab }); session.__defineGetter__("doc", function(){ return doc }); session.__defineGetter__("defaultEditor", function(){ return settings.getBool("user/terminal/@defaultEnvEditor"); }); session.attach = function(){ if (session.aceSession && aceterm) { aceterm.setSession(session.aceSession); aceterm.container.style.display = "block"; } else aceterm.container.style.display = "none"; }; session.detach = function(){ // if (session.aceSession) // aceterm.setSession(session.aceSession); }; session.kill = function(){ tmuxConnection.kill(this); }; session.warn = function(err){ if (err.code == "EINSTALL" && !installPrompted) { installPrompted = true; question.show("Wrong version of dependencies installed", err.message, "Cloud9 detected you have unsupported version of a dependency " + "installed. Would you like to open the installer " + "to update to the latest version?", function(){ // Yes installer.reinstall("Cloud9 IDE"); }, function(){ // No // Do nothing }); } } session.setState = function(state) { if (!plugin.loaded) return; if (session == currentSession) { var el = container.querySelector(".ace_content"); el.style.opacity = state == "connected" ? "" : "0.5"; } if (state == "connecting") { session.tab.classList.add("connecting"); } else if (state == "error" || state == "killed") { doc.tab.classList.add("error"); } else { doc.tab.classList.remove("error"); session.tab.classList.remove("connecting"); } }; var sizeChanged = null; var waitForServer = null; session.setSize = function(size) { if (size) { clearTimeout(waitForServer); waitForServer = null; var term = this.terminal; term.setSize(size.cols, size.rows); term.$resizeMessageT = Date.now(); term.$resizeDelay = (term.$resizeMessageT - term.$resizeMessageSentT) || 0; if (sizeChanged) { sizeChanged = false; this.updatePtySize(); } } }; session.updatePtySize = function() { // todo check tab.visible if (this.pty && this.cols > 1 && this.rows > 1 && !waitForServer) { this.terminal.$resizeMessageSentT = Date.now(); this.pty.resize(this.cols, this.rows); clearTimeout(waitForServer); waitForServer = setTimeout(function() { waitForServer = null; }, 1000); } else sizeChanged = true; }; session.resize = function(force) { if (!this.aceSession) return; var terminal = this.terminal; var ace = this.aceSession.ace; if (!terminal || !ace) return; var size = ace.renderer.$size; var config = ace.renderer.layerConfig; var h = size.scrollerHeight; var w = size.scrollerWidth - 2 * config.padding; if (!h || config.lineHeight <= 1) return false; // top 1px is for cursor outline var rows = Math.floor((h - 1) / config.lineHeight); if (rows <= 2 && !ace.renderer.scrollBarV.isVisible) w -= ace.renderer.scrollBarV.width; var cols = Math.floor(w / config.characterWidth); if (!cols || !rows) return; // Don't do anything if the size remains the same if (!force && cols == terminal.cols && rows == terminal.rows) return; // do not resize terminal to very small heights during initialization rows = Math.max(rows, 2); cols = Math.max(cols, 2); if (cols > 1000 || rows > 1000) { console.error("invalid terminal size"); return; } terminal.resize(cols, rows); session.cols = cols; session.rows = rows; this.updatePtySize(); }; function setTabColor(){ var bg = settings.get("user/terminal/@backgroundColor"); var shade = util.shadeColor(bg, 0.75); var skinName = settings.get("user/general/@skin"); var isLight = ~skinName.indexOf("flat") || shade.isLight; doc.tab.backgroundColor = isLight ? bg : shade.color; if (isLight) { if (~skinName.indexOf("flat") && !shade.isLight) { doc.tab.classList.add("dark"); container.className = "c9terminalcontainer flat-dark"; } else { doc.tab.classList.remove("dark"); container.className = "c9terminalcontainer"; } } else { doc.tab.classList.add("dark"); container.className = "c9terminalcontainer dark"; } } if (!isOutputTerminal) setTabColor(); // Prevent existing session from being reset if (session.terminal) { if (session.connecting) session.tab.classList.add("connecting"); return; } // Set id of previous session if applicable session.id = e.state && e.state.id || session.id || isOutputTerminal && "output"; session.root = VFSROOT; session.cwd = e.state.cwd || handleEmit("setTerminalCwd") || ""; session.output = isOutputTerminal; // When document gets unloaded everything should be cleaned up doc.on("unload", function(){ // Stop the shell process at the remote machine if (!options.testing) session.kill(); // Destroy the terminal if (session.terminal) session.terminal.destroy(); }, doc); doc.on("setTitle", function(e) { if (session.mnuItem) session.mnuItem.setAttribute("caption", e.title); }, doc); if (isOutputTerminal) { session.connect = function(){ session.connect = function(){}; // Connect to a new or attach to an existing tmux session createTerminal(session, e.state); // Resize session.resize(); }; return; } var tab = doc.tab; tab.on("beforeClose", function(){ if (!settings.getBool("user/terminal/noclosequestion") && !tab.meta.$ignore && !options.testing) { question.show("Close Terminal?", "Are you sure you want to close this terminal?", "Closing this terminal will stop any processes that it hosts.", function(){ // Yes tab.meta.$ignore = true; tab.close(); if (question.dontAsk) settings.set("user/terminal/noclosequestion", "true"); }, function(){ // No // do nothing; allow user to continue if (question.dontAsk) settings.set("user/terminal/noclosequestion", "true"); }, { showDontAsk: true }); return false; } }, session); handle.on("settingsUpdate", setTabColor, doc); // Some terminals won't set the title, lets set a default if (!doc.title) doc.title = "Terminal"; // Connect to a new or attach to an existing tmux session createTerminal(session, e.state); }); plugin.on("documentActivate", function(e) { // Remove the previously visible terminal if (currentSession) currentSession.detach(); // Set the current terminal as visible terminal currentDocument = e.doc; currentSession = e.doc.getSession(); currentSession.attach(); currentSession.resize(); var el = container.querySelector(".ace_content"); el.style.transition = "opacity 150ms"; el.style.transitionDelay = "50ms"; el.style.opacity = currentSession.connected ? "" : "0.5"; // Focus // plugin.focus(); }); plugin.on("documentUnload", function(e) { var session = e.doc.getSession(); // Remove the element from the container session.detach(); // Clear current session if (currentSession == session) { currentSession = null; currentDocument = null; aceterm && aceterm.setSession(dummySession); } }); plugin.on("getState", function(e) { var session = e.doc.getSession(); if (!session.id) return; e.state.id = session.id; e.state.cwd = session.cwd; e.state.width = barTerminal.lastWidth || barTerminal.getWidth(); e.state.height = barTerminal.lastHeight || barTerminal.getHeight(); // @todo scrollback log if (!e.filter && session.aceSession) { var aceSession = session.aceSession; e.state.scrollTop = aceSession.getScrollTop(); if (!aceSession.selection.isEmpty() || aceSession.selection.rangeCount > 1) e.state.selection = aceSession.selection.toJSON(); } }); plugin.on("setState", function(e) { var session = e.doc.getSession(); session.id = e.state.id; session.cwd = e.state.cwd; // @todo scrollback log var aceSession = session.aceSession; if (aceSession) { if (e.state.scrollTop) aceSession.setScrollTop(e.state.scrollTop); if (e.state.selection) aceSession.selection.fromJSON(e.state.selection); } }); plugin.on("clear", function(){ if (currentSession) { var t = currentSession.terminal; if (!t) return; t.ybase = 0; t.lines = t.lines.slice(-(t.ybase + t.rows)); } }); plugin.on("copy", function(e) { if (e.native) return; // Ace handles this herself var data = aceterm.getCopyText(); e.clipboardData.setData("text/plain", data); }); plugin.on("paste", function(e) { if (e.native) return; // Ace handles this herself var data = e.clipboardData.getData("text/plain"); if (data !== false) aceterm.onPaste(data); }); plugin.on("focus", function(e) { if (e.lost) blur(); else focus(); }); tabs.on("focus", function(e) { if (e.tab.editorType === "terminal") lastTerminal = e.tab; if (e.tab.editorType === "ace") lastEditor = e.tab; }); tabs.on("tabAfterClose", function(e) { if (e.tab === lastTerminal) lastTerminal = null; if (e.tab === lastEditor) lastEditor = null; }); plugin.on("blur", function(){ blur(); }); plugin.on("resize", function(e) { resize(e); }); plugin.on("enable", function(){ }); plugin.on("disable", function(){ }); plugin.on("unload", function(){ }); /***** Register and define API *****/ if (isOutputTerminal) plugin.freezePublicAPI.baseclass(); /** * The output handle, responsible for events that involve all * output instances. This is the object you get when you request * the output service in your plugin. * * Example: * * define(function(require, exports, module) { * main.consumes = ["output"]; * main.provides = ["myplugin"]; * return main; * * function main(options, imports, register) { * var outputHandle = imports.output; * }); * }); * * * @class output * @extends Plugin * @singleton */ /** * The terminal handle, responsible for events that involve all * terminal instances. This is the object you get when you request * the terminal service in your plugin. * * Example: * * define(function(require, exports, module) { * main.consumes = ["terminal"]; * main.provides = ["myplugin"]; * return main; * * function main(options, imports, register) { * var terminalHandle = imports.terminal; * }); * }); * * * @class terminal * @extends Plugin * @singleton */ /** * Output Editor for Cloud9. This editor does not allow * editing content. Instead it displays the output of a PTY in the * workspace. This editor is similar to terminal, except that it * doesn't start the default, instead it connects to an existing * TMUX session in which a process can be started using the * {@link run#run run} plugin. * * Example of instantiating a new output pane: * * tabManager.open({ * editorType : "output", * active : true, * document : { * title : "My Process Name", * output : { * id : "name_of_process" * } * } * }, function(){}); * * @class output.Output * @extends Terminal */ /** * The type of editor. Use this to create the output using * {@link tabManager#openEditor} or {@link editors#createEditor}. * @property {"output"} type * @readonly */ /** * Terminal Editor for Cloud9. This editor does not allow * editing content. Instead it displays the output of a PTY in the * workspace. * * Example of instantiating a new terminal: * * tabManager.openEditor("terminal", true, function(err, tab) { * if (err) throw err; * * var terminal = tab.editor; * terminal.write("ls\n"); * }); * * @class terminal.Terminal * @extends Editor */ /** * The type of editor. Use this to create the terminal using * {@link tabManager#openEditor} or {@link editors#createEditor}. * @property {"terminal"} type * @readonly */ /** * Retrieves the state of a document in relation to this editor * @param {Document} doc the document for which to return the state * @method getState * @return {Object} * @return {String} return.id The unique id of the terminal session. * @return {Number} return.width The width of the terminal in pixels. * @return {Number} return.height The height of the terminal in pixels. * @return {Number} return.scrollTop The amount of pixels scrolled. * @return {Object} return.selection Describing the current state * of the selection. This can become a complex object when * there are multiple selections. */ plugin.freezePublicAPI({ /** * Reference to the ace instance used by this terminal for * rendering the output of the terminal. * @property {Ace.Editor} ace * @readonly */ get ace(){ return aceterm; }, /** * The HTMLElement containing the termainl. * @property {HTMLElement} container * @readonly */ get container(){ return container; }, _events: [ /** * Fires when a connection attempt has failed * @event connectError * @param {Object} e * @param {Error} e.error describes the error that has occured. */ "connectError", /** * Fires when a connection with the PTY on the server is * established. From this moment on data can be received * by the terminal and data can be written to the terminal. * @event connect * @param {Object} e * @param {Tab} e.tab the tab of the terminal that got connected * @param {String} e.id the session id of the terminal */ "connect" ], /** * @ignore This is here to overwrite default behavior */ isClipboardAvailable: function(e) { return !e.fromKeyboard }, /** * Writes a string to the terminal. The message is send to the * server and interpreted as if it was typed by the user. You * can send modifier keys by using their hex representation. * @param {String} message the message to write to the terminal. */ write: write, // toggleMouse : toggleMouse, // toggleStatus : toggleStatus, // closeActivePane : closeActivePane, // splitPaneH : splitPaneH, // splitPaneV : splitPaneV, // moveUp : moveUp, // moveDown : moveDown, // moveLeft : moveLeft, // moveRight : moveRight, /** * @internal * @ignore */ Aceterm: Aceterm }); plugin.load((isOutputTerminal ? "output" : "terminal") + counter++); return plugin; } register(null, { terminal: handle }); } });