(This document only applies to iOS TeX Writer 5.0+. Last updated: 2018/4/22.)

The LISP language used in TeX Writer is small. If you are already familiar with emacs or scheme, then you should be really quick to pick it up. If you are an absolute beginner, don't worry, it won't take much learning to customize the looks of TeX Writer. Skip to the end of this document, there are quick examples that show you how to set colors.

All your customizations should be put in a file twinit.l, which must be saved in the top Local directory. TeX Writer loads this file when it starts. If you make any change to this file, you can reload it right away using the "LOAD" button on toolbar.

Data Types

LISP supports 6 fundamental data types: number, string, symbol, pair, array and dictionary.


Internally, all numbers are stored as a double precision floating number(Same as Javascript). For two numbers to be equal, they must match every bit.

(integer? 2000) => true
(= 1.0 1) => true
(eq? 1.0 1) => true

Here are common number operations.



(+ a b ...)

(- a b ...)

(* a b ...)

(/ a b ...)

(< a b ...)

(> a b ...)

(= a b ...)

(<= a b ...)

(<= a b ...)


Strings are immutable. Manipulation of strings, like (concat), always creates new instances. String is internally represented as UTF8 bytes.

"how are you?"
world!" => "hello\nworld!"
(eq? "hello" "hello") => true

(concat str1 str2 ...)

Return concatenation of strings.

(string-compare str1 str2)

Similar to C funtion strcmp().

(string-length str)

UTF8 byte count, not unicode chars.

(string->number str)

Convert a string to number. If str is not a numeric string, returns undefined.

(string? obj)

(substring str begin &optional end)


Symbols are names for variables and procedures. Some symbols evaluate to themselves, and some are reserved for special purpose, like unquote.


Predefined symbols. constant symbols. Evaluates to themselves. symbols beginning with : are also constants, mainly for used as key.


undefined is a special constant symbol. (car ()) returns undefined. Accessing out-of-bound indexes of an array, or non-existing keys in a dictionary, results in undefined as well. But remember that trying to reference an undefined variable will results in error. You can use (defined? varname) safely though, because defined? is a special form that works for undefined symbols.

(defined? unknown-var) => false
unknown-var            => error 
(define a undefined)
(defined? a)           => false

The following symbols can't be redefined:


(define var expr)

(set! var expr)

(string->symbol str)

(symbol? obj)

Pair, List and Association List

Pairs are immutable. There is no set-car! nor set-cdr!. If you want to change a pair, you need to reconstruct a whole new copy. It's impossible to make a ring with pairs, except that empty list () is defined as (undefined . ()), which is a pair whose cdr points to itself. Since LISP code is also made of pairs, this immutability makes it much easier to reason about. For example, we don't need to worry that static lists returning from a procedure will be modified by caller.

(joe . 6)
(cons 'alice 'bob)

From pairs, we can construct list and association list. Both of them are also immutable.

(1 2 3)  equals to (1 . (2 . (3 . ())))
((a b) () (c)) ;; list of lists

Empty list is a special constant. In some scheme implementations, empty list () needs to be quoted as '(). But in our version, the quote is unnecessary.

() => () ;; empty list evaluates to itself
(car ()) => undefined
(cdr ()) => ()

An association list (also known as a-list) is a list of pairs; each pair is an association. The car of a pair is called the key, and the cdr is called the datum.

((one . 1) (two . 2) (three . 3) (four . 19))

(cons a b)

Making a pair.

(car pair)

car of () is undefined.

(cdr pair)

cdr of () is also ().

(append list another-list)

Return a new list.

(pair? obj)

(list? obj)

Test if obj is a list, that the last pair's cdr is ().

(null? obj)

Test if obj is empty list ().

(nth list index)

Return nth item of the list. index is 0-based.

(list item1 ...)

Make a list out of provided items.

(length list)

Return the length of a list.

(assoc key alist)

Returns the first association pair for key in alist, comparing key against the alist elements.

(assoc 'r '((a . b) (c . d) (r . x) (s . y) (r . z))) 
        =>  (r . x) 

If not found, return undefined.


Array supports much more efficient random access than list. Also array supports mutable operations.

(array elem1 ...)

Making an array from elements. (array 1 2 3) can be written as [1 2 3].

(array->list arr)

(array-count arr)

(array-get arr index)

(array-set! arr index val)

(array-push arr obj)

(array-pop arr)

(array? obj)


