[main]Notes on TeXmacs
[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.
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):
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.
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
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.
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:
lazy-define declares functions; the module which defines them is loaded the first time one of them is called.
lazy-keyboard declares keymaps; they are loaded when TeXmacs is idle for a moment, or when the corresponding mode becomes active.
lazy-menu, lazy-format and lazy-initialize do the same for menus, file formats and other initializations, all of them loaded after a short idle time.
Your my-init-texmacs.scm runs before these idle-time loads. This has three consequences, which we observed in our tests:
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).
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.
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:
(delayed …) runs its body later, from the event loop. Combined with (lazy-keyboard-force #t), which loads all the pending keymaps immediately, it makes sure that your bindings are installed after the standard ones (keymaps of plugins loaded even later could still override them). (delayed accepts options such as (:idle 1000) to wait until the user is idle for a second, or (:pause 100).)
(import-from (module name)) loads a given module immediately, so that its functions and menus are defined before you use or extend them. The name of the module is the path of its file under progs: (convert tools output) is the file progs/convert/tools/output.scm.
;; 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.
A handful of Scheme forms cover almost all customizations.
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.
(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.
(delayed
(lazy-keyboard-force #t)
(kbd-map
("C-a" (kbd-left))
("C-f" (kbd-right))
("C-s" (kbd-up))
("C-d" (kbd-down))))
(delayed
(lazy-keyboard-force #t)
(kbd-map
(:mode in-session?)
("\\ _ var" (make-script #f #t))
("\\ ^ var" (make-script #t #t))))
(kbd-commands
("darkgreen" "" (make-with "color" "dark green")))
(kbd-commands
("today" "insert today's date"
(insert (strftime "%Y-%m-%d" (localtime (current-time))))))
(delayed
(lazy-keyboard-force #t)
(kbd-map
("A-space" (toggle-source-mode))
("A-F1" (toggle-source-mode))
("A-s" (toggle-source-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))))
(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")))
(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"))))
(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.
(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
(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.
(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 (
(set-preference "my:pdf on save" "on") ; turn it on (set-preference "my:pdf on save" "off") ; turn it off
(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.
(import-from (dynamic session-edit)) (set-session-multiline-input "python" "default" #t) (set-session-multiline-input "julia" "default" #t)
(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)))
(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.
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.
(when (not (buffer-has-name? (current-buffer))) (add-style-package "no-solutions-in-html") (buffer-pretend-saved (current-buffer)))
(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.
Try your code in a Scheme session (
Start TeXmacs from a terminal to see the messages printed during startup, including errors in my-init-texmacs.scm (on macOS, run /Applications/TeXmacs.app/Contents/MacOS/TeXmacs). On Windows, standard output is not available this way (see the article on standard output on Windows).
Enable
A few useful questions to ask in a Scheme session:
(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.
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 function url-system, suggested in the thread, does not exist; the conversion functions are url->system, url->unix, system->url and unix->url.
The import (use-modules ((link link-extract) #:select (tree-search))), proposed for older versions, now raises an error.
The example of my-init-buffer.scm in the manual uses buffer-newly-created?, which does not exist.
The developer documentation on lazy loading states that mode-specific keyboard bindings take precedence over general ones; in current versions the binding defined last wins, whatever its mode.
The forum thread is still open, and new snippets are welcome there or as pull requests to the repository of this website.