nftable-migration
  1;;; portal.el --- Run processes in portals
  2;;
  3;; Copyright (C) 2024 Chris Done
  4;;
  5;; This file is free software; you can redistribute it and/or modify
  6;; it under the terms of the GNU General Public License as published by
  7;; the Free Software Foundation; either version 2, or (at your option)
  8;; any later version.
  9
 10;; This file is distributed in the hope that it will be useful,
 11;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 12;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13;; GNU General Public License for more details.
 14
 15;; You should have received a copy of the GNU General Public License
 16;; along with GNU Emacs; see the file COPYING.  If not, write to
 17;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 18;; Boston, MA 02111-1307, USA.
 19
 20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 21;; Customizations
 22
 23(defgroup portal nil
 24  "Portal group."
 25  :group 'convenience)
 26
 27(defcustom portal-outputs-directory
 28  "~/.local/share/portals/"
 29  "Directory where to create output artifacts."
 30  :type 'string :group 'portal)
 31
 32(defcustom portal-default-stdout-buffer-len
 33  4096
 34  "Default buffer length for the stdout preview."
 35  :group 'portal :type 'number)
 36
 37(defcustom portal-default-stderr-buffer-len
 38  4096
 39  "Default buffer length for the stderr preview."
 40  :group 'portal :type 'number)
 41
 42(defface portal-face
 43  '((((class color) (background dark))
 44     (:foreground "#fff" :bold t))
 45    (((class color) (background light))
 46     (:foreground "#000" :bold t)))
 47  "Portal face."
 48  :group 'portal)
 49
 50(defface portal-exited-stdout-face
 51  '((t :foreground "#acac9e"))
 52  "Portal exited stdout face."
 53  :group 'portal)
 54
 55(defface portal-timestamp-face
 56  '((t :foreground "#888888"))
 57  "Portal exited stdout face."
 58  :group 'portal)
 59
 60(defface portal-exited-stderr-face
 61  '((t :foreground "#aa7070"))
 62  "Portal exited stderr face."
 63  :group 'portal)
 64
 65(defface portal-exit-success-face
 66  '((t :foreground "#89b664"))
 67  "Portal exit successful face."
 68  :group 'portal)
 69
 70(defface portal-exit-failure-face
 71  '((t :foreground "#ae6161"))
 72  "Portal exit failure face."
 73  :group 'portal)
 74
 75(defface portal-meta-face
 76  '((t :foreground "#89b664"))
 77  "Portal meta face."
 78  :group 'portal)
 79
 80(defface portal-stdout-face
 81  '((t :inherit 'default))
 82  "Portal stdout face."
 83  :group 'portal)
 84
 85(defface portal-stderr-face
 86  '((t :foreground "#ae6161"))
 87  "Portal stderr face."
 88  :group 'portal)
 89
 90;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 91;; Interactive commands
 92
 93(defun portal-insert-shell-command (command)
 94  "Launch an asynchronous shell of COMMAND, make a portal associated
 95with the current buffer and insert the portal into the current
 96buffer."
 97  (interactive "sCommand: ")
 98  (portal-insert-command
 99   (list shell-file-name shell-command-switch command)))
100
101(defun portal-open-stdout ()
102  "Open the stdout of the file at point."
103  (interactive)
104  (with-current-buffer (find-file-other-window (portal-file-name (portal-at-point) "stdout"))
105    (portal-ansi-colors-minor-mode)
106    (auto-revert-tail-mode)
107    (goto-char (point-max))
108    (push-mark (point-max))))
109
110(defun portal-open-stderr ()
111  "Open the stderr of the file at point."
112  (interactive)
113  (with-current-buffer (find-file-other-window (portal-file-name (portal-at-point) "stderr"))
114    (portal-ansi-colors-minor-mode)
115    (auto-revert-tail-mode)
116    (goto-char (point-max))
117    (push-mark (point-max))))
118
119(defun portal-interrupt ()
120  "Interrupt the process at point."
121  (interactive)
122  (let ((proc (get-process (portal-process-name (portal-at-point)))))
123    (when (process-live-p proc)
124      (interrupt-process proc))))
125
126(defun portal-rerun ()
127  "Re-run portal at point."
128  (interactive)
129  (portal-jump-to-portal)
130  (let* ((portal (portal-at-point))
131         (command (portal-read-json-file portal "command"))
132         (env (portal-read-json-file portal "env"))
133         (default-directory (portal-read-json-file portal "directory")))
134    (portal-interrupt)
135    (delete-region (line-beginning-position) (line-end-position))
136    (portal-wipe-summary)
137    (portal-insert-command (append command nil))
138    (portal-refresh-soon)))
139
140(defun portal-edit ()
141  "Edit and re-run portal at point."
142  (interactive)
143  (portal-jump-to-portal)
144  (portal-interrupt)
145  (let* ((portal (portal-at-point))
146         (command
147          (vector
148           shell-file-name
149           shell-command-switch
150           (read-from-minibuffer
151            "Edit command: "
152            (portal-as-shell-command (portal-read-json-file portal "command")))))
153         (env (portal-read-json-file portal "env"))
154         (default-directory (portal-read-json-file portal "directory")))
155    (delete-region (line-beginning-position) (line-end-position))
156    (portal-wipe-summary)
157    (portal-insert-command (append command nil))
158    (portal-refresh-soon)))
159
160(defun portal-clone ()
161  "Clone the portal at point."
162  (interactive)
163  (portal-jump-to-portal)
164  (let* ((portal (portal-at-point))
165         (command (portal-read-json-file portal "command"))
166         (env (portal-read-json-file portal "env"))
167         (default-directory (portal-read-json-file portal "directory")))
168    (save-excursion (insert "\n"))
169    (portal-insert-command (append  command nil))
170    (portal-refresh-soon)))
171
172;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
173;; Launching processes
174
175(defun portal-start (buffer portal stdout-path stderr-path program program-args)
176  "Run PROGRAM-PATH with ARGS, connect it to portal PORTAL in buffer
177BUFFER, and write the stdout to STDOUT-PATH and stderr to
178STDERR-PATH."
179  (let* ((stderr-pipe
180          (make-pipe-process
181           :name (portal-stderr-process-name portal)
182           :buffer buffer
183           :noquery t
184           :filter 'portal-process-filter
185           :sentinel 'portal-stderr-pipe-sentinel))
186         (main-process
187          (make-process
188           :name (portal-process-name portal)
189           :buffer buffer
190           :command (cons program program-args)
191           :noquery nil
192           :connection-type 'pipe
193           :sentinel 'portal-main-process-sentinel
194           :filter 'portal-process-filter
195           :stderr stderr-pipe)))
196
197    (process-put stderr-pipe :portal portal)
198    (process-put stderr-pipe :output-path stderr-path)
199    (process-put stderr-pipe :buffer "")
200    (process-put stderr-pipe :buffer-len portal-default-stderr-buffer-len)
201
202    (process-put main-process :portal portal)
203    (process-put main-process :output-path stdout-path)
204    (process-put main-process :buffer "")
205    (process-put main-process :buffer-len portal-default-stdout-buffer-len)
206
207    ;; Connect the two processes.
208    (process-put main-process :stderr-process stderr-pipe)
209
210    (portal-write-json-file portal "command" (apply #'vector (cons program program-args)))
211    (portal-write-json-file portal "env" (apply #'vector process-environment))
212    (portal-write-json-file portal "directory" default-directory)
213    (portal-write-json-file portal "status" (format "%S" (process-status main-process)))))
214
215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
216;; Process filtering
217
218(defun portal-process-filter (process output)
219  (let ((filepath (process-get process :output-path)))
220    (when debug-on-error
221      (message "portal-process-filter: Writing to %s" filepath))
222    (portal-accumulate-buffer process output)
223    (with-temp-buffer
224      (insert output)
225      (write-region (point-min) (point-max) filepath :append :no-messages))))
226
227(defun portal-accumulate-buffer (process output)
228  "Accumulate some OUTPUT into PROCESS's preview buffer."
229  (process-put
230   process
231   :buffer (portal-shrink-preview
232            (process-get process :buffer-len)
233            (concat (process-get process :buffer) output))))
234
235(defun portal-shrink-preview (len string)
236  "Shrink a preview buffer STRING to the right length."
237  (if (> (length string) len)
238      (substring string (- len))
239    string))
240
241;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
242;; Sentinels
243
244(defun portal-main-process-sentinel (process event)
245  "Handles the main process's status updates."
246  (when debug-on-error
247    (message "main-process-sentinel: %S: %S" process event))
248  (portal-write-json-file
249   (process-get process :portal)
250   "status" (format "%S" (process-exit-status process))))
251
252(defun portal-stderr-pipe-sentinel (process event)
253  "Handles the stderr pipe's status updates."
254  (when debug-on-error
255    (message "stderr-pipe-sentinel: %S: %S" process event)))
256
257;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
258;; File/directory operations
259
260(defun portal-ensure-directory (portal)
261  "Create the stdout/stderr files for PORTAL in an appropriate
262location."
263  (let ((directory (concat (file-name-as-directory portal-outputs-directory) portal)))
264    (make-directory directory :including-parents)
265    directory))
266
267(defun portal-directory-exists-p (portal)
268  "Check PORTAL has a directory that exists."
269  (let ((directory (concat (file-name-as-directory portal-outputs-directory) portal)))
270    (file-exists-p directory)))
271
272(defun portal-file-exists-p (portal name)
273  "Check PORTAL has a file NAME that exists."
274  (let ((directory (concat (file-name-as-directory portal-outputs-directory) portal)))
275    (file-exists-p (concat (file-name-as-directory directory) name))))
276
277(defun portal-persist-file (portal name content)
278  "Persist CONTENT to disk with filename NAME."
279  (with-temp-buffer
280    (insert content)
281    (write-region
282     (point-min) (point-max)
283     (portal-file-name portal name)
284     nil ; no-append
285     :no-messages))
286  content)
287
288(defun portal-write-json-file (portal name expr)
289  "Print EXPR to disk with filename NAME."
290  (with-temp-buffer
291    (insert (json-serialize expr))
292    (write-region
293     (point-min) (point-max)
294     (portal-file-name portal name)
295     nil ; no-append
296     :no-messages))
297  expr)
298
299(defun portal-read-json-file (portal name)
300  "Read JSON content from file NAME for the given PORTAL."
301  (with-temp-buffer
302    (insert-file-contents (portal-file-name portal name))
303    (json-parse-string (buffer-string))))
304
305(defun portal-read-file (portal name)
306  "Read content from file NAME for the given PORTAL."
307  (with-temp-buffer
308    (let ((file (portal-file-name portal name)))
309      (when (file-exists-p file)
310        (insert-file-contents file)))
311    (buffer-string)))
312
313(defun portal-tail-file (portal n name)
314  "Tail last N lines of file NAME for the given PORTAL."
315  (with-temp-buffer
316    (let ((file (portal-file-name portal name)))
317      (if (file-exists-p file)
318          (portal-tail-n-lines n file)
319        ""))))
320
321;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
322;; Nano-IDs
323
324(defun portal-generate-nanoid ()
325  "Generate a Nano ID of the form `portal_NGMyMDVkZjZiYTVlZTVhM' using SHA-1."
326  (let* ((random-string (format "%s%s%S" (emacs-pid) (current-time-string) (random)))
327         (sha1-hash (secure-hash 'sha1 random-string))
328         (base64-encoded (base64-encode-string sha1-hash))
329         (nanoid (string-trim-right (substring base64-encoded 0 21))))
330    (concat "portal_" nanoid)))
331
332;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
333;; A minor mode for applying ansi-term colors to a buffer
334
335(define-minor-mode portal-ansi-colors-minor-mode
336  "Apply ANSI colors for terminal outputs."
337  :init-value nil
338  :lighter "ANSI"
339  (ansi-color-apply-on-region (point-min) (point-max)))
340
341;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
342;; A small minor mode that just sets up a timer that runs a thing in a
343;; given buffer every N seconds
344
345(defvar-local portal-alpha-timer
346    nil)
347
348(define-minor-mode portal-alpha-minor-mode
349  "TODO"
350  :init-value nil
351  :lighter "@"
352  (when portal-alpha-timer (cancel-timer portal-alpha-timer))
353  (when portal-alpha-minor-mode
354    (setq portal-alpha-timer
355          (run-with-timer 1 2 'portal-beta-refresh (current-buffer)))))
356
357(defun portal-refresh-soon ()
358  "Trigger a refresh within the blink of an eye, but no sooner, or
359later."
360  (run-with-timer 0.100 nil 'portal-beta-refresh (current-buffer)))
361
362(defun portal-beta-refresh (buffer)
363  "Refresh portal displays."
364  (when (buffer-live-p buffer)
365    (let ((window (get-buffer-window buffer)))
366      (when window
367        (with-current-buffer buffer
368          (let ((point (point)))
369            (save-excursion
370              (goto-char (point-min))
371              (while (and (re-search-forward portal-regexp nil t nil)
372                          (<= (point) (window-end window)))
373                (when (<= (window-start window) (point) (window-end window))
374                  (let* ((portal (match-string 0))
375                         (process (get-process (portal-process-name portal)))
376                         (summary (if (portal-directory-exists-p portal)
377                                      (portal-summary portal process)
378                                    "# Invalid portal."))
379                         (match-end (match-end 0))
380                         (old-summary (get-text-property (line-beginning-position) 'portal-summary)))
381                    (unless (and old-summary (string= summary old-summary))
382                      (put-text-property (line-beginning-position) (point)
383                                         'portal-summary
384                                         summary)
385                      (put-text-property (line-beginning-position) (point)
386                                         'portal
387                                         portal)
388                      (portal-wipe-summary)
389                      (insert "\n" summary))))))
390            (goto-char point)))))))
391
392(defun portal-wipe-summary ()
393  "Wipe the '# summary' lines that follow the portal."
394  (save-excursion
395    (when (looking-at "\n#")
396      (forward-line 1)
397      (let ((point (point)))
398        (or (search-forward-regexp "^[^#]" nil t 1)
399            (goto-char (point-max)))
400        (delete-matching-lines "^#" point (point))))))
401
402(defun portal-summary (portal process)
403  "Generate a summary of the portal."
404  (let* ((command (portal-read-json-file portal "command"))
405         (directory (portal-read-json-file portal "directory"))
406         (status (portal-read-json-file portal "status"))
407         (stdout (if process
408                     (portal-last-n-lines
409                      5
410                      (process-get process :buffer))
411                   (portal-tail-file portal 5 "stdout")))
412         (stderr (if process
413                     (portal-last-n-lines
414                      5
415                      (process-get (process-get process :stderr-process) :buffer))
416                   (portal-tail-file portal 5 "stderr")))
417         (started-time
418          (file-attribute-modification-time (file-attributes (portal-file-name portal "command"))))
419         (exited-time
420          (file-attribute-modification-time (file-attributes (portal-file-name portal "status")))))
421    (with-temp-buffer
422      (insert (propertize
423               (concat "# (" (if (string= status "run") "🌀" status) ") " (portal-as-shell-command command))
424               'face
425               (if (string= status "run")
426                   'portal-meta-face
427                 (if (string= status "0")
428                     'portal-exit-success-face
429                   'portal-exit-failure-face))))
430      (insert "\n"
431              (concat
432               (propertize (format-time-string "# Started: %Y-%m-%d %T" started-time)
433                           'face 'portal-timestamp-face)
434               (if (string= status "run")
435                   ""
436                 (propertize (concat
437                              (format-time-string ", exited: %Y-%m-%d %T" exited-time)
438                              " => "
439                              (portal-display-time-difference started-time exited-time))
440                             'face 'portal-timestamp-face))))
441      ;; Only show if it's different to the current directory,
442      ;; otherwise it's noise.
443      (unless (string= default-directory directory) (insert "\n# " directory))
444      (unless (= 0 (length (string-trim stdout)))
445        (insert "\n"
446                (propertize (portal-clean-output stdout)
447                            'face (if (string= status "run")
448                                      'portal-stdout-face
449                                    'portal-exited-stdout-face))))
450      (unless (= 0 (length (string-trim stderr)))
451        (insert "\n"
452                (propertize (portal-clean-output stderr)
453                            'face
454                            (if (string= status "run")
455                                'portal-stderr-face
456                              'portal-exited-stderr-face))))
457      (propertize (buffer-string)
458                  'portal portal))))
459
460(defun portal-display-time-difference (start-time end-time)
461  "Display the time difference between START-TIME and END-TIME in human-readable format.
462START-TIME and END-TIME should be Emacs Lisp time values as returned by `current-time'.
463The function will display the time in the most appropriate unit (from ns to days)."
464  (let* ((diff (float-time (time-subtract end-time start-time))))
465    (apply #'format
466           (cons "%.3f %s"
467                 (cond
468                  ((< diff 1e-6)
469                   (list (* diff 1e9) "ns"))
470                  ((< diff 1e-3)
471                   (list (* diff 1e6) "us"))
472                  ((< diff 1)
473                   (list (* diff 1e3) "ms"))
474                  ((< diff 60)
475                   (list diff "s"))
476                  ((< diff 3600)
477                   (list (/ diff 60) "mins"))
478                  ((< diff 86400)
479                   (list (/ diff 3600) "hours"))
480                  (t
481                   (list (/ diff 86400) "days")))))))
482
483;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
484;; String generation
485
486(defun portal-as-shell-command (command)
487  "If the vector COMMAND is a shell run, strip the prefix, else return the whole thing joined."
488  (if (and (= 3 (length command))
489           (string= (elt command 0) shell-file-name)
490           (string= (elt command 1) shell-command-switch))
491      (elt command 2)
492    (mapconcat 'shell-quote-argument command " ")))
493
494(defun portal-clean-output (output)
495  "Clean output for previewing, prefixed with #."
496  (portal-limit-lines-to-80-columns
497   (concat "# " (replace-regexp-in-string
498                 "\n" "\n# "
499                 (portal-no-empty-lines output)))))
500
501(defun portal-limit-lines-to-80-columns (string)
502  "Limit all lines in STRING to 80 columns."
503  (with-temp-buffer
504    (insert string)
505    (goto-char (point-min))
506    (while (not (eobp))
507      (move-to-column 80 t)
508      (delete-region (point) (line-end-position))
509      (forward-line))
510    (buffer-string)))
511
512(defun portal-process-name (portal)
513  (concat portal "-main-process"))
514
515(defun portal-stderr-process-name (portal)
516  (concat portal "-stderr-pipe"))
517
518(defun portal-file-name (portal name)
519  (concat (file-name-as-directory (portal-ensure-directory portal)) name))
520
521(defun portal-no-empty-lines (string)
522  "Drop empty lines from a string."
523  (replace-regexp-in-string
524   ;; Drop ANSI codes from terminal output
525   ;; <https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream>
526   "\\(\x1B\\[[0-9;]*[A-Za-z]\\|[\x00-\x09\x0B-\x1F\x7F]\\|\n$\\|^\n\\)"
527   ""
528   string))
529
530(defun portal-last-n-lines (n string)
531  "Take last N lines from STRING."
532  (mapconcat #'identity (reverse (seq-take (reverse (split-string string "[\r\n]+" t)) n)) "\n"))
533
534(defun portal-tail-n-lines (n file-path)
535  "Tail the last N lines from FILE-PATH using tail, if possible. If
536not possible (due to lack of such tool), return nil."
537  (let ((this-buffer (current-buffer)))
538    (with-temp-buffer
539      (let ((out-buffer (current-buffer)))
540        (with-current-buffer this-buffer
541          (cl-case (call-process "tail" nil out-buffer nil "-n" (format "%d" n)
542                                 (expand-file-name file-path))
543            (0 (with-current-buffer out-buffer (buffer-string)))
544            (t "")))))))
545
546;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
547;; Finding portals and gathering information for them
548
549(defconst portal-regexp "\\<portal_[A-Za-z0-9]\\{21\\}\\>"
550  "Match on a portal's unique ID.")
551
552(defun portal-at-point ()
553  "Return the portal at point."
554  (or (save-excursion
555        (goto-char (line-beginning-position))
556        (when (looking-at portal-regexp)
557          (buffer-substring (match-beginning 0) (match-end 0))))
558      (get-text-property (point) 'portal)
559      (error "Not at a portal.")))
560
561(defun portal-jump-to-portal ()
562  "If there's a portal at point or a summary of a portal at point,
563jump to the portal at the beginning of the line upwards within
564the same paragraph."
565  (let ((portal (portal-at-point)))
566    (goto-char
567     (save-excursion
568       (goto-char (line-end-position))
569       (re-search-backward
570        (concat "^" (regexp-quote portal))
571        (save-excursion (forward-paragraph -1))
572        nil
573        1)))))
574
575;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
576;; Notes
577
578;; Use this on a portals buffer to stop it constantly being saved:
579;
580;; (setq buffer-save-without-query t)
581
582;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
583;; Major mode
584
585(defvar-keymap portal-mode-map
586  "M-!" 'portal-dwim-execute
587  "C-c C-c" 'portal-interrupt
588  "RET" 'portal-jump-to-thing-at-point
589  "M-p" 'portal-rerun
590  )
591
592(define-derived-mode portal-mode
593  fundamental-mode "Portals"
594  "Major mode for portals."
595  (setq buffer-save-without-query t)
596  (portal-alpha-minor-mode))
597
598(defun portal-insert-command (command)
599  "Launch an asynchronous proc of COMMAND, make a portal associated
600with the current buffer and insert the portal into the current
601buffer."
602  (let* ((portal (portal-generate-nanoid)))
603    (portal-start
604     (current-buffer)
605     portal
606     (portal-file-name portal "stdout")
607     (portal-file-name portal "stderr")
608     (car command)
609     (cdr command))
610    (insert portal)))
611
612(defun portal-dwim-execute ()
613  (interactive)
614  (call-interactively
615   (if (condition-case nil
616           (portal-at-point)
617         (error nil))
618       'portal-edit
619     'portal-shell-command)))
620
621(defun portal-shell-command (command)
622  "Run a shell command and insert it at point."
623  (interactive "sCommand: ")
624  (portal-insert-command
625   (list shell-file-name shell-command-switch command)))
626
627(defun portal-jump-to-thing-at-point ()
628  "Jump to the thing at point, i.e. an stdout/stderr output jumps to
629the file."
630  (interactive)
631  (let ((face (get-text-property (point) 'face)))
632    (cond
633     ((eq face 'portal-stderr-face)
634      (portal-open-stderr))
635     ((eq face 'portal-exited-stderr-face)
636      (portal-open-stderr))
637     ((eq face 'portal-stdout-face)
638      (portal-open-stdout))
639     ((eq face 'portal-exited-stdout-face)
640      (portal-open-stdout))
641     (t (call-interactively 'newline)))))
642
643(provide 'portal)
644