New helper function for mp.hex_view().
[mp-5.x.git] / mp_file.mpsl
blobe25794926a1692a37e5417685a988c27713d8524
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     File manipulation.
8     Copyright (C) 1991-2010 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();
41 mp.actions['next']      = sub (d) { mp.next(); };
42 mp.actions['prev']      = sub (d) { mp.prev(); };
44 mp.actions['save_as']   = sub (d) {
46         local t;
48         if ((t = mp.savefile(L("Save file as:"))) == NULL)
49                 return;
51         /* store new name */
52         d.name = t;
54         if (mp.long_op(mp.save, d) == -1)
55                 mp.alert(sprintf(L("Error saving file: %s"), ERRNO));
56         else
57                 mp.detect_syntax(d);
60 mp.actions['save']      = sub (d) {
62         /* name is <unnamed> or something similar; ask for one */
63         if (regex(d.name, "/^<.+>$/"))
64                 mp.actions.save_as(d);
65         else
66         if (mp.long_op(mp.save, d) == -1)
67                 mp.alert(sprintf(L("Error saving file: %s"), ERRNO));
70 mp.actions['close']     = sub (d) {
72         if (d.txt.mod) {
73                 local r;
74                 r = mp.confirm(L("File has changed. Save changes?"));
76                 /* cancel? don't close */
77                 if (r == 0)
78                         return;
79                 if (r == 1)
80                         mp.actions.save(d);
81         }
83         mp.close();
86 mp.actions['exit']      = sub (d) {
88         local s;
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));
106 mp.actions['revert']    = sub (d) {
107         /* save current name */
108         local p = d.name;
110         if (d.txt.mod) {
111                 local r;
112                 r = mp.confirm(L("File has changed. Are you sure?"));
114                 /* cancel? don't close */
115                 if (r == 0 || r == 2)
116                         return;
117         }
119         mp.close();
120         if (mp.long_op(mp.open, p) == NULL && ERRNO != NULL)
121                 mp.alert(sprintf("Error opening '%s': %s", p, ERRNO));
124 mp.actions['open_config_file']  = sub (d) {
126         mp.open(HOMEDIR ~ ".mp.mpsl");
129 mp.actions['sync'] = sub (d) {
131         /* save all modified documents */
132         foreach (d, grep(mp.docs, sub (e) { e.txt.mod; }))
133                 mp.actions.save(d);
136 mp.actions['exec_command']      = sub (d) {
138         local t = mp.form( [
139                 { 'label' => L("System command:"),
140                   'type' => 'text',
141                   'history' => 'system' }
142                 ]);
144         if (t != NULL) {
145                 local cmd = t[0];
147                 /* does it start with a pipe? */
148                 if (regex(cmd, '/^\|/')) {
149                         local p;
151                         /* yes; current document should be fed to it */
152                         cmd = sregex(cmd, '/^\|/');
154                         if ((p = popen(cmd, "w")) != NULL) {
155                                 foreach (l, mp.get_active_area(d))
156                                         write(p, l ~ mp.config.eol);
158                                 pclose(p);
159                         }
160                         else
161                                 mp.drv.alert(
162                                         sprintf(L("Error writing to command '%s'"), cmd));
163                 }
164                 else {
165                         /* no; execute command and insert into cursor */
166                         local p;
168                         if ((p = popen(cmd, "r")) != NULL) {
169                                 local l;
171                                 mp.store_undo(d);
173                                 while ((l = read(p)) != NULL) {
174                                         mp.insert(d, mp.chomp(l));
175                                         mp.insert_newline(d);
176                                 }
178                                 pclose(p);
179                         }
180                         else
181                                 mp.drv.alert(
182                                         sprintf(L("Error reading from command '%s'"), cmd));
183                 }
184         }
187 mp.actions['filter_selection'] = sub (d) {
188     local t = mp.form( [
189         { 'label' => L("System command:"),
190             'type' => 'text',
191             'history' => 'system2'
192         }
193     ]);
195     if (t != NULL) {
196         local cmd = t[0];
198         mp.store_undo(d);
200         /* if there is no selection, take full document */
201         if (d.txt.mark == NULL) {
202             mp.move_bof(d);
203             mp.mark(d);
204             mp.move_eof(d);
205             mp.move_eol(d);
206             mp.mark(d);
207         }
209         /* take it out */
210         mp.cut(d);
212         /* now feed it to the command */
213         local p = popen2(cmd);
215         if (p != NULL) {
216             write(p[1], join(mp.clipboard, "\n"));
217             pclose(p[1]);
219             local l;
220             while ((l = read(p[0])) != NULL)
221                 mp.insert(d, l);
223             pclose(p[0]);
224         }
225     }
228 mp.actions['close_all'] = sub {
230         local s;
232         while (s = size(mp.docs)) {
233                 local doc = mp.docs[mp.active_i];
234                 
235                 /* close current document */
236                 mp.actions.close(doc);
238                 /* if the size of the list hasn't changed,
239                    action was cancelled, so don't exit */
240                 if (s == size(mp.docs))
241                         return 0;
242         }
244         return 1;
247 mp.actions['open_under_cursor'] = sub (d) {
248         local w;
250         /* is the word under cursor file:line: ? */
251         if ((w = mp.get_word(d, '/[a-z\._0-9\/ :-]+:[0-9]+: ?/i')) != NULL) {
252                 w = split(w, ':');
254                 /* drop garbage */
255                 pop(w);
257                 /* pick the line */
258                 local l = pop(w) - 1;
260                 /* open the file, rejoining with : */
261                 local n = mp.open(join(w, ':'));
263                 /* now move to the line */
264                 mp.search_set_y(n, l);
265         }
266         else
267         if ((w = mp.get_word(d, '/[a-z\._0-9\/:-]+/i')) != NULL) {
268                 mp.open(w);
269         }
272 mp.actions['hex_view']  = sub (d) {
274         local n;
276         if ((n = mp.openfile(L("File to open:"))) != NULL && n ne "")
277                 if (mp.long_op(mp.hex_view, n) == NULL && ERRNO != NULL)
278                         mp.alert(sprintf("Error opening '%s': %s", n, ERRNO));
281 mp.actions['open_dropped_files'] = sub (d) {
282         while (size(mp.dropped_files))
283                 mp.open(shift(mp.dropped_files));
287 /** default key bindings **/
289 mp.keycodes['ctrl-n']           = 'next';
290 mp.keycodes['ctrl-o']           = 'open';
291 mp.keycodes['ctrl-q']           = 'exit';
292 mp.keycodes['ctrl-s']           = 'save';
293 mp.keycodes['ctrl-w']           = 'close';
294 mp.keycodes['ctrl-enter']       = 'open_under_cursor';
295 mp.keycodes['alt-enter']        = 'open_under_cursor';
296 mp.keycodes['dropped-files']    = 'open_dropped_files';
298 mp.keycodes['close-window']     = 'exit';
300 /** action descriptions **/
302 mp.actdesc['new']                = LL("New");
303 mp.actdesc['save']               = LL("Save...");
304 mp.actdesc['save_as']            = LL("Save as...");
305 mp.actdesc['next']               = LL("Next");
306 mp.actdesc['prev']               = LL("Previous");
307 mp.actdesc['open']               = LL("Open...");
308 mp.actdesc['exit']               = LL("Exit");
309 mp.actdesc['close']              = LL("Close");
310 mp.actdesc['revert']             = LL("Revert");
311 mp.actdesc['close_all']          = LL("Close all");
313 mp.actdesc['open_config_file']   = LL("Edit configuration file");
314 mp.actdesc['sync']               = LL("Save modified texts");
315 mp.actdesc['exec_command']       = LL("Run system command...");
316 mp.actdesc['filter_selection']   = LL("Filter selection through system command...");
317 mp.actdesc['open_under_cursor']  = LL("Open file under cursor");
318 mp.actdesc['hex_view']           = LL("Hexadecimal viewer...");
319 mp.actdesc['open_dropped_files'] = LL("Open dropped files");
321 /** code **/
323 sub mp.chomp(str)
324 /* chomps the end of file chars from a string */
326         sregex(str, "/\r*\n*$/");
330 sub mp.save(doc)
331 /* saves a file */
333         local f;
334         local s = NULL;
335         local nl = 0;
337         /* if unlink before write is desired, do it */
338         if (mp.config.unlink && (s = stat(doc.name)) != NULL)
339                 unlink(doc.name);
341         /* set the encoding for this file opening */
342         TEMP_ENCODING = doc.encoding;
344         if ((f = open(doc.name, "wb")) == NULL) {
345                 /* can't write? delete name */
346                 doc.name = L("<unnamed>");
347                 return -1;
348         }
350         /* if the document has a password, save it encrypted */
351         if (doc.password)
352                 nl = mp.crypt1_save(f, doc.txt.lines, doc.password);
353         else {
354                 /* save as a plain text file */
355                 foreach (l, doc.txt.lines) {
356                         /* write a line separator if it's not the first line */
357                         if (nl)
358                                 write(f, doc.eol || mp.config.eol);
360                         write(f, l);
361                         nl++;
362                 }
363         }
365         close(f);
367         doc.txt.mod = 0;
369         /* set back the permissions and ownership, if available */
370         if (s != NULL) {
371                 chmod(doc.name, s[2]);
372                 chown(doc.name, s[4], s[5]);
373         }
375         return nl;
379 sub mp.create(filename, lines)
380 /* creates a document */
382         local doc, txt;
384         txt = {};
385         txt.x = 0;
386         txt.y = 0;
387         txt.vx = 0;
388         txt.vy = 0;
389         txt.lines = lines || [ '' ];
390         txt.mod = 0;
392         doc = {};
393         doc.name = filename || L("<unnamed>");
394         doc.txt = txt;
396         doc.undo = [];
397         doc.redo = [];
399         doc.syntax = NULL;
401         return doc;
405 sub mp.new(filename, lines)
406 /* creates a new document */
408     local doc = mp.create(filename, lines);
410         /* store in the list and set as active */
411         push(mp.docs, doc);
412         mp.active_i = size(mp.docs) - 1;
414         mp.detect_syntax(doc);
416     return doc;
420 sub mp.next()
421 /* rotates through the document list */
423         if (++mp.active_i == size(mp.docs))
424                 mp.active_i = 0;
426         return mp.active();
430 sub mp.prev()
431 /* rotates through the document list, backwards */
433         if (--mp.active_i == -1)
434                 mp.active_i = size(mp.docs) - 1;
436         return mp.active();
440 sub mp.close()
441 /* closes the active document */
443         local k = mp.active_i;
445         /* delete from the list */
446         adel(mp.docs, mp.active_i);
448         /* rotate if it was the last one */
449         if (mp.active_i == size(mp.docs))
450                 mp.active_i = 0;
452         /* cannot call mp.active() */
456 sub mp.find_file_by_name(filename)
457 /* finds an open file by its name */
459         seek(
460                 map(
461                         mp.docs,
462                         sub(d) { d.name; }
463                 ),
464                 filename
465         );
469 sub mp.open(filename)
470 /* opens a new document (uses UI) */
472         local s;
474         /* looks first if the file is already open */
475         if ((s = mp.find_file_by_name(filename)) != -1) {
476                 mp.active_i = s;
477                 return mp.active();
478         }
480         if ((s = stat(filename)) == NULL) {
481                 mp.message = {
482                         'timeout' => time() + 2,
483                         'string'  => sprintf(L("New file '%s'"), filename)
484                 };
486                 return mp.new(filename);
487         }
489         /* canonicalize, if possible */
490         if (s[13] != NULL) {
491                 filename = s[13];
493                 /* look again for this filename in the open files */
494                 if ((s = mp.find_file_by_name(filename)) != -1) {
495                         mp.active_i = s;
496                         return mp.active();
497                 }
498         }
500         local d, f;
502         if ((f = open(filename, "rb")) == NULL)
503                 return NULL;
504         else {
505                 if (mp.crypt1_detect(f)) {
506                         /* password needed; ask for it */
507                         local p;
509                         if ((p = mp.form( [
510                                 { 'label'       => L("Password:"),
511                                   'type'        => 'password' }
512                                 ])) == NULL) {
513                                 /* cancel? fail, but not on error */
514                                 return NULL;
515                         }
517                         /* get the password */
518                         p = p[0];
520                         /* an empty password is equal to cancellation */
521                         if (p eq '')
522                                 return NULL;
524                         /* and load the file */
525                         d = mp.new(filename, mp.crypt1_load(f, p));
526                         d.password = p;
527                 }
528                 else {
529                         /* close file (needed for rewinding AND
530                            possible encoding autodetection) */
531                         close(f);
533                         /* reopen and read */
534                         f = open(filename, "rb");
535                         d = mp.new(filename, mp.plain_load(f));
536                 }
538                 close(f);
539         }
541         /* store the encoding */
542         d.encoding = DETECTED_ENCODING || ENCODING || '';
544         /* if original EOL is to be kept, store it */
545         if (mp.config.keep_eol)
546                 d.eol = mp.last_seen_eol;
548         return d;
552 sub mp.hex_view_th(f, d)
553 /* mp.hex_view() helper */
555     local c;
556     local l      = [];
557     local offset = 0;
559     local lines = d.txt.lines;
561     while (1) {
562         if ((c = getchar(f)) != NULL)
563             push(l, c);
565         if (size(l) == 16 || c == NULL) {
566             local h = '';
567             local a = ' ';
569             /* add hex view */
570             foreach (v, l) {
571                 h = h ~ sprintf(' %02X', ord(v));
573                 if (ord(v) == 0x0a)
574                     v = "\x{00b6}";
575                 else
576                 if (ord(v) < 32 || ord(v) > 126)
577                     v = "\x{00b7}";
579                 a = a ~ v;
580             }
582             local n = 16 - size(l);
584             /* fill up to 16 */
585             while (n--) {
586                 h = h ~ '   ';
587                 a = a ~ ' ';
588             }
590             push(lines, join([ sprintf('| %06X', offset), h, a, ''], ' |'));
591             offset += 16;
592             l = [];
594             if (c == NULL)
595                 break;
596         }
597     }
599     close(f);
603 sub mp.hex_view(filename)
604 /* shows a file as an hex dump */
606     local f;
607     local d = NULL;
609     if ((f = open(filename, "rb")) != NULL) {
610         local lines =   [];
612         d = mp.new('<' ~ filename ~ ' hex view>', lines);
613         d.read_only = 1;
614         d.syntax = mp.syntax.hex_view;
616         mp.hex_view_th(f, d);
617     }
619     return d;