#lang racket/base ;; Copyright (c) Neil Van Dyke. See file "info.rkt". (require (for-syntax racket/base racket/syntax) racket/system (planet neil/mcfly)) (doc (section "Introduction") (para "The " "CharTerm" " package provides a Racket interface for character-cell video display terminals on Unix-like systems -- such as for " (as-index "GNU Screen") " and " (as-index (code "tmux")) " sessions on " (index '("cloud server" "server") "cloud servers") ", " (as-index "XTerm") " windows on a workstation desktop, and some older hardware terminals (even the venerable " (as-index "DEC VT100") "). Currently, it implements a subset of features available on most terminals.") (para "This package could be used to implement a status/management console for a Racket-based server process (perhaps run in GNU Screen or " (code "tmux") " on a server machine, to be detached and reattached from SSH sessions), a lightweight user interface for a systems tool, a command-line REPL, a text editor, creative retro uses of old equipment, and, perhaps most importantly, a " ;; (hyperlink "http://en.wikipedia.org/wiki/Rogue_%28computer_game%29" "Rogue-like" ;;) " application.") (para "The " "CharTerm" " package does not include any native code (such as from " (as-index (code "terminfo")) ", " (as-index (code "termcap")) ", " (as-index (code "curses")) ", or " (as-index (code "ncurses")) ") in the Racket process, such as through the Racket FFI or C extensions, so there is less potential for a problem involving native code to threaten the reliability or security of a program. " "CharTerm" " is implemented in pure Racket code except for executing " (code "/bin/stty") " for some purposes. Specifically, " (code "/bin/stty") " at startup time and shutdown time, to set modes, and (for terminal types that don't seem to support a screen size report control sequence) when getting screen size. Besides security and stability, lower dependence on native code might also simplify porting to host platforms that don't have those native code facilities.")) (doc (subsection "Demo") (para "For a demonstration, the following command, run from a terminal, should install the " "CharTerm" " package (if not already installed), and run the demo:") (commandline "racket -pm neil/charterm/demo") (para "This demo reports what keys you pressed, while letting you edit a text field, and while displaying a clock. The clock is updated roughly once per second, and is not updated during heavy keyboard input, such as when typing fast. The demo responds to changing terminal sizes, such as when an XTerm is window is resized. It also displays the determined terminal size, and some small tests of the " (racket #:width) " argument to " (racket charterm-display) ". Exit the demo by pressing the " (bold "Esc") " key.") (para "Note: Although this demo includes an editable text field, as proof of concept, the current version of " "CharTerm" " does not provide editable text fields as reusable functionality.")) (doc (subsection "Simple Example") (para "Here's your first " "CharTerm" " program:") (RACKETBLOCK (UNSYNTAX (code "#lang racket/base")) (require (planet neil/charterm)) (with-charterm (charterm-clear-screen) (charterm-cursor 10 5) (charterm-display "Hello, ") (charterm-bold) (charterm-display "you") (charterm-normal) (charterm-display ".") (charterm-cursor 1 1) (charterm-display "Press a key...") (let ((key (charterm-read-key))) (charterm-cursor 1 1) (charterm-clear-line) (printf "You pressed: ~S\r\n" key)))) (para "Now you're living the dream of the '70s.")) (doc (section "Terminal Diversity") (para "Like people, few terminals are exactly the same.") (para "Some key (ha) terms (ha) used by " "CharTerm" " are:") (itemlist (item (tech "termvar") " --- a string value like from the Unix-like " (code "TERM") " environment variable, used to determine a default " (tech "protocol") " and " (tech "keydec") ".") (item (tech "protocol") " --- how to control the display, query for information, etc.") (item (tech "keydec") " --- how to decode key encodings of a particular terminal. A keydec is constructed from one or more keysets, can produce " (tech "keycode") "s or " (tech "keyinfo") "s.") (item (tech "keyset") " --- a specification of encoding some of the keys in a particular terminal, including " (tech "keylabel") "s and " (tech "keycode") "s.") (item (tech "keylabel") " --- a string for how a key is likely labeled on a keyboard, such as the DEC VT100 " (bold "PF1") " key would have a keylabel " (racket "PF1") " for a " (tech "keycode") " " (racket 'f1) ".") (item (tech "keycode") " --- a value produced by a decoded key, such as a character for normal printable keys, like " (racket #\a) " and " (racket #\space) ", a symbol for some recognized unprintable keys, like " (racket 'escape) " and " (racket 'f1) ", or possibly a number for unrecognized keys.") (item (tech "keyinfo") " --- an object that is used like a " (tech "keycode") ", except bundles together a keycode and a " (tech "keylabel") ", as well as alternatate keycodes and information about how the key was decoded (e.g., from which " (tech "keyset") ").")) (para "These terms are discussed in the following subsections.") (para "CharTerm" " is developed with help of original documentation such as that curated by Paul Williams at " (hyperlink "http://vt100.net/" "vt100.net") ", various commentary found on the Web, observed behavior with modern software terminals like XTerm, various emulators for hardware terminals, and sometimes original hardware terminals. Thanks to Mark Pearrow for contributing a TeleVideo 950, and Paul McCabe for a Wyse S50 WinTerm.") (para "At time of this writing, the author is looking to acquire a DEC VT525, circa 1994, for ongoing testing.") (para "The author welcomes feedback on useful improvements to " "CharTerm" "'s support for terminal diversity (no pun). If you have a terminal that is sending an escape sequence not recognized by the demo, you can run the demo with the " (Flag "n") " (aka " (DFlag "no-escape") ") argument to see the exact byte sequence:") (commandline "racket -pm- neil/charterm/demo -n") (para "When " (Flag "n") " is used, this will be indi
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html><head><title>Python: module ranger.container.history</title>
</head><body bgcolor="#f0f0f8">

