| name | widget |
| description | A guide to using the Emacs Widget Library for creating interactive UI elements in buffers. |
Emacs Widget Library
Build interactive forms and UI elements in Emacs buffers using the built-in widget library (wid-edit.el).
Overview
The Emacs Widget Library provides a framework for creating interactive UI elements within text buffers. Widgets enable building forms, configuration interfaces, and custom UI components without requiring external dependencies.
Core libraries:
widget.el- Top-level widget interfacewid-edit.el- Widget implementation and editing
Primary use cases:
- Custom configuration interfaces
- Interactive forms and dialogs
- Settings and preference editors
- Data entry and validation
Setup
(require 'widget)
(eval-when-compile
(require 'wid-edit))
Core Concepts
Widget Lifecycle
- Create -
widget-createreturns a widget object - Setup -
widget-setupenables interaction after creation - Interact - User edits/activates widgets
- Query - Retrieve values with
widget-value - Delete - Clean up with
widget-delete
Widget Buffer Setup
(defun my-widget-example ()
(interactive)
(switch-to-buffer "*Widget Example*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
;; Create widgets
(widget-insert "Example Form\n\n")
(widget-create 'editable-field
:format "Name: %v"
:size 30
"Enter name")
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Submitted!"))
"Submit")
(widget-insert "\n")
;; Enable widgets
(use-local-map widget-keymap)
(widget-setup))
Widget Properties
Widgets are configured via keyword arguments:
:value- Initial/current value:format- Display format string:tag- Label text:notify- Callback function on change:help-echo- Tooltip text
Widget Types
Text Input
editable-field
Basic text input field.
;; Simple field
(widget-create 'editable-field
:format "Label: %v\n"
"default text")
;; Sized field
(widget-create 'editable-field
:size 20
:format "Username: %v\n")
;; Password field
(widget-create 'editable-field
:secret ?*
:format "Password: %v\n")
;; With change notification
(widget-create 'editable-field
:notify (lambda (widget &rest ignore)
(message "Value: %s" (widget-value widget)))
:format "Email: %v\n")
text
Multi-line text area.
(widget-create 'text
:format "Comments:\n%v"
:value "Line 1\nLine 2\nLine 3")
Buttons
push-button
Clickable button that triggers action.
;; Basic button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Clicked!"))
"Click Me")
;; Styled button
(widget-create 'push-button
:button-face 'custom-button
:format "%[%t%]\n"
:tag "Submit Form"
:notify (lambda (&rest ignore)
(my-submit-form)))
link
Hyperlink that executes action.
;; Function link
(widget-create 'link
:button-face 'info-xref
:help-echo "View documentation"
:notify (lambda (&rest ignore)
(describe-function 'widget-create))
"Documentation")
;; URL link
(widget-create 'url-link
:format "%[%t%]"
:tag "Emacs Manual"
"https://www.gnu.org/software/emacs/manual/")
Selection
checkbox
Boolean toggle (checked/unchecked).
(widget-create 'checkbox
:format "%[%v%] Enable feature\n"
:notify (lambda (widget &rest ignore)
(message "Feature %s"
(if (widget-value widget)
"enabled"
"disabled")))
t) ; Initial state
toggle
Text-based on/off switch.
(widget-create 'toggle
:on "Enabled"
:off "Disabled"
:notify (lambda (widget &rest ignore)
(my-update-setting (widget-value widget)))
nil) ; Initially off
radio-button-choice
Single selection from multiple options.
(widget-create 'radio-button-choice
:value "medium"
:notify (lambda (widget &rest ignore)
(message "Selected: %s" (widget-value widget)))
'(item "small")
'(item "medium")
'(item "large"))
menu-choice
Dropdown menu selection.
(widget-create 'menu-choice
:tag "Output Format"
:value 'json
'(const :tag "JSON" json)
'(const :tag "XML" xml)
'(const :tag "Plain Text" text)
'(editable-field :menu-tag "Custom" "custom-format"))
checklist
Multiple selections (subset of options).
(widget-create 'checklist
:notify (lambda (widget &rest ignore)
(message "Selected: %S" (widget-value widget)))
'(const :tag "Option A" option-a)
'(const :tag "Option B" option-b)
'(const :tag "Option C" option-c))
Lists
editable-list
Dynamic list with add/remove buttons.
(widget-create 'editable-list
:entry-format "%i %d %v"
:value '("item1" "item2")
'(editable-field :value ""))
Format specifiers:
%i- Insert button (INS)%d- Delete button (DEL)%v- The value widget
Widget API
Creation and Setup
widget-create
Create widget and return widget object.
(widget-create TYPE [KEYWORD ARGUMENT]...)
;; Example
(setq my-widget
(widget-create 'editable-field
:size 25
:value "initial"))
widget-setup
Enable widgets after creation. Must be called before user interaction.
(widget-setup)
Required after:
- Initial widget creation
- Calling
widget-value-set - Modifying widget structure
widget-insert
Insert text at point (not a widget).
(widget-insert "Header Text\n\n")
Value Access
widget-value
Get current widget value.
(widget-value WIDGET)
;; Example
(let ((name (widget-value name-widget))
(enabled (widget-value checkbox-widget)))
(message "Name: %s, Enabled: %s" name enabled))
widget-value-set
Set widget value programmatically.
(widget-value-set WIDGET VALUE)
(widget-setup) ; Required after value change
;; Example
(widget-value-set email-widget "user@example.com")
(widget-setup)
Property Access
widget-get
Retrieve widget property.
(widget-get WIDGET PROPERTY)
;; Example
(widget-get my-widget :tag)
(widget-get my-widget :size)
widget-put
Set widget property.
(widget-put WIDGET PROPERTY VALUE)
;; Example
(widget-put my-widget :help-echo "Enter your email address")
widget-apply
Call widget method with arguments.
(widget-apply WIDGET PROPERTY &rest ARGS)
;; Example
(widget-apply my-widget :notify)
Navigation
widget-forward
Move to next widget (bound to TAB).
(widget-forward &optional COUNT)
widget-backward
Move to previous widget (bound to S-TAB/M-TAB).
(widget-backward &optional COUNT)
Cleanup
widget-delete
Delete widget and clean up.
(widget-delete WIDGET)
Common Patterns
Form with Validation
(defun my-registration-form ()
(interactive)
(let (username-widget email-widget)
(switch-to-buffer "*Registration*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Registration\n\n")
(setq username-widget
(widget-create 'editable-field
:format "Username: %v\n"
:size 25))
(widget-insert "\n")
(setq email-widget
(widget-create 'editable-field
:format "Email: %v\n"
:size 40))
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((user (widget-value username-widget))
(email (widget-value email-widget)))
(if (and (> (length user) 0)
(string-match-p "@" email))
(message "Registered: %s <%s>" user email)
(message "Invalid input"))))
"Register")
(use-local-map widget-keymap)
(widget-setup)))
Settings Interface
(defun my-settings ()
(interactive)
(let (theme-widget auto-save-widget)
(switch-to-buffer "*Settings*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Application Settings\n\n")
;; Theme selection
(widget-insert "Theme:\n")
(setq theme-widget
(widget-create 'radio-button-choice
:value my-current-theme
'(const :tag "Light" light)
'(const :tag "Dark" dark)
'(const :tag "Auto" auto)))
(widget-insert "\n")
;; Auto-save toggle
(setq auto-save-widget
(widget-create 'checkbox
:format "%[%v%] Enable auto-save\n"
my-auto-save-enabled))
(widget-insert "\n")
;; Save button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(setq my-current-theme
(widget-value theme-widget))
(setq my-auto-save-enabled
(widget-value auto-save-widget))
(message "Settings saved"))
"Save Settings")
(use-local-map widget-keymap)
(widget-setup)))
Dynamic Form
(defun my-dynamic-list ()
(interactive)
(let (items-widget)
(switch-to-buffer "*Dynamic List*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Todo List\n\n")
(setq items-widget
(widget-create 'editable-list
:format "%v%i\n"
:entry-format "%i %d %v\n"
:value '("Buy groceries" "Write code")
'(editable-field :size 40)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((items (widget-value items-widget)))
(message "Todo items: %S" items)))
"Show Items")
(use-local-map widget-keymap)
(widget-setup)))
Collecting Multiple Values
(defun my-collect-values (widgets)
"Collect values from multiple widgets into alist."
(mapcar (lambda (pair)
(cons (car pair)
(widget-value (cdr pair))))
widgets))
(defun my-form-with-collection ()
(interactive)
(let (name-w email-w age-w widgets)
(switch-to-buffer "*Form*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Information\n\n")
(setq name-w (widget-create 'editable-field
:format "Name: %v\n"))
(setq email-w (widget-create 'editable-field
:format "Email: %v\n"))
(setq age-w (widget-create 'editable-field
:format "Age: %v\n"))
(setq widgets `((name . ,name-w)
(email . ,email-w)
(age . ,age-w)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((data (my-collect-values widgets)))
(message "Data: %S" data)))
"Submit")
(use-local-map widget-keymap)
(widget-setup)))
Format Strings
Widget :format property controls display using escape sequences:
%v- Widget value%t- Tag%d- Documentation string%h- Help-echo%[- Button prefix%]- Button suffix%%- Literal %
;; Custom formats
(widget-create 'push-button
:format "Click %[here%] to continue\n"
:notify #'my-callback
"Continue")
(widget-create 'editable-field
:format "%t: %v (%h)\n"
:tag "Email"
:help-echo "user@domain.com")
Notifications and Callbacks
The :notify property specifies callback on widget change/activation.
Callback signature:
(lambda (widget &optional changed-widget &rest event)
;; widget - the widget with :notify property
;; changed-widget - widget that actually changed (for composite widgets)
;; event - interaction event
...)
Examples:
;; Simple notification
(widget-create 'checkbox
:notify (lambda (w &rest _)
(message "Checked: %s" (widget-value w)))
nil)
;; Access parent widget
(widget-create 'radio-button-choice
:notify (lambda (parent child &rest _)
(message "Parent value: %s" (widget-value parent))
(message "Child value: %s" (widget-value child)))
'(item "A")
'(item "B"))
;; Update other widgets
(let (field1 field2)
(setq field1
(widget-create 'editable-field
:notify (lambda (w &rest _)
(widget-value-set field2
(upcase (widget-value w)))
(widget-setup))))
(setq field2 (widget-create 'editable-field)))
Keybindings
widget-keymap provides default bindings:
TAB-widget-forward- Next widgetS-TAB/M-TAB-widget-backward- Previous widgetRET-widget-button-press- Activate buttonC-k-widget-kill-line- Kill to end of fieldM-TAB-widget-complete- Complete field (if supported)
Custom keymap:
(defvar my-widget-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map widget-keymap)
(define-key map (kbd "C-c C-c") 'my-submit)
(define-key map (kbd "C-c C-k") 'my-cancel)
map))
(use-local-map my-widget-mode-map)
Error Handling
Validation
(defun my-validate-email (widget)
(let ((value (widget-value widget)))
(unless (string-match-p "^[^@]+@[^@]+\\.[^@]+$" value)
(error "Invalid email format"))))
(widget-create 'editable-field
:format "Email: %v\n"
:notify (lambda (w &rest _)
(condition-case err
(my-validate-email w)
(error (message "Error: %s" (error-message-string err))))))
Safe Value Retrieval
(defun my-safe-widget-value (widget &optional default)
"Get widget value with fallback."
(condition-case nil
(widget-value widget)
(error default)))
Buffer Cleanup
(defun my-widget-cleanup ()
"Clean up widget buffer."
(interactive)
(when (eq major-mode 'my-widget-mode)
(remove-overlays)
(kill-buffer)))
;; With kill-buffer-hook
(defvar my-widget-mode-hook nil)
(add-hook 'my-widget-mode-hook
(lambda ()
(add-hook 'kill-buffer-hook
(lambda ()
(remove-overlays))
nil t)))
Defining Custom Widgets
(define-widget 'my-email-field 'editable-field
"Email input field with validation."
:format "Email: %v\n"
:size 40
:valid-regexp "^[^@]+@[^@]+\\.[^@]+$"
:notify (lambda (widget &rest ignore)
(let ((value (widget-value widget))
(regexp (widget-get widget :valid-regexp)))
(if (string-match-p regexp value)
(message "Valid email")
(message "Invalid email format")))))
;; Usage
(widget-create 'my-email-field)
Inheritance:
(define-widget 'my-readonly-field 'editable-field
"Read-only text field."
:keymap (let ((map (copy-keymap widget-field-keymap)))
(suppress-keymap map)
map))
Performance Considerations
Minimize redraws:
;; Bad: Multiple setups
(widget-value-set widget1 val1)
(widget-setup)
(widget-value-set widget2 val2)
(widget-setup)
;; Good: Batch updates
(widget-value-set widget1 val1)
(widget-value-set widget2 val2)
(widget-setup)
Use buffer-local variables:
(defvar-local my-widget-data nil
"Buffer-local widget storage.")
Avoid redundant notifications:
(defvar my-updating nil)
(widget-create 'editable-field
:notify (lambda (w &rest _)
(unless my-updating
(setq my-updating t)
(my-expensive-update)
(setq my-updating nil))))
Integration with Major Modes
(define-derived-mode my-widget-mode special-mode "MyWidget"
"Major mode for widget-based interface."
:group 'my-widget
(setq truncate-lines t)
(setq buffer-read-only nil))
(defun my-widget-interface ()
(interactive)
(switch-to-buffer "*My Interface*")
(my-widget-mode)
(erase-buffer)
;; Build interface
(widget-insert "My Application\n\n")
;; ... create widgets ...
(use-local-map widget-keymap)
(widget-setup)
(goto-char (point-min))
(widget-forward 1))
Debugging
;; Inspect widget properties
(pp-eval-expression '(widget-get my-widget :value))
;; Check widget type
(widget-type my-widget)
;; View all properties
(let ((widget my-widget))
(while widget
(prin1 (car widget))
(terpri)
(setq widget (cdr widget))))
;; Trace notifications
(widget-create 'checkbox
:notify (lambda (&rest args)
(message "Notify called with: %S" args)
(backtrace))
t)
Use Cases
Configuration Editor
Customize package using widgets instead of Custom interface:
(defun my-config-editor ()
(interactive)
(let (theme-w size-w)
(switch-to-buffer "*Config*")
(kill-all-local-variables)
(erase-buffer)
(widget-insert "Configuration\n\n")
(setq theme-w
(widget-create 'menu-choice
:tag "Theme"
:value my-theme
'(const light)
'(const dark)))
(setq size-w
(widget-create 'editable-field
:format "\nFont size: %v\n"
:value (number-to-string my-font-size)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest _)
(setq my-theme (widget-value theme-w))
(setq my-font-size
(string-to-number (widget-value size-w)))
(my-apply-config))
"Apply")
(use-local-map widget-keymap)
(widget-setup)))
Search/Filter Interface
(defun my-search-interface ()
(interactive)
(let (query-w type-w)
(switch-to-buffer "*Search*")
(kill-all-local-variables)
(erase-buffer)
(setq query-w
(widget-create 'editable-field
:format "Query: %v\n"
:size 50))
(setq type-w
(widget-create 'checklist
:format "\nTypes:\n%v\n"
'(const :tag "Functions" function)
'(const :tag "Variables" variable)
'(const :tag "Faces" face)))
(widget-create 'push-button
:notify (lambda (&rest _)
(my-perform-search
(widget-value query-w)
(widget-value type-w)))
"Search")
(use-local-map widget-keymap)
(widget-setup)))
Wizard/Multi-step Form
(defvar my-wizard-step 1)
(defvar my-wizard-data nil)
(defun my-wizard-next ()
(setq my-wizard-data
(plist-put my-wizard-data
(intern (format "step%d" my-wizard-step))
(my-collect-current-step)))
(setq my-wizard-step (1+ my-wizard-step))
(my-wizard-show))
(defun my-wizard-show ()
(erase-buffer)
(cond
((= my-wizard-step 1)
(my-wizard-step-1))
((= my-wizard-step 2)
(my-wizard-step-2))
(t
(my-wizard-finish)))
(widget-setup))
Common Pitfalls
Forgetting widget-setup:
;; Wrong
(widget-create 'editable-field)
;; User can't interact yet!
;; Correct
(widget-create 'editable-field)
(widget-setup)
Not calling widget-setup after value-set:
;; Wrong
(widget-value-set widget "new value")
;; Widget not updated!
;; Correct
(widget-value-set widget "new value")
(widget-setup)
Incorrect buffer setup:
;; Missing keymap
(erase-buffer)
(widget-create 'editable-field)
(widget-setup)
;; TAB won't navigate!
;; Correct
(erase-buffer)
(widget-create 'editable-field)
(use-local-map widget-keymap)
(widget-setup)
Not cleaning overlays:
;; Memory leak
(erase-buffer)
(widget-create ...)
;; Proper cleanup
(erase-buffer)
(remove-overlays)
(widget-create ...)
Resources
- Info manual:
C-h i m Widget RET - Source:
wid-edit.el,widget.elin Emacs distribution - Demo:
M-x widget-example(if available) - Tutorial: Official Emacs Widget Library manual