Notes on TeXmacs logo[main]Notes on TeXmacs

Customizing TeXmacs with Scheme: a commented collection of snippets

Many aspects of the behaviour of TeXmacs can be changed with a few lines of Scheme in your personal initialization files. We explain where such code goes, how TeXmacs loads its own Scheme code and why this matters for your customizations, and then present a collection of tested snippets for keyboard shortcuts, menus, export, sessions and new documents.

[Version 1.0 of 24.10.2026. Based on the forum wiki thread Assorted code snippets to customize TeXmacs' behaviour and its contributors.]

The forum wiki thread on which this article is based was started in 2021 by jeroen to collect small pieces of Scheme code that users put in their initialization files, and it was extended and debugged by pireddag, mgubi and others. Here we take its content one step further: we explain the mechanisms that the snippets rely on, we check every snippet against the source code, we fix those that do not work (or work only by luck), and we add a few more.

Everything was tested with TeXmacs 2.1.4, using a fresh user configuration, and checked against the development version (SVN trunk, revision 15370, October 2026), whose Scheme code is essentially identical. Links to the sources point to the GitHub mirror of the official repository.

1.Where your code goes

TeXmacs reads two personal Scheme files, both in the progs subdirectory of your TeXmacs home directory (~/.TeXmacs on Linux and macOS, %APPDATA%\TeXmacs on Windows):

my-init-texmacs.scm

is executed once, when TeXmacs starts, right after the system file init-texmacs.scm. This is the place for keyboard shortcuts, menus and new commands. Changes take effect after a restart.

my-init-buffer.scm

is executed each time a document is opened in a new view, both for new and for existing documents, after the system file init-buffer.scm. This is the place for settings of new documents. Changes take effect for the next document you open, without a restart.

The easiest way to edit them is from TeXmacs itself: enable Tools→Developer tool; a Developer menu appears, with the entries Open my-init-texmacs.scm and Open my-init-buffer.scm (which create the files if they do not exist yet). When you paste code from a web page, Edit→Paste from→Verbatim avoids surprises with formatting.

One important fact: an error in my-init-texmacs.scm stops the execution of the rest of the file. The error message is printed on the terminal (if you started TeXmacs from one), together with the line of the offending expression. If a customization seems to have no effect, check that nothing before it fails. Section 9 explains how to investigate.

2.How TeXmacs loads its Scheme code

Most surprises with personal customizations come from one design decision: to start quickly, TeXmacs loads most of its Scheme modules lazily. At startup, init-texmacs.scm (in TeXmacs/progs/init-texmacs.scm) loads a few essential modules, such as the main menus, and otherwise mostly declares where things are defined:

Your my-init-texmacs.scm runs before these idle-time loads. This has three consequences, which we observed in our tests:

  1. A keyboard shortcut defined at the top level of your file can be overridden by a standard keymap loaded later. For instance, a binding for C-s is replaced by the standard binding which makes text strong (on macOS, in text mode) or saves the document (on Linux and Windows, in all modes).

  2. A menu which you extend at the top level of your file may not be defined yet when your file runs, and its module may be loaded later. Whether your additions survive then depends on details of the loading order, so it is better not to rely on it.

  3. A function of a module which has not been loaded, and which is not declared with lazy-define, is not defined at all, and calling it is an error, which also aborts the rest of your file.

;; Does NOT work as intended in my-init-texmacs.scm:
(kbd-map ("C-s" (kbd-up)))        ; later overridden by a standard keymap
(output-set-line-length 1000000)  ; error: unbound variable

There are two remedies, both used throughout the snippets below:

;; Run after the standard keymaps have been loaded:
(delayed
  (lazy-keyboard-force #t)
  (kbd-map ("C-s" (kbd-up))))

;; Load the module which defines a function before using it:
(import-from (convert tools output))
(output-set-line-length 1000000)

The delayed variant was used in the original forum snippets, and is the right choice for keyboard shortcuts; import-from is more predictable for functions and menus.

3.The customization vocabulary

A handful of Scheme forms cover almost all customizations.

Keyboard shortcuts.
(kbd-map (key action) …) binds key sequences to actions. A key sequence is a string of keys separated by spaces, like "M-e p" (first M-e, then p). The modifiers are written S- (Shift), C- (Control), A- (Alt, or Option on a Mac) and M- (Meta: the Windows or Super key, or Command on a Mac). If your system intercepts a combination, the Escape key can be used as a modifier: on Linux and Windows it stands for M-, and pressed twice for A-; on macOS it stands for A- (Option), twice for C-, three times for M- (Command). Since the conventional modifier for commands differs between systems, TeXmacs also offers portable prefixes: std is C- on Linux and Windows and M- (Command) on a Mac, so that "std s" is the usual “save” shortcut everywhere; cmd, altcmd and extra are further portable combinations. These prefixes are defined by a lazily loaded keymap, so they only work once that keymap is loaded, that is after (lazy-keyboard-force #t); we always do this inside delayed, which is the safest choice.

A kbd-map may start with (:mode in-math?) (or in-text?, in-session?, in-prog?, …) to restrict the bindings to a mode. When several bindings apply, the one defined last wins, which is again why the order of loading matters.

Editor commands.
(kbd-commands (name help action) …) defines commands that you type as \name followed by Return, like the built-in \red which inserts red text.

Menus.
(menu-bind name entries …) redefines a menu. The special entry (former) stands for the previous content of the menu, so that you can add entries instead of replacing everything. In menus, --- is a separator, ("label" action) an entry, (-> "label" …) a submenu, (=> "label" …) a pull-down menu (as in the menu bar) and (group "text") a heading.

Functions and contextual overloading.
tm-define defines a function, like define, but it can be redefined in specific contexts. With (:require condition), the new definition is used only when the condition holds; otherwise the previous definition is called. This example from the TeXmacs developer documentation defines a command which behaves differently in math mode (to try it, bind (hello) to a key of your choice with kbd-map, and press it in text and in math mode):

(tm-define (hello) (insert "Hello"))

(tm-define (hello)
  (:require (in-math?))
  (insert-go-to "hello()" '(6)))

In the body of a redefinition, former refers to the previous definition, so that you can extend a function instead of replacing it:

(tm-define (save-buffer . args)
  (apply former args)
  (set-message "Saved!" "my-init-texmacs.scm"))

Much of TeXmacs is organized this way. For instance, the Return key calls (kbd-enter t shift?), where t is the innermost structure around the cursor, and each kind of structure (sessions, tables, lists…) overloads kbd-enter with a :require clause. Your own definitions can join in.

Preferences.
(get-preference "name") and (set-preference "name" "value") read and write the preferences stored in system/preferences.scm in your TeXmacs home directory. Values are strings, and set-preference saves the preferences file when a value changes; for settings that should simply persist, it is more natural to call it once, in a Scheme session, than at every start. You can invent your own preference names, as we do below.

4.Keyboard snippets

Emacs-like cursor movements.
Use C-a, C-f, C-s and C-d as arrow keys (from the forum thread). Without delayed, most of the four bindings are overridden by the standard ones. Note that this replaces standard shortcuts: on Linux and Windows C-a, C-f and C-s are “select all”, “find” and “save”, and on Linux C-d deletes the next character; on macOS C-a, C-s and C-d insert an abbreviation, strong text and a description list.

(delayed
  (lazy-keyboard-force #t)
  (kbd-map
    ("C-a" (kbd-left))
    ("C-f" (kbd-right))
    ("C-s" (kbd-up))
    ("C-d" (kbd-down))))

Subscripts and superscripts in sessions.
Type \ _ or \ ^ followed by Tab (Jupyter style) to insert a subscript or a superscript, only inside sessions. The key name var stands for Tab.

(delayed
  (lazy-keyboard-force #t)
  (kbd-map
    (:mode in-session?)
    ("\\ _ var" (make-script #f #t))
    ("\\ ^ var" (make-script #t #t))))

A shortcut for a color.
With this definition, typing \darkgreen and Return starts dark green text, just like the built-in \red. Command names cannot contain spaces.

(kbd-commands
  ("darkgreen" "" (make-with "color" "dark green")))

Insert today's date.
Typing \today and Return inserts the current date as text (unlike the date tag, it does not change later).

(kbd-commands
  ("today" "insert today's date"
   (insert (strftime "%Y-%m-%d" (localtime (current-time))))))

Alternative shortcuts for the source mode.
Some window managers (the forum mentions Sway) do not pass the default shortcut for Document→Source→Edit source tree (M-A-s, that is Command+Option+S on a Mac and Super+Alt+S elsewhere) to TeXmacs. Keep only the line you want. Note that with the developer tool enabled, A-F1 normally shows the documentation of the Scheme symbol at the cursor. The delayed makes sure that your choice wins over standard bindings: on macOS A-space enlarges the selection, and on Linux and Windows A-s makes text strong (or inserts a square root in math mode), which these bindings replace.

(delayed
  (lazy-keyboard-force #t)
  (kbd-map
    ("A-space" (toggle-source-mode))
    ("A-F1" (toggle-source-mode))
    ("A-s" (toggle-source-mode))))

Shortcuts that insert structures.
Bind a key to the insertion of a theorem in text mode (with the portable std prefix), and ; ; to a fraction in math mode. Any tag can be inserted with (make 'tag-name). The second binding replaces the standard meaning of ; ; in math mode.

(delayed
  (lazy-keyboard-force #t)
  (kbd-map (:mode in-text?)
    ("std T" (make 'theorem)))     ; Ctrl+Shift+T, or Cmd+Shift+T on a Mac
  (kbd-map (:mode in-math?)
    ("; ;" (make-fraction))))

5.Menus and toolbars

More fonts in the font menu of the toolbar.
The forum snippet used delayed; loading the module that defines the menu is more robust. The fonts must be installed on your system.

(import-from (generic document-menu))

(menu-bind document-short-font-menu
  (former)
  ---
  (group "My fonts")
  ("Cantarell" (init-font "Cantarell"))
  ("Garamond" (init-font "Garamond Libre"))
  ("Montserrat" (init-font "Montserrat")))

A menu of your own.
The empty menu texmacs-extra-menu is a hook, provided by TeXmacs for additions to the menu bar. Here we use it for a My tools menu (the first entry is defined in Section 6):

(menu-bind texmacs-extra-menu
  (former)
  (=> "My tools"
      ("Export PDF next to document" (export-to-same-name-pdf))
      ("Toggle source mode" (toggle-source-mode))
      ---
      ("Edit my-init-texmacs.scm"
       (load-document "$TEXMACS_HOME_PATH/progs/my-init-texmacs.scm"))))

A toolbar button.
The main toolbar is the menu texmacs-main-icons; / is a separator between icons. The command is again the one of Section 6 (the order of the definitions in the file does not matter, since the action only runs when the button is clicked):

(menu-bind texmacs-main-icons
  (former)
  /
  ((balloon (icon "tm_print.xpm") "Export PDF next to document")
   (export-to-same-name-pdf)))

An entry in the context menu (the menu that appears with a right click) is added in the same way, through texmacs-popup-menu; see the sessions snippet in Section 7.

6.Export

Export to a PDF file next to the document.
The first snippet of the forum thread binds M-e p to an export of the current document to a PDF file with the same name. The snippet in the thread builds the name of the PDF file from the name of the document:

(define (propose-pdf-name)
  (with name (propose-name-buffer)
    (if (string-ends? name ".tm")
        (string-append (string-drop-right name 3) ".pdf"))))

This works on Linux and macOS but fails on Windows with a “Could not concretize tuple” error, as found by pireddag (post 6 to post 14). The reason is instructive. Inside TeXmacs, file names are not strings but urls, structured values which know about the conventions of the operating system. propose-name-buffer converts the url of the document into a Unix-style string, so that on Windows C:\Users\me\x.tm becomes /C/Users/me/x.tm, without the colon after the drive letter. When the modified string is later converted back into a url, it is read with the Windows conventions, and without the colon it is no longer recognized as an absolute path. The lesson is general: do not build file names by string manipulation; keep them as urls and use the url functions (url-relative, url-basename, url-glue, url-exists?, …).

Here is a version which follows this advice, also checks that the document has been saved (and is a .tm file), and uses the same function as File→Export→Pdf:

(tm-define (same-name-pdf-url)
  (with u (current-buffer)
        (and (not (url-scratch? u))
         (== (url-suffix u) "tm")
         (url-relative u (string-append (url-basename u) ".pdf")))))

(tm-define (export-to-same-name-pdf)
  (with pdf (same-name-pdf-url)
    (cond ((not pdf)
           (set-message "Save the document first" "Export to PDF"))
          ((url-exists? pdf)
           (user-confirm "File already exists. Really overwrite?" #f
             (lambda (answ) (when answ (wrapped-print-to-file pdf)))))
          (else (wrapped-print-to-file pdf)))))

(kbd-map ("M-e p" (export-to-same-name-pdf)))

On Windows, M-e is Win+E, which the system uses to open the file explorer; choose another key there, or press Escape e p.

Update the PDF at every save.
Building on the previous snippet (it uses same-name-pdf-url, so keep both), we can extend save-buffer with former so that the PDF is updated whenever the document is saved, if a preference of our own is set. It calls wrapped-print-to-file directly, so that saving does not ask every time whether to overwrite the PDF:

(tm-define (save-buffer . args)
  (apply former args)
  (when (== (get-preference "my:pdf on save") "on")
    (with pdf (same-name-pdf-url)
      (when pdf (wrapped-print-to-file pdf)))))

Turn it on and off from a Scheme session (Insert→Session→Scheme):

(set-preference "my:pdf on save" "on")    ; turn it on
(set-preference "my:pdf on save" "off")   ; turn it off

Long lines in LaTeX export.
By default the LaTeX exporter breaks lines after 79 characters, which can be annoying when the LaTeX file is later edited or put under version control. This sets the line length to a huge value:

(import-from (convert tools output))
(output-set-line-length 1000000000)

The forum version, (delayed (:idle 1000) (output-set-line-length 1000000000)), usually works too, but only if the module has been loaded within one second; the explicit import-from removes the guesswork.

7.Sessions

Multiline input by default.
In a multiline session, Return inserts a new line and S-return evaluates. The arguments are the plugin and the session name, as in Insert→Session:

(import-from (dynamic session-edit))
(set-session-multiline-input "python" "default" #t)
(set-session-multiline-input "julia" "default" #t)

Evaluate all the sessions of a document.
The command below, bound to A-e and added to the context menu inside sessions, evaluates every session of the document in turn. (On Linux and Windows, A-e normally inserts an enumeration in text mode; pick another key if you use that.) Compared with the forum version, it searches for session-document-context? instead of subsession-document-context?, so that unfolded subsessions are not evaluated twice, and it drops an import of tree-search from the module (link link-extract): tree-search is now a standard function, and the old import raises an error in current versions.

(tm-define (evaluate-all-sessions)
  (for-each (lambda (doc)
              (tree-go-to doc :last 1 :end)
              (session-evaluate-all))
            (tree-search (buffer-tree) session-document-context?)))

(delayed
  (lazy-keyboard-force #t)
  (kbd-map ("A-e" (evaluate-all-sessions))))

(tm-menu (texmacs-popup-menu)
  (:require (inside? 'session))
  (former)
  ---
  ("Evaluate all sessions in document" (evaluate-all-sessions)))

Evaluate without creating a new input field.
At the end of the thread, hamorabi asked how to avoid the empty input field that appears after each evaluation (post 24 and post 26). There is no option for this: the new field is part of the protocol with the plugin, which writes its next prompt into it (see field-process-input in TeXmacs/progs/dynamic/session-edit.scm). We can, however, remove it once the evaluation is finished. The following redefines C-return inside session inputs (using contextual overloading of kbd-control-enter) to evaluate the current field, keep the cursor in it, and remove the empty field that was appended, if any. Plain Return keeps the usual behaviour; to add a new field when you need one, use Insert field below in the Focus menu or on the focus toolbar.

(import-from (dynamic session-edit))

(define (my-field-busy? t)
  (with out (and (tree? t) (> (tree-arity t) 2) (tree-ref t 2))
    (and out (tree-is? out 'document) (> (tree-arity out) 0)
         (tree-is? (tree-ref out :last) 'script-busy))))

(define (my-empty-input? u)
  (and (tree? u) (tree->path u)
       (tree-in? u '(input input-math input-text))
       (tree-empty? (tree-ref u 1))))

(tm-define (session-evaluate-in-place)
  (with-innermost t field-input-context?
    (with last? (not (tree-ref t :next))
      (session-evaluate)                  ; may append a new field u
      (with u (and last? (tree-ref t :next))
        (tree-go-to t 1 :end)             ; stay in the evaluated field
        (when u
          (delayed
            (:pause 100)
            (:require (not (my-field-busy? t)))  ; wait for the output
            (when (my-empty-input? u)
              (tree-remove (tree-ref u :up) (tree-index u) 1))))))))

(tm-define (kbd-control-enter t shift?)
  (:require (field-input-context? t))
  (session-evaluate-in-place))

We tested it with the Scheme plugin; other plugins use the same mechanism, but check it with yours.

8.New documents

The following snippets go into my-init-buffer.scm. Since that file is run for every document that is opened, they first check that the document is new, using (buffer-has-name? (current-buffer)), and end with buffer-pretend-saved, so that an untouched new document is not considered modified.

Add a style package to every new document.
From the forum, for a package (no-solutions-in-html) written by the user:

(when (not (buffer-has-name? (current-buffer)))
  (add-style-package "no-solutions-in-html")
  (buffer-pretend-saved (current-buffer)))

Default style, language and fonts.
Every new document is an article in British English, set in the TeX Gyre Termes fonts at 11 points:

(when (not (buffer-has-name? (current-buffer)))
  (init-style "article")
  (add-style-package "british")
  (init-font "termes" "math-termes")
  (init-env "font-base-size" "11")
  (buffer-pretend-saved (current-buffer)))

The manual of TeXmacs gives a similar example using a function buffer-newly-created?; this function does not exist in current versions, use buffer-has-name? as above.

9.When something goes wrong

(defined? 'output-set-line-length)        ; is the function loaded?
(procedure-sources kbd-control-enter)     ; all its definitions
(with b (kbd-find-key-binding (kbd-pre-rewrite "std s"))
  (and b (promise-source (car b))))      ; what does a key do?
(debug-message "std-warning" "my-init: reached line 42\n")

When you find the definition of a function you want to change, the pattern is almost always the same: load its module with import-from, then redefine it with tm-define, using a :require clause or former to keep the original behaviour where you do not need to change it.

10.Corrections to older advice

While testing the snippets we found a few pieces of advice, in the forum and in the official documentation, which are no longer accurate:

The forum thread is still open, and new snippets are welcome there or as pull requests to the repository of this website.