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