Tags: helm

RSS - Atom - Subscribe via email

Getting Helm and org-refile to clock in or create tasks

Posted: - Modified: | emacs, org

I've been thinking about how to improve the way that I navigate to, clock in, and create tasks in Org Mode. If the task is one of the ones I've planned for today, I use my Org agenda. If I know that the task exists, I use C-u C-c C-w (org-refile) to jump to it, and then ! (one of my org-speed-commands-user options) to clock in and track it on Quantified Awesome. If I want to resume an interrupted task, I use C-u C-c j (my shortcut for org-clock-goto). For new tasks, I go to the appropriate project entry and create it, although I really should be using org-capture instead.

I thought about how I can reduce some of these distinctions. For example, what if it didn't matter whether or not a task already exists? I can modify the org-refile interface to make it easier for me to create tasks if my description doesn't match anything. To make things simpler, I'll just reuse one of my org-capture-templates, and I'll pre-fill it with the candidate from Helm.

(ert-deftest sacha/org-capture-prefill-template ()
  (should
   ;; It should fill things in one field at a time
   (string=
    (sacha/org-capture-prefill-template
     "* TODO %^{Task}\nSCHEDULED: %^t\n:PROPERTIES:\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}\n:END:\n%?\n"
     "Hello World")
    "* TODO Hello World\nSCHEDULED: %^t\n:PROPERTIES:\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}\n:END:\n%?\n"
    ))
  (should
   (string=
    (sacha/org-capture-prefill-template
     "* TODO %^{Task}\nSCHEDULED: %^t\n:PROPERTIES:\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}\n:END:\n%?\n"
     "Hello World" "<2015-01-01>")
    "* TODO Hello World\nSCHEDULED: <2015-01-01>\n:PROPERTIES:\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}\n:END:\n%?\n"))
  (should
   (string=
    (sacha/org-capture-prefill-template
     "* TODO %^{Task}\nSCHEDULED: %^t\n:PROPERTIES:\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}\n:END:\n%?\n"
     "Hello World" "<2015-01-01>" "0:05")
    "* TODO Hello World\nSCHEDULED: <2015-01-01>\n:PROPERTIES:\n:Effort: 0:05\n:END:\n%?\n")))

(defun sacha/org-capture-prefill-template (template &rest values)
  "Pre-fill TEMPLATE with VALUES."
  (setq template (or template (org-capture-get :template)))
  (with-temp-buffer
    (insert template)
    (goto-char (point-min))
    (while (re-search-forward
            (concat "%\\("
                    "\\[\\(.+\\)\\]\\|"
                    "<\\([^>\n]+\\)>\\|"
                    "\\([tTuUaliAcxkKInfF]\\)\\|"
                    "\\(:[-a-zA-Z]+\\)\\|"
                    "\\^\\({\\([^}]*\\)}\\)"
                    "?\\([gGtTuUCLp]\\)?\\|"
                    "%\\\\\\([1-9][0-9]*\\)"
                    "\\)") nil t)
      (if (car values)
          (replace-match (car values) nil t))
      (setq values (cdr values)))
    (buffer-string)))

