Compare commits

..

4 commits

Author SHA1 Message Date
c548b6d530 Publish “Switch TODO state when clocking in” 2023-07-09 23:10:48 -07:00
63a15cddfd Rearrange and update “Switch TODO state when clocking in” 2023-07-09 23:07:37 -07:00
7811fd5198 Add first draft of “Switch TODO state when clocking in” 2023-06-28 23:09:56 -07:00
7bdefe9504 Add posts repository to website project
This doesn't belong with the usual publish actions, so exclude posts from being published.
2023-06-28 23:07:41 -07:00
4 changed files with 109 additions and 1 deletions

1
posts/README.org Normal file
View file

@ -0,0 +1 @@
These posts are not a normal part of the website project. They are not published by the org publish functions, they use [[https://git.sr.ht/~ryuslash/tekuti-el][tekuti-el]] to post to my blog at https://blog.ryuslash.org.

View file

@ -0,0 +1,26 @@
#+TITLE: IELM & Paredit
#+TAGS: emacs
#+COMMENT_STATUS: closed
#+STATUS: publish
#+DATE: Wed, 12 Apr 2023 07:02:45 GMT
#+UPDATE_URL: /admin/modify-post/2023%252f04%252f12%252fielm-paredit
For a while I've been bothered by being unable to use IELM to evaluate Emacs Lisp expressions. Then [[https://www.n16f.net/blog/making-ielm-more-comfortable/][Making IELM More Comfortable]] pointed out that using paredit in IELM causes the =RET= and =C-j= keybindings to be bound to the wrong functions and proposes to fix them. The given fix, however, appears to change the keybindings for all buffers using paredit. I don't want that, and Emacs can do better!
A quick search yielded [[https://stackoverflow.com/a/13102821][Buffer-locally overriding minor-mode key bindings in Emacs]] suggesting a fix. My adaptation of that solution:
#+begin_src emacs-lisp
(defun oni-elisp-ielm-remove-paredit-newline-keys ()
"Disable C-j and RET keybindings from paredit-mode."
(let ((oldmap (map-elt minor-mode-map-alist 'paredit-mode))
(newmap (make-sparse-keymap)))
(set-keymap-parent newmap oldmap)
(define-key newmap (kbd "RET") nil)
(define-key newmap (kbd "C-j") nil)
(make-local-variable 'minor-mode-overriding-map-alist)
(push `(paredit-mode . ,newmap) minor-mode-overriding-map-alist)))
(add-hook 'ielm-mode-hook #'oni-elisp-ielm-remove-paredit-newline-keys)
#+end_src
Defining =RET= and =C-j= as =nil= means that the keybinding defaults back to the major-mode keybinding.

View file

@ -0,0 +1,81 @@
#+title: Switch TODO state when clocking in
#+options: toc:nil num:nil
#+HTML_EMBED_SVG: t
#+tags: emacs, org-mode, config
#+comment_status: closed
#+status: publish
#+UPDATE_URL: /admin/modify-post/2023%252f07%252f10%252fswitch-todo-state-when-clocking-in
#+DATE: Mon, 10 Jul 2023 05:56:39 GMT
This Emacs configuration snippet for org-mode changes a task's state from whatever “head” state it's in into the next state in its sequence when you clock in to a task. I do this by setting the =org-clock-in-switch-to-state= variable.
* Different sequences of TODO states
First, just to make sure that this is explained in here, it's possible in org-mode to specify multiple sequences of task states, for example I had this in one of my org files:
#+begin_src org
,#+SEQ_TODO: TODO WIP BLOCKED | DONE
,#+SEQ_TODO: READ READING | FINISHED STOPPED
,#+SEQ_TODO: WATCH WATCHING | WATCHED
,#+SEQ_TODO: LISTEN LISTENING | DONE
#+end_src
This means that there are 4 sequences I've set up. A task can start as either =TODO=, =READ=, =WATCH=, or =LISTEN=, and then it'll move to a different next state[fn:1] depending on which initial state was picked. =WIP= comes after =TODO=, =WATCHING= after =WATCH=, etc. They generally don't cross, although org-mode will get confused as soon as I change any =TODO= or =LISTEN= task to =DONE= since at that point it can't figure out what it would change back to if it turns out I wasn't done after all. It'll make it =TODO= if I move forward from =DONE= in either case.
Here is the graph showing the paths of each sequence:
#+begin_src dot :file ../assets/20230628161532-switch_todo_state_when_clocking_in-state_sequences.svg :exports results
digraph {
rankdir=LR;
TODO -> WIP -> BLOCKED -> DONE -> TODO
READ -> READING -> FINISHED -> STOPPED -> READ
WATCH -> WATCHING -> WATCHED -> WATCH
LISTEN -> LISTENING -> DONE
}
#+end_src
#+RESULTS:
[[file:../assets/20230628161532-switch_todo_state_when_clocking_in-state_sequences.svg]]
Although this doesn't actually affect me at all in any way because I have =org-use-fast-todo-selection= set to =t=.
* Making The Switch
Getting back to my snippet: =org-clock-in-switch-to-state= can be set to either a string, which will just always change it to that particular state when you clock in, or a function that takes a single parameter (the current state of the task you're clocking in to). For this case I want the function, because I won't know which state I want to change to until I know the current state, since =TODO= will change to =WIP=, =READ= to =READING=, etc. but also when a task is already in the state =READING=, for example, I don't want it to change at all.
#+begin_src elisp
(defun oni-org-maybe-change-todo-state (current-state)
"Change the state of the current task to its next state.
Only do this outside of a capture buffer and when CURRENT-STATE
is the head state of whichever sequence of states applies to this
task."
(if (and (not org-capture-mode)
(member current-state org-todo-heads))
(cadr (member current-state org-todo-keywords-1))
current-state))
#+end_src
First I make sure that we're not in a capture buffer. Some of my capture templates state that they should clock in to whatever I'm capturing right away, and in that case the task I'm capturing might immediately change from =TODO= to =WIP=, for example.
Then I check to see if the current state is in the =org-todo-heads= variable, which contains only the first todo state of each possible sequence of states. Let's assume my todo states are:
#+begin_src org
,#+SEQ_TODO: TODO WIP BLOCKED | DONE
#+end_src
Checking that the =current-state= is in =org-todo-heads= basically means I check to make sure that =current-state= is =TODO= and not any of the other ones. I do this so that if I clock in to a =WIP= task, it doesn't automatically switch to blocked.
If I'm not in a capture buffer, and the current state is one of the head ones, I search for the current state in the =org-todo-keywords-1= which is a simple flat list of all the possible todo states org-mode knows about. This is easier to work with than =org-todo-keywords=, since that is an alist of ~(type . list-of-states)~ and has a bunch of information I don't need. I return whatever comes right after the current state.
Returning whatever next state is in the list does mean that if the next state is =DONE=, it'll immediately set it to done. But there is no real way to check that with the way I've done this. There is just the next state.
Finally you just set this function as the value of =org-clock-in-switch-to-state= and then you're good to go.
#+begin_src elisp
(setq org-clock-in-switch-to-state #'oni-org-maybe-change-todo-state)
#+end_src
* Footnotes
[fn:1] By which I mean by pressing =C-c C-t= when =org-use-fast-todo-selection= is =nil= or pressing =C-S-<right>= on the headline.

View file

@ -101,7 +101,7 @@
:base-extension "svg\\|png\\|jpg" :base-extension "svg\\|png\\|jpg"
:publishing-function org-publish-attachment :publishing-function org-publish-attachment
:publishing-directory "public/") :publishing-directory "public/")
("all" :components ("index" "posts" "assets")))) ("all" :components ("index" "assets"))))
(defvar publish-feed-url-format (defvar publish-feed-url-format
;"https://gitlab.com/ryuslash/ryuslash.org/-/commits/master/%s?feed_token=Rf8otgpS8YEiYakJN4NR&format=atom" ;"https://gitlab.com/ryuslash/ryuslash.org/-/commits/master/%s?feed_token=Rf8otgpS8YEiYakJN4NR&format=atom"