Dictionary supports fast key-value look up. Like arrays, dictionaries are also mutable. It's a better alternative than associated list if there are many elements or if changes need to be made. However, dictionary can only use symbols as keys.

(dict k1 v1 ...)

(dict->list d)

Return an association list of the dictionary key values.

(dict-get d k)

(dict-set! d k val)

(dict? obj)

Quote and Quasiquote

Quotes and Quasiquotes are the source of LISP magic. However, they are difficult to understand for a beginner.

To make program text easier to write and read, we support the following notations. The left side is equivalent to the right side.

'a                       (quote a)
`a                       (quasiquote a)
,a                       (unquote a)
,@a                      (unquote-splicing a)

(quote obj)

quote is a special form. Return obj without evaluating it.

(quote alice) => alice   ;; symbol alice is not evaluated, itself is returned.
(quote (1 2 3)) => (1 2 3)

(quasiquote obj)

quasiquote is similar to quote, except that it allows you to selectively evaluate elements of the quoted list.

(quasiquote (1 2 ,(+ 1 2)))  => (1 2 3)

The comma-expression is a syntactic sugar of unquote. The following is equivalent but harder to read:

(quasiquote (1 2 (unquote (+ 1 2))))

Procedures and Macros

Procedure Definition

(define (average a b) 
    (/ (+ a b) 2) )
(define average 
    (lambda (a b) (/ (+ a b) 2)) )

Usually, they are almost equivalent ways of defining a procedure. More strictly, however, you should write:

(define average (lambda (&label average a b) ...))

Including the label average in arguments helps our procedure to resist external redefinition of average. Inside procedure body, average will be referring to the procedure itself.

Proper tail recursion is supported.

Procedure Arguments

Procedure arguments are evaluated from right to left.

(average a b)

b is evaluated first, then a. Other LISP implementation may use different order. When you design a program, you should not rely on a particular convention. If you want to impose a left to right order, use (begin ...).

For argument modifiers, we use the emacs lisp style rather than scheme.

&label  --- Only available in TeX Writer LISP.

&label is a special modifier that associates a following name with current procedure being defined, so that it's possible to refers to itself inside lambda body.

(lambda (&label self n)
    (if (> n 1)
        (self (- n 1)))


Macros works like procedures, except that the arguments are passed in without evaluation, and the returned object will be evaluated again to serve as final result. Macros usually relies on quasiquotes.

(defmacro (when pred &rest body)
  `(if ,pred 
    (begin ,@body)))

(when (> 2 1) (println "This is a sane universe."))    

Core Procedures

Special Forms

(quote obj)

(quasiquote obj)

(if condition then-form else-form)

If evaluated value of condition is not false, then-form is evaluated and the result returned, otherwise else-form is evaulated. If condition evaluates to false but else-form is not provided, then undefined is returned.

(cond clause ...)

Unlike if, cond can take one or more condition tests.

clause is like: (condition then-form ...).

It's possible to provide a fallback clause that will always be executed:

(else ...)
(true ...)

As long as condition doesn't evaluate to false, this clause will always be evaluated and the result taken as the final result of cond.

(not obj)

If obj is false, then return true, otherwise return false.

(and ...)

Evaluates arguments from left to right. If any of arguments evaulated to false, then execution is terminated, and false if returned. If none of the arguments evaluates to false, then the last evaluated result is returned. If no arguments provided, true is returned.

(or ...)

Evaluates arguments from left to right.

If any of arguments evaulated to a value other than false, then execution is terminated, and the value is returned. If every arguments evaluates to false, then false is returned. If no arguments provided, false is returned.

(let ((id val-expr) ...) body-expr ...)

(define x 1)
(let ((a x)) (+ a a))

Equals to

((lambda (a) (+ a a)) x)

let also has another form:

(let label ((id val-expr) ...) body-expr ...))

In this version, label is bound to the lambda procedure itself. It's convenient to do looping. For example, the following code sums from 100 to 1.

(let loop ((sum 0) (n 100))
  (if (= n 0) 
    (loop (+ sum n) (- n 1))))

Equals to

((lambda (&label loop sum n)
  (if (= n 0) 
    (loop (+ sum n) (- n 1))))  0 100)

(begin ...)

Arguments are evaluated from left to right. And the last evaluated result is returned.

(set! var value)

Assign value to var.


These math functions are modeled after libc standard functions.

(abs x)

Return the absolute value of a floating-point number x.

(acos x)

Return the principle value of the arc cosine of x. The result is in the range [0, pi].

