-
Notifications
You must be signed in to change notification settings - Fork 7
Implement support for atomic regions in mistty #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
;;; mistty-atomic.el --- Atomic update support for mistty -*- lexical-binding: t -*- | ||
|
||
;; This program is free software: you can redistribute it and/or | ||
;; modify it under the terms of the GNU General Public License as | ||
;; published by the Free Software Foundation; either version 3 of the | ||
;; License, or (at your option) any later version. | ||
|
||
;; This program is distributed in the hope that it will be useful, | ||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
;; General Public License for more details. | ||
|
||
;; You should have received a copy of the GNU General Public License | ||
;; along with this program. If not, see | ||
;; `http://www.gnu.org/licenses/'. | ||
|
||
;;; Commentary: | ||
;; | ||
;; This file implements synchronized/atomic terminal updates based on | ||
;; https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 | ||
;; | ||
;; It intercepts CSI ? 2026 h/l sequences and buffers terminal output | ||
;; during atomic updates to prevent screen tearing. | ||
|
||
;;; Code: | ||
|
||
(require 'mistty-util) | ||
(require 'mistty-accum) | ||
|
||
(defcustom mistty-atomic-timeout-s 1.0 | ||
"Maximum seconds to buffer during atomic update before forcing flush." | ||
:type 'number | ||
:group 'mistty) | ||
|
||
;; State table: (expected-char next-state-if-match next-state-if-no-match) | ||
(defconst mistty--atomic-state-table | ||
[("\e" 1 0) ; 0: looking for \e | ||
(?\[ 2 0) ; 1: saw \e | ||
(?? 3 0) ; 2: saw \e[ | ||
(?2 4 0) ; 3: saw \e[? | ||
(?0 5 0) ; 4: saw \e[?2 | ||
(?2 6 0) ; 5: saw \e[?20 | ||
(?6 7 0) ; 6: saw \e[?202 | ||
(?h 8 0) ; 7: saw \e[?2026 | ||
("\e" 9 8) ; 8: in atomic mode, looking for \e | ||
(?\[ 10 8) ; 9: saw \e during atomic | ||
(?? 11 8) ; 10: saw \e[ | ||
(?2 12 8) ; 11: saw \e[? | ||
(?0 13 8) ; 12: saw \e[?2 | ||
(?2 14 8) ; 13: saw \e[?20 | ||
(?6 15 8) ; 14: saw \e[?202 | ||
(?l 0 8)] ; 15: saw \e[?2026 | ||
"State table for atomic update parser.") | ||
|
||
(defun mistty--substring-fast (string start end) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional |
||
"Efficient substring that avoids copy when possible." | ||
(if (and (= start 0) (= end (length string))) | ||
string | ||
(substring string start end))) | ||
|
||
(defun mistty--make-atomic-preprocessor (proc) | ||
"Create an atomic update preprocessor for process PROC. | ||
Return (PREPROCESSOR . FORCE-EXIT), where PREPROCESSOR is a function | ||
suitable as an accumulator preprocessor and FORCE-EXIT is a function of | ||
no arguments that, when called, will make the preprocessor exit an | ||
atomic region (if it's inside one) and dump any accumulated bytes ASAP." | ||
(let ((parse-state 0) | ||
(chunks nil) | ||
(timer nil) | ||
(force-exit nil)) | ||
(cl-labels | ||
((force-exit-atomic-mode () | ||
(when (and (process-live-p proc) (>= parse-state 8)) | ||
(setq force-exit t) | ||
(funcall (process-filter proc) proc ""))) | ||
(push-down-atomic-chunks (next) | ||
(when timer | ||
(cancel-timer timer) | ||
(setq timer nil)) | ||
(dolist (chunk (nreverse chunks)) | ||
(funcall next chunk)) | ||
(setq chunks nil)) | ||
|
||
(process-data (next data) | ||
(when force-exit | ||
(when (>= parse-state 8) | ||
(push-down-atomic-chunks next) | ||
(setq parse-state 0)) | ||
(setq force-exit nil)) | ||
(let* ((last-flush-pos 0) | ||
(pos 0) | ||
(len (length data)) | ||
;; Any sequence before this point will be found | ||
;; by regular string search and so would be | ||
;; pointless to scan for partial sequences. | ||
(possible-end-pos (- len 7))) | ||
(cl-labels | ||
((flush (state pos) | ||
(when (< last-flush-pos pos) | ||
(let ((chunk (mistty--substring-fast data last-flush-pos pos))) | ||
(if (< state 8) | ||
;; Not in atomic mode - send downstream | ||
(funcall next chunk) | ||
;; In atomic mode - accumulate | ||
(push chunk chunks))) | ||
(setq last-flush-pos pos)))) | ||
(while (< pos len) | ||
(let ((old-state parse-state)) | ||
(or | ||
;; Fast search for whole escapes | ||
(and (< pos possible-end-pos) | ||
(or (= parse-state 0) (= parse-state 8)) | ||
(if-let* ((found-pos | ||
(string-search | ||
(if (= parse-state 0) "\e[?2026h" "\e[?2026l") | ||
data pos))) | ||
;; Found complete sequence - fast forward | ||
(setq pos (+ found-pos 8) | ||
old-state (if (= parse-state 0) 7 15) | ||
parse-state (if (= parse-state 0) 8 0)) | ||
;; No complete sequence - jump to near end | ||
(setq pos (max pos possible-end-pos))))< 1E79 /span> | ||
;; State machine for partial sequences near end | ||
(when (< pos len) | ||
(pcase-let* | ||
((`(,expected ,next-match ,next-no-match) | ||
(aref mistty--atomic-state-table parse-state))) | ||
(or | ||
(and (stringp expected) | ||
(let ((found-pos (string-search expected data pos))) | ||
(if found-pos | ||
(setq pos (+ found-pos (length expected)) | ||
old-state parse-state | ||
parse-state next-match) | ||
(setq pos len parse-state next-no-match)))) | ||
(and (= (aref data pos) expected) | ||
(setq parse-state next-match pos (1+ pos))) | ||
;; No match: reset state and re-try this position | ||
(setq parse-state next-no-match))))) | ||
|
||
;; React to state changes | ||
(cond | ||
((and (= old-state 7) (= parse-state 8)) ; Enter atomic | ||
(mistty-log "ATOMIC REGION ENTER") | ||
(flush 0 (max 0 (- pos 8))) | ||
(cl-assert (null timer)) | ||
(cl-assert (null chunks)) | ||
(when timer | ||
(cancel-timer timer) | ||
(setq timer nil)) | ||
(setq timer (run-at-time mistty-atomic-timeout-s | ||
nil #'force-exit-atomic-mode))) | ||
|
||
((and (= old-state 15) (= parse-state 0)) ; Leave atomic | ||
;; No need for an explicit (flush ...) here: | ||
;; it's fine to send the atomic-region suffix | ||
;; and subsequent non-atomic data to the next | ||
;; pipeline stage as one string; this way, we | ||
;; can avoid splitting the string. | ||
(mistty-log "ATOMIC REGION NORMAL EXIT") | ||
(push-down-atomic-chunks next))))) | ||
|
||
;; Final flush at end | ||
(flush parse-state len))))) | ||
(cons #'process-data #'force-exit-atomic-mode)))) | ||
|
||
(provide 'mistty-atomic) | ||
|
||
;;; mistty-atomic.el ends here |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,7 @@ | |
(require 'mistty-log) | ||
(require 'mistty-queue) | ||
(require 'mistty-undo) | ||
(require 'mistty-atomic) | ||
|
||
(defvar term-width) ; defined in term.el | ||
|
||
|
@@ -719,6 +720,12 @@ This is updated at the same time as the marker, on both buffers.") | |
(defvar-local mistty--active-prompt nil | ||
"A `mistty--prompt' struct of the active prompt.") | ||
|
||
(defvar-local mistty--atomic-preprocessor nil | ||
"The atomic update preprocessor.") | ||
|
||
(defvar-local mistty--atomic-preprocessor-flush-f nil | ||
"Function to call to exit atomic mode early") | ||
|
||
(defvar-local mistty--sync-ov nil | ||
"An overlay that covers the region [`mistty-sync-marker', `(point-max)']. | ||
|
||
|
@@ -1037,6 +1044,12 @@ buffer and `mistty-proc' to that buffer's process." | |
(when proc | ||
(let ((accum (process-filter proc))) | ||
(mistty--accum-reset accum) | ||
(unless mistty--atomic-preprocessor | ||
(pcase-setq `(,mistty--atomic-preprocessor . | ||
,mistty--atomic-preprocessor-flush-f) | ||
(mistty--make-atomic-preprocessor proc))) | ||
(mistty--accum-add-pre-process-filter | ||
accum mistty--atomic-preprocessor) | ||
(mistty--add-prompt-detection accum) | ||
(mistty--add-osc-detection accum) | ||
(mistty--add-skip-unsupported accum) | ||
|
@@ -1107,7 +1120,9 @@ Returns M or a new marker." | |
(setq mistty--queue nil)) | ||
(when mistty-proc | ||
(let ((accum (process-filter mistty-proc))) | ||
(mistty--accum-reset accum)) | ||
(mistty--accum-reset accum) | ||
(setq mistty--atomic-preprocessor nil | ||
mistty--atomic-preprocessor-flush-f nil)) | ||
(set-process-sentinel mistty-proc #'term-sentinel) | ||
(setq mistty-proc nil))) | ||
|
||
|
@@ -2358,6 +2373,9 @@ This command is available in fullscreen mode." | |
(fire-and-forget (or mistty--forbid-edit | ||
(string-match "^[[:graph:]]+$" translated-key))) | ||
(positional (or positional (mistty-positional-p key)))) | ||
(mistty--with-live-buffer mistty-work-buffer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. required Should sending a key really flush the buffer? If atomic region is used for reasonably-sized changes, it would make sense that a user would send a command that requires a refresh, then start typing just after. Flushing for every key press would reveal changes that should be atomic. Are you worried about the screen freezing, without the user knowing what to do about it? |
||
(when mistty--atomic-preprocessor-flush-f | ||
(funcall mistty--atomic-preprocessor-flush-f))) | ||
(cond | ||
((and (buffer-live-p mistty-work-buffer) | ||
(not (buffer-local-value | ||
|
@@ -3654,7 +3672,10 @@ Width and height are limited to `mistty-min-terminal-width' and | |
(height (max height mistty-min-terminal-height))) | ||
(mistty--with-live-buffer mistty-term-buffer | ||
(set-process-window-size mistty-proc height width) | ||
(term-reset-size height width))))) | ||
(term-reset-size height width)) | ||
(mistty--with-live-buffer mistty-work-buffer | ||
(when mistty--atomic-preprocessor-flush-f | ||
(funcall mistty--atomic-preprocessor-flush-f)))))) | ||
|
||
(defun mistty--enter-fullscreen (proc) | ||
"Enter fullscreen mode for PROC." | ||
|
@@ -3680,6 +3701,8 @@ Width and height are limited to `mistty-min-terminal-width' and | |
|
||
(let ((accum (process-filter proc))) | ||
(mistty--accum-reset accum) | ||
(when mistty--atomic-preprocessor | ||
(mistty--accum-add-pre-process-filter accum mistty--atomic-preprocessor)) | ||
(mistty--add-osc-detection accum) | ||
(mistty--add-skip-unsupported accum) | ||
(mistty--add-toggle-cursor accum mistty-term-buffer) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an observation This duplicates logic that is supposed to be handled by the processors.
Ideally you should be able to just do:
And the processor logic would take care of incomplete commands.
That wouldn't work in this case, because, a processor cannot act as a buffer the way a pre-process does. Ideally, however, it should be possible to use them even in this case - I just don't know how at this time.
I'm not telling you to change anything in this pull request, just pointing out something that I see might be worth changing in the future..