kidd / org-gcal.el

Org sync with Google Calendar. (active maintained project as of 2019-11-06)
438 stars 47 forks source link

date of calendar event as org-deadline? #167

Closed benthamite closed 3 years ago

benthamite commented 3 years ago

When I create an event via org-gcal-post-at-point or by syncing an existing event on Google Calendar, org agenda displays it as an event with an ordinary time stamp. Is there a way to make org interpret this date as a deadline instead, i.e. as a date created using org-deadline? In my workflow, I draw a clear distinction between events that are merely scheduled and events with a proper deadline: the latter involve an external commitment, and are shown with a different color and font. I have already missed a couple of appointments because of this, so I was wondering if org-gcal could be configured so as to address the issue. Thank you.

benthamite commented 3 years ago

To answer my own question: I simply replaced the two functions containing org-schedule or :scheduled with org-deadline and :deadline, respectively. The functions in question are org-gcal--update-entry and ~org-gcal--back-to-heading~ org-gcal--get-time-and-desc. There may be a more elegant way of addressing the issue, but this worked for me. I did the replacement using el-patch.

telotortium commented 3 years ago

Thanks - I haven't heard of el-patch before - looks interesting. Would you mind sending me your el-patch commands? I'd like to see if I can easily use it to replace some parts of my init file.

There is a hook for customizing updating the event entry (i.e., headline), org-gcal-after-update-entry-functions. Unfortunately it was not documented in the README, so I fixed that in db3072a. For your use case, though, you have to customize how the time is read from the event as well (I assume you patched org-gcal--get-time-and-desc, not org-gcal--back-to-heading), so that hook wouldn't be enough, and el-patch looks like the best alternative.

benthamite commented 3 years ago

Thanks for explaining that. And you are correct: I meant org-gcal--get-time-and-desc; sorry for the confusion.

I'm an Elisp novice and this is actually the first time I use el-patch, so I can't guarantee I'm doing things properly. Below I include my entire org-gcal use-package declaration for completeness (it assumes straight, general and hydra are installed):

