Blog

Last edit

Summary: bez kalendarza

Deleted:

< month:+0


For the English part of the blog, see Content AND Presentation.

2025-08-25 Counting Org mode headlines

One thing I need pretty often is to be able to count Org mode headlines in the subtree I’m in. It’s a bit surprising that Org does not know how to do that, but this is Emacs, so fixing it should be very easy, right? Indeed, it is – although it’s probably worth noting that the problem isn’t well-defined. Do I want to only count direct children of the current headline or all its descendants? At first I decided that what I need is the former, but then it dawned on me – why not both? And that way, the following command was born.

(defun org-count-headlines ()
  "Count Org Mode headlines one level below the current one."
  (interactive)
  (if-let* ((level (org-current-level)))
      (save-excursion
        (save-restriction
          (org-narrow-to-subtree)
          (message "%s has %s direct level %s children and %s descendants"
                   (org-get-heading t t t t)
                   (1- (how-many (org-headline-re (1+ level))
                                 (point-min) (point-max)))
                   (1+ level)
                   (1- (how-many org-outline-regexp-bol
                                 (point-min) (point-max))))))
    (message "%s level 1 headlines and %s total headlines"
             (how-many (org-headline-re 1)
                       (point-min) (point-max))
             (how-many org-outline-regexp-bol
                       (point-min) (point-max)))))

It’s a very simple – even simplistic – one, with maybe one or two non-obvious points. The org-current-level function returns nil when the point is before the first headline in the file, so I used if-let* to avoid trying to perform arithmetical operations on nil (which would result in an error). Another thing worth mentioning is the org-headline-re function, which – combined with org-narrow-to-subtree and how-many – allows to count headlines on level not exceeding its parameter without parsing the Org syntax. On the other hand, org-outline-regexp-bol matches a headline on any level.

That’s it for today, happy counting!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode

Comments on this page

2025-08-18 Cutting clips from videos with Emacs

I often want to show someone a clip from a video. When that someone is sitting next to me, I can just seek the right place in the video and hit “start”. But what if not? What if I’d like to upload the clip somewhere, or maybe even email it? Or just I don’t want to search for the right place, or risk showing too much of the video (which might contain spoilers, for example)?

Now, the first answer that comes to my mind is to extract the clip to a separate video file and play that instead. Of course, if you know me even a bit, you won’t be surprised that I’d like to do that from within the comfort of Emacs. While Emacs itself is not capable of playing videos, it can interface with mpv, and I already work with Subed mode and can drive video from it.

There are two things I need to be able to do what I want: select the beginning and end of the clip, and actually perform the trimming. The latter can be obviously achieved by ffmpeg, and while I don’t know what options I should give it to do that, I know that I’ve already solved that problem once. (Although it turned out that for some reason my solution stopped working – it still trims the video, but has some issues with embedding subtitles. I poked around and came up with another solution, which – instead of embedding the subtitles as a selectable track in the video – just hardcodes them on the image. This is not a good idea for generating a high-quality video with a movie or something, but is perfectly fine for just a short clip I’d like to share with someone. Also, I might want not to embed the subtitles at all – using C-u seems a natural way to achieve that. After a while I decided to flip it the other way round, so that using the subtitles won’t be embedded unless C-u is used.)

The only thing that remains is how to select the clip itself. I can easily pause the video at any desired moment (possibly even moving forward or backward by frame) and just tell Emacs “this is the place I want my clip to start/end”.

So, let’s get coding.

asking on the group:

(defvar subed-clip-beginning nil
  "The beginning position to clip.")

(defvar subed-clip-width 320
  "The width of the clips generated.")

(defun subed-clip-mark-begin ()
  "Make the current point the beginning of the clip."
  (interactive)
  (setq subed-clip-beginning subed-mpv-playback-position))

