I would like emacs to check for a ~/emacs.d/scratch.txt and use it's contents instead of the default message when emacs starts.

How can this be done?

4 Answers 4


initial-buffer-choice allows to specify a path to a file or directory:

(let ((filename "~/.emacs.d/startup.txt"))
  (when (file-exists-p filename)
    (setq initial-buffer-choice filename)))
  • 1
    This has a fairly big disadvantage that the file gets loaded, so running emacs somefile.txt will not show the file given in the command line, but the startup file instead. It also doesn't use the scratch buffer name which is ignored for buffer switching in some cases.
    – ideasman42
    Feb 10, 2018 at 8:15
  • Well, you're changing what the initial buffer is, so it's hardly surprising it's not named *scratch* anymore. As for the other thing, if I run emacs somefile.txt, it opens that file in a split, additionally to the buffer I've specified. This is hardcoded in startup.el.
    – wasamasa
    Feb 10, 2018 at 9:39
  • 1
    My aim was simply to have text from a file to replace the default text in *scratch*, not to open a file or split windows. Basically, if I open emacs with no files: show a buffer with useful content, otherwise behave as normal.
    – ideasman42
    Feb 10, 2018 at 9:59

This will do the job.

(let ((filename (concat user-emacs-directory "scratch.txt")))
  (when (file-exists-p filename)
    (let ((scratch-buf (get-buffer "*scratch*")))
      (when scratch-buf
        (with-current-buffer scratch-buf
          (insert-file-contents filename))))))
  • Perfect! No initial-buffer-choice needed.
    – ideasman42
    Feb 10, 2018 at 12:19
  • Update, one down side is you can see the buffers changing at startup, updating own answer with different solution.
    – ideasman42
    Feb 13, 2018 at 2:29
  • I've edited own answer based on this one - which only runs when no other buffers are loaded (avoiding flickering at startup)
    – ideasman42
    Feb 17, 2018 at 21:51
  • Consider using expand-file-name instead of concat. (expand-file-name "scratch.txt" user-emacs-directory ))
    – RichieHH
    Feb 24, 2020 at 6:21

I would use initial-scratch-message.

(let ((file "~/.emacs.d/scratch.txt"))
  (when (file-exists-p file)
    (setq initial-scratch-message
        (insert-file-contents file)

This can be done using initial-buffer-choice,

While initial-buffer-choice can be set to a filename, this will load the file as well as any files passed via the command line (splitting the window or not even showing the buffer depending on your setup).
So reading file data into *scratch* buffer has the advantage that exact behavior is preserved, just replacing the text.

This example:

  • Only runs when the user starts without loading a file.

  • Loads in a text file into the startup buffer.

  • Users a default startup.txt, optionally taking a user defined startup file.

  • Sets the mode based on the filename, so users can pass in startup.org for an org-mode buffer.

  • Adds a short commented line at the top of the file, eg:

    # Emacs: 28.0, time: 0.93, packages: 58

  • When no startup file is found, the name of the file to create is referenced.

;; Load startup text when available.
;; Example usage:
;;   (my-scratch-buffer-from-file)
;; Or if you like to use an org-mode scratch buffer,
;; an option file path can be passed in.
;; The file extension is used to set the mode:
;;   (my-scratch-buffer-from-file (concat user-emacs-directory "scratch.org"))

(defvar my-scratch-buffer-from-file--value nil)
(defun my-scratch-buffer-from-file (&optional scratch-file)
  (setq inhibit-startup-screen t)
  (setq initial-scratch-message nil)
  (when scratch-file
    (setq my-scratch-buffer-from-file--value scratch-file))

   (lambda ()
     (if (buffer-file-name)
         (current-buffer) ;; leave as-is
       (let ((original-buffer (current-buffer))
              (or my-scratch-buffer-from-file--value
                  (concat user-emacs-directory "scratch.txt")))
             ;; Not essential, just gives some handy startup info.
               "Emacs: %d.%d, time: %.2f, packages: %d"
               (float-time (time-subtract after-init-time before-init-time))
               (length package-activated-list))))
         (with-current-buffer (get-buffer-create "*scratch*")
           ;; Don't track undo.

           ;; Set the mode based on the filename, users may use filenames that infer modes.
           (condition-case err
               (let ((buffer-file-name filename))
                 (set-auto-mode t))
             (error (message "Unable to activate mode: %s" err)))

           ;; Use the comment character set by the mode where possible.
           (let ((comment-start-or-empty
                  (if comment-start
                      ;; Ensure one trailing space (some comments include space).
                       (replace-regexp-in-string "[[:blank:]]*$" "" comment-start)
                       " ")
             (if (file-exists-p filename)
                 (insert-file-contents filename)
                (format "Scratch buffer, create '%s' to replace this text on startup."
               (goto-char (point-min)))
             ;; Add some startup info above the static text.
             (insert comment-start-or-empty startup-info "\n\n"))
           (set-buffer original-buffer)))))))

Others might suggest how this can be done better, it seems to work well enough though.

  • 1
    With proper indentation and the closing parentheses not spread apart.
    – wasamasa
    Feb 9, 2018 at 9:01
  • @wasamasa done.
    – ideasman42
    Feb 22, 2020 at 0:07
  • Your formatter does somewhat better, but it's still far from idiomatic. Even this thing does better than that: t3x.org/lisp64k/grinding.html
    – wasamasa
    Feb 22, 2020 at 11:44
  • It looks like this doesn't wrap long lines, so I don't think it's usable in it's current form.
    – ideasman42
    Feb 22, 2020 at 23:42
  • Thats a lot of code to replace initial-scratch-message. Also see the package melpa.org/#/elisp-format . Not ideal but is any elisp formatter? Everyone seems to have their own way....
    – RichieHH
    Feb 24, 2020 at 6:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.