Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / console / repl.nas
1 var _REPL_dbg_level = "debug";
2 #var _REPL_dbg_level = "alert";
3
4 var REPL = {
5         df_status: 0,
6         whitespace: [" ", "\t", "\n", "\r"],
7         end_statement: [";", ","],
8         statement_types: [
9                 "for", "foreach", "forindex",
10                 "while", "else", "func", "if", "elsif"
11         ],
12         operators_binary_unary: [
13                 "~", "+", "-", "*", "/",
14                 "!", "?", ":", ".", ",",
15                 "<", ">", "=", "|", "&", "^"
16         ],
17         brackets: {
18                 "(":")",
19                 "[":"]",
20                 "{":"}",
21         },
22         str_chars: ["'", '"', "`"],
23         brackets_rev: {},
24         brackets_start: [], brackets_end: [],
25         new: func(placement, name="<repl>", keep_history=1, namespace=nil) {
26                 if (namespace == nil) namespace = {};
27                 elsif (typeof(namespace) == 'scalar') namespace = globals[namespace];
28                 if (typeof(namespace) != 'hash') die("bad namespace!");
29                 var m = {
30                         parents: [REPL],
31                         placement: placement,
32                         name: name,
33                         keep_history: keep_history,
34                         namespace: namespace,
35                         history: [],
36                         current: nil,
37                 };
38                 return m;
39         },
40         execute: func() {
41                 var code = string.join("\n", me.current.line);
42
43                 me.current = nil;
44
45                 printlog(_REPL_dbg_level, "compiling code..."~debug.string(code));
46
47                 var fn = call(func compile(code, me.name), nil, var err=[]);
48                 if (size(err)) {
49                         var msg = err[0];
50                         var prefix = "Parse error: ";
51                         if (substr(msg, 0, size(prefix)) == prefix)
52                                 msg = substr(msg, size(prefix));
53                         var (msg, line) = split(" at line ", msg);
54                         #debug.dump(err);
55                         me.placement.handle_parse_error(msg, me.name, line); # message, (file)name, line number
56                         return 0;
57                 }
58                 var res = call(bind(fn, globals), nil, nil, me.namespace, err);
59                 if (size(err)) {
60                         me.placement.handle_runtime_error(err); # err vec
61                         return 0;
62                 }
63                 me.placement.display_result(res);
64                 return 1;
65         },
66         _is_str_char: func(char) {
67                 foreach (var c; me.str_chars)
68                         if (c == char or c[0] == char) return 1;
69                 return 0;
70         },
71         _handle_level: func(level, str, line_number) {
72                 if (size(str) != 1)
73                         var str = substr(str, 0, 1);
74                 if (contains(me.brackets, str)) {
75                         append(level, str);
76                         printlog(_REPL_dbg_level, "> level add "~str);
77                         return 1;
78                 } elsif (contains(me.brackets_rev, str)) {
79                         var l = pop(level);
80                         if (l == nil) {
81                                 me.placement.handle_parse_error("extra closing bracket "'"'~str~'"', me.name, line_number);
82                                 return nil;
83                         } elsif (me.brackets[l] != str) {
84                                 me.placement.handle_parse_error("bracket mismatch: "~me.brackets[l]~" vs "~str, me.name, line_number);
85                                 return nil;
86                         } else {
87                                 printlog(_REPL_dbg_level, "< level pop "~str);
88                                 return 1;
89                         }
90                 }
91                 return 0;
92         },
93         get_input: func() {
94                 var lines = me.placement.get_line();
95                 if (lines == nil or string.trim(lines) == "") return me.df_status;
96                 var ls = split("\n", lines); var lines = [];
97                 foreach (var l; ls) lines ~= split("\r", l);
98                 foreach (var line; lines) {
99                         var len = size(line);
100                         if (me.current == nil)
101                                 me.current = {
102                                         line: [],
103                                         brackets: [],
104                                         level: [],
105                                         statement: nil,
106                                         statement_level: nil,
107                                         last_operator: nil,
108                                 };
109                         for (var i=0; i<len; i+=1) {
110                                 if (string.isxspace(line[i])) continue;
111                                 if (size(me.current.level) and me._is_str_char(me.current.level[-1])) {
112                                         me.current.last_operator = nil;
113                                         if (line[i] == `\\`) {
114                                                 i += 1; # skip the next character
115                                                 printlog(_REPL_dbg_level, "  skip backslash");
116                                         } elsif (line[i] == me.current.level[-1][0]) {
117                                                 printlog(_REPL_dbg_level, "< out of string with "~me.current.level[-1]);
118                                                 pop(me.current.level);
119                                         }
120                                         continue;
121                                 }
122                                 if (line[i] == `#`) {
123                                         while(i<len and line[i] != `\n` and line[i] != `\r`) i+=1;
124                                         continue;
125                                 }
126                                 if (me.current.statement != nil) {
127                                         me.current.last_operator = nil;
128                                         if (me.current.statement_level == size(me.current.level) and
129                                                      (line[i] == `;` or line[i] == `,`)) {
130                                                 printlog(_REPL_dbg_level, "statement ended by ;/,");
131                                                 me.current.statement = nil;
132                                                 me.current.statement_level = nil;
133                                         } else {
134                                                 var ret = me._handle_level(me.current.level, chr(line[i]), size(me.current.line)+1);
135                                                 if (ret == nil) {# error
136                                                         me.current = nil;
137                                                         return 0;
138                                                 } elsif (me.current.statement_level > size(me.current.level)) {
139                                                         printlog(_REPL_dbg_level, "statement ended by level below");
140                                                         # cancel out of statement
141                                                         me.current.statement = nil;
142                                                         me.current.statement_level = nil;
143                                                 } elsif (line[i] == `{`) {
144                                                         # cancel out of looking for `;`, because we have a real block here
145                                                         printlog(_REPL_dbg_level, "statement ended by braces");
146                                                         me.current.statement = nil;
147                                                         me.current.statement_level = nil;
148                                                 }
149                                         }
150                                         continue;
151                                 } elsif (string.isalpha(line[i])) {
152                                         me.current.last_operator = nil;
153                                         foreach (var stmt; me.statement_types) {
154                                                 if (substr(line, i, size(stmt)) == stmt and
155                                                         (i+size(stmt) >= len
156                                                          or !string.isalnum(line[i+size(stmt)])
157                                                          and line[i+size(stmt)] != `_`)) {
158                                                         printlog(_REPL_dbg_level, "found: "~stmt);
159                                                         me.current.statement = stmt;
160                                                         me.current.statement_level = size(me.current.level);
161                                                         i += size(stmt)-1;
162                                                         break;
163                                                 }
164                                         }
165                                 } elsif (me._is_str_char(line[i])) {
166                                         me.current.last_operator = nil;
167                                         append(me.current.level, chr(line[i]));
168                                         printlog(_REPL_dbg_level, "> into string with "~me.current.level[-1]);
169                                 } else {
170                                         var ret = me._handle_level(me.current.level, chr(line[i]), size(me.current.line)+1);
171                                         me.current.last_operator = nil;
172                                         if (ret == nil) # error
173                                                 return 0;
174                                         elsif (ret == 0) {
175                                                 foreach (var o; me.operators_binary_unary)
176                                                         if (line[i] == o[0])
177                                                         { me.current.last_operator = o; printlog(_REPL_dbg_level, "found operator "~o); break }
178                                         }
179                                 }
180                         }
181                         append(me.current.line, line);
182                         if (me.keep_history)
183                                 append(me.history, {
184                                         type: "input",
185                                         line: line,
186                                 });
187                 }
188                 var execute = (me.current.statement == nil and me.current.last_operator == nil and !size(me.current.level));
189                 if (execute) {
190                         me.df_status = 0;
191                         return me.execute();
192                 } else
193                 return (me.df_status = -1);
194         },
195 };
196 foreach (var b; keys(REPL.brackets)) {
197         var v = REPL.brackets[b];
198         append(REPL.brackets_start, b);
199         append(REPL.brackets_end,   v);
200         REPL.brackets_rev[v] = b;
201 }
202
203 var CanvasPlacement = {
204         instances: [],
205         current_instance: nil,
206         keys: [
207                 "ESC",         "Exit/close this dialog",
208                 "Ctrl-d",      "Same as ESC",
209                 "Ctrl-v",      "Insert text (at the end of the current line)",
210                 "Ctrl-c",      "Copy the current line of text",
211                 "Ctrl-x",      "Copy and delete the current line of text",
212                 "Up",          "Previous line in history",
213                 "Down",        "Next line in history",
214                 "Left",         nil,
215                 "Right",        nil,
216                 "Shift+Left",   nil,
217                 "Shift+Right",  nil,
218         ],
219         translations: {
220                 "bad-result": "[Error: cannot display output]",
221                 "key-not-mapped": "[Not Implemented]",
222                 "help": "Welcome to the Nasal REPL Interpreter. Press any key to "
223                         "exit this message, ESC to exit the dialog (at any time "
224                         "afterwards), and type away to test code :).\n\nNote: "
225                         "this dialog will capture nearly all key-presses, so don't "
226                         "try to fly with the keyboard while this is open!"
227                         "\n\nImportant keys:",
228         },
229         styles: {
230                 "default": {
231                         size: [600, 300],
232                         separate_lines: 1,
233                         window_style: "default",
234                         padding: 5,
235                         max_output_chars: 200,
236                         colors: {
237                                 # TODO: integrate colors from debug.nas?
238                                 text: [1,1,1],
239                                 text_fill: nil,
240                                 background: [0.1,0.06,0.4,0.3],
241                                 error: [1,0.2,0.1],
242                         },
243                         font_size: 17,
244                         font_file: "LiberationFonts/LiberationMono-Bold.ttf",
245                         font_aspect_ratio: 1.5,
246                         font_max_width: nil,
247                 },
248                 "transparent-blue": {
249                         size: [600, 300],
250                         separate_lines: 1,
251                         window_style: nil,
252                         padding: 5,
253                         max_output_chars: 200,
254                         colors: {
255                                 text: [1,1,1],
256                                 text_fill: nil,
257                                 background: [0.1,0.06,0.4,0.3],
258                                 error: [1,0.2,0.1],
259                         },
260                         font_size: 17,
261                         font_file: "LiberationFonts/LiberationMono-Bold.ttf",
262                         font_aspect_ratio: 1.5,
263                         font_max_width: nil,
264                 },
265                 "transparent-red": {
266                         size: [600, 300],
267                         separate_lines: 1,
268                         window_style: nil,
269                         padding: 5,
270                         max_output_chars: 200,
271                         colors: {
272                                 text: [1,1,1],
273                                 text_fill: nil,
274                                 background: [0.8,0.06,0.07,0.4],
275                                 error: [1,0.2,0.1],
276                         },
277                         font_size: 17,
278                         font_file: "LiberationFonts/LiberationMono-Bold.ttf",
279                         font_aspect_ratio: 1.5,
280                         font_max_width: nil,
281                 },
282                 "canvas-default": {
283                         size: [600, 300],
284                         separate_lines: 1,
285                         window_style: "default",
286                         padding: 5,
287                         max_output_chars: 87,
288                         colors: {
289                                 text: [0.8,0.86,0.8],
290                                 text_fill: nil,
291                                 background: [0.05,0.03,0.2],
292                                 error: [1,0.2,0.1],
293                         },
294                         font_size: 17,
295                         font_file: "LiberationFonts/LiberationMono-Bold.ttf",
296                         font_aspect_ratio: 1.5,
297                         font_max_width: nil,
298                         #font_max_width: 588,
299                 },
300         },
301         new: func(name="<canvas-repl>", style="canvas-default") {
302                 if (typeof(style) == 'scalar') {
303                         style = CanvasPlacement.styles[style];
304                 }
305                 if (typeof(style) != 'hash') die("bad style");
306                 var m = {
307                         parents: [CanvasPlacement, style],
308                         state: "startup",
309                         listeners: [],
310                         window: canvas.Window.new(style.size, style.window_style, "REPL-interpreter-"~name),
311                         lines_of_text: [],
312                         history: [],
313                         curr: 0,
314                         completion_pos: 0,
315                         #tabs: [], # TODO: support multiple tabs
316                 };
317                 m.window.set("title", "Nasal REPL Interpreter");
318                 #debug.dump(m.window._node);
319                 m.window.del = func() {
320                         delete(me, "del");
321                         me.del(); # inherited canvas.Window.del();
322                         m.window = nil;
323                         m.del();
324                 };
325                 if (m.window_style != nil) m.window.setBool("resize", 1);
326                 m.canvas = m.window.createCanvas()
327                                    .setColorBackground(m.colors.background);
328                 m.group = m.canvas.createGroup("content");
329                 m.vbox = canvas.VBoxLayout.new();
330                 m.window.setLayout(m.vbox);
331                 m.scroll = canvas.gui.widgets
332                           .ScrollArea.new(m.group, canvas.style, {});
333                 m.scroll.setColorBackground(m.colors.background);
334                 m.vbox.addItem(m.scroll);
335                 m.group = m.scroll.getContent();
336                 m.create_msg();
337                 m.text_group = m.group.createChild("group", "text-display");
338                 m.text = nil;
339                 m.cursor = m.group.createChild("path")
340                         .moveTo(0, -m.padding)
341                         .lineTo(0, -11-m.padding)
342                         .setStrokeLineWidth(2)
343                         .setColor(m.colors.text)
344                         .hide();
345                 m.repl = REPL.new(placement:m, name:name);
346                 m.window.addEventListener("keydown", func(event) {
347                         var modifiers = {
348                                 "shift":event.shiftKey,
349                                 "ctrl":event.ctrlKey,
350                                 "alt":event.altKey,
351                                 "meta":event.metaKey
352                         };
353                         if (m.handle_key(event.key, modifiers, event.keyCode))
354                                 #keyN.setValue(-1);           # drop key event
355                 });
356                 m.update();
357                 append(CanvasPlacement.instances, m);
358                 return m;
359         },
360         del: func() {
361                 if (me.window != nil)
362                 { me.window.del(); me.window = nil }
363                 foreach (var l; me.listeners)
364                         removelistener(l);
365                 setsize(me.listeners, 0);
366                 forindex (var i; CanvasPlacement.instances)
367                         if (CanvasPlacement.instances[i] == me) {
368                                 CanvasPlacement.instances[i] = CanvasPlacement.instances[-1];
369                                 pop(CanvasPlacement.instances);
370                                 break;
371                         }
372         },
373         add_char: func(char, reset_view=0) {
374                 me.reset_input_from_history();
375                 me.input ~= chr(char);
376                 me.text.appendText(chr(char));
377                 if (reset_view) me.reset_view();
378                 return nil;
379         },
380         add_text: func(text, reset_view=0) {
381                 me.reset_input_from_history();
382                 me.input ~= text;
383                 me.text.appendText(text);
384                 if (reset_view) me.reset_view();
385                 return nil;
386         },
387         remove_char: func(reset_view=0) {
388                 me.reset_input_from_history();
389                 me.input = substr(me.input, 0, size(me.input) - 1);
390                 var t = me.text.get("text");
391                 if (size(t) <= me.text.stop) return nil;
392                 me.text.setText(substr(t, 0, size(t)-1));
393                 if (reset_view) me.reset_view();
394                 return t[-1];
395         },
396         clear_input: func(reset_view=0) {
397                 me.reset_input_from_history();
398                 var ret = me.input;
399                 me.input = "";
400                 var t = me.text.get("text");
401                 me.text.setText(substr(t, 0, me.text.stop));
402                 if (reset_view) me.reset_view();
403                 return ret;
404         },
405         replace_line: func(replacement, replace_input=1, reset_view=0) {
406                 if (replace_input) me.input = replacement;
407                 var t = me.text.get("text");
408                 me.text.setText(substr(t, 0, me.text.stop)~replacement);
409                 if (reset_view) me.reset_view();
410                 return nil;
411         },
412         add_line: func(text, reset_text=1, reset_view=0) {
413                 me.create_line(reset_text);
414                 me.text.appendText(text);
415                 if (reset_view) me.reset_view();
416         },
417         new_prompt: func() {
418                 me.add_line(">>> ");
419                 me.text.stop = size(me.text.get("text"));
420         },
421         continue_line: func(reset_text=1) {
422                 me.add_line("... ", reset_text);
423                 me.text.stop = size(me.text.get("text"));
424         },
425         reset_input_from_history: func(reset_view=0) {
426                 if (me.curr < size(me.history)) {
427                         me.input = me.history[me.curr];
428                         me.curr = size(me.history);
429                 }
430                 if (reset_view) me.reset_view();
431         },
432         reset_view: func() {
433                 me.group.update();
434                 me.scroll.scrollToLeft().scrollToBottom();
435         },
436         set_line_color: func(color) {
437                 if (me.separate_lines)
438                         # Only change colors if this is its own line
439                         me.text.setColor(color);
440         },
441         set_line_font: func(font) {
442                 if (me.separate_lines)
443                         # Only change font if this is its own line
444                         me.text.setFont(font);
445         },
446         clear: func() {
447                 me.text.del();
448                 foreach (var t; me.lines_of_text)
449                         t.del();
450                 setsize(me.history, 0);
451                 me.curr = 0;
452                 me.input = "";
453                 me.text = nil;
454                 setsize(me.lines_of_text, 0);
455                 me.reset_view();
456         },
457         create_msg: func() {
458                 # Text drawing mode: text and maybe a bounding box
459                 var draw_mode = canvas.Text.TEXT + (me.colors.text_fill != nil ? canvas.Text.FILLEDBOUNDINGBOX : 0);
460
461                 me.msg = me.group.createChild("group", "startup-message");
462                 me.msg.text = me.msg.createChild("text", "help")
463                         .setTranslation(me.padding, me.padding+10)
464                         .setAlignment("left-baseline")
465                         .setFontSize(me.font_size, me.font_aspect_ratio)
466                         .setFont(me.font_file)
467                         .setColor(me.colors.text)
468                         .setDrawMode(draw_mode)
469                         .setMaxWidth(me.window.get("content-size[0]") - me.padding)
470                         .setText(me.gettranslation("help"));
471                 if (me.colors.text_fill != nil)
472                         me.msg.text.setColorFill(me.colors.text_fill);
473                 me.msg.text.update();
474                 #debug.dump(me.msg.text.getTransformedBounds());
475                 me.msg.left_col = me.msg.createChild("text", "keys")
476                         .setTranslation(me.padding, me.msg.text.getTransformedBounds()[3] + 30)
477                         .setAlignment("left-baseline")
478                         .setFontSize(me.font_size, me.font_aspect_ratio)
479                         .setFont(me.font_file)
480                         .setColor(me.colors.text)
481                         .setDrawMode(draw_mode);
482                 if (me.colors.text_fill != nil)
483                         me.msg.left_col.setColorFill(me.colors.text_fill);
484                 me.msg.left_col.update();
485                 for (var i=0; i<size(me.keys); i+=2) {
486                         if (i) me.msg.left_col.appendText("\n");
487                         me.msg.left_col.appendText("- "~me.keys[i]);
488                 }
489                 #debug.dump(me.msg.left_col.getTransformedBounds());
490                 me.msg.right_col = me.msg.createChild("text", "keys")
491                         .setTranslation(me.msg.left_col.getTransformedBounds()[2] + 20,
492                                         me.msg.text.getTransformedBounds()[3] + 30)
493                         .setAlignment("left-baseline")
494                         .setFontSize(me.font_size, me.font_aspect_ratio)
495                         .setFont(me.font_file)
496                         .setColor(me.colors.text)
497                         .setDrawMode(draw_mode);
498                 if (me.colors.text_fill != nil)
499                         me.msg.right_col.setColorFill(me.colors.text_fill);
500                 for (var i=0; i<size(me.keys); i+=2) {
501                         if (i) me.msg.right_col.appendText("\n");
502                         desc = me.keys[i+1];
503                         if (desc == nil) desc = me.gettranslation("key-not-mapped");
504                         elsif (desc[-1] != `.`) desc ~= ".";
505                         me.msg.right_col.appendText(desc);
506                 }
507         },
508         create_line: func(reset_text=1) {
509                 # c.f. above, in me.create_msg()
510                 var draw_mode = canvas.Text.TEXT + (me.colors.text_fill != nil ? canvas.Text.FILLEDBOUNDINGBOX : 0);
511
512                 if (reset_text) me.input = "";
513                 # If we only use one line, and one exists, things are simple:
514                 if (!me.separate_lines and me.text != nil) {
515                         me.text.appendText("\n");
516                         return;
517                 }
518                 # Else, we have to create a new line
519                 if (me.text != nil)
520                         append(me.lines_of_text, me.text);
521                 me.text = me.text_group
522                         .createChild("text", "input"~size(me.lines_of_text))
523                         .setAlignment("left-baseline")
524                         .setFontSize(me.font_size, me.font_aspect_ratio)
525                         .setFont(me.font_file)
526                         .setColor(me.colors.text)
527                         .setDrawMode(draw_mode)
528                         .setText(size(me.lines_of_text) ? ">" : ""); # FIXME: hack, canvas::Text needs a printing character
529                                                                      # on the first line in order to recognize the newlines ?
530                 if (me.colors.text_fill != nil)                       
531                         me.text.setColorFill(me.colors.text_fill);
532                 if (me.font_max_width != nil)
533                         if (me.font_max_width < 0)
534                                 me.text.setMaxWidth(me.window.get("content-size[0]") - me.font_max_width);
535                         else
536                                 me.text.setMaxWidth(me.font_max_width);
537
538                 foreach (var t; me.lines_of_text)
539                         me.text.appendText("\n");
540         },
541         update: func() {
542                 #debug.dump(me.text.getTransformedBounds());
543                 if (me.state != "startup")
544                         me.cursor.setTranslation(
545                                 me.text.getTransformedBounds()[2] + 6,
546                                 me.text.getTransformedBounds()[3] + 5
547                         ).show();
548                 me.scroll.update();
549         },
550         handle_key: func(key, modifiers, keyCode) {
551                 var modifier_str = "";
552                 foreach (var m; keys(modifiers)) {
553                         if (modifiers[m])
554                                 modifier_str ~= substr(m,0,1);
555                 }
556                 if (me.state == "startup") {
557                         me.msg.del(); me.msg = nil;
558                         me.new_prompt(); # initialize a new line
559                         me.text.stop = size(me.text.get("text"));
560                         me.state = "accepting input";
561
562                 } elsif (!contains({"s":,"c":,"":}, modifier_str)) {
563                         return 0; # had extra modifiers, reject this event
564
565                 } elsif (modifiers.ctrl) {
566                         if (keyCode == `c`) {
567                                 printlog(_REPL_dbg_level, "ctrl+c: "~debug.string(me.input));
568                                 me.reset_input_from_history();
569                                 if( size(me.input) and !clipboard.setText(me.input) )
570                                         print("Failed to write to clipboard");
571                         } elsif (keyCode == `x`) {
572                                 printlog(_REPL_dbg_level, "ctrl+x");
573                                 me.reset_input_from_history();
574                                 if( size(me.input) and !clipboard.setText(me.clear_input()) )
575                                         print("Failed to write to clipboard");
576                         } elsif (keyCode == `v`) {
577                                 var input = clipboard.getText();
578                                 printlog(_REPL_dbg_level, "ctrl+v: "~debug.string(input));
579                                 me.reset_input_from_history();
580                                 var abnormal = func string.iscntrl(input[j]) or (string.isxspace(input[j]) and input[j] != ` `) or !string.isascii(input[j]);
581                                 var i=0;
582                                 while (i<size(input)) {
583                                         for (var j=i; j<size(input); j+=1)
584                                                 if (abnormal()) break;
585                                         if (j != i) me.add_text(substr(input, i, j-i));
586                                         while (j<size(input) and abnormal()) {
587                                                 # replace tabs with spaces
588                                                 if (input[j] == `\t`)
589                                                         me.add_char(` `);
590                                                 # handle newlines like they're shift+space, i.e. continue don't evaluate
591                                                 elsif (input[j] == `\n` or input[j] == `\r`) {
592                                                         if (j<size(input)-1 and input[j+1] == `\n`)
593                                                                 j+=1;
594                                                         me.input ~= "\n"; me.continue_line(reset_text:0);
595                                                 }
596                                                 # skip other non-ascii characters
597                                                 j += 1;
598                                         }
599                                         i=j;
600                                 }
601                         } elsif (keyCode == `d`) { # ctrl-D/EOF
602                                 printlog(_REPL_dbg_level, "EOF");
603                                 me.del();
604                                 return 1;
605                         } else return 0;
606
607                 } elsif (key == "Enter") {
608                         printlog(_REPL_dbg_level, "return (key: "~key~", shift: "~modifiers.shift~")");
609                         me.reset_input_from_history();
610                         var reset_text = 1;
611                         if (modifiers.shift) {
612                                 var res = -1;
613                                 me.input ~= "\n";
614                                 reset_text = 0;
615                         } else {
616                                 if (size(string.trim(me.input))) {
617                                         append(me.history, me.input);
618                                         me.curr += 1; # simplified version of: me.curr = size(me.history);
619                                         if (me.curr != size(me.history)) die(me.curr~" vs "~size(me.history));
620                                 }
621                                 CanvasPlacement.current_instance = me;
622                                 var res = me.repl.get_input();
623                                 CanvasPlacement.current_instance = nil;
624                                 printlog(_REPL_dbg_level, "return code: "~debug.string(res));
625                         }
626                         if (res == -1)
627                                 me.continue_line(reset_text:reset_text);
628                         else me.new_prompt();
629
630                 } elsif (key == "Backspace") {               # backspace
631                         printlog(_REPL_dbg_level, "back");
632                         me.reset_input_from_history();
633                         if (me.remove_char() == nil) return 1; # nothing happened, since the input
634                                                                # field was blank, but capture the event
635                         me.completion_pos = -1;
636
637                 } elsif (key == "Up") {             # up
638                         printlog(_REPL_dbg_level, "up");
639                         if (me.curr == 0) return 1;
640                         me.curr -= 1;
641                         if (me.curr == size(me.history))
642                                 me.replace_line(me.input, 0);
643                         else
644                                 me.replace_line(me.history[me.curr], 0);
645                         me.completion_pos = -1;
646
647                 } elsif (key == "Down") {             # down
648                         printlog(_REPL_dbg_level, "down");
649                         if (me.curr == size(me.history)) return 1;
650                         me.curr += 1;
651                         if (me.curr == size(me.history))
652                                 me.replace_line(me.input, 0);
653                         else
654                                 me.replace_line(me.history[me.curr], 0);
655                         me.completion_pos = -1;
656
657                 } elsif (key == "Escape") {  # escape -> cancel
658                         printlog(_REPL_dbg_level, "esc");
659                         me.del();
660                         return 1;
661
662                 } elsif (key == "Tab") {            # tab
663                         printlog(_REPL_dbg_level, "tab");
664                         return 0;
665                         me.reset_input_from_history();
666                         if (size(text) and text[0] == `/`) {
667                                 me.input = me.complete(me.input, modifiers.shift ? -1 : 1);
668                         }
669
670                 } elsif (size(key) > 1 or !string.isprint(key[0])) {
671                         printlog(_REPL_dbg_level, "other key: "~key);
672                         return 0;                  # pass other funny events
673
674                 } else {
675                         printlog(_REPL_dbg_level, "key: "~key[0]~" (`"~key~"`)");
676                         me.add_char(key[0]);
677                         me.completion_pos = -1;
678                 }
679
680                 #printlog(_REPL_dbg_level, "  -> "~me.input);
681
682                 me.update();
683                 me.reset_view();
684
685                 return 1;                                # discard key event
686         },
687         get_line: func() {
688                 return me.input;
689         },
690         display_result: func(res=nil) {
691                 if (res == nil) return 1; # don't display NULL results
692                 var res = call(debug.string, [res, 0], var err=[]);
693                 if (size(err)) {
694                         me.add_line(me.gettranslation("bad-result"));
695                         me.set_line_color(me.colors.error);
696                         if (me.font_file == "LiberationFonts/LiberationMono-Bold.ttf")
697                                 me.set_line_font("LiberationFonts/LiberationMono-BoldItalic.ttf");
698                         return 1;
699                 }
700                 if (size(res) > me.max_output_chars)
701                         res = substr(res, 0, me.max_output_chars-5)~". . .";
702                 me.add_line(res);
703                 return 1;
704         },
705         handle_runtime_error: func(err) {
706                 debug.printerror(err);
707                 me.add_line("Runtime error: "~err[0]);
708                 me.set_line_color(me.colors.error);
709                 for (var i=1; i<size(err); i+=2) {
710                         me.add_line("   at "~err[i]~", line "~err[i+1]);
711                 }
712         },
713         handle_parse_error: func(msg, file, line) {
714                 print("Parse error: "~msg~" on line "~line~" in "~file);
715                 me.add_line("Parse error: "~msg~" on line "~line~" in "~file);
716                 me.set_line_color(me.colors.error);
717         },
718         gettranslation: func(k) me.translations[k] or "[Error: no translation for key "~k~"]",
719 };
720
721 var print2 = func(i) {
722         console.CanvasPlacement.current_instance.display_result(i);
723         return nil; # just to suppress output
724 }
725 #CanvasPlacement.new("<styled-canvas-repl>", "canvas-default");
726