(defun subed-clip-mark-end (embed-subtitles)
  "Make the current point the end of the clip and perform the clipping.
With an interactive argument, embed the subtitles."
  (interactive "P")
  (let ((args `("-y"
                "-i" ,subed-mpv-media-file
                "-ss" ,(format "%dms" subed-clip-beginning)
                "-to" ,(format "%dms" subed-mpv-playback-position)
                "-filter:v"
                ,(format "%s%s"
                         (if embed-subtitles
                             (format "subtitles=%s,"
                                     (buffer-file-name))
                           "")
                         (format "scale=%d:trunc(ow/a/2)*2"
                                 subed-clip-width))
                "-c:v" "libx264"
                "-c:a" "copy"
                ,(file-name-concat "clip.mp4"))))
    (apply #'call-process "ffmpeg" nil nil nil args))
  (message "Clipping done."))

As you can see, these functions are very simplistic. I basically took the code I wrote a year ago and made it even simpler. There is no error checking – if you call subed-clip-mark-end when the current moment is before the one selected with subed-clip-mark-begin, the clipping just silently fails. This is, however, just a quick hack put together in 15 minutes, not a package ready for distribution. If you want similar functionality for yourself, just feel free to copy my code and modify it to suit your needs.

CategoryEnglish, CategoryBlog, CategoryEmacs, CategorySubtitles

Comments on this page

2025-08-11 Using Eldoc to show entities with given uuids in the echo area

This is the culmination of my series of posts on customization variables, timestamps, ElDoc, PostgreSQL and interacting with potentially unreliable external processes via stdin and stdout. All the time, my goal was to be able to show the database row corresponding to a uuid when the point is on that uuid. Since most of the hard work was already done in the preceding six posts, this one will be rather short (not including the code itself).

We already know how to write an ElDoc-compatible function which can check if the point is on some kind of entity and do something about this entity if yes. Last time it was easier, since the entity in question was a simple integer, and we could process it synchronously. This time the processing will be asynchronous – we’ll need to send the uuid to the program we’ve written earlier, and it is going to take some time to get the information we need from the database. One thing which makes this less nice than it could be is how ElDoc works with asynchronous code. The eldoc-documentation-functions hook must contain functions accepting a callback and, as the manual says, it should “arrange for CALLBACK to be called at a later time, using asynchronous processes or other asynchronous mechanisms”. The problem is, the code which will need to call that callback is the process filter, and the only way I know for the process filter to receive the callback is to pass it in some global variable. It’s very far from elegant and probably can give rise to some race conditions, when the user moves the point quickly between uuids, but I really don’t see any sensible alternative. Here is my solution.

(defvar show-row-by-uuid-callback nil
  "Callback to show the entity by uuid.")

(defun show-row-by-uuid-eldoc-function (callback)
  "An ElDoc-compatible wrapper around TODO."
  (when-let* ((uuid-at-point (thing-at-point 'uuid)))
    (setq show-row-by-uuid-callback callback)
    (show-row-by-uuid-send-input uuid-at-point)))

(define-minor-mode show-row-by-uuid-mode
  "Toggle ElDoc Timestamp Conversion mode.
When enabled, this mode causes numbers at point to be displayed as
timestamps in the echo area using ElDoc (which must be enabled, too).
Numbers greater than `maximum-unix-timestamp-for-conversion' are treated
as JavaScript `Date's and the rest as Unix timestamps (seconds since
1970-01-01)."
  :init-value nil
  :global t
  (if show-row-by-uuid-mode
      (progn
        (add-hook 'eldoc-documentation-functions
                  #'show-row-by-uuid-eldoc-function
                  nil nil)
        (show-row-by-uuid-start-process))
    (remove-hook 'eldoc-documentation-functions
                 #'show-row-by-uuid-eldoc-function
                 nil)
    (show-row-by-uuid-stop-process)))

(defvar show-row-by-uuid-process--process nil
  "The show-row-by-uuid process.")

(defvar show-row-by-uuid-process--auto-restart-p t
  "Non-nil means restart the show-row-by-uuid process after it dies.")

(defun show-row-by-uuid-process--sentinel (process event)
  "Restart the show-row-by-uuid process if it is dead."
  (when (and show-row-by-uuid-process--auto-restart-p
             (not (process-live-p show-row-by-uuid-process--process)))
    (message "Show-Row-By-Uuid process died, restarting.")
    (show-row-by-uuid-start-process)))

(defun show-row-by-uuid-start-process ()
  "Start `show-row-by-uuid.js' in the background unless already started."
  (interactive)
  (unless (process-live-p show-row-by-uuid-process--process)
    (setq show-row-by-uuid-process--auto-restart-p t)
    (setq show-row-by-uuid-process--process
          (start-process "show-row-by-uuid"
                         (get-buffer-create "*show-row-by-uuid*")
                         "show-row-by-uuid.js"))
    (set-marker-insertion-type
     (process-mark show-row-by-uuid-process--process) t)
    (setq show-row-by-uuid-process--output-start
          (copy-marker
           (process-mark show-row-by-uuid-process--process)))
    (set-process-filter show-row-by-uuid-process--process
                        #'show-row-by-uuid-process-filter)
    (set-process-sentinel show-row-by-uuid-process--process
                          #'show-row-by-uuid-process--sentinel)))

(defun show-row-by-uuid-stop-process ()
  "Stop the show-row-by-uuid process and do not restart it."
  (interactive)
  (setq show-row-by-uuid-process--auto-restart-p nil)
  (kill-process show-row-by-uuid-process--process))

(defun show-row-by-uuid-process--start-countdown ()
  "Count down to restart the show-row-by-uuid process when it hangs."
  (show-row-by-uuid-process--stop-countdown)
  (setq show-row-by-uuid-process--timeout-timer
        (run-with-timer show-row-by-uuid-process-timeout
                        nil
                        #'show-row-by-uuid-process--restart-process)))

(defun show-row-by-uuid-process--stop-countdown ()
  "Stop the countdown, see `show-row-by-uuid-process--start-countdown'."
  (when (timerp show-row-by-uuid-process--timeout-timer)
    (cancel-timer show-row-by-uuid-process--timeout-timer)))

(defun show-row-by-uuid-send-input (show-row-by-uuid-input)
  "Send INPUT to the show-row-by-uuid process."
  (interactive "sinput: \n")
  (if (process-live-p show-row-by-uuid-process--process)
      (with-current-buffer
          (process-buffer show-row-by-uuid-process--process)
        (goto-char (process-mark show-row-by-uuid-process--process))
        (insert show-row-by-uuid-input "\n")
        (setq show-row-by-uuid-process--output-start (point-marker))
        (process-send-string show-row-by-uuid-process--process
                             (concat show-row-by-uuid-input "\n"))
        (show-row-by-uuid-process--start-countdown))
    (user-error "show-row-by-uuid process is not alive")))

(defvar show-row-by-uuid-process--output-start nil
  "The place the current output should start.")

(defcustom show-row-by-uuid-process-timeout 2
  "The time (in seconds) to wait for output from `show-row-by-uuid.js'.")

(defvar show-row-by-uuid-process--timeout-timer nil
  "The timer to restart the show-row-by-uuid process when it times out.")

(defun show-row-by-uuid-process--restart-process ()
  "Restart the show-row-by-uuid process.
It does so by just killing the process; the actual restarting is handled
by the sentinel."
  (when (processp show-row-by-uuid-process--process)
    (kill-process show-row-by-uuid-process--process)))

(defun show-row-by-uuid-show-entity (entity-data)
  "Show ENTITY-DATA in the echo area.
Use `show-row-by-uuid-callback' if non-nil or `message' otherwise."
  (if show-row-by-uuid-callback
      (let ((eldoc-echo-area-use-multiline-p t)
            (print-length nil))
        (funcall show-row-by-uuid-callback
                 (format "%s" (plist-get entity-data :entity))
                 :thing (plist-get entity-data :table)
                 :face 'font-lock-keyword-face)
        (setq show-row-by-uuid-callback nil))
    (message "%s" entity-data)))

(defun show-row-by-uuid-process-filter (process output)
  "Insert OUTPUT to the buffer of PROCESS.
Also, message the user."
  (let ((buffer (process-buffer process)))
    (when (buffer-live-p buffer)
      (with-current-buffer buffer
        (save-excursion
          (goto-char (process-mark process))
          (insert output))
        (save-excursion
          (goto-char show-row-by-uuid-process--output-start)
          (if-let* ((output-end
                     (and (re-search-forward "^uuid:$" nil t)
                          (match-beginning 0))))
              (progn (goto-char show-row-by-uuid-process--output-start)
                     (condition-case error
                         (unless (bobp)
                           (show-row-by-uuid-show-entity
                            (json-parse-buffer :object-type 'plist)))
                       (json-parse-error
                        (message "invalid JSON received: %s"
                                 (buffer-substring-no-properties
                                  show-row-by-uuid-process--output-start
                                  (1- output-end)))))
                     (setq show-row-by-uuid-process--output-start
                           (1+ (match-end 0)))
                     (setq mark show-row-by-uuid-process--output-start)
                     (show-row-by-uuid-process--stop-countdown))
            (show-row-by-uuid-process--start-countdown)))))))

That code is still prototype-ish a bit, and I’m not even sure how to make it much better. For example, my mode does not automatically turn ElDoc mode on in JSON files opened before it was started. (The relation between ElDoc mode and Global ElDoc mode is still a bit mysterious to me, by the way.) The JSON provided by my script is formatted as a plist, which is probably not ideal, but works good enough for me.

Anyway, that’s it for now. I have some ideas how to make this work even better, but that will need to wait for another time.

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryPostgreSQL

Comments on this page

More...