(asin x)

Return the principal value of the arc sine of x. The result is in the range [-pi/2, +pi/2].

(cos x)

Return cosine of x (measured in radians).

(ceil x)

Return the smallest integral value greater than or equal to x.

(floor x)

Return the largest integral value less than or equal to x.

(round x)

Return the integral value nearest to x round- ing half-way cases away from zero.

(sin x)

Return the sine of x (measured in radians).

(sqrt x)

Return the non-negative square root of x.

(tan x)

Return the tangent of x (measured in radians).

(exp x)

Return e**x, the base-e exponential of x.

(log x)

Return the value of the natural logarithm of argument x.

(truncate x)

Return the integral value nearest to but no larger in magnitude than x.

(mod x y)

Return the floating-point remainder of x / y.

Specifically, the functions return the value x-i*y, for some integer i such that, if y is non-zero, the result has the same sign as x and magnitude less than the magnitude of y.

(atan x &optional y)

When y is omitted, return the principal value of the arc tangent of x. The result is in the range [-pi/2, +pi/2].

If y is presented, return the principal value of the arc tangent of x/y, using the signs of both arguments to determine the quadrant of the return value.


(eq? a b)

Perform equivalence check:

  1. If a and b is of different type, return false;
  2. If they are numbers, then they must has exactly same numeric value.
  3. If they are strings or symbols, then they must have same content.
  4. Otherwise, they must refer to the same object in memory.

(eval obj)

(apply proc args)

(format obj &key precision &key width)

Formatting an obj into a string. obj is either a number or string. If width is larger than actual representation, then padding is added. For numbers, whitespaces are left padded; for strings, whitespaces are padded on the right.

(format (/ 100 3) :precision 3 :width 8) => "  33.333"
(format "Hello" :width 8) => "Hello   "

(print arg1 ...)

Output provided arguments. println will output a newline character after all arguments are printed.

(load filename)

Load a file relative to current file running this expression.

(date &optional format unixtime)


Returns a datetime string. Examples:

(date "%F %T") => 2018-03-03 12:00:00


Return system unix timestamp.

TeX Writer LISP Extension

Unlike preceding sections, procedures in this section are implemented inside TeX Writer. They are not part of the core language.

Typically you write TeX Writer LISP inside twinit.l, which is then loaded when app starts. You must put twinit.l in your Local folder root.


TeX Writer borrows the concept of buffer from Emacs. You can think of buffer as an opened text file. However, buffers are not only about text files. Opened PDF documents, images, websites, are all called buffers despite that you can not edit them.

There are also special buffers:

These special buffers can't be closed.

Buffer Modes and Hooks

Buffer mode includes the configuration of interactive commands, key bindings, toolbar buttons, syntax tokens, keybar buttons. Different file types require different modes.

When opening a file, TeX Writer will compare filename against auto-mode-alist to decide which mode to apply.

