reddit / emacs

My own Transient example

vjgoh · 2022-05-24 21:23
After asking some questions and reading docs and getting some help, I have my own example of how to build a Transient. I honestly like this a lot better for this kind of work than Hydra; Hydra is fantastic, but this saves me a lot of duplicate work that I was doing with defining keymaps to assemble the right combination of arguments to pass to build functions. My usage of lisp is what I'd consider fairly simple. I'm just trying to build a command line to feed to `compile`. ;; -*- lexical-binding: t; -*- ;; Functions for building unreal on PROJ using dir-locals (defconst +proj-target-list+ '("Development" "Debug" "Release")) (defconst +proj-executable-list+ '("UnrealHeadertool" "ProjEditor" "ProjGame")) (defconst +proj-platform-list+ '("Win64")) (defconst +uproject-path+ "Proj/Proj.uproject") (defconst +batch-builder+ "Engine/Build/BatchFiles/Build.bat") (defconst +genproj-bat+ "GenerateProjectFiles.bat") (defun nop () (interactive) ) ;; For future queue work, args can be stuffed into a list and executed later. (transient-define-suffix proj-transient-build-unreal (args) "Build Unreal" :description "Build using current overrides" (interactive (list (transient-args transient-current-command))) (when-let ((build-path (getvar base-path))) (let* ((exe (transient-arg-value "--exe=" args)) (target (transient-arg-value "--target=" args)) (platform (transient-arg-value "--platform=" args)) (debug (s-present? (transient-arg-value "--debug" args))) (command (concat build-path +batch-builder+ " " exe " " platform " " target))) (setq args (cl-remove "--exe=" args :test #'s-contains?)) (setq args (cl-remove "--target=" args :test #'s-contains?)) (setq args (cl-remove "--platform=" args :test #'s-contains?)) (when (s-contains? "Editor" exe) (setq command (concat command " \"" build-path +uproject-path+ "\""))) (setq command (concat command " " (mapconcat #'identity args " "))) (if debug (message command) (compile command))))) (transient-define-suffix proj-transient-genproj (args) "Generate projects" :description "Generate the project files" (interactive (list (transient-args transient-current-command))) (setq args (cl-remove "--exe=" args :test #'s-contains?)) (setq args (cl-remove "--target=" args :test #'s-contains?)) (setq args (cl-remove "--platform=" args :test #'s-contains?)) (when-let ((build-path (getvar base-path))) (if (s-present? (transient-arg-value "--debug" args)) (message (concat build-path +genproj-bat+ " " (mapconcat #'identity args " "))) (compile (concat build-path +genproj-bat+ " " (mapconcat #'identity args " ")))))) ;; Define a transient to set all the arguments. (transient-define-prefix proj-transient-build () "A transient to construct build strings and execute them." ["Build Configuration" ["Current Project Environment" ("P" proj-set-project-override) ("//" nop :description (lambda () (format "Base Dir: \"%s\"" (getvar base-path))) :transient t) ("--" nop :description (lambda () (format "UProject Path: \"%s\"" +uproject-path+)) :transient t) ("@@" nop :description (lambda () (format "Batch Builder: \"%s\"" +batch-builder+)) :transient t) ("++" nop :description (lambda () (format "Genproj bat: \"%s\"" +genproj-bat+)) :transient t) ("%%" nop :description (lambda () (format "Search Instance: \"%s\"" (getvar helm-locate-instance))) :transient t)] ["Build args" ("-p" "Show Progress" "-progress") ("-N" "No Hot Reload" "-NoHotreloadfromide") ("-D" "Preview Build" "--debug")] ["Targets" ("e" "Set Executable" "--exe=" :always-read t :choices ,+proj-executable-list+) ("t" "Set Target" "--target=" :always-read t :choices ,+proj-target-list+) ("p" "Set Platform" "--platform=" :always-read t :choices ,+proj-platform-list+)]] ["Execute Commands" ("g" "Generate Projects" proj-transient-genproj) ("B" "Build Project" proj-transient-build-unreal)]) Some notes: * `getvar` is a macro I've written that returns the value of a variable that may or may not be set in a dir-locals.el file, and may furthermore be overridden. I need it because I often have to work in multiple different projects, but I'm still only compiling ONE SPECIFIC project, no matter what base directory I happen to be in. * `nop` is there so I can have entries that are purely informative. If you type out the keys corresponding to the option, it just exits and does nothing. It's a hack, since Transient doesn't like entries that are only descriptions. Still, it works just fine. * Unless you really need to for some other reason, try to work within these transient arguments. That is, unless you're trying to set a lisp variable for some other specific purpose, don't do what I did and write infixes that set lisp variables that you later access to do the compilation. It works just fine, but it's honestly more cumbersome and takes up more space. If your use case is like mine here--actually building a command to execute--the full-on Transient way ends up being cleaner.
soundslogical · 2022-05-25 06:18
I really like this idea! I'm still typing `compile` commands by hand for my project at work like a caveman. And I've never written a transient. This seems like a great reason to give it a try.
divinedominion · 2022-05-25 07:53
Never defined my own Transient, now I am curious. Thanks! What do you do with the result of your code? Do you bind a key to call `proj-transient-build` or something?
holgerschurig · 2022-05-25 08:36
Compile commands can have their own completion list. And I wrote some half-baked elisp so that my-compile-command is able to parse compile commands out of my files. So e.g. in an ORG document I have: ;; @compile; (org-html-export-to-html) and in some C project just: // @compile: make and in some C++/Qt project using Meson it is: // @compile: ninja -C build https://github.com/holgerschurig/emacs-doom-config/blob/master/my-compile.el
vjgoh · 2022-05-25 09:28
Yes, exactly. I could also use M-x, of course, but I happen to have it bound to M-m. I forgot to put that here because all my key bindings go in their own file.
divinedominion · 2022-05-25 10:06
Could you share an example? 🙏
vjgoh · 2022-05-25 14:59
I mean, this is literally all I've done: `(global-set-key "\M-m" 'proj-transient-build)` The nice thing about Transients and Hydras is how much keybinding work they do for you. All you need is something to invoke them and suddenly you've got a really excellent text menu system.
vjgoh · 2022-05-25 15:09
I forgot to include the project override stuff. I really do need to manipulate a lisp variable here, because I use this path throughout my libraries. Setting `:always-read` to `nil` means that it clears the variable if it's set, and tries to set if it's nil. In this case, I find that more useful than doing the work inside the reader to clear the variable. I'm far more likely to need to toggle between a path and nil than two paths. (defun proj-project-override-reader (prompt _initial-input _history) "Helper function to select from the cached base paths, or to clear the value." (completing-read prompt project--cached-base-paths)) (transient-define-infix proj-set-override (override-path) "Choose target executable" :transient t :always-read nil :description "Current Override Path:" :reader 'proj-project-override-reader :class 'transient-lisp-variable :variable 'project--path-override)
divinedominion · 2022-05-27 05:57
Oh wow, didn't realize that! Thanks!