Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/console.coffee
Views: 687
#########################################################################1# This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2# License: MS-RSL – see LICENSE.md for details3#########################################################################45# An Xterm Console Window67ACTIVE_INTERVAL_MS = 1000089MAX_TERM_HISTORY = 5000001011$ = window.$1213{debounce} = require('underscore')1415{EventEmitter} = require('events')16misc = require('@cocalc/util/misc')17{copy, filename_extension, required, defaults, to_json, uuid, from_json} = require('@cocalc/util/misc')18{redux} = require('./app-framework')19{alert_message} = require('./alerts')2021templates = $("#webapp-console-templates")22console_template = templates.find(".webapp-console")23{getStudentProjectFunctionality} = require('@cocalc/frontend/course/')2425{delay} = require('awaiting')2627feature = require('./feature')28{webapp_client} = require('./webapp-client')2930# How long to wait after any full rerender.31# Obviously, this is stupid code, but this code is slated to go away.32IGNORE_TERMINAL_TIME_MS = 5003334IS_TOUCH = feature.IS_TOUCH # still have to use crappy mobile for now on3536initfile_content = (filename) ->37"""# This initialization file is associated with your terminal in #{filename}.38# It is automatically run whenever it starts up -- restart the terminal via Ctrl-d and Return-key.3940# Usually, your ~/.bashrc is executed and this behavior is emulated for completeness:41source ~/.bashrc4243# You can export environment variables, e.g. to set custom GIT_* variables44# https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables45#export GIT_AUTHOR_NAME="Your Name"46#export GIT_AUTHOR_EMAIL="[email protected]"47#export GIT_COMMITTER_NAME="Your Name"48#export GIT_COMMITTER_EMAIL="[email protected]"4950# It is also possible to automatically start a program ...5152#sage53#sage -ipython54#top5556# ... or even define a terminal specific function.57#hello () { echo "hello world"; }58"""5960focused_console = undefined61client_keydown = (ev) ->62focused_console?.client_keydown(ev)6364class Console extends EventEmitter65constructor: (opts={}) ->66super()67@opts = defaults opts,68element : required # DOM (or jQuery) element that is replaced by this console.69project_id : required70path : required71title : ""72filename : ""73rows : 4074cols : 10075editor : undefined # FileEditor instance -- needed for some actions, e.g., opening a file76close : undefined # if defined, called when close button clicked.77reconnect : undefined # if defined, opts.reconnect?() is called when session console wants to reconnect; this should call set_session.7879font :80family : undefined81size : undefined # CSS font-size in points82line_height : 120 # CSS line-height percentage8384highlight_mode : 'none'85color_scheme : undefined86on_pause : undefined # Called after pause_rendering is called87on_unpause : undefined # Called after unpause_rendering is called88on_reconnecting: undefined89on_reconnected : undefined90set_title : undefined9192@_init_default_settings()9394@project_id = @opts.project_id95@path = @opts.path9697@mark_file_use = debounce(@mark_file_use, 3000)98@resize = debounce(@resize, 500)99100@_project_actions = redux.getProjectActions(@project_id)101102# The is_focused variable keeps track of whether or not the103# editor is focused. This impacts the cursor, and also whether104# messages such as open_file or open_directory are handled (see @init_mesg).105@is_focused = false106107# Create the DOM element that realizes this console, from an HTML template.108@element = console_template.clone()109@element.processIcons()110@textarea = @element.find(".webapp-console-textarea")111112# Record on the DOM element a reference to the console113# instance, which is useful for client code.114@element.data("console", @)115116# Actually put the DOM element into the (likely visible) DOM117# in the place specified by the client.118$(@opts.element).replaceWith(@element)119120# Set the initial title, though of course the term can change121# this via certain escape codes.122@set_title(@opts.title)123124# Create the new Terminal object -- this is defined in125# static/term/term.js -- it's a nearly complete implementation of126# the xterm protocol.127128@terminal = new Terminal129cols: @opts.cols130rows: @opts.rows131132@terminal.IS_TOUCH = IS_TOUCH133134@terminal.on 'title', (title) => @set_title(title)135136@init_mesg()137138# The first time Terminal.bindKeys is called, it makes Terminal139# listen on *all* keystrokes for the rest of the program. It140# only has to be done once -- any further times are ignored.141Terminal.bindKeys(client_keydown)142143@scrollbar = @element.find(".webapp-console-scrollbar")144145@scrollbar.scroll () =>146if @ignore_scroll147return148@set_term_to_scrollbar()149150@terminal.on 'scroll', (top, rows) =>151@set_scrollbar_to_term()152153@terminal.on 'data', (data) =>154#console.log('terminal data', @_ignore_terminal)155if @_ignore_terminal? and new Date() - @_ignore_terminal < IGNORE_TERMINAL_TIME_MS156#console.log("ignore", data)157return158#console.log("@terminal data='#{JSON.stringify(data)}'")159@conn?.write(data)160161@_init_ttyjs()162163# Initialize buttons164@_init_buttons()165@_init_input_line()166167# Initialize the "set default font size" button that appears.168@_init_font_make_default()169170# Initialize the paste bin171@_init_paste_bin()172173# Init pausing rendering when user clicks174@_init_rendering_pause()175176@textarea.on 'blur', =>177if @_focusing? # see comment in @focus.178@_focus_hidden_textarea()179180# delete scroll buttons except on mobile181if not IS_TOUCH182@element.find(".webapp-console-up").hide()183@element.find(".webapp-console-down").hide()184185@connect()186#window.c = @187188connect: =>189if getStudentProjectFunctionality(@opts.project_id).disableTerminals190# short lines since this is only used on mobile.191@render("Terminals are currently disabled\r\nin this project.\r\nPlease contact your192instructor\r\nif you have questions.\r\n");193return;194api = await webapp_client.project_client.api(@project_id)195# aux_path for compat with new frame terminal editor.196{aux_file} = require("@cocalc/util/misc")197aux_path = aux_file("#{@path}-0", "term");198@conn = await api.terminal(aux_path)199@conn.on 'end', =>200if @conn?201@connect()202@_ignore = true203first_render = true204@conn.on 'data', (data) =>205#console.log("@conn got data '#{data}'")206#console.log("@conn data='#{JSON.stringify(data)}'")207#console.log("@conn got data of length", data.length)208if typeof(data) == 'string'209if @_rendering_is_paused210@_render_buffer ?= ''211@_render_buffer += data212else213if first_render214first_render = false215@_ignore_terminal = new Date()216@render(data)217else if typeof(data) == 'object'218@handle_control_mesg(data)219@resize_terminal()220221reconnect: =>222@disconnect()223@connect()224225handle_control_mesg: (data) =>226#console.log('terminal command', data)227switch data.cmd228when 'size'229@handle_resize(data.rows, data.cols)230break231when 'burst'232@element.find(".webapp-burst-indicator").show()233break234when 'no-burst'235@element.find(".webapp-burst-indicator").fadeOut(2000)236break237when 'no-ignore'238delete @_ignore239break240when 'close'241# remote request that we close this tab, e.g., one user242# booting all others out of his terminal session.243alert_message(type:'info', message:"You have been booted out of #{@opts.filename}.")244@_project_actions?.close_file(@opts.filename)245@_project_actions?.set_active_tab('files')246break247248handle_resize: (rows, cols) =>249if @_terminal_size and rows == @_terminal_size.rows and cols == @_terminal_size.cols250return251# Resize the renderer252@_terminal_size = {rows:rows, cols:cols}253@terminal.resize(cols, rows)254@element.find(".webapp-console-terminal").css({width:null, height:null})255@full_rerender()256257append_to_value: (data) =>258# this @value is used for copy/paste of the session history and @value_orig for resize/refresh259@value_orig ?= ''260@value_orig += data261@value += data.replace(/\x1b\[.{1,5}m|\x1b\].*0;|\x1b\[.*~|\x1b\[?.*l/g,'')262if @value_orig.length > MAX_TERM_HISTORY263@value_orig = @value_orig.slice(@value_orig.length - Math.round(MAX_TERM_HISTORY/1.5))264@full_rerender()265266267init_mesg: () =>268@terminal.on 'mesg', (mesg) =>269if @_ignore or not @is_focused # ignore messages when terminal not in focus (otherwise collaboration is confusing)270return271try272mesg = from_json(mesg)273switch mesg.event274when 'open'275i = 0276foreground = false277for v in mesg.paths278i += 1279if i == mesg.paths.length280foreground = true281if v.file?282@_project_actions?.open_file(path:v.file, foreground:foreground)283if v.directory? and foreground284@_project_actions?.open_directory(v.directory)285catch e286console.log("issue parsing message -- ", e)287288reconnect_if_no_recent_data: =>289#console.log 'check for recent data'290if not @_got_remote_data? or new Date() - @_got_remote_data >= 15000291#console.log 'reconnecting since no recent data'292@reconnect()293294set_state_connected: =>295@element.find(".webapp-console-terminal").css('opacity':'1')296@element.find("a[href=\"#refresh\"]").removeClass('btn-success').find(".fa").removeClass('fa-spin')297298set_state_disconnected: =>299@element.find(".webapp-console-terminal").css('opacity':'.5')300@element.find("a[href=\"#refresh\"]").addClass('btn-success').find(".fa").addClass('fa-spin')301302###303config_session: () =>304# The remote server sends data back to us to display:305@session.on 'data', (data) =>306# console.log("terminal got #{data.length} characters -- '#{data}'")307@_got_remote_data = new Date()308@set_state_connected() # connected if we are getting data.309if @_rendering_is_paused310@_render_buffer += data311else312@render(data)313314if @_needs_resize315@resize()316317@session.on 'reconnecting', () =>318#console.log('terminal: reconnecting')319@_reconnecting = new Date()320@set_state_disconnected()321322@session.on 'reconnect', () =>323delete @_reconnecting324partial_code = false325@_needs_resize = true # causes a resize when we next get data.326@_connected = true327@_got_remote_data = new Date()328@set_state_connected()329@reset()330if @session.init_history?331#console.log("writing history")332try333ignore = @_ignore334@_ignore = true335@terminal.write(@session.init_history)336@_ignore = ignore337catch e338console.log(e)339#console.log("recording history for copy/paste buffer")340@append_to_value(@session.init_history)341342# On first write we ignore any queued terminal attributes responses that result.343@terminal.queue = ''344@terminal.showCursor()345346@session.on 'close', () =>347@_connected = false348349# Initialize pinging the server to keep the console alive350#@_init_session_ping()351352if @session.init_history?353#console.log("session -- history.length='#{@session.init_history.length}'")354try355ignore = @_ignore356@_ignore = true357@terminal.write(@session.init_history)358@_ignore = ignore359catch e360console.log(e)361# On first write we ignore any queued terminal attributes responses that result.362@terminal.queue = ''363@append_to_value(@session.init_history)364365@terminal.showCursor()366@resize()367###368369render: (data) =>370if not data?371return372try373@terminal.write(data)374@append_to_value(data)375376if @scrollbar_nlines < @terminal.ybase377@update_scrollbar()378379setTimeout(@set_scrollbar_to_term, 10)380# See https://github.com/sagemathinc/cocalc/issues/1301381#redux.getProjectActions(@project_id).flag_file_activity(@path)382catch e383# WARNING -- these are all basically bugs, I think...384# That said, try/catching them is better than having385# the whole terminal just be broken.386console.warn("terminal error -- ",e)387388reset: () =>389# reset the terminal to clean; need to do this on connect or reconnect.390#$(@terminal.element).css('opacity':'0.5').animate(opacity:1, duration:500)391@value = @value_orig = ''392@scrollbar_nlines = 0393@scrollbar.empty()394@terminal.reset()395396update_scrollbar: () =>397while @scrollbar_nlines < @terminal.ybase398@scrollbar.append($("<br>"))399@scrollbar_nlines += 1400401pause_rendering: (immediate) =>402if @_rendering_is_paused403return404#console.log 'pause_rendering'405@_rendering_is_paused = true406if not @_render_buffer?407@_render_buffer = ''408f = () =>409if @_rendering_is_paused410@element.find("a[href=\"#pause\"]").addClass('btn-success').find('i').addClass('fa-play').removeClass('fa-pause')411if immediate412f()413else414setTimeout(f, 500)415@opts.on_pause?()416417unpause_rendering: () =>418if not @_rendering_is_paused419return420#console.log 'unpause_rendering'421@_rendering_is_paused = false422f = () =>423@render(@_render_buffer)424@_render_buffer = ''425# Do the actual rendering the next time around, so that the copy operation completes with the426# current selection instead of the post-render empty version.427setTimeout(f, 0)428@element.find("a[href=\"#pause\"]").removeClass('btn-success').find('i').addClass('fa-pause').removeClass('fa-play')429@opts.on_unpause?()430431#######################################################################432# Private Methods433#######################################################################434435_on_pause_button_clicked: (e) =>436if @_rendering_is_paused437@unpause_rendering()438else439@pause_rendering(true)440return false441442_init_rendering_pause: () =>443444btn = @element.find("a[href=\"#pause\"]").click (e) =>445if @_rendering_is_paused446@unpause_rendering()447else448@pause_rendering(true)449return false450451e = @element.find(".webapp-console-terminal")452e.mousedown () =>453@pause_rendering(false)454455e.mouseup () =>456if not getSelection().toString()457@unpause_rendering()458return459460e.on 'copy', =>461@unpause_rendering()462setTimeout(@focus, 5) # must happen in next cycle or copy will not work due to loss of focus.463464mark_file_use: () =>465redux.getActions('file_use').mark_file(@project_id, @path, 'edit')466467client_keydown: (ev) =>468@allow_resize = true469470if @_ignore471# no matter what cancel ignore if the user starts typing, since we absolutely must not loose anything they type.472@_ignore = false473474@mark_file_use()475476if ev.ctrlKey and ev.shiftKey477switch ev.keyCode478when 190 # "control-shift->"479@_increase_font_size()480return false481when 188 # "control-shift-<"482@_decrease_font_size()483return false484else485delete @_ignore_terminal486487if (ev.metaKey or ev.ctrlKey or ev.altKey) and (ev.keyCode in [17, 86, 91, 93, 223, 224]) # command or control key (could be a paste coming)488#console.log("resetting hidden textarea")489#console.log("clear hidden text area paste bin")490# clear the hidden textarea pastebin, since otherwise491# everything that the user typed before pasting appears492# in the paste, which is very, very bad.493# NOTE: we could do this on all keystrokes. WE restrict as above merely for efficiency purposes.494# See http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript495@textarea.val('')496if @_rendering_is_paused497if not (ev.ctrlKey or ev.metaKey or ev.altKey)498@unpause_rendering()499else500return false501502_increase_font_size: () =>503@opts.font.size += 1504if @opts.font.size <= 159505@_font_size_changed()506507_decrease_font_size: () =>508if @opts.font.size >= 2509@opts.font.size -= 1510@_font_size_changed()511512_font_size_changed: () =>513@opts.editor?.local_storage("font-size",@opts.font.size)514$(@terminal.element).css('font-size':"#{@opts.font.size}px")515@element.find(".webapp-console-font-indicator-size").text(@opts.font.size)516@element.find(".webapp-console-font-indicator").stop().show().animate(opacity:1).fadeOut(duration:8000)517@resize_terminal()518519_init_font_make_default: () =>520@element.find("a[href=\"#font-make-default\"]").click () =>521redux.getTable('account').set(terminal:{font_size:@opts.font.size})522return false523524_init_default_settings: () =>525settings = redux.getStore('account').get_terminal_settings()526if not @opts.font.size?527@opts.font.size = settings?.font_size ? 14528if not @opts.color_scheme?529@opts.color_scheme = settings?.color_scheme ? "default"530if not @opts.font.family?531@opts.font.family = settings?.font ? "monospace"532533_init_ttyjs: () ->534# Create the terminal DOM objects535@terminal.open()536@terminal.set_color_scheme(@opts.color_scheme)537538# Give it our style; there is one in term.js (upstream), but it is named in a too-generic way.539@terminal.element.className = "webapp-console-terminal"540ter = $(@terminal.element)541@element.find(".webapp-console-terminal").replaceWith(ter)542543ter.css544'font-family' : @opts.font.family + ", monospace" # monospace fallback545'font-size' : "#{@opts.font.size}px"546'line-height' : "#{@opts.font.line_height}%"547548# Focus/blur handler.549if IS_TOUCH # so keyboard appears550@mobile_target = @element.find(".webapp-console-for-mobile").show()551@mobile_target.css('width', ter.css('width'))552@mobile_target.css('height', ter.css('height'))553@_click = (e) =>554if @is_hidden555return556t = $(e.target)557if t[0]==@mobile_target[0] or t.hasParent(@element).length > 0558@focus()559else560@blur()561$(document).on 'click', @_click562else563mousedown_focus = false564@_mousedown = (e) =>565if @is_hidden566return567if mousedown_focus or $(e.target).hasParent(@element).length > 0568mousedown_focus = true569@focus()570else571@blur()572$(document).on 'mousedown', @_mousedown573574@_mouseup = (e) =>575if @is_hidden576return577mousedown_focus = false578t = $(e.target)579sel = window.getSelection().toString()580if t.hasParent(@element).length > 0 and sel.length == 0581@_focus_hidden_textarea()582$(document).on 'mouseup', @_mouseup583584$(@terminal.element).bind 'copy', (e) =>585# re-enable paste but only *after* the copy happens586setTimeout(@_focus_hidden_textarea, 10)587588# call this when deleting the terminal (removing it from DOM, etc.)589remove: () =>590@disconnect()591if @_mousedown?592$(document).off('mousedown', @_mousedown)593if @_mouseup?594$(document).off('mouseup', @_mouseup)595if @_click?596$(document).off('click', @_click)597598_focus_hidden_textarea: () =>599@textarea.focus()600601_init_fullscreen: () =>602fullscreen = @element.find("a[href=\"#fullscreen\"]")603exit_fullscreen = @element.find("a[href=\"#exit_fullscreen\"]")604fullscreen.on 'click', () =>605@fullscreen()606exit_fullscreen.show()607fullscreen.hide()608return false609exit_fullscreen.hide().on 'click', () =>610@exit_fullscreen()611exit_fullscreen.hide()612fullscreen.show()613return false614615_init_buttons: () ->616editor = @terminal.editor617618@element.find("a").tooltip(delay:{ show: 500, hide: 100 })619620@element.find("a[href=\"#increase-font\"]").click () =>621@_increase_font_size()622return false623624@element.find("a[href=\"#decrease-font\"]").click () =>625@_decrease_font_size()626return false627628@element.find("a[href=\"#refresh\"]").click () =>629@reconnect()630return false631632@element.find("a[href=\"#paste\"]").click () =>633id = uuid()634s = "<h2><i class='fa project-file-icon fa-terminal'></i> Terminal Copy and Paste</h2>Copy and paste in terminals works as usual: to copy, highlight text then press ctrl+c (or command+c); press ctrl+v (or command+v) to paste. <br><br><span class='lighten'>NOTE: When no text is highlighted, ctrl+c sends the usual interrupt signal.</span><br><hr>You can copy the terminal history from here:<br><br><textarea readonly style='font-family: monospace;cursor: auto;width: 97%' id='#{id}' rows=10></textarea>"635bootbox.alert(s)636elt = $("##{id}")637elt.val(@value).scrollTop(elt[0].scrollHeight)638return false639640@element.find("a[href=\"#boot\"]").click () =>641@conn?.write({cmd:'boot'})642alert_message(type:'info', message:"This terminal should now close for all other users, which allows you to resize it as large as you want.")643644@element.find("a[href=\"#initfile\"]").click () =>645initfn = misc.console_init_filename(@opts.filename)646content = initfile_content(@opts.filename)647webapp_client.exec648project_id : @project_id649command : "test ! -r '#{initfn}' && echo '#{content}' > '#{initfn}'"650bash : true651err_on_exit : false652cb : (err, output) =>653if err654alert_message(type:'error', message:"problem creating initfile: #{err}")655else656@_project_actions?.open_file(path:initfn, foreground:true)657658open_copyable_history: () =>659id = uuid()660s = "<h2><i class='fa project-file-icon fa-terminal'></i> Terminal Copy and Paste</h2>Copy and paste in terminals works as usual: to copy, highlight text then press ctrl+c (or command+c); press ctrl+v (or command+v) to paste. <br><br><span class='lighten'>NOTE: When no text is highlighted, ctrl+c sends the usual interrupt signal.</span><br><hr>You can copy the terminal history from here:<br><br><textarea readonly style='font-family: monospace;cursor: auto;width: 97%' id='#{id}' rows=10></textarea>"661bootbox.alert(s)662elt = $("##{id}")663elt.val(@value).scrollTop(elt[0].scrollHeight)664665open_init_file: () =>666initfn = misc.console_init_filename(@opts.filename)667content = initfile_content(@opts.filename)668webapp_client.exec669project_id : @project_id670command : "test ! -r '#{initfn}' && echo '#{content}' > '#{initfn}'"671bash : true672err_on_exit : false673cb : (err, output) =>674if err675alert_message(type:'error', message:"problem creating initfile: #{err}")676else677@_project_actions?.open_file(path:initfn, foreground:true)678679_init_input_line: () =>680#if not IS_TOUCH681# @element.find(".webapp-console-mobile-input").hide()682# return683684if not IS_TOUCH685@element.find(".webapp-console-mobile-input").hide()686687input_line = @element.find('.webapp-console-input-line')688689input_line.on 'focus', =>690@_input_line_is_focused = true691@terminal.blur()692input_line.on 'blur', =>693@_input_line_is_focused = false694695submit_line = () =>696x = input_line.val()697# Apple text input replaces single and double quotes by some stupid698# fancy unicode, which is incompatible with most uses in the terminal.699# Here we switch it back. (Note: not doing exactly this renders basically all700# dev related tools on iPads very frustrating. Not so, CoCalc :-)701x = misc.replace_all(x, '“','"')702x = misc.replace_all(x, '”','"')703x = misc.replace_all(x, '‘',"'")704x = misc.replace_all(x, '’',"'")705x = misc.replace_all(x, '–', "--")706x = misc.replace_all(x, '—', "---")707@_ignore = false708@conn?.write(x)709input_line.val('')710711input_line.on 'keydown', (e) =>712if e.which == 13713e.preventDefault()714submit_line()715@_ignore = false716@conn?.write("\n")717return false718else if e.which == 67 and e.ctrlKey719submit_line()720@terminal.keyDown(keyCode:67, shiftKey:false, ctrlKey:true)721722@element.find(".webapp-console-submit-line").click () =>723#@focus()724submit_line()725@_ignore = false726@conn?.write("\n")727return false728729@element.find(".webapp-console-submit-submit").click () =>730#@focus()731submit_line()732return false733734@element.find(".webapp-console-submit-tab").click () =>735#@focus()736submit_line()737@terminal.keyDown(keyCode:9, shiftKey:false)738739@element.find(".webapp-console-submit-esc").click () =>740#@focus()741submit_line()742@terminal.keyDown(keyCode:27, shiftKey:false, ctrlKey:false)743744@element.find(".webapp-console-submit-up").click () =>745#@focus()746submit_line()747@terminal.keyDown(keyCode:38, shiftKey:false, ctrlKey:false)748749@element.find(".webapp-console-submit-down").click () =>750#@focus()751submit_line()752@terminal.keyDown(keyCode:40, shiftKey:false, ctrlKey:false)753754@element.find(".webapp-console-submit-left").click () =>755#@focus()756submit_line()757@terminal.keyDown(keyCode:37, shiftKey:false, ctrlKey:false)758759@element.find(".webapp-console-submit-right").click () =>760#@focus()761submit_line()762@terminal.keyDown(keyCode:39, shiftKey:false, ctrlKey:false)763764@element.find(".webapp-console-submit-ctrl-c").show().click (e) =>765#@focus()766submit_line()767@terminal.keyDown(keyCode:67, shiftKey:false, ctrlKey:true)768769@element.find(".webapp-console-submit-ctrl-b").show().click (e) =>770#@focus()771submit_line()772@terminal.keyDown(keyCode:66, shiftKey:false, ctrlKey:true)773774###775@element.find(".webapp-console-up").click () ->776vp = editor.getViewport()777editor.scrollIntoView({line:vp.from - 1, ch:0})778return false779780@element.find(".webapp-console-down").click () ->781vp = editor.getViewport()782editor.scrollIntoView({line:vp.to, ch:0})783return false784785if IS_TOUCH786@element.find(".webapp-console-tab").show().click (e) =>787@focus()788@terminal.keyDown(keyCode:9, shiftKey:false)789790@_next_ctrl = false791@element.find(".webapp-console-control").show().click (e) =>792@focus()793@_next_ctrl = true794$(e.target).removeClass('btn-info').addClass('btn-warning')795796@element.find(".webapp-console-esc").show().click (e) =>797@focus()798@terminal.keyDown(keyCode:27, shiftKey:false, ctrlKey:false)799###800801_init_paste_bin: () =>802pb = @textarea803804f = (evt) =>805@_ignore = false806data = pb.val()807pb.val('')808@conn?.write(data)809810pb.on 'paste', =>811pb.val('')812setTimeout(f,5)813814#######################################################################815# Public API816# Unless otherwise stated, these methods can be chained.817#######################################################################818819disconnect: () =>820x = @conn821delete @conn822if x?823try824x.end()825catch err826# pass827828# enter fullscreen mode829fullscreen: () =>830h = $(".navbar-fixed-top").height()831@element.css832position : 'absolute'833width : "97%"834top : h835left : 0836right : 0837bottom : 1838839$(@terminal.element).css840position : 'absolute'841width : "100%"842top : "3.5em"843bottom : 1844845@resize_terminal()846847# exit fullscreen mode848exit_fullscreen: () =>849for elt in [$(@terminal.element), @element]850elt.css851position : 'relative'852top : 0853width : "100%"854@resize_terminal()855856refresh: () =>857@terminal.refresh(0, @opts.rows-1)858@terminal.showCursor()859860full_rerender: =>861@_ignore_terminal = new Date() # ignore output from *client side* of terminal until user types.862locals =863value : @value_orig864ignore : @_ignore865@reset()866@_ignore = true867if locals.value?868@render(locals.value)869@_ignore = locals.ignore # do not change value of @_ignore870@terminal.showCursor()871872873resize_terminal: () =>874@element.find(".webapp-console-terminal").css({width:'100%', height:'100%'})875876# Wait for css change to take effect:877await delay(0)878879# Determine size of container DOM.880# Determine the average width of a character by inserting 10 characters,881# seeing how wide that is, and dividing by 10. The result is typically not882# an integer, which is why we have to use multiple characters.883@_c = $("<span>Term-inal </span>").prependTo(@terminal.element)884character_width = @_c.width()/10885@_c.remove()886elt = $(@terminal.element)887888# The above style trick for character width is not reliable for getting the height of each row.889# For that we use the terminal itself, since it already has rows, and hopefully at least890# one row has something in it (a div).891#892# The row height is in fact *NOT* constant -- it can vary by 1 (say) depending893# on what is in the row. So we compute the maximum line height, which is safe, so894# long as we throw out the outliers.895heights = ($(x).height() for x in elt.children())896# Eliminate weird outliers that sometimes appear (e.g., for last row); yes, this is897# pretty crazy...898heights = (x for x in heights when x <= heights[0] + 2)899row_height = Math.max( heights ... )900901if character_width == 0 or row_height == 0902# The editor must not yet be visible -- do nothing903return904905# Determine the number of columns from the width of a character, computed above.906font_size = @opts.font.size907new_cols = Math.max(1, Math.floor(elt.width() / character_width))908909# Determine number of rows from the height of the row, as computed above.910height = elt.height()911if IS_TOUCH912height -= 60913new_rows = Math.max(1, Math.floor(height / row_height))914915# Record our new size916@opts.cols = new_cols917@opts.rows = new_rows918919# Send message to project to impact actual size of tty920@send_size_to_project()921# width still doesn't work right...922@element.find(".webapp-console-terminal").css({width:'100%', height:''})923924send_size_to_project: =>925@conn?.write({cmd:'size', rows:@opts.rows, cols:@opts.cols})926927set_scrollbar_to_term: () =>928if @terminal.ybase == 0 # less than 1 page of text in buffer929@scrollbar.hide()930return931else932@scrollbar.show()933934if @ignore_scroll935return936@ignore_scroll = true937f = () =>938@ignore_scroll = false939setTimeout(f, 100)940max_scrolltop = @scrollbar[0].scrollHeight - @scrollbar.height()941@scrollbar.scrollTop(max_scrolltop * @terminal.ydisp / @terminal.ybase)942943set_term_to_scrollbar: () =>944max_scrolltop = @scrollbar[0].scrollHeight - @scrollbar.height()945ydisp = Math.floor( @scrollbar.scrollTop() * @terminal.ybase / max_scrolltop)946@terminal.ydisp = ydisp947@terminal.refresh(0, @terminal.rows-1)948949console_is_open: () => # not chainable950return @element.closest(document.documentElement).length > 0951952blur: () =>953if focused_console == @954focused_console = undefined955956@is_focused = false957958try959@terminal.blur()960catch e961# WARNING: probably should investigate term.js issues further(?)962# ignore -- sometimes in some states the terminal code can raise an exception when explicitly blur-ing.963# This would totally break the client, which is bad, so we catch is.964$(@terminal.element).addClass('webapp-console-blur').removeClass('webapp-console-focus')965966focus: (force) =>967if @_reconnecting? and new Date() - @_reconnecting > 10000968# reconnecting not working, so try again. Also, this handles the case969# when terminal switched to reconnecting state, user closed computer, comes970# back later, etc. Without this, would not attempt to reconnect until971# user touches keys.972@reconnect_if_no_recent_data()973974if @is_focused and not force975return976977# focusing the term blurs the textarea, so we save that fact here,978# so that the textarea.on 'blur' knows why it just blured979@_focusing = true980981focused_console = @982@is_focused = true983@textarea.blur()984$(@terminal.element).focus()985986if not IS_TOUCH987@_focus_hidden_textarea()988@terminal.focus()989990$(@terminal.element).addClass('webapp-console-focus').removeClass('webapp-console-blur')991setTimeout((()=>delete @_focusing), 5) # critical!992993set_title: (title) ->994@opts.set_title?(title)995@element.find(".webapp-console-title").text(title)996997998exports.Console = Console9991000$.fn.extend1001webapp_console: (opts={}) ->1002@each () ->1003t = $(this)1004if opts == false1005# disable existing console1006con = t.data('console')1007if con?1008con.remove()1009return t1010else1011opts0 = copy(opts)1012opts0.element = this1013return t.data('console', new Console(opts0))101410151016