<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading">
<tr bgcolor="#7799ee">
<td valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial">&nbsp;<br><big><big><strong><a href="ranger.html"><font color="#ffffff">ranger</font></a>.<a href="ranger.container.html"><font color="#ffffff">container</font></a>.history</strong></big></big></font></td
><td align=right valign=bottom
><font color="#ffffff" face="helvetica, arial"><a href=".">index</a><br><a href="file:/home/hut/ranger/ranger/container/history.py">/home/hut/ranger/ranger/container/history.py</a></font></td></tr></table>
    <p><tt>#&nbsp;Copyright&nbsp;(C)&nbsp;2009,&nbsp;2010&nbsp;&nbsp;Roman&nbsp;Zimbelmann&nbsp;&lt;romanz@lavabit.com&gt;<br>
#<br>
#&nbsp;This&nbsp;program&nbsp;is&nbsp;free&nbsp;software:&nbsp;you&nbsp;can&nbsp;redistribute&nbsp;it&nbsp;and/or&nbsp;modify<br>
#&nbsp;it&nbsp;under&nbsp;the&nbsp;terms&nbsp;of&nbsp;the&nbsp;GNU&nbsp;General&nbsp;Public&nbsp;License&nbsp;as&nbsp;published&nbsp;by<br>
#&nbsp;the&nbsp;Free&nbsp;Software&nbsp;Foundation,&nbsp;either&nbsp;version&nbsp;3&nbsp;of&nbsp;the&nbsp;License,&nbsp;or<br>
#&nbsp;(at&nbsp;your&nbsp;option)&nbsp;any&nbsp;later&nbsp;version.<br>
#<br>
#&nbsp;This&nbsp;program&nbsp;is&nbsp;distributed&nbsp;in&nbsp;the&nbsp;hope&nbsp;that&nbsp;it&nbsp;will&nbsp;be&nbsp;useful,<br>
#&nbsp;but&nbsp;WITHOUT&nbsp;ANY&nbsp;WARRANTY;&nbsp;without&nbsp;even&nbsp;the&nbsp;implied&nbsp;warranty&nbsp;of<br>
#&nbsp;MERCHANTABILITY&nbsp;or&nbsp;FITNESS&nbsp;FOR&nbsp;A&nbsp;PARTICULAR&nbsp;PURPOSE.&nbsp;&nbsp;See&nbsp;the<br>
#&nbsp;GNU&nbsp;General&nbsp;Public&nbsp;License&nbsp;for&nbsp;more&nbsp;details.<br>
#<br>
#&nbsp;You&nbsp;should&nbsp;have&nbsp;received&nbsp;a&nbsp;copy&nbsp;of&nbsp;the&nbsp;GNU&nbsp;General&nbsp;Public&nbsp;License<br>
#&nbsp;along&nbsp;with&nbsp;this&nbsp;program.&nbsp;&nbsp;If&nbsp;not,&nbsp;see&nbsp;&lt;<a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>&gt;.</tt></p>
<p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ee77aa">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#ffffff" face="helvetica, arial"><big><strong>Classes</strong></big></font></td></tr>
    
<tr><td bgcolor="#ee77aa"><tt>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl>
<dt><font face="helvetica, arial"><a href="__builtin__.html#object">__builtin__.object</a>
</font></dt><dd>
<dl>
<dt><font face="helvetica, arial"><a href="ranger.container.history.html#History">History</a>
</font></dt></dl>
</dd>
<dt><font face="helvetica, arial"><a href="exceptions.html#Exception">exceptions.Exception</a>(<a href="exceptions.html#BaseException">exceptions.BaseException</a>)
</font></dt><dd>
<dl>
<dt><font face="helvetica, arial"><a href="ranger.container.history.html#HistoryEmptyException">HistoryEmptyException</a>
</font></dt></dl>
</dd>
</dl>
 <p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#000000" face="helvetica, arial"><a name="History">class <strong>History</strong></a>(<a href="__builtin__.html#object">__builtin__.object</a>)</font></td></tr>
    
<tr><td bgcolor="#ffc8d8"><tt>&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%">Methods defined here:<br>
<dl><dt><a name="History-__init__"><strong>__init__</strong></a>(self, maxlen<font color="#909090">=None</font>)</dt></dl>

<dl><dt><a name="History-__iter__"><strong>__iter__</strong></a>(self)</dt></dl>

<dl><dt><a name="History-__len__"><strong>__len__</strong></a>(self)</dt></dl>

<dl><dt><a name="History-add"><strong>add</strong></a>(self, item)</dt></dl>

<dl><dt><a name="History-back"><strong>back</strong></a>(self)</dt></dl>

<dl><dt><a name="History-bottom"><strong>bottom</strong></a>(self)</dt></dl>

<dl><dt><a name="History-current"><strong>current</strong></a>(self)</dt></dl>

<dl><dt><a name="History-fast_forward"><strong>fast_forward</strong></a>(self)</dt></dl>

<dl><dt><a name="History-forward"><strong>forward</strong></a>(self)</dt></dl>

<dl><dt><a name="History-modify"><strong>modify</strong></a>(self, item)</dt></dl>

<dl><dt><a name="History-move"><strong>move</strong></a>(self, n)</dt></dl>

<dl><dt><a name="History-next"><strong>next</strong></a>(self)</dt></dl>

<dl><dt><a name="History-top"><strong>top</strong></a>(self)</dt></dl>

<hr>
Data descriptors defined here:<br>
<dl><dt><strong>__dict__</strong></dt>
<dd><tt>dictionary&nbsp;for&nbsp;instance&nbsp;variables&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
</td></tr></table> <p>
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
<td colspan=3 valign=bottom>&nbsp;<br>
<font color="#000000" face="helvetica, arial"><a name="HistoryEmptyException">class <strong>HistoryEmptyException</strong></a>(<a href="exceptions.html#Exception">exceptions.Exception</a>)</font></td></tr>
    
