;;; ob-tmux.el --- Babel Support for Interactive Terminal -*- lexical-binding: t; -*- ;; Copyright (C) 2009-2017 Free Software Foundation, Inc. ;; Copyright (C) 2017 Allard Hendriksen ;; Author: Benjamin Andresen ;; Keywords: literate programming, interactive shell ;; Homepage: http://orgmode.org ;; This file is NOT part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; Org-Babel support for interactive terminals. Mostly shell scripts. ;; Heavily inspired by 'eev' from Eduardo Ochs ;; ;; Adding :terminal as header arguments ;; :terminal must support the -T (title) and -e (command) parameter ;; or allow for the command to be added after '--' ;; ;; You can test the default setup. (gnome-terminal) with ;; M-x org-babel-tmux-test RET ;;; Code: (require 'ob) (require 'seq) (defvar org-babel-tmux-location "tmux" "The command location for tmux. In case you want to use a different tmux than one selected by your $PATH") (defvar org-babel-tmux-session-prefix "org-babel-session-" "The string that will be prefixed to tmux sessions started by ob-tmux") (defvar org-babel-tmux-default-window-name "ob1" "The default tmux window name used for windows that are not explicitly named in an org session.") (defvar org-babel-default-header-args:tmux '((:results . "silent") (:session . "default") (:terminal . "gnome-terminal")) "Default arguments to use when running tmux source blocks.") (add-to-list 'org-src-lang-modes '("tmux" . sh)) (defun org-babel-execute:tmux (body params) "Send a block of code via tmux to a terminal using Babel. \"default\" session is used when none is specified." (message "Sending source code block to interactive terminal session...") (save-window-excursion (let* ((session (cdr (assq :session params))) (session-alive (org-babel-tmux-session-alive-p session)) (window-alive (org-babel-tmux-window-alive-p session))) ;; Prepare session unless both the tmux session and window exist. (unless (and session-alive window-alive) (org-babel-prep-session:tmux session params)) ;; Disable window renaming from within tmux (org-babel-tmux-disable-renaming session) (org-babel-tmux-session-execute-string session (org-babel-expand-body:generic body params))))) (defun org-babel-prep-session:tmux (_session params) "Prepare SESSION according to the header arguments specified in PARAMS. Starts a terminal window if the tmux session does not yet exist. No terminal window is started, if the only tmux window must be created." (let* ((session (cdr (assq :session params))) (terminal (cdr (assq :terminal params))) (process-name (concat "org-babel: terminal (" session ")")) (session-alive (org-babel-tmux-session-alive-p session)) (window-alive (org-babel-tmux-window-alive-p session))) ;; First create tmux session and windows (unless session-alive (org-babel-tmux-create-session session)) (unless window-alive (org-babel-tmux-create-window session)) (unless session-alive (start-process process-name "*Messages*" terminal "--" org-babel-tmux-location "attach-session" "-t" (org-babel-tmux-target-session session))) ;; XXX: Is there a better way than the following? ;; wait until tmux session is available before returning (while (not (org-babel-tmux-session-alive-p session))))) ;; helper functions (defun org-babel-tmux-execute (&rest args) "Executes a tmux command with arguments as given." (apply 'start-process "ob-tmux" "*Messages*" org-babel-tmux-location args)) (defun org-babel-tmux-execute-string (&rest args) "Executes a tmux command with arguments as given. Returns stdout as a string." (shell-command-to-string (concat org-babel-tmux-location " " (s-join " " args)))) (defun org-babel-tmux-start-terminal-window (session terminal) "Starts a terminal window with tmux attached to session." (let* ((process-name (concat "org-babel: terminal (" session ")"))) (if (string-equal terminal "xterm") (start-process process-name "*Messages*" terminal "-T" (org-babel-tmux-target-session session) "-e" org-babel-tmux-location "attach-session" "-t" (org-babel-tmux-target-session session)) (start-process process-name "*Messages*" terminal "--" org-babel-tmux-location "attach-session" "-t" (org-babel-tmux-target-session session))))) (defun org-babel-tmux-create-session (session) "Creates a tmux session if it does not yet exist." (unless (org-babel-tmux-session-alive-p session) (org-babel-tmux-execute "new-session" "-d" ;; just create the session, don't attach. "-c" (expand-file-name "~") ;; start in home directory "-s" (org-babel-tmux-session session) "-n" (org-babel-tmux-window-default session)))) (defun org-babel-tmux-create-window (session) "Creates a tmux window in session if it does not yet exist." (unless (org-babel-tmux-window-alive-p session) (org-babel-tmux-execute "new-window" "-c" (expand-file-name "~") ;; start in home directory "-n" (org-babel-tmux-window-default session) "-t" (org-babel-tmux-session session)))) (defun org-babel-tmux-set-window-option (session option value) "If SESSION exists, set option for window." (let ((alive (org-babel-tmux-session-alive-p session))) (when alive (org-babel-tmux-execute "set-window-option" "-t" (org-babel-tmux-target-session session) option value)))) (defun org-babel-tmux-disable-renaming (session) "Disable renaming features for tmux window. Disabling renaming improves the chances that ob-tmux will be able to find the window again later." (progn (org-babel-tmux-set-window-option session "allow-rename" "off") (org-babel-tmux-set-window-option session "automatic-rename" "off"))) (defun org-babel-tmux-send-keys (session line) "If SESSION exists, send a line of text to it." (let ((alive (org-babel-tmux-session-alive-p session))) (when alive (org-babel-tmux-execute "send-keys" "-l" "-t" (org-babel-tmux-target-session session) line "\n")))) (defun org-babel-tmux-session-execute-string (session body) "If SESSION exists, send BODY to it." (let ((alive (org-babel-tmux-session-alive-p session))) (when alive (let ((lines (split-string body "[\n\r]+"))) (mapc (lambda (l) (org-babel-tmux-send-keys session l)) lines))))) (defun org-babel-tmux-session (org-session) "Extracts the tmux session from the org session string." (let* ((session (car (split-string org-session ":")))) (concat org-babel-tmux-session-prefix (if (string-empty-p session) "default" session)))) (defun org-babel-tmux-window (org-session) "Extracts the tmux window from the org session string. Can return nil if no window specified." (let* ((window (cadr (split-string org-session ":")))) (if (string-empty-p window) nil window))) (defun org-babel-tmux-window-default (org-session) "Extracts the tmux window from the org session string. Returns '1' if no window specified." (let* ((tmux-window (cadr (split-string org-session ":")))) (if tmux-window tmux-window org-babel-tmux-default-window-name))) (defun org-babel-tmux-target-session (org-session) "Constructs a tmux target from the org session string. If no window is specified, use first window." (let* ((target-session (org-babel-tmux-session org-session)) (window (org-babel-tmux-window org-session)) (target-window (if window (concat "=" window) "^"))) (concat target-session ":" target-window))) (defun org-babel-tmux-session-alive-p (org-session) "Check if SESSION exists by parsing output of \"tmux ls\"." (let* ((tmux-ls (org-babel-tmux-execute-string "ls -F '#S'")) (tmux-session (org-babel-tmux-session org-session))) (car (seq-filter (lambda (x) (string-equal tmux-session x)) (split-string tmux-ls "\n"))))) (defun org-babel-tmux-window-alive-p (org-session) "Check if WINDOW exists in tmux session. If no window is specified in org-session, returns 't." (let* ((tmux-window (org-babel-tmux-window org-session)) (tmux-target (org-babel-tmux-target-session org-session)) (tmux-lws (org-babel-tmux-execute-string "list-panes" "-F 'yes_exists'" "-t" (concat "'" tmux-target "'")))) (if tmux-window (string-equal "yes_exists\n" tmux-lws) 't))) (defun org-babel-tmux-open-file (path) (with-temp-buffer (insert-file-contents-literally path) (buffer-substring (point-min) (point-max)))) (defun org-babel-tmux-test () "Test if the default setup works. The terminal should shortly flicker." (interactive) (let* ((random-string (format "%s" (random 99999))) (tmpfile (org-babel-temp-file "ob-screen-test-")) (body (concat "echo '" random-string "' > " tmpfile " ; exit")) tmp-string) (org-babel-execute:tmux body org-babel-default-header-args:tmux) ;; XXX: need to find a better way to do the following (while (or (not (file-readable-p tmpfile)) (= 0 (length (org-babel-tmux-open-file tmpfile)))) ;; do something, otherwise this will be optimized away (format "org-babel-screen: File not readable yet.")) (setq tmp-string (org-babel-tmux-open-file tmpfile)) (delete-file tmpfile) (message (concat "org-babel-screen: Setup " (if (string-match random-string tmp-string) "WORKS." "DOESN'T work."))))) (provide 'ob-tmux) ;;; ob-tmux.el ends here