aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/calendar-sync-recurrence.el20
-rw-r--r--modules/calendar-sync-source.el15
-rw-r--r--modules/calendar-sync.el18
-rw-r--r--modules/markdown-config.el28
-rw-r--r--modules/prog-shell.el6
-rw-r--r--modules/undead-buffers.el8
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))))