<tr><td bgcolor="#ffc8d8"><tt>&nbsp;&nbsp;&nbsp;</tt></td><td>&nbsp;</td>
<td width="100%"><dl><dt>Method resolution order:</dt>
<dd><a href="ranger.container.history.html#HistoryEmptyException">HistoryEmptyException</a></dd>
<dd><a href="exceptions.html#Exception">exceptions.Exception</a></dd>
<dd><a href="exceptions.html#BaseException">exceptions.BaseException</a></dd>
<dd><a href="__builtin__.html#object">__builtin__.object</a></dd>
</dl>
<hr>
Data descriptors defined here:<br>
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<hr>
Methods inherited from <a href="exceptions.html#Exception">exceptions.Exception</a>:<br>
<dl><dt><a name="HistoryEmptyException-__init__"><strong>__init__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__init__">__init__</a>(...)&nbsp;initializes&nbsp;x;&nbsp;see&nbsp;x.__class__.__doc__&nbsp;for&nbsp;signature</tt></dd></dl>

<hr>
Data and other attributes inherited from <a href="exceptions.html#Exception">exceptions.Exception</a>:<br>
<dl><dt><strong>__new__</strong> = &lt;built-in method __new__ of type object&gt;<dd><tt>T.<a href="#HistoryEmptyException-__new__">__new__</a>(S,&nbsp;...)&nbsp;-&gt;&nbsp;a&nbsp;new&nbsp;<a href="__builtin__.html#object">object</a>&nbsp;with&nbsp;type&nbsp;S,&nbsp;a&nbsp;subtype&nbsp;of&nbsp;T</tt></dl>

<hr>
Methods inherited from <a href="exceptions.html#BaseException">exceptions.BaseException</a>:<br>
<dl><dt><a name="HistoryEmptyException-__delattr__"><strong>__delattr__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__delattr__">__delattr__</a>('name')&nbsp;&lt;==&gt;&nbsp;del&nbsp;x.name</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__getattribute__"><strong>__getattribute__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__getattribute__">__getattribute__</a>('name')&nbsp;&lt;==&gt;&nbsp;x.name</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__getitem__"><strong>__getitem__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__getitem__">__getitem__</a>(y)&nbsp;&lt;==&gt;&nbsp;x[y]</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__getslice__"><strong>__getslice__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__getslice__">__getslice__</a>(i,&nbsp;j)&nbsp;&lt;==&gt;&nbsp;x[i:j]<br>
&nbsp;<br>
Use&nbsp;of&nbsp;negative&nbsp;indices&nbsp;is&nbsp;not&nbsp;supported.</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__reduce__"><strong>__reduce__</strong></a>(...)</dt></dl>

<dl><dt><a name="HistoryEmptyException-__repr__"><strong>__repr__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__repr__">__repr__</a>()&nbsp;&lt;==&gt;&nbsp;repr(x)</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__setattr__"><strong>__setattr__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__setattr__">__setattr__</a>('name',&nbsp;value)&nbsp;&lt;==&gt;&nbsp;x.name&nbsp;=&nbsp;value</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__setstate__"><strong>__setstate__</strong></a>(...)</dt></dl>

<dl><dt><a name="HistoryEmptyException-__str__"><strong>__str__</strong></a>(...)</dt><dd><tt>x.<a href="#HistoryEmptyException-__str__">__str__</a>()&nbsp;&lt;==&gt;&nbsp;str(x)</tt></dd></dl>

<dl><dt><a name="HistoryEmptyException-__unicode__"><strong>__unicode__</strong></a>(...)</dt></dl>

