8000 Implement support for atomic regions in mistty by dcolascione · Pull Request #61 · szermatt/mistty · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions mistty-accum.el
10000
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ Usage example:
;; mistty--accum-add-post-processor
add-post-processor-f
;; mistty--accum-add-around-process-filter
add-around-f)
add-around-f
;; mistty--accum-add-pre-process-filter
add-pre-process-f)

(defsubst mistty--accum-reset (accum)
"Reset ACCUM to its post-creation state.
Expand Down Expand Up @@ -112,6 +114,26 @@ that buffer might be killed during processing and break process
filtering."
(funcall (mistty--accumulator--add-around-f accum) func))

(defsubst mistty--accum-add-pre-process-filter (accum func)
"Have FUNC filter the raw bytes delivered to the accumulator.

FUNC must be a function with the signature (NEXTFUNC DATA), with
NEXTFUNC the function to call to accept unprocessed bytes into the
accumualtor or the next pre-processor filter. DATA is unprocessed bytes
from either Emacs or the previous pre-process filter. NEXTFUNC expects
one parameter, the pre-processed unprocessed bytes. The filter may call
NEXTFUNC zero or more times for any one call to itself.
The pre-processing filter need not be reentrant: mistty takes care of
reentrancy on its own.

Note that FUNC is *not* called with any specific active buffer, just
like any process filter. The function should make sure to set the buffer
it needs and react to it having been killed. Further, the function
should avoid calling the function it wraps with any buffer active, as
that buffer might be killed during processing and break process
filtering."
(funcall (mistty--accumulator--add-pre-process-f accum) func))

(defsubst mistty--accum-add-processor-1 (accum processor)
"Register PROCESSOR, a `cl-accum-processor' in ACCUM.

Expand Down Expand Up @@ -199,10 +221,13 @@ The return value of this type is also an oclosure of type
mistty--accum whose slots can be accessed."
(let ((post-processors nil)
(around-process-filter nil)
(pre-processors nil)
(processors nil)
(processors-dirty nil)
(unprocessed (mistty--make-fifo))
(unprocessed-bytes 0)
(pre-processed (mistty--make-fifo))
(pre-processed-bytes 0)
(processed (mistty--make-fifo))
(look-back-ring (make-ring 8))
(incomplete nil)
Expand All @@ -215,6 +240,7 @@ mistty--accum whose slots can be accessed."
((reset ()
(setq post-processors nil)
(setq processors nil)
(setq pre-processors nil)
(setq around-process-filter nil)
(setq processors-dirty t))

Expand All @@ -233,6 +259,9 @@ mistty--accum whose slots can be accessed."
(add-around (func)
(push func around-process-filter))

(add-pre-processor (func)
(push func pre-processors))

;; Collect all pending strings from FIFO into one
;; single string.
;;
Expand Down Expand Up @@ -287,7 +316,7 @@ mistty--accum whose slots can be accessed."
processing-pending-output))))
(when (or (>= duration
mistty--max-accumulate-delay)
(>= unprocessed-bytes
(>= (+ unprocessed-bytes pre-processed-bytes)
mistty--max-accumulate-bytes))
(throw 'mistty-stop-accumlating nil))))
(prog1 t ; flush
Expand Down Expand Up @@ -370,11 +399,30 @@ mistty--accum whose slots can be accessed."
(ring-ref look-back-ring (- len i 1))))
str))

;; Process any data in unprocessed and move it to processed.
(process-data (proc)
(pre-process (data pre-processors base)

(if (null pre-processors)
(funcall base data)
(funcall (car pre-processors)
(lambda (next-data)
(pre-process next-data (cdr pre-processors) base))
data)))

;; Run pre-processors on unprocessed data and move to pre-processed.
(run-pre-processors ()
(while (not (mistty--fifo-empty-p unprocessed))
(let ((data (concat incomplete (fifo-to-string unprocessed))))
(let ((data (fifo-to-string unprocessed)))
(setq unprocessed-bytes 0)
(pre-process data pre-processors
(lambda (processed-data)
(mistty--fifo-enqueue pre-processed processed-data)
(cl-incf pre-processed-bytes (length processed-data)))))))

;; Process any data in pre-processed and move it to processed.
(process-data (proc)
(while (not (mistty--fifo-empty-p pre-processed))
(let ((data (concat incomplete (fifo-to-string pre-processed))))
(setq pre-processed-bytes 0)
(setq incomplete nil)
(while (not (string-empty-p data))
(update-processor-regexps)
Expand Down Expand Up @@ -417,13 +465,16 @@ mistty--accum whose slots can be accessed."
(oclosure-lambda (mistty--accumulator (reset-f #'reset)
(add-post-processor-f #'add-post-processor)
(add-processor-f #'add-processor)
(add-around-f #'add-around))
(add-around-f #'add-around)
(add-pre-process-f #'add-pre-processor))
(proc data)
(mistty--fifo-enqueue unprocessed data)
(cl-incf unprocessed-bytes (length data))

(when (toplevel-accumulator-p proc)
(process-data proc)
(while (not (mistty--fifo-empty-p unprocessed))
(run-pre-processors)
(process-data proc))
(flush proc)
(post-process proc))))))

Expand Down
169 changes: 169 additions & 0 deletions mistty-atomic.el
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
Copy link
Owner

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:

(mistty--add-processor accum '(seq CSI "?2026l") (lambda ...))
(mistty--add-processor accum '(seq CSI "?2026h") (lambda ...))

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..

[("\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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional mistty--substring-fast would be very much at home in mistty-util.el

"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
27 changes: 25 additions & 2 deletions mistty.el
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
(require 'mistty-log)
(require 'mistty-queue)
(require 'mistty-undo)
(require 'mistty-atomic)

(defvar term-width) ; defined in term.el

Expand Down Expand Up @@ -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)'].

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)))

Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The 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?
In mistty--post-command, there is code that reacts to the user pressing C-g (keyboard-quit). There would be a good place for flushing, I think.

(when mistty--atomic-preprocessor-flush-f
(funcall mistty--atomic-preprocessor-flush-f)))
(cond
((and (buffer-live-p mistty-work-buffer)
(not (buffer-local-value
Expand Down Expand Up @@ -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."
Expand All @@ -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)
Expand Down
0