1/*
2
3    Minimum Profit - A Text Editor
4
5    ttcdt <dev@triptico.com> et al.
6
7    This software is released into the public domain.
8    NO WARRANTY. See file LICENSE for details.
9
10*/
11
12/** startup: language first **/
13
14/* L(x) is the same as gettext(x) */
15L = gettext;
16
17/* LL(x) is the same as x */
18sub LL(x) { x; }
19
20/* set gettext() domain */
21gettext_domain('minimum-profit', APPDIR + 'locale');
22
23/* test if gettext() can do a basic translation */
24if (gettext('&File') == '&File' && ENV.LANG) {
25    /* no; try alternatives using the LANG variable */
26    local v = [ sregex(ENV.LANG, '!/!g') ]; /* es_ES.UTF-8 */
27    push(v, shift(split(v[-1], '.')));      /* es_ES */
28    push(v, shift(split(v[-1], '_')));      /* es */
29
30    foreach (l, v) {
31        eval('load("lang/' + l + '.mpsl");');
32
33        if (ERROR == NULL)
34            break;
35    }
36
37    ERROR = NULL;
38}
39
40/** main namespace **/
41
42global mp = {
43    VERSION:    mp_c.VERSION,
44
45    /* document list */
46    docs:       [],
47    active_i:   0,
48
49    /* configuration */
50    config:     {
51        undo_levels:            100,
52        word_wrap:              0,
53        auto_indent:            0,
54        tab_size:               4,
55        tabs_as_spaces:         1,
56        dynamic_tabs:           0,
57        unlink:                 1,
58        case_sensitive_search:  1,
59        global_replace:         0,
60        preread_lines:          60,
61        mark_eol:               0,
62        maximize:               0,
63        keep_eol:               1,
64        smart_bol:              1,
65        emacs_scrolling:        0,
66        no_text_mouse:          0,
67        show_line_numbers:      0,
68        double_page:            120,
69        eol:                    (mp_drv.id->regex("/^win(32|64)$/") && "\r\n" || "\n"),
70        status_format:          "%m%n %x,%y [%l] %R%O%W %s %e %t",
71        time_stamp_format:      "%a, %d %b %Y %H:%M:%S %z",
72        visual_wrap:            1,
73        use_unicode:            1,
74        dark_mode:              1,
75        font_weight:            0,
76        hw_cursor:              0,
77        auto_save_period:       120
78    },
79
80    /* regular expression matching a word (test fallback) */
81    word_regex:     "/[[:alnum:]_]+/i",
82
83    /* regular expression for matching programming language tokens */
84    token_regex:    "/[a-zA-Z_]+[a-zA-Z0-9_]*/",
85
86    /* viewport size */
87    window:         {},
88
89    /* allowed color names (order matters, match the Unix curses one) */
90    color_names:    [
91        "default",
92        "black", "red", "green", "yellow",
93        "blue", "magenta", "cyan", "white"
94    ],
95
96    /* color definitions */
97    colors:         {
98        normal: {
99            text:   [ 'default', 'default' ],
100            gui:    NULL
101        },
102        cursor: {
103            text:   [ 'default', 'default' ],
104            gui:    NULL,
105            flags:  [ 'reverse' ]
106        },
107        selection: {
108            text:   [ 'red', 'default' ],
109            gui:    NULL,
110            flags:  [ 'reverse' ]
111        },
112        comments: {
113            text:   [ 'green', 'default' ],
114            gui:    NULL
115        },
116        documentation: {
117            text:   [ 'cyan', 'default' ],
118            gui:    NULL
119        },
120        quotes: {
121            text:   [ 'blue', 'default' ],
122            gui:    NULL,
123            flags:  [ 'bright' ]
124        },
125        matching: {
126            text:   [ 'black', 'cyan' ],
127            gui:    NULL
128        },
129        word1: {
130            text:   [ 'green', 'default' ],
131            gui:    NULL,
132            flags:  [ 'bright' ]
133        },
134        word2: {
135            text:   [ 'red', 'default' ],
136            gui:    NULL,
137            flags:  [ 'bright' ]
138        },
139        word3: {
140            text:   [ 'default', 'default' ],
141            gui:    NULL,
142            flags:  [ 'bright' ]
143        },
144        tag: {
145            text:   [ 'cyan', 'default' ],
146            gui:    NULL,
147            flags:  [ 'underline' ]
148        },
149        spell: {
150            text:   [ 'red', 'default' ],
151            gui:    NULL,
152            flags:  [ 'bright', 'underline' ]
153        },
154        search: {
155            text:   [ 'black', 'green' ],
156            gui:    NULL
157        },
158        suggest: {
159            text:   [ 'black', 'white' ],
160            gui:    NULL,
161            flags:  [ 'bright' ]
162        }
163    },
164
165    /* hash of specially coloured words */
166    word_color: {},
167
168    /* generic keycodes */
169    keycodes:   {
170        ansi: {}
171    },
172
173    /* generic actions */
174    actions:    {},
175
176    /* action descriptions (generic and document specific) */
177    actdesc:    {},
178
179    /* form history */
180    history:    {},
181
182    /* state saved from run to run (window size, etc.) */
183    state:      {},
184
185    /* redraw counter */
186    redraw_counter: 0,
187
188    /* the menu */
189    menu:       [
190        [
191            LL("&File"), [
192                'new', 'open', 'open_recent', 'open_under_cursor', 'revert', '-',
193                'save', 'save_as', 'sync', 'export', '-',
194                'open_folder', 'hex_view', '-',
195                'set_password', '-',
196                'open_config_file', 'open_templates_file', '-',
197                'close', '-',
198                'suspend', 'exit'
199            ]
200        ],
201        [
202            LL("&Edit"), [
203                'undo', 'redo', '-',
204                'cut_mark', 'copy_mark', 'paste_mark', 'delete_mark',
205                'delete_line', 'cut_lines_with_string', '-',
206                'mark_tag', 'mark_tag_vertical', 'unmark', 'mark_all', '-',
207                'insert_template', '-',
208                'exec_command', 'filter_selection', '-',
209                'exec_action', 'eval', 'eval_doc'
210            ]
211        ],
212        [
213            LL("&Search"), [
214                'seek', 'seek_next', 'seek_prev', '-',
215                'replace', 'replace_next',
216                'replace_tabs_with_spaces', 'replace_spaces_with_tabs', '-',
217                'complete', '-',
218                'grep'
219            ]
220        ],
221        [
222            LL("&Go to"), [
223                'next', 'prev',
224                'move_bof', 'move_eof', 'move_bol', 'move_eol',
225                'goto', 'move_word_right', 'move_word_left', '-',
226                'section_list', 'move_section_up', 'move_section_down',
227                '-', 'document_list'
228            ]
229        ],
230        [
231            LL("&Project"), [
232                'build', '-',
233                'vcs', '-',
234                'find_tag', 'complete_symbol', '-',
235                'save_session', 'load_session'
236            ]
237        ],
238        [
239            LL("&Writing") , [
240                'toggle_spellcheck',
241                'seek_misspelled', 'ignore_last_misspell', '-',
242                'seek_repeated_word', '-',
243                'word_count', '-',
244                'word_wrap_paragraph', 'join_paragraph', '-',
245                'insert_next_item', 'insert_time_stamp',
246                'insert_page_break', 'insert_m_dash'
247            ]
248        ],
249        [
250            LL("&Options"), [
251                'record_macro', 'play_macro', '-',
252                'toggle_visual_wrap',
253                'encoding', 'tab_options', 'line_options',
254                'repeated_words_options', 'complete_options', '-',
255                'zoom_in', 'zoom_out', '-',
256                'pipes', '-', 'check_for_updates', 'release_notes', 'about'
257            ]
258        ]
259    ]
260};
261
262
263/** new color schemes **/
264
265/* don't remember where I took this one */
266/*
267mp.colors.normal.gui        = [ 0x000000, 0xeeeeee ];
268mp.colors.cursor.gui        = [ 0x000000, 0xeeeeee ];
269mp.colors.quotes.gui        = [ 0x006666, 0xeeeeee ];
270mp.colors.comments.gui      = [ 0x880000, 0xeeeeee ];
271mp.colors.word1.gui         = [ 0x000088, 0xeeeeee ];
272mp.colors.word2.gui         = [ 0x660066, 0xeeeeee ];
273mp.colors.word3.gui         = [ 0x666600, 0xeeeeee ];
274mp.colors.documentation.gui = [ 0x8888ff, 0xeeeeee ];
275*/
276
277/* Tomorrow: https://github.com/chriskempson/tomorrow-theme */
278/*
279mp.colors.normal.gui        = [ 0x4d4d4c, 0xffffff ];
280mp.colors.cursor.gui        = [ 0x4d4d4c, 0xffffff ];
281mp.colors.quotes.gui        = [ 0x3e999f, 0xffffff ];
282mp.colors.comments.gui      = [ 0x8e908c, 0xffffff ];
283mp.colors.word1.gui         = [ 0x4271ae, 0xffffff ];
284mp.colors.word2.gui         = [ 0xeab700, 0xffffff ];
285mp.colors.word3.gui         = [ 0xf5871f, 0xffffff ];
286mp.colors.documentation.gui = [ 0x8959a8, 0xffffff ];
287mp.colors.selection.gui     = [ 0xd6d6d6, 0x000000 ];
288*/
289
290/* Tomorrow Night Eighties: https://github.com/chriskempson/tomorrow-theme */
291/*
292mp.colors.normal.gui        = [ 0xcccccc, 0x2d2d2d ];
293mp.colors.cursor.gui        = [ 0xcccccc, 0x2d2d2d ];
294mp.colors.quotes.gui        = [ 0x66cccc, 0x2d2d2d ];
295mp.colors.comments.gui      = [ 0x999999, 0x2d2d2d ];
296mp.colors.word1.gui         = [ 0x6699cc, 0x2d2d2d ];
297mp.colors.word2.gui         = [ 0xffcc66, 0x2d2d2d ];
298mp.colors.word3.gui         = [ 0xf99157, 0x2d2d2d ];
299mp.colors.documentation.gui = [ 0xcc99cc, 0x2d2d2d ];
300mp.colors.selection.gui     = [ 0xf2777a, 0x000000 ];
301*/
302
303/* Tomorrow Night: https://github.com/chriskempson/tomorrow-theme */
304/*
305mp.colors.normal.gui        = [ 0xc5c8c6, 0x1d1f21 ];
306mp.colors.cursor.gui        = [ 0xc5c8c6, 0x1d1f21 ];
307mp.colors.quotes.gui        = [ 0x8abeb7, 0x1d1f21 ];
308mp.colors.comments.gui      = [ 0x969896, 0x1d1f21 ];
309mp.colors.word1.gui         = [ 0x81a2be, 0x1d1f21 ];
310mp.colors.word2.gui         = [ 0xf0c674, 0x1d1f21 ];
311mp.colors.word3.gui         = [ 0xde935f, 0x1d1f21 ];
312mp.colors.documentation.gui = [ 0xb294bb, 0x1d1f21 ];
313mp.colors.selection.gui     = [ 0xcc6666, 0x000000 ];
314*/
315
316/* Tomorrow Night Bright: https://github.com/chriskempson/tomorrow-theme */
317/*
318mp.colors.normal.gui        = [ 0xeaeaea, 0x000000 ];
319mp.colors.cursor.gui        = [ 0xeaeaea, 0x000000 ];
320mp.colors.quotes.gui        = [ 0x70c0b1, 0x000000 ];
321mp.colors.comments.gui      = [ 0x969896, 0x000000 ];
322mp.colors.word1.gui         = [ 0x7aa6da, 0x000000 ];
323mp.colors.word2.gui         = [ 0xe7c547, 0x000000 ];
324mp.colors.word3.gui         = [ 0xe7c547, 0x000000 ];
325mp.colors.documentation.gui = [ 0xc397d8, 0x000000 ];
326mp.colors.selection.gui     = [ 0xd54e53, 0x000000 ];
327*/
328
329/* Minimum Profit Classic */
330/*
331mp.colors.normal.gui        = [ 0x000000, 0xffffff ];
332mp.colors.cursor.gui        = mp.colors.normal.gui;
333mp.colors.selection.gui     = [ 0xff0000, 0xffffff ];
334mp.colors.comments.gui      = [ 0x00cc77, 0xffffff ];
335mp.colors.documentation.gui = [ 0x8888ff, 0xffffff ];
336mp.colors.quotes.gui        = [ 0x0000ff, 0xffffff ];
337mp.colors.matching.gui      = [ 0x000000, 0xffff00 ];
338mp.colors.word1.gui         = [ 0x00aa00, 0xffffff ];
339mp.colors.word2.gui         = [ 0xff6666, 0xffffff ];
340mp.colors.word3.gui         = [ 0x000088, 0xffffff ];
341mp.colors.tag.gui           = [ 0x8888ff, 0xffffff ];
342mp.colors.spell.gui         = [ 0xff8888, 0xffffff ];
343mp.colors.search.gui        = [ 0x000000, 0x00cc77 ];
344*/
345
346
347/** MP base class **/
348
349global mp_base = {
350    title:          NULL,
351    name:           'MP',
352    keycodes:       {},
353    actions:        {},
354    keypress:       sub (doc, key)  { doc; },
355    render:         sub (doc, opt)  { return []; },
356    pre_event:      sub (doc, key)  { key; },
357    post_event:     sub (doc, key)  { doc; },
358    query_close:    sub (doc)       { 1; },
359    get_name:       sub (doc)       { name; },
360    get_status:     sub (doc)       { name; },
361    busy:           sub (doc, b)    { mp.busy(b); return doc; },
362    state:          sub (doc)       { return NULL; }
363};
364
365
366/** MP document class **/
367
368global mp_doc = new(mp_base, {
369        name:       L("<unnamed>"),
370        txt: {
371            x:      0,
372            y:      0,
373            vx:     0,
374            vy:     0,
375            mod:    0,
376            lines:  [ '' ]
377        },
378        undo_q:     [],
379        redo_q:     [],
380        syntax:     NULL,
381
382        /* rendering function for MP docs in mp_core.c */
383        render:     mp_c.render,
384
385        visual_wrap: sub (d) {
386            return d.force_visual_wrap || mp.config.visual_wrap;
387        }
388    }
389);
390
391
392/** unicode **/
393
394mp.unicode_tbl = [
395    {
396        "ellipsis":  "...",
397        "pilcrow":   "\x{00b6}",
398        "middledot": ".",
399        "wordwrap":  "\x{00b8}",
400        "horiz":     "-",
401        "vert":      "|",
402        "arrowup":   "^",
403        "arrowdn":   "V",
404        "nwcorner":  ".",
405        "necorner":  ".",
406        "swcorner":  "'",
407        "secorner":  "'",
408        "ncorner":   "-",
409        "scorner":   "-",
410        "wcorner":   "|",
411        "ecorner":   "|",
412        "ccorner":   "+",
413        "tab":       "\x{00ac}",
414        "formfeed":  "\x{00a4}",
415        "m_dash":    "-",
416        "unknown":   "?"
417    },
418    {
419        "ellipsis":  "\x{2026}",
420        "pilcrow":   "\x{00b6}",
421        "middledot": "\x{00b7}",
422        "wordwrap":  "\x{00b8}",
423        "horiz":     "\x{2500}",
424        "vert":      "\x{2502}",
425        "arrowup":   "\x{25b2}",
426        "arrowdn":   "\x{25bc}",
427        "nwcorner":  "\x{250c}",
428        "necorner":  "\x{2510}",
429        "swcorner":  "\x{2514}",
430        "secorner":  "\x{2518}",
431        "ncorner":   "\x{252c}",
432        "scorner":   "\x{2534}",
433        "wcorner":   "\x{251c}",
434        "ecorner":   "\x{2524}",
435        "ccorner":   "\x{253c}",
436        "tab":       "\x{2192}",
437        "formfeed":  "\x{21a1}",
438        "m_dash":    "\x{2014}",
439        "unknown":   "\x{fffd}"
440    }
441];
442
443sub mp.unicode(key) { return mp.unicode_tbl[!!mp.config.use_unicode][key] || "-"; }
444
445/** code **/
446
447/**
448 * mp.redraw - Triggers a redraw on the next cycle.
449 *
450 * Triggers a full document redraw in the next cycle.
451 */
452sub mp.redraw()
453{
454    /* just increment the redraw trigger */
455    mp.redraw_counter += 1;
456}
457
458
459sub mp.active()
460/* returns the active document */
461{
462    /* if there is a list of pending files to open, do it */
463    while (size(mp.active_pending))
464        mp.actions.open(NULL, pop(mp.active_pending));
465
466    /* empty document list? create a new, empty one */
467    if (size(mp.docs) == 0)
468        mp.actions.new();
469
470    local doc = mp.docs[mp.active_i];
471
472    if (doc->exists("visual_wrap") && doc->visual_wrap()) {
473        if (doc.txt.last_tx != mp.window.tx || doc.txt.last_ty != mp.window.ty) {
474            doc->vw_wrap();
475            doc.txt.last_tx = mp.window.tx;
476            doc.txt.last_ty = mp.window.ty;
477        }
478    }
479
480    /* get active document */
481    return doc;
482}
483
484
485sub mp.set_active(i)
486/* sets the active document */
487{
488    local doc;
489
490    mp.active_i = i;
491
492    doc = mp.docs[mp.active_i];
493
494    /* set the idle time */
495    mp_drv.idle(doc.idle_seconds);
496
497    return doc;
498}
499
500
501sub mp.process_action(action)
502/* processes an action */
503{
504    local f, doc;
505
506    doc = mp.active();
507
508    f = doc.actions[action] || mp.actions[action];
509
510    if (f != NULL)
511        f(doc);
512    else {
513        mp.message = {
514            timeout:    time() + 2,
515            string:     sprintf(L("Unknown action '%s'"), action)
516        };
517    }
518
519    return doc;
520}
521
522
523sub mp.process_event(k)
524/* processes a key event */
525{
526    local t = time();
527    local doc = mp.active();
528
529    /* special process for the 'idle' pseudo-keycode */
530    if (k == "idle") {
531        /* if no idle_seconds are set or not enough time elapsed, ignore it */
532        if (doc.idle_seconds == 0 || t - doc.event_time < doc.idle_seconds)
533            k = NULL;
534    }
535
536    /* notify the document that an event is to be processed */
537    k = doc->pre_event(k);
538
539    if (k != NULL) {
540        /* get the action or next keycode from the possible sources */
541        local a = mp.keycodes_t[k] || doc.keycodes[k] || mp.keycodes[k];
542
543        mp.keycodes_t = NULL;
544
545        /* get the action asociated to the keycode */
546        if (a != NULL) {
547            /* if it's a hash, store for further testing */
548            if (type(a) == "object")
549                mp.keycodes_t = a;
550            else {
551                /* if it's executable, run it */
552                if (is_exec(a))
553                    a(doc);
554                else
555                /* if it's an array, process it sequentially */
556                if (type(a) == "array")
557                    foreach(l, a)
558                        mp.process_action(l);
559                else
560                    mp.process_action(doc.keycodes[a] || mp.keycodes[a] || a);
561            }
562        }
563        else
564            doc->keypress(k);
565
566        mp.shift_pressed = NULL;
567
568        doc->post_event(k);
569
570        /* store the time this event was processed */
571        doc.event_time = t;
572    }
573
574    return NULL;
575}
576
577
578sub mp.build_status_line()
579/* returns the string to be drawn in the status line */
580{
581    local r;
582
583    if (mp.message) {
584        r = mp.message.string;
585
586        /* is the message still active? */
587        if (mp.message.timeout <= time())
588            mp.message = NULL;
589    }
590    else
591        r = mp.active()->get_status();
592
593    return r;
594}
595
596
597sub mp.backslash_codes(s, d)
598/* encodes (d == 0) or decodes (d == 1) backslash codes
599   (like \n, \r, etc.) */
600{
601    local r;
602
603    if (d) {
604        r = s->sregex("/[\r\n\t]/g", { "\r" => '\r', "\n" => '\n', "\t" => '\t' });
605    }
606    else {
607        r = s->
608            sregex("/\\\\[rnt]/g", { '\r' => "\r", '\n' => "\n", '\t' => "\t" })->
609            sregex("/\\\\x\\{[0-9A-Za-z]+\\}/g", sub (e) {
610                    /* expand \x{HHHH} to character */
611                    e = ("0x" + e->regex("/[0-9A-Za-z]+/")) + 0;
612                    chr(e);
613                }
614            );
615    }
616
617    return r;
618}
619
620
621sub mp.long_op(func, a1, a2, a3, a4)
622/* executes a potentially long function */
623{
624    local r;
625
626    mp.busy(1);
627    r = func(a1, a2, a3, a4);
628    mp.busy(0);
629
630    return r;
631}
632
633
634sub mp.get_history(key)
635/* returns a history for the specified key */
636{
637    return mp.history[key] |= [];
638}
639
640
641sub mp.menu_label(action)
642/* returns a label for the menu for an action */
643{
644    local l = NULL;
645
646    /* if action is '-', it's a menu separator */
647    if (action != '-') {
648        l = L(mp.actdesc[action]) || action;
649
650        /* replace the ... with the Unicode character */
651        l = l->sregex("/\.\.\./", mp.unicode("ellipsis"));
652
653        /* search this action in mp_doc and global keycodes */
654        local ks = (mp_doc.keycodes + mp.keycodes)->
655            grep(sub(v) { v == action; })->map(index);
656
657        if (size(ks))
658            l = l + ' [' + join(ks, ', ') + ']';
659    }
660
661    return l;
662}
663
664
665sub mp.trim(str, max)
666/* trims the string to the last max characters, adding ellipsis if done */
667{
668    if (count(str) > max) {
669        local ell = mp.unicode("ellipsis");
670        max -= count(ell);
671        str = ell + slice(str, -max, -1);
672    }
673
674    return str;
675}
676
677
678sub mp.get_doc_names(max)
679/* returns an array with the trimmed names of the documents */
680{
681    mp.docs->map(sub(e) { mp.trim(e->get_name(), (max || 32)); });
682}
683
684
685sub mp.usage()
686/* set mp.exit_message with an usage message (--help) */
687{
688    mp.exit_message =
689        sprintf(L("Minimum Profit %s - Programmer Text Editor"), mp.VERSION) +
690        "\nttcdt <dev@triptico.com> et al.\n\n" +
691        L("Usage: mp-5 [options] [files...]\n"\
692            "\n"\
693            "Options:\n"\
694            "\n"\
695            " -t {tag}           Edits the file where tag is defined\n"\
696            " -e {mpsl_code}     Executes MPSL code\n"\
697            " -f {mpsl_script}   Executes MPSL script file\n"\
698            " -F {mpsl_script}   Executes MPSL script file and exits\n"\
699            " -d {directory}     Set current directory\n"\
700            " -x {file}          Opens file in the hexadecimal viewer\n"\
701            " -txt               Uses text mode instead of GUI\n"\
702            " +NNN               Moves to line number NNN of last file\n"\
703            " -ni                Non-interactive (to be used with -e or -f)"\
704            "\n");
705}
706
707
708sub mp.process_cmdline()
709/* process the command line arguments (ARGV) */
710{
711    local o, nl;
712
713    /* skip ARGV[0] */
714    mp.binary_name = shift(ARGV);
715
716    /* files requested to be opened */
717    mp.active_pending = glob(HOMEDIR + ".mp-*.recov");
718
719    while (o = shift(ARGV)) {
720        if (o == '-h' || o == '--help') {
721            mp.usage();
722            mp_c.exit();
723            return;
724        }
725        else
726        if (o == '-e') {
727            /* execute code */
728            local c = shift(ARGV);
729
730            if (! regex(c, '/;\s*$/'))
731                c = c + ';';
732
733            eval(c);
734        }
735        else
736        if (o == '-f' || o == '-F') {
737            /* execute script */
738            local s = shift(ARGV);
739
740            if (stat(s) == NULL)
741                ERROR = sprintf(L("Cannot open '%s'"), s);
742            else {
743                eval(sub { local INC = NULL; load(s); } );
744            }
745
746            /* the rest of ARGV are assumed to be arguments
747               to the script, so stop processing them as if
748               they were MP arguments */
749            break;
750        }
751        else
752        if (o == '-d')
753            chdir(shift(ARGV));
754        else
755        if (o == '-t')
756            mp.open_tag(shift(ARGV));
757        else
758        if (o == '-x')
759            mp.actions.hex_view(NULL, shift(ARGV));
760        else
761        if (o == '-txt')
762            mp.config.text_mode = 1;
763        else
764        if (o == '-ni') {
765            /* do nothing */
766            ;
767        }
768        else
769        if (regex(o, '/^\+[0-9]+$/')) {
770            nl = o + 0;
771        }
772        else {
773            if (nl) {
774                push(mp.active_pending, o + ':' + nl + ':');
775                nl = NULL;
776            }
777            else
778                push(mp.active_pending, o);
779        }
780    }
781
782    if (ERROR) {
783        mp.exit_message = ERROR;
784        ERROR = NULL;
785        mp_c.exit();
786        return;
787    }
788
789    /* if no files are to be loaded, try a session */
790    if (size(mp.active_pending) == 0 && mp.config.auto_sessions) {
791        mp.load_session();
792    }
793}
794
795
796sub mp.load_profile()
797/* loads the configuration file(s) */
798{
799    local cfdir, cf;
800
801    /* if /etc/mp.mpsl exists, execute it */
802    if (stat('/etc/mp.mpsl') != NULL) {
803        eval( sub {
804            local INC = [ '/etc' ];
805            load('mp.mpsl');
806        });
807    }
808
809    /* find where the config file is */
810    cfdir = HOMEDIR;
811    cf    = ".mp.mpsl";
812
813    if (stat(cfdir + cf) == NULL) {
814        local ncf = ENV["XDG_CONFIG_HOME"] || (HOMEDIR + ".config");
815
816        /* if ~/.config is a directory, use ~/.config/mp.mpsl */
817        if (stat(ncf)->get(2)->bitand(0x4000)) {
818            cfdir = ncf;
819            cf    = "mp.mpsl";
820        }
821    }
822
823    /* ensure the configuration directory ends with a / */
824    if (cfdir[-1] != "/")
825        cfdir += "/";
826
827    mp.config_file = cfdir + cf;
828
829    /* if the config file exists, execute it */
830    if (ERROR == NULL && stat(mp.config_file) != NULL) {
831        eval( sub {
832            local INC = [ cfdir ];
833            load(cf);
834        });
835    }
836
837    /* errors? show in a message */
838    if (ERROR != NULL) {
839        mp.message = {
840            'timeout'   => time() + 20,
841            'string'    => ERROR
842        };
843
844        ERROR = NULL;
845    }
846}
847
848
849sub mp.normalize_version(vs)
850/* converts a version string to something usable with cmp() */
851{
852    /* strip possible string and split by dots */
853    local l = vs->sregex("/-.+$/")->split(".");
854
855    return sprintf("%d.%04d", l->shift(), l->join());
856}
857
858
859sub mp.assert_version(found, minimal, package)
860/* asserts that 'found' version of 'package' is at least 'minimal',
861   or generate a warning otherwise */
862{
863    if (mp.normalize_version(found) < mp.normalize_version(minimal)) {
864        mp.alert(sprintf(L("WARNING: %s version found is %s, but %s is needed"),
865                package, found, minimal));
866    }
867}
868
869
870sub mp.test_versions()
871/* tests component versions */
872{
873    mp.assert_version(MPDM.version, "2.72", "MPDM");
874    mp.assert_version(MPSL.VERSION, "2.62", "MPSL");
875}
876
877
878sub mp.load_state()
879{
880    local f = open(HOMEDIR + ".mp_state.json", "r");
881    f->flock(1);
882
883    /* read state file */
884    local j = f->join();
885
886    f->close();
887
888    if (j) {
889        /* parse as json and take the first scanned value */
890        j = j->sscanf("%j")->shift();
891
892        /* must be an object; otherwise, drop it */
893        if (type(j) != "object")
894            j = NULL;
895    }
896
897    mp.state = j || {};
898
899    mp.history = mp.state.history || {};
900}
901
902
903sub mp.save_state()
904{
905    del(mp.history, "[NULL]");
906    mp.state.history = mp.history;
907
908    local f = open(HOMEDIR + ".mp_state.json", "w");
909    f->flock(2);
910
911    f->write(fmt("%j", mp.state))->close();
912}
913
914
915sub mp.exit()
916{
917    mp_c.exit();
918}
919
920
921sub mp.prepare()
922/* things to be done just before driver initializing */
923{
924    if (mp.config.dark_mode) {
925        /* Minimum Profit Dark */
926        mp.colors.normal.gui        |= [ 0xf0f0f0, 0x1b1b1b ];
927        mp.colors.cursor.gui        |= [ 0xf0f0f0, 0x1b1b1b ];
928        mp.colors.selection.gui     |= [ 0xf2777a, 0x000000 ];
929        mp.colors.comments.gui      |= [ 0x00cc77, 0x1b1b1b ];
930        mp.colors.documentation.gui |= [ 0x8888ff, 0x1b1b1b ];
931        mp.colors.quotes.gui        |= [ 0x629FCF, 0x1b1b1b ];
932        mp.colors.matching.gui      |= [ 0xf0f0f0, 0x4b4b4b ];
933        mp.colors.word1.gui         |= [ 0x00aa00, 0x1b1b1b ];
934        mp.colors.word2.gui         |= [ 0xff6666, 0x1b1b1b ];
935        mp.colors.word3.gui         |= [ 0xaaaaaa, 0x1b1b1b ];
936        mp.colors.tag.gui           |= [ 0x8888ff, 0x1b1b1b ];
937        mp.colors.spell.gui         |= [ 0xff8888, 0x1b1b1b ];
938        mp.colors.search.gui        |= [ 0x000000, 0x00cc77 ];
939        mp.colors.search.gui        |= [ 0x000000, 0x00cc77 ];
940        mp.colors.suggest.gui       |= [ 0x000000, 0x808080 ];
941    }
942    else {
943        /* Minimum Profit Classic */
944        mp.colors.normal.gui        |= [ 0x000000, 0xffffff ];
945        mp.colors.cursor.gui        |= [ 0x000000, 0xffffff ];
946        mp.colors.selection.gui     |= [ 0xff0000, 0xffffff ];
947        mp.colors.comments.gui      |= [ 0x00cc77, 0xffffff ];
948        mp.colors.documentation.gui |= [ 0x8888ff, 0xffffff ];
949        mp.colors.quotes.gui        |= [ 0x0000ff, 0xffffff ];
950        mp.colors.matching.gui      |= [ 0x000000, 0xffff00 ];
951        mp.colors.word1.gui         |= [ 0x00aa00, 0xffffff ];
952        mp.colors.word2.gui         |= [ 0xff6666, 0xffffff ];
953        mp.colors.word3.gui         |= [ 0x0000aa, 0xffffff ];
954        mp.colors.tag.gui           |= [ 0x8888ff, 0xffffff ];
955        mp.colors.spell.gui         |= [ 0xff8888, 0xffffff ];
956        mp.colors.search.gui        |= [ 0x000000, 0x00cc77 ];
957        mp.colors.suggest.gui       |= [ 0x000000, 0x808080 ];
958    }
959}
960
961
962/** modules **/
963
964local mp_modules = [
965    'drv',
966    'move',
967    'edit',
968    'file',
969    'clipboard',
970    'search',
971    'tags',
972    'syntax',
973    'macro',
974    'templates',
975    'spell',
976    'misc',
977    'crypt',
978    'session',
979    'build',
980    'writing'
981];
982
983foreach (m, mp_modules) {
984    eval('load("mp_' + m + '.mpsl");');
985
986    if (ERROR != NULL) {
987        local log = mp.open(L("<internal errors>"));
988        log->move_eof();
989        log->insert(ERROR + "\n");
990        log->move_bof();
991
992        log->set_read_only();
993
994        ERROR = NULL;
995    }
996}
997
998/** main **/
999
1000if (ERROR == NULL) {
1001    mp.load_profile();
1002    mp.load_state();
1003    mp.process_cmdline();
1004    mp.prepare();
1005    mp_drv.startup();
1006    mp.test_versions();
1007    mp_drv.main_loop();
1008    mp_drv.shutdown();
1009    mp.save_state();
1010}
1011