The 'Pipes' game is assumed as working (Closes: #1293).
[mp-5.x.git] / mp_file.mpsl
blob0e99903675eedebbee9bf2d172c7ebd3aafdc0cf
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     mp.close();
122     if (mp.long_op(mp.open, p) == NULL && ERRNO != NULL)
123         mp.alert(sprintf("Error opening '%s': %s", p, ERRNO));
125     return d;
128 mp.actions['open_config_file']  = sub (d) {
130     mp.open(HOMEDIR ~ ".mp.mpsl");
133 mp.actions['sync'] = sub (d) {
135     /* save all modified documents */
136     foreach (d, grep(mp.docs, sub (e) { e.txt.mod; }))
137         mp.actions.save(d);
139     return d;
142 mp.actions['exec_command']      = sub (d, cmd) {
144     if (cmd == NULL) {
145         local t = mp.form(
146             [
147                 {
148                     'label' => L("System command:"),
149                     'type' => 'text',
150                     'history' => 'system'
151                 }
152             ]
153         );
155         if (t != NULL)
156             cmd = t[0];
157     }
158     
159     if (cmd != NULL) {
160         /* does it start with a pipe? */
161         if (regex(cmd, '/^\|/')) {
162             local p;
164             /* yes; current document should be fed to it */
165             cmd = sregex(cmd, '/^\|/');
167             if ((p = popen(cmd, "w")) != NULL) {
168                 mp.busy(1);
170                 foreach (l, mp.get_active_area(d))
171                     write(p, l ~ mp.config.eol);
173                 pclose(p);
174                 mp.busy(0);
175             }
176             else
177                 mp.drv.alert(
178                     sprintf(L("Error writing to command '%s'"), cmd));
179         }
180         else {
181             /* no; execute command and insert into cursor */
182             local p;
184             if ((p = popen(cmd, "r")) != NULL) {
185                 local l;
187                 mp.store_undo(d);
188                 mp.busy(1);
190                 while ((l = read(p)) != NULL) {
191                     mp.insert(d, mp.chomp(l));
192                     mp.insert_newline(d);
193                 }
195                 pclose(p);
196                 mp.busy(0);
197             }
198             else
199                 mp.drv.alert(
200                     sprintf(L("Error reading from command '%s'"), cmd));
201         }
202     }
204     return d;
207 mp.actions['filter_selection'] = sub (d, cmd) {
209     if (cmd == NULL) {
210         local t = mp.form(
211             [
212                 {
213                     'label'     => L("System command:"),
214                     'type'      => 'text',
215                     'history'   => 'system2'
216                 }
217             ]
218         );
220         if (t != NULL)
221             cmd = t[0];
222     }
224     if (cmd != NULL) {
225         mp.store_undo(d);
227         /* if there is no selection, take full document */
228         if (d.txt.mark == NULL) {
229             mp.move_bof(d);
230             mp.mark(d);
231             mp.move_eof(d);
232             mp.move_eol(d);
233             mp.mark(d);
234         }
236         /* take it out */
237         mp.cut(d);
239         /* now feed it to the command */
240         local p = popen2(cmd);
242         if (p != NULL) {
243             write(p[1], join(mp.clipboard, "\n"));
244             pclose(p[1]);
246             local l;
247             while ((l = read(p[0])) != NULL)
248                 mp.insert(d, l);
250             pclose(p[0]);
251         }
252     }
254     return d;
257 mp.actions['close_all'] = sub {
259     local s;
261     while (s = size(mp.docs)) {
262         local doc = mp.docs[mp.active_i];
263                 
264         /* close current document */
265         mp.actions.close(doc);
267         /* if the size of the list hasn't changed,
268             action was cancelled, so don't exit */
269         if (s == size(mp.docs))
270             return 0;
271     }
273     return 1;
276 mp.actions['open_under_cursor'] = sub (d) {
277         local w;
279     /* is the word under cursor file:line: ? */
280     if ((w = mp.get_word(d, '/[a-z\._0-9\/ -]+:[0-9]+:/i')) != NULL) {
281         w = split(w, ':');
283         /* drop garbage */
284         pop(w);
286         /* pick the line */
287         local l = pop(w) - 1;
289         /* open the file, rejoining with : */
290         local n = mp.open(join(w, ':'));
292         /* now move to the line */
293         mp.search_set_y(n, l);
295         d = n;
296     }
297     else
298     if ((w = mp.get_word(d, '/[a-z\._0-9\/:-]+/i')) != NULL) {
299         d = mp.open(w);
300     }
302     return d;
305 mp.actions['hex_view']  = sub (d) {
306         local n;
308         if ((n = mp.openfile(L("File to open:"))) != NULL && n ne "")
309                 if (mp.long_op(mp.hex_view, n) == NULL && ERRNO != NULL)
310                         mp.alert(sprintf("Error opening '%s': %s", n, ERRNO));
311         else
312             d = n;
314     return d;
317 mp.actions['open_dropped_files'] = sub (d) {
318     while (size(mp.dropped_files))
319         mp.open(shift(mp.dropped_files));
323 /** default key bindings **/
325 mp.keycodes['ctrl-n']           = 'next';
326 mp.keycodes['ctrl-o']           = 'open';
327 mp.keycodes['ctrl-q']           = 'exit';
328 mp.keycodes['ctrl-s']           = 'save';
329 mp.keycodes['ctrl-w']           = 'close';
330 mp.keycodes['ctrl-enter']       = 'open_under_cursor';
331 mp.keycodes['alt-enter']        = 'open_under_cursor';
332 mp.keycodes['dropped-files']    = 'open_dropped_files';
334 mp.keycodes['close-window']     = 'exit';
336 /** action descriptions **/
338 mp.actdesc['new']                = LL("New");
339 mp.actdesc['save']               = LL("Save...");
340 mp.actdesc['save_as']            = LL("Save as...");
341 mp.actdesc['next']               = LL("Next");
342 mp.actdesc['prev']               = LL("Previous");
343 mp.actdesc['open']               = LL("Open...");
344 mp.actdesc['exit']               = LL("Exit");
345 mp.actdesc['close']              = LL("Close");
346 mp.actdesc['revert']             = LL("Revert");
347 mp.actdesc['close_all']          = LL("Close all");
349 mp.actdesc['open_config_file']   = LL("Edit configuration file");
350 mp.actdesc['sync']               = LL("Save modified texts");
351 mp.actdesc['exec_command']       = LL("Run system command...");
352 mp.actdesc['filter_selection']   = LL("Filter selection through system command...");
353 mp.actdesc['open_under_cursor']  = LL("Open file under cursor");
354 mp.actdesc['hex_view']           = LL("Hexadecimal viewer...");
355 mp.actdesc['open_dropped_files'] = LL("Open dropped files");
357 /** code **/
359 sub mp.chomp(str)
360 /* chomps the end of file chars from a string */
362         sregex(str, "/\r*\n*$/");
366 sub mp.save_th(f, doc)
367 /* mp.save() helper */
369     local nl = 0;
370     local eol = doc.eol || mp.config.eol;
372     doc.disk_op = 1;
374     /* save as a plain text file */
375     foreach (l, doc.txt.lines) {
376         /* write a line separator if it's not the first line */
377         if (nl)
378             write(f, eol);
380         write(f, l);
381         nl++;
382     }
384     doc.disk_op = 0;
386     close(f);
388     return nl;
392 sub mp.save(doc)
393 /* saves a file */
395     local f;
396     local s = NULL;
397     local ret = 0;
399     /* if unlink before write is desired, do it */
400     if (mp.config.unlink && (s = stat(doc.name)) != NULL)
401         unlink(doc.name);
403     /* set the encoding for this file opening */
404     TEMP_ENCODING = doc.encoding;
406     if ((f = open(doc.name, "wb")) == NULL) {
407         /* can't write? delete name */
408         doc.name = L("<unnamed>");
409         ret = -1;
410     }
411     else {
412         ret = 0;
414         /* if the document has a password, save it encrypted */
415         if (doc.password)
416             mp.crypt1_save(f, doc.txt.lines, doc.password);
417         else
418             mp.save_th(f, doc);
419     
420         doc.txt.mod = 0;
421     
422         /* set back the permissions and ownership, if available */
423         if (s != NULL) {
424             chmod(doc.name, s[2]);
425             chown(doc.name, s[4], s[5]);
426         }
427     }
428     
429         return ret;
433 sub mp.save_on_close(doc)
434 /* on_close function to save modified files */
436     local r = 1;
438     if (doc.txt.mod) {
439         r = mp.confirm(L("File has changed. Save changes?"));
441         /* confirm? save */
442         if (r == 1)
443             mp.actions.save(d);
444     }
446     return r;
450 sub mp.create(filename, lines)
451 /* creates a document */
453     local doc = {
454         txt: {
455             x:      0,
456             y:      0,
457             vx:     0,
458             vy:     0,
459             mod:    0,
460             lines:  lines || [ '' ]
461         },
462         name:       filename || L("<unnamed>"),
463         undo:       [],
464         redo:       [],
465         syntax:     NULL,
466         on_close:   [ mp.save_on_close ]
467     };
469         return doc;
473 sub mp.new(filename, lines)
474 /* creates a new document */
476     local doc = mp.create(filename, lines);
478         /* store in the list and set as active */
479     ins(mp.docs, doc, mp.active_i);
481         mp.detect_syntax(doc);
483     return doc;
487 sub mp.next()
488 /* rotates through the document list */
490         if (++mp.active_i == size(mp.docs))
491                 mp.active_i = 0;
493         return mp.active();
497 sub mp.prev()
498 /* rotates through the document list, backwards */
500         if (--mp.active_i == -1)
501                 mp.active_i = size(mp.docs) - 1;
503         return mp.active();
507 sub mp.close()
508 /* closes the active document */
510         local k = mp.active_i;
512         /* delete from the list */
513         adel(mp.docs, mp.active_i);
515         /* rotate if it was the last one */
516         if (mp.active_i == size(mp.docs))
517                 mp.active_i = 0;
519         /* cannot call mp.active() */
523 sub mp.find_file_by_name(filename)
524 /* finds an open file by its name */
526         seek(
527                 map(
528                         mp.docs,
529                         sub(d) { d.name; }
530                 ),
531                 filename
532         );
536 sub mp.open(filename)
537 /* opens a new document (uses UI) */
539         local s;
541         /* looks first if the file is already open */
542         if ((s = mp.find_file_by_name(filename)) != -1) {
543                 mp.active_i = s;
544                 return mp.active();
545         }
547         if ((s = stat(filename)) == NULL) {
548                 mp.message = {
549                         'timeout' => time() + 2,
550                         'string'  => sprintf(L("New file '%s'"), filename)
551                 };
553                 return mp.new(filename);
554         }
556         /* canonicalize, if possible */
557         if (s[13] != NULL) {
558                 filename = s[13];
560                 /* look again for this filename in the open files */
561                 if ((s = mp.find_file_by_name(filename)) != -1) {
562                         mp.active_i = s;
563                         return mp.active();
564                 }
565         }
567         local d, f;
569         if ((f = open(filename, "rb")) == NULL)
570                 return NULL;
571         else {
572                 if (mp.crypt1_detect(f)) {
573                         /* password needed; ask for it */
574                         local p;
576                         if ((p = mp.form( [
577                                 { 'label'       => L("Password:"),
578                                   'type'        => 'password' }
579                                 ])) == NULL) {
580                                 /* cancel? fail, but not on error */
581                                 return NULL;
582                         }
584                         /* get the password */
585                         p = p[0];
587                         /* an empty password is equal to cancellation */
588                         if (p eq '')
589                                 return NULL;
591                         /* and load the file */
592                         d = mp.new(filename, mp.crypt1_load(f, p));
593                         d.password = p;
594                 }
595                 else {
596                         /* close file (needed for rewinding AND
597                            possible encoding autodetection) */
598                         close(f);
600                         /* reopen and read */
601                         f = open(filename, "rb");
602                         d = mp.new(filename, mp.plain_load(f));
603                 }
605                 close(f);
606         }
608         /* store the encoding */
609         d.encoding = DETECTED_ENCODING || ENCODING || '';
611         /* if original EOL is to be kept, store it */
612         if (mp.config.keep_eol)
613                 d.eol = mp.last_seen_eol;
615         return d;
619 sub mp.hex_view_th(filename, d)
620 /* mp.hex_view() helper */
622     local c;
623     local l      = [];
624     local offset = 0;
626     local lines = d.txt.lines;
628     local f = open(filename, 'rb');
630     while (1) {
631         if ((c = getchar(f)) != NULL)
632             push(l, c);
634         if (size(l) == 16 || c == NULL) {
635             local h = '';
636             local a = ' ';
638             /* add hex view */
639             foreach (v, l) {
640                 h = h ~ sprintf(' %02X', ord(v));
642                 if (ord(v) == 0x0a)
643                     v = "\x{00b6}";
644                 else
645                 if (ord(v) < 32 || ord(v) > 126)
646                     v = "\x{00b7}";
648                 a = a ~ v;
649             }
651             local n = 16 - size(l);
653             /* fill up to 16 */
654             while (n--) {
655                 h = h ~ '   ';
656                 a = a ~ ' ';
657             }
659             push(lines, join([ sprintf('| %06X', offset), h, a, ''], ' |'));
660             offset += 16;
661             l = [];
663             if (c == NULL)
664                 break;
665         }
666     }
668     close(f);
670     d.disk_op = 0;
674 sub mp.hex_view(filename)
675 /* shows a file as an hex dump */
677     local f;
678     local d = NULL;
680     if ((f = open(filename, "rb")) != NULL) {
681         close(f);
683         d = mp.new('<' ~ filename ~ ' hex view>', []);
684         d.read_only = 1;
685         d.syntax = mp.syntax.hex_view;
686         d.disk_op = 1;
688         mp.hex_view_th(filename, d);
689     }
691     return d;