<hr>
Data descriptors inherited from <a href="exceptions.html#BaseException">exceptions.BaseException</a>:<br>
<dl><dt><strong>__dict__</strong></dt>
</dl>
<dl><dt><strong>args</strong></dt>
</dl>
<dl><dt><strong>message</strong></dt>
</dl>
</td></tr></table></td></tr></table>
</body></html>
a-control ct) (%charterm:protocol-case '%charterm:screen-size-via-control (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[18t") (cond ((%charterm:read-regexp-response ct #rx#"\e\\[8;([0-9]+);([0-9]+)t") => (lambda (m) (values (%charterm:bytes-ascii->nonnegative-integer (list-ref m 1)) (%charterm:bytes-ascii->nonnegative-integer (list-ref m 0))))) ;; TODO: We could do "ioctl" "TIOCGWINSZ", but that means FFI. ;; ;; TODO: We could execute "stty -a" (or perhaps "stty -g") to get ;; around doing an FFI call. (else (values #f #f)))) ((wyse-wy50 televideo-925) (%charterm:protocol-unreachable '%charterm:screen-size-via-control ct)))) (define (%charterm:screen-size-via-stty ct) (let* ((stdout (open-output-bytes)) (stderr (open-output-bytes)) (proc (list-ref (process*/ports stdout (open-input-bytes #"") stderr "/bin/stty" %charterm:stty-minus-f-arg-string (charterm-tty ct) "-a") 4)) (bstr (begin (proc 'wait) (get-output-bytes stdout)))) (if (eq? 'done-ok (proc 'status)) (let-values (((width height) (cond ((regexp-match-positions #rx#"rows +([0-9]+);.*columns +([0-9]+)" bstr) => (lambda (m) (values (%charterm:bytes-ascii->nonnegative-integer (subbytes bstr (caaddr m) (cdaddr m))) (%charterm:bytes-ascii->nonnegative-integer (subbytes bstr (caadr m) (cdadr m)))))) ((regexp-match-positions #rx#"columns +([0-9]+);.*rows +([0-9]+)" bstr) => (lambda (m) (values (%charterm:bytes-ascii->nonnegative-integer (subbytes bstr (caadr m) (cdadr m))) (%charterm:bytes-ascii->nonnegative-integer (subbytes bstr (caaddr m) (cdaddr m)))))) (else #f #f)))) ;; Note: These checks for 0 are for if "stty" returns 0, such as ;; seems to happen in the emulator on the Wyse S50 when in SSH rather than Telnet. (values (if (zero? width) #f width) (if (zero? height) #f height))) (values #f #f)))) (doc (section "Display Control")) (define (%charterm:shift-buf ct) (let ((buf-start (charterm-buf-start ct)) (buf-end (charterm-buf-end ct))) (if (= buf-start buf-end) ;; Buffer is empty, so are buf-start and buf-end at 0? (if (zero? buf-end) (void) (begin (set-charterm-buf-start! ct 0) (set-charterm-buf-end! ct 0))) ;; Buffer is not empty, so is buf-start at 0? ;; ;; TODO: Maybe make this shift only if we need to to free N additional ;; bytes at the end? (if (zero? buf-start) (void) (let ((buf (charterm-buf ct))) (bytes-copy! buf 0 buf buf-start buf-end) (set-charterm-buf-start! ct 0) (set-charterm-buf-end! ct (- buf-end buf-start))))))) (define (%charterm:read-into-buf/timeout ct timeout) (let ((in (charterm-in ct))) (let loop () (let ((sync-result (sync/timeout/enable-break timeout in))) (cond ((not sync-result) #f) ((eq? sync-result in) ;; TODO: if buf is empty, then read into start 0! (let ((read-result (read-bytes-avail! (charterm-buf ct) in (charterm-buf-end ct) (charterm-buf-size ct)))) (if (zero? read-result) ;; TODO: If there's a timeout, subtract from it? (loop) (begin (set-charterm-buf-end! ct (+ (charterm-buf-end ct) read-result)) read-result)))) (else (error '%charterm:read-into-buf/timeout "*DEBUG* sync returned ~S" sync-result))))))) (define (%charterm:read-regexp-response ct rx #:timeout-seconds (timeout-seconds 1.0)) (let ((in (charterm-in ct))) (%charterm:shift-buf ct) ;; TODO: Implement timeout better, by checking clock and doing ;; sync/timeout, or by setting timer. (let loop ((timeout-seconds timeout-seconds)) (if (= (charterm-buf-end ct) (charterm-buf-size ct)) (begin ;; TODO: Make this an exception instead of #f? #f) (begin (or (let ((buf (charterm-buf ct)) (buf-start (charterm-buf-start ct)) (buf-end (charterm-buf-end ct))) (cond ((regexp-match-positions rx buf buf-start buf-end) => (lambda (m) ;; TODO: Audit and test some of this buffer ;; code here and elsewhere. (let ((match-start (caar m)) (match-end (cdar m))) (if (= match-start buf-start) (set-charterm-buf-start! ct match-end) (if (= match-end buf-end) (set-charterm-buf-end! ct match-start) (begin (bytes-copy! buf match-start buf match-end buf-end) (set-charterm-buf-end! ct (+ match-start (- buf-end match-end))))))) (map (lambda (pos) (subbytes buf (car pos) (cdr pos))) (cdr m)))) (else #f))) (if (%charterm:read-into-buf/timeout ct timeout-seconds) (loop timeout-seconds) #f ))))))) (define (%charterm:bytes-ascii->nonnegative-integer bstr) (let ((bstr-len (bytes-length bstr))) (let loop ((i 0) (result 0)) (if (= i bstr-len) result (let* ((b (bytes-ref bstr i)) (b-num (- b 48))) (if (<= 0 b-num 9) (loop (+ 1 i) (+ (* 10 result) b-num)) (error '%charterm:bytes-ascii->nonnegative-integer "invalid byte ~S" b))))))) (doc (subsection "Cursor")) (doc (defproc (charterm-cursor (x exact-positive-integer?) (y exact-positive-integer?) (#:charterm ct charterm? (current-charterm))) void? (para "Positions the cursor at column " (racket x) ", row " (racket y) ", with the upper-left character cell being (1, 1)."))) (provide charterm-cursor) (define (charterm-cursor x y #:charterm (ct (current-charterm))) (%charterm:position ct x y)) (doc (defproc (charterm-newline (#:charterm ct charterm? (current-charterm))) void? (para "Sends a newline to the terminal. This is typically a CR-LF sequence."))) (provide charterm-newline) (define (charterm-newline #:charterm (ct (current-charterm))) (%charterm:write-bytes ct #"\r\n")) (doc (subsection "Displaying")) (define %charterm:err-byte 63) (doc (defproc (charterm-display (#:charterm ct charterm? (current-charterm)) (#:width width (or/c #f exact-positive-integer?) #f) (#:pad pad (or/c 'width boolean?) 'width) (#:truncate truncate (or/c 'width boolean?) 'width) ( arg any/c) ...) void? (para "Displays each " (racket arg) " on the terminal, as if formatted by " (racket display) ", with the exception that unprintable or non-ASCII characters might not be displayed. (The exact behavior of what is permitted is expected to change in a later version of " "CharTerm" ", so avoid trying to send your own control sequences or using newlines, making assumptions about non-ASCII characters, etc.)") (para "If " (racket width) " is a number, then " (racket pad) " and " (racket truncate) " specify whether or not to pad with spaces or truncate the output, respectively, to " (racket width) " characters. When " (racket pad) " or " (racket width) " is " (racket 'width) ", that is a convenience meaning ``true if, and only if, " (racket width) " is not " (racket #f) ".''"))) (provide charterm-display) (define (charterm-display #:charterm (ct (current-charterm)) #:width (width #f) #:pad (pad 'width) #:truncate (truncate 'width) . args) ;; TODO: make it replace unprintable and non-ascii characters with "?". Even newlines, tabs, etc? ;; ;; TODO: Do we want buffering? (let ((out (charterm-out ct)) (pad (if (eq? 'width pad) (if width #t #f) pad)) (truncate (if (eq? 'width truncate) (if width #t #f) truncate))) (and pad (not width) (error 'charterm-display "#:pad cannot be true if #:width is not")) (and truncate (not width) (error 'charterm-display "#:truncate cannot be true if #:width is not")) (let loop ((args args) (remaining-width (or width 0))) (if (null? args) (if (and pad (> remaining-width 0)) ;; TODO: Get rid of this allocation. (begin (%charterm:write-bytes ct (make-bytes remaining-width 32)) (void)) (void)) (let* ((arg (car args)) (bytes (cond ((bytes? arg) arg) ((string? arg) (string->bytes/latin-1 arg %charterm:err-byte 0 (if truncate (min (string-length arg) remaining-width) (string-length arg)))) ((number? arg) (string->bytes/latin-1 (number->string arg) %charterm:err-byte)) (else (let ((arg (format "~A" arg))) (string->bytes/latin-1 arg %charterm:err-byte 0 (if truncate (min (string-length arg) remaining-width) (string-length arg))))))) (remaining-width (- remaining-width (bytes-length bytes)))) (cond ((or (not truncate) (> remaining-width 0)) (%charterm:write-bytes ct bytes) (loop (cdr args) remaining-width)) ((zero? remaining-width) (%charterm:write-bytes ct bytes) (void)) (else (%charterm:write-subbytes ct bytes 0 (+ (bytes-length bytes) remaining-width)) (void)))))))) (define (%charterm:send-code ct . args) ;; TODO: Do we want buffering? (let ((out (charterm-out ct))) (let loop ((args args)) (if (null? args) (void) (let ((arg (car args))) (cond ((bytes? arg) (write-bytes arg out)) ((string? arg) (write-string arg out)) ((integer? arg) (display (inexact->exact arg) out)) ((pair? arg) (loop (car arg)) (loop (cdr arg))) (else (error '%charterm:send-code "don't know how to send ~S" arg))) (loop (cdr args))))))) ;; (define %charterm:2-digit-bytes-vector ;; (vector #"00" #"01" #"02" #"03" #"04" #"05" #"06" #"07" ;; #"08" #"09" #"10" #"11" #"12" #"13" #"14" #"15" ;; #"16" #"17" #"18" #"19" #"20" #"21" #"22" #"23" ;; #"24" #"25" #"26" #"27" #"28" #"29" #"30" #"31" ;; #"32" #"33" #"34" #"35" #"36" #"37" #"38" #"39" ;; #"40" #"41" #"42" #"43" #"44" #"45" #"46" #"47" ;; #"48" #"49" #"50" #"51" #"52" #"53" #"54" #"55" ;; #"56" #"57" #"58" #"59" #"60" #"61" #"62" #"63" ;; #"64" #"65" #"66" #"67" #"68" #"68" #"69" #"70" ;; #"72" #"73" #"74" #"75" #"76" #"77" #"78" #"79" ;; #"80" #"81" #"82" #"83" #"84" #"85" #"86" #"87")) (define %charterm:televideo-925-cursor-position-to-byte-vector (list->vector (cons #f (for/list ((n (in-range 1 96))) (+ 31 n))))) ;; (provide/contract with error-checks on args (define (%charterm:position ct x y) (%charterm:protocol-case '%charterm:position (charterm-protocol ct) ((ansi) (if (and (= 1 x) (= 1 y)) (%charterm:write-bytes ct #"\e[;H") (%charterm:send-code ct #"\e[" y #";" x #"H"))) ((wyse-wy50) ;; Note: We are using the WY-50 long codes because we don't know ;; confidently that we are an 80-column screen. (if (and (= 1 x) (= 1 y)) (%charterm:write-bytes ct #"\ea1R1C") (%charterm:send-code ct #"\ea" y #"R" x #"C"))) ((televideo-925) (if (and (= 1 x) (= 1 y)) (%charterm:write-bytes ct #"\e= ") (begin (%charterm:write-bytes ct #"\e=") (%charterm:write-byte ct (vector-ref %charterm:televideo-925-cursor-position-to-byte-vector y)) (%charterm:write-byte ct (vector-ref %charterm:televideo-925-cursor-position-to-byte-vector x))))))) (doc (subsection "Video Attributes")) ;; TODO: !!! document link to protocol section ;; TODO: !!! define "charterm-has-video-attributes?" (doc (defproc* (((charterm-normal (#:charterm ct charterm? (current-charterm))) void?) ((charterm-inverse (#:charterm ct charterm? (current-charterm))) void?) ((charterm-underline (#:charterm ct charterm? (current-charterm))) void?) ((charterm-blink (#:charterm ct charterm? (current-charterm))) void?) ((charterm-bold (#:charterm ct charterm? (current-charterm))) void?)) (para "Sets the " (deftech "video attributes") " for subsequent writes to the terminal. In this version of " (code "charterm") ", each is mutually-exclusive, so, for example, setting " (italic "bold") " clears " (italic "inverse") ". Note that that video attributes are currently supported only for protocol " (racket 'ansi) ", due to limitations of the TeleVideo and Wyse models for video attributes."))) (provide charterm-normal) (define (charterm-normal #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-normal (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[m")) ((wyse-wy50) (void)) ; (%charterm:write-bytes ct #"\eA00")) ((televideo-925) (void)))) (provide charterm-inverse) (define (charterm-inverse #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-inverse (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[;7m")) ((wyse-wy50) (void)) ; (%charterm:write-bytes ct #"\eA04")) ((televideo-925) (void)))) (provide charterm-underline) (define (charterm-underline #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-underline (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[4m")) ((wyse-wy50) (void)) ; (%charterm:write-bytes ct #"\eA08")) ((televideo-925) (void)))) (provide charterm-blink) (define (charterm-blink #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-blink (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[5m")) ((wyse-wy50) (void)) ; (%charterm:write-bytes ct #"\eA02")) ((televideo-925) (void)))) (provide charterm-bold) (define (charterm-bold #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-bold (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[1m")) ((wyse-wy50) (void)) ; (%charterm:write-bytes ct #"\eA0<")) ((televideo-925) (void)))) (doc (subsection "Clearing")) (doc (defproc (charterm-clear-screen (#:charterm ct charterm? (current-charterm))) void? (para "Clears the screen, including first setting the video attributes to normal, and positioning the cursor at (1, 1)."))) (provide charterm-clear-screen) (define (charterm-clear-screen #:charterm (ct (current-charterm))) ;; TODO: Have a #:style argument? Or #:background argument? (%charterm:protocol-case 'charterm-clear-screen (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[m\e[2J\e[;H")) ((wyse-wy50) (%charterm:write-bytes ct #"\e+\e*\ea1R1C")) ((televideo-925) (%charterm:write-bytes ct #"\e+\e= ")))) (doc (defproc* (((charterm-clear-line (#:charterm ct charterm? (current-charterm))) void?) ((charterm-clear-line-left (#:charterm ct charterm? (current-charterm))) void?) ((charterm-clear-line-right (#:charterm ct charterm? (current-charterm))) void?)) (para "Clears text from the line with the cursor, or part of the line with the cursor."))) (provide charterm-clear-line) (define (charterm-clear-line #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm:clear-line (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[2K")) ((televideo-925) (%charterm:write-bytes ct #"\r\eT")) ;; TODO: wyse-wy50 is clearing to nulls, not spaces. ((wyse-wy50) (%charterm:write-bytes ct #"\r\et")))) (provide charterm-clear-line-left) (define (charterm-clear-line-left #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-clear-line-left (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[1K")) ((televideo-925 wyse-wy50) ;; TODO: Do this by getting cursor position, then reposition and write spaces? (%charterm:unimplemented ct 'clearterm-clear-line-left)))) (provide charterm-clear-line-right) (define (charterm-clear-line-right #:charterm (ct (current-charterm))) (%charterm:protocol-case 'charterm-clear-line-right (charterm-protocol ct) ((ansi) (%charterm:write-bytes ct #"\e[K")) ((televideo-925) (%charterm:write-bytes ct #"\eT")) ;; TODO: wyse-wy50 is clearing to nulls, not spaces. ((wyse-wy50) (%charterm:write-bytes ct #"\et")))) (doc (subsection "Line Insert and Delete")) (doc (defproc (charterm-insert-line (count exact-positive-integer? 1) (#:charterm ct charterm? (current-charterm))) void? (para "Inserts " (racket count) " blank lines at cursor. Note that not all terminals support this."))) (provide charterm-insert-line) (define (charterm-insert-line (count 1) #:charterm (ct (current-charterm))) (if (integer? count) (cond ((= count 0) (void)) ((> count 0) (%charterm:protocol-case 'charterm-insert-line (charterm-protocol ct) ((ansi) (%charterm:send-code ct #"\e[" count "L")) ((wyse-wy50 televideo-925) (%charterm:write-bytes ct #"\eE")))) (else (error 'charterm-insert-line "invalid count: ~S" count))) (error 'charterm-insert-line "invalid count: ~S" count))) (doc (defproc (charterm-delete-line (count exact-positive-integer? 1) (#:charterm ct charterm? (current-charterm))) void? (para "Deletes " (racket count) " blank lines at cursor. Note that not all terminals support this."))) (provide charterm-delete-line) (define (charterm-delete-line (count 1) #:charterm (ct (current-charterm))) (if (integer? count) (cond ((= count 0) (void)) ((> count 0) (%charterm:protocol-case 'charterm-delete-line (charterm-protocol ct) ((ansi) (%charterm:send-code ct #"\e[" count "M")) ((wyse-wy50 televideo-925) (if (= 1 count) (%charterm:write-bytes ct #"\eR") (let ((bstr (make-bytes (* 2 count) 82))) (let loop ((n (* 2 (- count 1)))) (bytes-set! bstr n 27) (if (zero? n) (%charterm:write-bytes ct bstr) (loop (- n 2))))))))) (else (error 'charterm-delete-line "invalid count: ~S" count))) (error 'charterm-delete-line "invalid count: ~S" count))) (doc (subsubsection "Misc. Output")) (doc (defproc (charterm-bell (#:charterm ct charterm? (current-charterm))) void? (para "Rings the terminal bell. This bell ringing might manifest as a beep, a flash of the screen, or nothing."))) (provide charterm-bell) (define (charterm-bell #:charterm (ct (current-charterm))) (%charterm:write-bytes ct #"\007")) (doc (section "Keyboard Input") ;; TODO: !!! document link to terminal diversity section (para "Normally you will get keyboard input using the " (racket charterm-read-key) " procedure.")) (doc (defproc (charterm-byte-ready? (#:charterm ct charterm? (current-charterm))) boolean? (para "Returns true/false for whether at least one byte is ready for reading (either in a buffer or on the port) from " (racket ct) ". Note that, since some keys are encoded as multiple bytes, just because this procedure returns true doesn't mean that " (racket charterm-read-key) " won't block temporarily because it sees part of a potential multiple-byte key encoding."))) (provide charterm-byte-ready?) (define (charterm-byte-ready? #:charterm (ct (current-charterm))) (or (> (charterm-buf-end ct) (charterm-buf-start ct)) (byte-ready? (charterm-in ct)))) (doc (defproc (charterm-read-key (#:charterm ct charterm? (current-charterm)) (#:timeout timeout (or/c #f positive?) #f)) (or #f char? symbol?) (para "Reads a key from " (racket ct) ", blocking indefinitely or until sometime after " (racket timeout) " seconds has been reached, if " (racket timeout) " is non-" (racket #f) ". If timeout is reached, " (racket #f) " is returned.") (para "Many keys are returned as characters, especially ones that correspond to printable characters. For example, the unshifted " (bold "Q") " key is returned as character " (racket #\q) ". Some other keys are returned as symbols, such as " (racket 'return) ", " (racket 'escape) ", " (racket 'f1) ", " (racket 'shift-f12) ", " (racket 'right) ", and many others.") (para "Since some keys are sent as ambiguous sequences, " (racket charterm-read-key) " employs separate timeouts internally, such as to disambuate the " (bold "Esc") " key (byte sequence 27) from what on some terminals would be the " (bold "F10") " key (bytes sequence 27, 91, 50, 49, 126)."))) (provide charterm-read-key) (define (charterm-read-key #:charterm (ct (current-charterm)) #:timeout (timeout #f)) (%charterm:read-keyinfo-or-key 'charterm-read-key ct timeout #f)) (doc (defproc (charterm-read-keyinfo (#:charterm ct charterm? (current-charterm)) (#:timeout timeout (or/c #f positive?) #f)) charterm-keyinfo? (para "Like " (racket charterm-read-keyinfo) " except instead of returning a " (tech "keycode") ", it returns a " (tech "keyinfo") "."))) (provide charterm-read-keyinfo) (define (charterm-read-keyinfo #:charterm (ct (current-charterm)) #:timeout (timeout #f)) (%charterm:read-keyinfo-or-key 'charterm-read-keyinfo ct timeout #t)) (define (%charterm:read-keyinfo-or-key error-name ct timeout keyinfo?) ;; TODO: Maybe make this shift decision smarter -- compile the key tree ahead ;; of time so we know the max depth, and then we know exactly the max space ;; we will need for this call. (and (< (- (charterm-buf-size ct) (charterm-buf-start ct)) 10) (%charterm:shift-buf ct)) (let ((buf (charterm-buf ct)) (buf-start (charterm-buf-start ct)) (buf-end (charterm-buf-end ct)) (buf-size (charterm-buf-size ct)) (keydec (charterm-keydec* ct)) (b1 (%charterm:read-byte/timeout ct timeout))) (if b1 (or (let loop ((tree (charterm-keydec-primary-keytree keydec)) (probe-start (+ 1 buf-start)) (b b1)) (cond ((hash-ref tree b #f) => (lambda (code-or-subtree) (cond ((hash? code-or-subtree) ;; We have more subtree to search. (if (or (< probe-start buf-end) (and (< buf-end buf-size) (%charterm:read-into-buf/timeout ct 0.5))) ;; We have at least one more byte, so recurse. (loop code-or-subtree (+ 1 probe-start) (bytes-ref buf probe-start)) ;; We have hit timeout or end of buffer, so ;; just accept the original byte. #f)) ((charterm-keyinfo? code-or-subtree) ;; We found our keyinfo, so consume the input and return the value. (begin (set-charterm-buf-start! ct probe-start) (if keyinfo? code-or-subtree (charterm-keyinfo-keycode code-or-subtree)) )) (else (error error-name "invalid object in keytree keyinfo position: ~S" code-or-subtree))))) (else #f))) ;; We didn't find a key code, so try secondary keytree with initial byte. (cond ((hash-ref (charterm-keydec-secondary-keytree keydec) b1 #f) => (lambda (keyinfo) (if keyinfo? keyinfo (charterm-keyinfo-keycode keyinfo)))) (else (if keyinfo? ;; TODO: Cache these keyinfos for unrecognized keys ;; in the charterm object, or make a fallback ;; keyset for them (although the fallback keyset, ;; while it works for 8-bit characters, becomes ;; less practical if we implement multibyte). (make-charterm-keyinfo #f #f (list b1) "???" b1 (list b1)) (integer->char b1))))) ;; Got a timeout, so return #f. #f))) (define (%charterm:write-byte ct byt) (write-byte byt (charterm-out ct))) (define (%charterm:write-bytes ct bstr . rest-bstrs) (write-bytes bstr (charterm-out ct)) (or (null? rest-bstrs) (for-each (lambda (bstr) (write-bytes bstr (charterm-out ct))) rest-bstrs))) (define (%charterm:write-subbytes ct bstr start end) (write-bytes bstr (charterm-out ct) start end)) (define (%charterm:read-byte/timeout ct timeout) (let ((buf-start (charterm-buf-start ct))) (if (or (< buf-start (charterm-buf-end ct)) (%charterm:read-into-buf/timeout ct timeout)) (begin0 (bytes-ref (charterm-buf ct) buf-start) (set-charterm-buf-start! ct (+ 1 buf-start))) #f))) (define (%charterm:read-byte ct) (%charterm:read-byte/timeout ct #f)) (doc (section "References") (para "[" (deftech "ANSI X3.64") "] " (url "http://en.wikipedia.org/wiki/ANSI_escape_code")) (para "[" (deftech "ASCII") "] " (url "http://en.wikipedia.org/wiki/Ascii")) (para "[" (deftech "ECMA-43") "] " (hyperlink "http://www.ecma-international.org/publications/standards/Ecma-043.htm" (italic "Standard ECMA-43: 8-bit Coded Character Set Structure and Rules")) ", 3rd Ed., 1991-12") (para "[" (deftech "ECMA-48") "] " (hyperlink "http://www.ecma-international.org/publications/standards/Ecma-048.htm" (italic "Standard ECMA-48: Control Functions for Coded Character Sets")) ", 5th Ed., 1991-06") (para "[" (deftech "Gregory") "] " "Phil Gregory, ``" (hyperlink "http://aperiodic.net/phil/archives/Geekery/term-function-keys.html" "Terminal Function Key Escape Codes") ",'' 2005-12-13 Web post, as viewed on 2012-06") (para "[" (deftech "PowerTerm") "] " "Ericom PowerTerm InterConnect 8.2.0.1000 terminal emulator, as run on Wyse S50 WinTerm") (para "[" (deftech "TVI-925-IUG") "] " (hyperlink "http://vt100.net/televideo/tvi925_ig.pdf" (italic "TeleVideo Model 925 CRT Terminal Installation and User's Guide"))) (para "[" (deftech "TVI-950-OM") "] " (hyperlink "http://www.mirrorservice.org/sites/www.bitsavers.org/pdf/televideo/Operators_Manual_Model_950_1981.pdf" (italic "TeleVideo Operator's Manual Model 950")) ", 1981") (para "[" (deftech "VT100-TM") "] " "Digital Equipment Corp., " (hyperlink "http://vt100.net/docs/vt100-tm/" (italic "VT100 Series Technical Manual")) ", 2nd Ed., 1980-09") (para "[" (deftech "VT100-UG") "] " "Digital Equipment Corp., " (hyperlink "http://vt100.net/docs/vt100-ug/" (italic "VT100 User Guide")) ", 3rd Ed., 1981-06") (para "[" (deftech "VT100-WP") "] " "Wikipedia, " (hyperlink "http://en.wikipedia.org/wiki/VT100" "VT100")) (para "[" (deftech "WY-50-QRG") "] " (hyperlink "http://vt100.net/wyse/wy-50-qrg/wy-50-qrg.pdf" (italic "Wyse WY-50 Display Terminal Quick-Reference Guide"))) (para "[" (deftech "WY-60-UG") "] " (hyperlink "http://vt100.net/wyse/wy-60-ug/wy-60-ug.pdf" (italic "Wyse WY-60 User's Guide"))) (para "[" (deftech "wy60") "] " (hyperlink "http://code.google.com/p/wy60/" (code "wy60") " terminal emulator")) (para "[" (deftech "XTerm-ctlseqs") "] " "Edward Moy, Stephen Gildea, Thomas Dickey, ``" (hyperlink "http://invisible-island.net/xterm/ctlseqs/ctlseqs.html" "Xterm Control Sequences") ",'' 2012") (para "[" (deftech "XTerm-Dickey") "] " (url "http://invisible-island.net/xterm/")) (para "[" (deftech "XTerm-FAQ") "] " "Thomas E. Dickey, ``" (hyperlink "http://invisible-island.net/xterm/xterm.faq.html" "XTerm FAQ") ",'' dated 2012") (para "[" (deftech "XTerm-WP") "] " "Wikipedia, " (hyperlink "http://en.wikipedia.org/wiki/Xterm" "xterm")) ) (doc (section "Known Issues") (itemlist (item "Need to support ANSI alternate CSI for 8-bit terminals, even before supporting 8-bit characters and multibyte.") (item "Only supports ASCII characters. Adding UTF-8 support, for terminal emulators that support it, would be nice.") (item "Expose the character-decoding mini-language as a configurable option. Perhaps wait until we implement timeout-based disambiguation at arbitrary points in the the DFA rather than just at the top. Also, might be better to resolve multi-byte characters first, in case that affects the mini-language.") (item "More controls for terminal features can be added.") (item "Currently only implemented to work on Unix-like systems like GNU/Linux.") (item "Implement text input controls, either as part of this library or another, using " (racket charterm-demo) " as a starting point."))) ;; Note: Different ways to test demo: ;; ;; racket -t demo.rkt -m ;; screen racket -t demo.rkt -m ;; tmux -c "racket -t demo.rkt -m" ;; xterm -e racket -t demo.rkt -m ;; rxvt -e racket -t demo.rkt -m ;; wy60 -c racket -t demo.rkt -m ;; ;; racket -t demo.rkt -m- -n ;; TODO: Source for TeleVideo manuals: ;; http://www.mirrorservice.org/sites/www.bitsavers.org/pdf/televideo/ ;; TODO: Add shifted function keys from T60 keyboard (not USB one). (doc history (#:planet 3:1 #:date "2013-05-13" (itemlist (item "Now uses lowercase " (code "-f") " argument on MacOS X. (Thanks to Jens Axel S\u00F8gaard for reporting.)") (item "Documentation tweaks."))) (#:planet 3:0 #:date "2012-07-13" (itemlist (item "Changed ``" (code "ansi-ish") "'' in identifiers to ``" (code "ansi") "'', hence the PLaneT major version number change.") (item "Documentation tweaks.") (item "Renamed package from ``" (code "charterm") "'' to ``CharTerm''."))) (#:planet 2:5 #:date "2012-06-28" (itemlist (item "A " (racket charterm) " object is now a synchronizable event.") (item "Documentation tweaks."))) (#:planet 2:4 #:date "2012-06-25" (itemlist (item "Documentation fix for return type of " (racket charterm-read-keyinfo) "."))) (#:planet 2:3 #:date "2012-06-25" (itemlist (item "Fixed problem determining screen size on some XTerms. (Thanks to Eli Barzilay for reporting.)"))) (#:planet 2:2 #:date "2012-06-25" (itemlist (item "Added another variation of encoding for XTerm arrow, Home, and End keys. (Thanks to Eli Barzilay.)"))) (#:planet 2:1 #:date "2012-06-24" (itemlist (item "Corrected PLaneT version number in " (racket require) " in an example."))) (#:planet 2:0 #:date "2012-06-24" (itemlist (item "Greatly increased the sophistication of handling of terminal diversity.") (item "Added the " (code "wyse-wy50") " and " (code "televideo-950") " [Correction: " (code "televideo-925") "] protocols, for supporting the native modes of Wyse and TeleVideo terminals, respectively, and compatibles.") (item "More support for different key encodings and termvars.") (item "Demo is now in a separate file, mainly for convenience in giving command lines that run it. This breaks a command line example previously documented, so changed PLaneT major version, although the previously-published example will need to have " (code ":1") " added to it anyway.") (item (racket charterm-screen-size) " now defaults to (80,24) when all else fails.") (item "Documentation changes."))) (#:planet 1:1 #:date "2012-06-17" (itemlist (item "For " (code "screen") " and " (code "tmux") ", now gets screen size via " (code "stty") ". This resolves the sluggishness reported with " (code "screen") ". [Correction: In version 1:1, this behavior is adaptive for all terminals, with the shortcut for " (tech "termvar") " " (code "screen") " that it doesn't bother trying the control sequence.]") (item "Documentation tweaks."))) (#:planet 1:0 #:date "2012-06-16" (itemlist (item "Initial version."))))