(use-package org-gcal
  :straight (org-gcal :type git :host github :repo "kidd/org-gcal.el")
  :custom
  (org-gcal-file-alist `((,ps/personal-gmail . ,ps/file-calendar)))
  (org-gcal-client-id (auth-source-pass-get "host" "auth-sources/org-gcal"))
  (org-gcal-client-secret (auth-source-pass-get 'secret "auth-sources/org-gcal"))
  (org-gcal-up-days 7)
  (org-gcal-down-days 7)
  (org-gcal-recurring-events-mode 'nested)
  (org-gcal-remove-api-cancelled-events nil) ; never remove cancelled events
  (org-gcal-auto-archive nil)
  :config
  (defhydra hydra-org-gcal (:hint nil)
    "
_f_etch all       |_s_ync all        |_p_ost at point   |token _r_equest   |_u_nlock sync
_F_etch buffer    |_S_ync buffer     |_d_elete at point |token _c_lear     |_q_uit  "
    ("f" org-gcal-fetch)
    ("F" org-gcal-fetch-buffer)
    ("s" org-gcal-sync)
    ("S" org-gcal-sync-buffer)
    ("p" org-gcal-post-at-point)
    ("d" org-gcal-delete-at-point)
    ("u" org-gcal--sync-unlock)
    ("r" org-gcal-request-token)
    ("c" org-gcal-sync-tokens-clear)
    ("q" nil))
  :init/el-patch
  (defun org-gcal--get-time-and-desc ()
    "Get the timestamp and description of the event at point.

  Return a plist with :start, :end, and :desc keys. The value for a key is nil if
  not present."
    (let (start end desc tobj elem)
      (save-excursion
    (org-gcal--back-to-heading)
    (setq elem (org-element-at-point))
    ;; Parse :org-gcal: drawer for event time and description.
    (when
        (re-search-forward
         (format "^[ \t]*:%s:[ \t]*$" org-gcal-drawer-name)
         (save-excursion (outline-next-heading) (point))
         'noerror)
      ;; First read any event time from the drawer if present. It's located
      ;; at the beginning of the drawer.
      (save-excursion
        (when
        (re-search-forward "<[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
                   (save-excursion (outline-next-heading) (point))
                   'noerror)
          (goto-char (match-beginning 0))
          (setq tobj (org-element-timestamp-parser))))
      ;; Lines after the timestamp contain the description. Skip leading
      ;; blank lines.
      (forward-line)
      (beginning-of-line)
      (re-search-forward
       "\\(?:^[ \t]*$\\)*\\([^z-a]*?\\)\n?[ \t]*:END:"
       (save-excursion (outline-next-heading) (point)))
      (setq desc (match-string-no-properties 1))
      (setq desc
        (if (string-match-p "\\‘\n*\\’" desc)
            nil
          (replace-regexp-in-string
           "^✱" "*"
           (replace-regexp-in-string
            "\\`\\(?: *<[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].*?>$\\)\n?\n?"
            ""
            (replace-regexp-in-string
             " *:PROPERTIES:\n *\\(.*\\(?:\n.*\\)*?\\) *:END:\n+"
             ""
             desc)))))))
      ;; Prefer to read event time from the SCHEDULE property if present.
      (setq tobj (or (org-element-property (el-patch-swap :scheduled :deadline) elem) tobj))
      (when tobj
    (when (plist-get (cadr tobj) :year-start)
      (setq
       start
       (org-gcal--format-org2iso
        (plist-get (cadr tobj) :year-start)
        (plist-get (cadr tobj) :month-start)
        (plist-get (cadr tobj) :day-start)
        (plist-get (cadr tobj) :hour-start)
        (plist-get (cadr tobj) :minute-start)
        (when (plist-get (cadr tobj) :hour-start) t))))
    (when (plist-get (cadr tobj) :year-end)
      (setq
       end
       (org-gcal--format-org2iso
        (plist-get (cadr tobj) :year-end)
        (plist-get (cadr tobj) :month-end)
        (plist-get (cadr tobj) :day-end)
        (plist-get (cadr tobj) :hour-end)
        (plist-get (cadr tobj) :minute-end)
        (when (plist-get (cadr tobj) :hour-end) t)))))
      (list :start start :end end :desc desc)))

  (defun org-gcal--update-entry (calendar-id event &optional update-mode)
    "Update the entry at the current heading with information from EVENT.

EVENT is parsed from the Calendar API JSON response using ‘org-gcal--json-read’.
CALENDAR-ID must be passed as well. Point must be located on an Org-mode heading
line or an error will be thrown. Point is not preserved.

If UPDATE-MODE is passed, then the functions in
‘org-gcal-after-update-entry-functions' are called in order with the same
arguments as passed to this function and the point moved to the beginning of the
heading."
    (unless (org-at-heading-p)
      (user-error "Must be on Org-mode heading."))
    (let* ((smry  (or (plist-get event :summary)
              "busy"))
       (desc  (plist-get event :description))
       (loc   (plist-get event :location))
       (_link  (plist-get event :htmlLink))
       (meet  (plist-get event :hangoutLink))
       (etag (plist-get event :etag))
       (event-id    (plist-get event :id))
       (stime (plist-get (plist-get event :start)
                 :dateTime))
       (etime (plist-get (plist-get event :end)
                 :dateTime))
       (sday  (plist-get (plist-get event :start)
                 :date))
       (eday  (plist-get (plist-get event :end)
                 :date))
       (start (if stime (org-gcal--convert-time-to-local-timezone stime org-gcal-local-timezone) sday))
       (end   (if etime (org-gcal--convert-time-to-local-timezone etime org-gcal-local-timezone) eday))
       (old-time-desc (org-gcal--get-time-and-desc))
       (old-start (plist-get old-time-desc :start))
       (old-end (plist-get old-time-desc :start))
       (recurrence (plist-get event :recurrence))
       (elem))
      (when loc (replace-regexp-in-string "\n" ", " loc))
      (org-edit-headline smry)
      (org-entry-put (point) org-gcal-etag-property etag)
      (when recurrence (org-entry-put (point) "recurrence" (format "%s" recurrence)))
      (when loc (org-entry-put (point) "LOCATION" loc))
      (when meet
    (org-entry-put
     (point)
     "HANGOUTS"
     (format "[[%s][%s]]"
         meet
         "Join Hangouts Meet")))
      (org-entry-put (point) org-gcal-calendar-id-property calendar-id)
      (org-gcal--put-id (point) calendar-id event-id)
      ;; Insert event time and description in :ORG-GCAL: drawer, erasing the
      ;; current contents.
      (org-gcal--back-to-heading)
      (setq elem (org-element-at-point))
      (save-excursion
    (when (re-search-forward
           (format
        "^[ \t]*:%s:[^z-a]*?\n[ \t]*:END:[ \t]*\n?"
        (regexp-quote org-gcal-drawer-name))
           (save-excursion (outline-next-heading) (point))
           'noerror)
      (replace-match "" 'fixedcase)))
      (unless (re-search-forward ":PROPERTIES:[^z-a]*?:END:"
                 (save-excursion (outline-next-heading) (point))
                 'noerror)
    (message "PROPERTIES not found: %s (%s) %d"
         (buffer-name) (buffer-file-name) (point)))
      (end-of-line)
      (newline)
      (insert (format ":%s:" org-gcal-drawer-name))
      (newline)
      ;; Keep existing timestamps for parent recurring events.
      (when (and recurrence old-start old-end)
    (setq start old-start
          end old-end))
      (let*
      ((timestamp
        (if (or (string= start end) (org-gcal--alldayp start end))
        (org-gcal--format-iso2org start)
          (if (and
           (= (plist-get (org-gcal--parse-date start) :year)
              (plist-get (org-gcal--parse-date end)   :year))
           (= (plist-get (org-gcal--parse-date start) :mon)
              (plist-get (org-gcal--parse-date end)   :mon))
           (= (plist-get (org-gcal--parse-date start) :day)
              (plist-get (org-gcal--parse-date end)   :day)))
          (format "<%s-%s>"
              (org-gcal--format-date start "%Y-%m-%d %a %H:%M")
              (org-gcal--format-date end "%H:%M"))
        (format "%s--%s"
            (org-gcal--format-iso2org start)
            (org-gcal--format-iso2org
             (if (< 11 (length end))
                 end
               (org-gcal--iso-previous-day end))))))))
    (if (org-element-property (el-patch-swap :scheduled :deadline) elem)
        (unless (and recurrence old-start) ((el-patch-swap org-schedule org-deadline) nil timestamp))
      (insert timestamp)
      (newline)
      (when desc (newline))))
      ;; Insert event description if present.
      (when desc
    (insert (replace-regexp-in-string "^\*" "✱" desc))
    (insert (if (string= "\n" (org-gcal--safe-substring desc -1)) "" "\n")))
      (insert ":END:")
      (when (org-gcal--event-cancelled-p event)
    (save-excursion
      (org-back-to-heading t)
      (org-gcal--handle-cancelled-entry)))
      (when update-mode
    (cl-dolist (f org-gcal-after-update-entry-functions)
      (save-excursion
        (org-back-to-heading t)
        (funcall f calendar-id event update-mode))))))

  :general
  (org-mode-map
   "s-g" 'hydra-org-gcal/body))

As you can see, the only difference between the patched function and the original one is to be found in the lines containing :schedule and org-schedule.

benthamite commented 3 years ago

BTW, it may be possible to avoid having to copy the entire defun, as discussed here, though I haven't yet investigated this.