diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/calendar-sync-recurrence.el | 20 | ||||
| -rw-r--r-- | modules/calendar-sync-source.el | 15 | ||||
| -rw-r--r-- | modules/calendar-sync.el | 18 | ||||
| -rw-r--r-- | modules/markdown-config.el | 28 | ||||
| -rw-r--r-- | modules/prog-shell.el | 6 | ||||
| -rw-r--r-- | modules/undead-buffers.el | 8 |
6 files changed, 77 insertions, 18 deletions
diff --git a/modules/calendar-sync-recurrence.el b/modules/calendar-sync-recurrence.el index d4f70b7d1..72576a6f7 100644 --- a/modules/calendar-sync-recurrence.el +++ b/modules/calendar-sync-recurrence.el @@ -51,7 +51,15 @@ Returns nil if not found." "Parse a RECURRENCE-ID override EVENT-STR into an exception plist, or nil. Returns nil when EVENT-STR carries no RECURRENCE-ID, or its recurrence-id / start time fail to parse. The plist holds :recurrence-id (localized), -:recurrence-id-raw, :start, :end, :summary, :description, :location." +:recurrence-id-raw, :start, :end, :summary, :description, :location, +:attendees. + +:attendees is carried so `calendar-sync--apply-single-exception' can +re-derive the user's status when a single occurrence is declined: a +RECURRENCE-ID override is exactly how a calendar marks one occurrence of a +recurring series declined, and without the attendee block here the override +inherits the series' \"accepted\" status and the declined occurrence is never +dropped by `calendar-sync--filter-declined'." (let ((recurrence-id (calendar-sync--get-recurrence-id event-str))) (when recurrence-id (let* ((recurrence-id-line (calendar-sync--get-recurrence-id-line event-str)) @@ -72,7 +80,12 @@ start time fail to parse. The plist holds :recurrence-id (localized), (description (calendar-sync--clean-text (calendar-sync--get-property event-str "DESCRIPTION"))) (location (calendar-sync--clean-text - (calendar-sync--get-property event-str "LOCATION")))) + (calendar-sync--get-property event-str "LOCATION"))) + ;; Carry the override's attendee block so a singly-declined + ;; occurrence can re-derive the user's status downstream. + (attendee-lines (calendar-sync--get-all-property-lines event-str "ATTENDEE")) + (attendees (delq nil (mapcar #'calendar-sync--parse-attendee-line + attendee-lines)))) (when (and recurrence-id-parsed start-parsed) (list :recurrence-id (calendar-sync--localize-parsed-datetime recurrence-id-parsed recurrence-id-is-utc recurrence-id-tzid) @@ -81,7 +94,8 @@ start time fail to parse. The plist holds :recurrence-id (localized), :end end-parsed :summary summary :description description - :location location)))))) + :location location + :attendees attendees)))))) (defun calendar-sync--collect-recurrence-exceptions (ics-content) "Collect all RECURRENCE-ID events from ICS-CONTENT. diff --git a/modules/calendar-sync-source.el b/modules/calendar-sync-source.el index d9efc885b..15c91c594 100644 --- a/modules/calendar-sync-source.el +++ b/modules/calendar-sync-source.el @@ -90,7 +90,13 @@ Hash table mapping calendar name (string) to state plist with: (let ((cal-states (alist-get 'calendar-states state))) (clrhash calendar-sync--calendar-states) (dolist (entry cal-states) - (puthash (car entry) (cdr entry) calendar-sync--calendar-states))))) + (let ((st (cdr entry))) + ;; A persisted `syncing' status is stale in a fresh process + ;; (no sync is actually running), so reset it; otherwise the + ;; in-flight guard would skip that calendar forever. + (when (eq (plist-get st :status) 'syncing) + (setq st (plist-put (copy-sequence st) :status 'never))) + (puthash (car entry) st calendar-sync--calendar-states)))))) (error (calendar-sync--log-silently "calendar-sync: Error loading state: %s" (error-message-string err)))))) @@ -98,6 +104,13 @@ Hash table mapping calendar name (string) to state plist with: "Get state plist for CALENDAR-NAME, or nil if not found." (gethash calendar-name calendar-sync--calendar-states)) +(defun calendar-sync--syncing-p (calendar-name) + "Return non-nil when CALENDAR-NAME has an in-flight sync. +Used to skip an overlapping sync when a timer tick fires while the previous +sync for the same calendar is still running." + (eq (plist-get (calendar-sync--get-calendar-state calendar-name) :status) + 'syncing)) + (defun calendar-sync--set-calendar-state (calendar-name state) "Set STATE plist for CALENDAR-NAME." (puthash calendar-name state calendar-sync--calendar-states)) diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 297d1fe61..804d71faf 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -211,10 +211,20 @@ fetcher) or :account + :calendar-id (the \\='api fetcher). Dispatches on the :fetcher key, defaulting to the .ics path. Updates calendar state and saves to disk on completion. The fetch and conversion run in external processes so parsing and writing large -calendar files do not block the interactive Emacs thread." - (if (eq (plist-get calendar :fetcher) 'api) - (calendar-sync--sync-calendar-api calendar) - (calendar-sync--sync-calendar-ics calendar))) +calendar files do not block the interactive Emacs thread. + +Skips a calendar whose previous sync is still in flight, so a timer tick that +fires before a slow fetch finishes does not launch a second overlapping sync for +the same calendar." + (let ((name (plist-get calendar :name))) + (cond + ((calendar-sync--syncing-p name) + (calendar-sync--log-silently + "calendar-sync: [%s] sync already in flight; skipping overlapping tick" name)) + ((eq (plist-get calendar :fetcher) 'api) + (calendar-sync--sync-calendar-api calendar)) + (t + (calendar-sync--sync-calendar-ics calendar))))) (defun calendar-sync--require-calendars () "Return non-nil if calendars are configured, else warn and return nil." diff --git a/modules/markdown-config.el b/modules/markdown-config.el index 4b6c9947d..cb37556f7 100644 --- a/modules/markdown-config.el +++ b/modules/markdown-config.el @@ -70,10 +70,34 @@ lives in a separate command." ;; rendered. (browse-url "http://localhost:8080/imp")) +;; strapdown.js renders the markdown client-side. It is vendored under +;; assets/ and embedded inline rather than loaded from +;; http://ndossougbe.github.io/strapdown/dist/strapdown.js: the CDN was plain +;; HTTP (mixed content), an unmaintained third-party github.io page (a +;; supply-chain and MITM surface), and made the preview fail with no network. +;; The bundle (jQuery + marked + bootstrap themes) is self-contained, so inlining +;; it serves the whole preview from localhost. Refresh by re-downloading the +;; dist build into assets/strapdown.js. +(defconst cj/markdown--strapdown-file + (expand-file-name "assets/strapdown.js" user-emacs-directory) + "Path to the vendored strapdown.js bundle served with the markdown preview.") + +(defvar cj/markdown--strapdown-cache nil + "Cached contents of `cj/markdown--strapdown-file', read once on first preview.") + +(defun cj/markdown--strapdown-js () + "Return the vendored strapdown.js source, reading and caching it once." + (or cj/markdown--strapdown-cache + (setq cj/markdown--strapdown-cache + (with-temp-buffer + (insert-file-contents-literally cj/markdown--strapdown-file) + (buffer-string))))) + (defun cj/markdown-html (buffer) (princ (with-current-buffer buffer - (format "<!DOCTYPE html><html><title>Impatient Markdown</title><xmp theme=\"united\" style=\"display:none;\"> %s </xmp><script src=\"http://ndossougbe.github.io/strapdown/dist/strapdown.js\"></script></html>" - (buffer-substring-no-properties (point-min) (point-max)))) + (format "<!DOCTYPE html><html><title>Impatient Markdown</title><xmp theme=\"united\" style=\"display:none;\"> %s </xmp><script>%s</script></html>" + (buffer-substring-no-properties (point-min) (point-max)) + (cj/markdown--strapdown-js))) (current-buffer))) ;; Bind the preview key after the defun so use-package's `:bind' autoload diff --git a/modules/prog-shell.el b/modules/prog-shell.el index d7f97932b..3ed51da11 100644 --- a/modules/prog-shell.el +++ b/modules/prog-shell.el @@ -166,8 +166,12 @@ Overrides default prog-mode keybindings with shell-specific commands." ;; Automatically set execute permission on shell scripts with shebangs (defun cj/make-script-executable () - "Make the current file executable if it has a shebang." + "Make the current file executable if it is a script buffer with a shebang. +Runs from a global `after-save-hook', so it gates on `prog-mode': a shebang in a +text, org, or fundamental-mode buffer (a script being read, quoted, or reviewed) +is left alone rather than silently made executable." (when (and buffer-file-name + (derived-mode-p 'prog-mode) (not (file-executable-p buffer-file-name)) (save-excursion (goto-char (point-min)) diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el index 4780ef227..cbd2c0d7e 100644 --- a/modules/undead-buffers.el +++ b/modules/undead-buffers.el @@ -87,12 +87,6 @@ Undead-buffers are buffers in `cj/undead-buffer-list'." (buffer-file-name buf) (buffer-modified-p buf)))) -(defun cj/save-some-buffers (&optional arg) - "Save some buffers, omitting those in `cj/undead-buffer-list'. -ARG is passed to `save-some-buffers'." - (interactive "P") - (save-some-buffers arg #'cj/undead-buffer-p)) - (defun cj/kill-buffer-and-window () "Delete window and kill or bury its buffer." (interactive) @@ -129,7 +123,7 @@ split is preserved. Buffers in `cj/undead-buffer-list' are buried." (defun cj/kill-all-other-buffers-and-windows () "Kill or bury all other buffers, then delete other windows." (interactive) - (cj/save-some-buffers) + (save-some-buffers nil #'cj/undead-buffer-p) (delete-other-windows) (mapc #'cj/kill-buffer-or-bury-alive (delq (current-buffer) (buffer-list)))) |