(define auto-mode-alist '())
(add-to-list auto-mode-alist '("\\.tex$" . tex-mode))
(add-to-list auto-mode-alist '("\\.bib$" . bib-mode))

tex-mode is the name of a mode function that can be applied to set up configuration of a TeX file buffer.

Hooks are run at the end of each mode.

(add-hook tex-mode-hook (lambda () 
    ;; Do your additional setup

When you add an interactive command for a particular file type, you should add it in a hook.

Colors and faces are global. You should not customize the appearance in mode hook.

Buffer Procedures


File path of current buffer.


URL of current buffer.

(message msg)

Show msg in the echo area, also append it to *Messages* buffer with a timestamp.


Save current buffer. Bound to "command-s".

(open-url url)

Open a url in app.

(system-open url)

Different from open-url, this procedure opens a web url in external browser. If url is a file path, then shows an "Open in.." popover dialog.

(open path)

Open a file.

(save-as path)

Save current buffer to path, also update the buffer title to new name.

Text Editing

Typing on touch screen is hard. By exposing its core editing functions, TeX Writer encourages you to define powerful commands to insert text automatically.

Let's begin with a real example -- the begin-env command you can find in the tex mode. It inserts a pair of \begin{xxx} and \end{xxx}, leaving a blank line in between, and the caret on the blank line. Its implementation is:

1 (define (begin-env env)
2   (insert "\\begin{")
3   (insert env)
4   (insert "}")
5   (insert-newline)
6   (insert-newline)
7   (insert "\\end{")
8   (insert env)
9   (insert "}")
10  (goto-line (- (line-number-at-pos (point)) 1)))

Line 1 declares the command name and the only argument it takes -- an environment name. Line 2, 3 and 4 together insert "\begin{env}". Note that in line 2 two backslashes produce only one character. Line 5, 6 insert two newline characters, just as if you type the return key twice. The result is an empty line between begin and end, where the actual body goes. Line 7, 8 and 9 insert "\end{env}". Line 10 looks complicated, but if we hide the details then it is actually saying go to previous line, which is the empty line we've just created. previous-line is the result of (- (line-number-at-pos (point)) 1)}, where (point)returns the current caret position, and(line-number-at-pos (point))` returns the current line number.

Now we have defined the begin-env command, we need to register it before we can actually use it from the command bar. The interactive function registers a command to the command bar:

(interactive 'begin-env "Environment:%s" 
 '("document" "enumerate" "figure" "tabular"
   "verbatim" "center"
   "align" "align*"
   "display" "quote" ))

The first argument of interactive is the command name, which identifies the command you are adding and will be displayed on the command bar. The name should be preceded with a single quote. If your command doesn't take any argument, then you are done now.

In our case, begin-env command takes one argument, so we need to set up a prompt for receiving input. Following the command name, "Environment:\%s" defines the prompt string and the type of input we are expecting. \%s is the indicator for string. The last part is a list of available suggestions. They are shown on the right side of the command bar before you start to type. If one of them happens to be what you want, simply tap on it. Then as you type, unmatched suggestions will disappear, leaving only the ones with same prefix as your input so far.

Unlike begin-env, goto-line requires a numeric argument as the line number:

(interactive 'goto-line "Goto line:%d" 
 (lambda () (list 1 (line-max))))

%d means numeric input. The suggestions part for goto-line is a procedure. It means that suggestions are calculated on the fly. We can see that the final result is a list of two numbers --- first line and last line.

Built-in editing procedures

Here is the list of built-in functions that you can use to define your own commands.


The maximal value of text position, equals to the length of text. The minimal point is location of the first char, always zero.


The maximal value of line number, pointing to the last line. Unlike text position, the minimal line number starts at 1.


The current text position indicated by the caret. When there is selected text, caret won't be showing, then it returns the start of the selected range.


Returns a pair in the form of (start . end). start points to the first selected char, and end points to the location after the last selected char. end is exclusive. When start equals to end, selected text length is zero, meaning no text is selected, start is where the caret is. To get the start and end location as integer, you use

(car (selected-range)) => start
(cdr (selected-range)) => end


(set-selected-range start end)

Select the text between start and end, exclusively. When start equals to end, it moves caret to location start, which is effectively same as (goto-char start).

(goto-char pos)

Move caret to the position pos.

(string-from-range start end)

Return the text in the range.

(goto-line num)

Go to the beginning of line num.


Go to the beginning of next line.

(beginning-of-line num)

Return the beginning text position of the specified line.

(line-number-at-pos pos)

Return the line number at the point.

(insert text)

Insert the text at caret position. If there is selected text, it replace the current selected text. In the latter case, if text is empty, its effect is same as delete.

(goto-line line-number)

($) commands goes to the end of file . (courtesy to VIM).

(interactive command &optional prompt &rest suggestions)

Registers a command to the command bar. format is a string that contains prompt string and input type. suggestions can be either a list of values or a lambda that can generate a list at run time.

(interactive* abbr-name command &optional prompt &rest suggestions)

Same as interactive except that you can use a shorter name abbr-name to invoke the command. Note that you can use the 'abbr-name' in either bind-key, or toolbar and keybar item definitions.

(spell-check lang)

Enable spell check on current buffer with a specified language. If lang is false, then turn off spell check. You should be able to run spell or spell-off from the interactive commands. Supported languages is shown below the command prompt when you try to run spell command.

Customize UI and Appearances

Before customizing TeX Writer, you should know that TeX Writer's UI consists of 4 parts, from top to bottom:

TeX Writer Screentshot

Customize toolbars

It's possible to add buttons to toolbar:

(add-hook tex-mode-hook (lambda ()
    (add-to-toolbar "TeX" 'insert-tex)

insert-tex must be defined previously as a procedure.

Customize keybars

Text buffers have a keybar that will show up with system keyboard when they gain input focus. The keybar usually contains characters hard to find on iOS keyboard, or symbols that represent customized text input procedures.

Like with toolbar, keybar customization should be kept in a mode hook.

(add-hook tex-mode-hook (lambda ()
    (set-keybar '(
        ("\\" . tex-insert-backslash)
        ("{}" . insert-braces)
        ("$$" . tex-insert-inline-math)
        ("%"  . tex-insert-percent)
        ("_"  . tex-insert-underscore)
        ("^"  . tex-insert-circumflex)
        ("`"  . insert-backquote)
        ("()" . insert-parens)
        ("[]" . insert-brackets)

    (add-to-keybar "TeX" 'insert-tex)

In the code above, we use two different ways to customize key bar. set-keybar replaces existing bar items with the provided list, and add-to-keybar adds a new bar item.

Notice the differences between them:

Add Interactive Commands

You use (interactive 'command) to register a new command.

Then you can invoke interactive commands in any of the following ways:

  1. From toolbar "VIEW => Command..."
  2. From keybar "cmd"
  3. Use external keyboard "alt-x" (yes, it's like emacs)

Add Key Bindings

You can add keyboard bindings in mode hook.

(add-hook tex-mode-hook (lambda () 
    (bind-key "ctrl-u" 'some-procedure-you-defined)

Colors and Faces

Colors are specified with a hex color string (like in HTML).

(define background-color "#ffffff")
(define line-number-background-color "#ccc") 
(define fringe-color "#eee")
(define echo-background-color "#ffffff") 
(define caret-color "#ff8800") 
(define status-line-background-color "#bbb") 
(define status-line-highlight-background-color "#eee") 
(define spell-error-color "#f00")

A face is defined as an ordinary association list. For example, default-face is defined as:

(define default-face '((font-name . "CourierNewPSMT")
                       (font-size . 16) 
                       (color . "#000000")))

(define keyword-face '((color . "#001996")))
(define comment-face '((color . "#008019")))
(define string-literal-face '((color . "#960303")))
(define status-line-face '((color . "#000000")(font-size . 16)))
(define echo-face '((color . "#000000") (font-size . 16)))
(define line-number-face '((color . "#333") (font-size . 12)));;RGB(30,30,30)

Other face definitions don't have font name attribute, they simply use the same font from default-face.

You can define your own face and use it in token definition.

(add-to-face face attr value)

Update face definition. Set attr with value. For example, the following code updates default-face's font to "Courier".

  (add-to-face default-face 'font-name "Courier")

Note that the first argument is without quote.

(set-keyboard-appearance type)

type : could be 'light or 'dark.

Setting keyboard appearance affects the system status bar color as well. You need to configure the rest so that the entire application looks consistent.

(add-to-face face key value)

It's simple to define a new face so that you can use it in token defintion.

(Need an image for describing various parts)

Add Token Definitions

You can define additional tokens for syntax highlighting.

(add-token token-definition)

token-definition is a pair of regular expression and face symbol. Example:

(add-token '("%.*$" . comment-face))
(add-token '("(\\${1,2}).*?\\1" . string-literal-face))

For regular expression syntax, please refer to:

Appendix: sample twinit.l

(set! background-color "#272822")
(set! line-number-background-color "#ccc") 
(set! fringe-color "#eee")
(set! echo-background-color "#ffffff") 
(set! caret-color "#ff8800") 
(set! status-line-background-color "#bbb") 
(set! status-line-highlight-background-color "#eee") 
(set! spell-error-color "#f00")

;; Set default font
(add-to-face default-face 'font-name "AmericanTypewriter")
(add-to-face default-face 'color "#f8f8f2")
(add-to-face keyword-face 'color "#001996")
(add-to-face comment-face 'color "#008019")
(add-to-face string-literal-face 'color "#960303")
(add-to-face status-line-face 'color "#000000")
(add-to-face status-line-face 'font-size 16)
(add-to-face echo-face 'color "#000000") 
(add-to-face echo-face 'font-size 16)
(add-to-face line-number-face 'color "#333")
(add-to-face line-number-face 'font-size  12)
(set-keyboard-appearance 'light)  ;; or 'dark

(add-hook text-mode-hook (lambda () 
    (bind-key "ctrl-u" 'page-up)
    (bind-key "ctrl-d" 'page-down)

(define (my-page-up)
    (message "My Page Up"))

(define (insert-tex) 
    (insert "\\TeX\\ "))

(add-hook tex-mode-hook (lambda () 
    (add-to-keybar "TeX" 'insert-tex)
    (interactive 'page-up)
    (bind-key "ctrl-u" 'my-page-up)