Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39549
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
23
###########################################
24
#
25
# An Xterm Console Window
26
#
27
###########################################
28
29
ACTIVE_INTERVAL_MS = 10000
30
31
$ = window.$
32
33
{debounce} = require('underscore')
34
35
{EventEmitter} = require('events')
36
{alert_message} = require('./alerts')
37
misc = require('smc-util/misc')
38
{copy, filename_extension, required, defaults, to_json, uuid, from_json} = require('smc-util/misc')
39
{redux} = require('./smc-react')
40
{alert_message} = require('./alerts')
41
42
misc_page = require('./misc_page')
43
44
templates = $("#webapp-console-templates")
45
console_template = templates.find(".webapp-console")
46
47
feature = require('./feature')
48
49
IS_TOUCH = feature.IS_TOUCH # still have to use crappy mobile for now on
50
51
CSI = String.fromCharCode(0x9b)
52
53
initfile_content = (filename) ->
54
"""# This initialization file is associated with your terminal in #{filename}.
55
# It is automatically run whenever it starts up -- restart the terminal via Ctrl-d and Return-key.
56
57
# Usually, your ~/.bashrc is executed and this behavior is emulated for completeness:
58
source ~/.bashrc
59
60
# You can export environment variables, e.g. to set custom GIT_* variables
61
# https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables
62
#export GIT_AUTHOR_NAME="Your Name"
63
#export GIT_AUTHOR_EMAIL="[email protected]"
64
#export GIT_COMMITTER_NAME="Your Name"
65
#export GIT_COMMITTER_EMAIL="[email protected]"
66
67
# It is also possible to automatically start a program ...
68
69
#sage
70
#sage -ipython
71
#top
72
73
# ... or even define a terminal specific function.
74
#hello () { echo "hello world"; }
75
"""
76
77
focused_console = undefined
78
client_keydown = (ev) ->
79
focused_console?.client_keydown(ev)
80
81
class Console extends EventEmitter
82
constructor: (opts={}) ->
83
@opts = defaults opts,
84
element : required # DOM (or jQuery) element that is replaced by this console.
85
project_id : required
86
path : required
87
session : undefined # a console_session; use .set_session to set it later instead.
88
title : ""
89
filename : ""
90
rows : 16
91
cols : 80
92
editor : undefined # FileEditor instance -- needed for some actions, e.g., opening a file
93
close : undefined # if defined, called when close button clicked.
94
reconnect : undefined # if defined, opts.reconnect?() is called when session console wants to reconnect; this should call set_session.
95
96
font :
97
family : undefined
98
size : undefined # CSS font-size in points
99
line_height : 120 # CSS line-height percentage
100
101
highlight_mode : 'none'
102
color_scheme : undefined
103
on_pause : undefined # Called after pause_rendering is called
104
on_unpause : undefined # Called after unpause_rendering is called
105
on_reconnecting: undefined
106
on_reconnected : undefined
107
set_title : undefined
108
109
@_init_default_settings()
110
111
@project_id = @opts.project_id
112
@path = @opts.path
113
114
@mark_file_use = debounce(@mark_file_use, 3000)
115
@resize = debounce(@resize, 500)
116
117
@_project_actions = redux.getProjectActions(@project_id)
118
119
# The is_focused variable keeps track of whether or not the
120
# editor is focused. This impacts the cursor, and also whether
121
# messages such as open_file or open_directory are handled (see @init_mesg).
122
@is_focused = false
123
124
#@user_is_active()
125
126
# Create the DOM element that realizes this console, from an HTML template.
127
@element = console_template.clone()
128
@textarea = @element.find(".webapp-console-textarea")
129
130
# Record on the DOM element a reference to the console
131
# instance, which is useful for client code.
132
@element.data("console", @)
133
134
# Actually put the DOM element into the (likely visible) DOM
135
# in the place specified by the client.
136
$(@opts.element).replaceWith(@element)
137
138
# Set the initial title, though of course the term can change
139
# this via certain escape codes.
140
@set_title(@opts.title)
141
142
# Create the new Terminal object -- this is defined in
143
# static/term/term.js -- it's a nearly complete implementation of
144
# the xterm protocol.
145
146
@_init_colors()
147
148
@terminal = new Terminal
149
cols: @opts.cols
150
rows: @opts.rows
151
152
@terminal.IS_TOUCH = IS_TOUCH
153
154
@init_mesg()
155
156
# The first time Terminal.bindKeys is called, it makes Terminal
157
# listen on *all* keystrokes for the rest of the program. It
158
# only has to be done once -- any further times are ignored.
159
Terminal.bindKeys(client_keydown)
160
161
@scrollbar = @element.find(".webapp-console-scrollbar")
162
163
@scrollbar.scroll () =>
164
if @ignore_scroll
165
return
166
@set_term_to_scrollbar()
167
168
@terminal.on 'scroll', (top, rows) =>
169
@set_scrollbar_to_term()
170
171
@_init_ttyjs()
172
173
# Initialize buttons
174
@_init_buttons()
175
@_init_input_line()
176
177
# Initialize the "set default font size" button that appears.
178
@_init_font_make_default()
179
180
# Initialize the paste bin
181
@_init_paste_bin()
182
183
# Init pausing rendering when user clicks
184
@_init_rendering_pause()
185
186
@textarea.on 'blur', =>
187
if @_focusing? # see comment in @focus.
188
@_focus_hidden_textarea()
189
190
# delete scroll buttons except on mobile
191
if not IS_TOUCH
192
@element.find(".webapp-console-up").hide()
193
@element.find(".webapp-console-down").hide()
194
195
if opts.session?
196
@set_session(opts.session)
197
198
# call this whenever the *user* actively does something --
199
# this gives them control of the terminal size...
200
user_is_active: =>
201
@_last_active = new Date()
202
203
user_was_recently_active: =>
204
return new Date() - (@_last_active ? 0) <= ACTIVE_INTERVAL_MS
205
206
append_to_value: (data) =>
207
# this @value is used for copy/paste of the session history and @value_orig for resize/refresh
208
@value_orig += data
209
@value += data.replace(/\x1b\[.{1,5}m|\x1b\].*0;|\x1b\[.*~|\x1b\[?.*l/g,'')
210
211
init_mesg: () =>
212
@terminal.on 'mesg', (mesg) =>
213
if @_ignore or not @is_focused # ignore messages when terminal not in focus (otherwise collaboration is confusing)
214
return
215
try
216
mesg = from_json(mesg)
217
switch mesg.event
218
when 'open'
219
i = 0
220
foreground = false
221
for v in mesg.paths
222
i += 1
223
if i == mesg.paths.length
224
foreground = true
225
if v.file?
226
@_project_actions?.open_file(path:v.file, foreground:foreground)
227
if v.directory? and foreground
228
@_project_actions?.open_directory(v.directory)
229
catch e
230
console.log("issue parsing message -- ", e)
231
232
reconnect_if_no_recent_data: =>
233
#console.log 'check for recent data'
234
if not @_got_remote_data? or new Date() - @_got_remote_data >= 15000
235
#console.log 'reconnecting since no recent data'
236
@session?.reconnect()
237
238
set_session: (session) =>
239
if @session?
240
# Don't allow set_session to be called multiple times, since both sessions could
241
# display data at the same time.
242
console.warn("BUG: set_session called after session already set -- ignoring")
243
return
244
245
# Store the remote session, which is a connection to a HUB
246
# that is in turn connected to a console_server:
247
@session = session
248
249
@_ignore = true
250
@_connected = true
251
@_needs_resize = true
252
253
# Plug the remote session into the terminal.
254
# data = output *from the local terminal* to the remote pty.
255
# This is usually caused by the user typing,
256
# but can also be the result of a device attributes request.
257
@terminal.on 'data', (data) =>
258
if @_ignore
259
return
260
if not @_connected
261
# not connected, so first connect, then write the data.
262
@session.reconnect (err) =>
263
if not err
264
@session.write_data(data)
265
return
266
267
@session.write_data(data)
268
269
# In case nothing comes back soon, we reconnect -- maybe the session is dead?
270
# We wait 20x the ping time (or 10s), so if connection is slow, this won't
271
# constantly reconnect, but it is very fast in case the connection is fast.
272
{webapp_client} = require('./webapp_client')
273
latency = webapp_client.latency()
274
if latency?
275
delay = Math.min(10000, latency*20)
276
setTimeout(@reconnect_if_no_recent_data, delay)
277
278
# The terminal receives a 'set my title' message.
279
@terminal.on 'title', (title) => @set_title(title)
280
281
@reset()
282
283
# We resize the terminal first before replaying history, etc. so that it looks better,
284
# and also the terminal has initialized so it can show the history.
285
@resize_terminal()
286
@config_session()
287
288
set_state_connected: =>
289
@element.find(".webapp-console-terminal").css('opacity':'1')
290
@element.find("a[href=\"#refresh\"]").removeClass('btn-success').find(".fa").removeClass('fa-spin')
291
292
set_state_disconnected: =>
293
@element.find(".webapp-console-terminal").css('opacity':'.5')
294
@element.find("a[href=\"#refresh\"]").addClass('btn-success').find(".fa").addClass('fa-spin')
295
296
config_session: () =>
297
# The remote server sends data back to us to display:
298
@session.on 'data', (data) =>
299
# console.log("terminal got #{data.length} characters -- '#{data}'")
300
@_got_remote_data = new Date()
301
@set_state_connected() # connected if we are getting data.
302
if @_rendering_is_paused
303
@_render_buffer += data
304
else
305
@render(data)
306
307
if @_needs_resize
308
@resize()
309
310
@session.on 'reconnecting', () =>
311
#console.log('terminal: reconnecting')
312
@_reconnecting = new Date()
313
@set_state_disconnected()
314
315
@session.on 'reconnect', () =>
316
delete @_reconnecting
317
partial_code = false
318
@_needs_resize = true # causes a resize when we next get data.
319
@_connected = true
320
@_got_remote_data = new Date()
321
@set_state_connected()
322
@reset()
323
if @session.init_history?
324
#console.log("writing history")
325
try
326
@_ignore = true
327
@terminal.write(@session.init_history)
328
catch e
329
console.log(e)
330
#console.log("recording history for copy/paste buffer")
331
@append_to_value(@session.init_history)
332
333
# On first write we ignore any queued terminal attributes responses that result.
334
@terminal.queue = ''
335
@terminal.showCursor()
336
337
@session.on 'close', () =>
338
@_connected = false
339
340
# Initialize pinging the server to keep the console alive
341
#@_init_session_ping()
342
343
if @session.init_history?
344
#console.log("session -- history.length='#{@session.init_history.length}'")
345
try
346
@terminal.write(@session.init_history)
347
catch e
348
console.log(e)
349
# On first write we ignore any queued terminal attributes responses that result.
350
@terminal.queue = ''
351
@append_to_value(@session.init_history)
352
353
@terminal.showCursor()
354
@resize()
355
356
render: (data) =>
357
#console.log "render '#{data}'"
358
if not data?
359
return
360
try
361
@terminal.write(data)
362
@append_to_value(data)
363
364
if @scrollbar_nlines < @terminal.ybase
365
@update_scrollbar()
366
367
setTimeout(@set_scrollbar_to_term, 10)
368
# See https://github.com/sagemathinc/cocalc/issues/1301
369
#redux.getProjectActions(@project_id).flag_file_activity(@path)
370
catch e
371
# WARNING -- these are all basically bugs, I think...
372
# That said, try/catching them is better than having
373
# the whole terminal just be broken.
374
console.warn("terminal error -- ",e)
375
376
reset: () =>
377
# reset the terminal to clean; need to do this on connect or reconnect.
378
#$(@terminal.element).css('opacity':'0.5').animate(opacity:1, duration:500)
379
@value = @value_orig = ''
380
@scrollbar_nlines = 0
381
@scrollbar.empty()
382
@terminal.reset()
383
384
update_scrollbar: () =>
385
while @scrollbar_nlines < @terminal.ybase
386
@scrollbar.append($("<br>"))
387
@scrollbar_nlines += 1
388
389
pause_rendering: (immediate) =>
390
if @_rendering_is_paused
391
return
392
#console.log 'pause_rendering'
393
@_rendering_is_paused = true
394
if not @_render_buffer?
395
@_render_buffer = ''
396
f = () =>
397
if @_rendering_is_paused
398
@element.find("a[href=\"#pause\"]").addClass('btn-success').find('i').addClass('fa-play').removeClass('fa-pause')
399
if immediate
400
f()
401
else
402
setTimeout(f, 500)
403
@opts.on_pause?()
404
405
unpause_rendering: () =>
406
if not @_rendering_is_paused
407
return
408
#console.log 'unpause_rendering'
409
@_rendering_is_paused = false
410
f = () =>
411
@render(@_render_buffer)
412
@_render_buffer = ''
413
# Do the actual rendering the next time around, so that the copy operation completes with the
414
# current selection instead of the post-render empty version.
415
setTimeout(f, 0)
416
@element.find("a[href=\"#pause\"]").removeClass('btn-success').find('i').addClass('fa-pause').removeClass('fa-play')
417
@opts.on_unpause?()
418
419
#######################################################################
420
# Private Methods
421
#######################################################################
422
423
_on_pause_button_clicked: (e) =>
424
if @_rendering_is_paused
425
@unpause_rendering()
426
else
427
@pause_rendering(true)
428
return false
429
430
_init_rendering_pause: () =>
431
432
btn = @element.find("a[href=\"#pause\"]").click (e) =>
433
@user_is_active()
434
if @_rendering_is_paused
435
@unpause_rendering()
436
else
437
@pause_rendering(true)
438
return false
439
440
e = @element.find(".webapp-console-terminal")
441
e.mousedown () =>
442
@user_is_active()
443
@pause_rendering(false)
444
445
e.mouseup () =>
446
@user_is_active()
447
if not getSelection().toString()
448
@unpause_rendering()
449
return
450
s = misc_page.get_selection_start_node()
451
if s.closest(e).length == 0
452
# nothing in the terminal is selected
453
@unpause_rendering()
454
455
e.on 'copy', =>
456
@user_is_active()
457
@unpause_rendering()
458
setTimeout(@focus, 5) # must happen in next cycle or copy will not work due to loss of focus.
459
460
_init_colors: () =>
461
colors = Terminal.color_schemes[@opts.color_scheme].colors
462
for i in [0...16]
463
Terminal.colors[i] = colors[i]
464
465
if colors.length > 16
466
Terminal.defaultColors =
467
fg: colors[16]
468
bg: colors[17]
469
else
470
Terminal.defaultColors =
471
fg: colors[15]
472
bg: colors[0]
473
474
Terminal.colors[256] = Terminal.defaultColors.bg
475
Terminal.colors[257] = Terminal.defaultColors.fg
476
477
mark_file_use: () =>
478
redux.getActions('file_use').mark_file(@project_id, @path, 'edit')
479
480
client_keydown: (ev) =>
481
#console.log("client_keydown")
482
@allow_resize = true
483
484
if @_ignore
485
# no matter what cancel ignore if the user starts typing, since we absolutely must not loose anything they type.
486
@_ignore = false
487
488
@mark_file_use()
489
@user_is_active()
490
491
if ev.ctrlKey and ev.shiftKey
492
switch ev.keyCode
493
when 190 # "control-shift->"
494
@_increase_font_size()
495
return false
496
when 188 # "control-shift-<"
497
@_decrease_font_size()
498
return false
499
if (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)
500
#console.log("resetting hidden textarea")
501
#console.log("clear hidden text area paste bin")
502
# clear the hidden textarea pastebin, since otherwise
503
# everything that the user typed before pasting appears
504
# in the paste, which is very, very bad.
505
# NOTE: we could do this on all keystrokes. WE restrict as above merely for efficiency purposes.
506
# See http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript
507
@textarea.val('')
508
if @_rendering_is_paused and not (ev.ctrlKey or ev.metaKey or ev.altKey)
509
@unpause_rendering()
510
511
_increase_font_size: () =>
512
@opts.font.size += 1
513
if @opts.font.size <= 159
514
@_font_size_changed()
515
516
_decrease_font_size: () =>
517
if @opts.font.size >= 2
518
@opts.font.size -= 1
519
@_font_size_changed()
520
521
_font_size_changed: () =>
522
@opts.editor?.local_storage("font-size",@opts.font.size)
523
$(@terminal.element).css('font-size':"#{@opts.font.size}px")
524
@element.find(".webapp-console-font-indicator-size").text(@opts.font.size)
525
@element.find(".webapp-console-font-indicator").stop().show().animate(opacity:1).fadeOut(duration:8000)
526
@resize()
527
528
_init_font_make_default: () =>
529
@element.find("a[href=\"#font-make-default\"]").click () =>
530
@user_is_active()
531
redux.getTable('account').set(terminal:{font_size:@opts.font.size})
532
return false
533
534
_init_default_settings: () =>
535
settings = redux.getStore('account').get_terminal_settings()
536
if not @opts.font.size?
537
@opts.font.size = settings?.font_size ? 14
538
if not @opts.color_scheme?
539
@opts.color_scheme = settings?.color_scheme ? "default"
540
if not @opts.font.family?
541
@opts.font.family = settings?.font ? "monospace"
542
543
_init_ttyjs: () ->
544
# Create the terminal DOM objects
545
@terminal.open()
546
# Give it our style; there is one in term.js (upstream), but it is named in a too-generic way.
547
@terminal.element.className = "webapp-console-terminal"
548
ter = $(@terminal.element)
549
@element.find(".webapp-console-terminal").replaceWith(ter)
550
551
ter.css
552
'font-family' : @opts.font.family + ", monospace" # monospace fallback
553
'font-size' : "#{@opts.font.size}px"
554
'line-height' : "#{@opts.font.line_height}%"
555
556
# Focus/blur handler.
557
if IS_TOUCH # so keyboard appears
558
@mobile_target = @element.find(".webapp-console-for-mobile").show()
559
@mobile_target.css('width', ter.css('width'))
560
@mobile_target.css('height', ter.css('height'))
561
@_click = (e) =>
562
@user_is_active()
563
t = $(e.target)
564
if t[0]==@mobile_target[0] or t.hasParent(@element).length > 0
565
@focus()
566
else
567
@blur()
568
$(document).on 'click', @_click
569
else
570
@_mousedown = (e) =>
571
@user_is_active()
572
if $(e.target).hasParent(@element).length > 0
573
@focus()
574
else
575
@blur()
576
$(document).on 'mousedown', @_mousedown
577
578
@_mouseup = (e) =>
579
@user_is_active()
580
t = $(e.target)
581
sel = window.getSelection().toString()
582
if t.hasParent(@element).length > 0 and sel.length == 0
583
@_focus_hidden_textarea()
584
$(document).on 'mouseup', @_mouseup
585
586
$(@terminal.element).bind 'copy', (e) =>
587
@user_is_active()
588
# re-enable paste but only *after* the copy happens
589
setTimeout(@_focus_hidden_textarea, 10)
590
591
# call this when deleting the terminal (removing it from DOM, etc.)
592
remove: () =>
593
@session?.close()
594
delete @session
595
@_connected = false
596
if @_mousedown?
597
$(document).off('mousedown', @_mousedown)
598
if @_mouseup?
599
$(document).off('mouseup', @_mouseup)
600
if @_click?
601
$(document).off('click', @_click)
602
603
_focus_hidden_textarea: () =>
604
@textarea.focus()
605
606
_init_fullscreen: () =>
607
fullscreen = @element.find("a[href=\"#fullscreen\"]")
608
exit_fullscreen = @element.find("a[href=\"#exit_fullscreen\"]")
609
fullscreen.on 'click', () =>
610
@user_is_active()
611
@fullscreen()
612
exit_fullscreen.show()
613
fullscreen.hide()
614
return false
615
exit_fullscreen.hide().on 'click', () =>
616
@user_is_active()
617
@exit_fullscreen()
618
exit_fullscreen.hide()
619
fullscreen.show()
620
return false
621
622
_init_buttons: () ->
623
editor = @terminal.editor
624
625
@element.find("a").tooltip(delay:{ show: 500, hide: 100 })
626
627
@element.find("a[href=\"#increase-font\"]").click () =>
628
@user_is_active()
629
@_increase_font_size()
630
return false
631
632
@element.find("a[href=\"#decrease-font\"]").click () =>
633
@user_is_active()
634
@_decrease_font_size()
635
return false
636
637
@element.find("a[href=\"#refresh\"]").click () =>
638
@user_is_active()
639
@session?.reconnect()
640
return false
641
642
@element.find("a[href=\"#paste\"]").click () =>
643
@user_is_active()
644
id = uuid()
645
s = "<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>"
646
bootbox.alert(s)
647
elt = $("##{id}")
648
elt.val(@value).scrollTop(elt[0].scrollHeight)
649
return false
650
651
@element.find("a[href=\"#initfile\"]").click () =>
652
initfn = misc.console_init_filename(@opts.filename)
653
content = initfile_content(@opts.filename)
654
{webapp_client} = require('./webapp_client')
655
webapp_client.exec
656
project_id : @project_id
657
command : "test ! -r '#{initfn}' && echo '#{content}' > '#{initfn}'"
658
bash : true
659
err_on_exit : false
660
cb : (err, output) =>
661
if err
662
alert_message(type:'error', message:"problem creating initfile: #{err}")
663
else
664
@_project_actions?.open_file(path:initfn, foreground:true)
665
666
open_copyable_history: () =>
667
id = uuid()
668
s = "<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>"
669
bootbox.alert(s)
670
elt = $("##{id}")
671
elt.val(@value).scrollTop(elt[0].scrollHeight)
672
673
open_init_file: () =>
674
initfn = misc.console_init_filename(@opts.filename)
675
content = initfile_content(@opts.filename)
676
{webapp_client} = require('./webapp_client')
677
webapp_client.exec
678
project_id : @project_id
679
command : "test ! -r '#{initfn}' && echo '#{content}' > '#{initfn}'"
680
bash : true
681
err_on_exit : false
682
cb : (err, output) =>
683
if err
684
alert_message(type:'error', message:"problem creating initfile: #{err}")
685
else
686
@_project_actions?.open_file(path:initfn, foreground:true)
687
688
_init_input_line: () =>
689
#if not IS_TOUCH
690
# @element.find(".webapp-console-mobile-input").hide()
691
# return
692
693
if not IS_TOUCH
694
@element.find(".webapp-console-mobile-input").hide()
695
696
input_line = @element.find('.webapp-console-input-line')
697
698
input_line.on 'focus', =>
699
@_input_line_is_focused = true
700
@terminal.blur()
701
input_line.on 'blur', =>
702
@_input_line_is_focused = false
703
704
submit_line = () =>
705
x = input_line.val()
706
# Apple text input replaces single and double quotes by some stupid
707
# fancy unicode, which is incompatible with most uses in the terminal.
708
# Here we switch it back. (Note: not doing exactly this renders basically all
709
# dev related tools on iPads very frustrating. Not so, CoCalc :-)
710
x = misc.replace_all(x, '“','"')
711
x = misc.replace_all(x, '”','"')
712
x = misc.replace_all(x, '‘',"'")
713
x = misc.replace_all(x, '’',"'")
714
x = misc.replace_all(x, '–', "--")
715
x = misc.replace_all(x, '—', "---")
716
@_ignore = false
717
@session?.write_data(x)
718
input_line.val('')
719
720
input_line.on 'keydown', (e) =>
721
if e.which == 13
722
e.preventDefault()
723
submit_line()
724
@_ignore = false
725
@session?.write_data("\n")
726
return false
727
else if e.which == 67 and e.ctrlKey
728
submit_line()
729
@terminal.keyDown(keyCode:67, shiftKey:false, ctrlKey:true)
730
731
@element.find(".webapp-console-submit-line").click () =>
732
#@focus()
733
submit_line()
734
@_ignore = false
735
@session?.write_data("\n")
736
return false
737
738
@element.find(".webapp-console-submit-submit").click () =>
739
#@focus()
740
submit_line()
741
return false
742
743
@element.find(".webapp-console-submit-tab").click () =>
744
#@focus()
745
submit_line()
746
@terminal.keyDown(keyCode:9, shiftKey:false)
747
748
@element.find(".webapp-console-submit-esc").click () =>
749
#@focus()
750
submit_line()
751
@terminal.keyDown(keyCode:27, shiftKey:false, ctrlKey:false)
752
753
@element.find(".webapp-console-submit-up").click () =>
754
#@focus()
755
submit_line()
756
@terminal.keyDown(keyCode:38, shiftKey:false, ctrlKey:false)
757
758
@element.find(".webapp-console-submit-down").click () =>
759
#@focus()
760
submit_line()
761
@terminal.keyDown(keyCode:40, shiftKey:false, ctrlKey:false)
762
763
@element.find(".webapp-console-submit-left").click () =>
764
#@focus()
765
submit_line()
766
@terminal.keyDown(keyCode:37, shiftKey:false, ctrlKey:false)
767
768
@element.find(".webapp-console-submit-right").click () =>
769
#@focus()
770
submit_line()
771
@terminal.keyDown(keyCode:39, shiftKey:false, ctrlKey:false)
772
773
@element.find(".webapp-console-submit-ctrl-c").show().click (e) =>
774
#@focus()
775
submit_line()
776
@terminal.keyDown(keyCode:67, shiftKey:false, ctrlKey:true)
777
778
@element.find(".webapp-console-submit-ctrl-b").show().click (e) =>
779
#@focus()
780
submit_line()
781
@terminal.keyDown(keyCode:66, shiftKey:false, ctrlKey:true)
782
783
###
784
@element.find(".webapp-console-up").click () ->
785
vp = editor.getViewport()
786
editor.scrollIntoView({line:vp.from - 1, ch:0})
787
return false
788
789
@element.find(".webapp-console-down").click () ->
790
vp = editor.getViewport()
791
editor.scrollIntoView({line:vp.to, ch:0})
792
return false
793
794
if IS_TOUCH
795
@element.find(".webapp-console-tab").show().click (e) =>
796
@focus()
797
@terminal.keyDown(keyCode:9, shiftKey:false)
798
799
@_next_ctrl = false
800
@element.find(".webapp-console-control").show().click (e) =>
801
@focus()
802
@_next_ctrl = true
803
$(e.target).removeClass('btn-info').addClass('btn-warning')
804
805
@element.find(".webapp-console-esc").show().click (e) =>
806
@focus()
807
@terminal.keyDown(keyCode:27, shiftKey:false, ctrlKey:false)
808
###
809
810
_init_paste_bin: () =>
811
pb = @textarea
812
813
f = (evt) =>
814
@_ignore = false
815
data = pb.val()
816
pb.val('')
817
@session?.write_data(data)
818
819
pb.on 'paste', =>
820
pb.val('')
821
setTimeout(f,5)
822
823
#######################################################################
824
# Public API
825
# Unless otherwise stated, these methods can be chained.
826
#######################################################################
827
828
terminate_session: () =>
829
@session?.terminate_session()
830
831
# enter fullscreen mode
832
fullscreen: () =>
833
h = $(".navbar-fixed-top").height()
834
@element.css
835
position : 'absolute'
836
width : "97%"
837
top : h
838
left : 0
839
right : 0
840
bottom : 1
841
842
$(@terminal.element).css
843
position : 'absolute'
844
width : "100%"
845
top : "3.5em"
846
bottom : 1
847
848
@resize()
849
850
# exit fullscreen mode
851
exit_fullscreen: () =>
852
for elt in [$(@terminal.element), @element]
853
elt.css
854
position : 'relative'
855
top : 0
856
width : "100%"
857
@resize()
858
859
refresh: () =>
860
@terminal.refresh(0, @opts.rows-1)
861
@terminal.showCursor()
862
863
864
# Determine the current size (rows and columns) of the DOM
865
# element for the editor, then resize the renderer and the
866
# remote PTY.
867
resize: =>
868
if not @user_was_recently_active()
869
return
870
871
if not @session?
872
# don't bother if we don't even have a remote connection
873
# FUTURE: could queue this up to send
874
return
875
876
if not @_connected
877
return
878
879
if not @value
880
# Critical that we wait to receive something before doing any sort of resize; otherwise,
881
# the terminal will get "corrupted" with control codes.
882
return
883
884
@resize_terminal()
885
886
# Resize the remote PTY
887
resize_code = (cols, rows) ->
888
# See http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
889
# CSI Ps ; Ps ; Ps t
890
# CSI[4];[height];[width]t
891
return CSI + "4;#{rows};#{cols}t"
892
893
# console.log 'connected: sending resize code'
894
@session.write_data(resize_code(@opts.cols, @opts.rows))
895
896
@full_rerender()
897
898
# Refresh depends on correct @opts being set!
899
@refresh()
900
901
@_needs_resize = false
902
903
full_rerender: =>
904
value = @value_orig
905
@reset()
906
# start ignoring terminal output until the user explicitly does something (keys or paste)
907
@_ignore = true
908
@render(value)
909
910
resize_terminal: () =>
911
# Determine size of container DOM.
912
# Determine the average width of a character by inserting 10 characters,
913
# seeing how wide that is, and dividing by 10. The result is typically not
914
# an integer, which is why we have to use multiple characters.
915
@_c = $("<span>Term-inal&nbsp;</span>").prependTo(@terminal.element)
916
character_width = @_c.width()/10
917
@_c.remove()
918
elt = $(@terminal.element)
919
920
# The above style trick for character width is not reliable for getting the height of each row.
921
# For that we use the terminal itself, since it already has rows, and hopefully at least
922
# one row has something in it (a div).
923
#
924
# The row height is in fact *NOT* constant -- it can vary by 1 (say) depending
925
# on what is in the row. So we compute the maximum line height, which is safe, so
926
# long as we throw out the outliers.
927
heights = ($(x).height() for x in elt.children())
928
# Eliminate weird outliers that sometimes appear (e.g., for last row); yes, this is
929
# pretty crazy...
930
heights = (x for x in heights when x <= heights[0] + 2)
931
row_height = Math.max( heights ... )
932
933
if character_width == 0 or row_height == 0
934
# The editor must not yet be visible -- do nothing
935
return
936
937
# Determine the number of columns from the width of a character, computed above.
938
font_size = @opts.font.size
939
new_cols = Math.max(1, Math.floor(elt.width() / character_width))
940
941
# Determine number of rows from the height of the row, as computed above.
942
height = elt.height()
943
if IS_TOUCH
944
height -= 60
945
new_rows = Math.max(1, Math.floor(height / row_height))
946
947
# Resize the renderer
948
@terminal.resize(new_cols, new_rows)
949
950
# Record new size
951
@opts.cols = new_cols
952
@opts.rows = new_rows
953
954
set_scrollbar_to_term: () =>
955
if @terminal.ybase == 0 # less than 1 page of text in buffer
956
@scrollbar.hide()
957
return
958
else
959
@scrollbar.show()
960
961
if @ignore_scroll
962
return
963
@ignore_scroll = true
964
f = () =>
965
@ignore_scroll = false
966
setTimeout(f, 100)
967
max_scrolltop = @scrollbar[0].scrollHeight - @scrollbar.height()
968
@scrollbar.scrollTop(max_scrolltop * @terminal.ydisp / @terminal.ybase)
969
970
set_term_to_scrollbar: () =>
971
max_scrolltop = @scrollbar[0].scrollHeight - @scrollbar.height()
972
ydisp = Math.floor( @scrollbar.scrollTop() * @terminal.ybase / max_scrolltop)
973
@terminal.ydisp = ydisp
974
@terminal.refresh(0, @terminal.rows-1)
975
976
console_is_open: () => # not chainable
977
return @element.closest(document.documentElement).length > 0
978
979
blur: () =>
980
if focused_console == @
981
focused_console = undefined
982
983
@is_focused = false
984
985
try
986
@terminal.blur()
987
catch e
988
# WARNING: probably should investigate term.js issues further(?)
989
# ignore -- sometimes in some states the terminal code can raise an exception when explicitly blur-ing.
990
# This would totally break the client, which is bad, so we catch is.
991
$(@terminal.element).addClass('webapp-console-blur').removeClass('webapp-console-focus')
992
993
focus: (force) =>
994
if @_reconnecting? and new Date() - @_reconnecting > 10000
995
# reconnecting not working, so try again. Also, this handles the case
996
# when terminal switched to reconnecting state, user closed computer, comes
997
# back later, etc. Without this, would not attempt to reconnect until
998
# user touches keys.
999
@reconnect_if_no_recent_data()
1000
1001
if @is_focused and not force
1002
return
1003
1004
# focusing the term blurs the textarea, so we save that fact here,
1005
# so that the textarea.on 'blur' knows why it just blured
1006
@_focusing = true
1007
1008
focused_console = @
1009
@is_focused = true
1010
@textarea.blur()
1011
$(@terminal.element).focus()
1012
1013
if not IS_TOUCH
1014
@_focus_hidden_textarea()
1015
@terminal.focus()
1016
1017
$(@terminal.element).addClass('webapp-console-focus').removeClass('webapp-console-blur')
1018
setTimeout((()=>delete @_focusing), 5) # critical!
1019
1020
set_title: (title) ->
1021
@opts.set_title?(title)
1022
@element.find(".webapp-console-title").text(title)
1023
1024
1025
exports.Console = Console
1026
1027
$.fn.extend
1028
webapp_console: (opts={}) ->
1029
@each () ->
1030
t = $(this)
1031
if opts == false
1032
# disable existing console
1033
con = t.data('console')
1034
if con?
1035
con.remove()
1036
return t
1037
else
1038
opts0 = copy(opts)
1039
opts0.element = this
1040
return t.data('console', new Console(opts0))
1041
1042
1043