Do not use backslash-b to select backspaces.
[mp-5.x.git] / mp_file.mpsl
blobd5355e822d2f61ab8c9587ea2f1b979154083859
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     File manipulation.
8     Copyright (C) 1991-2011 Angel Ortega <angel@triptico.com>
10     This program is free software; you can redistribute it and/or
11     modify it under the terms of the GNU General Public License
12     as published by the Free Software Foundation; either version 2
13     of the License, or (at your option) any later version.
15     This program is distributed in the hope that it will be useful,
16     but WITHOUT ANY WARRANTY; without even the implied warranty of
17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18     GNU General Public License for more details.
20     You should have received a copy of the GNU General Public License
21     along with this program; if not, write to the Free Software
22     Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
24     http://www.triptico.com
28 /** editor actions **/
30 mp.actions['new']       = sub (d) {
31     d = mp.find_file_by_name(L("<unnamed>"));
33     if (d != -1) {
34         mp.active_i = d;
35         d = mp.active();
36     }
37     else
38         d = mp.new();
40     return d;
43 mp.actions['next']      = sub (d) { mp.next(); };
44 mp.actions['prev']      = sub (d) { mp.prev(); };
46 mp.actions['save_as']   = sub (d, newname) {
48     if (newname == NULL)
49         newname = mp.savefile(L("Save file as:"));
51     if (newname != NULL) {
52         /* store new name */
53         d.name = newname;
55         if (mp.long_op(mp.save, d) == -1)
56             mp.alert(sprintf(L("Error saving file: %s"), ERRNO));
57         else
58             mp.detect_syntax(d);
59     }
61     return d;
64 mp.actions['save']      = sub (d) {
66     /* name is <unnamed> or something similar; ask for one */
67     if (regex(d.name, "/^<.+>$/"))
68         mp.actions.save_as(d);
69     else
70     if (mp.long_op(mp.save, d) == -1)
71         mp.alert(sprintf(L("Error saving file: %s"), ERRNO));
73     return d;
76 mp.actions['close']     = sub (d) {
78     /* call all 'on_close' functions */
79     foreach (f, d.on_close) {
81         /* if it returns 0, cancel close */
82         if (f(d) == 0)
83             return;
84     }
86     mp.close();
89 mp.actions['exit']      = sub (d) {
90     if (mp.config.auto_sessions)
91         mp.save_session();
93     if (mp.actions.close_all())
94         mp.exit();
97 mp.actions['open']      = sub (d) {
99     local n;
101     if ((n = mp.openfile(L("File to open:"))) != NULL && n ne "")
102         if (mp.long_op(mp.open, n) == NULL && ERRNO != NULL)
103             mp.alert(sprintf("Error opening '%s': %s", n, ERRNO));
105     return n;
108 mp.actions['revert']    = sub (d) {
109     /* save current name */
110     local p = d.name;
112     if (d.txt.mod) {
113         local r;
114         r = mp.confirm(L("File has changed. Are you sure?"));
116         /* cancel? don't close */
117         if (r == 0 || r == 2)
118             return d;
119     }
121     local x     = d.txt.x;
122     local y     = d.txt.y;
123     local vy    = d.txt.vy;
125     mp.close();
126     if (mp.long_op(mp.open, p) == NULL && ERRNO != NULL)
127         mp.alert(sprintf("Error opening '%s': %s", p, ERRNO));
129     d = mp.active();
131     mp.set_y(d, y);
132     d.txt.vy = vy;
133     mp.set_x(d, x);
135     return d;
138 mp.actions['open_config_file']  = sub (d) {
140     mp.open(HOMEDIR ~ ".mp.mpsl");
143 mp.actions['sync'] = sub (d) {
145     /* save all modified documents */
146     foreach (d, grep(mp.docs, sub (e) { e.txt.mod; }))
147         mp.actions.save(d);
149     return d;
152 mp.actions['exec_command']      = sub (d, cmd) {
154     if (cmd == NULL) {
155         local t = mp.form(
156             [
157                 {
158                     'label' => L("System command:"),
159                     'type' => 'text',
160                     'history' => 'system'
161                 }
162             ]
163         );
165         if (t != NULL)
166             cmd = t[0];
167     }
168     
169     if (cmd != NULL) {
170         /* does it start with a pipe? */
171         if (regex(cmd, '/^\|/')) {
172             local p;
174             /* yes; current document should be fed to it */
175             cmd = sregex(cmd, '/^\|/');
177             if ((p = popen(cmd, "w")) != NULL) {
178                 mp.busy(1);
180                 foreach (l, mp.get_active_area(d))
181                     write(p, l ~ mp.config.eol);
183                 pclose(p);
184                 mp.busy(0);
185             }
186             else
187                 mp.drv.alert(
188                     sprintf(L("Error writing to command '%s'"), cmd));
189         }
190         else {
191             /* no; execute command and insert into cursor */
192             local p;
194             if ((p = popen(cmd, "r")) != NULL) {
195                 local l;
197                 mp.store_undo(d);
198                 mp.busy(1);
200                 while ((l = read(p)) != NULL) {
201                     mp.insert(d, mp.chomp(l));
202                     mp.insert_newline(d);
203                 }
205                 pclose(p);
206                 mp.busy(0);
207             }
208             else
209                 mp.drv.alert(
210                     sprintf(L("Error reading from command '%s'"), cmd));
211         }
212     }
214     return d;
217 mp.actions['filter_selection'] = sub (d, cmd) {
219     if (cmd == NULL) {
220         local t = mp.form(
221             [
222                 {
223                     'label'     => L("System command:"),
224                     'type'      => 'text',
225                     'history'   => 'system2'
226                 }
227             ]
228         );
230         if (t != NULL)
231             cmd = t[0];
232     }
234     if (cmd != NULL) {
235         mp.store_undo(d);
237         /* if there is no selection, take full document */
238         if (d.txt.mark == NULL) {
239             mp.move_bof(d);
240             mp.mark(d);
241             mp.move_eof(d);
242             mp.move_eol(d);
243             mp.mark(d);
244         }
246         /* take it out */
247         mp.cut(d);
249         /* now feed it to the command */
250         local p = popen2(cmd);
252         if (p != NULL) {
253             write(p[1], join(mp.clipboard, "\n"));
254             pclose(p[1]);
256             local l;
257             while ((l = read(p[0])) != NULL)
258                 mp.insert(d, l);
260             pclose(p[0]);
261         }
262     }
264     return d;
267 mp.actions['close_all'] = sub {
269     local s;
271     while (s = size(mp.docs)) {
272         local doc = mp.docs[mp.active_i];
273                 
274         /* close current document */
275         mp.actions.close(doc);
277         /* if the size of the list hasn't changed,
278             action was cancelled, so don't exit */
279         if (s == size(mp.docs))
280             return 0;
281     }
283     return 1;
286 mp.actions['open_under_cursor'] = sub (d) {
287         local w;
289     /* is the word under cursor file:line: ? */
290     if ((w = mp.get_word(d, '/[a-z\._0-9\/ -]+:[0-9]+:/i')) != NULL) {
291         w = split(w, ':');
293         /* drop garbage */
294         pop(w);
296         /* pick the line */
297         local l = pop(w) - 1;
299         /* open the file, rejoining with : */
300         local n = mp.open(join(w, ':'));
302         /* now move to the line */
303         mp.search_set_y(n, l);
305         d = n;
306     }
307     else
308     if ((w = mp.get_word(d, '/[a-z\._0-9\/:-]+/i')) != NULL) {
309         d = mp.open(w);
310     }
312     return d;
315 mp.actions['hex_view']  = sub (d) {
316         local n;
318         if ((n = mp.openfile(L("File to open:"))) != NULL && n ne "")
319                 if (mp.long_op(mp.hex_view, n) == NULL && ERRNO != NULL)
320                         mp.alert(sprintf("Error opening '%s': %s", n, ERRNO));
321         else
322             d = n;
324     return d;
327 mp.actions['open_dropped_files'] = sub (d) {
328     while (size(mp.dropped_files))
329         mp.open(shift(mp.dropped_files));
333 /** default key bindings **/
335 mp.keycodes['ctrl-n']           = 'next';
336 mp.keycodes['ctrl-o']           = 'open';
337 mp.keycodes['ctrl-q']           = 'exit';
338 mp.keycodes['ctrl-s']           = 'save';
339 mp.keycodes['ctrl-w']           = 'close';
340 mp.keycodes['ctrl-enter']       = 'open_under_cursor';
341 mp.keycodes['alt-enter']        = 'open_under_cursor';
342 mp.keycodes['dropped-files']    = 'open_dropped_files';
344 mp.keycodes['close-window']     = 'exit';
346 /** action descriptions **/
348 mp.actdesc['new']                = LL("New");
349 mp.actdesc['save']               = LL("Save...");
350 mp.actdesc['save_as']            = LL("Save as...");
351 mp.actdesc['next']               = LL("Next");
352 mp.actdesc['prev']               = LL("Previous");
353 mp.actdesc['open']               = LL("Open...");
354 mp.actdesc['exit']               = LL("Exit");
355 mp.actdesc['close']              = LL("Close");
356 mp.actdesc['revert']             = LL("Revert");
357 mp.actdesc['close_all']          = LL("Close all");
359 mp.actdesc['open_config_file']   = LL("Edit configuration file");
360 mp.actdesc['sync']               = LL("Save modified texts");
361 mp.actdesc['exec_command']       = LL("Run system command...");
362 mp.actdesc['filter_selection']   = LL("Filter selection through system command...");
363 mp.actdesc['open_under_cursor']  = LL("Open file under cursor");
364 mp.actdesc['hex_view']           = LL("Hexadecimal viewer...");
365 mp.actdesc['open_dropped_files'] = LL("Open dropped files");
367 /** code **/
369 sub mp.chomp(str)
370 /* chomps the end of file chars from a string */
372         sregex(str, "/\r*\n*$/");
376 sub mp.save_th(f, doc)
377 /* mp.save() helper */
379     local nl = 0;
380     local eol = mp.config.keep_eol && doc.eol || mp.config.eol;
382     doc.disk_op = 1;
384     /* save as a plain text file */
385     foreach (l, doc.txt.lines) {
386         /* write a line separator if it's not the first line */
387         if (nl)
388             write(f, eol);
390         write(f, l);
391         nl++;
392     }
394     doc.disk_op = 0;
396     close(f);
398     return nl;
402 sub mp.save(doc)
403 /* saves a file */
405     local f;
406     local s = NULL;
407     local ret = 0;
409     /* if unlink before write is desired, do it */
410     if (mp.config.unlink && (s = stat(doc.name)) != NULL)
411         unlink(doc.name);
413     /* set the encoding for this file opening */
414     TEMP_ENCODING = doc.encoding;
416     if ((f = open(doc.name, "wb")) == NULL) {
417         /* can't write? delete name */
418         doc.name = L("<unnamed>");
419         ret = -1;
420     }
421     else {
422         ret = 0;
424         /* if the document has a password, save it encrypted */
425         if (doc.password)
426             mp.crypt1_save(f, doc.txt.lines, doc.password);
427         else
428             mp.save_th(f, doc);
429     
430         doc.txt.mod = 0;
431     
432         /* set back the permissions and ownership, if available */
433         if (s != NULL) {
434             chmod(doc.name, s[2]);
435             chown(doc.name, s[4], s[5]);
436         }
438         s = stat(doc.name);
439         doc.mtime = s[9];
440     }
441     
442         return ret;
446 sub mp.save_on_close(doc)
447 /* on_close function to save modified files */
449     local r = 1;
451     if (doc.txt.mod) {
452         r = mp.confirm(L("File has changed. Save changes?"));
454         /* confirm? save */
455         if (r == 1)
456             mp.actions.save(d);
457     }
459     return r;
463 sub mp.create(filename, lines)
464 /* creates a document */
466     local doc = {
467         txt: {
468             x:      0,
469             y:      0,
470             vx:     0,
471             vy:     0,
472             mod:    0,
473             lines:  lines || [ '' ]
474         },
475         name:       filename || L("<unnamed>"),
476         undo:       [],
477         redo:       [],
478         syntax:     NULL,
479         on_close:   [ mp.save_on_close ]
480     };
482         return doc;
486 sub mp.new(filename, lines)
487 /* creates a new document */
489     local doc = mp.create(filename, lines);
491         /* store in the list and set as active */
492     ins(mp.docs, doc, mp.active_i);
494         mp.detect_syntax(doc);
496     return doc;
500 sub mp.next()
501 /* rotates through the document list */
503         if (++mp.active_i == size(mp.docs))
504                 mp.active_i = 0;
506         return mp.active();
510 sub mp.prev()
511 /* rotates through the document list, backwards */
513         if (--mp.active_i == -1)
514                 mp.active_i = size(mp.docs) - 1;
516         return mp.active();
520 sub mp.close()
521 /* closes the active document */
523         local k = mp.active_i;
525         /* delete from the list */
526         adel(mp.docs, mp.active_i);
528         /* rotate if it was the last one */
529         if (mp.active_i == size(mp.docs))
530                 mp.active_i = 0;
532         /* cannot call mp.active() */
536 sub mp.find_file_by_name(filename)
537 /* finds an open file by its name */
539         seek(
540                 map(
541                         mp.docs,
542                         sub(d) { d.name; }
543                 ),
544                 filename
545         );
549 sub mp.open(filename)
550 /* opens a new document (uses UI) */
552         local s;
554         /* looks first if the file is already open */
555         if ((s = mp.find_file_by_name(filename)) != -1) {
556                 mp.active_i = s;
557                 return mp.active();
558         }
560         if ((s = stat(filename)) == NULL) {
561                 mp.message = {
562                         'timeout' => time() + 2,
563                         'string'  => sprintf(L("New file '%s'"), filename)
564                 };
566                 return mp.new(filename);
567         }
569         /* canonicalize, if possible */
570         if (s[13] != NULL) {
571                 filename = s[13];
573                 /* look again for this filename in the open files */
574                 if ((s = mp.find_file_by_name(filename)) != -1) {
575                         mp.active_i = s;
576                         return mp.active();
577                 }
578         }
580         local d, f;
582         if ((f = open(filename, "rb")) == NULL)
583                 return NULL;
584         else {
585                 if (mp.crypt1_detect(f)) {
586                         /* password needed; ask for it */
587                         local p;
589                         if ((p = mp.form( [
590                                 { 'label'       => L("Password:"),
591                                   'type'        => 'password' }
592                                 ])) == NULL) {
593                                 /* cancel? fail, but not on error */
594                                 return NULL;
595                         }
597                         /* get the password */
598                         p = p[0];
600                         /* an empty password is equal to cancellation */
601                         if (p eq '')
602                                 return NULL;
604                         /* and load the file */
605                         d = mp.new(filename, mp.crypt1_load(f, p));
606                         d.password = p;
607                 }
608                 else {
609                         /* close file (needed for rewinding AND
610                            possible encoding autodetection) */
611                         close(f);
613                         /* reopen and read */
614                         f = open(filename, "rb");
615                         d = mp.new(filename, mp.plain_load(f));
616                 }
618         s = stat(filename);
619         d.mtime = s[9];
621                 close(f);
622         }
624         /* store the encoding */
625         d.encoding = DETECTED_ENCODING || ENCODING || '';
627     /* always keep original EOL */
628     d.eol = mp.last_seen_eol;
630         return d;
634 sub mp.hex_view_th(filename, d)
635 /* mp.hex_view() helper */
637     local c;
638     local l      = [];
639     local offset = 0;
641     local lines = d.txt.lines;
643     local f = open(filename, 'rb');
645     while (1) {
646         if ((c = getchar(f)) != NULL)
647             push(l, c);
649         if (size(l) == 16 || c == NULL) {
650             local h = '';
651             local a = ' ';
653             /* add hex view */
654             foreach (v, l) {
655                 h = h ~ sprintf(' %02X', ord(v));
657                 if (ord(v) == 0x0a)
658                     v = "\x{00b6}";
659                 else
660                 if (ord(v) < 32 || ord(v) > 126)
661                     v = "\x{00b7}";
663                 a = a ~ v;
664             }
666             local n = 16 - size(l);
668             /* fill up to 16 */
669             while (n--) {
670                 h = h ~ '   ';
671                 a = a ~ ' ';
672             }
674             push(lines, join([ sprintf('| %06X', offset), h, a, ''], ' |'));
675             offset += 16;
676             l = [];
678             if (c == NULL)
679                 break;
680         }
681     }
683     close(f);
685     d.disk_op = 0;
689 sub mp.hex_view(filename)
690 /* shows a file as an hex dump */
692     local f;
693     local d = NULL;
695     if ((f = open(filename, "rb")) != NULL) {
696         close(f);
698         d = mp.new('<' ~ filename ~ ' hex view>', []);
699         d.read_only = 1;
700         d.syntax = mp.syntax.hex_view;
701         d.disk_op = 1;
703         mp.hex_view_th(filename, d);
704     }
706     return d;