Fixed file permissions.
[mp-5.x.git] / mp_core.mpsl
blob4d1e6452f7698b48fd9b01cd0d043622b7cd8422
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     Copyright (C) 1991-2011 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         local d, a;
290         /* empty document list? do nothing */
291         if (size(mp.docs) == 0)
292                 return;
294         d = mp.active();
296         if (mp.keycodes_t == NULL)
297                 mp.keycodes_t = mp.keycodes;
299         /* get the action asociated to the keycode */
300         if ((a = mp.keycodes_t[k]) != NULL) {
302                 /* if it's a hash, store for further testing */
303                 if (is_hash(a))
304                         mp.keycodes_t = a;
305                 else {
306                         /* if it's executable, run it */
307                         if (is_exec(a))
308                                 a(d);
309                         else
310                         /* if it's an array, process it sequentially */
311                         if (is_array(a))
312                                 foreach(l, a)
313                                         mp.process_action(l);
314                         else
315                                 mp.process_action(a);
317                         mp.keycodes_t = NULL;
318                 }
319         }
320         else {
321                 mp.insert_keystroke(d, k);
322                 mp.keycodes_t = NULL;
323         }
325         mp.shift_pressed = NULL;
327         /* if there is a keypress notifier function, call it */
328         if (is_exec(d.keypress))
329                 d.keypress(d, k);
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.0', 'MPDM');
635         mp.assert_version(MPSL.VERSION, '2.0.1', 'MPSL');
639 /** MAIN **/
641 load("mp_drv.mpsl");
642 load("mp_move.mpsl");
643 load("mp_edit.mpsl");
644 load("mp_file.mpsl");
645 load("mp_clipboard.mpsl");
646 load("mp_search.mpsl");
647 load("mp_tags.mpsl");
648 load("mp_syntax.mpsl");
649 load("mp_macro.mpsl");
650 load("mp_templates.mpsl");
651 load("mp_spell.mpsl");
652 load("mp_misc.mpsl");
653 load("mp_crypt.mpsl");
654 load("mp_keyseq.mpsl");
655 load("mp_session.mpsl");
656 load("mp_build.mpsl");
657 load("mp_writing.mpsl");
658 load("mp_toys.mpsl");
660 mp.load_profile();
661 mp.setup_language();
662 mp.drv.startup();
663 mp.process_cmdline();
664 mp.test_versions();
665 mp.drv.main_loop();
666 mp.drv.shutdown();