Call mp.test_mtime() from inside mp.process_event().
[mp-5.x.git] / mp_core.mpsl
blob20234789e4b6fe8580a85ca3995d1ca3b766f8d9
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     Copyright (C) 1991-2012 Angel Ortega <angel@triptico.com>
8     This program is free software; you can redistribute it and/or
9     modify it under the terms of the GNU General Public License
10     as published by the Free Software Foundation; either version 2
11     of the License, or (at your option) any later version.
13     This program is distributed in the hope that it will be useful,
14     but WITHOUT ANY WARRANTY; without even the implied warranty of
15     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16     GNU General Public License for more details.
18     You should have received a copy of the GNU General Public License
19     along with this program; if not, write to the Free Software
20     Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22     http://www.triptico.com
26 /** global data **/
28 /* L(x) is the same as gettext(x) */
29 L = gettext;
31 /* LL(x) is the same as x */
32 sub LL(x) { x; }
34 /* configuration */
36 mp.config = {
37     undo_levels:            100,
38     word_wrap:              0,
39     auto_indent:            0,
40     tab_size:               8,
41     tabs_as_spaces:         0,
42     dynamic_tabs:           0,
43     unlink:                 1,
44     case_sensitive_search:  1,
45     global_replace:         0,
46     preread_lines:          60,
47     mark_eol:               0,
48     maximize:               0,
49     keep_eol:               1,
50     smart_bol:              1
53 /* default end of line, system dependent */
54 if (mp.drv.id eq 'win32')
55         mp.config.eol = "\r\n";
56 else
57         mp.config.eol = "\n";
59 /* status line */
60 mp.config.status_format = "%m%n %x,%y [%l] %R%O %s %e %t";
61 mp.status_line_info = {
62         '%V'    =>      mp.VERSION,
63         '%m'    =>      sub { mp.active.disk_op && '!' || (mp.active.txt.mod && '*' || ''); },
64         '%x'    =>      sub { mp.active.txt.x + 1; },
65         '%y'    =>      sub { mp.active.txt.y + 1; },
66         '%l'    =>      sub { size(mp.active.txt.lines); },
67         '%R'    =>      sub { mp.macro.process_event && 'R' || ''; },
68         '%O'    =>      sub { mp.config.insert && 'O' || ''; },
69         '%s'    =>      sub { mp.active.syntax.name; },
70         '%t'    =>      sub { mp.tags[mp.get_word(mp.active())].label; },
71         '%n'    =>      sub { mp.active.name; },
72         '%w'    =>      sub { mp.word_count(mp.active()); },
73         '%e'    =>      sub { mp.active.encoding || ''; },
74         '%%'    =>      '%'
77 /* a regex for selecting words */
78 mp.word_regex = "/[[:alnum:]_]+/i";
80 /* if it does not work (i.e. not GNU regex), fall back */
81 if (regex("test", mp.word_regex) == NULL)
82         mp.word_regex = '/[A-Z_][A-Z0-9_]*/i';
84 /* document list */
85 mp.docs = [];
86 mp.active_i = 0;
88 /* allowed color names (order matters, match the Unix curses one) */
89 mp.color_names = [
90     "default",
91     "black", "red", "green", "yellow",
92     "blue", "magenta", "cyan", "white"
95 /* color definitions */
96 mp.colors = {
97     normal: {
98         text:   [ 'default', 'default' ],
99         gui:    [ 0x000000, 0xffffff ]
100     },
101     cursor: {
102         text:   [ 'default', 'default' ],
103         gui:    [ 0x000000, 0xffffff ],
104         flags:  [ 'reverse' ]
105     },
106     selection: {
107         text:   [ 'red', 'default' ],
108         gui:    [ 0xff0000, 0xffffff ],
109         flags:  [ 'reverse']
110     },
111     comments: {
112         text:   [ 'green', 'default' ],
113         gui:    [ 0x00cc77, 0xffffff ]
114     },
115     documentation: {
116         text:   [ 'cyan', 'default' ],
117         gui:    [ 0x8888ff, 0xffffff ]
118     },
119     quotes: {
120         text:   [ 'blue', 'default' ],
121         gui:    [ 0x0000ff, 0xffffff ],
122         flags:  [ 'bright' ]
123     },
124     matching: {
125         text:   [ 'black', 'cyan' ],
126         gui:    [ 0x000000, 0xffff00 ]
127     },
128     word1: {
129         text:   [ 'green', 'default' ],
130         gui:    [ 0x00aa00, 0xffffff ],
131         flags:  [ 'bright' ]
132     },
133     word2: {
134         text:   [ 'red', 'default' ],
135         gui:    [ 0xff6666, 0xffffff ],
136         flags:  [ 'bright' ]
137     },
138     tag: {
139         text:   [ 'cyan', 'default' ],
140         gui:    [ 0x8888ff, 0xffffff ],
141         flags:  [ 'underline' ]
142     },
143     spell: {
144         text:   [ 'red', 'default' ],
145         gui:    [ 0xff8888, 0xffffff ],
146         flags:  [ 'bright', 'underline' ]
147     },
148     search: {
149         text:   [ 'black', 'green' ],
150         gui:    [ 0x000000, 0x00cc77 ]
151     }
154 /* hash of specially coloured words */
155 mp.word_color = {};
157 mp.keycodes = {};
159 mp.actions = {};
161 mp.actdesc = {};
163 mp.alert_log = [];
165 /** the menu **/
167 mp.menu = [
168     [
169         LL("&File"), [
170             'new', 'open', 'save', 'save_as', 'close', 'revert',
171             'open_under_cursor',
172             '-', 'hex_view',
173             '-', 'set_password',
174             '-', 'open_config_file', 'open_templates_file',
175             '-', 'sync',
176             '-', 'save_session', 'load_session',
177             '-', 'exit'
178         ]
179     ],
180     [
181         LL("&Edit"), [
182             'undo', 'redo', '-',
183             'cut_mark', 'copy_mark', 'paste_mark', 'delete_mark',
184             'delete_line', 'cut_lines_with_string', '-',
185             'mark',     'mark_vertical', 'unmark', 'mark_all', '-',
186             'insert_template', 'insert_next_item', '-',
187             'word_wrap_paragraph', 'join_paragraph', '-',
188             'exec_command', 'filter_selection', '-',
189             'exec_action', 'eval', 'eval_doc'
190         ]
191     ],
192     [
193         LL("&Search"), [
194             'seek', 'seek_next', 'seek_prev', 'replace', '-',
195             'complete', '-',
196             'seek_misspelled', 'ignore_last_misspell', '-',
197             'seek_repeated_word', '-',
198             'find_tag', 'complete_symbol', '-', 'grep'
199         ]
200     ],
201     [
202         LL("&Go to"), [
203             'next', 'prev',
204             'move_bof', 'move_eof', 'move_bol', 'move_eol',
205             'goto', 'move_word_right', 'move_word_left',
206             'section_list',
207             '-', 'document_list'
208         ]
209     ],
210     [
211         LL("&Options"), [
212             'record_macro', 'play_macro', '-',
213             'encoding', 'tab_options', 'line_options',
214             'repeated_words_options', 'toggle_spellcheck', '-',
215             'word_count', '-',
216             'zoom_in', 'zoom_out', '-',
217             'about'
218         ]
219     ]
222 mp.actions_by_menu_label = {};
224 /** code **/
227  * mp.redraw - Triggers a redraw on the next cycle.
229  * Triggers a full document redraw in the next cycle.
230  */
231 sub mp.redraw()
233         /* just increment the redraw trigger */
234         mp.redraw_counter++;
238 sub mp.active()
239 /* returns the active document */
241         local d;
243         /* empty document list? create a new, empty one */
244         if (size(mp.docs) == 0)
245                 mp.new();
247         /* get active document */
248         d = mp.docs[mp.active_i];
250         /* if it's read only but has modifications, revert them */
251         if (d.read_only && size(d.undo)) {
252                 while (size(d.undo))
253                         mp.undo(d);
255                 mp.message = {
256                         'timeout'       => time() + 2,
257                         'string'        => '*' ~ L("Read-only document") ~ '*'
258                 };
259         }
261         return d;
265 sub mp.process_action(a)
266 /* processes an action */
268     local f, d;
270     d = mp.active();
272     if ((f = mp.actions[a]) != NULL)
273         f(d);
274     else {
275         mp.message = {
276             timeout:    time() + 2,
277             string:     sprintf(L("Unknown action '%s'"), a)
278         };
279     }
281     return NULL;
285 sub mp.process_event(k)
286 /* processes a key event */
288     if (size(mp.docs)) {
289         local d, a;
291         d = mp.active();
293         mp.test_mtime(d);
294     
295         if (mp.keycodes_t == NULL)
296             mp.keycodes_t = mp.keycodes;
297     
298         /* get the action asociated to the keycode */
299         if ((a = mp.keycodes_t[k]) != NULL) {
300     
301             /* if it's a hash, store for further testing */
302             if (is_hash(a))
303                 mp.keycodes_t = a;
304             else {
305                 /* if it's executable, run it */
306                 if (is_exec(a))
307                     a(d);
308                 else
309                 /* if it's an array, process it sequentially */
310                 if (is_array(a))
311                     foreach(l, a)
312                         mp.process_action(l);
313                 else
314                     mp.process_action(a);
315     
316                 mp.keycodes_t = NULL;
317             }
318         }
319         else {
320             mp.insert_keystroke(d, k);
321             mp.keycodes_t = NULL;
322         }
323     
324         mp.shift_pressed = NULL;
325     
326         /* if there is a keypress notifier function, call it */
327         if (is_exec(d.keypress))
328             d.keypress(d, k);
329     }
331     return NULL;
335 sub mp.build_status_line()
336 /* returns the string to be drawn in the status line */
338         if (mp.message) {
339                 /* is the message still active? */
340                 if (mp.message.timeout > time())
341                         return mp.message.string;
343                 mp.message = NULL;
344         }
346         return sregex(mp.config.status_format, "/%./g", mp.status_line_info);
350 sub mp.backslash_codes(s, d)
351 /* encodes (d == 0) or decodes (d == 1) backslash codes
352    (like \n, \r, etc.) */
354         d && sregex(s, "/[\r\n\t]/g", { "\r" => '\r', "\n" => '\n', "\t" => '\t'}) ||
355                  sregex(s, "/\\\\[rnt]/g", { '\r' => "\r", '\n' => "\n", '\t' => "\t"});
359 sub mp.long_op(func, a1, a2, a3, a4)
360 /* executes a potentially long function */
362         local r;
364         mp.busy(1);
365         r = func(a1, a2, a3, a4);
366         mp.busy(0);
368         return r;
372 sub mp.get_history(key)
373 /* returns a history for the specified key */
375         if (key == NULL)
376                 return NULL;
377         if (mp.history == NULL)
378                 mp.history = {};
379         if (mp.history[key] == NULL)
380                 mp.history[key] = [];
382         return mp.history[key];
386 sub mp.menu_label(action)
387 /* returns a label for the menu for an action */
389         local l;
391         /* if action is '-', it's a menu separator */
392         if (action eq '-')
393                 return NULL;
395         /* no recognized action? return */
396         if (!exists(mp.actions, action))
397                 return action ~ "?";
399         /* get the translated description */
400         l = L(mp.actdesc[action]) || action;
402         /* is there a keycode that generates this action? */
403         foreach (i, sort(keys(mp.keycodes))) {
404                 if (mp.keycodes[i] eq action) {
405                         /* avoid mouse and window pseudo-keycodes */
406                         if (!regex(i, "/window/") && !regex(i, "/mouse/")) {
407                                 l = l ~ ' [' ~ i ~ ']';
408                                 break;
409                         }
410                 }
411         }
413         mp.actions_by_menu_label[l] = action;
415         return l;
419 sub mp.trim_with_ellipsis(str, max)
420 /* trims the string to the last max characters, adding ellipsis if done */
422         local v = regex(str, '/.{' ~ max ~ '}$/');
423         return v && '...' ~ v || str;
427 sub mp.get_doc_names(max)
428 /* returns an array with the trimmed names of the documents */
430         map(
431                 mp.docs,
432                 sub(e) {
433                         (e.txt.mod && '* ' || '') ~ mp.trim_with_ellipsis(e.name, (max || 24));
434                 }
435         );
439 sub mp.usage()
440 /* set mp.exit_message with an usage message (--help) */
442         mp.exit_message = 
443         sprintf(L(
444                 "Minimum Profit %s - Programmer Text Editor\n"\
445                 "Copyright (C) Angel Ortega <angel@triptico.com>\n"\
446                 "This software is covered by the GPL license. NO WARRANTY.\n"\
447                 "\n"\
448                 "Usage: mp-5 [options] [files...]\n"\
449                 "\n"\
450                 "Options:\n"\
451                 "\n"\
452                 " -t {tag}           Edits the file where tag is defined\n"\
453                 " -e {mpsl_code}     Executes MPSL code\n"\
454                 " -f {mpsl_script}   Executes MPSL script file\n"\
455                 " -d {directory}     Set current directory\n"\
456                 " -x {file}          Open file in the hexadecimal viewer\n"\
457                 " -txt               Use text mode instead of GUI\n"\
458                 " +NNN               Moves to line number NNN of last file\n"\
459                 "\n"\
460                 "Homepage: http://triptico.com/software/mp.html\n"\
461                 "Mailing list: mp-subscribe@lists.triptico.com\n"
462         ), mp.VERSION);
466 sub mp.process_cmdline()
467 /* process the command line arguments (ARGV) */
469         local o, line;
471         mp.load_tags();
473         /* skip ARGV[0] */
474         shift(ARGV);
476         while (o = shift(ARGV)) {
477                 if (o eq '-h' || o eq '--help') {
478                         mp.usage();
479                         mp.exit();
480                         return;
481                 }
482                 else
483                 if (o eq '-e') {
484                         /* execute code */
485                         local c = shift(ARGV);
487                         if (! regex(c, '/;\s*$/'))
488                                 c = c ~ ';';
490                         eval(c);
491                 }
492                 else
493                 if (o eq '-f') {
494                         /* execute script */
495                         local s = shift(ARGV);
497                         if (stat(s) == NULL)
498                                 ERROR = sprintf(L("Cannot open '%s'"), s);
499                         else {
500                                 mp.open(s);
501                                 eval(join(mp.active.txt.lines, "\n"));
502                                 mp.close();
503                         }
504                 }
505                 else
506                 if (o eq '-d')
507                         chdir(shift(ARGV));
508                 else
509                 if (o eq '-t')
510                         mp.open_tag(shift(ARGV));
511                 else
512                 if (o eq '-x') {
513                         local s = shift(ARGV);
515                         if (mp.hex_view(s) == NULL)
516                                 ERROR = sprintf(L("Cannot open '%s'"), s);
517                 }
518                 else
519                 if (o eq '-txt')
520                         mp.config.text_mode = 1;
521                 else
522                 if (regex(o, '/^\+/')) {
523                         /* move to line */
524                         line = o - 1;
525                 }
526                 else
527                         mp.open(o);
528         }
530         if (ERROR) {
531                 mp.exit_message = ERROR ~ "\n";
532                 ERROR = NULL;
533                 mp.exit();
534                 return;
535         }
537         /* if no files are loaded, try a session */
538         if (size(mp.docs) == 0 && mp.config.auto_sessions) {
539                 mp.load_session();
540         }
541         else {
542                 /* set the first as the active one */
543                 mp.active_i = 0;
544         }
546         mp.active();
548         /* if there is a line defined, move there */
549         if (line != NULL)
550                 mp.set_y(mp.active(), line);
554 sub mp.load_profile()
555 /* loads ~/.mp.mpsl */
557         /* if /etc/mp.mpsl exists, execute it */
558         if (stat('/etc/mp.mpsl') != NULL) {
559                 eval( sub {
560                         local INC = [ '/etc' ];
561                         load('mp.mpsl');
562                 });
563         }
565         /* if ~/.mp.mpsl exists, execute it */
566         if (ERROR == NULL && stat(HOMEDIR ~ '.mp.mpsl') != NULL) {
567                 eval( sub {
568                         local INC = [ HOMEDIR ];
569                         load(".mp.mpsl");
570                 });
571         }
573         /* errors? show in a message */
574         if (ERROR != NULL) {
575                 mp.message = {
576                         'timeout'       => time() + 20,
577                         'string'        => ERROR
578                 };
580                 ERROR = NULL;
581         }
585 sub mp.setup_language()
586 /* sets up the language */
588         /* set gettext() domain */
589         gettext_domain('minimum-profit', APPDIR ~ 'locale');
591         /* test if gettext() can do a basic translation */
592         if (gettext('&File') eq '&File' && ENV.LANG) {
593                 /* no; try alternatives using the LANG variable */
594                 local v = [ sregex(ENV.LANG, '!/!g') ]; /* es_ES.UTF-8 */
595                 push(v, shift(split(v[-1], '.')));      /* es_ES */
596                 push(v, shift(split(v[-1], '_')));      /* es */
598                 foreach (l, v) {
599                         eval('load("lang/' ~ l ~ '.mpsl");');
601                         if (ERROR == NULL)
602                                 break;
603                 }
605                 ERROR = NULL;
606         }
610 sub mp.normalize_version(vs)
611 /* converts a version string to something usable with cmp() */
613     vs->sregex('/-.+$/')->split('.')->map(sub(e) { sprintf("%03d", e); });
617 sub mp.assert_version(found, minimal, package)
618 /* asserts that 'found' version of 'package' is at least 'minimal',
619    or generate a warning otherwise */
621         if (cmp(mp.normalize_version(found),
622                 mp.normalize_version(minimal)) < 0) {
623                 mp.alert(sprintf(L("WARNING: %s version found is %s, but %s is needed"),
624                                 package, found, minimal));
625         }
629 sub mp.test_versions()
630 /* tests component versions */
632         local mpdm = MPDM();
634         mp.assert_version(mpdm.version, '2.0.2', 'MPDM');
635         mp.assert_version(MPSL.VERSION, '2.0.2', 'MPSL');
639 /** modules **/
641 local mp_modules = [
642     'drv',
643     'move',
644     'edit',
645     'file',
646     'clipboard',
647     'search',
648     'tags',
649     'syntax',
650     'macro',
651     'templates',
652     'spell',
653     'misc',
654     'crypt',
655     'keyseq',
656     'session',
657     'build',
658     'writing',
659     'toys',
660     'vcs'
663 foreach (m, mp_modules) {
664     eval('load("mp_' ~ m ~ '.mpsl");');
666     if (ERROR != NULL)
667         mp.exit_message = mp.exit_message ~ ERROR ~ "\n";
670 ERROR = NULL;
672 /** main **/
674 mp.load_profile();
675 mp.setup_language();
676 mp.drv.startup();
677 mp.process_cmdline();
678 mp.test_versions();
679 mp.drv.main_loop();
680 mp.drv.shutdown();