[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: Propose to add setup-wizard.el to ELPA
From: |
Philip Kaludercic |
Subject: |
Re: Propose to add setup-wizard.el to ELPA |
Date: |
Sun, 02 Jan 2022 12:02:11 +0000 |
I am not sure if I brought this up last time, but how difficult do you
think it would be to generalise this into a "wizard.el" package, that
could be used by any package to define an interactive configuration
wizard?
Yuan Fu <casouri@gmail.com> writes:
> A while ago I wrote a package that helps a new user to configure
> Emacs: it takes a user through some interactive pages, where changes
> takes effect immediately; and in the end it generates some code that
> can be copied to init.el.
>
> Demo for the original package: https://youtu.be/0qMskTAR2aw
>
> I made some improvements to that package and renamed it
> setup-wizard. Do you think we could add it to ELPA? Maybe the name is
> too “official”, in that case I can rename it to yuan’s-setup-wizard or
> something.
>
> I don’t know how useful could it be, since nowadays every body
> (understandably) starts with some community distribution rather than
> vanilla Emacs, but surely it is better than not having a wizard.
>
> You can try it out with emacs -q -l setup-wizard.el -f setup-wizard
>
> Yuan
>
> ;;; setup-wizard.el --- Setup wizard -*- lexical-binding: t; -*-
>
> ;; Copyright (C) 2019-2020 Free Software Foundation, Inc.
>
> ;; Author: Yuan Fu <casouri@gmail.com>
> ;; Maintainer: Yuan Fu <casouri@gmail.com>
> ;; URL: https://github.com/casouri/setup-wizard
> ;; Version: 1.0.0
> ;; Keywords: convenience
> ;; Package-Requires: ((emacs "26.0"))
>
> ;; This file is part of GNU Emacs.
>
> ;; GNU Emacs is free software: you can redistribute it and/or modify
> ;; it under the terms of the GNU General Public License as published by
> ;; the Free Software Foundation, either version 3 of the License, or
> ;; (at your option) any later version.
>
> ;; GNU Emacs is distributed in the hope that it will be useful,
> ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
> ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> ;; GNU General Public License for more details.
>
> ;; You should have received a copy of the GNU General Public License
> ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
>
> ;;; Commentary:
> ;;
> ;; This package provides a setup wizard that takes a user through an
> ;; interactive interface, in which he or she can configure key
> ;; bindings schemes, UI elements, Fonts, packages, etc.
>
> ;;; Code:
>
> (require 'widget)
> (require 'wid-edit)
> (require 'pcase)
> (require 'seq)
> (require 'cl-lib)
>
> ;;; Configs
>
> (defvar setup-wizard--config nil
> "An alist (OPTION . (FORM COMMENT)) of configurations.
> We use FORM and COMMENT to produce the final config.")
>
> (defun setup-wizard--save-option-and-eval
> (option form comment &optional additional)
> "Save OPTION FORM and COMMENT, and evaluate FORM.
> If ADDITIONAL is non-nil, eval that too."
> (when form
> (setf (alist-get option setup-wizard--config)
> (list form comment))
> (eval form))
> (when additional (eval additional)))
>
> ;;; Pages
>
> (defun setup-wizard--insert (&rest args)
> "Insert ARGS and replace emojis if they can’t be displayed."
> (widget-insert
> (mapconcat (lambda (text)
> (if (and (char-displayable-p ?🧙)
> (char-displayable-p ?🧚))
> text
> (string-replace
> "🧚" "Fairy"
> (string-replace
> "🧙" "Wizard" text))))
> args)))
>
> ;;;; Themes
>
> (defvar setup-wizard--c-demo
> " #include <stdlib.h>
>
> struct point
> {
> x: int;
> y: int;
> };
>
> int main(int arg, int* argv)
> {
> int x = -1;
> int y = 2;
> void *buf = malloc(sizeof(uin32_t));
> return add(x, y) - 3;
> }
> "
> "Demo C code.")
>
> (defun setup-wizard--theme-page ()
> "Theme configuration page."
> (setup-wizard--insert
> "🧚: Heya! You are here for help setting up your Emacs, right?
> Wizard will be here when you read to the next line.
>
> 🧙: Emacs comes with a couple of themes built-in, which are shown
> below. You can browse for more themes online or in the package
> manager.
>
> 🧚: Here are the built-in themes!
>
> Theme preview:\n\n")
> ;; Insert a C demo.
> (widget-insert
> (with-temp-buffer
> (insert setup-wizard--c-demo)
> (c-mode)
> (font-lock-fontify-region (point-min) (point-max))
> (buffer-string)))
> (widget-insert "\n")
> ;; Insert theme selection menu.
> (apply #'widget-create 'radio-button-choice
> :follow-link t
> :value "default"
> ;; Enable the theme when the user selects it.
> :notify (lambda (widget &rest _)
> (mapc #'disable-theme custom-enabled-themes)
> (let* ((theme (intern (widget-value widget)))
> (form (if (eq theme 'default)
> nil
> `(load-theme ',theme))))
> (setup-wizard--save-option-and-eval
> 'theme form (format "Load %s theme" theme))))
> (cons '(item "default")
> (cl-loop for theme in (custom-available-themes)
> collect `(item ,(symbol-name theme)))))
> (setup-wizard--insert "\n🧚: Want to ")
> (widget-create
> 'push-button
> :notify (lambda (&rest _)
> (package-refresh-contents)
> (list-packages t)
> (goto-char (point-min))
> (let ((inhibit-read-only t))
> (keep-lines "-theme")))
> :value "browse the package manager for themes")
> (widget-insert "?\n"))
>
> ;;;; Keybinding
>
> (defun setup-wizard--keybinding-page ()
> "Keybinding page."
> (setup-wizard--insert "🧙: This is the notation for modifiers in Emacs:
>
> C (control) Ctrl
> M (meta) Alt/Option
> s (super) Windows/Command
> S (shift) Shift
>
> 🧚: Which binding scheme do you like?\n\n")
> (widget-create 'radio-button-choice
> :follow-link t
> :value "default"
> :notify
> (lambda (widget &rest _)
> (setup-wizard--save-option-and-eval
> 'keybinding
> (cond
> ((equal (widget-value widget)
> "Alternative")
> '(cua-mode))
> ((equal (widget-value widget)
> "Wizard’s choice")
> `(progn
> (global-set-key (kbd "s-c") #'kill-ring-save)
> (global-set-key (kbd "s-x") #'kill-region)
> (global-set-key (kbd "s-v") #'yank))))
> "Set bindings for copy/cut/paste."))
> '(item :value "Default"
> :format "%v\n\n%d\n"
> :doc " M-w Copy
> C-w Cut
> C-y Paste")
> '(item :value "Alternative"
> :format "%v\n\n%d\n"
> :doc " C-c Copy
> C-x Cut
> C-v Paste")
> '(item :value "Wizard’s choice"
> :format "%v\n\n%d\n"
> :doc " s-c Copy
> s-x Cut
> s-v Paste"))
> (setup-wizard--insert
> "\n🧙: In the alternative binding scheme, the binding for copy
> and cut only take effect when some text is selected. So when
> nothing is selected, they are still normal prefix keys.\n"))
>
> ;;;; UI features
>
> (defun setup-wizard--ui-features-page ()
> "UI features page."
> (setup-wizard--insert "🧚: What UI elements do you like?\n\n")
> ;; Line numbers.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'line-number
> `(global-display-line-numbers-mode
> ,(if val 1 -1))
> (format "%s line number."
> (if val "Display" "Don’t display")))))
> :value nil)
> (widget-insert " Line numbers.\n")
> ;; Thin cursor.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'thin-cursor
> `(setq-default cursor-type
> ',(if val 'bar t))
> (format "Use %s cursor"
> (if val "thin" "default")))))
> :value nil)
> (widget-insert " Thin cursor bar.\n")
> ;; Blink cursor.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'blink-cursor
> `(blink-cursor-mode ,(if val 1 -1))
> (format "%s cursor"
> (if val "Blink" "Do not blink")))))
> :value blink-cursor-mode)
> (widget-insert " Blink cursor.\n")
> ;; Tool bar.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'tool-bar
> `(tool-bar-mode ,(if val 1 -1))
> (format "%s tool bar."
> (if val "Enable" "Disable")))))
> :value tool-bar-mode)
> (widget-insert " Tool bar.\n")
> ;; Menu bar.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'menu-bar
> `(menu-bar-mode ,(if val 1 -1))
> (format "%s menu bar."
> (if val "Enable" "Disable")))))
> :value menu-bar-mode)
> (widget-insert " Menu bar.\n")
> ;; Scroll bar.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'scroll-bar
> `(scroll-bar-mode ,(if val 1 -1))
> (format "%s scroll bar"
> (if val "Enable" "Disable")))))
> :value scroll-bar-mode)
> (widget-insert " Scroll bar.\n")
> ;; Font.
> (widget-insert "\n")
> (let* (default-font-field
> variable-font-field
> cjk-font-field
> size-field
> action
> (phrase "The quick brown fox jumps over the lazy dog.\n"))
> (widget-insert "Font preview:\n\n")
> (widget-insert " " phrase)
> (widget-insert " " (propertize phrase 'face 'variable-pitch))
> (widget-insert " 大漠孤烟直,长河落日圆。\n")
> (widget-insert " 射は仁の道なり。射は正しきを己に求む。\n\n")
> (setq default-font-field
> (widget-create 'editable-field
> :size 20
> :value ""
> :format "Default font: %v \n"))
> (setq variable-font-field
> (widget-create 'editable-field
> :size 20
> :value ""
> :format "Variable-pitch font: %v \n"))
> (setq cjk-font-field
> (widget-create 'editable-field
> :size 20
> :value ""
> :format "CJK font: %v \n"))
> (setq size-field
> (widget-create 'editable-field
> :size 2
> :value ""
> :format "Font size: %v \n\n"))
> (setq action
> (lambda (&rest _)
> (let* ((default-font
> (string-trim (widget-value default-font-field)))
> (variable-font
> (string-trim (widget-value variable-font-field)))
> (cjk-font
> (string-trim (widget-value cjk-font-field)))
> (size (string-to-number
> (string-trim
> (widget-value size-field)))))
> (unless (equal default-font "")
> (setup-wizard--save-option-and-eval
> 'font `(set-face-attribute
> 'default nil :family ,default-font)
> "Set default font."))
> (unless (equal variable-font "")
> (setup-wizard--save-option-and-eval
> 'variable-font
> `(set-face-attribute
> 'variable-pitch nil :family ,variable-font)
> "Set variable-pitch font."))
> (unless (equal cjk-font "")
> (setup-wizard--save-option-and-eval
> 'cjk-font
> `(dolist (charset '(kana han cjk-misc))
> (set-fontset-font
> t charset (font-spec :family ,cjk-font)))
> "Set CJK font."))
> (unless (eq size 0)
> (setup-wizard--save-option-and-eval
> 'font-size
> `(set-face-attribute 'default nil :height ,(* size 10))
> "Set font size.")))))
> (widget-create 'push-button
> :value "Apply font settings"
> :notify action)
> (widget-insert "\n")))
>
> (defun setup-wizard--undo-page ()
> "Undo page."
> (setup-wizard--insert
> "🧙: Emacs has a powerful (but probably unintuitive) undo system,
> where undo operations themselves are recorded in the undo
> history, and redo is done by undoing an previous undo operation.
>
> 🧚: Which undo system do you like?\n\n")
> (widget-create 'radio-button-choice
> :value "default"
> :follow-lint t
> :notify (lambda (widget &rest _)
> (when (equal (widget-value widget)
> "Linear")
> (setup-wizard--save-option-and-eval
> 'undo
> `(global-set-key [remap undo] #'undo-only)
> "Use linear undo style.")))
> '(item :value "Default"
> :format "%v\n\n%d\n"
> :doc " One undo rules them all")
> '(item :value "Linear"
> :format "%v\n\n%d\n"
> :doc " Undo and redo"))
> (let (undo-key redo-key)
> (widget-insert "\n")
> (setq undo-key
> (widget-create 'editable-field
> :size 5
> :value "C-/"
> :format "Bind undo to: %v "))
> (widget-create 'push-button
> :value "Apply"
> :notify
> (lambda (&rest _)
> (let ((key (string-trim (widget-value undo-key))))
> (setup-wizard--save-option-and-eval
> 'undo-key
> `(global-set-key (kbd ,key) #'undo)
> "Set binding for ‘undo’."))))
> (widget-insert "\n")
> (setq redo-key
> (widget-create 'editable-field
> :size 5
> :value "C-?"
> :format "Bind redo to: %v "))
> (widget-create 'push-button
> :value "Apply"
> :notify
> (lambda (&rest _)
> (let ((key (string-trim (widget-value redo-key))))
> (setup-wizard--save-option-and-eval
> 'undo-key
> `(global-set-key (kbd ,key) #'undo-redo)
> "Set binding for ‘undo-redo’."))))
> (setup-wizard--insert "\n\n🧙: I bind redo to C-.\n")))
>
> ;;;; Extra package
>
> (defun setup-wizard--package-activate (package mode)
> "Return a form that activates PACKAGE and enable MODE."
> `(progn
> (require 'package)
> (unless (package-installed-p ',package)
> (package-install ',package))
> (package-activate 'ivy)
> (require ',package)
> (,mode)))
>
> (defun setup-wizard--package-page ()
> "Extra package page."
> (setup-wizard--insert
> "🧙: Here are some packages that I always install:\n\n")
> ;; Ivy.
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'ivy
> (when val
> `(progn
> ,(setup-wizard--package-activate
> 'ivy 'ivy-mode)
> (setq enable-recursive-minibuffers t
> ivy-use-selectable-prompt t
> ivy-use-virtual-buffers t)
> ,(setup-wizard--package-activate
> 'counsel 'counsel-mode)))
> "Install and enable ‘ivy-mode’ and ‘counsel-mode’."
> `(progn
> (ivy-mode ,(if val 1 -1))
> (counsel-mode ,(if val 1 -1))))))
> :value nil)
> (widget-insert
> " Ivy: A completion package that makes typing file names, buffer
> names, commands, etc so much easier.\n")
> ;; Company
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'company
> (setup-wizard--package-activate
> 'company 'company-mode)
> "Install and enable ‘company-mode’."
> `(company-mode ,(if val 1 -1)))))
> :value nil)
> (widget-insert
> " Company: Popup completion menu when writing programs.\n")
> (widget-create 'checkbox
> :notify
> (lambda (widget &rest _)
> (let ((val (widget-value widget)))
> (setup-wizard--save-option-and-eval
> 'electric-pair-mode
> (when val
> `(electric-pair-mode))
> "Enable ‘electric-pair-mode’."
> `(electric-pair-mode ,(if val 1 -1)))))
> :value nil)
> (widget-insert
> " Electric-pair-mode (built-in): Automatically closes parenthesis\n")
> (setup-wizard--insert "\n🧙: ...\n\n")
> (setup-wizard--insert "🧙: I don’t use many packages.\n"))
>
> ;;; Guide
>
> (defun setup-wizard--with-boilerplate
> (setup-fn &optional page-list finish-fn)
> "Call page setup function SETUP-FN with widget boilerplate.
> PAGE-LIST is a list of setup function for pages to show in a
> series. FINISH-FN is called when user clicks the finish button.
> If PAGE-LIST or FINISH-FN are nil, don’t insert navigation
> buttons."
> (kill-all-local-variables)
> (let ((inhibit-read-only t))
> (erase-buffer))
> (remove-overlays)
> (funcall setup-fn)
> (widget-insert "\n")
> (when (and page-list finish-fn)
> (setup-wizard--insert-step-buttons setup-fn page-list finish-fn))
> (use-local-map widget-keymap)
> (widget-setup)
> (goto-char (point-min))
> (local-set-key (kbd "q") #'setup-wizard--quit))
>
> (defun setup-wizard--quit (&rest _)
> "Quite the wizard."
> (interactive)
> (kill-buffer)
> (message (with-temp-buffer
> (setup-wizard--insert "🧚: See ya!")
> (buffer-string))))
>
> (defun setup-wizard--insert-step-buttons (page page-list finish-fn)
> "Insert buttons that go to previous and next page of PAGE.
> PAGE-LIST is a list of setup function for pages to show in a series.
> Insert a Button that calls FINISH-FN at the last page."
> (let* ((idx (seq-position page-list page))
> (previous-page (if (eq idx 0) nil
> (nth (1- idx) page-list)))
> (next-page (nth (1+ idx) page-list)))
> (setup-wizard--insert
> (format "🧚: We are at step %s/%s, what’s next? "
> (1+ idx) (length page-list)))
> (when previous-page
> (widget-create
> 'push-button
> :notify (lambda (&rest _)
> (setup-wizard--with-boilerplate
> previous-page page-list finish-fn))
> :value "Back"))
> (widget-insert " ")
> (if next-page
> (widget-create
> 'push-button
> :notify (lambda (&rest _)
> (setup-wizard--with-boilerplate
> next-page page-list finish-fn))
> :value "Next")
> (widget-create
> 'push-button
> :notify (lambda (&rest _) (funcall finish-fn))
> :value "Finish"))
> (widget-insert " ")
> (widget-create
> 'push-button
> :value "Quit"
> :notify #'setup-wizard--quit)
> (widget-insert "\n")))
>
> (defun setup-wizard--insert-config ()
> "Insert configuration in ‘setup-wizard--config’ line-by-line."
> (dolist (config (reverse setup-wizard--config))
> (insert ";; " (nth 2 config) "\n")
> (dolist (conf (if (eq (car (nth 1 config)) 'progn)
> (cdr (nth 1 config))
> (list (nth 1 config))))
> (insert (prin1-to-string conf) "\n"))))
>
> (defun setup-wizard--finish ()
> "The default finish function.
> Constructs the config and display them."
> (setup-wizard--with-boilerplate
> (lambda ()
> (setup-wizard--insert
> "🧚: Here is your configuration! Do you want me to append it to
> init.el for you? ")
> (widget-create 'push-button
> :notify
> (lambda (&rest _)
> (let ((init-file (locate-user-emacs-file
> "init.el" ".emacs")))
> (find-file init-file)
> (goto-char (point-max))
> (insert "\n")
> (setup-wizard--insert-config)))
> :value "Append to init.el")
> (widget-insert "\n\n")
> (widget-insert
> (with-temp-buffer
> (setup-wizard--insert-config)
> (emacs-lisp-mode)
> (font-lock-fontify-region (point-min) (point-max))
> (buffer-string))))))
>
> (defun setup-wizard ()
> "Run the setup wizard."
> (interactive)
> (switch-to-buffer (get-buffer-create "*mage tower*"))
> (setq setup-wizard--config nil)
> (let ((page-list '(setup-wizard--theme-page
> setup-wizard--keybinding-page
> setup-wizard--ui-features-page
> setup-wizard--undo-page
> setup-wizard--package-page)))
> (setup-wizard--with-boilerplate
> (car page-list) page-list
> #'setup-wizard--finish)))
>
> ;;; Backport
>
> (unless (fboundp 'undo--last-change-was-undo-p)
> (defun undo--last-change-was-undo-p (undo-list)
> (while (and (consp undo-list) (eq (car undo-list) nil))
> (setq undo-list (cdr undo-list)))
> (gethash undo-list undo-equiv-table)))
>
> (unless (fboundp 'undo-redo)
> (defun undo-redo (&optional arg)
> "Undo the last ARG undos."
> (interactive "*p")
> (cond
> ((not (undo--last-change-was-undo-p buffer-undo-list))
> (user-error "No undo to undo"))
> (t
> (let* ((ul buffer-undo-list)
> (new-ul
> (let ((undo-in-progress t))
> (while (and (consp ul) (eq (car ul) nil))
> (setq ul (cdr ul)))
> (primitive-undo arg ul)))
> (new-pul (undo--last-change-was-undo-p new-ul)))
> (message "Redo%s" (if undo-in-region " in region" ""))
> (setq this-command 'undo)
> (setq pending-undo-list new-pul)
> (setq buffer-undo-list new-ul))))))
>
> (unless (fboundp 'undo-only)
> (defun undo-only (&optional arg)
> "Undo some previous changes.
> Repeat this command to undo more changes.
> A numeric ARG serves as a repeat count.
> Contrary to `undo', this will not redo a previous undo."
> (interactive "*p")
> (let ((undo-no-redo t)) (undo arg))))
>
> (provide 'setup-wizard)
>
> ;;; setup-wizard.el ends here
>
--
Philip Kaludercic
- Re: Propose to add setup-wizard.el to ELPA, (continued)
- Re: Propose to add setup-wizard.el to ELPA, Stefan Kangas, 2022/01/02
- Re: Propose to add setup-wizard.el to ELPA, Joost Kremers, 2022/01/03
- Re: Propose to add setup-wizard.el to ELPA, Eli Zaretskii, 2022/01/03
- Re: Propose to add setup-wizard.el to ELPA, Po Lu, 2022/01/03
- Re: Propose to add setup-wizard.el to ELPA, Joost Kremers, 2022/01/03
Re: Propose to add setup-wizard.el to ELPA, Po Lu, 2022/01/02
Re: Propose to add setup-wizard.el to ELPA,
Philip Kaludercic <=
Re: Propose to add setup-wizard.el to ELPA, Jean Louis, 2022/01/07
Re: Propose to add setup-wizard.el to ELPA, Simon Pugnet, 2022/01/02
Re: Propose to add setup-wizard.el to ELPA, Simon Pugnet, 2022/01/02