(defun sacha/helm-org-create-task (candidate)
  (let ((entry (org-capture-select-template "T")))
    (org-capture-set-plist entry)
    (org-capture-get-template)
    (org-capture-set-target-location)
    (condition-case error
        (progn
          (org-capture-put
           :template
           (org-capture-fill-template
            (sacha/org-capture-prefill-template (org-capture-get :template)
                                                candidate)))
          (org-capture-place-template
           (equal (car (org-capture-get :target)) 'function)))
      ((error quit)
       (if (get-buffer "*Capture*") (kill-buffer "*Capture*"))
       (error "Capture abort: %s" error)))) t)

Next, I want to add this to the way that Helm prompts me to refile. That means that my creation task should return something ready for org-refile. Actually, maybe I don't have to do that if I know I'm always going to call it when I want to jump to something. I might as well add that bit of code that sets up clocking in, too.

(defvar sacha/helm-org-refile-locations nil)

(defun sacha/helm-org-clock-in-and-track-from-refile (candidate)
  (let ((location (org-refile--get-location candidate sacha/helm-org-refile-locations)))
    (save-window-excursion
      (org-refile 4 nil location)
      (sacha/org-clock-in-and-track)
      t)))

(defun sacha/helm-org-refile-read-location (tbl)
  (setq sacha/helm-org-refile-locations tbl)
  (helm
   (list
    (helm-build-sync-source "Refile targets"
      :candidates (mapcar 'car tbl)
      :action '(("Select" . identity)
                ("Clock in and track" . sacha/helm-org-clock-in-and-track-from-refile))
      :history 'org-refile-history)
    (helm-build-dummy-source "Create task"
      :action (helm-make-actions
               "Create task"
               'sacha/helm-org-create-task)))))

(defun sacha/org-refile-get-location (&optional prompt default-buffer new-nodes no-exclude)
  "Prompt the user for a refile location, using PROMPT.
PROMPT should not be suffixed with a colon and a space, because
this function appends the default value from
`org-refile-history' automatically, if that is not empty.
When NO-EXCLUDE is set, do not exclude headlines in the current subtree,
this is used for the GOTO interface."
  (let ((org-refile-targets org-refile-targets)
        (org-refile-use-outline-path org-refile-use-outline-path)
        excluded-entries)
    (when (and (derived-mode-p 'org-mode)
               (not org-refile-use-cache)
               (not no-exclude))
      (org-map-tree
       (lambda()
         (setq excluded-entries
               (append excluded-entries (list (org-get-heading t t)))))))
    (setq org-refile-target-table
          (org-refile-get-targets default-buffer excluded-entries)))
  (unless org-refile-target-table
    (user-error "No refile targets"))
  (let* ((cbuf (current-buffer))
         (partial-completion-mode nil)
         (cfn (buffer-file-name (buffer-base-buffer cbuf)))
         (cfunc (if (and org-refile-use-outline-path
                         org-outline-path-complete-in-steps)
                    'org-olpath-completing-read
                  'org-icompleting-read))
         (extra (if org-refile-use-outline-path "/" ""))
         (cbnex (concat (buffer-name) extra))
         (filename (and cfn (expand-file-name cfn)))
         (tbl (mapcar
               (lambda (x)
                 (if (and (not (member org-refile-use-outline-path
                                       '(file full-file-path)))
                          (not (equal filename (nth 1 x))))
                     (cons (concat (car x) extra " ("
                                   (file-name-nondirectory (nth 1 x)) ")")
                           (cdr x))
                   (cons (concat (car x) extra) (cdr x))))
               org-refile-target-table))
         (completion-ignore-case t)
         cdef
         (prompt (concat prompt
                         (or (and (car org-refile-history)
                                  (concat " (default " (car org-refile-history) ")"))
                             (and (assoc cbnex tbl) (setq cdef cbnex)
                                  (concat " (default " cbnex ")"))) ": "))
         pa answ parent-target child parent old-hist)
    (setq old-hist org-refile-history)
    ;; Use Helm's sources instead
    (setq answ (sacha/helm-org-refile-read-location tbl))
    (if (and (stringp answ)
             (setq pa (org-refile--get-location answ tbl)))
        (progn
          (org-refile-check-position pa)
          (when (or (not org-refile-history)
                    (not (eq old-hist org-refile-history))
                    (not (equal (car pa) (car org-refile-history))))
            (setq org-refile-history
                  (cons (car pa) (if (assoc (car org-refile-history) tbl)
                                     org-refile-history
                                   (cdr org-refile-history))))
            (if (equal (car org-refile-history) (nth 1 org-refile-history))
                (pop org-refile-history)))
          pa)
      (if (and (stringp answ) (string-match "\\`\\(.*\\)/\\([^/]+\\)\\'" answ))
          (progn
            (setq parent (match-string 1 answ)
                  child (match-string 2 answ))
            (setq parent-target (org-refile--get-location parent tbl))
            (when (and parent-target
                       (or (eq new-nodes t)
                           (and (eq new-nodes 'confirm)
                                (y-or-n-p (format "Create new node \"%s\"? "
                                                  child)))))
              (org-refile-new-child parent-target child)))
        (if (not (equal answ t)) (user-error "Invalid target location"))))))

(fset 'org-refile-get-location 'sacha/org-refile-get-location)

Hooray! Now C-u C-c C-w (org-refile) also lets me use TAB or F2 to select the alternative action of quickly clocking in on a task. Mwahaha.

You can check out this code in my config to see if anything has been updated. Want to learn more about modifying Helm? Check out these posts by John Kitchin and Rubikitch.

I think I'm getting the hang of tweaking Helm.  Yay!

View or add comments (Disqus), or e-mail me at sacha@sachachua.com

Digital index piles with Emacs: Rapid categorization of Org Mode items

Posted: - Modified: | emacs, org

Somewhat daunted by the prospect of categorizing more than a hundred sketches and blog posts for my monthly review, I spent some time figuring out how to create the digital equivalent of sorting index cards into various piles.

2015-02-01 Digital piles of index card -- index card #indexing #organization #pkm

In fact, wouldn't it be super-cool if the items could automatically guess which category they should probably go in, prompting me only if it wasn't clear?

I wanted to write a function that could take a list structured like this:

It should file Link 1 and 3 under Keyword A, Link 2 under Keyword B, and prompt me for the category for Link 4. At that prompt, I should be able to select Keyword A or Keyword B, or specify a new category.

Inspired by John Kitchin's recent post on defining a Helm source, I wanted to get it to work with Helm.

First step: I needed to figure out the structure of the list, maybe including a sample from the category to make it clearer what's included. org-list.el seemed to have useful functions for this. org-list-struct gave me the structure of the current list. Let's say that a category is anything whose text does not match org-bracket-link-regexp.

(defun sacha/org-get-list-categories ()
  "Return a list of (category indent matching-regexp sample).
List categories are items that don't contain links."
  (let ((list (org-list-struct)) last-category results)
    (save-excursion
      (mapc
       (lambda (x)
         (goto-char (car x))
         (let ((current-item
                (buffer-substring-no-properties
                 (+ (point)
                    (elt x 1)
                    (length (elt x 2)))
                 (line-end-position))))
           (if (string-match
                org-bracket-link-regexp
                (buffer-substring-no-properties
                 (point)
                 (line-end-position)))
               ;; Link - update the last category
               (when last-category
                 (if (< (elt x 1) (elt last-category 1))
                     (setq results
                           (cons (append last-category
                                         (list
                                          (match-string-no-properties
                                           3
                                           (buffer-substring-no-properties
                                            (point)
                                            (line-end-position)))))
                                 (cdr results))))
                 (setq last-category nil))
             ;; Category
             (setq results
                     (cons
                      (setq last-category
                            (list
                             current-item
                             (elt x 1)
                             (concat "^"
                                     (make-string (elt x 1) ?\ )
                                     (regexp-quote
                                      (concat (elt x 2)
                                              current-item))
                                     "$")))
                      results)))))
       list))
    results))

The next step was to write a function that guessed the list category based on the item text, and moved the item there.

(defvar sacha/helm-org-list-candidates nil)
(defun sacha/helm-org-list-categories-init-candidates ()
  "Return a list of categories from this list in a form ready for Helm."
  (setq sacha/helm-org-list-candidates
        (mapcar (lambda (x)
                  (cons (if (elt x 3)
                            (format "%s - %s" (car x) (elt x 3))
                          (car x))
                        x))
                (sacha/org-get-list-categories))))

(defun sacha/org-move-current-item-to-category (category)
  (when category
    (let* ((beg (line-beginning-position))
           (end (line-end-position))
           (string (buffer-substring-no-properties beg end)))
      (save-excursion
        (when (re-search-backward (elt category 2) nil t)
          (delete-region beg (min (1+ end) (point-max)))
          (forward-line 1)
          (insert (make-string (+ 2 (elt category 1)) ?\ )
                  string "\n")))) t))

(defun sacha/org-guess-list-category (&optional categories)
  (interactive)
  (require 'cl-lib)
  (unless categories
    (setq categories
          (sacha/helm-org-list-categories-init-candidates)))
  (let* ((beg (line-beginning-position))
         (end (line-end-position))
         (string (buffer-substring-no-properties beg end))
         (found
          (cl-member string
                     categories
                     :test
                     (lambda (string cat-entry)
                       (string-match (regexp-quote (downcase (car cat-entry)))
                                     string)))))
    (when (car found)
      (sacha/org-move-current-item-to-category
       (cdr (car found)))
      t)))

After that, I wrote a function that used Helm to prompt me for a category in case it couldn't guess the category. It took me a while to figure out that I needed to use :init instead of :candidates because I wanted to read information from the buffer before Helm kicked in.

(setq sacha/helm-org-list-category-source
      (helm-build-sync-source
          "Non-link categories in the current list"
        :init 'sacha/helm-org-list-categories-init-candidates
        :candidates 'sacha/helm-org-list-candidates
        :action 'sacha/org-move-current-item-to-category
        :fuzzy-match t))

(defun sacha/org-guess-uncategorized ()
  (interactive)
  (sacha/helm-org-list-categories-init-candidates)
  (let (done)
    (while (not done)
      (save-excursion
        (unless (sacha/org-guess-list-category sacha/helm-org-list-candidates)
          (unless
              (helm :sources
                    '(sacha/helm-org-list-category-source
                      sacha/helm-org-list-category-create-source))
            (setq done t))))
      (unless done
        (setq done (not (looking-at "^[-+] \\[")))))))

The :action above refers to this function, which creates a category if it doesn't exist yet.

(setq sacha/helm-org-list-category-create-source
      (helm-build-dummy-source
          "Create category"
        :action (helm-make-actions
                 "Create category"
                 (lambda (candidate)
                   (save-excursion
                     (let* ((beg (line-beginning-position))
                            (end (line-end-position))
                            (string (buffer-substring beg end)))
                       (delete-region beg (min (1+ end) (point-max)))
                       (org-beginning-of-item-list)
                       (insert "- " candidate "\n  " string "\n")))
                   (sacha/helm-org-list-categories-init-candidates)))))

I'm new to fiddling with Helm, so this implementation is not the best it could be. But it's nifty and it works the way I want it to, hooray! Now I can generate a list of blog posts and unblogged sketches, categorize them quickly, and then tweak the categorizations afterwards.

2015-02-01 Index card sketches and monthly reviews -- index card #organization #pkm #indexing

You can see the results in my January 2015 review, or check my config to see if the code has changed.

My next step for learning more about Helm sources is probably to write a Helm command that creates a montage of selected images. John Kitchin has a post about handling multiple selection in Helm, so I just need to combine that with my code for using Imagemagick to create a montage of images. Whee!

View or add comments (Disqus), or e-mail me at sacha@sachachua.com

Emacs kaizen: helm-swoop and editing

Posted: - Modified: | emacs

Continuing on this quest to focus on one tiny little workflow change at a time, so that I can get even better at using Emacs…

One of those packages I installed but never got around to trying out was all, which lets you interactively edit all lines matching a given regular expression. It’s like an editable occur, sorta.

It turns out that helm-swoop lets you use C-c C-e to edit matching lines interactively (so you can use keyboard macros or replace-regexp or whatever). You can type C-x C-s to save it back to the buffer.

On a related note, I’m still tickled pink every time I use dired-toggle-read-only (C-x C-q) to make a Dired buffer editable so that I can batch-rename filenames.

View or add comments (Disqus), or e-mail me at sacha@sachachua.com

Emacs kaizen: ace-isearch combines ace-jump-mode and helm-swoop

Posted: - Modified: | emacs

I’m a fan of clever little things that change their behaviour depending on what you’re doing while letting you mentally think of it as just one function.

ace-isearch looks like it’ll be useful for collapsing three different functions into one mental thing in my head:

If you install the ace-isearch package and turn on global-ace-isearch-mode, then searching for a single character triggers ace-jump-mode, searching for less than 5 characters triggers isearch, and anything longer triggers helm-swoop-from-isearch. You can customize those thresholds, of course.

Neat!

View or add comments (Disqus), or e-mail me at sacha@sachachua.com

Emacs: M-y as helm-show-kill-ring

Posted: - Modified: | emacs

After realizing that I barely scratched the surface of Helm's awesomeness (really, I basically use it as an ido-vertical-mode), I made a concerted effort to explore more of the interesting things in the Helm toolkit. helm-show-kill-ring is one such thing. I've bound it to M-y, which I had previously configured to be browse-kill-ring, but helm-show-kill-ring is much cooler because it makes it easy to dynamically filter your kill ring. Also, Kcode>M-y works better for me than C-y does because I know when I want the last thing I killed, but going beyond that is a little annoying.

That said, browse-kill-ring does make it easy to edit a kill ring entry. Maybe I should learn how to modify Helm's behaviour so that I can add an edit action. There's already a delete action. Besides, I haven't used that feature in browse-kill-ring yet, so I can probably get by even without it.

ido fans: you can use helm-show-kill-ring without activating helm-mode, if you want.

On a related note, I like how rebinding M-x (execute-extended-comand) to helm-M-x shows me keybindings as I search for commands. You do have to get used to the quirk of typing C-u and other prefixes after M-x instead of before, but I haven't had a problem with this yet. This is mostly because I haven't dug into just how many commands do awesome things when given a prefix argument. I know about using C-u C-c C-w (org-refile) to jump to places instead of refiling notes, but that's about it. I haven't gone anywhere close to C-u C-u. Does anyone have a favourite command they use that does really smart things when given that prefix? =)

This Helm intro has animated GIFs and a few other useful commands. Check it out!

View or add comments (Disqus), or e-mail me at sacha@sachachua.com

Emacs Basics: Call commands by name with M-x (with tips for better completion using ido or helm)

Posted: - Modified: | emacs, emacs-basics, podcast

Emacs has way too many keyboard shortcuts to memorize. Fortunately, you can call commands by name by typing M-x and the name of the command. M- stands for the Meta key. If your keyboard does not have a Meta key (and most don't, these days), use Alt or Option. For example, on a PC keyboard, you can type Alt-x. Alternatively, you can replace Meta with ESC. M-x then becomes ESC x.

If you know the name of the command to execute, you can type it after M-x, and then press RET (the Return key, which is the same as the Enter key). For example, M-x find-file opens a file. M-x save-buffer saves the current file. You can use TAB to complete words. Use <up> and <down> to go through your command history.

What if you don't know the name of the command to execute? You can use M-x apropos-command to search for the command using keywords. If you know the keyboard shortcut or you can find the command on a menu, you can also use M-x describe-key and then do the keyboard shortcut or select it from the menu.

If a command you execute has a keyboard shortcut, it will flash briefly at the bottom of your screen. For example:

You can run the command `find-file' with C-x C-f

Using TAB for completion can be a little slow. Here are two ways to make that and a whole lot of other things faster: ido and helm. To explore these approaches, you will need to add the MELPA package repository to your configuration. To set that up, add the following to the beginning of your ~/.emacs.d/init.el file.

(package-initialize)
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t)

Then use M-x eval-buffer to load the changes into your current Emacs, and use M-x package-refresh-contents to reload the list of packages.

Helm mode

This is what completion with Helm looks like:

2014-03-17 13_06_54-c__sacha_personal_organizer.org.png

Figure 2: Helm

Use M-x package-install to install the helm package. Then you can try it out with M-x helm-mode . After you start Helm mode, try M-x again. You can type in multiple words to search for a command, and you can use <up> and <down> to go through completions. Use M-p and M-n to go through your command history.

If you like it, here's some code that you can add to your ~/.emacs.d/init.el file to load it automatically next time, and to tweak it for more convenience.

(require 'helm-config) 
(helm-mode 1)

Use M-x eval-buffer to load your changes.

If you change your mind and want to disable helm-mode, you can toggle it off with M-x helm-mode .

If you like how that works, you may want to (global-set-key (kbd "M-x") 'helm-M-x). If you do, you'll be able to see keybindings when you call commands with M-x. Note that if you want to use a prefix argument (ex: C-u), you will need to do that after calling M-x instead of before.

Ido, ido-hacks, smex, ido-vertical-mode, and flx-ido

Ido is like Helm, but it takes a different approach. Here's what this combination will get you:

2014-03-17 12_40_40-MELPA.png

Figure 1: ido, smex, ido-vertical-mode, and flx-ido

If you want to give this a try, remove or comment out (helm-mode 1) from your ~/.emacs.d/init.el (if you added it), and disable helm-mode if you still have it active from the previous section.

To set Ido up, use M-x package-install to install ido, smex, ido-vertical-mode, ido-hacks, and flx-ido.

After the packages are installed, add the following code to your ~/.emacs.d/init.el .

(ido-mode 1)
(require 'ido-hacks nil t)
(if (commandp 'ido-vertical-mode) 
    (progn
      (ido-vertical-mode 1)
      (setq ido-vertical-define-keys 'C-n-C-p-up-down-left-right)))
(if (commandp 'smex)
    (global-set-key (kbd "M-x") 'smex))
(if (commandp 'flx-ido-mode)
    (flx-ido-mode 1))

Use M-x eval-buffer to load your changes, then try M-x again. You should now have much better completion. You'll be able to call commands by typing in part of their names. Use <up> and <down> to go through the completion options, and use <left> and <right> to go through your history.

Try it for a week. If you like it, keep it. If you don't like it, try the Helm approach.

Other tips

When you learn keyboard shortcuts, try to remember the names of the commands as well. You can do that with C-h k (describe-key). For example, M-x calls the command execute-extended-command. That way, even if you forget the keyboard shortcut, you can call the command by name.

If you forget the name of the command and you don't know the keyboard shortcut for it, you can look for it in the menus or in the help file. You can open the help file with C-h i (info). You can also use M-x apropos-command to search through the commands that you can call with M-x.

Make your own cheat sheet with frequently-used keyboard shortcuts and commands to help you learn more about Emacs. Good luck!

Emacs Basics: M-x

Emacs Basics: M-x

You can download the MP3 from archive.org.

View or add comments (Disqus), or e-mail me at sacha@sachachua.com