Skip to content

Latest commit

 

History

History
973 lines (763 loc) · 27.8 KB

wal-org.org

File metadata and controls

973 lines (763 loc) · 27.8 KB

Org Mode

The best thing about Emacs. Check out the manual or run org-info.

Header

;;; wal-org.el --- Org. -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Provide org packages/configurations.

;;; Code:

(eval-when-compile
  (require 'wal-useful nil t)
  (require 'wal-package nil t)
  (require 'wal-key-bindings nil t)
  (require 'wal-bridge nil t))

(declare-function consult-org-agenda "wal-complete.el")
(declare-function mixed-pitch-mode "ext:mixed-pitch.el")
(declare-function org-roam-buffer-p "ext:org-roam.el")
(declare-function org-archive-subtree "ext:org-archive.el")
(declare-function project-prompt-project-dir "ext:project.el")
(declare-function project-root "ext:project.el")
(declare-function visual-fill-column-mode "ext:visual-fill-column.el")
(declare-function wal-advise-many "wal-useful.el")
(declare-function wal-append "wal-useful.el")
(declare-function wal-project-local-value "wal-workspace.el")
(declare-function wal-replace-in-alist "wal-fun.el")

(defvar org-agenda-window-setup)
(defvar org-archive-default-command)
(defvar org-clock-current-task)
(defvar org-log-note-marker)
(defvar org-roam-dailies-capture-templates)
(defvar org-roam-dailies-directory)
(defvar org-roam-directory)
(defvar org-super-agenda-groups)
(defvar partial-recall-meaningful-traits)
(defvar project-current-directory-override)
(defvar text-scale-mode-step)

;;;; Customization:

(defgroup wal-org nil
  "Change settings used for org packages."
  :group 'wal
  :tag "Org")

(defcustom wal-org-agenda-file ".agenda"
  "The index of the agenda.
This variable is relative to the `org-directory'."
  :group 'wal-org
  :type 'string)

Packages

(junk-expand org
  "Additional Org-related packages."
  :extras (org-habit-stats))

org

If not the reason why you came to Emacs, probably the one that makes you stay. Org is a structured plain-text format that can be manipulated to work for document authoring, task-planning and more. Read the manual in Emacs or on the web.

(defun wal-first-require-ox-md (&rest _args)
  "Advise to require `ox-md' before export dispatch."
  (require 'ox-md nil t))

(defun wal-org-hide-emphasis-markers (&optional show)
  "Hide emphasis markers.

If SHOW is t, show them instead."
  (interactive "P")

  (defvar org-hide-emphasis-markers)

  (setq org-hide-emphasis-markers (not show))
  (font-lock-flush))

(defvar wal-org--unsaved nil)

(defun wal-org--first-record-buffer (&rest _)
  "Record the buffer from the log marker."
  (and-let* (((boundp 'org-log-note-marker))
             (marker org-log-note-marker)
             (buffer (marker-buffer marker)))

    (setq wal-org--unsaved buffer)))

(defun wal-org--then-save-unsaved-buffer (&rest _)
  "Save the buffer for which a note has been taken."
  (when wal-org--unsaved

    (with-current-buffer wal-org--unsaved
      (save-buffer))

    (setq wal-org--unsaved nil)))

(use-package org
  :commands (org-add-note org-find-exact-heading-in-directory)

  :init
  (harpoon org-mode
    :checker disabled
    :messages ("Organize! Seize the means of production!")
    :bind t)

  :config
  ;; TEMP: Getting missing face errors otherwise.
  (require 'org-indent)

  ;; Require `ox-md' before calling dispatch as it might not be loaded.
  (advice-add
   'org-export-dispatch :before
   #'wal-first-require-ox-md)

  (advice-add
   'org-add-log-setup :after
   #'wal-org--first-record-buffer)

  (advice-add
   'org-store-log-note :after
   #'wal-org--then-save-unsaved-buffer)

  ;; Create register file if it doesn't yet exist
  (let ((register (expand-file-name wal-org-agenda-file org-directory)))

    (unless (file-exists-p register)
      (make-empty-file register)))

  (with-no-warnings
    (wal-transient-define-major org-mode ()
      "Access `org-mode' commands."
      [["Edit"
        ("w" "cut subtree" org-cut-subtree
         :inapt-if-not org-at-heading-p)
        ("y" "paste subtree" org-paste-subtree)
        ("n" "add note" org-add-note)
        ("." "toggle timestamp" org-toggle-timestamp-type
         :inapt-if-not (lambda () (org-at-timestamp-p 'inactive)))
        ("s" "sort" org-sort
         :inapt-if-not (lambda () (or (org-at-item-p) (org-at-heading-p))))]

       ["Footnotes"
        ("f" "add footnote" org-footnote-new
         :inapt-if org-in-src-block-p)
        ("n" "normalize footnotes" org-footnote-normalize)]]

      [["Visibility"
        ("c" "show content" org-content)
        ("a" "show all" org-fold-show-all)
        ("i" "toggle indentation" org-indent-mode)
        ("v" "visual line" visual-line-mode)
        ("m" "hide emphasis markers" wal-org-hide-emphasis-markers)]

       ["Help"
        ("h" "node info" org-info-find-node)]]))

  :custom
  ;; Make it look nice and tidy.
  (org-adapt-indentation nil)
  (org-ellipsis "")
  (org-startup-with-inline-images t)
  (org-startup-folded 'nofold)
  (org-cycle-separator-lines 1)

  ;; Logging.
  (org-log-done 'time)
  (org-log-into-drawer t)

  ;; Set up directories.
  (org-default-notes-file (expand-file-name "notes.org" org-directory))
  (org-agenda-files (expand-file-name wal-org-agenda-file org-directory))

  ;; Be sure to add archive tag with `org-toggle-archive-tag'.
  (org-archive-location "::* Archived")

  ;; Adapt keywords, tags and speed commands.
  (org-todo-keywords
   '((sequence "TODO(t)" "IN PROGRESS(p)" "WAITING(w@/!)" "BLOCKED(b@/@)" "|" "DONE(d)" "CANCELED(c@/!)")))
  (org-tag-persistent-alist
   '((:startgroup)
     ("depth")
     (:grouptags)
     ("@immersive")
     ("@process")
     (:endgroup)

     (:startgroup)
     ("context")
     (:grouptags)
     ("@work")
     ("@home")
     ("@away")
     (:endgroup)

     (:startgroup)
     ("characteristic")
     (:grouptags)
     ("@unbillable")
     ("@repeated")
     ("@intermittent")
     (:endgroup)

     (:startgroup)
     ("energy")
     (:grouptags)
     ("@easy")
     ("@average")
     ("@challenge")
     (:endgroup)

     (:startgroup)
     ("category")
     (:grouptags)
     ("@development")
     ("@talk")
     ("@contribution")
     ("@wellbeing")
     ("@education")
     ("@chore")
     (:endgroup)))

  ;; Use group energy to identify projects.
  (org-stuck-projects '("+energy/-ARCHIVE" ("TODO" "IN PROGRESS") nil ""))

  ;; Show archived items.
  (org-sparse-tree-open-archived-trees t)

  ;; Enforce dependencies.
  (org-enforce-todo-checkbox-dependencies t)
  (org-enforce-todo-dependencies t)

  :bind
  (:map org-mode-map
   ("M-p" . org-previous-visible-heading)
   ("M-n" . org-next-visible-heading)))

org-habit-stats

(use-package org-habit-stats
  :defer 3
  :after org-agenda

  :config
  (transient-append-suffix 'org-mode-major '(1 -1)
    '["Habits"
      ("S" "stats" org-habit-stats-view-habit-at-point
       :inapt-if-not (lambda () (org-is-habit-p (point))))]))

org-agenda

Plan your day, week and year. This adapts the agenda view to show what I need day-to-day and adds a consult buffer source.

(defun wal-agenda-buffer-p (buffer)
  "Check if BUFFER contributes to the agenda."
  (declare-function org-agenda-file-p "ext:org.el")

  (org-agenda-file-p (buffer-file-name buffer)))

(defun wal-org-agenda--then-rename-tab (&rest _)
  "Rename the tab if we set up the window using tabs."
  (when (eq org-agenda-window-setup 'other-tab)
    (tab-bar-rename-tab "agenda")))

(use-package org-agenda
  :config
  (with-eval-after-load 'partial-recall
    (parallel-mirror wal-agenda-buffer-p :type boolean)
    (put 'parallel-mirror-wal-agenda-buffer-p 'partial-recall-non-meaningful-explainer "Agenda buffer")
    (add-to-list 'partial-recall-meaningful-traits 'parallel-mirror-wal-agenda-buffer-p))

  (wal-replace-in-alist 'org-agenda-prefix-format '((agenda . "  %?-12t%?-12c%? s%?b")))

  (advice-add
   'org-agenda-prepare-window :after
   #'wal-org-agenda--then-rename-tab)

  :custom
  (org-agenda-hide-tags-regexp "^@")
  (org-agenda-span 'day)
  (org-agenda-window-setup 'other-tab)
  (org-agenda-time-leading-zero t)
  (org-agenda-log-mode-items '(clock))
  (org-agenda-start-with-clockreport-mode t)
  (org-agenda-start-with-log-mode t)
  (org-agenda-clockreport-parameter-plist
   '(:link t
     :maxlevel 3
     :fileskip0 t
     :emphasize t
     :match "-@unbillable"))

  :bind
  (("C-c a" . org-agenda)
   :map org-agenda-mode-map
   ("C-c C-t" . org-agenda-todo-yesterday)
   ("<RET>" . org-agenda-goto)
   ("M-<RET>" . org-agenda-switch-to)))

org-habit

Habits are a special kind of todo to keep track of what you keep doing/forgetting to do.

(use-package org-habit
  :custom
  (org-habit-show-habits-only-for-today nil)
  (org-habit-graph-column 70))

org-super-agenda

Allows for nicer grouping in the agenda view. The groups relate to custom groups and todo keywords.

(defvar wal-org-super-agenda-groups
  '(;; (Re-)Schedule.
    (:name "Schedule" :time-grid t :order 2)
    (:name "Any time" :and (:scheduled today :not (:habit t)) :order 1)
    (:name "Leftovers"
           :and (:scheduled past
                            :todo t
                            :not (:habit t))
           :order 3)

    ;; Items with deadlines.
    (:name "Upcoming" :and (:scheduled nil :deadline future) :order 4)
    (:name "Catch up" :deadline past :order 6)
    (:name "Achieved" :and (:deadline today :todo "DONE") :order 8)
    (:name "Don't forget" :and (:scheduled nil :deadline today) :order 0)

    ;; Habits.
    (:order-multi (5 (:name "Education" :and (:habit t :tag "@education"))
                     (:name "Contribution" :and (:habit t :tag "@contribution"))
                     (:name "Well-being" :and (:habit t :tag "@wellbeing"))
                     (:name "Chores" :and (:habit t :tag "@chore"))
                     (:name "Other habits" :habit t)))

    ;; Show blocked and those that are associated with today, discard the rest.
    (:name "Blocked" :todo "BLOCKED" :order 7)
    (:name "Today" :date today :order 1)
    (:discard (:anything t))))

(defun wal-org-super-agenda--with-groups (fun &rest args)
  "Call FUN with ARGS."
  (let ((org-super-agenda-groups wal-org-super-agenda-groups))

    (apply fun args)))

(use-package org-super-agenda
  :demand t
  :after org-agenda

  :config
  (org-super-agenda-mode)

  (advice-add
   'org-agenda-list :around
   #'wal-org-super-agenda--with-groups)

  :custom
  (org-super-agenda-final-group-separator "\n")

  :functions (org-super-agenda-mode))

org-roam

Trying to organize my thoughts using Zettelkästen. This package allows you to create a web of interconnected nodes of org files.

This adds a function to refile only within org-roam files.

Note that you will need to install sqlite3 manually.

(junk-expand org-roam
  "Note rhizome."
  :packages (org-roam)
  :extras (org-roam-ui))

(use-package org-roam
  :wal-ways t
  :if (executable-find "sqlite3")

  :commands
  (org-roam-buffer-display-dedicated
   org-roam-capture
   org-roam-node-create
   org-roam-node-find
   org-roam-node-read
   wal-org-roam)

  :init
  (setq org-roam-v2-ack t)

  :config
  ;; Show roam buffer on the right.
  (wdb-nearby org-roam-buffer :side 'right :no-other t)

  (transient-define-prefix wal-org-roam ()
    "Run `org-roam' commands."
    [["Capture"
      ("t" "today" org-roam-dailies-capture-today)]
     ["Find"
      ("f" "note" org-roam-node-find)
      ("T" "today" org-roam-dailies-goto-today)
      ("d" "daily" org-roam-dailies-goto-date)
      ("D" "daily directory" org-roam-dailies-find-directory)]
     ["Actions"
      ("b" "toggle roam buffer" org-roam-buffer-toggle)
      ("w" "roam refile" org-roam-refile
       :inapt-if-not-mode 'org-mode)
      ("i" "insert node" org-roam-node-insert
       :inapt-if-not-mode 'org-mode)
      ("@" "add tag" org-roam-tag-add
       :inapt-if-not-mode 'org-mode)]
     ["Visualization"
      ("g" "graph" org-roam-graph)]])

  (org-roam-db-autosync-enable)

  :custom
  ;; Setup directories and file names.
  (org-roam-directory (expand-file-name "zettelkasten" org-directory))
  (org-roam-extract-new-file-path "${slug}.org")

  ;; Simple capture templates.
  (org-roam-capture-templates
   '(("d" "default" plain "%?"
      :target (file+head "${slug}.org"
                         "#+title: ${title}\n")
      :unnarrowed t)))

  :wal-bind
  (("p" . org-roam-capture)
   ("M-p" . org-roam-dailies-capture-today)
   ("C-p" . wal-org-roam))

  :functions (org-roam-db-autosync-enable)
  :defines (org-roam-buffer org-roam-v2-ack))

org-roam-dailies

(defun wal-org-roam-dailies--with-first-template-only (func &rest args)
  "Run dailies command FUNC with templates set to nil.

ARGS are passed to FUNC."
  (let ((org-roam-dailies-capture-templates (seq-subseq
                                             org-roam-dailies-capture-templates
                                             0
                                             1)))

    (funcall func args)))

(defun wal-org-archive-subtree ()
  "Archive normally.

If the current file is a dailies file, archive in a single location."
  (interactive)

  (let* ((file-name (buffer-file-name))
         (dailies-file-p (and file-name
                              (string-match-p org-roam-dailies-directory file-name)))
         (org-archive-location (if dailies-file-p
                                   (expand-file-name "dailies_archive.org::* From %s" org-roam-directory)
                                 org-archive-location)))

    ;; FIXME: In Emacs 30 this doesn't emit a warning.
    (with-no-warnings
      (call-interactively #'org-archive-subtree))))

(use-package org-roam-dailies
  :after org-roam
  :demand t

  :config
  (setq org-archive-default-command #'wal-org-archive-subtree)

  ;; Don't force template selection when just visiting a file.
  (wal-advise-many
   'wal-org-roam-dailies--with-first-template-only :around
   '(org-roam-dailies-goto-date
     org-roam-dailies-goto-today
     org-roam-dailies-goto-tomorrow
     org-roam-dailies-goto-next-note
     org-roam-dailies-goto-yesterday
     org-roam-dailies-goto-previous-note))

  :custom
  (org-roam-dailies-directory "tagebuch/")
  (org-roam-dailies-capture-templates
   '(("t" "default" entry
      "* %?\n:PROPERTIES:\n:TASK: %K\n:END:"
      :target (file+head "%<%Y-%m-%d>.org"
                         "#+title: %<%Y-%m-%d>\n")
      :empty-lines 1)
     ("d" "task with deadline" entry
      "* TODO %?\nDEADLINE:%t\n:PROPERTIES:\n:TASK: %K\n:END:"
      :target (file+head "%<%Y-%m-%d>.org"
                         "#+title: %<%Y-%m-%d>\n")
      :empty-lines 1))))

org-tree-slide

Turn any org-mode buffer into a presentation. The custom functions make sure that content is centered and only code retains fixed pitch.

(defun wal-relative-column-width (&optional target-width)
  "Get the relative column width of TARGET-WIDTH."
  (let ((width (or target-width 160))
        (scale (if (and (boundp 'text-scale-mode-amount)
                        (numberp text-scale-mode-amount))
                   (expt text-scale-mode-step text-scale-mode-amount)
                 1)))

    (ceiling (/ width scale))))

(defun wal-org-tree-slide-toggle-visibility ()
  "Toggle visibility of cursor."
  (interactive)

  (if cursor-type
      (setq cursor-type nil)
    (setq cursor-type t)))

(defun wal-org-tree-slide-play ()
  "Hook into `org-tree-slide-play'."
  (setq-local visual-fill-column-width (wal-relative-column-width 160)
              visual-fill-column-center-text t
              cursor-type nil)
  (visual-fill-column-mode 1)

  (mixed-pitch-mode 1)

  (wal-org-hide-emphasis-markers))

(defun wal-org-tree-slide-stop ()
  "Hook into `org-tree-slide-stop'."
  (setq-local visual-fill-column-width nil
              visual-fill-column-center-text nil
              cursor-type t
              org-hide-emphasis-markers nil)
  (visual-fill-column-mode -1)

  (declare-function outline-show-all "ext:outline.el")

  (outline-show-all)

  (mixed-pitch-mode -1)

  (text-scale-adjust 0)

  (wal-org-hide-emphasis-markers t))

(defun wal-org-tree-slide-text-scale ()
  "Hook into `text-scale-mode-hook' for `org-tree-slide'."
  (when (and (boundp 'org-tree-slide-mode) org-tree-slide-mode)
    (wal-org-tree-slide-play)))

(use-package org-tree-slide
  :after org

  :hook
  ((org-tree-slide-play . wal-org-tree-slide-play)
   (org-tree-slide-stop . wal-org-tree-slide-stop)
   (text-scale-mode . wal-org-tree-slide-text-scale))

  :init
  (transient-append-suffix 'org-mode-major '(1 -1)
    '["Presentation"
      ("p" "present" org-tree-slide-mode)])

  :custom
  (org-tree-slide-never-touch-face t)
  (org-tree-slide-cursor-init nil)
  (org-tree-slide-activate-message "We're on a road to nowhere")
  (org-tree-slide-deactivate-message "Take you here, take you there")
  (org-tree-slide-indicator '(:next "   >>>" :previous "<<<" :content "< Here is where time is on our side >"))

  :bind
  (:map org-tree-slide-mode-map
   ("q" . org-tree-slide-mode) ; To close it again.
   ("n" . org-tree-slide-move-next-tree)
   ("p" . org-tree-slide-move-previous-tree)
   ("i" . text-scale-increase)
   ("d" . text-scale-decrease)
   ("v" . wal-org-tree-slide-toggle-visibility))

  :defines (org-tree-slide-mode-map))

org-src

Editing source blocks in Org files.

Loads a few more languages and disables native tabs in source blocks.

(use-package org-src
  :after org

  :config
  (wal-append
   'org-src-lang-modes
   '(("dockerfile" . dockerfile)
     ("conf" . conf)
     ("markdown" . markdown)
     ("fish" . fish)))

  (transient-append-suffix 'org-mode-major '(0 0 -1)
    '("e" "edit source" org-edit-src-code
      :inapt-if-not org-in-src-block-p))

  :custom
  (org-src-tab-acts-natively nil)
  (org-edit-src-content-indentation 0)

  :bind
  (:map org-src-mode-map
   ("C-c C-c" . org-edit-src-exit))

  :delight " osc")

org-capture

If you want to just write a quick note or todo for yourself, org-capture is your friend. This adds the concept of project tasks that are collected in distinct files under a desired heading. They can be created using one of the custom templates. The others are for taking notes related to the currently clocked task and one for dailies (although org-roam is preferred for these).

(defvar-local wal-org-capture-tasks-heading "Tasks")
(put 'wal-org-capture-tasks-heading 'safe-local-variable #'stringp)

(defvar-local wal-org-capture-tasks-file nil)
(put 'wal-org-capture-tasks-file 'safe-local-variable #'stringp)

(defun wal-org-capture--find-project-tasks-heading (&optional arg)
  "Find the heading of a project's tasks.

The project is the current project unless ARG is t."
  (declare-function org-find-exact-heading-in-directory "ext:org.el")
  (declare-function org-find-exact-headline-in-buffer "ext:org.el")

  (let ((project-current-directory-override (or (and arg (project-prompt-project-dir))
                                                project-current-directory-override)))

    (if-let* ((project (project-current t))
              (root (project-root project))
              (heading (wal-project-local-value 'wal-org-capture-tasks-heading project))
              (marker (or (and-let* ((file (wal-project-local-value 'wal-org-capture-tasks-file project))
                                     (canonicalized (and (boundp 'org-directory)
                                                         (expand-file-name file org-directory)))
                                     (buffer (and (file-exists-p canonicalized)
                                                  (find-file-noselect canonicalized))))
                            (org-find-exact-headline-in-buffer heading buffer))
                          (org-find-exact-heading-in-directory heading (or (wal-project-local-value 'wal-project-parent-project) root)))))
        marker
      (user-error "Couldn't find heading '%s'" wal-org-capture-tasks-heading))))

(defun wal-org-capture-locate-project-tasks (&optional other-project)
  "Locate project tasks.

If OTHER-PROJECT is t, do that for another project."
  (let ((marker (wal-org-capture--find-project-tasks-heading other-project)))

    (set-buffer (marker-buffer marker))
    (goto-char (marker-position marker))))

(defun wal-org-capture-project-tasks (&optional goto)
  "Go to project tasks.

See `org-capture' for the usage of GOTO."
  (interactive "P")

  (org-capture goto "p"))

(use-package org-capture
  :custom
  (org-capture-templates
   `(("c" "clocking task" plain
      (clock)
      "\n%?\n"
      :unnarrowed t)
     ("d" "daily note" plain
      (file+olp+datetree ,(concat org-directory "/dailies.org"))
      "%i\n%?"
      :empty-lines-before 1
      :empty-lines-after 1)
     ("t" "new project task" entry
      (function wal-org-capture-locate-project-tasks)
      "* TODO %?\n\n%i"
      :empty-lines-before 1
      :empty-lines-after 1
      :before-finalize (org-set-tags-command))
     ("T" "new project task (other project)" entry
      (function (lambda () (wal-org-capture-locate-project-tasks t)))
      "* TODO %?\n\n%i"
      :empty-lines-before 1
      :empty-lines-after 1
      :before-finalize (org-set-tags-command))
     ("p" "project tasks" plain
      (function wal-org-capture-locate-project-tasks)
      ""
      :unnarrowed t)))
  (org-capture-bookmark nil) ; Prevents countless edit buffers since we annotate bookmarks.

  :bind
  (("C-c c" . org-capture)
   ("C-c M-c" . wal-org-capture-project-tasks))

  :delight " cap")

org-refile

Configure refiling headings. Reduces the depth for agenda files.

(defun wal-org-refile--maybe-use-default-directory (&optional arg &rest _rest)
  "If ARG is 5, set `org-agenda-files' to the `default-directory'."
  (declare-function org-refile "ext:org-refile.el")

  (when (eq 5 (prefix-numeric-value arg))
    (let ((org-agenda-files (list default-directory)))

        (org-refile))))

(use-package org-refile
  :config
  (advice-add
   'org-refile
   :before-until #'wal-org-refile--maybe-use-default-directory)

  :custom
  (org-refile-targets
   '((nil . (:maxlevel . 4))
     (org-agenda-files . (:maxlevel . 3)))))

org-babel

Source block interaction.

Loads a few more languages and doesn’t require confirmation of block evaluation.

(use-package ob
  :config
  (wal-append
   'org-babel-load-languages
   '((shell . t)
     (python . t)
     (latex . t)
     (js . t)))

  :custom
  (org-confirm-babel-evaluate nil))

org-clock

You know the drill. Clock in, clock out. Makes sure that headings with a todo keyword are set to in progress when clocked into. Also adds a command to ignore continuous clocking as well as one to add a note to the clocked task.

(defvar-local wal-org-clock-in-progress-state "IN PROGRESS"
  "The state signifying a task is in progress.")
(put 'wal-org-clock-in-progress-state 'safe-local-variable #'stringp)

(defun wal-org-clock-in-switch-to-state (todo-state)
  "Only switch state to IN PROGRESS if TODO-STATE was given."
  (defvar org-todo-keywords-1)

  (when (and todo-state
             (member wal-org-clock-in-progress-state org-todo-keywords-1))
    wal-org-clock-in-progress-state))

(defun wal-org-clock-out-switch-to-state (todo-state)
  "Switch from TODO-STATE to a user-selected state.

The possible states is reduced to those following the current
state if that state is known."
  (defvar org-todo-keywords-1)
  (defvar org-clock-current-task)

  (and-let* (todo-state
             (keywords org-todo-keywords-1)
             (task (or org-clock-current-task "Current task"))
             (prompt (format "Switch `%s' from %s to: " task todo-state)))

    (completing-read prompt keywords nil t)))

(defun wal-org-clock-heading ()
  "Render a truncated heading for modeline."
  (declare-function org-link-display-format "ext:org-link.el")
  (declare-function org-get-heading "ext:org.el")

  (let ((heading (org-link-display-format
	              (org-no-properties (org-get-heading t t t t)))))

    (truncate-string-to-width heading 12 0 nil t)))

(defun wal-org-clock-in-from-now ()
  "Force `org-clock-in' without continuous logging."
  (defvar org-clock-continuously)
  (declare-function org-clock-in "ext:org-clock.el")

  (let ((org-clock-continuously nil))

    (org-clock-in)))

(defun wal-org-clock-kill-current-task ()
  "Insert the current task."
  (interactive)

  (unless org-clock-current-task
    (user-error "No current task"))

  (let ((no-props (substring-no-properties org-clock-current-task)))

    (kill-new no-props)
    (message "Added '%s' to kill ring" no-props)))

(use-package org-clock
  :after org

  :init
  (org-clock-persistence-insinuate)

  :config
  (with-eval-after-load 'org-keys
    (add-to-list 'org-speed-commands '("N" . wal-org-clock-in-from-now)))

  :custom
  ;; We want a continuous, persistent clock.
  (org-clock-continuously t)
  (org-clock-persist 'clock)

  ;; Resolve after idling.
  (org-clock-idle-time 120)

  ;; Switch state conditionally and resume.
  (org-clock-in-switch-to-state 'wal-org-clock-in-switch-to-state)
  (org-clock-in-resume t)

  ;; Switch state conditionally and remove zero clocks.
  (org-clock-out-switch-to-state 'wal-org-clock-out-switch-to-state)
  (org-clock-out-remove-zero-time-clocks t)

  (org-clock-report-include-clocking-task t)

  ;; Truncate overly long tasks.
  (org-clock-heading-function #'wal-org-clock-heading))

org-duration

Set up durations for a 40-hour week.

(use-package org-duration
  :after org

  :config
  ;; 40h working week, one month of vacation.
  (wal-replace-in-alist
    'org-duration-units
    `(("d" . ,(* 60 8))
      ("w" . ,(* 60 8 5))
      ("m" . ,(* 60 8 5 4))
      ("y" . ,(* 60 8 5 4 11)))))

org-keys

Add some user speed commands.

(use-package org-keys
  :after org

  :custom
  (org-use-speed-commands t)
  (org-return-follows-link t))

org-modern

Modern look.

(use-package org-modern
  :hook (org-mode . org-modern-mode)

  :custom
  (org-modern-hide-stars " ")
  (org-modern-star '("" "" "" "" "" "" "")))

Footer

(provide 'wal-org)

;;; wal-org.el ends here