├── docs ├── requirements.txt └── source │ ├── conf.py │ ├── index.rst │ ├── contrib.rst │ ├── faq.rst │ ├── shells.rst │ └── extensions.rst ├── screengrab.gif ├── .gitmodules ├── .gitignore ├── .readthedocs.yaml ├── .github └── workflows │ ├── CI.yml │ └── doc.yml ├── test ├── mistty-osc-colors-test.el ├── mistty-project-test.el ├── mistty-log-test.el ├── mistty-osc7-test.el ├── mistty-tramp-test.el ├── mistty-term-test.el ├── mistty-changeset-test.el ├── mistty-compat-test.el └── mistty-accum-test.el ├── mistty-osc-colors.el ├── mistty-project.el ├── mistty-launch.el ├── extras └── mistty-reverse-input-decode-map.el ├── mistty-osc7.el ├── mistty-undo.el ├── Eldev ├── README.md ├── mistty-log.el ├── mistty-accum-macros.el ├── mistty-changeset.el └── mistty-util.el /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.5 2 | sphinx-rtd-theme==1.3.0 3 | -------------------------------------------------------------------------------- /screengrab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szermatt/mistty/HEAD/screengrab.gif -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/turtles"] 2 | path = deps/turtles 3 | url = https://github.com/szermatt/turtles.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eldev 2 | /Eldev-local 3 | /docs/build 4 | /dist 5 | /dir 6 | /venv 7 | mistty.info 8 | .cask 9 | *-autoloads.el* 10 | *.elc 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.10" 7 | 8 | python: 9 | install: 10 | - requirements: docs/requirements.txt 11 | 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | - 'docs/**' 8 | pull_request: 9 | paths-ignore: 10 | - '**.md' 11 | - 'docs/**' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | emacs_version: 19 | - 29.1 20 | - 29.4 21 | - 30.1 22 | 23 | steps: 24 | - name: Install Nix and set up Emacs 25 | uses: jcs090218/setup-emacs@master 26 | with: 27 | version: ${{matrix.emacs_version}} 28 | 29 | - name: Install Test dependencies 30 | run: | 31 | nix profile install nixpkgs/8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7#zsh 32 | nix profile install nixpkgs/8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7#fish 33 | nix profile install nixpkgs/8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7#vim 34 | nix profile install nixpkgs/566e53c2ad750c84f6d31f9ccb9d00f823165550#python311Packages.ipython 35 | 36 | - name: Install Eldev 37 | uses: emacs-eldev/setup-eldev@v1 38 | 39 | - name: Check out the source code 40 | uses: actions/checkout@v4 41 | 42 | - name: Test the project 43 | run: 'eldev -p -dtT test --ci || eldev -p -dtT test --ci :failed' 44 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: doc 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docs/**' 7 | - '.github/workflows/doc.yml' 8 | 9 | jobs: 10 | doc: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Install Nix and set up Emacs 15 | uses: jcs090218/setup-emacs@master 16 | with: 17 | version: 29.1 18 | 19 | - name: Install Eldev 20 | uses: emacs-eldev/setup-eldev@v1 21 | 22 | - name: Check out the source code 23 | uses: actions/checkout@v4 24 | with: 25 | submodules: true 26 | 27 | - name: Install Sphinx 28 | run: | 29 | nix profile install nixpkgs/b833ff01a0d694b910daca6e2ff4a3f26dee478c#texinfo 30 | pip3 install -r docs/requirements.txt 31 | 32 | - name: Re-build mistty.texi 33 | run: 'eldev build --force mistty.texi' 34 | 35 | - name: Update mistty.texi 36 | # Only run on main branch push (e.g. after pull request merge) in one matrix version. 37 | run: | 38 | if git diff -s --exit-code mistty.texi; then 39 | echo "mistty.texi not modified by this commit" 40 | else 41 | git config user.name github-actions 42 | git config user.email github-actions@github.com 43 | git add mistty.texi 44 | git commit -m "Auto-update mistty.texi" --author "${{ env.CI_COMMIT_AUTHOR }}" 45 | git push 46 | fi 47 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Project information 4 | 5 | project = 'MisTTY' 6 | copyright = '2023-2025, Stephane Zermatten' 7 | author = 'Stephane Zermatten' 8 | 9 | release = '1.5.1snapshot' 10 | version = '1.5.1snapshot' 11 | 12 | # -- General configuration 13 | 14 | extensions = [ 15 | 'sphinx.ext.duration', 16 | 'sphinx.ext.doctest', 17 | 'sphinx.ext.autodoc', 18 | 'sphinx.ext.autosummary', 19 | 'sphinx.ext.intersphinx', 20 | ] 21 | 22 | root_doc = "index" 23 | 24 | intersphinx_mapping = { 25 | 'python': ('https://docs.python.org/3/', None), 26 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 27 | } 28 | intersphinx_disabled_domains = ['std'] 29 | 30 | templates_path = ['_templates'] 31 | 32 | # -- Options for HTML output 33 | 34 | html_theme = 'sphinx_rtd_theme' 35 | 36 | # -- Options for EPUB output 37 | epub_show_urls = 'footnote' 38 | 39 | # -- Options for Texinfo output 40 | 41 | texinfo_documents = [ 42 | ( 43 | # startdocname 44 | root_doc, 45 | # targetname 46 | "mistty", 47 | # title 48 | "MisTTY", 49 | # author 50 | "Stephane Zermatten", 51 | # dir_entry 52 | "MisTTY", 53 | # description 54 | "Shell/comint alternative with a fully-functional terminal", 55 | # category 56 | "Emacs", 57 | # toctree_only 58 | False, 59 | ) 60 | ] 61 | -------------------------------------------------------------------------------- /test/mistty-osc-colors-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-osc-colors -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'ert) 18 | (require 'ert-x) 19 | (require 'tramp) 20 | 21 | (require 'mistty) 22 | (require 'mistty-osc-colors) 23 | 24 | (require 'mistty-testing) 25 | (require 'turtles) 26 | 27 | (turtles-ert-deftest mistty-test-osc-10-11 (:instance 'mistty) 28 | (mistty-with-test-buffer (:selected t) 29 | (set-face-foreground 'default "#ffff00") 30 | (set-face-background 'default "#0000ff") 31 | 32 | (mistty--send-string mistty-proc "read -rs -d \\\\ -p $'\\e]10;?\\e\\\\' fg; ") 33 | (mistty--send-string mistty-proc "read -rs -d \\\\ -p $'\\e]11;?\\e\\\\' bg; ") 34 | (mistty-send-and-wait-for-prompt) 35 | 36 | (mistty-send-text "echo $fg | strings") 37 | (should (equal "]10;rgb:ffff/ffff/0000" 38 | (mistty-send-and-capture-command-output))) 39 | 40 | (mistty-send-text "echo $bg | strings") 41 | (should (equal "]11;rgb:0000/0000/ffff" 42 | (mistty-send-and-capture-command-output))))) 43 | -------------------------------------------------------------------------------- /mistty-osc-colors.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-osc-colors.el --- Add support for OSC 10/11 to term-mode -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file defines a handler for OSC 10 and 11 for querying 20 | ;; foreground and background colors. 21 | ;; 22 | 23 | (require 'faces) 24 | 25 | ;;; Code: 26 | 27 | (defun mistty-osc-query-color (code seq) 28 | "Handle OSC 10 and 11 to query terminal colors. 29 | 30 | CODE should be either 10 or 11 and SEQ must be ?. Any other value or 31 | combination is ignored. 32 | 33 | This function returns the foreground and background color configured for 34 | the face \\='term." 35 | (when (string= seq "?") 36 | (let* ((color (cond 37 | ((string= "10" code) 38 | (color-values (face-attribute 'term :foreground nil t))) 39 | ((string= "11" code) 40 | (color-values (face-attribute 'term :background nil t)))))) 41 | (when color 42 | (process-send-string (get-buffer-process (current-buffer)) 43 | (format "\e]%s;rgb:%04x/%04x/%04x\e\\" 44 | code (nth 0 color) (nth 1 color) (nth 2 color))))))) 45 | 46 | (provide 'mistty-osc-colors) 47 | 48 | ;;; mistty-osc-colors.el ends here 49 | -------------------------------------------------------------------------------- /test/mistty-project-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-project.el -*- lexical-binding: t -*- 2 | 3 | (require 'mistty) 4 | (require 'mistty-project) 5 | (require 'mistty-testing) 6 | 7 | (require 'project) 8 | 9 | ;; defines a project type 'fake 10 | (cl-defmethod project-name ((_project (head fake))) 11 | "fake") 12 | 13 | (cl-defmethod project-root ((project (head fake))) 14 | (nth 1 project)) 15 | 16 | (cl-defmethod project-buffers ((project (head fake))) 17 | (cdr (cdr project))) 18 | 19 | (ert-deftest mistty-project-test-mistty-in-project () 20 | (ert-with-temp-directory project-dir 21 | (let* ((mistty-shell-command mistty-test-bash-exe) 22 | (default-directory project-dir) 23 | (project-current-directory-override project-dir) 24 | (fake-project (list 'fake project-dir)) 25 | (project-find-functions (list (lambda (dir) 26 | (when (string= dir project-dir) 27 | fake-project)))) 28 | buf buf-not-in-project) 29 | (unwind-protect 30 | (progn 31 | (should (equal fake-project (project-current))) 32 | ;; mistty-in-project should ignore this buffer 33 | (setq buf-not-in-project (mistty-create)) 34 | 35 | ;; create new with a specific name 36 | (setq buf (mistty-in-project)) 37 | (should (equal "*fake-mistty*" (buffer-name buf))) 38 | (setcdr (cdr fake-project) (list buf)) 39 | (should (equal (list buf) (project-buffers fake-project))) 40 | 41 | ;; choose existing 42 | (switch-to-buffer "*scratch*") 43 | (should (equal buf (mistty-in-project)))) 44 | 45 | ;; kill projects 46 | (let ((kill-buffer-query-functions nil)) 47 | (when buf 48 | (kill-buffer buf)) 49 | (when buf-not-in-project 50 | (kill-buffer buf-not-in-project))))))) 51 | 52 | (ert-deftest mistty-project-test-kill-buffer-condition () 53 | (let ((project-kill-buffer-conditions 54 | '(buffer-file-name 55 | (and 56 | (major-mode . fundamental-mode) 57 | "\\`[^ ]") 58 | (and (derived-mode . special-mode) 59 | (not (major-mode . help-mode)) 60 | (not (derived-mode . gnus-mode))) 61 | (derived-mode . compilation-mode) 62 | (derived-mode . dired-mode) 63 | (derived-mode . diff-mode) 64 | (derived-mode . comint-mode) 65 | (derived-mode . eshell-mode) 66 | (derived-mode . change-log-mode)))) 67 | (mistty-project-init-kill-buffer) 68 | (should (equal '(buffer-file-name 69 | (and 70 | (major-mode . fundamental-mode) 71 | "\\`[^ ]") 72 | (and (derived-mode . special-mode) 73 | (not (major-mode . help-mode)) 74 | (not (derived-mode . gnus-mode))) 75 | (derived-mode . compilation-mode) 76 | (derived-mode . dired-mode) 77 | (derived-mode . diff-mode) 78 | (derived-mode . comint-mode) 79 | ;; added: 80 | (derived-mode . mistty-mode) 81 | (derived-mode . eshell-mode) 82 | (derived-mode . change-log-mode)) 83 | project-kill-buffer-conditions)))) 84 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | MisTTY 2 | ====== 3 | 4 | **MisTTY** is a major mode for :program:`Emacs` 29.1 and up that runs 5 | a shell inside of a buffer, similarly to comint mode. It is built on 6 | top of :file:`term.el`. Check out its project page at 7 | https://github.com/szermatt/mistty. 8 | 9 | :kbd:`M-x mistty` creates a buffer with an interactive shell. (:ref:`launching`) 10 | 11 | MisTTY feels very much like comint mode: you can move around freely 12 | and run any Emacs command you want - until you press TAB and end up 13 | with the native completion or notice the shell autosuggestions. With 14 | MisTTY you have access to both Emacs and the shell commands and 15 | editing tools. 16 | 17 | Additionally, commands that take over the entire screen 18 | (:ref:`fullscreen`) such as :command:`less` or :command:`vi` also 19 | work, temporarily taking over the window, while scrollback remains 20 | available in another buffer. 21 | 22 | .. only:: builder_html 23 | 24 | MisTTY works well with Bash and ZSH, but it is especially well 25 | suited to running `Fish `_: you get 26 | autosuggestions, completion in full colors. Here's what the end 27 | result might look like: 28 | 29 | .. image:: ../../screengrab.gif 30 | :width: 600 31 | :alt: Screen grab showing MisTTY in action 32 | 33 | MisTTY is known to work on Linux and MacOS. It also supports non-shell 34 | command-line programs, such as :program:`python`. 35 | 36 | The latest version of this documentation is available at 37 | https://mistty.readthedocs.io/en/latest/. Once MisTTY is installed, 38 | this documentation can be accessed from inside Emacs using :kbd:`M-x 39 | info g mistty` 40 | 41 | .. note:: 42 | 43 | If you encounter issues, please take the time to file a bug. (:ref:`reporting`) 44 | 45 | Comparison with other packages 46 | ------------------------------ 47 | 48 | MisTTY isn't a terminal emulator, but rather a frontend to an existing 49 | terminal emulator, the built-in :file:`term.el`. Its goal is to make 50 | it more convenient to use while inside of Emacs and better integrate 51 | with Emacs itself. In theory, other terminal emulators than 52 | :file:`term.el` might be used as engine for MisTTY, such as `vterm 53 | `_ and `eat 54 | `_. 55 | 56 | MisTTY has some similarities with `coterm 57 | `_; it offers the same switch 58 | between full-screen and line mode. 59 | 60 | :program:`Coterm`, :program:`term.el` and :program:`eat` all have a 61 | line mode, just like :program:`comint` does, which allows you to edit 62 | a command line as a whole before sending it to the shell. While in 63 | line mode, rendering is done by Emacs and editing commands are Emacs 64 | commands. In constrast, with MisTTY, all rendering is done by the 65 | shell through the terminal. This is why native shell completion and 66 | autosuggestion is available with MisTTY and not in line modes and why 67 | you can freely mix shell commands with Emacs commands while editing 68 | the command line. 69 | 70 | :program:`term.el` and :program:`eat` also have a char mode, where 71 | rendering and command execution is handled by the shell, and editing 72 | with Emacs isn't available. The difference with MisTTY is then that 73 | MisTTY makes Emacs editing commands available when possible. 74 | 75 | :program:`eat` also has a semi-char mode, which is the closest there 76 | is to MisTTY. In that mode, Emacs movements commands are available. 77 | However, Emacs commands that modify the buffer, aren't available to 78 | edit the command line. In contrast, MisTTY allows Emacs to navigate to 79 | and edit the whole buffer, then replays changes made to the 80 | command-line. 81 | 82 | Contents 83 | -------- 84 | 85 | .. toctree:: 86 | 87 | usage 88 | shells 89 | extensions 90 | faq 91 | contrib 92 | -------------------------------------------------------------------------------- /mistty-project.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-project.el --- Project integration for mistty -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file defines a function `mistty-in-project' for starting a 20 | ;; MisTTY shell buffer in a project directory, tied to that project. 21 | ;; 22 | ;; For full integration, it is recommended to run 23 | ;; `mistty-project-init-kill-buffer' some time during initialization 24 | ;; so MisTTY buffers created by `mistty-in-project' can be killed just 25 | ;; like comint buffers are. See that function docstring. 26 | 27 | (require 'project) 28 | (require 'mistty) 29 | 30 | ;;; Code: 31 | 32 | ;;;###autoload 33 | (defun mistty-in-project (&optional other-window) 34 | "Start or go to a MisTTY buffer in the project's root directory. 35 | 36 | If a MisTTY buffer already exists for running a shell in the 37 | project's root, switch to it. If we're already on that buffer, 38 | create a new buffer in that project, like `mistty' does. 39 | 40 | Otherwise, create a new MisTTY shell buffer. With 41 | \\[universal-argument] prefix arg, create a new shell buffer even 42 | if one already exists. 43 | 44 | If OTHER-WINDOW is nil, execute the default action configured by 45 | `display-comint-buffer-action' to pop to the existing or newly-created 46 | buffer. If OTHER-WINDOW is a function, it is passed to `pop-to-buffer` 47 | to be used as a `display-buffer' action. Otherwise, display the buffer 48 | in another window. 49 | 50 | You might prefer configuring `display-buffer-alist' for 51 | comint category buffers to get the exact behavior you want 52 | instead of passing OTHER-WINDOW." 53 | (interactive) 54 | (let* ((pr (project-current t)) 55 | (bufs (project-buffers pr))) 56 | (mistty-cycle-or-create 57 | (lambda (buf) (memq buf bufs)) 58 | (lambda (other-window) 59 | (let ((default-directory (project-root pr)) 60 | (mistty-buffer-name 61 | (cons (concat (project-name pr) "-") mistty-buffer-name))) 62 | (mistty-create nil other-window))) 63 | other-window))) 64 | 65 | (defun mistty-in-project-other-window () 66 | "Start or go to a MisTTY buffer in the project's root in another window. 67 | 68 | You might prefer configuring `display-buffer-alist' for 69 | comint category buffers and calling `mistty-in-project' 70 | directly to get the exact behavior you want instead of using 71 | `mistty-in-project-other-window'. 72 | 73 | See the documentation of the function `mistty-other-window' and 74 | `mistty-in-project' for details." 75 | (interactive) 76 | (mistty-in-project 'other-window)) 77 | 78 | (defun mistty-project-init-kill-buffer () 79 | "Have `project-kill-buffers' treat MisTTY buffers as comint. 80 | 81 | If `project-kill-buffer-conditions' is configured to kill 82 | `comint-mode' buffers, modify the configuratin to kill 83 | `mistty-mode' buffers as well, as they're meant to be used the 84 | same way. 85 | 86 | Usage example: 87 | 88 | (use-package mistty-project 89 | :config 90 | (mistty-project-init-kill-buffer))" 91 | (let ((comint-cond 92 | (member '(derived-mode . comint-mode) 93 | project-kill-buffer-conditions))) 94 | (setcdr comint-cond 95 | (cons '(derived-mode . mistty-mode) 96 | (cdr comint-cond))))) 97 | 98 | (provide 'mistty-project) 99 | 100 | ;;; mistty-project.el ends here 101 | -------------------------------------------------------------------------------- /docs/source/contrib.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | .. _reporting: 5 | 6 | Reporting issues 7 | ---------------- 8 | 9 | At this time, the most useful thing you can do to help is and useful 10 | bug reports to the `Issue Tracker`_ 11 | 12 | In your report, please discuss what you wanted to happen as well as 13 | what happened. Also, please include enough information to reproduce 14 | the issue. Such as: 15 | 16 | - the name and version of the program you were running - usually a shell 17 | - the version of Emacs you're running, taken, for example, from :kbd:`M-x about-emacs` 18 | - whether you're running Emacs in a window environment or a terminal 19 | - what kind of prompt you're using, that is, what it looks like, what 20 | character it ends with, how many lines it has and whether you're 21 | using any kind of right prompt 22 | 23 | .. index:: 24 | pair: command; mistty-start-log 25 | pair: command; mistty-stop-log 26 | 27 | If you can reproduce reliably, please include the content of the 28 | buffer :code:`*mistty-log*` into your report, as follows: 29 | 30 | - Enable logging by calling :kbd:`M-x mistty-start-log` 31 | - Reproduce the issue 32 | - Go to the buffer :code:`*mistty-log*` 33 | - Call :kbd:`M-x mistty-stop-log` to avoid getting more log entries 34 | - Copy the buffer content and paste it into the issue. The log 35 | includes everything that you write to the terminal and everything 36 | that you get back from the terminal. Please make sure you're not 37 | including any private information, such as password - remove them if 38 | necessary... 39 | 40 | If you cannot reproduce reliably, 41 | 42 | - go to :kbd:`M-x customize-option mistty-backlog-size` to set the 43 | backlog size to a large value, such as 50 44 | - use MisTTY normally, until the issue comes back 45 | - once it has happened again, immediately call :kbd:`M-x 46 | mistty-start-log`. The log will then contain entries for events that 47 | happened just *before* you called the command. 48 | - call :kbd:`M-x mistty-stop-log` 49 | - copy the content of the :code:`*mistty-log*` buffer, strip out 50 | anything private, and include it into the issue. 51 | 52 | .. _Issue tracker: https://github.com/szermatt/mistty/issues 53 | 54 | .. _discussion: https://github.com/szermatt/mistty/discussions 55 | 56 | Suggesting features 57 | ------------------- 58 | 59 | Please create a new `discussion`_ in the Ideas category or add a 60 | feature suggestions to the `Issue Tracker`_. 61 | 62 | Asking questions 63 | ---------------- 64 | 65 | Start a new `discussion`_ with your question in the General category. 66 | 67 | Code contributions 68 | ------------------ 69 | 70 | To contribute code to the project, open a `Pull Request`_. 71 | 72 | Before you do that, please make sure the any new features is covered 73 | by tests and that the tests pass. 74 | 75 | To run the tests, install and setup `eldev`_, then run :command:`eldev 76 | test`. 77 | 78 | Tests can also be run from inside of Emacs, using `M-x 79 | ert-run-tests-interactively` but when you do so, be aware that there 80 | might be unexpected interaction with your Emacs configurations. The 81 | tests passing reliably when run using :command:`eldev test` is what 82 | matters. 83 | 84 | Please also make sure your commit message follows `Conventional 85 | Commits 1.0.0 `_, in 86 | short, the commit message of new features should start with "feat: ", 87 | fixes with "fix: ", refactorings with "refactor: " and tests with 88 | "test: ". 89 | 90 | .. _eldev: https://github.com/emacs-eldev/eldev 91 | 92 | Documentation contributions 93 | --------------------------- 94 | 95 | You don't need to be a developer to contribute! Contribution to the 96 | documentation or code comments are very welcome. Please open a `Pull 97 | Request`_ with your proposed modifications. To follow `Conventional 98 | Commits 1.0.0 `_, the 99 | commit message should start with "docs: " 100 | 101 | The documentation is written in reStructuredText. You'll need to 102 | install `Sphinx `_ to build it: 103 | 104 | .. code-block:: bash 105 | 106 | python3 -m venv venv 107 | . venv/bin/activate # or activate.fish on fish 108 | pip3 install -r docs/requirements.txt 109 | 110 | Then run :command:`eldev html` to build the documentation. 111 | 112 | .. _Pull Request: https://github.com/szermatt/mistty/pulls 113 | -------------------------------------------------------------------------------- /mistty-launch.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-launch.el --- Convenience TRAMP-based launcher for MisTTY -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file defines convenience functions for starting mistty buffers 20 | ;; with TRAMP. 21 | 22 | (require 'pcmpl-unix) 23 | 24 | ;;; Code: 25 | 26 | (defvar mistty-ssh-history nil 27 | "History variable for host completion for `mistty-ssh'.") 28 | 29 | (defvar mistty-docker-history nil 30 | "History variable for host completion for `mistty-docker'.") 31 | 32 | (defvar mistty-docker-exe "docker" 33 | "Path to the docker command-line tool. 34 | 35 | This is used by `mistty-docker'") 36 | 37 | ;;;###autoload 38 | (defun mistty-ssh (&optional host command other-window) 39 | "Open SSH running on HOST in a MisTTY buffer. 40 | 41 | This is a convenience function that uses TRAMP to connect to HOST and 42 | executor COMMAND. 43 | 44 | For more control, run \\[mistty-create] with an ; argument. That'll 45 | allow you to setup any TRAMP remote path you'd like without 46 | restrictions. 47 | 48 | See Info Node `(mistty)Remote Shells with TRAMP' for more 49 | details. 50 | 51 | If OTHER-WINDOW is nil, execute the default action configured by 52 | `display-comint-buffer-action'. If OTHER-WINDOW is a function, it is 53 | passed to `pop-to-buffer` to be used as a `display-buffer' action. With 54 | any other non-nil value, display the buffer in another window. 55 | 56 | You might prefer configuring `display-buffer-alist' for 57 | comint category buffers to get the exact behavior you want instead 58 | of passing OTHER-WINDOW." 59 | (interactive 60 | (list 61 | (completing-read "Host: " (pcmpl-ssh-hosts) 62 | nil nil nil 'mistty-ssh-history))) 63 | (let ((default-directory (format "/ssh:%s:~" host))) 64 | (cd default-directory) ;; Connect, ask password 65 | (mistty-create command other-window))) 66 | 67 | ;;;###autoload 68 | (defun mistty-docker (&optional instance command other-window) 69 | "Connect to a docker instance in a MisTTY buffer. 70 | 71 | This connects to docker INSTANCE and executo COMMAND. 72 | 73 | This is a convenience function that uses TRAMP to connect to a host and 74 | open a shell to it. For more control, run \\[mistty-create] with an 75 | argument. That'll allow you to setup any TRAMP remote path you'd like 76 | without restrictions. 77 | 78 | See Info Node `(mistty)Remote Shells with TRAMP' for details. 79 | 80 | Note that docker instances often run older versions of Bash; MisTTY will 81 | work on Bash versions < 5.1, with some limitations. Additionally, for 82 | Bash 4.3 and earlier, you might have to set `mistty-set-EMACS' to 83 | non-nil for directory tracking to work. You can set it per instance as a 84 | connection-local variable. See Info Anchor `(mistty)shells 85 | bash-dirtrack' for details. 86 | 87 | If OTHER-WINDOW is nil, execute the default action configured by 88 | `display-comint-buffer-action'. If OTHER-WINDOW is a function, it is 89 | passed to `pop-to-buffer` to be used as a `display-buffer' action. With 90 | any other non-nil value, display the buffer in another window. 91 | 92 | You might prefer configuring `display-buffer-alist' for comint 93 | category buffers to get the exact behavior you want instead of 94 | passing OTHER-WINDOW." 95 | (interactive 96 | (list 97 | (completing-read 98 | "Instance: " 99 | (string-split 100 | (shell-command-to-string 101 | (concat mistty-docker-exe " ps --format '{{.Names}}'")) 102 | "\n" 'omit-nulls "[[:space:]]+") 103 | nil nil nil 'mistty-docker-history))) 104 | (let ((default-directory (format "/docker:%s:~" instance))) 105 | (cd default-directory) ;; Connect, ask password 106 | (mistty-create command other-window))) 107 | 108 | (provide 'mistty-launch) 109 | 110 | ;;; mistty-launch.el ends here 111 | -------------------------------------------------------------------------------- /extras/mistty-reverse-input-decode-map.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-reverse-input-decode-map.el --- generate keymaps for mistty-term.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | 18 | ;;; Commentary: 19 | ;; 20 | ;; This is a development utility for changing or updating 21 | ;; `mistty-term-key-map', defined in mistty-term.el. 22 | ;; 23 | ;; This is not part of MisTTY's distribution. 24 | 25 | (require 'seq) 26 | (require 'help-fns) 27 | 28 | ;;; Code: 29 | 30 | ;;;###autoload 31 | (defun mistty-reverse-input-decode-map (map) 32 | "Generate elisp code for `mistty-term-key-map' given a MAP. 33 | 34 | This command reverses input-decode maps, such as the ones defined 35 | in elisp/term/ and outputs a definition that's appropriate to use 36 | as `mistty-term-key-map' into a buffer. 37 | 38 | You might find it useful if you'd like MisTTY to generate a set 39 | of keys from a different terminal than xterm." 40 | (interactive 41 | (let ((def (variable-at-point)) 42 | (enable-recursive-minibuffers t) 43 | (orig-buffer (current-buffer)) 44 | accept choice) 45 | (setq accept 46 | (lambda (sym) 47 | (let ((resolved (and (symbolp sym) 48 | (boundp sym) 49 | (or (buffer-local-value sym orig-buffer) 50 | (symbol-value sym))))) 51 | (if (keymapp resolved) resolved)))) 52 | (setq choice 53 | (completing-read 54 | (format-prompt "Reverse map " (funcall accept def)) 55 | #'help--symbol-completion-table 56 | accept 57 | t nil nil 58 | (if (funcall accept def) (symbol-name def)))) 59 | (list (if (equal choice "") 60 | choice (funcall accept (intern choice)))))) 61 | (with-current-buffer (get-buffer-create "*mistty-reverse-map*") 62 | (delete-region (point-min) (point-max)) 63 | (emacs-lisp-mode) 64 | (switch-to-buffer (current-buffer)) 65 | (goto-char (point-min)) 66 | (insert "(let ((map (make-sparse-keymap)))\n") 67 | (let ((start (point)) 68 | (exists-table (make-hash-table :test #'equal))) 69 | (map-keymap 70 | (lambda (e b) 71 | (mistty--reverse-input-decode-map-1 e b [] exists-table)) 72 | map) 73 | (sort-lines nil start (point))) 74 | (insert "\n map)\n") 75 | (goto-char (point-min)))) 76 | 77 | (defun mistty--reverse-input-decode-map-1 (event-type binding prefix exists-table) 78 | "Recursive function to follow BINDINGS and output statements. 79 | 80 | This function goes into a map, recursively, if necessary, until 81 | it gets to a leaf binding, a binding of a key to a string. 82 | 83 | EVENT-TYPE is the key of BINDING, BINDING is the current binding, 84 | which can be a sequence to reverse or a map to enter. PREFIX 85 | tracks the current key binding context when going into maps 86 | recursively. 87 | 88 | EXISTS-TABLE is a table of key bindings that have already been 89 | output. If a decoder map contains more than one control sequence 90 | that corresponds to a key, only the first one is output." 91 | (let ((full-event (vconcat prefix (vector event-type)))) 92 | (cond 93 | ((keymapp binding) 94 | (map-keymap 95 | (lambda (e b) 96 | (mistty--reverse-input-decode-map-1 e b full-event exists-table)) 97 | binding)) 98 | ((functionp binding)) 99 | ((sequencep binding) ; a key seq 100 | (let ((key (key-description binding))) 101 | (unless (gethash key exists-table) 102 | (puthash key t exists-table) 103 | (insert (format 104 | " (define-key map (kbd %S) \"%s\")\n" 105 | key 106 | (seq-mapcat #'mistty--char-string full-event 'string))))))))) 107 | 108 | (defun mistty--char-string (c) 109 | "Format C for inclusion into a nice elisp string." 110 | (cond 111 | ((= c ?\e) "\\e") 112 | ((= c ?\t) "\\t") 113 | ((= c ?\a) "\\a") 114 | ((= c ?\d) "\\d") 115 | ((and (>= c ?\ ) (<= c 126) (make-string 1 c))) 116 | (t (format "\\x%2.2x" c)))) 117 | 118 | (provide 'mistty-reverse-input-decode-map) 119 | 120 | ;;; mistty-reverse-input-decode-map.el ends here 121 | -------------------------------------------------------------------------------- /mistty-osc7.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-osc7.el --- Add support for OSC 7 to term-mode -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file defines a handler for OSC 7 code sequences that can be 20 | ;; registered in `mistty-osc-handlers' to allow shells to report 21 | ;; current directories to Emacs (dirtrack). 22 | ;; 23 | 24 | (require 'url-util) 25 | 26 | ;;; Code: 27 | 28 | (defcustom mistty-allow-tramp-paths t 29 | "If true, allow generating TRAMP paths as shell-specified directory. 30 | 31 | This affects directories set using OSC 7, which can then build 32 | TRAMP remote paths based on the hostname specified in the file:// 33 | URL, using the default method. 34 | 35 | This option doesn't affect hosts configured using 36 | `mistty-host-to-tramp-path-alist'." 37 | 38 | :group 'mistty 39 | :type '(boolean)) 40 | 41 | (defcustom mistty-host-to-tramp-path-alist nil 42 | "Maps hostnames to TRAMP paths. 43 | 44 | This alist maps hosts as self-declared using OSC 7 to the TRAMP 45 | path MisTTY should use for directory tracking. 46 | 47 | This is useful for hosts that require a specific method to 48 | connect to. 49 | 50 | The values should be of the same form as the remote identifiers 51 | returned by `file-remote-p', with method, user and host, for 52 | example \"/ssh:example.com:\". Invalid values are ignored. 53 | 54 | The values can also be nil to disable directory tracking for that 55 | specific host. 56 | 57 | Note that for the ssh method, it's often easier to add the 58 | configuration option you need to a host to ~/.ssh/config." 59 | :group 'mistty 60 | :type '(alist :key-type (string :tag "Host") 61 | :value-type (choice (string :tag "TRAMP Path") 62 | (const nil)))) 63 | 64 | (defun mistty-osc7 (_ osc-seq) 65 | "Store OSC-SEQ as path to the current directory. 66 | 67 | OSC-SEQ must be a file:// URL pointing to a directory to be used 68 | as the current directory for the current shell. 69 | 70 | By default, only local paths are taken into account. To allow 71 | remote paths, configure `mistty-allow-tramp-paths. 72 | 73 | The remote paths that are generated by default use the default 74 | TRAMP method, which can be configured using 75 | `tramp-default-method' and `tramp-default-method-alist'. If that 76 | doesn't work for some hosts, add them to 77 | `mistty-host-to-tramp-path-alist'. 78 | 79 | Such sequences are typically printed out by shells just before 80 | printing a prompt by a command such as: 81 | 82 | printf \"\\e]7;file://%s%s\\e\\\\\" \"$HOSTNAME\" \"$PWD\" 83 | 84 | This can be used as a drop-in replacement for 85 | `ansi-osc-directory-tracker', with the following tweaks: 86 | 87 | - it decodes percent-encoded non-ASCII characters in the paths, 88 | so such paths look better 89 | 90 | - it has optional supports for remote paths." 91 | (when (string-match "file://\\([^/]*\\)\\(/.*\\)" osc-seq) 92 | (let ((hostname (url-unhex-string (match-string 1 osc-seq))) 93 | (path (file-name-as-directory 94 | (decode-coding-string 95 | (url-unhex-string (match-string 2 osc-seq)) 96 | 'utf-8 97 | 'nocopy))) 98 | host-config 99 | host-remote-path) 100 | (when (or (string= hostname "") 101 | (string= hostname "localhost")) 102 | (setq hostname (system-name))) 103 | (cond 104 | ;; Interpret path as being in the same TRAMP connection as 105 | ;; default-directory if the host names match. This is useful 106 | ;; for things like the sudo method, where the hostname is the 107 | ;; same, but accessed differently. 108 | ((equal (file-remote-p default-directory 'host) hostname) 109 | (setq default-directory 110 | (concat (file-remote-p default-directory) path))) 111 | 112 | ;; Interpret path as a straightforward local path. 113 | ((string= hostname (system-name)) 114 | (ignore-errors (cd-absolute path))) 115 | 116 | ;; Host disabled in mistty-host-to-tramp-path-alist. Do 117 | ;; nothing. 118 | ((and (setq host-config 119 | (assoc hostname mistty-host-to-tramp-path-alist)) 120 | (null (cdr host-config)))) 121 | 122 | ;; Using remote path from mistty-host-to-tramp-path-alist, if 123 | ;; it's valid. 124 | ((and (cdr host-config) 125 | (setq host-remote-path (file-remote-p (cdr host-config)))) 126 | (setq default-directory (concat host-remote-path path))) 127 | 128 | ;; Generate default paths for a host, if enabled. 129 | (mistty-allow-tramp-paths 130 | (setq default-directory 131 | (concat (file-remote-p (concat "/-:" hostname ":")) 132 | path))))))) 133 | 134 | (provide 'mistty-osc7) 135 | 136 | ;;; mistty-osc7.el ends here 137 | -------------------------------------------------------------------------------- /mistty-undo.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-undo.el --- mistty undo utilities -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file collects undo-related utilities used by mistty.el. 20 | 21 | ;;; Code: 22 | 23 | (require 'mistty-util) 24 | 25 | (eval-when-compile 26 | (require 'cl-lib)) 27 | 28 | (cl-defstruct (mistty--undo-data 29 | (:constructor mistty--make-undo-data 30 | (start &aux (inserted ""))) 31 | (:conc-name mistty--undo-data-) 32 | (:copier nil)) 33 | ;; the position of the beginning of a chain of undoable keys, never nil 34 | start 35 | 36 | ;; what was inserted so far after start, may be empty but never nil 37 | inserted 38 | 39 | ;; The type of the last key that was sent, if the last command was a 40 | ;; send key command for an undoable key. 41 | ;; 42 | ;; Can be nil, 'self-insert 'delete-char or 'backward-delete-char. 43 | key-type) 44 | 45 | (defvar-local mistty--this-undo-data nil) 46 | 47 | (defvar-local mistty--last-undo-data nil) 48 | 49 | (defmacro mistty--inhibit-undo (&rest body) 50 | "Execute BODY with undo disabled." 51 | (let ((saved (make-symbol "saved-buffer-undo-list"))) 52 | `(let ((,saved buffer-undo-list)) 53 | (setq buffer-undo-list t) 54 | (unwind-protect 55 | (progn ,@body) 56 | (setq buffer-undo-list ,saved))))) 57 | 58 | (defun mistty--pre-command-for-undo () 59 | "Prepare undo data before a command. 60 | 61 | This is meant to be called from the pre-command hook." 62 | (setq mistty--this-undo-data nil)) 63 | 64 | (defun mistty--post-command-for-undo () 65 | "Finish undo data after a command. 66 | 67 | This is meant to be called from the post-command hook." 68 | (setq mistty--last-undo-data mistty--this-undo-data)) 69 | 70 | (defun mistty--maybe-add-key-to-undo (n key cursor) 71 | "Add KEY N times to `buffer-undo-list'. 72 | 73 | A key that's sent directly to the process connected to the 74 | terminal isn't automatically added to the undo list. This 75 | function does that, for `self-inserted keys', `delete-char', and 76 | `backward-delete-char'." 77 | (when-let* ((c (and (length= key 1) (elt key 0))) 78 | (key-type 79 | (cond 80 | ((mistty-self-insert-p key) 81 | 'self-insert) 82 | ((eq ?\x7f c) ;; DEL 83 | 'backward-delete-char) 84 | ((eq ?\C-h c) ;; BS 85 | 'backward-delete-char) 86 | ((eq ?\C-d c) ;; C-d 87 | 'delete-char)))) 88 | (let ((n (or n 1)) 89 | (undo-data (or mistty--last-undo-data 90 | (mistty--make-undo-data cursor))) 91 | pos entry) 92 | (cl-assert (> n 0)) 93 | 94 | (setq mistty--this-undo-data undo-data) 95 | 96 | ;; Amalgamate if the key type is the same as last time. 97 | (when (eq key-type (mistty--undo-data-key-type undo-data)) 98 | (setq buffer-undo-list (cdr buffer-undo-list))) 99 | (setf (mistty--undo-data-key-type undo-data) key-type) 100 | 101 | ;; Use position from undo data, as when typing fast enough, or 102 | ;; on slow connection, cursor might not be up-to-date. 103 | (setq pos (+ (length (mistty--undo-data-inserted undo-data)) 104 | (mistty--undo-data-start undo-data))) 105 | 106 | (pcase key-type 107 | ('self-insert 108 | (setq entry (cons pos (+ pos n))) 109 | (setf (mistty--undo-data-inserted undo-data) 110 | (concat (mistty--undo-data-inserted undo-data) 111 | (make-string n c)))) 112 | 113 | ('delete-char 114 | (setq entry (cons (mistty--safe-bufstring pos (+ pos n)) pos))) 115 | 116 | ('backward-delete-char 117 | ;; Get the text to be deleted first from the inserted data, 118 | ;; then from the buffer at START, as the buffer content 119 | ;; might not have been updated when typing fast enough. 120 | (let* ((inserted (mistty--undo-data-inserted undo-data)) 121 | (inserted-len (length inserted)) 122 | (start (mistty--undo-data-start undo-data)) 123 | (buffer-n (max 0 (- n inserted-len))) 124 | (inserted-n (min n inserted-len)) 125 | (del-text (concat 126 | (mistty--safe-bufstring (- start buffer-n) 127 | start) 128 | (substring inserted 129 | (- inserted-len inserted-n) 130 | inserted-len)))) 131 | (setq entry (cons del-text (- pos (length del-text)))) 132 | (setf (mistty--undo-data-inserted undo-data) 133 | (substring inserted 0 (- inserted-len inserted-n)))))) 134 | 135 | (when entry 136 | (push entry buffer-undo-list))))) 137 | 138 | (provide 'mistty-undo) 139 | 140 | ;;; mistty-undo.el ends here 141 | -------------------------------------------------------------------------------- /Eldev: -------------------------------------------------------------------------------- 1 | ; -*- mode: emacs-lisp; lexical-binding: t -*- 2 | 3 | (eldev-use-package-archive 'gnu-elpa) 4 | 5 | (setq eldev-build-treat-warnings-as-errors t) 6 | (setf eldev-project-loading-mode 'byte-compiled) 7 | (setq sentence-end-double-space nil) 8 | (setq eldev-files-to-package `(:and ,eldev-files-to-package 9 | (concat "!extras/*"))) 10 | 11 | (eldev-use-plugin 'autoloads) 12 | 13 | (eldev-add-loading-roots 'test "test") 14 | (eldev-add-extra-dependencies 15 | '(test emacs) 16 | '(:package tempel :version "0.8")) 17 | 18 | (eldev-add-extra-dependencies '(test emacs) 'turtles) 19 | (eldev-use-vc-repository 20 | 'turtles :github "szermatt/turtles" :commit "2.0.1" 21 | :setup (setq eldev-build-treat-warnings-as-errors nil 22 | eldev-build-suppress-warnings t) 23 | :teardown (setq eldev-build-treat-warnings-as-errors t 24 | eldev-build-suppress-warnings nil)) 25 | 26 | (eldev-add-extra-dependencies '(test emacs) 'yasnippet) 27 | (eldev-use-vc-repository 28 | 'yasnippet :github "joaotavora/yasnippet" :commit "0.14.0" 29 | :setup (setq eldev-build-treat-warnings-as-errors nil 30 | eldev-build-suppress-warnings t) 31 | :teardown (setq eldev-build-treat-warnings-as-errors t 32 | eldev-build-suppress-warnings nil)) 33 | 34 | ;; Exclude work and build files so Eldev doctor ignores them. 35 | (setf eldev-standard-excludes 36 | `(:or ,eldev-standard-excludes 37 | "./venv" "./docs/build" "./license" "./deps")) 38 | 39 | (eldev-defoption mistty-test-bash-exe (&optional path) 40 | "Tests look for Bash at PATH." 41 | :options (--bash) 42 | :optional-value PATH 43 | :for-command test 44 | (setq mistty-test-bash-exe (executable-find path))) 45 | 46 | (eldev-defoption mistty-test-zsh-exe (&optional path) 47 | "Tests look for Zsh at PATH. Empty to skip zsh tests." 48 | :options (--zsh) 49 | :optional-value PATH 50 | :for-command test 51 | (setq mistty-test-zsh-exe (executable-find path))) 52 | 53 | (eldev-defoption mistty-test-fish-exe (&optional path) 54 | "Tests look for Fish at PATH. Empty to skip fish tests." 55 | :options (--fish) 56 | :optional-value PATH 57 | :for-command test 58 | (setq mistty-test-fish-exe (executable-find path))) 59 | 60 | (eldev-defoption mistty-test-ipython-exe (&optional path) 61 | "Tests look for Ipython at PATH. Empty to skip ipython tests." 62 | :options (--ipython) 63 | :optional-value PATH 64 | :for-command test 65 | (setq mistty-test-ipython-exe (executable-find path))) 66 | 67 | (eldev-defoption mistty-test-log () 68 | "Enable mistty-log when running tests." 69 | :options (--enable-test-log) 70 | :for-command test 71 | (setq mistty-test-log t)) 72 | 73 | (defvar mistty-test-ci nil) 74 | (eldev-defoption mistty-test-ci () 75 | "Let tests know they're running on CI." 76 | :options (--ci) 77 | :for-command test 78 | (setq mistty-test-ci t)) 79 | 80 | (eldev-defbuilder mistty-builder-build-info (source target) 81 | :short-name "SPHINX-TEXI" 82 | :message source-and-target 83 | :type many-to-one 84 | :source-files "docs/source/*.rst" 85 | :targets ("mistty.texi") 86 | :collect ":default" 87 | (mistty-eldev-build-texinfo)) 88 | 89 | (defun mistty-eldev-build-texinfo () 90 | (eldev-call-process "sphinx-build" '("-M" "texinfo" "docs/source" "docs/build") 91 | :forward-output t 92 | :die-on-error t) 93 | ;; Remove the date and version to keep the output stable from change 94 | ;; to change. 95 | (with-temp-buffer 96 | (insert-file-contents "docs/build/texinfo/mistty.texi") 97 | (goto-char (point-min)) 98 | (search-forward-regexp "@\\*Generated by Sphinx [0-9.]+\\(snapshot\\)?\\.@\\*") 99 | (replace-match "@*Generated by Sphinx.@*") 100 | (goto-char (point-min)) 101 | (search-forward-regexp "@quotation\nMisTTY \\([0-9.]+\\(snapshot\\)?\\), .*$") 102 | (replace-match "@quotation\nMisTTY \\1") 103 | (write-file "mistty.texi"))) 104 | 105 | (eldev-defcommand mistty-html-doc (&rest parameters) 106 | "Generate documentation HTML into docs/build/html. 107 | 108 | First install Sphinx with: 109 | python3 -m venv venv 110 | . venv/bin/activate 111 | pip3 install -r docs/requirements.txt" 112 | :command documentation 113 | :aliases (html) 114 | (eldev-call-process "sphinx-build" '("-M" "html" "docs/source" "docs/build") 115 | :forward-output t 116 | :die-on-error t)) 117 | 118 | (eldev-defcleaner mistty-cleaner-docs () 119 | "Delete Sphinx build dir" 120 | :default t 121 | '("docs/build")) 122 | 123 | (eldev-use-plugin 'maintainer) 124 | (setq eldev-release-test-local t) 125 | (defun mistty-release-preparator (version &optional post-release-version) 126 | (eldev-with-file-buffer "docs/source/conf.py" 127 | (dolist (tag '("version" "release")) 128 | (goto-char (point-min)) 129 | (search-forward-regexp (concat tag " = '\\([0-9.]+\\(snapshot\\)?\\)' *$")) 130 | (replace-match 131 | (format "%s = '%s'" tag (package-version-join 132 | (or post-release-version version)))))) 133 | (mistty-eldev-build-texinfo)) 134 | (add-hook 'eldev-release-preparators #'mistty-release-preparator) 135 | 136 | ;; After a release, add a snapshot to tag a development version, so if the 137 | ;; release is 1.0.0, the following development version is 1.0.1snapshot 138 | ;; and the next release is going to be 1.0.2 or 1.1. 139 | (setq eldev-release-post-release-commit 140 | (lambda (release-version) 141 | (let ((major (or (nth 0 release-version) 0)) 142 | (minor (or (nth 1 release-version) 0)) 143 | (patch (or (nth 2 release-version) 0))) 144 | (list major minor (1+ patch) -4)))) 145 | 146 | (setq eldev-release-commit-message 147 | "chore: Release @[formatted-name]@ @[version-string]@") 148 | (setq eldev-release-post-release-commit-message 149 | "chore: Development version, following release @[version-string]@") 150 | (add-hook 'eldev-release-post-release-preparators #'mistty-release-preparator) 151 | -------------------------------------------------------------------------------- /test/mistty-log-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-log.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'mistty-log) 18 | (require 'ert) 19 | (require 'ert-x) 20 | 21 | (ert-deftest mistty-log-test-start-stop-log-in-buffer () 22 | (ert-with-test-buffer () 23 | (let ((mistty-log-to-messages nil) 24 | (mistty-log nil) 25 | (mistty-backlog-size 0) 26 | log-buffer) 27 | (should (not mistty-log)) 28 | (mistty-log "too early; will not be logged") 29 | (setq log-buffer (save-excursion (mistty-start-log))) 30 | (unwind-protect 31 | (progn 32 | (should (equal log-buffer mistty-log-buffer)) 33 | (should mistty-log) 34 | (mistty-log "will be logged (1)") 35 | (mistty-log "will be logged (2)") 36 | (mistty-stop-log) 37 | (should (not mistty-log)) 38 | (mistty-log "too late; will not be logged") 39 | (with-current-buffer log-buffer 40 | (goto-char (point-min)) 41 | (should (search-forward "log enabled")) 42 | (should (search-forward "will be logged (1)")) 43 | (should (search-forward "will be logged (2)")) 44 | (goto-char (point-min)) 45 | (should (not (search-forward "will not be logged" nil 'noerror))))) 46 | (let ((kill-buffer-query-functions nil)) 47 | (kill-buffer log-buffer)))))) 48 | 49 | (ert-deftest mistty-log-test-disable-log-when-buffer-is-killed () 50 | (ert-with-test-buffer () 51 | (let ((mistty-log-to-messages nil) 52 | (mistty-log nil) 53 | (mistty-backlog-size 0) 54 | log-buffer) 55 | (setq log-buffer (save-excursion (mistty-start-log))) 56 | (kill-buffer log-buffer) 57 | (should-not mistty-log)))) 58 | 59 | (ert-deftest mistty-log-test-start-stop-log-logging-to-messages () 60 | (ert-with-test-buffer () 61 | (let ((mistty-log-to-messagse t) 62 | (mistty-log nil) 63 | (mistty-backlog-size 0) 64 | (mistty-log-buffer nil) 65 | (inhibit-message t) 66 | log-buffer) 67 | (ert-with-message-capture messages 68 | (should (not mistty-log)) 69 | (mistty-log "too early; will not be logged") 70 | (setq log-buffer (save-excursion (mistty-start-log))) 71 | (should (equal log-buffer mistty-log-buffer)) 72 | (should mistty-log) 73 | (mistty-log "will be logged (1)") 74 | (mistty-log "will be logged (2)") 75 | (mistty-stop-log) 76 | (should (not mistty-log)) 77 | (mistty-log "too late; will not be logged") 78 | 79 | (with-temp-buffer 80 | (insert messages) 81 | (goto-char (point-min)) 82 | (should (search-forward "log enabled")) 83 | (should (search-forward "will be logged (1)")) 84 | (should (search-forward "will be logged (2)")) 85 | (goto-char (point-min)) 86 | (should (not (search-forward 87 | "will not be logged" nil 'noerror)))))))) 88 | 89 | (ert-deftest mistty-log-test-drop-log () 90 | (ert-with-test-buffer () 91 | (let ((mistty-log-to-messages nil) 92 | (mistty-log nil) 93 | (mistty-log-buffer nil) 94 | (log-buffer nil)) 95 | ;; Make sure not to call (mistty-start-log) outside of the (let 96 | ;; mistty-log ...) 97 | (setq log-buffer (save-excursion (mistty-start-log))) 98 | (unwind-protect 99 | (progn 100 | (mistty-log "dummy entry") 101 | (mistty-drop-log) 102 | (should (not (buffer-live-p log-buffer))) 103 | (should (null mistty-log-buffer))) 104 | (let ((kill-buffer-query-functions nil)) 105 | (kill-buffer log-buffer)))))) 106 | 107 | (ert-deftest mistty-log-test-backlog () 108 | (ert-with-test-buffer () 109 | (let ((mistty-log-to-messages nil) 110 | (mistty-log nil) 111 | (mistty-log-buffer nil) 112 | (mistty-backlog-size 3) 113 | log-buffer) 114 | (dotimes (i 10) 115 | (mistty-log "log entry %s" i)) 116 | (setq log-buffer (save-excursion (mistty-start-log))) 117 | (unwind-protect 118 | (progn 119 | (mistty-stop-log) 120 | (with-current-buffer log-buffer 121 | (goto-char (point-min)) 122 | (should (search-forward "log entry 7")) 123 | (should (search-forward "log entry 8")) 124 | (should (search-forward "log entry 9")) 125 | (should (not (search-forward "log entry [0-6]" nil 'noerror))))) 126 | (let ((kill-buffer-query-functions nil)) 127 | (kill-buffer log-buffer)))))) 128 | 129 | (ert-deftest mistty-log-test-log-quoted-nonascii () 130 | (ert-with-test-buffer () 131 | (let ((mistty-log-to-messages nil) 132 | (mistty-log nil) 133 | (mistty-backlog-size 0) 134 | log-buffer) 135 | (setq log-buffer (save-excursion (mistty-start-log))) 136 | (unwind-protect 137 | (progn 138 | (mistty-log "DATA1: %S" "\eOC\eOA") 139 | (mistty-log "DATA2: %S" "text\r\n") 140 | (mistty-stop-log) 141 | (with-current-buffer log-buffer 142 | (goto-char (point-min)) 143 | (search-forward "DATA1:") 144 | (should (equal " \"\\33OC\\33OA\"" 145 | (buffer-substring (point) (pos-eol)))) 146 | (search-forward "DATA2:") 147 | (should (equal " \"text\\15\\n\"" 148 | (buffer-substring (point) (pos-eol)))))) 149 | (let ((kill-buffer-query-functions nil)) 150 | (kill-buffer log-buffer)))))) 151 | -------------------------------------------------------------------------------- /test/mistty-osc7-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests MisTTY's OSC7 support -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'ert) 18 | (require 'ert-x) 19 | (require 'tramp) 20 | 21 | (require 'mistty) 22 | (require 'mistty-osc7) 23 | 24 | (require 'mistty-testing) 25 | 26 | (ert-deftest mistty-test-osc7-local-path () 27 | (mistty-with-test-buffer (:shell zsh) 28 | (let ((mistty-osc-handlers '(("7" . mistty-osc7)))) 29 | ;; Not using bash, because it sends \032 dirtrack, which would 30 | ;; interfere with this test. 31 | (mistty-test-send-osc7 (system-name) "/var/log") 32 | (mistty-send-and-wait-for-prompt) 33 | (should (equal "/var/log/" default-directory)) 34 | 35 | (mistty-test-send-osc7 "localhost" "/home") 36 | (mistty-send-and-wait-for-prompt) 37 | (should (equal "/home/" default-directory)) 38 | 39 | (mistty-test-send-osc7 "" "/") 40 | (mistty-send-and-wait-for-prompt) 41 | (should (equal "/" default-directory))))) 42 | 43 | (ert-deftest mistty-test-osc7-utf8-path () 44 | (mistty-with-test-buffer (:shell zsh) 45 | (ert-with-temp-directory tempdir 46 | (let ((utf8-dir (file-name-as-directory (expand-file-name "αβγδ" tempdir)))) 47 | (make-directory utf8-dir) 48 | (mistty-test-send-osc7 (system-name) (concat tempdir (url-hexify-string "αβγδ"))) 49 | (mistty-send-and-wait-for-prompt) 50 | (should (equal utf8-dir default-directory)))))) 51 | 52 | (ert-deftest mistty-test-osc7-remote-path () 53 | (mistty-with-test-buffer (:shell zsh) 54 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 55 | (mistty-allow-tramp-paths t) 56 | (tramp-default-method "ssh") 57 | (tramp-default-method-alist nil)) 58 | (mistty-test-send-osc7 "testmachine.example" "/var/log") 59 | (mistty-send-and-wait-for-prompt) 60 | (should (equal "/ssh:testmachine.example:/var/log/" default-directory))))) 61 | 62 | (ert-deftest mistty-test-osc7-remote-path-disallowed () 63 | (mistty-with-test-buffer (:shell zsh) 64 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 65 | (mistty-allow-tramp-paths nil) 66 | (orig-default-directory default-directory)) 67 | (mistty-test-send-osc7 "testmachine.example" "/var/log") 68 | (mistty-send-and-wait-for-prompt) 69 | (should (equal orig-default-directory default-directory))))) 70 | 71 | (ert-deftest mistty-test-osc7-remote-path-with-host-default () 72 | (mistty-with-test-buffer (:shell zsh) 73 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 74 | (mistty-allow-tramp-paths t) 75 | (tramp-default-method "ssh") 76 | (tramp-default-method-alist '(("testmachine.example" nil "scp")))) 77 | (mistty-test-send-osc7 "testmachine.example" "/var/log") 78 | (mistty-send-and-wait-for-prompt) 79 | (should (equal "/scp:testmachine.example:/var/log/" default-directory))))) 80 | 81 | (ert-deftest mistty-test-osc7-host-to-tramp-path () 82 | (mistty-with-test-buffer (:shell zsh) 83 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 84 | (mistty-allow-tramp-paths nil) 85 | (tramp-default-method "ssh") 86 | (mistty-host-to-tramp-path-alist '(("testmachine" . "/scp:testmachine.example:")))) 87 | (mistty-test-send-osc7 "testmachine" "/var/log") 88 | (mistty-send-and-wait-for-prompt) 89 | (should (equal "/scp:testmachine.example:/var/log/" default-directory))))) 90 | 91 | (ert-deftest mistty-test-osc7-ignore-invalid-host-to-tramp-path () 92 | (mistty-with-test-buffer (:shell zsh) 93 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 94 | (mistty-allow-tramp-paths t) 95 | (tramp-default-method "ssh") 96 | (mistty-host-to-tramp-path-alist '(("testmachine" . "scp:testmachine.example/")))) 97 | (mistty-test-send-osc7 "testmachine" "/var/log") 98 | (mistty-send-and-wait-for-prompt) 99 | (should (equal "/ssh:testmachine:/var/log/" default-directory))))) 100 | 101 | (ert-deftest mistty-test-osc7-disable-host () 102 | (mistty-with-test-buffer (:shell zsh) 103 | (let ((mistty-osc-handlers '(("7" . mistty-osc7))) 104 | (mistty-allow-tramp-paths t) 105 | (tramp-default-method "ssh") 106 | (mistty-host-to-tramp-path-alist '(("testmachine" . nil)))) 107 | (mistty-test-send-osc7 "testmachine" "/var/log") 108 | (mistty-send-and-wait-for-prompt) 109 | (should-not (file-remote-p default-directory))))) 110 | 111 | (ert-deftest mistty-test-osc7-keep-prefix-when-host-matches () 112 | (let* ((tramp-methods (mistty-test-tramp-methods)) 113 | (tramp-prefix (mistty-test-tramp-prefix)) 114 | (default-directory (concat tramp-prefix "/"))) 115 | (mistty-with-test-buffer (:shell zsh) 116 | 117 | (mistty-test-send-osc7 (system-name) "/var/log") 118 | (mistty-send-and-wait-for-prompt) 119 | (should (equal (concat tramp-prefix "/var/log/") default-directory)) 120 | 121 | (mistty-test-send-osc7 (system-name) "/") 122 | (mistty-send-and-wait-for-prompt) 123 | (should (equal (concat tramp-prefix "/") default-directory))))) 124 | 125 | (ert-deftest mistty-test-osc7-keep-prefix-when-localhost () 126 | (let* ((tramp-methods (mistty-test-tramp-methods)) 127 | (tramp-prefix (mistty-test-tramp-prefix)) 128 | (default-directory (concat tramp-prefix "/"))) 129 | (mistty-with-test-buffer (:shell zsh) 130 | 131 | (mistty-test-send-osc7 "" "/var/log") 132 | (mistty-send-and-wait-for-prompt) 133 | (should (equal (concat tramp-prefix "/var/log/") default-directory)) 134 | 135 | (mistty-test-send-osc7 "localhost" "/") 136 | (mistty-send-and-wait-for-prompt) 137 | (should (equal (concat tramp-prefix "/") default-directory))))) 138 | 139 | (defun mistty-test-send-osc7 (host path) 140 | (mistty--send-string 141 | mistty-proc 142 | (concat "printf '\\e]7;file://%s%s\\e\\\\\\\n' '" host "' '" path "'"))) 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MisTTY, a shell/comint alternative with a fully functional terminal 2 | 3 | [![CI Status](https://github.com/szermatt/mistty/actions/workflows/CI.yml/badge.svg)](https://github.com/szermatt/mistty/actions/workflows/CI.yml) 4 | [![Documentation Status](https://readthedocs.org/projects/mistty/badge/?version=latest)](https://mistty.readthedocs.io/en/latest/?badge=latest) 5 | [![MELPA](https://melpa.org/packages/mistty-badge.svg)](https://melpa.org/#/mistty) 6 | [![MELPA Stable](https://stable.melpa.org/packages/mistty-badge.svg)](https://stable.melpa.org/#/mistty) 7 | 8 | **MisTTY** is a major mode for Emacs 29.1 and up that runs 9 | a shell inside of a buffer, similarly to comint mode. It is built on 10 | top of `term.el`. 11 | 12 | `M-x mistty` creates a buffer with an interactive shell. See 13 | [launching](https://mistty.readthedocs.io/en/latest/usage.html#launching) 14 | for details. 15 | 16 | MisTTY feels very much like `comint` mode: you can move around freely 17 | and run any Emacs command you want - until you press TAB and end up 18 | with the native completion or notice the shell autosuggestions. With 19 | MisTTY you have access to both Emacs and the shell commands and 20 | editing tools. Like `comint`, MisTTY supports [remote shells with 21 | TRAMP](https://mistty.readthedocs.io/en/latest/usage.html#tramp). 22 | 23 | Additionally, commands that take over the entire screen, such as 24 | `less` or `vi` also work, temporarily taking over the window, while 25 | scrollback remains available in another buffer. 26 | 27 | MisTTY works well with Bash and ZSH, but it is especially well 28 | suited to running [Fish](https://fishshell.com): you get 29 | autosuggestions, completion in full colors. Here's what the end 30 | result might look like: 31 | 32 | ![screen grab](https://github.com/szermatt/mistty/blob/master/screengrab.gif?raw=true) 33 | 34 | MisTTY is known to work on Linux and MacOS. It also supports non-shell 35 | command-line programs, such as python or ipython. MisTTY should work 36 | with most shells, but it is possible that a shell confuses it, 37 | especially advanced ones. In such a case, please consider [turning on 38 | OSC133 39 | sequences](https://mistty.readthedocs.io/en/latest/usage.html#osc133) 40 | and [filing a bug report]( 41 | https://mistty.readthedocs.io/en/latest/contrib.html#reporting-issues). 42 | 43 | ## COMPARISON 44 | 45 | MisTTY isn't a terminal emulator, but rather a frontend to an existing 46 | terminal emulator, the built-in `term.el`. Its goal is to make it more 47 | convenient to use while inside of Emacs and better integrate with 48 | Emacs itself. In theory, other terminal emulators than `term.el` might 49 | be used as engine for MisTTY, such as `vterm` and `eat`. 50 | 51 | MisTTY has some similarities with `coterm`; it offers the same switch 52 | between full-screen and line mode. 53 | 54 | `Coterm`, `ansi-term` and `eat` all have a line mode, just like 55 | `comint`. While in line mode, rendering is done by Emacs and editing 56 | commands are Emacs commands. In constrast, with MisTTY, all rendering 57 | is done by the shell through the terminal. This is why native shell 58 | completion and autosuggestion is available with MisTTY and not with 59 | these other line modes and why you can freely mix shell commands with 60 | Emacs commands while editing the command line. 61 | 62 | `ansi-term` and `eat` also have a char mode, where rendering and 63 | command execution is handled by the shell, and editing with Emacs 64 | isn't available. The difference with MisTTY is then that MisTTY makes 65 | Emacs editing commands available when possible. 66 | 67 | `eat` also has a semi-char mode, which is the closest there is to 68 | MisTTY. In that mode, Emacs movements commands are available. However, 69 | Emacs commands that modify the buffer, aren't available to edit the 70 | command line. In contrast, MisTTY allows Emacs to navigate to and edit 71 | the whole buffer, then replays changes made to the command-line. 72 | 73 | ## INSTALLATION 74 | 75 | > **The following is just a quick introduction. Read the full documentation at https://mistty.readthedocs.io/en/latest/** 76 | 77 | You can install MisTTY: 78 | - from [MELPA](https://melpa.org/#/getting-started), by typing `M-x package-install mistty` 79 | - from source, by executing `(package-vc-install "https://github.com/szermatt/mistty")` 80 | 81 | ## USAGE 82 | 83 | Type `M-x mistty` to launch a new shell buffer in MisTTY mode, then 84 | use it as you would comint. 85 | 86 | You'll quickly notice some differences. For example TAB completion 87 | working just like in a terminal instead of relying of Emacs 88 | completion. 89 | 90 | The purple line on the left indicates the portion of the buffer 91 | that's a terminal. What you type in there gets sent to the program, 92 | usually a shell, and translated by that program. The rest of the 93 | buffer is normal, editable, text. 94 | 95 | Commands that takes the whole screen such as `less` or `vi` take you 96 | into terminal mode for the duration of that command. You can still 97 | access previous commands in the "scrollback" MisTTY buffer by typing 98 | `C-c C-j`. 99 | 100 | If you ever get into a situation where a command needs you to press 101 | keys normally sent to Emacs, such as the arrow keys, press `C-c C-q`. 102 | It'll send all key strokes directly to the terminal until you exit 103 | the mode by pressing `C-g`. To send a single key to the terminal 104 | you can also press `C-q ` instead. 105 | 106 | You will very likely want to send some keys you use often directly 107 | to the terminal. This is done by binding keys to `mistty-send-key` 108 | in `mistty-prompt-map`. For example: 109 | 110 | ```elisp 111 | (use-package mistty 112 | :bind (("C-c s" . mistty) 113 | 114 | ;; bind here the shortcuts you'd like the 115 | ;; shell to handle instead of Emacs. 116 | :map mistty-prompt-map 117 | 118 | ;; fish: directory history 119 | ("M-" . mistty-send-key) 120 | ("M-" . mistty-send-key) 121 | ("M-" . mistty-send-key) 122 | ("M-" . mistty-send-key))) 123 | ``` 124 | 125 | In addition to that, unless you're using Bash, which supports it out 126 | of the box, you'll also likely want to enable directory tracking [in 127 | your shell](https://mistty.readthedocs.io/en/latest/shells.html). 128 | 129 | See also [the documentation](https://mistty.readthedocs.io/en/latest/) 130 | for more details on configuring MisTTY . 131 | 132 | ## SOMETHING IS WRONG ! 133 | 134 | Please check the [FAQ](https://mistty.readthedocs.io/en/latest/faq.html) 135 | and, if that doesn't help, take the time to [file a bug report](https://mistty.readthedocs.io/en/latest/contrib.html#reporting-issues). 136 | 137 | ## CONTRIBUTING 138 | 139 | See the [Contributing](https://mistty.readthedocs.io/en/latest/contrib.html) 140 | section of the documentation. 141 | 142 | ## COMPATIBILITY 143 | 144 | MisTTY requires Emacs 29.1 or later. 145 | -------------------------------------------------------------------------------- /mistty-log.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-log.el --- Logging infrastructure for mistty.el. -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file contains helper utilities for mistty.el for logging 20 | ;; events, for debugging. 21 | ;; 22 | ;; To turn on logging in a buffer, call the command 23 | ;; `mistty-start-log'. This command creates a buffer that gets filled 24 | ;; with events reported to the function `mistty-log' on that buffer 25 | ;; and any other buffer on which `mistty-start-log' was called. 26 | 27 | ;;; Code: 28 | 29 | (require 'ring) 30 | (eval-when-compile 31 | (require 'cl-lib)) 32 | 33 | (defvar mistty-log-buffer nil 34 | "Buffer when log messages are directed, might not be live.") 35 | 36 | (defvar mistty-log nil 37 | "Whether logging is enabled globally. 38 | 39 | This is usually set by calling `mistty-start-log'. 40 | 41 | Calling the function `mistty-log' is a no-op unless this is set.") 42 | 43 | (defvar mistty-log-to-messages noninteractive 44 | "If non-nil, send log to messages instead of mistty log buffer.") 45 | 46 | (defcustom mistty-backlog-size 0 47 | "Log entries to track when logging is disabled. 48 | 49 | As many as `mistty-backlog-size' entries will be backfilled 50 | by `mistty-start-log' when logging is enabled. 51 | 52 | Setting this value allows turning on logging once something wrong 53 | has happened." 54 | :group 'mistty 55 | :type '(integer)) 56 | 57 | (defvar-local mistty--backlog nil 58 | "If non-nil, a ring of `mistty--log' arguments.") 59 | 60 | (defvar-local mistty--log-start-time nil 61 | "Base for logged times. 62 | 63 | This is also the time the log buffer was created.") 64 | 65 | (defface mistty-log-header-face '((t (:italic t))) 66 | "Face applied to the headers in `mistty-log' buffer. 67 | 68 | This applies to log entries created by the function `mistty-log'." 69 | :group 'mistty) 70 | 71 | (defface mistty-log-message-face nil 72 | "Face applied to the message in `mistty-log' buffer. 73 | 74 | This applies to log entries created by the function `mistty-log'." 75 | :group 'mistty) 76 | 77 | (defsubst mistty-log (str &rest args) 78 | "Format STR with ARGS and add them to the debug log buffer, when enabled. 79 | 80 | String arguments are formatted and decoded to UTF-8, so terminal 81 | communication can safely be sent out. 82 | 83 | This does nothing unless logging is enabled for the current 84 | buffer. It is usually enabled by calling mistty-start-log." 85 | (when (or mistty-log (> mistty-backlog-size 0)) 86 | (mistty--log str args))) 87 | 88 | (defun mistty-start-log () 89 | "Enable logging and display the log buffer. 90 | 91 | If logging is already enabled, just show the buffer." 92 | (interactive) 93 | (if (and mistty-log (buffer-live-p mistty-log-buffer)) 94 | (switch-to-buffer-other-window mistty-log-buffer) 95 | (setq mistty-log t) 96 | (dolist (buffer (buffer-list)) 97 | (with-current-buffer buffer 98 | (when (ring-p mistty--backlog) 99 | (dolist (args (nreverse (ring-elements mistty--backlog))) 100 | (apply #'mistty--log args)) 101 | (setq mistty--backlog nil)))) 102 | (setq mistty--backlog nil) 103 | (mistty--log "Log enabled." nil) 104 | (when (buffer-live-p mistty-log-buffer) 105 | (switch-to-buffer-other-window mistty-log-buffer)))) 106 | 107 | (defun mistty-stop-log () 108 | "Disable logging for all MisTTY buffers." 109 | (interactive) 110 | (when mistty-log 111 | (unwind-protect 112 | (when (buffer-live-p mistty-log-buffer) 113 | (mistty-log "Log disabled")) 114 | (setq mistty-log nil)))) 115 | 116 | (defun mistty-drop-log () 117 | "Disable logging and kill the log buffer." 118 | (interactive) 119 | (setq mistty-log nil) 120 | (when (buffer-live-p mistty-log-buffer) 121 | (kill-buffer mistty-log-buffer)) 122 | (setq mistty-log-buffer nil)) 123 | 124 | (defun mistty--log (format-str args &optional event-time) 125 | "Append FORMAT-STR and ARGS to the log. 126 | 127 | This is normally called from `mistty-log', which first checks 128 | whether logging or the backlog are enabled. 129 | 130 | If logging is disabled, but the backlog is enabled, add a new 131 | entry to the backlog. 132 | 133 | If logging is enabled, add a new entry to the buffer 134 | *mistty-log*. 135 | 136 | EVENT-TIME is the time of the event to log, a float time, as 137 | returned by `float-time'. It defaults to the current time. 138 | 139 | Calling this function creates `mistty-log-buffer' if it doesn't 140 | exit already." 141 | (let ((event-time (or event-time (float-time))) 142 | (calling-buffer (current-buffer))) 143 | (cond 144 | 145 | ;; not enabled; add to backlog 146 | ((and (not mistty-log) (> mistty-backlog-size 0)) 147 | (ring-insert 148 | (or mistty--backlog 149 | (setq mistty--backlog (make-ring mistty-backlog-size))) 150 | (list format-str args event-time))) 151 | 152 | ;; enabled; interactive: log to buffer 153 | ((and mistty-log (not mistty-log-to-messages)) 154 | (with-current-buffer 155 | (or (and (buffer-live-p mistty-log-buffer) mistty-log-buffer) 156 | (setq mistty-log-buffer 157 | (get-buffer-create "*mistty-log*"))) 158 | (add-hook 'kill-buffer-hook #'mistty-stop-log nil t) 159 | (setq-local window-point-insertion-type t) 160 | (goto-char (point-max)) 161 | (insert-before-markers 162 | (propertize (mistty--log-header event-time calling-buffer) 163 | 'face 'mistty-log-header-face)) 164 | (insert-before-markers 165 | (propertize 166 | (if args 167 | (mistty--log-format format-str args) 168 | format-str) 169 | 'face 'mistty-log-message-face)) 170 | (insert-before-markers "\n"))) 171 | 172 | ;; enabled; batch: output 173 | ((and mistty-log mistty-log-to-messages) 174 | (message "%s %s" 175 | (mistty--log-header event-time calling-buffer) 176 | (mistty--log-format format-str args)))))) 177 | 178 | (defun mistty--log-format (format-str args) 179 | "Configure how %S behaves then format FORMAT-STR with ARGS. 180 | 181 | This is used to include terminal data." 182 | (let ((print-quoted t) 183 | (print-escape-newlines t) 184 | (print-escape-control-characters t) 185 | (print-escape-nonascii t) 186 | (print-escape-multibyte nil) 187 | (print-length nil)) 188 | (apply #'format format-str args))) 189 | 190 | (defun mistty--log-header (event-time buf) 191 | "Format a header for the macro `mistty-log'. 192 | 193 | The header include EVENT-TIME and the name of BUF." 194 | (format "[%s] %3.3f " 195 | (buffer-name buf) 196 | (- event-time 197 | (or mistty--log-start-time 198 | (setq mistty--log-start-time event-time))))) 199 | 200 | (provide 'mistty-log) 201 | 202 | ;;; mistty-log.el ends here 203 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | **The display is all messed up** 5 | 6 | First, check the value of the environment variable :code:`TERM`. 7 | It MUST be :code:`eterm-color` or :code:`eterm-direct`; nothing 8 | else will work reliably. 9 | 10 | If that still doesn't work, please file a bug as described in 11 | :ref:`reporting`. 12 | 13 | **warning: Could not set up terminal** 14 | 15 | If you're getting errors such as the following from programs such 16 | as :program:`less` or :program:`vi`, this means that the 17 | :code:`TERM` environment variable is set properly, but the host 18 | doesn't know about the terminal :code:`eterm-color` or 19 | :code:`eterm-direct`. 20 | 21 | .. code-block:: text 22 | 23 | warning: Could not set up terminal. 24 | warning: TERM environment variable set to 'eterm-color'. 25 | warning: Check that this terminal type is supported on this system. 26 | 27 | This might easily happen if you ssh into another host from inside 28 | a MisTTY buffer. SSH typically forwards the value of the 29 | :code:`TERM` environment variable, which contains the terminal 30 | name, but not :code:`TERMCAP` environment variable, which contains 31 | the terminal definition. 32 | 33 | To fix this issue, you can do any one of the following: 34 | 35 | - Connect using TRAMP, as described in :ref:`remote_shells`. TRAMP takes 36 | care of setting all necessary environment variables. 37 | 38 | - Add the definition of :code:`eterm-color` to all hosts you 39 | regularly log into. To do that, follow the instructions in 40 | :file:`/e/README`, where 41 | :code:`` is the "etc" directory of your Emacs 42 | installation, as shown by `M-x describe-variable 43 | data-directory` - usually, that's 44 | :file:`/usr/share/emacs//etc/e/README` 45 | 46 | - Tell ssh to forward the :code:`TERMCAP` environment variable. This 47 | requires changing *both* the server and client configuration. On the 48 | server :file:`sshd_config`, add :code:`AcceptEnv TERMCAP`. On the 49 | client, add :code:`SendEnv TERMCAP` to :file:`ssh_config` or to 50 | :file:`~/.ssh/config` 51 | 52 | **What are all those OCOCOCO or ODODODO that appear on the screen?** 53 | 54 | .. index:: pair: variable; mistty-forbid-edit-regexps 55 | 56 | These are the terminal sequences that MisTTY sends to a program 57 | to move the cursor left or right. If you see these printed on the 58 | terminal, it means that the program that's currently controlling 59 | the terminal doesn't support these. 60 | 61 | In such situation, you can: 62 | 63 | - Only type self-inserting characters and :kbd:`DEL`. 64 | 65 | - Press :kbd:`C-c C-q` or :kbd:`M-x mistty-send-key-sequence` to 66 | send what you type directly to the terminal until you turn it 67 | off with :kbd:`C-g`. 68 | 69 | - Write a regexp that identifies the situation and add it to 70 | :kbd:`M-x customize-option mistty-forbid-edit-regexps` so MisTTY 71 | knows it should not attempt to move the cursor. The default value 72 | identifies most shell backward search prompts. 73 | 74 | See :ref:`term-vs-scroll` for more details. 75 | 76 | **The shell isn't answering!** 77 | 78 | Press :kbd:`C-g`. If this is just a one-time thing, this will do 79 | the trick. 80 | 81 | If this keeps happening, check the modeline. Does it contain CMD? 82 | It might look like this: *misTTY CMD:run*. In that case, MisTTY is 83 | stuck in long-running command mode. This is likely due to some 84 | package leaving overlays to the buffer that confuse MisTTY. To fix 85 | that, turn off the option :kbd:`M-x customize-option 86 | mistty-detect-foreign-overlays` or, if you know which package is 87 | causing trouble, remove the corresponding property in in :kbd:`M-x 88 | customize-option mistty-foreign-overlay-properties`. 89 | 90 | For details, see :ref:`lrc` 91 | 92 | If this keeps happening and the modeline does not contain CMD, 93 | this is likely a bug. For details on filing a bug report, see 94 | :ref:`reporting` 95 | 96 | **Why is the cursor jumping around when I move it?** 97 | 98 | MisTTY jumps over spaces which just "aren't there", such as the 99 | spaces between the command and the right prompt, spaces added by 100 | :program:`fish` for indentation in multi-line prompts. 101 | 102 | If it doesn't work with your shell or setup, or if you find it 103 | confusing, type :kbd:`M-x customize-option 104 | mistty-skip-empty-spaces` to turn it off. 105 | 106 | **What's with the purple line?** 107 | 108 | This line indicates the region of the buffer that works as a 109 | terminal, meaning that it is not fully editable and that some 110 | shortcuts, such as :kbd:`TAB` are sent directly to the program 111 | controlling the terminal. 112 | 113 | This is covered in depth in :ref:`term-vs-scroll`. 114 | 115 | If you just don't want to see that line, turn it off with 116 | :kbd:`M-x customize-option mistty-fring-enabled` 117 | 118 | **Why doesn't work in the terminal region?** 119 | 120 | The terminal region of MisTTY behaves very differently from a 121 | normal buffer; many things can go wrong with commands that do more 122 | than just editing text. 123 | 124 | One such issue is with interactivly editing the buffer over 125 | multiple Emacs command, which MisTTY calls a long-running command. 126 | There are ways of making such commands work if they don't already, 127 | described in :ref:`lrc`. 128 | 129 | Another such issue is with autocomplete, with can also be made to 130 | work as described in :ref:`autocomplete`. 131 | 132 | While this works with some packages, it might not necessarily work 133 | with the package you want - it might even not be possible to make 134 | it work, but we can always try. Please `file a bug 135 | `_ if you encounter 136 | such a package you'd like to work with MisTTY. 137 | 138 | **... but it used to work!** 139 | 140 | Older versions used to detect any unknown overlay as a 141 | long-running command, described in :ref:`lrc`. Unfortunately, this 142 | caused problems with many packages which, leaving overlays around 143 | for a long time, prevented MisTTY from working at all. 144 | 145 | The good news is that it's likely easy to make it work again by 146 | detecting the specific kind of overlays the package is using. 147 | Please see :ref:`lrc`, or file a bug (:ref:`reporting`) mentioning 148 | the package you're using, its version and how you installed it. 149 | 150 | **Why am I getting connection errors from TRAMP?** 151 | 152 | MisTTY tries to track the current directory whenever possible, 153 | including from remote shells. You might get connection errors if 154 | you connect to a remote or special shell from an existing MisTTY 155 | that Emacs cannot access through TRAMP and then Emacs tries to 156 | access a nonexisting remote file. 157 | 158 | The best solution in such case is to configure TRAMP to connect to 159 | that host, adding an entry to :kbd:`M-x configure-option 160 | mistty-host-to-tramp-path-alist`, if that's necessary. 161 | 162 | If that's not possible or if you don't want to bother, you might 163 | find it convenient to just disable the generation of TRAMP paths 164 | using :kbd:`M-x customize-option mistty-allow-tramp-paths`. 165 | 166 | For more details, see :ref:`dirtrack`. 167 | 168 | **The buffer is killed when the shell finishes. This didn't use to happen!** 169 | 170 | .. index:: pair: variable; mistty-at-end 171 | 172 | MisTTY now by default kills the buffer and its containing window 173 | when the shell ends. If you don't like that, change the option at 174 | :kbd:`M-x customize-option mistty-at-end` to do nothing. 175 | -------------------------------------------------------------------------------- /test/mistty-tramp-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests MisTTY's TRAMP integration -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'ert) 18 | (require 'ert-x) 19 | (require 'tramp) 20 | (eval-when-compile 21 | (require 'cl-lib)) 22 | 23 | (require 'mistty) 24 | (require 'mistty-testing) 25 | 26 | (ert-deftest mistty-tramp-test-shell-start () 27 | (let* ((tramp-methods (mistty-test-tramp-methods)) 28 | (tramp-prefix (mistty-test-tramp-prefix)) 29 | (home (file-name-directory "/")) 30 | (default-directory (concat tramp-prefix home))) 31 | (mistty-with-test-buffer () 32 | (should (equal (concat tramp-prefix "/") 33 | (buffer-local-value 'default-directory (current-buffer)))) 34 | (should (equal mistty-test-bash-exe (mistty-test-remote-command))) 35 | 36 | ;; This just makes sure the shell is functional. 37 | (mistty-send-text "echo hello") 38 | (should (equal "hello" (mistty-send-and-capture-command-output))) 39 | 40 | ;; TRAMP sets INSIDE_EMACS and reports its version. 41 | (mistty-send-string "echo $INSIDE_EMACS") 42 | (should (equal (format "%s,term:%s,tramp:%s" emacs-version term-protocol-version tramp-version) 43 | (mistty-send-and-capture-command-output)))))) 44 | 45 | (ert-deftest mistty-tramp-test-connection-local-explicit-shell-file-name () 46 | (skip-unless mistty-test-zsh-exe) 47 | (let* ((tramp-methods (mistty-test-tramp-methods)) 48 | (tramp-prefix (mistty-test-tramp-prefix)) 49 | (default-directory (concat tramp-prefix "/")) 50 | (connection-local-profile-alist nil) 51 | (connection-local-criteria-alist nil) 52 | (mistty-shell-command nil) 53 | (explicit-shell-file-name "/bin/sh") 54 | buf) 55 | 56 | (connection-local-set-profile-variables 57 | 'test-profile 58 | `((explicit-shell-file-name . ,mistty-test-zsh-exe))) 59 | (connection-local-set-profiles (mistty-test-tramp-protocol) 'test-profile) 60 | 61 | ;; This makes sure the connection-local setup above works. 62 | (should (equal mistty-test-zsh-exe 63 | (with-connection-local-variables explicit-shell-file-name))) 64 | 65 | (unwind-protect 66 | (progn 67 | (setq buf (mistty-create)) 68 | (should (process-get mistty-proc 'remote-command)) 69 | ;; This makes sure that the connection-local value of 70 | ;; explicit-shell-file-name is the one that's used, and not 71 | ;; the global value. 72 | (should (equal mistty-test-zsh-exe (mistty-test-remote-command)))) 73 | (when (buffer-live-p buf) 74 | (let ((kill-buffer-query-functions nil)) 75 | (kill-buffer buf)))))) 76 | 77 | (ert-deftest mistty-tramp-test-connection-local-explicit-mistty-shell-command () 78 | (skip-unless mistty-test-zsh-exe) 79 | (let* ((tramp-methods (mistty-test-tramp-methods)) 80 | (tramp-prefix (mistty-test-tramp-prefix)) 81 | (default-directory (concat tramp-prefix "/")) 82 | (connection-local-profile-alist nil) 83 | (connection-local-criteria-alist nil) 84 | (mistty-shell-command "/bin/sh") 85 | buf) 86 | 87 | (connection-local-set-profile-variables 88 | 'test-profile 89 | `((mistty-shell-command . ,mistty-test-zsh-exe))) 90 | (connection-local-set-profiles (mistty-test-tramp-protocol) 'test-profile) 91 | 92 | ;; This makes sure the connection-local setup above works. 93 | (should (equal mistty-test-zsh-exe 94 | (with-connection-local-variables mistty-shell-command))) 95 | 96 | (unwind-protect 97 | (progn 98 | (setq buf (mistty-create)) 99 | (should (process-get mistty-proc 'remote-command)) 100 | ;; This makes sure that the connection-local value of 101 | ;; explicit-shell-file-name is the one that's used, and not 102 | ;; the global value. 103 | (should (equal mistty-test-zsh-exe (mistty-test-remote-command)))) 104 | (when (buffer-live-p buf) 105 | (let ((kill-buffer-query-functions nil)) 106 | (kill-buffer buf)))))) 107 | 108 | (ert-deftest mistty-tramp-test-termcap () 109 | (let* ((tramp-methods (mistty-test-tramp-methods)) 110 | (tramp-prefix (mistty-test-tramp-prefix)) 111 | (default-directory (concat tramp-prefix "/")) 112 | (term-term-name "eterm-test")) 113 | (mistty-with-test-buffer () 114 | 115 | ;; TERMINFO shouldn't be set in remote shells. 116 | (mistty-send-text "echo TERMINFO=${TERMINFO}.") 117 | (should (equal "TERMINFO=." (mistty-send-and-capture-command-output))) 118 | 119 | ;; TERMCAP should be set. 120 | (mistty-send-text "if [ -n \"$TERMCAP\" ]; then echo set; else echo unset; fi") 121 | (should (equal "set" (mistty-send-and-capture-command-output))) 122 | 123 | ;; captoinfo reads and processes the TERMCAP env variable. This 124 | ;; makes sure that the content of TERMCAP is valid. 125 | (mistty-send-text "captoinfo") 126 | (should (string-match "eterm-test,\n +am, mir.*" (mistty-send-and-capture-command-output)))))) 127 | 128 | (ert-deftest mistty-tramp-test-dirtrack-on-sg () 129 | (let* ((tramp-methods (mistty-test-tramp-methods)) 130 | (tramp-prefix (mistty-test-tramp-prefix)) 131 | (default-directory (concat tramp-prefix "/"))) 132 | (mistty-with-test-buffer (:shell zsh) 133 | ;; Not using bash, because it sends \032 dirtrack, which would 134 | ;; interfere with this test. 135 | 136 | (should (equal (concat tramp-prefix "/") default-directory)) 137 | 138 | (mistty--send-string mistty-proc "printf '\\032//var/log\\nok\\n'") 139 | (should (equal "ok" (mistty-send-and-capture-command-output))) 140 | (should (equal (concat tramp-prefix "/var/log/") default-directory)) 141 | 142 | (mistty--send-string mistty-proc "printf '\\032//home\\nok\\n'") 143 | (should (equal "ok" (mistty-send-and-capture-command-output))) 144 | (should (equal (concat tramp-prefix "/home/") default-directory))))) 145 | 146 | (turtles-ert-deftest mistty-tramp-test-window-size () 147 | (let* ((tramp-methods (mistty-test-tramp-methods)) 148 | (tramp-prefix (mistty-test-tramp-prefix)) 149 | (home (file-name-directory "/")) 150 | (default-directory (concat tramp-prefix home))) 151 | (delete-other-windows) 152 | (mistty-with-test-buffer (:selected t) 153 | (let ((win (selected-window)) 154 | cols-before cols-after) 155 | (mistty--send-string mistty-proc "tput cols") 156 | (setq cols-before (string-to-number (mistty-send-and-capture-command-output))) 157 | 158 | (split-window-horizontally nil win) 159 | (mistty--window-size-change win) 160 | 161 | ;; Make sure the terminal has taken the size change into 162 | ;; account. 163 | (mistty-send-text "echo ok") 164 | (mistty-send-and-wait-for-prompt) 165 | 166 | ;; This makes sure the terminal is told about the window size 167 | ;; change. 168 | (mistty--send-string mistty-proc "tput cols") 169 | (setq cols-after (string-to-number (mistty-send-and-capture-command-output))) 170 | (should-not (equal cols-before cols-after)) 171 | (should (< cols-after cols-before)))))) 172 | 173 | (ert-deftest mistty-tramp-test-buffer-name () 174 | (let ((mistty-buffer-name '("mistty" mistty-buffer-name-user mistty-buffer-name-host))) 175 | (let ((default-directory "/ssh:someuser@test.example:/")) 176 | (should (equal "*mistty-someuser@test.example*" (mistty-new-buffer-name)))) 177 | (let ((default-directory "/ssh:test.example:/")) 178 | (should (equal "*mistty@test.example*" (mistty-new-buffer-name)))) 179 | (let ((default-directory "/sudo::/")) 180 | (should (equal "*mistty-root*" (mistty-new-buffer-name)))))) 181 | 182 | (ert-deftest mistty-tramp-test-set-EMACS () 183 | (setenv "EMACS" nil) ;; Sometimes set to run Eldev 184 | 185 | (let* ((tramp-methods (mistty-test-tramp-methods)) 186 | (tramp-prefix (mistty-test-tramp-prefix)) 187 | (home (file-name-directory "/")) 188 | (default-directory (concat tramp-prefix home)) 189 | (connection-local-profile-alist nil) 190 | (connection-local-criteria-alist nil) 191 | (mistty-set-EMACS nil)) 192 | 193 | (mistty-with-test-buffer () 194 | (mistty-send-text "if test -n \"$EMACS\"; then echo set; else echo unset; fi") 195 | (should (equal "unset" (mistty-send-and-capture-command-output)))) 196 | 197 | (connection-local-set-profile-variables 198 | 'test-profile 199 | '((mistty-set-EMACS . t))) 200 | (connection-local-set-profiles (mistty-test-tramp-protocol) 'test-profile) 201 | 202 | ;; This makes sure the connection-local setup above works. 203 | (should (equal t (with-connection-local-variables mistty-set-EMACS))) 204 | 205 | (mistty-with-test-buffer () 206 | (mistty-send-text "if test -n \"$EMACS\"; then echo set; else echo unset; fi") 207 | (should (equal "set" (mistty-send-and-capture-command-output)))))) 208 | -------------------------------------------------------------------------------- /mistty-accum-macros.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-accum-macros.el --- Macros for mistty-accum.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file defines macros built on top of mistty-accum.el so they 20 | ;; can be included with eval-when-compile and the macro code can 21 | ;; be left out of the compiled output. 22 | ;; 23 | ;; Usage: 24 | ;; (require 'mistty-accum) 25 | ;; (eval-when-compile (require 'mistty-accum-macros)) 26 | 27 | ;;; Code: 28 | 29 | (require 'rx) 30 | (require 'cl-lib) 31 | (require 'mistty-accum) 32 | 33 | (cl-defmacro mistty--accum-add-processor-lambda (accum (ctx rx-regexp) &rest body) 34 | "Define a processor for processing RX-REGEXP in ACCUM. 35 | 36 | BODY defines a lambda that takes as arguments CTX and any pcase-style 37 | let capture defined in RX-REGEXP. Except for the let capture, RX-REGEXP 38 | is written in the same language as `mistty--accum-add-processor'. 39 | 40 | For example, the following defines a processor for \"\\e[0-9]?J\" that 41 | makes the number available to the lamba as under the symbol num. 42 | 43 | \(mistty--accum-add-processor-lambda accum 44 | (ctx \\='(seq ESC (let num Pn) ?J)) 45 | ...)" 46 | (declare (indent 2)) 47 | (let ((rx-regexp (mistty--accum-unquote-rx-regexp rx-regexp))) 48 | `(mistty--accum-add-processor 49 | ,accum 50 | (quote ,(mistty--accum-strip-let rx-regexp)) 51 | (pcase-lambda (,ctx (rx ,(mistty--accum-expand-shortcuts rx-regexp))) 52 | ,@body)))) 53 | 54 | (defmacro mistty--accum-add-processor (accum rx-regexp processor) 55 | "Register PROCESSOR in ACCUM for processing RX-REGEXP. 56 | 57 | RX-REGEXP is a regexp in a restricted subset of the RX notation, 58 | which supports: 59 | (seq ...) 60 | (or ...) 61 | (? ...) 62 | (* ...) 63 | (+ ...) 64 | (char ...) 65 | (not (char ...)) 66 | char 67 | string 68 | 69 | In addition, the following notation shortcuts are supported, freely 70 | adapted from https://www.xfree86.org/current/ctlseqs.html#Definitions 71 | 72 | ESC \\e 73 | BEL \\a 74 | TAB \\t 75 | CR \\r 76 | LF \\n 77 | SP (Space) 78 | CSI ESC [ (Control Sequence Introducer ) 79 | OSC ESC ] (Operating System Command) 80 | DSC ESC P (Device Control String) 81 | Ps (Single optional numeric parameter) 82 | Pm (Multiple optional numeric parameters ;-separated) 83 | ST BEL | ESC \\ (String Terminator) 84 | 85 | RX-REGEXP must be a quoted list. 86 | 87 | PROCESSOR must be a function with signature (CTX STR). With CTX a 88 | `mistty--accum-ctx' instance and STR the terminal sequence that matched 89 | the regexp. The processor is executed with the process buffer as current 90 | buffer. 91 | 92 | If PROCESSOR does nothing, the terminal sequence matching REGEXP is 93 | simply swallowed. To forward or modify it, PROCESSOR must call 94 | `mistty--accum-ctx-push-down'. 95 | 96 | If PROCESSOR needs to check the state of the process buffer, it must 97 | first make sure that that state has been fully updated to take into 98 | account everything that was sent before the matching terminal sequence 99 | by calling `mistty--accum-ctx-flush'." 100 | (let* ((rx-regexp (mistty--accum-expand-shortcuts 101 | (mistty--accum-unquote-rx-regexp rx-regexp))) 102 | (regexp (rx-to-string rx-regexp 'no-group)) 103 | (hold-back (mistty--accum-build-hold-back rx-regexp))) 104 | ;; canary: check the regexps at macro expansion time, so any error 105 | ;; is thrown early with extra information. 106 | (condition-case err 107 | (string-match regexp "") 108 | (invalid-regexp 109 | (signal 'invalid-regexp 110 | (format "%s [%S]" err regexp)))) 111 | (let ((hold-back-str (mapconcat #'identity hold-back "\\|"))) 112 | (condition-case err 113 | (string-match hold-back-str "") 114 | (invalid-regexp 115 | (signal 'invalid-regexp 116 | (format "%s [%S]" err hold-back-str))))) 117 | 118 | `(mistty--accum-add-processor-1 119 | ,accum 120 | (mistty--accum-make-processor 121 | :regexp ,regexp 122 | :hold-back-regexps (list ,@hold-back) 123 | :func ,processor)))) 124 | 125 | (defun mistty--accum-unquote-rx-regexp (arg) 126 | "Check the type of ARG and return it unquoted. 127 | 128 | The returned type is something that can be passed to 129 | `mistty--accum-expand-shorts' then `mistty--accum-build-hold-back'." 130 | (pcase arg 131 | (`(quote ,elt) elt) 132 | ((or (pred stringp) (pred characterp)) arg) 133 | (_ (error "Macro expected quoted RX notation regexp, not %s" 134 | arg)))) 135 | 136 | (defun mistty--accum-strip-let (tree) 137 | "In-place removal of (let var rx-tree) from TREE." 138 | (pcase tree 139 | (`(let ,_ ,arg) 140 | (mistty--accum-strip-let arg)) 141 | (`(,op . ,args) 142 | (cons 143 | op (mapcar #'mistty--accum-strip-let args))) 144 | (_ tree))) 145 | 146 | (defun mistty--accum-expand-shortcuts (tree) 147 | "Expand special shortcuts into base RX-notation. 148 | 149 | TREE might include notations inspired from 150 | https://www.xfree86.org/current/ctlseqs.html#Definitions 151 | 152 | The notation is documented in `mistty--accum-add-processor'. 153 | 154 | A transformed version of TREE is returned." 155 | (pcase tree 156 | (`(,op . ,args) 157 | (cons 158 | op (mapcar #'mistty--accum-expand-shortcuts args))) 159 | ('ESC ?\e) 160 | ('BEL ?\a) 161 | ('CSI '(seq ?\e ?\[)) 162 | ('OSC '(seq ?\e ?\])) 163 | ('DCS '(seq ?\e ?P)) 164 | ('ST '(or ?\a (seq ?\e ?\\))) 165 | ('SP ?\ ) 166 | ('TAB ?\t) 167 | ('CR ?\r) 168 | ('LF ?\n) 169 | ('Ps '(* (char "0-9"))) 170 | ('Pm '(* (char "0-9;"))) 171 | 172 | ;; Note on Pt: ECMA 48 8.3.89 only allows 0x08-0x0d 0x20-7e. That 173 | ;; would disallow all non-US-ASCII characters, often used in file 174 | ;; names, which would then need to be encoded. This would be 175 | ;; inconvenient and error-prone, so we disallow the US-ASCII 176 | ;; characters disallowed by ECMA 48 and allow all non-US-ASCII 177 | ;; chars (usually multibyte UTF-8). 178 | ('Pt '(* (not (char "\x00-\x07\x0e-\x1f\x7f")))) 179 | (_ tree))) 180 | 181 | (defun mistty--accum-build-hold-back (tree) 182 | "Build a set of hold-back regexps for TREE. 183 | 184 | TREE must be written in a limited subset of the RX notation, documented 185 | in `mistty--accum-add-processor'. 186 | 187 | TREE must have been transformed with `mistty--accum-expand-shortcuts' first." 188 | (let ((collect (list ""))) 189 | (cl-labels 190 | ((collect (tree) 191 | (pcase tree 192 | (`(seq . ,sub-trees) 193 | ;; Collect partial sequences into sub. 194 | (dolist (sub sub-trees) 195 | (collect sub))) 196 | 197 | (`(? ,sub) 198 | ;; The case without sub is already in collect. 199 | (collect sub)) 200 | 201 | (`(or . ,subs) 202 | (let ((start collect) 203 | (subcollects nil)) 204 | 205 | (dolist (sub subs) 206 | ;; match an incomplete sub-rx tree 207 | (setq collect (list (car start))) 208 | (collect sub) 209 | (push (trim-list collect) subcollects)) 210 | 211 | (setq collect start) 212 | (dolist (subcollect subcollects) 213 | (addall subcollect)) 214 | 215 | ;; match a complete a or a complete b 216 | (addone 217 | (concat (car start) 218 | (rx-to-string tree))))) 219 | 220 | ((or `(* ,sub) `(+ ,sub)) 221 | ;; The case without sub is already in collect, add the 222 | ;; case with one or more sub, followed optionally by one 223 | ;; partial sub. 224 | (addone 225 | (concat (car collect) 226 | (rx-to-string (list (car tree) sub) 'no-group))) 227 | 228 | (collect sub) 229 | ;; Remove the last where sub is complete; this is already 230 | ;; covered by (+ sub) 231 | (pop collect)) 232 | 233 | ((and (pred characterp) c) 234 | ;; The case without the character is already in collect. 235 | ;; Add the case with the character. 236 | (addone 237 | (concat (car collect) (rx-to-string c)))) 238 | 239 | ((and (pred stringp) str) 240 | (cl-loop for c across str 241 | do (collect c))) 242 | 243 | (`(char ,set) 244 | ;; The case without the character set is already in 245 | ;; collect. Add the case with the character set. 246 | (addone 247 | (concat (car collect) "[" set "]"))) 248 | 249 | (`(not (char ,set)) 250 | ;; The case without the character set is already in 251 | ;; collect. Add the case with the character set. 252 | (addone 253 | (concat (car collect) "[^" set "]"))) 254 | 255 | (_ (error "Unsupported RX notation: %s" tree)))) 256 | (addall (lst) 257 | (dolist (elt lst) 258 | (addone elt))) 259 | (addone (elt) 260 | (cl-pushnew elt collect :test #'string=)) 261 | (trim-list (lst) 262 | (cdr (butlast lst)))) 263 | (collect tree) 264 | (pop collect) 265 | (cdr (nreverse collect))))) 266 | 267 | (provide 'mistty-accum-macros) 268 | 269 | ;;; mistty-accum-macros.el ends here 270 | -------------------------------------------------------------------------------- /mistty-changeset.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-changeset.el --- Accumulate and replays changes -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file contains helper utilities for mistty.el for collecting 20 | ;; buffer modifications and forward them to the terminal. 21 | ;; 22 | ;; It defines `mistty--changesets' a list of `mistty--changeset' 23 | ;; struct instances. 24 | ;; 25 | ;; Each `mistty--changeset' tracks of a set of buffer modifications to 26 | ;; be replayed, some of which might be kept in the buffer as text with 27 | ;; specific text properties. Such changes kept in the buffer are 28 | ;; linked to the active changeset, returned by 29 | ;; `mistty--active-changeset'. 30 | ;; 31 | ;; New buffer modifications are reported and linked to the active 32 | ;; changeset by `mistty-changeset-mark-region'. They are then kept in 33 | ;; the buffer until they're collected and eventually transformed into 34 | ;; a set of modifications to be replayed by 35 | ;; `mistty--changeset-modifications'. 36 | 37 | (require 'generator) 38 | 39 | ;;; Code: 40 | 41 | (eval-when-compile 42 | (require 'cl-lib)) 43 | 44 | (require 'mistty-util) 45 | 46 | (defvar-local mistty--changesets nil 47 | "Set of active changes, of type mistty--changeset. 48 | 49 | The first change is the most recent one, which might be active. 50 | Use `mistty--active-changeset' to access it.") 51 | 52 | (cl-defstruct (mistty--changeset 53 | (:constructor mistty--make-changeset) 54 | (:conc-name mistty--changeset-) 55 | (:copier nil)) 56 | ;; beginning of the changed region in the work buffer. 57 | beg 58 | ;; end of the changed region in the work buffer. 59 | end 60 | ;; True if the end of work buffer was truncated. 61 | deleted-point-max 62 | 63 | ;; Modification intervals collected from the buffer. As long as this 64 | ;; is nil, details about the modifications are stored in the buffer, 65 | ;; as text properties. 66 | intervals) 67 | 68 | (defsubst mistty--changeset-collected (changeset) 69 | "Evaluate to a true value if CHANGESET intervals exist. 70 | 71 | Calling `mistty-changeset-collect' guarantees this to be true." 72 | (mistty--changeset-intervals changeset)) 73 | 74 | (defun mistty--activate-changeset () 75 | "Create a new active changeset. 76 | 77 | Returns the changeset." 78 | (let ((changeset (mistty--active-changeset))) 79 | (unless changeset 80 | (setq changeset (mistty--make-changeset)) 81 | (push changeset mistty--changesets)) 82 | changeset)) 83 | 84 | (defun mistty--release-changeset (changeset) 85 | "Remove CHANGESET from `mistty-changeset'. 86 | 87 | This function also cleans up any change information left in the 88 | work buffer." 89 | (when (and (mistty--changeset-beg changeset) 90 | (not (mistty--changeset-collected changeset)) 91 | (< (mistty--changeset-beg changeset) (point-max))) 92 | (remove-text-properties (mistty--changeset-beg changeset) 93 | (point-max) '(mistty-change t))) 94 | (setq mistty--changesets (delq changeset mistty--changesets))) 95 | 96 | (defun mistty--active-changeset () 97 | "Return the active changeset or nil. 98 | 99 | The active changeset is the first changeset on 100 | `mistty--changesets' and hasn't been collected yet." 101 | (let ((head (car mistty--changesets))) 102 | (when 103 | (and head 104 | (not (mistty--changeset-collected head))) 105 | head))) 106 | 107 | (defun mistty--changeset-mark-region (changeset beg end old-end) 108 | "Store insertion and deletion for CHANGESET into buffer properties. 109 | 110 | Creates and activate a changeset as necessary. 111 | 112 | BEG to END reports a newly inserted string, BEG to OLD-END a 113 | recently deleted string." 114 | (let ((beg (mistty--safe-pos beg)) 115 | (end (mistty--safe-pos end))) 116 | (setf (mistty--changeset-beg changeset) 117 | (if-let* ((changeset-beg (mistty--changeset-beg changeset))) 118 | (min (mistty--safe-pos changeset-beg) beg) 119 | beg)) 120 | (setf (mistty--changeset-end changeset) 121 | (if-let* ((changeset-end (mistty--changeset-end changeset))) 122 | (max (mistty--safe-pos changeset-end) end) 123 | end)) 124 | 125 | ;; Mark the text that was inserted 126 | (when (> end beg) 127 | (put-text-property beg end 'mistty-change '(inserted))) 128 | 129 | ;; Update the shift value of everything that comes after. 130 | (let ((shift (- old-end end)) 131 | (pos end)) 132 | (while (< pos (point-max)) 133 | (let ((next-pos (next-single-property-change pos 'mistty-change (current-buffer) (point-max)))) 134 | (when (> next-pos pos) 135 | (pcase (get-text-property pos 'mistty-change) 136 | (`(shift ,old-shift) 137 | (put-text-property pos next-pos 'mistty-change `(shift ,(+ old-shift shift)))) 138 | ('() (put-text-property pos next-pos 'mistty-change `(shift ,shift))))) 139 | (setq pos next-pos))) 140 | (when (and (> old-end beg) (= end (point-max))) 141 | (setf (mistty--changeset-deleted-point-max changeset) t))))) 142 | 143 | (defun mistty--changeset-single-insert (cs) 144 | "If CS is just a single insertion, return it. 145 | 146 | Otherwise return nil." 147 | (pcase (mistty--changeset-collect cs) 148 | (`((,pos1 inserted) (,pos2 shift ,_)) 149 | (mistty--safe-bufstring pos1 pos2)) 150 | (`((,pos1 inserted)) 151 | (mistty--safe-bufstring pos1 (point-max))))) 152 | 153 | (defun mistty--changeset-collect (changeset) 154 | "Extract CHANGESET data stored into text properties. 155 | 156 | The second time this is called, this just returns 157 | `mistty--changeset-intervals'." 158 | (when (not (mistty--changeset-intervals changeset)) 159 | (save-excursion 160 | (save-restriction 161 | (narrow-to-region (min (point-max) (mistty--changeset-beg changeset)) (point-max)) 162 | (let ((last-point (point-min)) 163 | intervals last-at-point ) 164 | (goto-char last-point) 165 | (while (let ((at-point (get-text-property (point) 'mistty-change))) 166 | (when last-at-point 167 | (push `(,last-point . ,last-at-point) intervals)) 168 | (setq last-at-point at-point) 169 | (setq last-point (point)) 170 | (goto-char (next-single-property-change (point) 'mistty-change (current-buffer) (point-max))) 171 | (not (eobp)))) 172 | (when last-at-point 173 | (push `(,last-point . ,last-at-point) intervals)) 174 | (when (mistty--changeset-deleted-point-max changeset) 175 | (push `(,(point-max) deleted-to-end) intervals)) 176 | (let ((inhibit-read-only t) 177 | (inhibit-modification-hooks t)) 178 | (remove-text-properties (point-min) (point-max) '(mistty-change t))) 179 | (setf (mistty--changeset-intervals changeset) (nreverse intervals)))))) 180 | (mistty--changeset-intervals changeset)) 181 | 182 | (defun mistty--changeset-restrict (changeset min-pos) 183 | "Restrict the content of CHANGESET to the range [MIN-POS,]. 184 | 185 | The function returns the difference between the work buffer and 186 | term buffer at MIN-POS (shift), or nil if a restriction isn't 187 | possible after MIN-POS." 188 | (let ((intervals (mistty--changeset-collect changeset))) 189 | (if (and (caar intervals) (>= (caar intervals) min-pos)) 190 | 0 ;; shift is 0, intervals don't change. 191 | 192 | ;; apply restrictions 193 | (while (pcase intervals 194 | ((and `((,_ . ,_ ) (,pos2 . ,_) . ,_) (guard (<= pos2 min-pos))) 195 | (setq intervals (cdr intervals)) 196 | t))) 197 | 198 | ;; intervals now points to the first relevant section, which likely 199 | ;; starts before min-pos. 200 | (let ((base-shift 201 | (pcase intervals 202 | (`((,_ shift ,shift) . ,_) shift) 203 | (`((,_ inserted) (,pos2 shift ,shift) . ,_) 204 | (+ shift (- pos2 min-pos))) 205 | (_ ;; other interval restrictions aren't supported 206 | nil)))) 207 | (when (and base-shift intervals) 208 | (setcar (car intervals) min-pos) 209 | 210 | ;; shifts must be relative to base-shift 211 | (setq intervals 212 | (mapcar 213 | (lambda (cur) 214 | (pcase cur 215 | (`(,pos shift ,shift) `(,pos shift ,(- shift base-shift))) 216 | (_ cur))) 217 | intervals)) 218 | (setf (mistty--changeset-intervals changeset) intervals) 219 | base-shift))))) 220 | 221 | (defun mistty--changeset-modifications (changeset) 222 | "Return modifications to re-apply for CHANGESET. 223 | 224 | This function returns a list of (beg content old-length), with beg 225 | the beginning position, content text that\'s inserted at beg and 226 | old-length the length of text deleted from beg. old-length might 227 | be -1 to mean delete everything from pos to the end of the 228 | buffer." 229 | (let ((intervals (mistty--changeset-collect changeset)) 230 | (changes nil) 231 | (last-shift 0)) 232 | (while intervals 233 | (pcase intervals 234 | ;; insert in the middle, possibly replacing a section of text 235 | (`((,start inserted) (,end shift ,end-shift) . ,_) 236 | (push (list (+ start last-shift) 237 | (mistty--safe-bufstring start end) 238 | (- (+ end end-shift) (+ start last-shift))) 239 | changes) 240 | ;; processed 2 entries this loop, instead of just 1 241 | (setq intervals (cdr intervals))) 242 | 243 | ;; insert at end, delete everything after 244 | (`((,start inserted) (,end deleted-to-end)) 245 | (push (list (+ start last-shift) 246 | (mistty--safe-bufstring start end) 247 | -1) 248 | changes) 249 | ;; processed 2 entries this loop, instead of just 1 250 | (setq intervals (cdr intervals))) 251 | 252 | ;; insert at end 253 | (`((,start inserted)) 254 | (push (list (+ start last-shift) 255 | (mistty--safe-bufstring start (point-max)) 256 | 0) 257 | changes)) 258 | 259 | ;; delete a section of original text 260 | ((and `((,pos shift ,shift) . ,_) 261 | (guard (> shift last-shift))) 262 | (push (list (+ pos last-shift) 263 | "" 264 | (- shift last-shift)) 265 | changes)) 266 | 267 | ;; delete to the end of the original text 268 | (`((,pos deleted-to-end)) 269 | (push (list (+ pos last-shift) "" -1) 270 | changes))) 271 | 272 | ;; prepare for next loop 273 | (pcase (car intervals) 274 | (`(,_ shift ,shift) (setq last-shift shift))) 275 | (setq intervals (cdr intervals))) 276 | changes)) 277 | 278 | (provide 'mistty-changeset) 279 | 280 | ;;; mistty-changeset.el ends here 281 | -------------------------------------------------------------------------------- /test/mistty-term-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-term.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'mistty-term) 18 | (require 'ert) 19 | (require 'ert-x) 20 | 21 | (ert-deftest mistty-term-translate-key () 22 | (should (equal "a" (mistty-translate-key (kbd "a") 1))) 23 | (should (equal "aaa" (mistty-translate-key (kbd "a") 3))) 24 | 25 | (should (equal "\C-a" (mistty-translate-key (kbd "C-a") 1))) 26 | 27 | (should (equal "\ea" (mistty-translate-key (kbd "M-a") 1))) 28 | (should (equal "\ea\ea\ea" (mistty-translate-key (kbd "M-a") 3))) 29 | 30 | (should (equal mistty-left-str (mistty-translate-key (kbd "") 1))) 31 | (should (equal mistty-right-str (mistty-translate-key (kbd "") 1))) 32 | 33 | (should (equal mistty-up-str (mistty-translate-key (kbd "") 1))) 34 | (should (equal mistty-down-str (mistty-translate-key (kbd "") 1)))) 35 | 36 | (ert-deftest mistty-test-postprocess-indent-and-end () 37 | (ert-with-test-buffer () 38 | (insert (concat "$ for i in a b c " (propertize " " 'mistty-maybe-skip t) "\n")) 39 | (insert (concat (propertize " " 'mistty-maybe-skip t) "echo ok " (propertize " " 'mistty-maybe-skip t) "\n")) 40 | (insert (concat "end" (propertize " " 'mistty-maybe-skip t))) 41 | 42 | (mistty--term-postprocess (point-min) 80) 43 | 44 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'right-prompt)) 45 | (should (equal (concat "$ for i in a b c\n" 46 | "[ ]echo ok\n" 47 | "end") 48 | (mistty-test-content :show-property '(mistty-skip indent)))) 49 | (should (equal (concat "$ for i in a b c [ ]\n" 50 | " echo ok [ ]\n" 51 | "end[ ]") 52 | (mistty-test-content :show-property '(mistty-skip trailing)))))) 53 | 54 | (ert-deftest mistty-test-postprocess-indent-empty-lines () 55 | (ert-with-test-buffer () 56 | (insert "$ for i in a b c\n") 57 | (insert (concat (propertize " " 'mistty-maybe-skip t) "\n")) 58 | (insert (concat (propertize " " 'mistty-maybe-skip t) "echo foo\n")) 59 | (insert (concat (propertize "" 'mistty-maybe-skip t) "\n")) 60 | (insert (concat (propertize " " 'mistty-maybe-skip t) "echo bar\n")) 61 | (insert (concat (propertize " " 'mistty-maybe-skip t) "\n")) 62 | (insert (concat (propertize " " 'mistty-maybe-skip t) "\n")) 63 | (insert (concat "end" (propertize " " 'mistty-maybe-skip t))) 64 | 65 | (mistty--term-postprocess (point-min) 80) 66 | 67 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'right-prompt)) 68 | (should (equal (concat "$ for i in a b c\n" 69 | "\n" 70 | "[ ]echo foo\n" 71 | "\n" 72 | "[ ]echo bar\n" 73 | "[ ]\n" 74 | "[ ]\n" 75 | "end") 76 | (mistty-test-content :show-property '(mistty-skip indent)))) 77 | (should (equal (concat "$ for i in a b c\n" 78 | "[ ]\n" 79 | " echo foo\n" 80 | "\n" 81 | " echo bar\n" 82 | " [ ]\n" 83 | " [ ]\n" 84 | "end[ ]") 85 | (mistty-test-content :show-property '(mistty-skip trailing)))))) 86 | 87 | (ert-deftest mistty-test-postprocess-ignore-skip-in-the-middle () 88 | (ert-with-test-buffer () 89 | (insert (concat "$ echo " (propertize " " 'mistty-maybe-skip t) "ok " (propertize " " 'mistty-maybe-skip t) "\n")) 90 | 91 | (mistty--term-postprocess (point-min) 80) 92 | 93 | (should (equal "$ echo ok [ ]" 94 | (mistty-test-content :show-property '(mistty-skip trailing)))))) 95 | 96 | (ert-deftest mistty-test-postprocess-ignore-nonws () 97 | (ert-with-test-buffer () 98 | (insert (propertize "$ echo foo bar" 'mistty-maybe-skip t)) 99 | 100 | (mistty--term-postprocess (point-min) 80) 101 | 102 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'indent)) 103 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'right-prompt)) 104 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'trailing)))) 105 | 106 | (ert-deftest mistty-test-postprocess-right-prompt () 107 | (ert-with-test-buffer () 108 | (select-window (display-buffer (current-buffer))) 109 | (delete-other-windows) 110 | 111 | (let* ((w 80) 112 | (left-prompt " left > ") 113 | (right-prompt " < right ") 114 | (spaces (- w (length left-prompt) (length right-prompt)))) 115 | (insert left-prompt) 116 | (insert (propertize (make-string spaces ?\ ) 'mistty-maybe-skip t)) 117 | (insert right-prompt) 118 | (should (= (current-column) w)) 119 | (insert "\n") 120 | 121 | (mistty--term-postprocess (point-min) w)) 122 | 123 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'indent)) 124 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'trailing)) 125 | (should (string-match "^ left > \\[ + < right \\]$" 126 | (mistty-test-content :show-property '(mistty-skip right-prompt)))))) 127 | 128 | (ert-deftest mistty-test-postprocess-right-prompt-with-tolerance () 129 | (ert-with-test-buffer () 130 | (select-window (display-buffer (current-buffer))) 131 | (delete-other-windows) 132 | 133 | (let* ((w 80) 134 | (left-prompt " left > ") 135 | (right-prompt " < right ") 136 | (spaces (- w (length left-prompt) (length right-prompt) 2))) 137 | (insert left-prompt) 138 | (insert (propertize (make-string spaces ?\ ) 'mistty-maybe-skip t)) 139 | (insert right-prompt) 140 | (insert "\n") 141 | 142 | (mistty--term-postprocess (point-min) w)) 143 | 144 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'indent)) 145 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'trailing)) 146 | (should (string-match "^ left > \\[ + < right \\]$" 147 | (mistty-test-content :show-property '(mistty-skip right-prompt)))))) 148 | 149 | (ert-deftest mistty-test-postprocess-empty-right-prompt () 150 | (ert-with-test-buffer () 151 | (select-window (display-buffer (current-buffer))) 152 | (delete-other-windows) 153 | 154 | (let* ((w 80) 155 | (right-prompt " < right ") 156 | (spaces (- w (length right-prompt)))) 157 | (insert (propertize (make-string spaces ?\ ) 'mistty-maybe-skip t)) 158 | (insert right-prompt) 159 | (should (= (current-column) w)) 160 | (insert "\n") 161 | 162 | (mistty--term-postprocess (point-min) w)) 163 | 164 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'indent)) 165 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'trailing)) 166 | (should (string-match "^\\[ + < right \\]$" 167 | (mistty-test-content :show-property '(mistty-skip right-prompt)))))) 168 | 169 | (ert-deftest mistty-test-postprocess-ipython-continue-prompt () 170 | (ert-with-test-buffer () 171 | (insert (concat "In [3]: for i in (1, 2, 3):" (propertize " " 'mistty-maybe-skip t) "\n")) 172 | (insert (concat " ...: if i > 1: " (propertize " " 'mistty-maybe-skip t) "\n")) 173 | (insert (concat " ...: print(i) " (propertize " " 'mistty-maybe-skip t) "\n")) 174 | (insert (concat "In [133]: for i in (1, 2, 3):\n")) 175 | (insert (concat " ...: print(i)\n")) 176 | 177 | (mistty--term-postprocess (point-min) 80) 178 | 179 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'right-prompt)) 180 | (should-not (text-property-any (point-min) (point-max) 'mistty-skip 'indent)) 181 | (should (equal 182 | (concat "In [3]: for i in (1, 2, 3):\n" 183 | "[ ...: ] if i > 1:\n" 184 | "[ ...: ] print(i)\n" 185 | "In [133]: for i in (1, 2, 3):\n" 186 | "[ ...: ] print(i)") 187 | (mistty-test-content :show-property '(mistty-skip continue-prompt)))))) 188 | 189 | (ert-deftest mistty-test-bridge-ws-with-props () 190 | (ert-with-test-buffer () 191 | (term-mode) 192 | (add-hook 'after-change-functions #'mistty--after-change-on-term nil t) 193 | (insert " \n\n") 194 | (goto-char (point-min)) 195 | (insert " ") 196 | (mistty-register-text-properties 'test '(myprop 1)) 197 | (insert "foo") 198 | (goto-char (1- (point-max))) 199 | (insert "bar") 200 | 201 | (should (equal " [foo \nbar]\n" 202 | (mistty-test-content :show-property '(myprop 1) :trim nil))))) 203 | 204 | (ert-deftest mistty-prompt-contains-open-ended () 205 | (let ((mistty--prompt-cell (mistty--make-prompt-cell))) 206 | (should (mistty--prompt-contains (mistty--make-prompt 'test 10) 10)) 207 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10) 9)) 208 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10) 1)) 209 | (should (mistty--prompt-contains (mistty--make-prompt 'test 10) 11)) 210 | (should (mistty--prompt-contains (mistty--make-prompt 'test 10) 100)))) 211 | 212 | (ert-deftest mistty-prompt-contains-closed () 213 | (let ((mistty--prompt-cell (mistty--make-prompt-cell))) 214 | (should (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 10)) 215 | (should (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 11)) 216 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 9)) 217 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 12)) 218 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 1)) 219 | (should-not (mistty--prompt-contains (mistty--make-prompt 'test 10 12) 100)))) 220 | 221 | (ert-deftest mistty-add-to-prompt-archive () 222 | (let ((mistty--prompt-cell (mistty--make-prompt-cell))) 223 | (setf (mistty--prompt) (mistty--make-prompt 'test 1)) 224 | (setf (mistty--prompt) (mistty--make-prompt 'test 2)) 225 | (setf (mistty--prompt) (mistty--make-prompt 'test 3)) 226 | (should (equal 3 (mistty--prompt-start (mistty--prompt)))) 227 | (should (equal '(2 1) (mapcar #'mistty--prompt-start (mistty--prompt-archive)))) 228 | 229 | (should (equal (mistty--prompt-cell-current mistty--prompt-cell) 230 | (mistty--prompt))) 231 | (should (equal (mistty--prompt-cell-archive mistty--prompt-cell) 232 | (mistty--prompt-archive))) 233 | (should (equal (mistty--prompt-cell-counter mistty--prompt-cell) 234 | 3)) 235 | 236 | (setf (mistty--prompt) nil) 237 | (should (null (mistty--prompt))) 238 | (should (equal '(3 2 1) (mapcar #'mistty--prompt-start (mistty--prompt-archive)))))) 239 | 240 | (ert-deftest mistty-set-prompt-archive () 241 | (let ((mistty--prompt-cell (mistty--make-prompt-cell))) 242 | (setf (mistty--prompt-archive) (list (mistty--make-prompt 'test 1) 243 | (mistty--make-prompt 'test 2))) 244 | (should (equal '(1 2) (mapcar #'mistty--prompt-start (mistty--prompt-archive)))) 245 | (setf (mistty--prompt-archive) nil) 246 | (should (null (mistty--prompt-archive))))) 247 | -------------------------------------------------------------------------------- /test/mistty-changeset-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-changeset.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'ert) 18 | (require 'ert-x) 19 | (eval-when-compile 20 | (require 'cl-lib)) 21 | 22 | (require 'mistty) 23 | (require 'mistty-testing) 24 | (require 'mistty-changeset) 25 | 26 | (ert-deftest mistty-changeset-test-collect-modifications-delete-after-replace () 27 | (ert-with-test-buffer () 28 | (insert "$ ") 29 | (setq mistty-sync-marker (point)) 30 | 31 | (insert "abcdefghijklmno<>") 32 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 33 | 34 | (delete-region 6 9) 35 | (goto-char 6) 36 | (insert "new-value") 37 | 38 | (delete-region 18 21) 39 | 40 | (should (equal "$ abcnew-valueghimno<>" (buffer-substring-no-properties (point-min) (point-max)))) 41 | 42 | ;; The deletion is reported first, even though it was applied 43 | ;; last. If we did the reverse and a newline was inserted in the 44 | ;; middle of new-value, the deletion would not apply to the right 45 | ;; region. 46 | (should (equal '((12 "" 3) (6 "new-value" 3)) (mistty--changeset-modifications (mistty--active-changeset)))))) 47 | 48 | (ert-deftest mistty-changeset-test-collect-modifications-delete-at-end () 49 | (ert-with-test-buffer () 50 | (insert "$ ") 51 | (setq mistty-sync-marker (point)) 52 | 53 | (insert "abcdefghijklmno<>") 54 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 55 | 56 | (delete-region 6 (point-max)) 57 | 58 | (should (equal "$ abc" (buffer-substring-no-properties (point-min) (point-max)))) 59 | 60 | (should (equal '((6 "" -1)) (mistty--changeset-modifications (mistty--active-changeset)))))) 61 | 62 | (ert-deftest mistty-changeset-test-collect-modifications-insert-then-delete-at-end () 63 | (ert-with-test-buffer () 64 | (insert "$ ") 65 | (setq mistty-sync-marker (point)) 66 | 67 | (insert "abcdefghijklmno<>") 68 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 69 | 70 | (delete-region 6 (point-max)) 71 | (goto-char 6) 72 | (insert "new-value") 73 | 74 | (should (equal "$ abcnew-value" (buffer-substring-no-properties (point-min) (point-max)))) 75 | 76 | (should (equal '((6 "new-value" -1)) (mistty--changeset-modifications (mistty--active-changeset)))))) 77 | 78 | (ert-deftest mistty-changeset-test-collect-modifications-insert-skip-then-delete-at-end () 79 | (ert-with-test-buffer () 80 | (insert "$ ") 81 | (setq mistty-sync-marker (point)) 82 | 83 | (insert "abcdefghijklmno<>") 84 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 85 | 86 | (delete-region 15 (point-max)) 87 | (delete-region 9 12) 88 | (goto-char 6) 89 | (insert "new-value") 90 | 91 | (should (equal "$ abcnew-valuedefjkl" (buffer-substring-no-properties (point-min) (point-max)))) 92 | 93 | (should (equal '((15 "" -1) (9 "" 3) (6 "new-value" 0)) (mistty--changeset-modifications (mistty--active-changeset)))))) 94 | 95 | (ert-deftest mistty-changeset-test-collect-modifications-inserts () 96 | (ert-with-test-buffer () 97 | (insert "$ ") 98 | (setq mistty-sync-marker (point)) 99 | 100 | (insert "abcdefghijklmno<>") 101 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 102 | 103 | (goto-char 12) 104 | (insert "NEW") 105 | 106 | (goto-char 9) 107 | (insert "NEW") 108 | 109 | (goto-char 6) 110 | (insert "NEW") 111 | 112 | (should (equal "$ abcNEWdefNEWghiNEWjklmno<>" (buffer-substring-no-properties (point-min) (point-max)))) 113 | 114 | (should (equal '((12 "NEW" 0) (9 "NEW" 0) (6 "NEW" 0)) (mistty--changeset-modifications (mistty--active-changeset)))))) 115 | 116 | (ert-deftest mistty-changeset-test-collect-modifications-insert-at-end () 117 | (ert-with-test-buffer () 118 | (insert "$ ") 119 | (setq mistty-sync-marker (point)) 120 | 121 | (insert "abcdef") 122 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 123 | 124 | (goto-char 9) 125 | (insert "NEW") 126 | 127 | (should (equal "$ abcdefNEW" (buffer-substring-no-properties (point-min) (point-max)))) 128 | 129 | (should (equal '((9 "NEW" 0)) (mistty--changeset-modifications (mistty--active-changeset)))))) 130 | 131 | (ert-deftest mistty-changeset-test-collect-modifications-replaces () 132 | (ert-with-test-buffer () 133 | (insert "$ ") 134 | (setq mistty-sync-marker (point)) 135 | 136 | (insert "abcdefghijklmno<>") 137 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 138 | 139 | (goto-char 12) 140 | (delete-region 12 15) 141 | (insert "NEW") 142 | 143 | (goto-char 6) 144 | (delete-region 6 9) 145 | (insert "NEW") 146 | 147 | (should (equal "$ abcNEWghiNEWmno<>" (buffer-substring-no-properties (point-min) (point-max)))) 148 | 149 | (should (equal '((12 "NEW" 3) (6 "NEW" 3)) (mistty--changeset-modifications (mistty--active-changeset)))))) 150 | 151 | (ert-deftest mistty-changeset-test-restrict-intervals () 152 | (ert-with-test-buffer () 153 | (insert "$ ") 154 | (setq mistty-sync-marker (point)) 155 | 156 | (insert "abcdefghijklmno<>") 157 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 158 | 159 | (delete-region 6 9) 160 | (goto-char 6) 161 | (insert "new-value") 162 | 163 | (delete-region 18 21) 164 | 165 | (should (equal "$ abcnew-valueghimno<>" (buffer-substring-no-properties (point-min) (point-max)))) 166 | ;; "$ abcdefg hijklmno<>" 167 | (let ((cs (mistty--active-changeset))) 168 | (should (equal '((6 inserted) 169 | (15 shift -6) 170 | (18 shift -3)) (mistty--changeset-collect cs))) 171 | (should (equal -6 (mistty--changeset-restrict cs 16))) 172 | (should (equal '((16 shift 0) (18 shift 3)) (mistty--changeset-intervals cs))) 173 | (should (equal '((18 "" 3)) (mistty--changeset-modifications cs)))))) 174 | 175 | (ert-deftest mistty-changeset-test-restrict-intervals-before-changes () 176 | (ert-with-test-buffer () 177 | (insert "$ ") 178 | (setq mistty-sync-marker (point)) 179 | 180 | (insert "abcd<>") 181 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 182 | 183 | (goto-char 6) 184 | (insert "new-value") 185 | 186 | (should (equal "$ abcnew-valued<>" (buffer-substring-no-properties (point-min) (point-max)))) 187 | (let ((cs (mistty--active-changeset))) 188 | (should (equal '((6 inserted) (15 shift -9)) (mistty--changeset-collect cs))) 189 | (should (equal 0 (mistty--changeset-restrict cs 4))) 190 | (should (equal '((6 inserted) (15 shift -9)) (mistty--changeset-intervals cs)))))) 191 | 192 | (ert-deftest mistty-changeset-test-restrict-intervals-exactly-before-insert () 193 | (ert-with-test-buffer () 194 | (insert "$ ") 195 | (setq mistty-sync-marker (point)) 196 | 197 | (insert "abcd<>") 198 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 199 | 200 | (delete-region 6 7) 201 | (goto-char 6) 202 | (insert "new-value") 203 | 204 | (should (equal "$ abcnew-value<>" (buffer-substring-no-properties (point-min) (point-max)))) 205 | (let ((cs (mistty--active-changeset))) 206 | (should (equal '((6 inserted) 207 | (15 shift -8)) (mistty--changeset-collect cs))) 208 | (should (equal 0 (mistty--changeset-restrict cs 6))) 209 | (should (equal '((6 inserted) (15 shift -8)) (mistty--changeset-intervals cs)))))) 210 | 211 | (ert-deftest mistty-changeset-test-restrict-intervals-exactly-before-shift () 212 | (ert-with-test-buffer () 213 | (insert "$ ") 214 | (setq mistty-sync-marker (point)) 215 | 216 | (insert "abcd<>") 217 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 218 | 219 | (delete-region 6 7) 220 | 221 | (should (equal "$ abc<>" (buffer-substring-no-properties (point-min) (point-max)))) 222 | (let ((cs (mistty--active-changeset))) 223 | (should (equal '((6 shift 1)) (mistty--changeset-collect cs))) 224 | (should (equal 0 (mistty--changeset-restrict cs 6))) 225 | (should (equal '((6 shift 1)) (mistty--changeset-intervals cs)))))) 226 | 227 | (ert-deftest mistty-changeset-test-restrict-intervals-starts-within-insert () 228 | (ert-with-test-buffer () 229 | (insert "$ ") 230 | (setq mistty-sync-marker (point)) 231 | 232 | (insert "abcdefghijklmno<>") 233 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 234 | 235 | (delete-region 6 9) 236 | (goto-char 6) 237 | (insert "new-value") 238 | 239 | (delete-region 18 21) 240 | 241 | (should (equal "$ abcnew-valueghimno<>" (buffer-substring-no-properties (point-min) (point-max)))) 242 | ;; "$ abcdef ghijklmno<>" 243 | (let ((cs (mistty--active-changeset))) 244 | (should (equal '((6 inserted) (15 shift -6) (18 shift -3)) (mistty--changeset-collect cs))) 245 | (should (equal -1 (mistty--changeset-restrict cs 10))) 246 | (should (equal '((10 inserted) (15 shift -5) (18 shift -2)) (mistty--changeset-intervals cs))) 247 | (should (equal '((13 "" 3) (10 "value" 0)) (mistty--changeset-modifications cs)))))) 248 | 249 | (ert-deftest mistty-changeset-test-restrict-intervals-starts-within-insert-at-end () 250 | (ert-with-test-buffer () 251 | (insert "$ ") 252 | (setq mistty-sync-marker (point)) 253 | 254 | (insert "abcdef") 255 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 256 | 257 | (goto-char 9) 258 | (insert "NEW") 259 | 260 | (should (equal "$ abcdefNEW" (buffer-substring-no-properties (point-min) (point-max)))) 261 | ;; "$ abcdef" 262 | (let ((cs (mistty--active-changeset))) 263 | (should (equal '((9 inserted)) (mistty--changeset-collect cs))) 264 | (should (equal nil (mistty--changeset-restrict cs 10)))))) 265 | 266 | (ert-deftest mistty-changeset-test-restrict-intervals-within-insert-then-delete-at-end () 267 | (ert-with-test-buffer () 268 | (insert "$ ") 269 | (setq mistty-sync-marker (point)) 270 | 271 | (insert "abcdefghijklmno<>") 272 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 273 | 274 | (delete-region 6 (point-max)) 275 | (goto-char 6) 276 | (insert "new-value") 277 | 278 | (should (equal "$ abcnew-value" (buffer-substring-no-properties (point-min) (point-max)))) 279 | 280 | (let ((cs (mistty--active-changeset))) 281 | (should (equal '((6 inserted) (15 deleted-to-end)) (mistty--changeset-collect cs))) 282 | (should (equal nil (mistty--changeset-restrict cs 10)))))) 283 | 284 | (ert-deftest mistty-changeset-test-restrict-intervals-within-delete-at-end () 285 | (ert-with-test-buffer () 286 | (insert "$ ") 287 | (setq mistty-sync-marker (point)) 288 | 289 | (insert "abcdefghijklmno<>") 290 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 291 | 292 | (delete-region 6 (point-max)) 293 | (goto-char 6) 294 | (insert "new-value") 295 | 296 | (should (equal "$ abcnew-value" (buffer-substring-no-properties (point-min) (point-max)))) 297 | 298 | (let ((cs (mistty--active-changeset))) 299 | (should (equal '((6 inserted) (15 deleted-to-end)) (mistty--changeset-collect cs))) 300 | (should (equal nil (mistty--changeset-restrict cs 15)))))) 301 | 302 | (ert-deftest mistty-changeset-test-single-insert () 303 | (ert-with-test-buffer () 304 | (insert "$ ") 305 | (setq mistty-sync-marker (point)) 306 | 307 | (insert "abcd") 308 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 309 | 310 | (goto-char 6) 311 | (insert "new") 312 | 313 | (should (equal "$ abcnewd" (buffer-substring-no-properties (point-min) (point-max)))) 314 | (let ((cs (mistty--active-changeset))) 315 | (should (equal "new" (mistty--changeset-single-insert cs))) 316 | (should (eq 6 (mistty--changeset-beg cs)))))) 317 | 318 | 319 | (ert-deftest mistty-changeset-test-single-insert-at-end () 320 | (ert-with-test-buffer () 321 | (insert "$ ") 322 | (setq mistty-sync-marker (point)) 323 | 324 | (insert "abc") 325 | (add-hook 'after-change-functions #'mistty--after-change-on-work nil t) 326 | 327 | (goto-char 6) 328 | (insert "at-end") 329 | 330 | (should (equal "$ abcat-end" (buffer-substring-no-properties (point-min) (point-max)))) 331 | (let ((cs (mistty--active-changeset))) 332 | (should (equal "at-end" (mistty--changeset-single-insert cs))) 333 | (should (eq 6 (mistty--changeset-beg cs)))))) 334 | -------------------------------------------------------------------------------- /test/mistty-compat-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests compatibility with other packages -*- lexical-binding: t -*- 2 | 3 | (require 'mistty) 4 | (require 'mistty-testing) 5 | (require 'thingatpt) 6 | (require 'minibuffer) 7 | (require 'cua-base) 8 | 9 | (require 'yasnippet) 10 | (require 'tempel nil 'noerror) 11 | 12 | (ert-deftest mistty-test-detect-foreign-overlay-cua-rectangle () 13 | (let ((mistty-detect-foreign-overlays t) 14 | (orig-cua-mode cua-mode)) 15 | (unwind-protect 16 | (mistty-with-test-buffer (:selected t :shell fish) 17 | ;; CUA rectangle mark mode is an example of an interactive command 18 | ;; that uses overlays. It has the advantage of being built-in. 19 | ;; Other examples would be template engines, such as yasnippet and 20 | ;; templ. 21 | (cua-mode 'on) 22 | (mistty-send-text "for i in a b c\necho $i\nend") 23 | (mistty-test-goto "in") 24 | (execute-kbd-macro (kbd "C- b o o SPC")) 25 | (mistty-wait-for-output :test (lambda () 26 | (equal '(mistty-overlays) mistty--inhibit))) 27 | (should (mistty-long-running-command-p)) 28 | 29 | (should (equal (concat "$ for i in boo a b c\n" 30 | " echo<> boo $i\n" 31 | " end") 32 | (mistty-test-content :show (point)))) 33 | (execute-kbd-macro (kbd "C-")) 34 | (mistty-wait-for-output :str "boo a b c" :start (point-min)) 35 | (mistty-wait-for-output :test (lambda () 36 | (not mistty--inhibit))) 37 | (should-not (mistty-long-running-command-p)) 38 | (should (equal (concat "$ for i in boo a b c\n" 39 | " echo<> boo $i\n" 40 | " end") 41 | (mistty-test-content :show (point))))) 42 | ;; unwind 43 | (cua-mode (if orig-cua-mode nil -1))))) 44 | 45 | (ert-deftest mistty-compat-test-hippie-expand () 46 | (mistty-with-test-buffer () 47 | (mistty-send-text "echo hello, hullo, hallo, hi") 48 | (mistty-send-and-wait-for-prompt) 49 | 50 | (let ((hippie-expand-try-functions-list '(try-expand-dabbrev)) 51 | (start (point))) 52 | 53 | (mistty-send-text "echo h") 54 | (should (equal "echo h<>" 55 | (mistty-test-content :start start :show (point)))) 56 | 57 | (mistty-run-command 58 | (setq this-command 'hippie-expand) 59 | (call-interactively 'hippie-expand)) 60 | (should (equal "echo hi<>" 61 | (mistty-test-content :start start :show (point)))) 62 | 63 | (mistty-run-command 64 | (setq this-command 'hippie-expand) 65 | (setq last-command 'hippie-expand) 66 | (call-interactively 'hippie-expand)) 67 | (should (equal "echo hallo<>" 68 | (mistty-test-content :start start :show (point))))))) 69 | 70 | (ert-deftest mistty-compat-test-yas-expand () 71 | (yas-define-snippets 72 | 'mistty-mode 73 | ;; (KEY TEMPLATE NAME ...) 74 | '(("bif" "if ${1}; then ${0}; fi" "bif"))) 75 | (mistty-with-test-buffer (:selected t) 76 | (yas-minor-mode-on) 77 | (keymap-local-set "C-c y" #'yas-expand) 78 | (mistty-send-text "bif") 79 | (execute-kbd-macro (kbd "C-c y t r u e TAB e c h o SPC o k TAB")) 80 | (mistty-wait-for-output :test (lambda () (not mistty--inhibit))) 81 | (mistty-wait-for-output :str "echo ok") 82 | (should-not mistty--inhibit) 83 | (should (equal "$ if true; then echo ok; fi<>" 84 | (mistty-test-content :show (point)))) 85 | (should (equal "ok" (mistty-send-and-capture-command-output))))) 86 | 87 | (ert-deftest mistty-compat-yas-expand-multiline () 88 | (yas-define-snippets 89 | 'mistty-mode 90 | ;; (KEY TEMPLATE NAME ...) 91 | '(("bif" "if ${1}\nthen ${0}\nfi" "bif"))) 92 | (mistty-with-test-buffer (:selected t) 93 | (yas-minor-mode-on) 94 | (keymap-local-set "C-c y" #'yas-expand) 95 | (mistty-send-text "bif") 96 | (execute-kbd-macro (kbd "C-c y t r u e TAB e c h o SPC o k TAB")) 97 | (mistty-wait-for-output :test (lambda () (not mistty--inhibit))) 98 | (mistty-wait-for-output :str "echo ok") 99 | (should-not mistty--inhibit) 100 | (should (equal "$ if true\nthen echo ok\nfi<>" 101 | (mistty-test-content :show (point)))) 102 | (should (equal "ok" (mistty-send-and-capture-command-output))))) 103 | 104 | (ert-deftest mistty-compat-yas-expand-multiline-fish () 105 | (yas-define-snippets 106 | 'mistty-mode 107 | ;; (KEY TEMPLATE NAME ...) 108 | '(("fif" "if ${1}\n${0}\nend" "fif"))) 109 | (mistty-with-test-buffer (:shell fish :selected t) 110 | (yas-minor-mode-on) 111 | (keymap-local-set "C-c y" #'yas-expand) 112 | (mistty-send-text "fif") 113 | (execute-kbd-macro (kbd "C-c y t r u e TAB e c h o SPC o k TAB")) 114 | (mistty-wait-for-output :test (lambda () (not mistty--inhibit))) 115 | (mistty-wait-for-output :test (lambda () (mistty--queue-empty-p mistty--queue))) 116 | (should (equal (concat "$ if true\n" 117 | " echo ok\n" 118 | " end<>") 119 | (mistty-test-content :show (point)))) 120 | (mistty-send-and-wait-for-prompt) 121 | (should (equal (concat "$ if true\n" 122 | " echo ok\n" 123 | " end\n" 124 | "ok\n" 125 | "$ <>") 126 | (mistty-test-content :show (point)))))) 127 | 128 | (ert-deftest mistty-compat-yas-expand-multiline-fish-insert () 129 | (yas-define-snippets 130 | 'mistty-mode 131 | ;; (KEY TEMPLATE NAME ...) 132 | '(("fif" "if ${1}\n${0}\nend" "fif"))) 133 | (mistty-with-test-buffer (:shell fish :selected t) 134 | (yas-minor-mode-on) 135 | (keymap-local-set "C-c y" #'yas-expand) 136 | (mistty-send-text "fif") 137 | (execute-kbd-macro (kbd "C-c y")) 138 | (mistty-run-command 139 | (insert "true")) 140 | (execute-kbd-macro (kbd "TAB")) 141 | (mistty-run-command 142 | (insert "echo ok")) 143 | (execute-kbd-macro (kbd "TAB")) 144 | (mistty-wait-for-output :test (lambda () (not mistty--inhibit))) 145 | (mistty-wait-for-output :test (lambda () (mistty--queue-empty-p mistty--queue))) 146 | (should (equal (concat "$ if true\n" 147 | " echo ok\n" 148 | " end<>") 149 | (mistty-test-content :show (point)))) 150 | (mistty-send-and-wait-for-prompt) 151 | (should (equal (concat "$ if true\n" 152 | " echo ok\n" 153 | " end\n" 154 | "ok\n" 155 | "$ <>") 156 | (mistty-test-content :show (point)))))) 157 | 158 | (ert-deftest mistty-compat-test-wrap-capf () 159 | (let ((completion-at-point-functions (list #'mistty-compat-test-capf)) 160 | (completion-in-region-function #'mistty-compat-test-completion-in-region) 161 | (mistty-wrap-capf-functions t)) 162 | (mistty-with-test-buffer (:shell fish) 163 | (let (start) 164 | (mistty-send-text "echo hello") 165 | (mistty-send-and-wait-for-prompt) 166 | 167 | (setq start (pos-bol)) 168 | 169 | ;; hello should be suggested 170 | (let ((start (point))) 171 | (mistty-send-text "echo h") 172 | (mistty-wait-for-output :str "echo hello" :start start)) 173 | (should (equal "$ echo h<>ello" 174 | (mistty-test-content 175 | :start start :show (point)))) 176 | (mistty-run-command 177 | (completion-at-point)) 178 | 179 | ;; hallo doesn't start with hello, but it does start with h. 180 | (should (equal "$ echo hallo<>" 181 | (mistty-test-content 182 | :start start :show (point)))))))) 183 | 184 | (ert-deftest mistty-compat-test-wrap-capf-in-scrollback-region () 185 | (let ((completion-at-point-functions (list #'mistty-compat-test-capf)) 186 | (completion-in-region-function #'mistty-compat-test-completion-in-region) 187 | (mistty-wrap-capf-functions t)) 188 | (mistty-with-test-buffer (:shell fish) 189 | (mistty-send-text "echo hel") 190 | (mistty-send-and-wait-for-prompt) 191 | 192 | (mistty-test-goto-after "echo h") 193 | (should (equal 194 | (concat "$ echo h<>el\n" 195 | "hel\n" 196 | "$") 197 | (mistty-test-content 198 | :show (point)))) 199 | (mistty-run-command 200 | (completion-at-point)) 201 | 202 | ;; completion-at-point should have seen "hel" not just h "h" 203 | (should (equal 204 | (concat "$ echo hello<>\n" 205 | "hel\n" 206 | "$") 207 | (mistty-test-content 208 | :show (point))))))) 209 | 210 | (ert-deftest mistty-compat-test-wrap-capf-disabled () 211 | (let ((completion-at-point-functions (list #'mistty-compat-test-capf)) 212 | (completion-in-region-function #'mistty-compat-test-completion-in-region) 213 | (mistty-wrap-capf-functions nil)) 214 | (mistty-with-test-buffer (:shell bash) 215 | (let (start) 216 | (mistty-send-text "echo hello") 217 | (mistty-send-and-wait-for-prompt) 218 | 219 | (setq start (pos-bol)) 220 | 221 | (mistty-send-text "echo ho") 222 | (mistty-run-command 223 | (goto-char (1- (point)))) 224 | (should (equal "$ echo h<>o" 225 | (mistty-test-content 226 | :start start :show (point)))) 227 | 228 | (mistty-run-command 229 | (completion-at-point)) 230 | 231 | ;; completion-at-point should have seen "ho" not just "h" 232 | (should (equal "$ echo ho!ho!ho!<>" 233 | (mistty-test-content 234 | :start start :show (point)))))))) 235 | 236 | (defun mistty-compat-test-capf () 237 | "Test function for `completion-at-point-functions'." 238 | (let ((bounds (bounds-of-thing-at-point 'word))) 239 | (list (car bounds) (cdr bounds) 240 | (list "ho!ho!ho!" "hi" "hello" "hallo")))) 241 | 242 | (defun mistty-compat-test-completion-in-region (start end collection &optional _) 243 | "Strange `completion-in-region-function'. 244 | 245 | It overwrites START-END with the *last* element of COLLECTION 246 | that starts with the text currently between START and END." 247 | (let ((text (buffer-substring-no-properties start end))) 248 | (when-let* ((replacement (car 249 | (last 250 | (delq nil 251 | (mapcar (lambda (completion) 252 | (when (string-prefix-p text completion) 253 | completion)) 254 | collection)))))) 255 | (goto-char start) 256 | (delete-region start end) 257 | (insert replacement)))) 258 | 259 | (ert-deftest mistty-compat-test-tempel-smoke () 260 | (skip-unless (featurep 'tempel)) 261 | ;; This makes sure that the tempel integration works at all 262 | (ert-with-test-buffer () 263 | (let* ((mistty-test-tempel-templates '((test "THIS IS A TEST"))) 264 | (tempel-template-sources (list (lambda () mistty-test-tempel-templates)))) 265 | (tempel-insert 'test) 266 | (should (equal "THIS IS A TEST" (mistty-test-content)))))) 267 | 268 | (ert-deftest mistty-compat-test-tempel-detect-overlays () 269 | (skip-unless (featurep 'tempel)) 270 | (mistty-with-test-buffer (:selected t) 271 | (let* ((mistty-test-tempel-templates '((test "for " p " in " p "; do " p "; done"))) 272 | (tempel-template-sources (list (lambda () mistty-test-tempel-templates)))) 273 | (keymap-local-set "C-c n" #'tempel-next) 274 | (keymap-local-set "C-c d" #'tempel-done) 275 | (mistty-run-command 276 | (tempel-insert 'test)) 277 | (mistty-wait-for-output :test (lambda () mistty--inhibit)) 278 | (execute-kbd-macro (kbd "i C-c n a SPC b SPC c C-c n e c h o SPC $ i C-c d")) 279 | (mistty-wait-for-output :test (lambda () (not mistty--inhibit))) 280 | (mistty-wait-for-output :str "for i in a b c; do echo $i; done")))) 281 | 282 | (turtles-ert-deftest mistty-compat-goto-address-mode (:instance 'mistty) 283 | (unwind-protect 284 | (progn 285 | (global-goto-address-mode) 286 | (mistty-with-test-buffer (:selected t) 287 | (mistty-send-text "echo http://www.example.com/") 288 | (redisplay t) ;; force fontification 289 | (mistty-send-and-wait-for-prompt) 290 | (redisplay t) 291 | (goto-char (point-min)) 292 | (search-forward "http://www.example.com") 293 | (should (mistty-test-has-goto-address-overlay-at (match-beginning 0))) 294 | (search-forward "http://www.example.com") 295 | (should (mistty-test-has-goto-address-overlay-at (match-beginning 0))))) 296 | (global-goto-address-mode -1))) 297 | 298 | (defun mistty-test-has-goto-address-overlay-at (pos) 299 | "Check whether POS has goto-address overlays." 300 | (when (delq nil (mapcar (lambda (ov) (overlay-get ov 'goto-address)) 301 | (overlays-at pos))) 302 | t)) 303 | -------------------------------------------------------------------------------- /mistty-util.el: -------------------------------------------------------------------------------- 1 | ;;; mistty-util.el --- random utils used by mistty -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | ;;; Commentary: 18 | ;; 19 | ;; This file collects some random low-level utilities used by mistty 20 | ;; that usually have nothing to do with MisTTY itself. 21 | 22 | ;;; Code: 23 | 24 | (require 'cl-lib) 25 | 26 | (defmacro mistty--with-live-buffer (buf &rest body) 27 | "Execute BODY with BUF enabled, if BUF is live. 28 | 29 | The execution of BODY is silently skipped if BUF is not a live 30 | buffer." 31 | (declare (indent 1)) 32 | (let ((tempvar (make-symbol "buf"))) 33 | `(let ((,tempvar ,buf)) 34 | (when (buffer-live-p ,tempvar) 35 | (with-current-buffer ,tempvar 36 | ,@body))))) 37 | 38 | (defun mistty--bol (pos &optional n) 39 | "Return the Nth beginning of line position at POS." 40 | (save-excursion 41 | (goto-char pos) 42 | (pos-bol n))) 43 | 44 | (defun mistty--eol (pos &optional n) 45 | "Return the Nth end of line position at POS." 46 | (save-excursion 47 | (goto-char pos) 48 | (pos-eol n))) 49 | 50 | (defun mistty--line-width () 51 | "Return the column number at EOL." 52 | (save-excursion 53 | (goto-char (pos-eol)) 54 | (current-column))) 55 | 56 | (defun mistty--repeat-string (n segment) 57 | "Return a string containing SEGMENT N times." 58 | (let ((segment-len (length segment))) 59 | (if (= 1 segment-len) 60 | (make-string n (aref segment 0)) 61 | (let ((str (make-string (* n segment-len) ?\ ))) 62 | (dotimes (i n) 63 | (dotimes (j segment-len) 64 | (aset str (+ (* i segment-len) j) (aref segment j)))) 65 | str)))) 66 | 67 | (defun mistty--safe-bufstring (start end) 68 | "Return buffer content from START to END, or an empty string. 69 | 70 | Given an invalid buffer range, this alternative to 71 | `buffer-substring-no-properties' returns an empty string instead 72 | of failing." 73 | (let ((start (max (point-min) (min (point-max) start))) 74 | (end (max (point-min) (min (point-max) end)))) 75 | (if (> end start) 76 | (buffer-substring-no-properties start end) 77 | ""))) 78 | 79 | (defun mistty--safe-pos (pos) 80 | "Make sure POS is within the range `point-min' to `point-max'." 81 | (min (point-max) (max (point-min) pos))) 82 | 83 | (defun mistty--lines () 84 | "Return list of markers to the beginning of the buffer's line." 85 | (save-excursion 86 | (goto-char (point-min)) 87 | (let ((lines (list (point-min-marker)))) 88 | (while (search-forward "\n" nil t) 89 | (push (point-marker) lines)) 90 | (nreverse lines)))) 91 | 92 | (defun mistty--col (pos) 93 | "Return the column number at POS." 94 | (- pos (mistty--bol pos))) 95 | 96 | (defun mistty--line (pos) 97 | "Return the line number at POS." 98 | (save-excursion 99 | (let ((count 0)) 100 | (goto-char pos) 101 | (while (zerop (forward-line -1)) 102 | (setq count (1+ count))) 103 | count))) 104 | 105 | (defsubst mistty--nonempty-str-p (str) 106 | "Return non-nil if STR is a nonempty string." 107 | (and (stringp str) 108 | (length> str 0))) 109 | 110 | (defun mistty--same-line-p (a b) 111 | "Return non-nil if positions A and B are on the same line." 112 | (= (mistty--bol a) (mistty--bol b))) 113 | 114 | (defun mistty--remove-text-with-property (prop &optional pred) 115 | "Remove text with property PROP whose value matches PRED. 116 | 117 | If PRED is unspecified, remove any PROP with a non-nil value." 118 | (let ((pos (point-min)) 119 | (pred (or pred #'identity))) 120 | (while (setq pos (text-property-not-all pos (point-max) prop nil)) 121 | (let ((next-pos (next-single-property-change pos prop nil (point-max)))) 122 | (if (funcall pred (get-text-property pos prop)) 123 | (delete-region pos next-pos) 124 | (setq pos next-pos)))))) 125 | 126 | (defun mistty--remove-fake-newlines (start end &optional column-width) 127 | "Remove newlines marked \\='term-line-wrap between START and END. 128 | 129 | COLUMN-WIDTH is the number of columns of the terminal. This is used to double 130 | check that a newline is indeed a line-wrap." 131 | (save-excursion 132 | (goto-char start) 133 | (while (search-forward "\n" end 'noerror) 134 | (when (save-excursion 135 | (goto-char (1- (point))) 136 | (and (get-text-property (point) 'term-line-wrap) 137 | (or (null column-width) 138 | (zerop (% (current-column) column-width))))) 139 | (setq end (1- end)) 140 | (replace-match "" nil t))))) 141 | 142 | (defun mistty-self-insert-p (key) 143 | "Return non-nil if KEY is a key that is normally just inserted." 144 | (and (length= key 1) 145 | (characterp (aref key 0)) 146 | (not (string= "Cc" 147 | (get-char-code-property (aref key 0) 148 | 'general-category))))) 149 | 150 | (defun mistty--truncate-string (str n) 151 | "Truncate STR to N chars, if necessary. 152 | 153 | Add an ellipsis if STR is truncated." 154 | (if (length> str n) 155 | (concat (substring str 0 n) "...") 156 | str)) 157 | 158 | (defun mistty--last-non-ws () 159 | "Return the position of the last non-whitespace in the buffer." 160 | (save-excursion 161 | (goto-char (point-max)) 162 | (skip-chars-backward "[:blank:]\n\r") 163 | (point))) 164 | 165 | (defun mistty--has-text-properties (pos props) 166 | "Return non-nil if properties at POS include PROPS. 167 | 168 | Return nil if PROPS is nil." 169 | (when props 170 | (let ((actual (text-properties-at pos)) 171 | (current props) 172 | (ret t)) 173 | (while (and current ret) 174 | (let ((p (pop current)) 175 | (val (pop current))) 176 | (unless (equal val (plist-get actual p)) 177 | (setq ret nil)))) 178 | ret))) 179 | 180 | (defun mistty--count-lines (beg end &optional pred) 181 | "Return number of newlines between BEG and END, including invisible ones. 182 | 183 | If END < BEG, return a negative number. 184 | 185 | If specified, PRED is a function that takes a position pointing to a 186 | newline and return non-nil if that newline should be counted." 187 | (save-excursion 188 | (let ((count 0) 189 | (sign (if (> beg end) -1 1)) 190 | (beg (min beg end)) 191 | (end (max beg end)) 192 | (pred (or pred (lambda (_) t)))) 193 | (goto-char beg) 194 | (while (search-forward "\n" end 'noerror) 195 | (when (funcall pred (match-beginning 0)) 196 | (cl-incf count))) 197 | 198 | (* sign count)))) 199 | 200 | (defun mistty--count-scrollines (beg end) 201 | "Count the number of scrollines between BEG and END." 202 | (mistty--count-lines 203 | beg end 204 | #'mistty--real-nl-p)) 205 | 206 | (defun mistty--fake-nl-p (&optional pos) 207 | "Check whether newline at POS is a fake newline. 208 | 209 | POS defaults to the current point." 210 | (get-text-property (or pos (point)) 'term-line-wrap)) 211 | 212 | (defun mistty--real-nl-p (&optional pos) 213 | "Check whether char at POS is a real newline." 214 | (let ((pos (or pos (point)))) 215 | (and (eq ?\n (char-after pos)) 216 | (not (mistty--fake-nl-p pos))))) 217 | 218 | (defun mistty--go-down-scrollines (count) 219 | "Go down COUNT scrollines from the current position. 220 | 221 | If COUNT is < 0, go up that many scrollines instead. 222 | 223 | Put point at the beginning of a scrolline. 224 | 225 | Go as far up as possible and return the remaining number of scrollines 226 | to go down to, normally 0." 227 | 228 | ;; Go down, skipping fake newlines 229 | (while (and (> count 0) (search-forward "\n" nil 'noerror)) 230 | (unless (mistty--fake-nl-p (match-beginning 0)) 231 | (cl-decf count))) 232 | 233 | ;; Go up, skipping fake newlines 234 | (while (and (< count 0) (search-backward "\n" nil 'noerror)) 235 | (unless (mistty--fake-nl-p (match-beginning 0)) 236 | (cl-incf count))) 237 | 238 | (mistty--go-beginning-of-scrolline) 239 | 240 | count) 241 | 242 | (defsubst mistty--go-up-scrollines (count) 243 | "Go up COUNT scrollines, skipping fake newlines. 244 | 245 | Go down that many scrollines if COUNT is negative. 246 | 247 | Put point at the beginning of a scrolline. 248 | 249 | Go as far down as possible and return the number of scrollines to go up 250 | to, normally 0." 251 | (- (mistty--go-down-scrollines (- count)))) 252 | 253 | (defun mistty--go-beginning-of-scrolline () 254 | "Go to the beginning of the scrolline, skipping fake newlines." 255 | (while (progn 256 | (goto-char (pos-bol)) 257 | (and (> (point) (point-min)) 258 | (mistty--fake-nl-p (1- (point))))) 259 | (goto-char (1- (point))))) 260 | 261 | (defsubst mistty--beginning-of-scrolline-pos () 262 | "Position of the beginning of the current scrolline." 263 | (save-excursion 264 | (mistty--go-beginning-of-scrolline) 265 | (point))) 266 | 267 | (defun mistty--go-end-of-scrolline () 268 | "Go to the end of the scrolline, skipping fake newlines." 269 | (while (progn 270 | (goto-char (pos-eol)) 271 | (and (< (point) (point-max)) 272 | (mistty--fake-nl-p (point)))) 273 | (goto-char (1+ (point))))) 274 | 275 | (defsubst mistty--end-of-scrolline-pos () 276 | "Position of the end of the current scrolline." 277 | (save-excursion 278 | (mistty--go-end-of-scrolline) 279 | (point))) 280 | 281 | (defun mistty--current-scrolline-text (&optional no-properties) 282 | "Return the text of the scrolline at point as a string. 283 | 284 | Any fake newlines are stripped. 285 | 286 | If NO-PROPERTIES is non-nil, strip text properties from the returned 287 | string." 288 | (mistty--text-without-fake-lines (mistty--beginning-of-scrolline-pos) 289 | (mistty--end-of-scrolline-pos) 290 | no-properties)) 291 | 292 | (defun mistty--scrolline-text-before-point (&optional no-properties) 293 | "Return text from the beginning of the scrolline to the current point. 294 | 295 | Any fake newlines are stripped. 296 | 297 | If NO-PROPERTIES is non-nil, strip text properties from the returned 298 | string." 299 | (mistty--text-without-fake-lines (mistty--beginning-of-scrolline-pos) 300 | (point) 301 | no-properties)) 302 | 303 | (defun mistty--text-without-fake-lines (beg end &optional no-properties) 304 | "Return text between BEG and END without fake newlines. 305 | 306 | If NO-PROPERTIES is non-nil, remove text properties." 307 | (let ((text (buffer-substring beg end))) 308 | (with-temp-buffer 309 | (insert text) 310 | (mistty--remove-text-with-property 'term-line-wrap) 311 | (if no-properties 312 | (buffer-substring-no-properties (point-min) (point-max)) 313 | (buffer-string))))) 314 | 315 | (cl-defstruct (mistty--fifo 316 | (:constructor mistty--make-fifo ()) 317 | (:conc-name mistty--fifo-)) 318 | "A FIFO datastructure based on a doubly-linked list. 319 | 320 | `mistty--fifo-enqueue' adds to the fifo. `mistty--fifo-dequeue' 321 | removes and returns the oldest item in the fifo." 322 | ;; nodes of type (cons (cons ITEM OLDER) NEWER) 323 | newest 324 | oldest) 325 | 326 | (defun mistty--fifo-empty-p (fifo) 327 | "Return non-nil if FIFO is empty." 328 | (null (mistty--fifo-oldest fifo))) 329 | 330 | (defun mistty--fifo-clear (fifo) 331 | "Clear the FIFO." 332 | (setf (mistty--fifo-newest fifo) nil) 333 | (setf (mistty--fifo-oldest fifo) nil)) 334 | 335 | (defun mistty--fifo-enqueue (fifo item) 336 | "Add ITEM into the FIFO." 337 | (let* ((older (mistty--fifo-newest fifo)) 338 | (node (cons (cons item older) nil))) 339 | (setf (mistty--fifo-newest fifo) node) 340 | (if older 341 | (setcdr older node) 342 | (setf (mistty--fifo-oldest fifo) node)))) 343 | 344 | (defun mistty--fifo-dequeue (fifo) 345 | "Remove the oldest entry from FIFO and return it. 346 | 347 | Return nil if there are no entry." 348 | (when-let* ((oldest (mistty--fifo-oldest fifo))) 349 | (let ((item (caar oldest)) 350 | (newer (cdr oldest))) 351 | (setf (mistty--fifo-oldest fifo) newer) 352 | (if newer 353 | (setcdr (car newer) nil) 354 | (setf (mistty--fifo-newest fifo) nil)) 355 | 356 | item))) 357 | 358 | (defun mistty--fifo-to-list (fifo) 359 | "Destructively convert FIFO into a list." 360 | (let ((list (mistty--fifo-oldest fifo))) 361 | (mistty--fifo-clear fifo) 362 | 363 | ;; Turn (cons (cons ITEM OLDER) NEWER) 364 | ;; into (cons ITEM NEWER) 365 | (let ((cur list)) 366 | (while cur 367 | (setcar cur (caar cur)) 368 | (setq cur (cdr cur)))) 369 | 370 | list)) 371 | 372 | (provide 'mistty-util) 373 | 374 | ;;; mistty-util.el ends here 375 | -------------------------------------------------------------------------------- /docs/source/shells.rst: -------------------------------------------------------------------------------- 1 | .. _shells: 2 | 3 | Shells 4 | ====== 5 | 6 | .. _bash: 7 | 8 | Bash 9 | ---- 10 | 11 | A recent version of Bash is preferable. Bash 5.1 or later is 12 | recommended. 13 | 14 | MisTTY works best with shells that support bracketed paste. Without 15 | bracketed paste support, MisTTY will still work, but might behaves 16 | unexpectedly when yanking text containing special characters. 17 | 18 | Bash 4.5 to 5.0 supports bracketed paste, but it must be turned 19 | on in your :file:`.inputrc`, as follows: 20 | 21 | .. code-block:: 22 | 23 | set enable-bracketed-paste on 24 | 25 | Bash versions older than 4.5 don't support bracketed paste. 26 | 27 | Additionally, Bash versions older than 4.4 require extra setup to 28 | enable directory tracking, as documented in :ref:`bash_dirtrack`. 29 | 30 | Multi-line prompts in Bash 31 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 32 | 33 | When you press :kbd:`RET` on an incomplete command, :program:`bash` 34 | has the annoying habit of starting a secondary prompt which doesn't 35 | let you go back to the previous line with the default keybindings. 36 | 37 | To work around that, type :kbd:`S-` instead of :kbd:`RET` 38 | while on the terminal zone of a MisTTY buffer. This sends a newline 39 | without running the command. You'll then end up with one multi-line 40 | prompt that you can edit normally. This requires Bash 5.1 or an 41 | earlier version with bracketed paste mode turned on. 42 | 43 | You'll get the same effect if you yank a multi-line command while in a 44 | prompt or go up the command history to a previous multi-line command. 45 | 46 | Please be aware that when editing a multi-line command in Bash, MisTTY 47 | may leave trailing spaces at the end of some lines. In situations 48 | where these are significant, you will need to remove trailing spaces 49 | using :kbd:`C-d` or :kbd:`DEL`. 50 | 51 | .. _bash_dirtrack: 52 | 53 | Directory tracking in Bash 54 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 55 | 56 | .. index:: pair: variable; mistty-set-EMACS 57 | 58 | Recent versions of :program:`bash` already send the current directory 59 | when they detects that it's called from Emacs with 60 | :code:`TERM=eterm-color`. This works fine for local shell as well as remote 61 | shells run with TRAMP. 62 | 63 | If you ssh into a host from an existing MisTTY buffer, however, 64 | :program:`bash` will not send the remote directory. If you want this 65 | use case to work, extend your prompt to send out an OSC7 sequence to 66 | have :program:`bash` send the current directory and hostname to 67 | MisTTY. 68 | 69 | To do that, you might add the following to :file:`~/.bashrc`: 70 | 71 | .. code-block:: bash 72 | 73 | if [ "$TERM" = "eterm-color" ]; then 74 | PS1='\[\e]7;file://$HOSTNAME$PWD\e\\\\\]'$PS1 75 | fi 76 | 77 | Such sequence are either ignored or understood by most terminals, so 78 | you don't absolutely need to check TERM. 79 | 80 | Versions of :program:`bash` older than 4.4 only enable directory 81 | tracking if the env variable EMACS is set. You can have MisTTY set 82 | this env variable when it starts a shell by going to `M-x 83 | customize-option mistty-set-EMACS`. :code:`mistty-set-EMACS` also 84 | works as a connection-local variable, to set the EMACS env variable 85 | only on some hosts that use an old version of :program:`bash`. 86 | 87 | For example: 88 | 89 | .. code-block:: elisp 90 | 91 | (connection-local-set-profile-variables 92 | 'profile-old-bash 93 | '((mistty-set-EMACS . t) 94 | (mistty-shell-command . ("/bin/bash" "-i")))) 95 | 96 | (connection-local-set-profiles '(:machine "oldhost.example.com") 97 | 'profile-old-bash) 98 | (connection-local-set-profiles '(:protocol "docker") 99 | 'profile-old-bash)" 100 | 101 | VI mode in Bash 102 | ^^^^^^^^^^^^^^^ 103 | 104 | To communicate with :program:`bash`, MisTTY requires the shell to be 105 | in its default editing mode, that is, the emacs mode. **Please make 106 | sure you haven't put readline or bash in vi mode before trying out 107 | MisTTY.** 108 | 109 | To turn on vi mode in readline everywhere but MisTTY, you can add 110 | something like the following into :file:`~/.inputrc`: 111 | 112 | .. code-block:: 113 | 114 | $if term=eterm 115 | set editing-mode emacs 116 | $else 117 | set editing-mode vi 118 | $endif 119 | 120 | Or, in bash :file:`~/.bashrc`: 121 | 122 | .. code-block:: bash 123 | 124 | if [ "$TERM" != "eterm-color" ]; then 125 | set -o vi 126 | fi 127 | 128 | Fancy prompts in Bash 129 | ^^^^^^^^^^^^^^^^^^^^^ 130 | 131 | MisTTY is compatible with stylized prompts, such as those produced by 132 | `powerline-go `_. However, 133 | given the numerous variations in how these prompts are created, issues 134 | may arise. 135 | 136 | If you suspect that your shell prompt may be causing problems, try 137 | configuring your shell to send out :ref:`OSC 133 ` codes to 138 | help MisTTY correctly identify your prompt. Assuming you have a 139 | working PS1 already, OSC 133 support can be added with: 140 | 141 | .. code-block:: bash 142 | 143 | PS1='\[\e]133;A\007\]'$PS1'\[\e]133;B\007\]' 144 | 145 | If you forget to put the OSC sequences in PS1 within ``\[...\]``, Bash 146 | might be confused by the OSC sequences and you might have strange 147 | issues when exiting reverse-i-search. 148 | 149 | .. _fish: 150 | 151 | Fish 152 | ---- 153 | 154 | A recent version of Fish is preferable. MisTTY relies on bracketed 155 | paste mode, on by default, so it should not be turned off. 156 | 157 | Autosuggestions in Fish 158 | ^^^^^^^^^^^^^^^^^^^^^^^ 159 | 160 | :program:`fish` autosuggestions work normally in MisTTY. However, the 161 | usual way of accepting an autosuggestion, pressing the right arrow 162 | key, is very inconvenient as this is bound to an Emacs point movement. 163 | 164 | The recommended way of accepting an autosuggestion in MisTTY is to 165 | type :kbd:`C-e`. This works in normal terminals as well. 166 | 167 | Command History in Fish 168 | ^^^^^^^^^^^^^^^^^^^^^^^ 169 | 170 | To make full use of :program:`fish` command history, you'll want to 171 | forward some additional shortcuts to fish: 172 | 173 | .. code-block:: elisp 174 | 175 | (keymap-set mistty-prompt-map "M-" #'mistty-send-key) 176 | (keymap-set mistty-prompt-map "M-" #'mistty-send-key) 177 | (keymap-set mistty-prompt-map "M-" #'mistty-send-key) 178 | (keymap-set mistty-prompt-map "M-" #'mistty-send-key) 179 | 180 | This can also be done by calling :code:`use-package` as shown in 181 | :ref:`installation`. 182 | 183 | When in reverse history search mode, :program:`fish` enters a mode 184 | that lets you select an option using the arrow keys. To send 185 | up/down/left/right directly to :program:`fish`, you can: 186 | 187 | - use :kbd:`M-p` to go up and :kbd:`M-n` to go down, or, if you prefer 188 | 189 | - use :kbd:`C-q ` :kbd:`C-q ` :kbd:`C-q ` :kbd:`C-q `, or even 190 | 191 | - :kbd:`C-c C-q` to temporarily send all send key presses to :program:`fish` 192 | 193 | .. _fish_dirtrack: 194 | 195 | Directory tracking in Fish 196 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 197 | 198 | Starting with version 4.0.0, Fish sends out an OSC7 sequence by 199 | default, so directory tracking doesn't require any configuration. 200 | 201 | For older versions of fish, extend your prompt to send out an OSC7 202 | sequence to have :program:`fish` send the current directory and 203 | hostname to MisTTY. To do that, you might add the following to 204 | :file:`~/.local/config/fish/config.fish`: 205 | 206 | .. code-block:: fish 207 | 208 | if [ "$TERM" = "eterm-color" ] 209 | function osc7_send_pwd --on-event fish_prompt 210 | printf "\e]7;file://%s%s\e\\\\" (hostname) "$PWD" 211 | end 212 | end 213 | 214 | such sequence are either ignored or understood by most terminals. You 215 | might already have it set up. 216 | 217 | Multi-line prompts in Fish 218 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 219 | 220 | :program:`fish` automatically detects when a command is incomplete 221 | when you type :kbd:`RET` and launches a multi-line prompt, which 222 | MisTTY knows to navigate. 223 | 224 | .. index:: pair: variable; mistty-skip-empty-spaces 225 | 226 | The cursor jumps over indent space while on such a prompt, just like 227 | in a normal terminal. :kbd:`M-x customize-option 228 | mistty-skip-empty-spaces` allows you to turn that on or off in a 229 | MisTTY buffer. 230 | 231 | VI mode in Fish 232 | ^^^^^^^^^^^^^^^ 233 | 234 | To communicate with :program:`fish`, MisTTY requires the shell to be 235 | in its default editing mode, that is, the emacs mode. **Please make 236 | sure you haven't put readline or bash in vi mode before trying out 237 | MisTTY.** 238 | 239 | To turn on vi mode in readline everywhere but in MisTTY, you can add 240 | something like the following in :file:`~/.zshrc`: 241 | 242 | .. code-block:: fish 243 | 244 | if [ "$TERM" != "eterm-color" ] 245 | fish_vi_key_bindings 246 | end 247 | 248 | Fancy prompts in Fish 249 | ^^^^^^^^^^^^^^^^^^^^^ 250 | 251 | MisTTY is known to work with powerline-shell prompts or `Tide, on Fish 252 | `_, including right prompts. 253 | 254 | If you suspect that your shell prompt may be causing problems, try 255 | configuring your shell to send out :ref:`OSC 133 ` codes to 256 | help MisTTY correctly identify your prompt. 257 | 258 | .. _zsh: 259 | 260 | Zsh 261 | --- 262 | 263 | A recent version of Zsh is preferable. 264 | 265 | Zsh supports bracketed paste, which MisTTY relies on, since version 266 | 5.1. Older versions will work, but with limitations, and you might get 267 | unexpected results if you yank text containing special characters. 268 | 269 | Autosuggestions in Zsh 270 | ^^^^^^^^^^^^^^^^^^^^^^ 271 | 272 | Fish-like :program:`zsh` autosuggestions work normally in MisTTY, if 273 | you've turned these on. However, the usual way of accepting an 274 | autosuggestion, pressing the right arrow key, is very inconvenient as 275 | this is normally bound to an Emacs point movement. 276 | 277 | The recommended way of accepting an autosuggestion in MisTTY is to 278 | type :kbd:`C-e`. This works in normal terminals as well. 279 | 280 | .. _zsh_dirtrack: 281 | 282 | Directory tracking in Zsh 283 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 284 | 285 | Extend your prompt to send out an OSC7 sequence to have 286 | :program:`zsh` send the current directory and hostname to MisTTY. To 287 | do that, you might add the following to :file:`~/.zshrc`: 288 | 289 | .. code-block:: zsh 290 | 291 | function osc7_precmd() { 292 | printf "\e]7;file://%s%s\e\\\\" "$HOSTNAME" "$PWD" 293 | } 294 | precmd_functions+=(osc7_precmd) 295 | 296 | Such sequence are either ignored or understood by any well-behaved 297 | terminals, so you shouldn't need to check the terminal. 298 | 299 | Multi-line prompts in Zsh 300 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 301 | 302 | When you press :kbd:`RET` on an incomplete command, :program:`zsh` 303 | has the annoying habit of starting a secondary prompt. MisTTY doesn't 304 | know how to go back to the previous prompt from such a prompt. 305 | 306 | To work around that, type :kbd:`S-` instead of :kbd:`RET` 307 | while on the terminal zone of a MisTTY buffer. This sends a newline 308 | without running the command. You'll then end up with one multi-line 309 | prompt that you can edit normally. This requires a version of Zsh that 310 | supports bracketed paste mode, 5.1 or later. 311 | 312 | You'll get the same effect if you yank a multi-line command while in a 313 | prompt or go up the command history to a previous multi-line command. 314 | 315 | Please be aware that when editing a multi-line command in Zsh, MisTTY 316 | may leave trailing spaces at the end of some lines. In situations 317 | where these are significant, you will need to remove trailing spaces 318 | using :kbd:`C-d` or :kbd:`DEL`. 319 | 320 | VI mode in Zsh 321 | ^^^^^^^^^^^^^^ 322 | 323 | To communicate with :program:`zsh`, MisTTY requires the shell to be in 324 | its default editing mode, that is, the emacs mode. **Please make sure 325 | you haven't put readline or bash in vi mode before trying out 326 | MisTTY.** 327 | 328 | To turn on vi mode in readline everywhere but in MisTTY, you can add 329 | something like the following in :file:`~/.zshrc`: 330 | 331 | .. code-block:: zsh 332 | 333 | if [ "$TERM" != "eterm-color" ]; then 334 | bindkey -v 335 | fi 336 | 337 | Fancy prompts in Zsh 338 | ^^^^^^^^^^^^^^^^^^^^ 339 | 340 | MisTTY is compatible with right prompts and fancy multi-line prompts, 341 | such as the ones created by `powerlevel10k 342 | `_, though there are some 343 | limitations. 344 | 345 | Transient prompts can interfere with MisTTY's commands, such as 346 | `mistty-previous-output` (:kbd:`C-c C-p`) and 347 | `mistty-create-buffer-with-output` (:kbd:`C-c C-r`). If these commands 348 | are important to you, disable transient prompts when `TERM` is set to 349 | `eterm-color`. 350 | 351 | When using a multi-line prompt, to ensure proper functionality, 352 | configure your shell to send OSC 133 (Final Term) codes, at least A 353 | and C, so MisTTY correctly recognizes the beginning and end of the 354 | prompt. See :ref:`OSC 133 ` 355 | 356 | The minimum configuration that would help MisTTY might look like this: 357 | 358 | .. code-block:: zsh 359 | 360 | function osc133_precmd() { 361 | printf '\033]133;A\007' 362 | } 363 | precmd_functions+=(osc133_precmd) 364 | 365 | function osc133_preexec() { 366 | printf '\033]133;C\007' 367 | } 368 | preexec_functions+=(osc133_preexec) 369 | 370 | If you suspect that your shell prompt may be causing problems, try 371 | configuring your shell to send out :ref:`OSC 133 codes ` to 372 | help MisTTY correctly identify your prompt. 373 | 374 | .. _ipython: 375 | 376 | IPython 377 | ------- 378 | 379 | Editing, and cursor movements should work out of the box with 380 | :program:`ipython`, even in multi-line commands, *provided you use the 381 | default prompts*. 382 | 383 | .. index:: 384 | pair: variable; mistty-move-vertically-regexps 385 | pair: variable; mistty-multi-line-continue-prompts 386 | 387 | If you modified the :program:`ipython` prompts, you'll need to teach 388 | MisTTY about these prompts for multi-line movement and editing to 389 | work. 390 | 391 | Go to :kbd:`M-x configure-option mistty-move-vertically-regexps` and 392 | add to the list a regular expression that matches your prompt. Make 393 | sure that your regular expression is specific to your IPython prompt, 394 | as mistakenly matching with :program:`bash` or :program:`zsh` would 395 | have rather catastrophic results. 396 | 397 | Go to :kbd:`M-x configure-option mistty-multi-line-continue-prompts` 398 | and add to the list a regular expression that matches your IPython 399 | continue prompt, that is, the prompt that IPython adds to the second 400 | and later lines of input. Again, be specific. The regular expression 401 | shouldn't match any other prompts. You don't need to do anything here 402 | if you configured IPython to not output any continue prompt. 403 | -------------------------------------------------------------------------------- /docs/source/extensions.rst: -------------------------------------------------------------------------------- 1 | Extending MisTTY 2 | ================ 3 | 4 | .. _hooks: 5 | 6 | Hooks 7 | ----- 8 | 9 | mistty-mode-hook 10 | ^^^^^^^^^^^^^^^^ 11 | 12 | .. index:: 13 | pair: variable; mistty-mode-hook 14 | pair: hook; mistty-mode-hook 15 | 16 | The hook :code:`mistty-mode-hook` is called on every MisTTY buffer 17 | just after creating the buffer and selecting a window for it but 18 | before executing the shell, with the buffer selected. 19 | 20 | If you have enabled autocomplete or autosuggestion globally, you might 21 | want to disable it for MisTTY buffers from a function called by 22 | :code:`mistty-mode-hook`. 23 | 24 | This hook also provides a good time to rename the buffer, change its 25 | directory or change environment variables, to be inherited by the 26 | process. 27 | 28 | For example, if you wanted a more generic name for the MisTTY buffers, 29 | you could do: 30 | 31 | .. code-block:: elisp 32 | 33 | (defun my-lets-call-it-shell () 34 | (rename-buffer (generate-new-buffer-name "*shell*"))) 35 | (add-hook 'mistty-mode-hook #'my-lets-call-it-shell) 36 | 37 | .. index:: 38 | pair: variable; mistty-interactive-insert-hook 39 | pair: hook; mistty-interactive-interactive 40 | 41 | mistty-interactive-insert-hook 42 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 43 | 44 | :code:`mistty-interactive-insert-hook` is a hook that is called when 45 | text is typed in the terminal region. It's not called, for example, 46 | for text that is inserted or displayed by the shell. 47 | 48 | This hook provides an appropriate time to trigger auto-completion UI, 49 | which, by default, doesn't work in the terminal region, as discussed 50 | in :ref:`autocomplete`. 51 | 52 | Auto-completion doesn't work in the terminal region because it often 53 | requires calling a post-command function. To work around that, in most 54 | case, it's enough to just turn on the option :kbd:`M-x 55 | customize-option mistty-simulate-self-insert-command`, which enables 56 | the function :code:`mistty-self-insert-command`, called by this hook 57 | by default. 58 | 59 | This might not always work and have unintended effects, so you might 60 | prefer to trigger the auto-completion UI yourself by adding your own 61 | function to this hook and turning the above option off. 62 | 63 | mistty-after-process-start-hook 64 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 65 | 66 | :code:`mistty-after-process-start-hook` is a normal hook called from 67 | within a new MisTTY work buffer just after starting the process, 68 | usually a shell. The process itself is available as 69 | :code:`mistty-proc`. At the time this hook is called, the buffer is 70 | typically empty, as no output from the process has been processed. 71 | 72 | mistty-after-process-end-hook 73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | :code:`mistty-after-process-end-hook` is called from within a MisTTY 76 | work buffer just after the process ended. The process is passed as an 77 | argument to the hook and its status can be accessed using 78 | :code:`process-status`. 79 | 80 | This can be used to, for example, kill the MisTTY work buffer after 81 | the shell exits successfully, with :code:`mistty-kill-buffer` or 82 | :code:`mistty-kill-buffer-and-window`. 83 | 84 | .. code-block:: elisp 85 | 86 | (add-hook 'mistty-after-process-end-hook 87 | 'mistty-kill-buffer-and-window) 88 | 89 | mistty-entered-fullscreen-hook 90 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 91 | 92 | :code:`mistty-entered-fullscreen-hook` is a normal hook called from 93 | within a MisTTY work buffer just after switching to fullscreen mode. 94 | 95 | In this mode, :code:`mistty-fullscreen` is non-nil and user commands 96 | run within the terminal buffer, available as 97 | :code:`mistty-term-buffer`. The work buffer is kept, but usually 98 | buried until :code:`mistty-toggle-buffers` is called. 99 | 100 | mistty-left-fullscreen-hook 101 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 102 | 103 | :code:`mistty-left-fullscreen-hook` is a normal hook called from 104 | within a MisTTY work buffer just after switching back to normal mode. 105 | 106 | In this mode, :code:`mistty-fullscreen` is nil and user commands run 107 | in the work buffer. 108 | 109 | .. _ext_osc: 110 | 111 | OSC Sequences 112 | ------------- 113 | 114 | OSC are “operating system command” control sequences. MisTTY supports 115 | some of these sequences and ignores the others. 116 | 117 | By default, the supported sequences are OSC 2 (set window title), 7 118 | (directory tracking, already mentioned in :ref:`dirtrack`) and 8 119 | (hyperlinks), thanks to :file:`ansi-osc.el`. 120 | 121 | .. index:: pair: variable; mistty-osc-handlers 122 | 123 | To add more, register handlers to :code:`mistty-osc-handlers`. The 124 | format is the same as the handlers supported for 125 | :code:`ansi-osc-handlers` and can usually be used interchangeably. 126 | 127 | When working on OSC handlers for MisTTY, it's important to keep the 128 | following in mind: MisTTY separate buffers for the terminal (a 129 | :code:`term-mode` buffer) and for MisTTY itself. The OSC handlers run 130 | in the term-mode buffer. 131 | 132 | .. index:: pair: variable; mistty-variables-to-copy 133 | 134 | One consequence of this is that if you set a buffer-local variable in 135 | a handler, it won't be available in the MisTTY buffer unless you 136 | register it to :kbd:`M-x configure-option mistty-variables-to-copy` 137 | 138 | MisTTY provides helpers for writing OSC handlers that set text 139 | properties: 140 | 141 | .. index:: 142 | pair: command; mistty-register-text-properties 143 | pair: command; mistty-unregister-text-properties 144 | 145 | - The function :code:`mistty-register-text-properties` registers a set 146 | of text properties to set on any text written to the terminal until 147 | :code:`mistty-unregister-text-properties` is called with the 148 | same argument. 149 | 150 | .. _custom-commands: 151 | 152 | Writing Your Own Commands 153 | ------------------------- 154 | 155 | You might find the following functions useful if you'd like to write 156 | commands that extend MisTTY's behavior: 157 | 158 | .. index:: pair: function; mistty-mode 159 | 160 | (mistty-mode): function 161 | This function sets the major mode of the current buffer to 162 | mistty-mode. This only useful when followed by a call to 163 | ``mistty-exec``, described below. 164 | 165 | .. index:: pair: function; mistty-exec 166 | 167 | (mistty-exec PROGRAM &optional :width WIDTH :height HEIGHT) 168 | This functions starts PROGRAM in the current buffer, which must 169 | be a ``mistty-mode`` buffer. 170 | 171 | PROGRAM is normally a list of executable and its argument. It can 172 | also be a string containing only the executable, if no arguments 173 | to be set. 174 | 175 | It is a good idea to display the buffer before calling this 176 | function, because the size of the terminal when PROGRAM is started 177 | is taken from the windows displaying the buffer. 178 | 179 | Example: 180 | 181 | .. code-block:: elisp 182 | 183 | (with-current-buffer (generate-new-buffer "*terminal*") 184 | (mistty-mode) 185 | (pop-to-buffer (current-buffer)) 186 | (mistty-exec '("bash" "-i"))) 187 | 188 | 189 | If the buffer isn't displayed, the terminal size is taken from the 190 | currently selected window, which might not be what you want. You can 191 | also set an arbitrary terminal size by passing the :width and 192 | :height optional keyword arguments and calling 193 | ``mistty-terminal-size-tracks-windows`` once the buffer has been 194 | tied to a window of a reasonable size, but that might cause a 195 | visible terminal refresh. 196 | 197 | .. index:: pair: function; mistty-send-string 198 | 199 | (mistty-send-string STR): function 200 | This function sends a string to the terminal, unmodified. The string 201 | that is sent appear only after the function return - and it might 202 | not ever appear at all depending on the application attached to the 203 | terminal. This is used to implement :code:`mistty-sudo` for example. 204 | 205 | .. index:: pair: function; mistty-on-prompt-p 206 | 207 | (mistty-on-prompt-p POS) : function 208 | This function returns non-nil if the POS is inside of a prompt 209 | MisTTY is aware of. This is useful for writing commands that behave 210 | differently on a prompt than on program output, even while inside of 211 | the terminal zone. It is used to implement 212 | :code:`mistty-beginning-of-line` for example. 213 | 214 | .. index:: pair: function; mistty-maybe-realize-possible-prompt 215 | 216 | (mistty-maybe-realize-possible-prompt &optional POS) : function 217 | This function might be useful to call in your commands to tell 218 | MisTTY that there's likely a prompt at the current pointer 219 | position or at POS. 220 | 221 | .. index:: pair: function; mistty-before-position 222 | 223 | (mistty-before-positional) : function 224 | This function not only checks whether there's a prompt at the 225 | position, but also attempt to move the terminal cursor to that 226 | position. 227 | 228 | .. _term-keymap: 229 | 230 | Terminal Keymap 231 | --------------- 232 | 233 | .. index:: 234 | pair: function; mistty-translate-key 235 | pair: map; mistty-term-key-map 236 | 237 | To forward a key binding to the application attached to the terminal 238 | `mistty-send-key` first needs to convert that key binding to something 239 | applications will understand. The translation is done by 240 | :code:`mistty-translate-key`. 241 | 242 | mistty-translate-key : function 243 | This function takes an Emacs key binding, as returned by `kbd` and 244 | returns a string containing the sequence of characters that 245 | correspond to that key to send to the application tied to the 246 | terminal. 247 | 248 | The default terminal keymap used by MisTTY mimics :program:`xterm` key 249 | bindings. You might extend it or change it by changing the map 250 | :code:`mistty-term-key-map`. 251 | 252 | For example, you can change the string that correspond to the first 253 | function keys from their default ("\\eOP" - "\\eOS") as follows: 254 | 255 | .. code-block:: elisp 256 | 257 | (define-key mistty-term-key-map (kbd "") "\e[11~") 258 | (define-key mistty-term-key-map (kbd "") "\e[12~") 259 | (define-key mistty-term-key-map (kbd "") "\e[13~") 260 | (define-key mistty-term-key-map (kbd "") "\e[14~") 261 | 262 | .. index:: pair: function; mistty-reverse-input-decode-map 263 | 264 | mistty-reverse-input-decode-map: function 265 | This function generates alternative values for 266 | :code:`mistty-term-key-map` for you if you'd like mimic another 267 | set of key bindings than xterm, for example, to generate a keymap 268 | that simulates rxvt, you might do: 269 | 270 | .. code-block:: elisp 271 | 272 | (load-library "term/rxvt.el") 273 | (mistty-reverse-input-decode-map rxvt-function-map) 274 | 275 | :file:`mistty-reverse-input-decode-map.el` is not included into the 276 | distribution; it's only available on `github 277 | `_. 278 | 279 | .. _autocomplete: 280 | 281 | Auto-complete 282 | ------------- 283 | 284 | .. index:: 285 | pair: variable; mistty-simulate-self-insert-command 286 | 287 | Auto-complete is a completion UI that shows up automatically after 288 | some delay, without having to call `completion-at-point`. This used 289 | not to work in MisTTY terminal region. The hook 290 | :code:`mistty-simulates-self-insert-command` was introduced to fix 291 | that. See :code:`mistty-interactive-insert-hook` in :ref:`hooks`. 292 | 293 | By default this hook calls the buffer :code:`pre-command-hook` and 294 | :code:`post-command-hook` with :code:`this-command` set to 295 | :code:`self-insert-command`, as this is the way auto-complete is 296 | normally triggered. This can be turned off if necessary using the 297 | option on :kbd:`M-x customize-option 298 | mistty-simulate-self-insert-command`. 299 | 300 | If the behavior described above doesn't work for some packages, it 301 | should be possible to build a custom bridge between this hook and the 302 | auto-completion package. 303 | 304 | .. _lrc: 305 | 306 | Long-running commands 307 | --------------------- 308 | 309 | In Emacs, most editing tools are run as a single Emacs command, but 310 | some tools span multiple Emacs command, for example, when you expand a 311 | snippet with `yasnippet `_, 312 | the snippet template is inserted into the buffer, together with 313 | placeholders for you to fill some missing information. 314 | 315 | Filling in a template is a series of Emacs commands, that, together, 316 | have a single effect: to insert a snippet of text. MisTTY calls this a 317 | long-running command. 318 | 319 | When run in the terminal region, such long-running commands fail as 320 | MisTTY sends the initial text to the shell, which echoes it back to be 321 | redisplayed, possibly jumbling things and definitely destroying any 322 | overlays. 323 | 324 | To avoid such situations, MisTTY holds back sending text to the shell 325 | until long-running commands are done. For that to work, MisTTY needs 326 | to know when such command start and end. 327 | 328 | You can tell whether MisTTY thinks a long-running command is active, 329 | as it displays *CMD* in the modeline. You can also do it 330 | programmatically: 331 | 332 | .. index:: 333 | pair: function; mistty-long-running-command-p 334 | 335 | mistty-long-running-running-command-p : function 336 | This function returns non-nil if MisTTY thinks a long-running 337 | command is active. 338 | 339 | 340 | .. index:: 341 | pair: variable; mistty-detect-foreign-overlays 342 | pair: option; mistty-detect-foreign-overlays 343 | pair: variable; mistty-foreign-overlay-properties 344 | pair: option; mistty-foreign-overlay-properties 345 | 346 | MisTTY detects some long-running commands by looking for overlays they 347 | typically add to the buffer. This can be extended with :kbd:`M-x 348 | customize-option mistty-foreign-overlay-properties` or turned off with 349 | :kbd:`M-x customize-option mistty-detect-foreign-overlays`. 350 | 351 | To add a new property to `mistty-foreign-overlay-properties`, start 352 | the interactive command, look for overlays with `overlays-in` then get 353 | their properties with `overlay-properties`. You can then choose, on 354 | that list, a property or face that identifies the feature or package. 355 | 356 | If you find yourself extending `mistty-foreign-overlay-properties`, 357 | please add an issue to https://github.com/szermatt/mistty/issues/new 358 | so it can be integrated into the next version. 359 | 360 | Alternatively, as not all long-running commands that can be confused 361 | by MisTTY use overlays, you might need to tell MisTTY about them. 362 | MisTTY does it already for :code:`completion-in-region`. 363 | 364 | .. index:: 365 | pair: function; mistty-report-long-running-command 366 | 367 | mistty-report-long-running-command : function 368 | This function can be called to tell MisTTY when a long-running 369 | command start and end. It's typically called from hooks provided 370 | by the package of the long-running command. 371 | 372 | Here's an example of code that would detect 373 | :code:`completion-in-region-mode` if MisTTY didn't already do it: 374 | 375 | .. code-block:: elisp 376 | 377 | (defun my-completion-in-region () 378 | (mistty-report-long-running-command 379 | 'my-completion-in-region completion-in-region-mode)) 380 | (defun my-detect-completion-in-region () 381 | (add-hook 'completion-in-region-mode-hook 382 | #'my-completion-in-region nil t)) 383 | (add-hook 'mistty-mode-hook #'my-detect-completion-in-region) 384 | -------------------------------------------------------------------------------- /test/mistty-accum-test.el: -------------------------------------------------------------------------------- 1 | ;;; Tests mistty-accum.el -*- lexical-binding: t -*- 2 | 3 | ;; This program is free software: you can redistribute it and/or 4 | ;; modify it under the terms of the GNU General Public License as 5 | ;; published by the Free Software Foundation; either version 3 of the 6 | ;; License, or (at your option) any later version. 7 | 8 | ;; This program is distributed in the hope that it will be useful, 9 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | ;; General Public License for more details. 12 | 13 | ;; You should have received a copy of the GNU General Public License 14 | ;; along with this program. If not, see 15 | ;; `http://www.gnu.org/licenses/'. 16 | 17 | (require 'mistty-accum) 18 | (require 'mistty-accum-macros) 19 | (require 'ert) 20 | (require 'ert-x) 21 | 22 | (require 'mistty-testing) 23 | 24 | (ert-deftest mistty-accum-smoke () 25 | (mistty-with-test-process (proc) 26 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 27 | (set-process-filter proc accum) 28 | 29 | (process-send-string proc "hello, ") 30 | (process-send-string proc "world!") 31 | 32 | (while (accept-process-output nil 0.1)) 33 | 34 | (should (equal "hello, world!" 35 | (mistty-test-proc-buffer-string proc)))))) 36 | 37 | (ert-deftest mistty-accum-processor-remove-seq () 38 | (mistty-with-test-process (proc) 39 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 40 | (set-process-filter proc accum) 41 | 42 | (mistty--accum-add-processor 43 | accum '(seq ESC ?=) #'ignore) 44 | 45 | (funcall accum proc "foo\e=bar") 46 | (should (equal "foobar" 47 | (mistty-test-proc-buffer-string proc)))))) 48 | 49 | (ert-deftest mistty-accum-processor-forward-data () 50 | (mistty-with-test-process (proc) 51 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 52 | (set-process-filter proc accum) 53 | 54 | (mistty--accum-add-processor 55 | accum '(seq ESC ?=) 56 | (lambda (ctx str) 57 | (mistty--accum-ctx-push-down ctx str))) 58 | 59 | (funcall accum proc "foo\e=bar") 60 | (should (equal "foo\e=bar" 61 | (mistty-test-proc-buffer-string proc)))))) 62 | 63 | (ert-deftest mistty-accum-processor-push-down-data () 64 | (mistty-with-test-process (proc) 65 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 66 | (set-process-filter proc accum) 67 | 68 | (mistty--accum-add-processor 69 | accum '(seq ESC ?=) 70 | (lambda (ctx _) 71 | (mistty--accum-ctx-push-down ctx "-") 72 | (mistty--accum-ctx-push-down ctx "-"))) 73 | 74 | (funcall accum proc "foo\e=bar") 75 | (should (equal "foo--bar" 76 | (mistty-test-proc-buffer-string proc)))))) 77 | 78 | (ert-deftest mistty-accum-processor-flush () 79 | (mistty-with-test-process (proc) 80 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 81 | (set-process-filter proc accum) 82 | 83 | (mistty--accum-add-processor 84 | accum '(seq ESC ?=) 85 | (lambda (ctx _) 86 | (mistty--accum-ctx-flush ctx) 87 | (should (equal "foo" 88 | (mistty-test-proc-buffer-string proc))))) 89 | 90 | (funcall accum proc "foo\e=bar") 91 | (should (equal "foobar" 92 | (mistty-test-proc-buffer-string proc)))))) 93 | 94 | (ert-deftest mistty-accum-processor-flush-and-push-down () 95 | (mistty-with-test-process (proc) 96 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 97 | (set-process-filter proc accum) 98 | 99 | (mistty--accum-add-processor 100 | accum '(seq ESC ?=) 101 | (lambda (ctx data) 102 | ;; Anything pushed before flush is visible afterwards. 103 | (mistty--accum-ctx-push-down ctx "-") 104 | (mistty--accum-ctx-flush ctx) 105 | (should (equal "foo-" 106 | (mistty-test-proc-buffer-string proc))) 107 | 108 | ;; There can be more push-down and flushes. 109 | (mistty--accum-ctx-push-down ctx "-") 110 | (mistty--accum-ctx-push-down ctx "-") 111 | (mistty--accum-ctx-flush ctx) 112 | (should (equal "foo---" 113 | (mistty-test-proc-buffer-string proc))) 114 | 115 | ;; Anything pushed after the flush is not visible 116 | ;; until later. 117 | (mistty--accum-ctx-push-down ctx "-") 118 | (should (equal "foo---" 119 | (mistty-test-proc-buffer-string proc))))) 120 | 121 | (funcall accum proc "foo\e=bar") 122 | (should (equal "foo----bar" 123 | (mistty-test-proc-buffer-string proc)))))) 124 | 125 | (ert-deftest mistty-accum-post-processor () 126 | (mistty-with-test-process (proc) 127 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 128 | (set-process-filter proc accum) 129 | 130 | (mistty--accum-add-post-processor 131 | accum 132 | (lambda () 133 | (upcase-region (point-min) (point-max)))) 134 | 135 | (funcall accum proc "foobar") 136 | 137 | (should (equal "FOOBAR" 138 | (mistty-test-proc-buffer-string proc)))))) 139 | 140 | (ert-deftest mistty-accum-post-processor-and-processors () 141 | (mistty-with-test-process (proc) 142 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 143 | (set-process-filter proc accum) 144 | 145 | (mistty--accum-add-post-processor 146 | accum 147 | (lambda () 148 | (upcase-region (point-min) (point-max)))) 149 | 150 | (mistty--accum-add-processor 151 | accum '(seq ESC ?=) 152 | (lambda (ctx _) 153 | (mistty--accum-ctx-push-down ctx "-foo-") 154 | (mistty--accum-ctx-flush ctx) 155 | ;; The post-process hasn't been called on the current data 156 | ;; yet. It's only called at the end. 157 | (should (equal "FIRST-before-foo-" 158 | (mistty-test-proc-buffer-string proc))))) 159 | 160 | (funcall accum proc "first-") 161 | (funcall accum proc "before\e=after") 162 | 163 | (should (equal "FIRST-BEFORE-FOO-AFTER" 164 | (mistty-test-proc-buffer-string proc)))))) 165 | 166 | (ert-deftest mistty-accum-post-processor-called-in-order () 167 | (mistty-with-test-process (proc) 168 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 169 | (set-process-filter proc accum) 170 | 171 | (mistty--accum-add-post-processor 172 | accum (lambda () (insert "1"))) 173 | 174 | (mistty--accum-add-post-processor 175 | accum (lambda () (insert "2"))) 176 | 177 | (funcall accum proc "foo") 178 | 179 | (should (equal "foo12" 180 | (mistty-test-proc-buffer-string proc)))))) 181 | 182 | (ert-deftest mistty-accum-around () 183 | (mistty-with-test-process (proc) 184 | (let* ((calls nil) 185 | (real-process-filter (process-filter proc)) 186 | (accum (mistty--make-accumulator (lambda (proc data) 187 | (push 'process-filter calls) 188 | (funcall real-process-filter proc data))))) 189 | (set-process-filter proc accum) 190 | 191 | (funcall accum proc "baa") 192 | (should (equal '(process-filter) (reverse calls))) 193 | (setq calls nil) 194 | 195 | (mistty--accum-add-around-process-filter 196 | accum 197 | (lambda (func) 198 | (push 'around-1 calls) 199 | (funcall func))) 200 | 201 | (funcall accum proc "baa") 202 | (should (equal '(around-1 process-filter) (reverse calls))) 203 | (setq calls nil) 204 | 205 | (mistty--accum-add-around-process-filter 206 | accum 207 | (lambda (func) 208 | (push 'around-2 calls) 209 | (funcall func))) 210 | 211 | (funcall accum proc "baa") 212 | (should (equal '(around-2 around-1 process-filter) 213 | (reverse calls))) 214 | (setq calls nil)))) 215 | 216 | (ert-deftest mistty-accum-around-and-processors () 217 | (mistty-with-test-process (proc) 218 | (let* ((calls nil) 219 | (accum (mistty--make-accumulator (process-filter proc)))) 220 | (set-process-filter proc accum) 221 | 222 | (mistty--accum-add-around-process-filter 223 | accum 224 | (lambda (func) 225 | (push 'around calls) 226 | (funcall func))) 227 | 228 | (mistty--accum-add-processor 229 | accum '(seq "aa") 230 | (lambda (ctx str) 231 | (mistty--accum-ctx-push-down ctx "oo") 232 | 233 | ;; around should be called from flush 234 | (let ((call-count (length calls))) 235 | (mistty--accum-ctx-flush ctx) 236 | (should (equal (1+ call-count) (length calls)))))) 237 | 238 | (funcall accum proc "baa, baa, black sheep") 239 | 240 | ;; Around should be called once for each "aa" and once at the 241 | ;; end. 242 | (should (equal 3 (length calls))) 243 | 244 | (should (equal "boo, boo, black sheep" 245 | (mistty-test-proc-buffer-string proc)))))) 246 | 247 | (ert-deftest mistty-accum-hold-back () 248 | (mistty-with-test-process (proc) 249 | (let* ((capture nil) ;; captured data in reverse order 250 | (accum (mistty--make-accumulator 251 | (lambda (proc data) 252 | (push data capture))))) 253 | (set-process-filter proc accum) 254 | 255 | (mistty--accum-add-processor-1 256 | accum (mistty--accum-make-processor 257 | :regexp "\e\\[[0-9;]*J" 258 | :hold-back-regexps '("\e" "\e\\[[0-9;]*") 259 | :func (lambda (ctx str) 260 | (mistty--accum-ctx-push-down ctx str)))) 261 | (funcall accum proc "foo\e") 262 | (funcall accum proc "[") 263 | (funcall accum proc "2") 264 | (funcall accum proc "J") 265 | (funcall accum proc "-\e[") 266 | (funcall accum proc "Jbar") 267 | (should (equal '("foo" "\e[2J" "-" "\e[Jbar") 268 | (reverse capture)))))) 269 | 270 | (ert-deftest mistty--accum-build-hold-back () 271 | ;; test string 272 | (should (equal '("h" "he" "hel" "hell") 273 | (mistty--accum-build-hold-back "hello"))) 274 | 275 | ;; test seq 276 | (should (equal 277 | '("\e") 278 | (mistty--accum-build-hold-back 279 | '(seq ?\e ?\=)))) 280 | 281 | ;; test star 282 | (should (equal 283 | '("\e" "\e[0-9;]*") 284 | (mistty--accum-build-hold-back 285 | '(seq ?\e (* (char "0-9;")) ?\J)))) 286 | 287 | ;; test optional and plus 288 | (should (equal 289 | '("\e" "\e[0-9]+" "\e[0-9]+;" "\e[0-9]+;[0-9]+") 290 | (mistty--accum-build-hold-back 291 | '(seq ?\e (? (seq (+ (char "0-9")) ?\; (+ (char "0-9")))) ?H)))) 292 | 293 | ;; test seq in star 294 | (should (equal 295 | '("\e" "\e\\(?:[0-9];\\)*" "\e\\(?:[0-9];\\)*[0-9]") 296 | (mistty--accum-build-hold-back 297 | '(seq ?\e (* (seq (char "0-9") ?\;)) ?H)))) 298 | 299 | ;; test or 300 | (should (equal 301 | '("\e" "\e]" "\e][a-z]*" "\e][a-z]*\e") 302 | (mistty--accum-build-hold-back 303 | '(seq ?\e ?\] (* (char "a-z")) (or ?\a (seq ?\e ?\\)))))) 304 | 305 | ;; test or followed by something 306 | (should (equal 307 | '("\e" "\e]" "\e][a-z]*" "\e][a-z]*\e" 308 | "\e][a-z]*\\(?:\a\\|\e\\\\\\)") 309 | (mistty--accum-build-hold-back 310 | '(seq ?\e ?\] (* (char "a-z")) (or ?\a (seq ?\e ?\\)) ?.))))) 311 | 312 | (ert-deftest mistty--accum-build-hold-back-csi-to-regexp () 313 | ;; CSI includes [, which must be quoted properly 314 | (should (equal '("\e" "\e\\[" "\e\\[[0-9]*") 315 | (mistty--accum-build-hold-back 316 | (mistty--accum-expand-shortcuts 317 | '(seq CSI Ps ?J)))))) 318 | 319 | (defun mistty-test-expand-shortcuts (tree) 320 | (rx-to-string (mistty--accum-expand-shortcuts tree) 'no-group)) 321 | 322 | (ert-deftest mistty--accum-expand-shortcuts-single () 323 | (should (equal 324 | "\e\\[" 325 | (mistty-test-expand-shortcuts 'CSI))) 326 | (should (equal 327 | "\e]" 328 | (mistty-test-expand-shortcuts 'OSC))) 329 | (should (equal 330 | "[0-9]*" 331 | (mistty-test-expand-shortcuts 'Ps))) 332 | (should (equal 333 | "[0-9;]*" 334 | (mistty-test-expand-shortcuts 'Pm))) 335 | (should (equal 336 | "\a\\|\e\\\\" 337 | (mistty-test-expand-shortcuts 'ST)))) 338 | 339 | (ert-deftest mistty--accum-expand-shortcuts-recursive () 340 | (should (equal "\e\\[[0-9]*J" 341 | (mistty-test-expand-shortcuts '(seq CSI Ps ?J)))) 342 | 343 | (should (equal "\e][^\0-\7\x0e-\x1f\x7f]*\\(?:\a\\|\e\\\\\\)" 344 | (mistty-test-expand-shortcuts '(seq OSC Pt ST))))) 345 | 346 | (ert-deftest mistty-accum-test-split-incomplete-chars () 347 | (ert-with-test-buffer () 348 | (should (equal '("foo" . nil) 349 | (mistty--split-incomplete-chars "foo"))) 350 | 351 | (should (equal '("foo bar abcde\342\224\200" . nil) 352 | (mistty--split-incomplete-chars 353 | "foo bar abcde\342\224\200"))) 354 | 355 | (should (equal '("foo bar abcde\342\224\200" . "\342\224") 356 | (mistty--split-incomplete-chars 357 | "foo bar abcde\342\224\200\342\224"))))) 358 | 359 | (ert-deftest mistty-accum-test-join-incomplete-chars () 360 | (mistty-with-test-process (proc) 361 | (let ((accum (mistty--make-accumulator (process-filter proc)))) 362 | (set-process-filter proc accum) 363 | 364 | (funcall accum proc "|\342\224\200") 365 | (funcall accum proc "\342") (funcall accum proc "\224\200") 366 | (funcall accum proc "\342\224") (funcall accum proc "\200") 367 | (funcall accum proc "\342\224\200|") 368 | 369 | (should (equal "|────|" 370 | (decode-coding-string 371 | (mistty-test-proc-buffer-string proc) 372 | 'utf-8)))))) 373 | 374 | (ert-deftest mistty-accum-processor-look-back () 375 | (mistty-with-test-process (proc) 376 | (let ((accum (mistty--make-accumulator (process-filter proc))) 377 | (lookbacks nil)) 378 | (set-process-filter proc accum) 379 | 380 | ;; Change each aa with oo and remember what the lookback buffer 381 | ;; looked before and after adding the oo. 382 | (mistty--accum-add-processor 383 | accum '(seq ?a ?a) 384 | (lambda (ctx _) 385 | (push (mistty--accum-ctx-look-back ctx) lookbacks) 386 | (mistty--accum-ctx-push-down ctx "oo") 387 | (push (mistty--accum-ctx-look-back ctx) lookbacks))) 388 | 389 | ;; Force a flush at every comma. This should have no impact. 390 | (mistty--accum-add-processor 391 | accum ?, 392 | (lambda (ctx _) 393 | (mistty--accum-ctx-push-down ctx ",") 394 | (mistty--accum-ctx-flush ctx))) 395 | 396 | (funcall accum proc ">") 397 | (funcall accum proc "baa, baa, baa!") 398 | 399 | (should (equal '(">b" ; first baa 400 | ">boo" ; first baa, oo was pushed 401 | ">boo, b" ; second baa 402 | "boo, boo" ; second baa, oo was pushed, 403 | ", boo, b" ; third baa 404 | "boo, boo" ; third baa, oo was pushed 405 | ) 406 | (nreverse lookbacks)))))) 407 | --------------------------------------------------------------------------------