[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/subed d8bf4ed 001/389: Initial commit
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/subed d8bf4ed 001/389: Initial commit |
Date: |
Fri, 3 Dec 2021 10:59:44 -0500 (EST) |
branch: elpa/subed
commit d8bf4ed5a7f5a48d20116e46e1a5eff7265800f1
Author: Random User <rndusr@posteo.de>
Commit: Random User <rndusr@posteo.de>
Initial commit
---
README.org | 61 ++++++
subed/subed-config.el | 226 ++++++++++++++++++++++
subed/subed-mpv.el | 292 +++++++++++++++++++++++++++++
subed/subed-srt.el | 397 +++++++++++++++++++++++++++++++++++++++
subed/subed.el | 447 +++++++++++++++++++++++++++++++++++++++++++
tests/test-subed-mpv.el | 139 ++++++++++++++
tests/test-subed-srt.el | 489 ++++++++++++++++++++++++++++++++++++++++++++++++
tests/test-subed.el | 96 ++++++++++
8 files changed, 2147 insertions(+)
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..0c03b67
--- /dev/null
+++ b/README.org
@@ -0,0 +1,61 @@
+* subed
+subed is an Emacs major mode for editing subtitles while playing the
+corresponding video with [[https://mpv.io/][mpv]]. At the moment, the only
supported format is
+SubRip ( ~.srt~).
+
+subed is still alpha software. Expect it to kill your Emacs session.
+
+** Features
+ - Quickly jump to next (~M-n~) and previous (~M-p~) subtitle text.
+ - Quickly jump to the beginning (~C-M-a~) and end (~C-M-e~) of the current
+ subtitle's text.
+ - Insert (~M-i~) and kill (~M-k~) subtitles.
+ - Adjust subtitle start (~M-[~ / ~M-]~) and stop (~M-{~ / ~M-}~) time
stamps.
+ - When a subtitle time is adjusted, jump to its start time in mpv (~C-x /~
to
+ toggle).
+ - Sort and re-number subtitles (~M-s~).
+ - Open videos with ~C-x C-v~ or automatically when entering subed-mode if
the
+ video file is named like the subtitle file but with a video extension
+ (e.g. ~.mkv~ or ~.avi~).
+ - Every time you safe (~C-x s~), the subtitles automatically sorted and
+ re-numbered and then reloaded in mpv.
+ - Synchronize point and playback position:
+ - mpv jumps automatically to the position of the subtitle on point as
point
+ moves between subtitles (~C-x ,~ to toggle).
+ - Point is automatically moved as the video is playing so that point is
+ always on the relevant subtitle (~C-x .~ to toggle).
+ - Loop over the subtitle on point in mpv (~C-x l~).
+ - Automatically pause or slow down playback in mpv while you are typing
(~C-x
+ p~ to toggle).
+
+** Installation
+ For now, you have to install it manually. For example, copy ~subed/*.el~ to
+ ~~/.emacs.d/elisp/~ and add ~~/.emacs.d/elisp/~ to your ~load-path~.
+
+ #+BEGIN_SRC elisp
+ (use-package subed
+ ;; Tell emacs where to find subed
+ :load-path "~/.emacs.d/elisp/"
+ :config
+ ;; Disable automatic movement of point
+ (add-hook 'subed-mode-hook 'subed-disable-sync-point-to-player)
+ ;; Break lines automatically while typing
+ (add-hook 'subed-mode-hook 'turn-on-auto-fill))
+ ;; Break lines at 50 characters
+ (add-hook 'subed-mode-hook (lambda () (setq-local fill-column 50)))
+ #+END_SRC
+
+** License
+ subed is free software: you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any later
+ version.
+
+ This program is distributed in the hope that it will be useful but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS
+ FOR A PARTICULAR PURPOSE. See the
[[https://www.gnu.org/licenses/gpl-3.0.txt][GNU General Public License]] for
more
+ details.
+
+#+STARTUP: showeverything
+#+OPTIONS: num:nil
+#+OPTIONS: ^:{}
diff --git a/subed/subed-config.el b/subed/subed-config.el
new file mode 100644
index 0000000..9b3ffe9
--- /dev/null
+++ b/subed/subed-config.el
@@ -0,0 +1,226 @@
+;;; subed-config.el --- Customization variables and hooks for subed
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+;;; Code:
+
+;; Key bindings
+
+(defvar subed-mode-map
+ (let ((subed-mode-map (make-keymap)))
+ (define-key subed-mode-map (kbd "M-n") 'subed-forward-subtitle-text)
+ (define-key subed-mode-map (kbd "M-p") 'subed-backward-subtitle-text)
+ (define-key subed-mode-map (kbd "C-M-a") 'subed-move-to-subtitle-text)
+ (define-key subed-mode-map (kbd "C-M-e") 'subed-move-to-subtitle-end)
+ (define-key subed-mode-map (kbd "M-[") 'subed-decrease-start-time-100ms)
+ (define-key subed-mode-map (kbd "M-]") 'subed-increase-start-time-100ms)
+ (define-key subed-mode-map (kbd "M-{") 'subed-decrease-stop-time-100ms)
+ (define-key subed-mode-map (kbd "M-}") 'subed-increase-stop-time-100ms)
+ (define-key subed-mode-map (kbd "M-i") 'subed-subtitle-insert)
+ (define-key subed-mode-map (kbd "M-k") 'subed-subtitle-kill)
+ (define-key subed-mode-map (kbd "M-s") 'subed-sort)
+ (define-key subed-mode-map (kbd "C-x C-v") 'subed-mpv-find-video)
+ (define-key subed-mode-map (kbd "M-SPC") 'subed-mpv-toggle-pause)
+ (define-key subed-mode-map (kbd "C-x .")
'subed-toggle-sync-point-to-player)
+ (define-key subed-mode-map (kbd "C-x ,")
'subed-toggle-sync-player-to-point)
+ (define-key subed-mode-map (kbd "C-x p") 'subed-toggle-pause-while-typing)
+ (define-key subed-mode-map (kbd "C-x l") 'subed-toggle-subtitle-loop)
+ (define-key subed-mode-map (kbd "C-x /")
'subed-toggle-replay-adjusted-subtitle)
+ ;; (define-key subed-mode-map (kbd "C-x [")
'subed-copy-subtitle-start-time)
+ ;; (define-key subed-mode-map (kbd "C-x ]") 'subed-copy-subtitle-stop-time)
+ (define-key subed-mode-map (kbd "C-x d") 'subed-toggle-debugging)
+ subed-mode-map)
+ "Keymap for subed-mode")
+
+
+;; Syntax highlighting
+
+(defface subed-srt-id-face
+ '((t (:foreground "sandybrown")))
+ "Each subtitle's consecutive number")
+
+(defface subed-srt-time-face
+ '((t (:foreground "skyblue")))
+ "Start and stop times of subtitles")
+
+(defface subed-srt-time-separator-face
+ '((t (:foreground "dimgray")))
+ "Separator between the start and stop time (\" --> \")")
+
+(defface subed-srt-text-face
+ '((t (:foreground "brightyellow")))
+ "Text of the subtitle")
+
+
+;; Variables
+
+(defgroup subed nil
+ "Major mode for editing subtitles."
+ :group 'languages
+ :group 'hypermedia
+ :prefix "subed-")
+
+(defvar-local subed--debug-enabled nil
+ "Whether `subed-debug' prints to `subed-debugging-buffer'.")
+
+(defcustom subed-debug-buffer "*subed-debug*"
+ "Name of the buffer that contains debugging messages."
+ :type 'string
+ :group 'subed)
+
+(defcustom subed-mode-hook nil
+ "Functions to call when entering subed mode."
+ :type 'hook
+ :group 'subed)
+
+(defcustom subed-video-extensions '("mkv" "mp4" "webm" "avi" "ts")
+ "Video file name extensions."
+ :type 'list
+ :group 'subed)
+
+(defcustom subed-auto-find-video t
+ "Whether to open the video automatically when opening a subtitle file.
+The corresponding video is found by replacing the file extension
+of `buffer-file-name' with those in `subed-video-extensions'.
+The first existing file is then passed to `subed-open-video'."
+ :type 'boolean
+ :group 'subed)
+
+
+(defvar subed-subtitle-time-adjusted-hook ()
+ "Functions to call when a subtitle's start or stop time has
+changed.")
+
+
+(defcustom subed-playback-speed-while-typing 0.3
+ "Video playback speed while the user is editing the buffer. If
+set to zero or smaller, playback is paused."
+ :type 'float
+ :group 'subed)
+
+(defcustom subed-playback-speed-while-not-typing 1.0
+ "Video playback speed while the user is not editing the
+buffer."
+ :type 'float
+ :group 'subed)
+
+(defcustom subed-unpause-after-typing-delay 1.0
+ "Number of seconds to wait after typing stopped before
+unpausing the player."
+ :type 'float
+ :group 'subed)
+
+(defvar-local subed--unpause-after-typing-timer nil
+ "Timer that waits before unpausing the player after the user
+typed something.")
+
+(defvar-local subed--player-is-auto-paused nil
+ "Whether the player was paused by the user or automatically.")
+
+
+(defcustom subed-loop-seconds-before 0
+ "When looping over a single subtitle, start the loop this many
+seconds before the subtitle starts."
+ :type 'float
+ :group 'subed)
+
+(defcustom subed-loop-seconds-after 0
+ "When looping over a single subtitle, end the loop this many
+seconds after the subtitle stop."
+ :type 'float
+ :group 'subed)
+
+(defvar-local subed--subtitle-loop-start nil
+ "Start position of loop in player in milliseconds.")
+
+(defvar-local subed--subtitle-loop-stop nil
+ "Stop position of loop in player in milliseconds.")
+
+
+(defcustom subed-point-sync-delay-after-motion 1.0
+ "Number of seconds the player can't adjust point after point
+was moved by the user."
+ :type 'float
+ :group 'subed)
+
+(defvar-local subed--point-sync-delay-after-motion-timer nil
+ "Timer that waits before re-adding
+`subed--sync-point-to-player' after temporarily removing it.")
+
+(defvar-local subed--point-was-synced nil
+ "When temporarily disabling point-to-player sync, this variable
+remembers whether it was originally enabled by the user.")
+
+
+(defcustom subed-mpv-socket "/tmp/mpv-socket"
+ "Path to Unix IPC socket that is passed to mpv --input-ipc-server."
+ :type 'file
+ :group 'subed)
+
+(defcustom subed-mpv-executable "mpv"
+ "Path or filename of mpv executable."
+ :type 'file
+ :group 'subed)
+
+(defcustom subed-mpv-arguments '("--osd-level" "2" "--osd-fractions")
+ "Additional arguments for \"mpv\".
+The options --input-ipc-server=SRTEDIT-MPV-SOCKET and --idle are
+hardcoded."
+ :type '(repeat string)
+ :group 'subed)
+
+
+;; Hooks
+
+(defvar-local subed-point-motion-hook nil
+ "Functions to call after point changed.")
+
+(defvar-local subed-subtitle-motion-hook nil
+ "Functions to call after current subtitle changed.")
+
+(defvar-local subed--status-point 1
+ "Keeps track of `(point)' to detect changes.")
+
+(defvar-local subed--status-subtitle-id 1
+ "Keeps track of `(subed--subtitle-id)' to detect changes.")
+
+(defun subed--post-command-handler ()
+ "Detect point motion and user entering text and signal hooks."
+ ;; Check for point motion first; skip checking for other changes if it didn't
+ (let ((new-point (point)))
+ (when (and new-point subed--status-point
+ (not (= new-point subed--status-point)))
+
+ ;; If point is synced to playback position, temporarily disable that to
+ ;; prevent race conditions that make the cursor doesn't move
unexpectedly.
+ (subed-disable-sync-point-to-player-temporarily)
+
+ (setq subed--status-point new-point)
+ ;; Signal point motion
+ (run-hooks 'subed-point-motion-hook)
+ (let ((new-sub-id (subed--subtitle-id)))
+ (when (and new-sub-id subed--status-subtitle-id
+ (not (= subed--status-subtitle-id new-sub-id)))
+ (setq subed--status-subtitle-id new-sub-id)
+ ;; Signal motion between subtitles
+ (run-hooks 'subed-subtitle-motion-hook))))))
+
+(provide 'subed-config)
+;;; subed-config.el ends here
diff --git a/subed/subed-mpv.el b/subed/subed-mpv.el
new file mode 100644
index 0000000..e1aab24
--- /dev/null
+++ b/subed/subed-mpv.el
@@ -0,0 +1,292 @@
+;;; subed-mpv.el --- mpv integration for subed
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+;;; Commentary:
+;;
+;; Based on:
+;; https://github.com/mk-fg/emacs-setup/blob/master/extz/emms-player-mpv.el
+
+;;; Code:
+
+(require 'json)
+
+(defvar-local subed-mpv-is-playing nil
+ "Whether mpv is currently playing or paused.")
+
+(defvar-local subed-mpv-playback-speed nil
+ "How fast mpv is playing the video.
+1.0 is normal speed, 0.5 is half speed, etc.")
+
+(defvar-local subed-mpv-playback-position nil
+ "Current playback position in milliseconds.")
+
+(defvar-local subed-mpv-playback-position-hook nil
+ "Functions to call when mpv changes playback position.")
+
+(defvar-local subed-mpv--server-proc nil
+ "Running mpv process.")
+
+(defvar-local subed-mpv--client-proc nil
+ "IPC socket process that communicates over `subed-mpv-socket'.")
+
+(defvar-local subed-mpv--client-buffer " *subed-mpv-buffer*"
+ "Buffer for JSON responses from server.")
+
+(defconst subed-mpv--client-test-request
+ (json-encode (list :command '(get_property mpv-version)))
+ "Request as a string to send to check whether IPC connection is working.")
+
+(defconst subed-mpv--retry-delays
+ ;; Sums up to 5 seconds in total before failing
+ '(0.1 0.1 0.1 0.1 0.2 0.2 0.3 0.4 0.5 0.5 0.5 0.5 0.5 0.5 0.5)
+ "List of delays between attemps to connect to `subed-mpv-socket'.")
+
+(defvar-local subed-mpv--client-command-queue nil
+ "Commands to call when connection to `subed-mpv-socket' is established.")
+
+
+;;; Server (mpv process that provides an IPC socket)
+
+(defun subed-mpv--server-start (&rest args)
+ "Run mpv in JSON IPC mode."
+ (subed-mpv--server-stop)
+ (let ((argv (append (list subed-mpv-executable
+ (format "--input-ipc-server=%s" subed-mpv-socket)
+ "--idle")
+ args)))
+ (subed-debug "Running %s" argv)
+ (condition-case err
+ (setq subed-mpv--server-proc (make-process :command argv
+ :name "subed-mpv-server"
+ :buffer nil
+ :noquery t))
+ (error
+ (error "%s" (mapconcat 'identity (cdr (cdr err)) ": "))))))
+
+(defun subed-mpv--server-stop ()
+ "Kill a running mpv process."
+ (when (and subed-mpv--server-proc (process-live-p subed-mpv--server-proc))
+ (delete-process subed-mpv--server-proc)
+ (subed-debug "Killed mpv process"))
+ (setq subed-mpv--server-proc nil))
+
+(defun subed-mpv--server-started-p ()
+ "Whether `subed-mpv--server-proc' is a running process."
+ (if subed-mpv--server-proc t nil))
+
+
+;;; Client (elisp process that connects to server's IPC socket)
+
+(defun subed-mpv--client-connect (delays)
+ "Try to connect to `subed-mpv-socket'.
+If a connection attempt fails, wait (car delays) seconds and try
+again, passing (cdr delays)."
+ (subed-debug "Attempting to connect to IPC socket: %s" subed-mpv-socket)
+ (subed-mpv--client-disconnect)
+ ;; NOTE: make-network-process doesn't fail when the socket file doesn't exist
+ (let ((proc (make-network-process :name "subed-mpv-client"
+ :family 'local
+ :service subed-mpv-socket
+ :coding '(utf-8 . utf-8)
+ :buffer
(get-buffer-create subed-mpv--client-buffer)
+ :filter #'subed-mpv--client-filter
+ :noquery t
+ :nowait t)))
+ ;; Test connection by sending a test request
+ (condition-case err
+ (progn
+ (process-send-string proc (concat subed-mpv--client-test-request
"\n"))
+ (subed-debug "Connected to %s (%s)" proc (process-status proc))
+ (setq subed-mpv--client-proc proc))
+ (error
+ (if delays
+ (progn
+ (subed-debug "Failed to connect (trying again in %s seconds)"
(car delays))
+ (run-at-time (car delays) nil #'subed-mpv--client-connect (cdr
delays)))
+ (progn
+ (subed-debug "Connection failed: %s" err))))))
+ ;; Run commands that were sent while the connection wasn't up yet
+ (when (subed-mpv--client-connected-p)
+ (while subed-mpv--client-command-queue
+ (let ((cmd (pop subed-mpv--client-command-queue)))
+ (subed-debug "Running queued command: %s" cmd)
+ (apply 'subed-mpv--client-send (list cmd))))))
+
+(defun subed-mpv--client-disconnect ()
+ "Close connection to mpv process, if there is one."
+ (when (subed-mpv--client-connected-p)
+ (delete-process subed-mpv--client-proc)
+ (subed-debug "Closed connection to mpv process"))
+ (setq subed-mpv--client-proc nil
+ subed-mpv-is-playing nil
+ subed-mpv-playback-position nil))
+
+(defun subed-mpv--client-connected-p ()
+ "Whether the server connection has been established and tested successfully."
+ (if subed-mpv--client-proc t nil))
+
+(defun subed-mpv--client-send (cmd)
+ "Send JSON IPC command.
+If we're not connected yet but the server has been started, add
+CMD to `subed-mpv--client-command-queue' which is evaluated by
+`subed-mpv--client-connect' when the connection is up."
+ (if (subed-mpv--client-connected-p)
+ (let ((request-data (concat (json-encode (list :command cmd)))))
+ (subed-debug "Sending request: %s" request-data)
+ (condition-case err
+ (process-send-string subed-mpv--client-proc (concat request-data
"\n"))
+ (error
+ (subed-mpv-kill)
+ (error "Unable to send commands via %s: %s" subed-mpv-socket (cdr
err))))
+ t)
+ (when (subed-mpv--server-started-p)
+ (subed-debug "Queueing command: %s" cmd)
+ (setq subed-mpv--client-command-queue (append
subed-mpv--client-command-queue (list cmd)))
+ t)))
+
+(defun subed-mpv--client-filter (proc response)
+ "Handle response from the server."
+ ;; JSON-STRING contains zero or more lines with JSON encoded objects, e.g.:
+ ;; {"data":"mpv 0.29.1","error":"success"}
+ ;; {"data":null,"request_id":1,"error":"success"}
+ ;; {"event":"start-file"}{"event":"tracks-changed"}
+ ;; JSON-STRING cal also contain incomplete JSON at the end,
+ ;; e.g. `{"error":"succ'. Therefore we maintain a buffer and process only
+ ;; complete lines.
+ (when (buffer-live-p (process-buffer proc))
+ (let ((orig-buffer (current-buffer)))
+ (with-current-buffer (process-buffer proc)
+ ;; Insert new response where previous response ended
+ (let ((moving (= (point) (process-mark proc))))
+ (save-excursion
+ (goto-char (process-mark proc))
+ (insert response)
+ (set-marker (process-mark proc) (point)))
+ (if moving (goto-char (process-mark proc))))
+ ;; Process and remove all complete lines of JSON
+ (let ((p0 (point-min)))
+ (while (progn (goto-char p0)
+ (end-of-line)
+ (equal (following-char) ?\n))
+ (let* ((p1 (point))
+ (line (buffer-substring p0 p1)))
+ (delete-region p0 (+ p1 1))
+ ;; Return context to the subtitle file buffer because we're using
+ ;; buffer-local variables to store player state.
+ (with-current-buffer orig-buffer
+ (subed-mpv--client-handle-json line)))))))))
+
+(defun subed-mpv--client-handle-json (json-string)
+ "Process a single JSON object from the server."
+ (let* ((json-data (condition-case err
+ (json-read-from-string json-string)
+ (error
+ (subed-debug "Unable to parse JSON response:\n%S"
json-string))))
+ (event (alist-get 'event json-data)))
+ (when event
+ (subed-mpv--handle-event json-data))))
+
+(defun subed-mpv--handle-event (json-data)
+ "Handler for relevant mpv events.
+See \"List of events\" in mpv(1)."
+ (let ((event (alist-get 'event json-data)))
+ (pcase event
+ ("property-change"
+ (when (string= (alist-get 'name json-data) "time-pos")
+ (let ((pos-msecs (* 1000 (alist-get 'data json-data))))
+ (setq subed-mpv-playback-position pos-msecs)
+ (run-hook-with-args 'subed-mpv-playback-position-hook pos-msecs))))
+ ((or "unpause" "file-loaded")
+ (setq subed-mpv-is-playing t)
+ (subed-debug "Playing status changed: playing=%s" subed-mpv-is-playing))
+ ((or "pause" "end-file" "shutdown" "idle")
+ (setq subed-mpv-is-playing nil)
+ (subed-debug "Playing status changed: playing=%s"
subed-mpv-is-playing)))))
+
+
+;;; High-level functions
+
+(defun subed-mpv-pause ()
+ "Stop playback."
+ (interactive)
+ (when (eq subed-mpv-is-playing t)
+ (when (subed-mpv--client-send `(set_property pause yes))
+ (subed-mpv--handle-event '((event . "pause"))))))
+
+(defun subed-mpv-unpause ()
+ "Start playback."
+ (interactive)
+ (when (eq subed-mpv-is-playing nil)
+ (when (subed-mpv--client-send `(set_property pause no))
+ (subed-mpv--handle-event '((event . "unpause"))))))
+
+(defun subed-mpv-toggle-pause ()
+ "Start or stop playback."
+ (interactive)
+ (if subed-mpv-is-playing (subed-mpv-pause) (subed-mpv-unpause)))
+
+(defun subed-mpv-playback-speed (factor)
+ "Play video slower (FACTOR < 1) or faster (FACTOR > 1)."
+ (interactive)
+ (when (not (eq subed-mpv-playback-speed factor))
+ (when (subed-mpv--client-send `(set_property speed ,factor))
+ (setq subed-mpv-playback-speed factor))))
+
+(defun subed-mpv-seek (msec)
+ "Move playback position SEC seconds relative to current position."
+ (subed-mpv--client-send `(seek ,(/ msec 1000.0) relative+exact)))
+
+(defun subed-mpv-jump (msec)
+ "Move playback position to absolute position SEC seconds."
+ (subed-mpv--client-send `(seek ,(/ msec 1000.0) absolute+exact)))
+
+(defun subed-mpv-reload-subtitles ()
+ "Reload subtitle file from disk."
+ (subed-mpv--client-send '(sub-reload)))
+
+(defun subed-mpv--is-video-file-p (filename)
+ "Return whether FILENAME is a video file or directory."
+ (and (not (or (string= filename ".") (string= filename "..")))
+ (let ((filepath (expand-file-name filename)))
+ (or (file-directory-p filepath)
+ (member (file-name-extension filename) subed-video-extensions)))))
+
+(defun subed-mpv-find-video (file)
+ "Open video file in mpv.
+Video files are expected to have any of the extensions listed in
+`subed-video-extensions'."
+ (interactive (list (read-file-name "Find video: " nil nil t nil
'subed-mpv--is-video-file-p)))
+ (let ((filepath (expand-file-name file)))
+ (when (apply 'subed-mpv--server-start subed-mpv-arguments)
+ (subed-debug "Opening video file: %s" filepath)
+ (subed-mpv--client-connect subed-mpv--retry-delays)
+ (subed-mpv--client-send `(loadfile ,filepath replace))
+ (subed-mpv--client-send `(sub-add ,(buffer-file-name) select))
+ (subed-mpv--client-send `(observe_property 1 time-pos))
+ (subed-mpv-playback-speed subed-playback-speed-while-not-typing))))
+
+(defun subed-mpv-kill ()
+ "Close connection to mpv process and kill the process."
+ (subed-mpv--client-disconnect)
+ (subed-mpv--server-stop))
+
+(provide 'subed-mpv)
+;;; subed-mpv.el ends here
diff --git a/subed/subed-srt.el b/subed/subed-srt.el
new file mode 100644
index 0000000..272750f
--- /dev/null
+++ b/subed/subed-srt.el
@@ -0,0 +1,397 @@
+;;; subed-srt.el --- SubRip/srt implementation for subed
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+;;; Code:
+
+;;; Syntax highlighting
+
+(defconst subed-srt-font-lock-keywords
+ (list
+ '("^[0-9]+$" . 'subed-srt-id-face)
+ '("[0-9]+:[0-9]+:[0-9]+,[0-9]+" . 'subed-srt-time-face)
+ '(",[0-9]+ \\(-->\\) [0-9]+:" 1 'subed-srt-time-separator-face t)
+ '("^.*$" . 'subed-srt-text-face))
+ "Highlighting expressions for subed-mode")
+
+
+;;; Parsing
+
+(defconst subed-srt--regexp-timestamp
"\\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\),\\([0-9]+\\)")
+(defconst subed-srt--regexp-duration (concat subed-srt--regexp-timestamp "[
]+\\(-->\\)[ ]+"
+ subed-srt--regexp-timestamp))
+(defconst subed-srt--regexp-separator "\\([[:blank:]]*\n\\)+[[:blank:]]*\n")
+(defconst subed-srt--length-timestamp 12) ;; String length of "01:45:32,091"
+
+(defun subed-srt--timestamp-to-msecs (time-string)
+ "Find HH:MM:SS,MS pattern in TIME-STRING and convert it to milliseconds.
+Return nil if TIME-STRING doesn't match the pattern."
+ (when (string-match subed-srt--regexp-timestamp time-string)
+ (let ((hours (string-to-number (match-string 1 time-string)))
+ (mins (string-to-number (match-string 2 time-string)))
+ (secs (string-to-number (match-string 3 time-string)))
+ (msecs (string-to-number (match-string 4 time-string))))
+ (+ (* (truncate hours) 3600000)
+ (* (truncate mins) 60000)
+ (* (truncate secs) 1000)
+ (truncate msecs)))))
+
+(defun subed-srt--msecs-to-timestamp (msecs)
+ "Convert MSECS to string in the format HH:MM:SS,MS."
+ (concat (format-seconds "%02h:%02m:%02s" (/ msecs 1000))
+ "," (format "%03d" (mod msecs 1000))))
+
+(defun subed-srt--subtitle-id ()
+ "Return the ID of subtitle at point or nil if there is no ID."
+ (save-excursion
+ (when (subed-srt-move-to-subtitle-id)
+ (string-to-number (current-word)))))
+
+(defun subed-srt--subtitle-id-at-msecs (msecs)
+ "Return the ID of the subtitle at MSECS milliseconds.
+If MSECS is between subtitles, return the subtitle that starts
+after MSECS if there is one and its start time is >= MSECS +
+1000. Otherwise return the closest subtitle before MSECS."
+ (save-excursion
+ (goto-char (point-min))
+ (let* ((secs (/ msecs 1000))
+ (only-hours (truncate (/ secs 3600)))
+ (only-mins (truncate (/ (- secs (* only-hours 3600)) 60))))
+ ;; Move to first subtitle in the relevant hour
+ (when (re-search-forward (format "\\(\n\n\\|\\`\\)[0-9]+\n%02d:"
only-hours) nil t)
+ (beginning-of-line)
+ ;; Move to first subtitle in the relevant hour and minute
+ (re-search-forward (format "\\(\n\n\\|\\`\\)[0-9]+\n%02d:%02d"
only-hours only-mins) nil t)))
+ ;; Move to first subtitle that starts at or after MSECS
+ (catch 'last-subtitle-reached
+ (while (<= (subed-srt--subtitle-msecs-start) msecs)
+ (unless (subed-srt-forward-subtitle-id)
+ (throw 'last-subtitle-reached nil))))
+ ;; Move back to previous subtitle if start of current subtitle is in the
+ ;; future (i.e. MSECS is between subtitles)
+ (when (> (subed-srt--subtitle-msecs-start) msecs)
+ (subed-srt-backward-subtitle-id))
+ (subed-srt--subtitle-id)))
+
+(defun subed-srt--subtitle-msecs-start (&optional sub-id)
+ "Subtitle start time in milliseconds."
+ (let ((timestamp (save-excursion
+ (subed-srt-move-to-subtitle-time-start sub-id)
+ (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)))))
+ (subed-srt--timestamp-to-msecs timestamp)))
+
+(defun subed-srt--subtitle-msecs-stop (&optional sub-id)
+ "Subtitle stop time in milliseconds."
+ (let ((timestamp (save-excursion
+ (subed-srt-move-to-subtitle-time-stop sub-id)
+ (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)))))
+ (subed-srt--timestamp-to-msecs timestamp)))
+
+(defun subed-srt--subtitle-relative-point ()
+ "Point relative to subtitle's ID, i.e. point within subtitle."
+ (let ((start-point (save-excursion
+ (progn (subed-srt-move-to-subtitle-id)
+ (point)))))
+ (- (point) start-point)))
+
+
+;;; Traversing
+
+(defun subed-srt-move-to-subtitle-id (&optional sub-id)
+ "Move to the ID of a subtitle and return point.
+If SUBTITLE-ID is not given, focus the current subtitle's ID.
+Return point or nil if no subtitle ID could be found."
+ (interactive)
+ (if sub-id
+ (progn
+ ;; Start on the first ID and search forward for a line that contains
+ ;; only the ID, preceded by one or more blank lines.
+ (save-excursion
+ (goto-char (point-min))
+ (setq regex (format
"\\(\\([[:blank:]]*\n\\)+[[:blank:]]*\n\\|\\`\\)%d$" sub-id))
+ (setq match-found (re-search-forward regex nil t)))
+ (when match-found
+ (goto-char (match-end 0))
+ (beginning-of-line)
+ (point)))
+ (progn
+ ;; Find one or more blank lines.
+ (re-search-forward "\\([[:blank:]]*\n\\)+" nil t)
+ ;; Find two or more blank lines or the beginning of the buffer, followed
+ ;; by line composed of only digits.
+ (re-search-backward (concat "\\(" subed-srt--regexp-separator
"\\|\\`\\)[0-9]+$") nil t)
+ (goto-char (match-end 0))
+ (beginning-of-line)
+ (when (looking-at "^\\([0-9]+\\)$")
+ (point)))))
+
+(defun subed-srt-move-to-subtitle-time-start (&optional sub-id)
+ "Move point to subtitle's start time.
+Return point or nil if no start time could be found."
+ (interactive)
+ (when (subed-srt-move-to-subtitle-id sub-id)
+ (forward-line)
+ (when (looking-at subed-srt--regexp-timestamp)
+ (point))))
+
+(defun subed-srt-move-to-subtitle-time-stop (&optional sub-id)
+ "Move point to subtitle's stop time.
+Return point or nil if no stop time could be found."
+ (interactive)
+ (when (subed-srt-move-to-subtitle-id sub-id)
+ (search-forward " --> " nil t)
+ (when (looking-at subed-srt--regexp-timestamp)
+ (point))))
+
+(defun subed-srt-move-to-subtitle-text (&optional sub-id)
+ "Move point on the first character of subtitle's text.
+Return point."
+ (interactive)
+ (when (subed-srt-move-to-subtitle-id sub-id)
+ (forward-line 2)
+ (point)))
+
+(defun subed-srt-move-to-subtitle-id-at-msecs (msecs)
+ "Move point to the ID of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-srt--subtitle-id-at-msecs'."
+ (let ((current-sub-id (subed-srt--subtitle-id))
+ (target-sub-id (subed-srt--subtitle-id-at-msecs msecs)))
+ (when (and target-sub-id current-sub-id (not (= target-sub-id
current-sub-id)))
+ (subed-srt-move-to-subtitle-id target-sub-id))))
+
+(defun subed-srt-move-to-subtitle-text-at-msecs (msecs)
+ "Move point to the text of the subtitle that is playing at MSECS.
+Return point or nil if point is still on the same subtitle.
+See also `subed-srt--subtitle-id-at-msecs'."
+ (when (subed-srt-move-to-subtitle-id-at-msecs msecs)
+ (subed-srt-move-to-subtitle-text)))
+
+(defun subed-srt-move-to-subtitle-end (&optional sub-id)
+ "Move point after the last character of the subtitle's text.
+Return point unless point did not change."
+ (interactive)
+ (when (not (looking-at "\\([[:blank:]]*\n\\)*\\'"))
+ (subed-srt-move-to-subtitle-text sub-id)
+ (re-search-forward (concat "\\(" subed-srt--regexp-separator
"\\|\\([[:blank:]]*\n\\)+\\'\\)") nil t)
+ (goto-char (match-beginning 0))))
+
+(defun subed-srt-forward-subtitle-id ()
+ "Move point to next subtitle's ID.
+Return new point or nil if point didn't change (e.g. if called on
+the last subtitle)."
+ (interactive)
+ (when (re-search-forward (concat subed-srt--regexp-separator "[[:alnum:]]")
nil t)
+ (subed-srt-move-to-subtitle-id)))
+
+(defun subed-srt-backward-subtitle-id ()
+ "Move point to previous subtitle's ID.
+Return new point or nil if point didn't change (e.g. if called on
+the first subtitle)."
+ (interactive)
+ (when (re-search-backward subed-srt--regexp-separator nil t)
+ (subed-srt-move-to-subtitle-id)))
+
+(defun subed-srt-forward-subtitle-text ()
+ "Move point to next subtitle's text.
+Return new point"
+ (interactive)
+ (subed-srt-forward-subtitle-id)
+ (subed-srt-move-to-subtitle-text))
+
+(defun subed-srt-backward-subtitle-text ()
+ "Move point to previous subtitle's text"
+ (interactive)
+ (subed-srt-backward-subtitle-id)
+ (subed-srt-move-to-subtitle-text))
+
+(defun subed-srt-forward-subtitle-time-start ()
+ "Move point to next subtitle's start time."
+ (interactive)
+ (subed-srt-forward-subtitle-id)
+ (subed-srt-move-to-subtitle-time-start))
+
+(defun subed-srt-backward-subtitle-time-start ()
+ "Move point to previous subtitle's start time."
+ (interactive)
+ (subed-srt-backward-subtitle-id)
+ (subed-srt-move-to-subtitle-time-start))
+
+(defun subed-srt-forward-subtitle-time-stop ()
+ "Move point to next subtitle's stop time."
+ (interactive)
+ (subed-srt-forward-subtitle-id)
+ (subed-srt-move-to-subtitle-time-stop))
+
+(defun subed-srt-backward-subtitle-time-stop ()
+ "Move point to previous subtitle's stop time."
+ (interactive)
+ (subed-srt-backward-subtitle-id)
+ (subed-srt-move-to-subtitle-time-stop))
+
+
+;;; Manipulation
+
+(defun subed-srt--adjust-subtitle-start-relative (msecs)
+ "Add MSECS milliseconds to start time (use negative value to subtract)."
+ (let ((msecs-new (+ (subed-srt--subtitle-msecs-start) msecs)))
+ (save-excursion
+ (subed-srt-move-to-subtitle-time-start)
+ (delete-region (point) (+ (point) subed-srt--length-timestamp))
+ (insert (subed-srt--msecs-to-timestamp msecs-new)))
+ (when subed-subtitle-time-adjusted-hook
+ (let ((sub-id (subed-srt--subtitle-id)))
+ (run-hook-with-args 'subed-subtitle-time-adjusted-hook sub-id
msecs-new)))))
+
+(defun subed-srt--adjust-subtitle-stop-relative (msecs)
+ "Add MSECS milliseconds to stop time (use negative value to subtract)."
+ (let ((msecs-new (+ (subed-srt--subtitle-msecs-stop) msecs)))
+ (save-excursion
+ (subed-srt-move-to-subtitle-time-stop)
+ (delete-region (point) (+ (point) subed-srt--length-timestamp))
+ (insert (subed-srt--msecs-to-timestamp msecs-new)))
+ (when subed-subtitle-time-adjusted-hook
+ (let ((sub-id (subed-srt--subtitle-id)))
+ (run-hook-with-args 'subed-subtitle-time-adjusted-hook sub-id
msecs-new)))))
+
+(defun subed-srt-increase-start-time-100ms ()
+ "Add 100 milliseconds to start time of current subtitle."
+ (interactive)
+ (subed-srt--adjust-subtitle-start-relative 100))
+
+(defun subed-srt-decrease-start-time-100ms ()
+ "Subtract 100 milliseconds from start time of current subtitle."
+ (interactive)
+ (subed-srt--adjust-subtitle-start-relative -100))
+
+(defun subed-srt-increase-stop-time-100ms ()
+ "Add 100 milliseconds to stop time of current subtitle."
+ (interactive)
+ (subed-srt--adjust-subtitle-stop-relative 100))
+
+(defun subed-srt-decrease-stop-time-100ms ()
+ "Subtract 100 milliseconds from stop time of current subtitle."
+ (interactive)
+ (subed-srt--adjust-subtitle-stop-relative -100))
+
+;; TODO: Write tests
+;; TODO: Implement support for prefix argument to
+;; - insert n subtitles with C-u n M-i.
+;; - insert 1 subtitle before the current one with C-u M-i.
+(defun subed-srt-subtitle-insert ()
+ "Insert a subtitle after the current."
+ (interactive)
+ (let ((start-time (+ (subed-srt--subtitle-msecs-stop) 100))
+ (stop-time (- (save-excursion
+ (subed-srt-forward-subtitle-id)
+ (subed-srt--subtitle-msecs-start)) 100)))
+ (subed-srt-forward-subtitle-id)
+ (insert (format "1\n%s --> %s\n\n\n"
+ (subed-srt--msecs-to-timestamp start-time)
+ (subed-srt--msecs-to-timestamp stop-time))))
+ (previous-line 2))
+
+;; TODO: Implement support for prefix argument to
+;; kill n subtitles with C-u n M-k.
+(defun subed-srt-subtitle-kill ()
+ "Remove subtitle at point."
+ (interactive)
+ (let ((beg (save-excursion
+ (subed-srt-move-to-subtitle-id)
+ (point)))
+ (end (save-excursion
+ (subed-srt-move-to-subtitle-id)
+ (when (subed-srt-forward-subtitle-id)
+ (point)))))
+ (if (not end)
+ (progn
+ (let ((beg (save-excursion
+ (goto-char beg)
+ (subed-srt-backward-subtitle-text)
+ (subed-srt-move-to-subtitle-end)
+ (1+ (point))))
+ (end (save-excursion
+ (goto-char (point-max)))))
+ (delete-region beg end)))
+ (progn
+ (delete-region beg end)))))
+
+
+;;; Maintenance
+
+(defun subed-srt--regenerate-ids ()
+ "Ensure subtitle IDs start at 1 and are incremented by 1 for
+each subtitle."
+ (save-excursion
+ (goto-char (point-min))
+ (let ((id 1))
+ (while (looking-at "^[0-9]+$")
+ (kill-word 1)
+ (insert (format "%d" id))
+ (setq id (1+ id))
+ (subed-srt-forward-subtitle-id)))))
+
+(defun subed-srt-sanitize ()
+ "Remove surplus newlines and whitespace"
+ (interactive)
+ (subed--save-excursion
+ ;; Remove trailing whitespace from lines and empty lines from end of buffer
+ (delete-trailing-whitespace (point-min) nil)
+
+ ;; Remove leading whitespace lines
+ (goto-char (point-min))
+ (while (re-search-forward "^[[:blank:]]+" nil t)
+ (replace-match ""))
+
+ ;; Remove excessive newlines between subtitles
+ (goto-char (point-min))
+ (while (re-search-forward subed-srt--regexp-separator nil t)
+ (replace-match "\n\n"))
+
+ ;; Remove any newlines from beginning of buffer
+ (goto-char (point-min))
+ (while (re-search-forward "\\`\n+" nil t)
+ (replace-match ""))
+
+ ;; Ensure single newline at end of buffer
+ (goto-char (point-max))
+ (when (not (looking-back "\n"))
+ (insert "\n"))
+ ))
+
+(defun subed-srt-sort ()
+ "Sanitize, then sort subtitles by start time and re-number them."
+ (interactive)
+ (subed-srt-sanitize)
+ (subed--save-excursion
+ (goto-char (point-min))
+ (sort-subr nil
+ ;; nextrecfun (move to next record/subtitle or to end-of-buffer
+ ;; if there are no more records)
+ (lambda () (unless (subed-srt-forward-subtitle-id)
+ (goto-char (point-max))))
+ ;; endrecfun (move to end of current record/subtitle)
+ 'subed-srt-move-to-subtitle-end
+ ;; startkeyfun (return sort value of current record/subtitle)
+ 'subed-srt--subtitle-msecs-start))
+ (subed-srt--regenerate-ids))
+
+(provide 'subed-srt)
+;;; subed-srt.el ends here
diff --git a/subed/subed.el b/subed/subed.el
new file mode 100644
index 0000000..e14c1e4
--- /dev/null
+++ b/subed/subed.el
@@ -0,0 +1,447 @@
+;;; subed.el --- A major mode for editing SubRip (srt) subtitles
+
+;;; License:
+;;
+;; This file is not part of GNU Emacs.
+;;
+;; This is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+;;
+;;
+;; See README.org or https://github.com/rndusr/subed for more information.
+;;
+;;
+;;; Code:
+
+(add-to-list 'auto-mode-alist '("\\.srt$" . subed-mode))
+
+(require 'subed-config)
+(require 'subed-srt)
+(require 'subed-mpv)
+
+;; Abstraction layer to allow support for other subtitle formats
+(set 'subed-font-lock-keywords 'subed-srt-font-lock-keywords)
+
+(fset 'subed--subtitle-id 'subed-srt--subtitle-id)
+(fset 'subed--subtitle-msecs-start 'subed-srt--subtitle-msecs-start)
+(fset 'subed--subtitle-msecs-stop 'subed-srt--subtitle-msecs-stop)
+(fset 'subed--subtitle-relative-point 'subed-srt--subtitle-relative-point)
+
+(fset 'subed-move-to-subtitle-id 'subed-srt-move-to-subtitle-id)
+(fset 'subed-move-to-subtitle-text-at-msecs
'subed-srt-move-to-subtitle-text-at-msecs)
+(fset 'subed-move-to-subtitle-text 'subed-srt-move-to-subtitle-text)
+(fset 'subed-move-to-subtitle-end 'subed-srt-move-to-subtitle-end)
+
+(fset 'subed-forward-subtitle-id 'subed-srt-forward-subtitle-id)
+(fset 'subed-backward-subtitle-id 'subed-srt-backward-subtitle-id)
+(fset 'subed-forward-subtitle-text 'subed-srt-forward-subtitle-text)
+(fset 'subed-backward-subtitle-text 'subed-srt-backward-subtitle-text)
+(fset 'subed-forward-subtitle-time-start
'subed-srt-forward-subtitle-time-start)
+(fset 'subed-backward-subtitle-time-start
'subed-srt-backward-subtitle-time-start)
+(fset 'subed-forward-subtitle-time-stop 'subed-srt-forward-subtitle-time-stop)
+(fset 'subed-backward-subtitle-time-stop
'subed-srt-backward-subtitle-time-stop)
+
+(fset 'subed-increase-start-time-100ms 'subed-srt-increase-start-time-100ms)
+(fset 'subed-decrease-start-time-100ms 'subed-srt-decrease-start-time-100ms)
+(fset 'subed-increase-stop-time-100ms 'subed-srt-increase-stop-time-100ms)
+(fset 'subed-decrease-stop-time-100ms 'subed-srt-decrease-stop-time-100ms)
+
+(fset 'subed-subtitle-insert 'subed-srt-subtitle-insert)
+(fset 'subed-subtitle-kill 'subed-srt-subtitle-kill)
+(fset 'subed-sanitize 'subed-srt-sanitize)
+(fset 'subed-sort 'subed-srt-sort)
+
+
+;;; Debugging
+
+(defun subed-enable-debugging ()
+ "Hide debugging messages and set `debug-on-error' to `nil'."
+ (interactive)
+ (unless subed--debug-enabled
+ (setq subed--debug-enabled t
+ debug-on-error t)
+ (let ((debug-buffer (get-buffer-create subed-debug-buffer))
+ (debug-window (split-window-right 50)))
+ (set-window-buffer debug-window debug-buffer)
+ (with-current-buffer debug-buffer
+ (buffer-disable-undo)
+ (setq-local buffer-read-only t)))
+ (add-hook 'kill-buffer-hook (lambda ()
+ (kill-buffer subed-debug-buffer)
+ (delete-window (get-buffer-window
subed-debug-buffer)))
+ :append :local)
+ (message "Enabled debugging messages")))
+
+(defun subed-disable-debugging ()
+ "Display debugging messages in separate window and set
+`debug-on-error' to `t'."
+ (interactive)
+ (when subed--debug-enabled
+ (setq subed--debug-enabled nil
+ debug-on-error nil)
+ (delete-window (get-buffer-window subed-debug-buffer))
+ (message "Disabled debugging messages")))
+
+(defun subed-toggle-debugging ()
+ "Display or hide debugging messages in separate window and set
+`debug-on-error' to `t' or `nil'."
+ (interactive)
+ (if subed--debug-enabled
+ (subed-disable-debugging)
+ (subed-enable-debugging)))
+
+(defun subed-debug (format-string &rest args)
+ "Display message in debugging buffer if debugging is enabled."
+ (when subed--debug-enabled
+ (with-current-buffer (get-buffer-create subed-debug-buffer)
+ (setq-local buffer-read-only nil)
+ (insert (apply 'format (concat format-string "\n") args))
+ (setq-local buffer-read-only t)
+ (let ((debug-window (get-buffer-window subed-debug-buffer)))
+ (set-window-point debug-window (goto-char (point-max)))))))
+
+
+;;; Replay time-adjusted subtitle
+(defun subed-replay-adjusted-subtitle-p ()
+ "Whether adjusting a subtitle's start/stop time causes the
+player to jump to the subtitle's start position."
+ (member 'subed--replay-adjusted-subtitle subed-subtitle-time-adjusted-hook))
+
+(defun subed-enable-replay-adjusted-subtitle ()
+ "Automatically replay a subtitle when its start/stop time is adjusted."
+ (interactive)
+ (unless (subed-replay-adjusted-subtitle-p)
+ (add-hook 'subed-subtitle-time-adjusted-hook
'subed--replay-adjusted-subtitle :append :local)
+ (subed-debug "Enabled replaying adjusted subtitle: %s"
subed-subtitle-time-adjusted-hook)
+ (message "Enabled replaying adjusted subtitle")))
+
+(defun subed-disable-replay-adjusted-subtitle ()
+ "Do not replay a subtitle automatically when its start/stop time is
adjusted."
+ (interactive)
+ (when (subed-replay-adjusted-subtitle-p)
+ (remove-hook 'subed-subtitle-time-adjusted-hook
'subed--replay-adjusted-subtitle :local)
+ (subed-debug "Disabled replaying adjusted subtitle: %s"
subed-subtitle-time-adjusted-hook)
+ (message "Disabled replaying adjusted subtitle")))
+
+(defun subed-toggle-replay-adjusted-subtitle ()
+ "Enable or disable automatic replaying of subtitle when its
+start/stop time is adjusted."
+ (interactive)
+ (if (subed-replay-adjusted-subtitle-p)
+ (subed-disable-replay-adjusted-subtitle)
+ (subed-enable-replay-adjusted-subtitle)))
+
+(defun subed--replay-adjusted-subtitle (sub-id msecs-start)
+ "Move point to currently playing subtitle."
+ (subed-mpv-jump msecs-start)
+ (subed-debug "Replaying subtitle at: %s" (subed-srt--msecs-to-timestamp
msecs-start)))
+
+
+;;; Sync point-to-player
+
+(defun subed-sync-point-to-player-p ()
+ "Whether point is automatically moved to currently playing subtitle."
+ (member 'subed--sync-point-to-player subed-mpv-playback-position-hook))
+
+(defun subed-enable-sync-point-to-player ()
+ "Automatically move point to the currently playing subtitle."
+ (interactive)
+ (unless (subed-sync-point-to-player-p)
+ (add-hook 'subed-mpv-playback-position-hook 'subed--sync-point-to-player
:append :local)
+ (subed-debug "Enabled syncing point to playback position: %s"
subed-mpv-playback-position-hook)
+ (message "Enabled syncing point to playback position")))
+
+(defun subed-disable-sync-point-to-player ()
+ "Do not move point automatically to the currently playing
+subtitle."
+ (interactive)
+ (when (subed-sync-point-to-player-p)
+ (remove-hook 'subed-mpv-playback-position-hook
'subed--sync-point-to-player :local)
+ (subed-debug "Disabled syncing point to playback position: %s"
subed-mpv-playback-position-hook)
+ (message "Disabled syncing point to playback position")))
+
+(defun subed-toggle-sync-point-to-player ()
+ "Enable or disable moving point automatically to the currently
+playing subtitle."
+ (interactive)
+ (if (subed-sync-point-to-player-p)
+ (subed-disable-sync-point-to-player)
+ (subed-enable-sync-point-to-player)))
+
+(defun subed--sync-point-to-player (msecs)
+ "Move point to currently playing subtitle."
+ (when (subed-move-to-subtitle-text-at-msecs msecs)
+ (subed-debug "Synchronized point to playback position: %s -> #%s"
+ (subed-srt--msecs-to-timestamp msecs) (subed--subtitle-id))
+ ;; post-command-hook is not triggered because we didn't move interactively.
+ ;; But there's not really a difference, e.g. the minor mode `hl-line'
breaks
+ ;; unless we call its post-command function, so we do it manually.
+ ;; It's also important NOT to call our own post-command function because
+ ;; that causes player-to-point syncing, which would get hairy.
+ (remove-hook 'post-command-hook 'subed--post-command-handler)
+ (run-hooks 'post-command-hook)
+ (add-hook 'post-command-hook 'subed--post-command-handler :append :local)))
+
+(defun subed-disable-sync-point-to-player-temporarily ()
+ "If point is synced to playback position, temporarily disable
+that for `subed-point-sync-delay-after-motion' seconds."
+ (if subed--point-sync-delay-after-motion-timer
+ (progn
+ (subed-debug "Cancelling old timer (should be nil: %s)"
(subed-sync-point-to-player-p))
+ (cancel-timer subed--point-sync-delay-after-motion-timer))
+ (progn
+ (setq subed--point-was-synced (subed-sync-point-to-player-p))
+ (subed-debug "Remembering whether point was originally synced: %s"
subed--point-was-synced)))
+
+ (when subed--point-was-synced
+ (subed-debug "Temporarily disabling point-to-player syncing (should be t:
%s)"
+ (subed-sync-point-to-player-p))
+ (subed-disable-sync-point-to-player))
+
+ (when subed--point-was-synced
+ (subed-debug "Re-enabling point-to-player syncing in %s seconds"
subed-point-sync-delay-after-motion)
+ (setq subed--point-sync-delay-after-motion-timer
+ (run-at-time subed-point-sync-delay-after-motion nil
+ (lambda ()
+ (setq subed--point-sync-delay-after-motion-timer nil)
+ (subed-enable-sync-point-to-player)
+ (subed-debug "Re-added: %s"
subed-mpv-playback-position-hook))))))
+
+
+;;; Sync player-to-point
+
+(defun subed-sync-player-to-point-p ()
+ "Whether playback position is automatically adjusted to
+subtitle at point."
+ (member 'subed--sync-player-to-point subed-subtitle-motion-hook))
+
+(defun subed-enable-sync-player-to-point ()
+ "Automatically seek player to subtitle at point."
+ (interactive)
+ (unless (subed-sync-player-to-point-p)
+ (subed--sync-player-to-point)
+ (add-hook 'subed-subtitle-motion-hook 'subed--sync-player-to-point :append
:local)
+ (subed-debug "Enabled syncing playback position to point: %s"
subed-subtitle-motion-hook)
+ (message "Enabled syncing playback position to point")))
+
+(defun subed-disable-sync-player-to-point ()
+ "Do not automatically seek player to subtitle at point."
+ (interactive)
+ (when (subed-sync-player-to-point-p)
+ (remove-hook 'subed-subtitle-motion-hook 'subed--sync-player-to-point
:local)
+ (subed-debug "Disabled syncing playback position to point: %s"
subed-subtitle-motion-hook)
+ (message "Disabled syncing playback position to point")))
+
+(defun subed-toggle-sync-player-to-point ()
+ "Enable or disable automatically seeking player to subtitle at point."
+ (interactive)
+ (if (subed-sync-player-to-point-p)
+ (subed-disable-sync-player-to-point)
+ (subed-enable-sync-player-to-point)))
+
+(defun subed--sync-player-to-point ()
+ "Seek player to currently focused subtitle."
+ (subed-debug "Seeking player to subtitle at point %s" (point))
+ (let ((cur-sub-start (subed--subtitle-msecs-start))
+ (cur-sub-stop (subed--subtitle-msecs-stop)))
+ (when (and subed-mpv-playback-position cur-sub-start cur-sub-stop
+ (or (< subed-mpv-playback-position cur-sub-start)
+ (> subed-mpv-playback-position cur-sub-stop)))
+ (subed-mpv-jump cur-sub-start)
+ (subed-debug "Synchronized playback position to point: #%s -> %s"
+ (subed--subtitle-id) cur-sub-start))))
+
+
+;;; Loop over single subtitle
+
+(defun subed-subtitle-loop-p ()
+ "Whether player is rewinded to start of current subtitle every
+time it reaches the subtitle's stop time."
+ (or subed--subtitle-loop-start subed--subtitle-loop-stop))
+
+(defun subed-toggle-subtitle-loop ()
+ "Enable or disable looping in player over currently focused
+subtitle."
+ (interactive)
+ (if (subed-subtitle-loop-p)
+ (progn
+ (remove-hook 'subed-mpv-playback-position-hook
'subed--ensure-subtitle-loop :local)
+ (remove-hook 'subed-subtitle-motion-hook 'subed--set-subtitle-loop
:local)
+ (setq subed--subtitle-loop-start nil
+ subed--subtitle-loop-stop nil)
+ (subed-debug "Disabling loop: %s - %s" subed--subtitle-loop-start
subed--subtitle-loop-stop))
+ (progn
+ (subed--set-subtitle-loop (subed--subtitle-id))
+ (add-hook 'subed-mpv-playback-position-hook 'subed--ensure-subtitle-loop
:append :local)
+ (add-hook 'subed-subtitle-motion-hook 'subed--set-subtitle-loop :append
:local)
+ (subed-debug "Enabling loop: %s - %s" subed--subtitle-loop-start
subed--subtitle-loop-stop))))
+
+(defun subed--set-subtitle-loop (&optional sub-id)
+ "Set loop positions to start/stop time of SUB-ID or current subtitle."
+ (setq subed--subtitle-loop-start (- (subed--subtitle-msecs-start sub-id)
+ (* subed-loop-seconds-before 1000))
+ subed--subtitle-loop-stop (+ (subed--subtitle-msecs-stop sub-id)
+ (* subed-loop-seconds-after 1000)))
+ (subed-debug "Set loop: %s - %s"
+ (subed-srt--msecs-to-timestamp subed--subtitle-loop-start)
+ (subed-srt--msecs-to-timestamp subed--subtitle-loop-stop)))
+
+(defun subed--ensure-subtitle-loop (cur-msecs)
+ "Seek back to `subed--subtitle-loop-start' if player is after
+`subed--subtitle-loop-stop'."
+ (when (and subed--subtitle-loop-start subed--subtitle-loop-stop
+ subed-mpv-is-playing)
+ (when (or (< cur-msecs subed--subtitle-loop-start)
+ (> cur-msecs subed--subtitle-loop-stop))
+ (subed-debug "%s -> Looping over %s - %s"
+ (subed-srt--msecs-to-timestamp cur-msecs)
+ (subed-srt--msecs-to-timestamp subed--subtitle-loop-start)
+ (subed-srt--msecs-to-timestamp subed--subtitle-loop-stop))
+ (subed-mpv-jump subed--subtitle-loop-start))))
+
+
+;;; Pause player while the user is editing
+
+(defun subed-pause-while-typing-p ()
+ "Whether player is automatically paused or slowed down while
+the user is editing the buffer.
+See `subed-playback-speed-while-typing' and
+`subed-playback-speed-while-not-typing'."
+ (member 'subed--pause-while-typing after-change-functions))
+
+(defun subed-enable-pause-while-typing ()
+ "Automatically pause player while the user is editing the
+buffer for `subed-unpause-after-typing-delay' seconds."
+ (unless (subed-pause-while-typing-p)
+ (add-hook 'after-change-functions 'subed--pause-while-typing :append
:local)
+ (if (>= 0 subed-playback-speed-while-typing)
+ (message "Pausing playback when during editing actions")
+ (message "Slowing down playback to %s during editing actions"
subed-playback-speed-while-typing))))
+
+(defun subed-disable-pause-while-typing ()
+ "Do not automatically pause player while the user is editing
+the buffer."
+ (when (subed-pause-while-typing-p)
+ (remove-hook 'after-change-functions 'subed--pause-while-typing :local)
+ (message "Not pausing or slowing down playback during editing actions")))
+
+(defun subed-toggle-pause-while-typing ()
+ "Enable or disable auto-pausing while the user is editing the
+buffer."
+ (interactive)
+ (if (subed-pause-while-typing-p)
+ (subed-disable-pause-while-typing)
+ (subed-enable-pause-while-typing)))
+
+(defun subed--pause-while-typing (&rest args)
+ "Pause or slow down playback for `subed-unpause-after-typing-delay' seconds."
+ (when subed--unpause-after-typing-timer
+ (cancel-timer subed--unpause-after-typing-timer))
+
+ (when (or subed-mpv-is-playing subed--player-is-auto-paused)
+ (if (>= 0 subed-playback-speed-while-typing)
+ ;; Pause playback
+ (progn
+ (subed-mpv-pause)
+ (setq subed--player-is-auto-paused t)
+ (setq subed--unpause-after-typing-timer
+ (run-at-time subed-unpause-after-typing-delay nil
+ (lambda ()
+ (setq subed--player-is-auto-paused nil)
+ (subed-mpv-unpause)))))
+ ;; Slow down playback
+ (progn
+ (subed-mpv-playback-speed subed-playback-speed-while-typing)
+ (setq subed--player-is-auto-paused t)
+ (setq subed--unpause-after-typing-timer
+ (run-at-time subed-unpause-after-typing-delay nil
+ (lambda ()
+ (setq subed--player-is-auto-paused nil)
+ (subed-mpv-playback-speed
subed-playback-speed-while-not-typing))))))))
+
+
+;;; Stuff
+
+(defmacro subed--save-excursion (&rest body)
+ "Restore relative point within current subtitle after executing BODY.
+This also works if the buffer changes as long the subtitle IDs
+don't change."
+ `(let ((sub-id (subed--subtitle-id))
+ (sub-pos (subed--subtitle-relative-point)))
+ (progn ,@body)
+ (subed-move-to-subtitle-id sub-id)
+ ;; Subtitle text may have changed and we may not be able to move to the
+ ;; exact original position
+ (condition-case nil
+ (forward-char sub-pos)
+ ('beginning-of-buffer nil)
+ ('end-of-buffer nil))))
+
+(defun subed-guess-video-file ()
+ "Return path to video if replacing the buffer file name's
+extension with members of `subed-video-extensions' yields an
+existing file."
+ (catch 'found-videofile
+ (let ((file-base (file-name-sans-extension (buffer-file-name))))
+ (dolist (extension subed-video-extensions)
+ (let ((file-video (format "%s.%s" file-base extension)))
+ (when (file-exists-p file-video)
+ (throw 'found-videofile file-video)))))))
+
+
+(defun subed-mode ()
+ "Major mode for editing subtitles.
+
+Key bindings:
+\\{subed-mode-map}"
+ (interactive)
+
+ ;; Buffer-local variables
+ (kill-all-local-variables)
+ (setq-local font-lock-defaults '(subed-font-lock-keywords))
+ (setq-local paragraph-start "^[[:alnum:]\n]+")
+ (setq-local paragraph-separate "\n\n")
+
+ ;; Keybindings
+ (use-local-map subed-mode-map)
+
+ ;; Provide point-motion and subtitle-motion hooks
+ (add-hook 'post-command-hook 'subed--post-command-handler :append :local)
+
+ ;; Sort and reload subtitles in player on C-x C-s
+ (add-hook 'before-save-hook 'subed-sort :append :local)
+ (add-hook 'after-save-hook 'subed-mpv-reload-subtitles :append :local)
+
+ ;; Close player when buffer is killed
+ (add-hook 'kill-buffer-hook 'subed-mpv-kill :append :local)
+
+ ;; Auto-open relevant video file
+ (when subed-auto-find-video
+ (let ((video-file (subed-guess-video-file)))
+ (when video-file
+ (subed-debug "Auto-discovered video file: %s" video-file)
+ (subed-mpv-find-video video-file))))
+
+ (subed-enable-pause-while-typing)
+ (subed-enable-sync-point-to-player)
+ (subed-enable-sync-player-to-point)
+ (subed-enable-replay-adjusted-subtitle)
+
+ (setq major-mode 'subed-mode
+ mode-name "SubEd")
+ (run-mode-hooks 'subed-mode-hook))
+
+(provide 'subed)
+;;; subed.el ends here
diff --git a/tests/test-subed-mpv.el b/tests/test-subed-mpv.el
new file mode 100644
index 0000000..1d39b25
--- /dev/null
+++ b/tests/test-subed-mpv.el
@@ -0,0 +1,139 @@
+(add-to-list 'load-path "./subed")
+(require 'subed)
+
+(describe "Starting mpv"
+ (it "passes arguments to make-process."
+ (spy-on 'make-process)
+ (subed-mpv--server-start "foo" "--bar")
+ (expect 'make-process :to-have-been-called-with
+ :command (list subed-mpv-executable
+ (format "--input-ipc-server=%s"
subed-mpv-socket)
+ "--idle" "foo" "--bar")
+ :name "subed-mpv-server" :buffer nil :noquery t))
+ (it "sets subed-mpv--server-proc on success."
+ (spy-on 'make-process :and-return-value "mock process")
+ (subed-mpv--server-start)
+ (expect subed-mpv--server-proc :to-equal "mock process"))
+ (it "signals error on failure."
+ (spy-on 'make-process :and-throw-error 'error)
+ (expect (subed-mpv--server-start) :to-throw 'error))
+ )
+
+(describe "Stopping mpv"
+ (before-each
+ (setq subed-mpv--server-proc "mock running mpv process")
+ (spy-on 'process-live-p :and-return-value t)
+ (spy-on 'delete-process))
+ (it "kills the mpv process."
+ (subed-mpv--server-stop)
+ (expect 'delete-process :to-have-been-called-with "mock running
mpv process"))
+ (it "resets subed-mpv--server-proc."
+ (expect subed-mpv--server-proc :not :to-be nil)
+ (subed-mpv--server-stop)
+ (expect subed-mpv--server-proc :to-be nil))
+ )
+
+(describe "Connecting"
+ (before-each
+ (spy-on 'delete-process))
+ (it "resets global status variables."
+ (spy-on 'subed-mpv--client-connected-p :and-return-value t)
+ (spy-on 'make-network-process :and-return-value "mock client
process")
+ (spy-on 'process-send-string)
+ (spy-on 'subed-mpv--client-send)
+ (setq subed-mpv--client-proc "foo"
+ subed-mpv-is-playing "baz"
+ subed-mpv--client-command-queue '(foo bar baz))
+ (subed-mpv--client-connect '(0 0 0))
+ (expect subed-mpv--client-proc :to-equal "mock client process")
+ (expect subed-mpv-is-playing :to-be nil)
+ (expect subed-mpv--client-command-queue :to-be nil))
+ (it "correctly calls make-network-process."
+ (spy-on 'make-network-process)
+ (spy-on 'process-send-string)
+ (subed-mpv--client-connect '(0 0 0))
+ (expect 'make-network-process :to-have-been-called-with
+ :name "subed-mpv-client"
+ :family 'local
+ :service subed-mpv-socket
+ :coding '(utf-8 . utf-8)
+ :buffer (get-buffer-create
subed-mpv--client-buffer)
+ :filter #'subed-mpv--client-filter
+ :noquery t
+ :nowait t))
+ (describe "tests the connection"
+ (it "and sets subed-mpv--client-proc if the test succeeds."
+ (spy-on 'make-network-process :and-return-value "mock
client process")
+ (spy-on 'process-send-string)
+ (subed-mpv--client-connect '(0 0 0))
+ (expect 'process-send-string :to-have-been-called-with
+ "mock client process" (concat
subed-mpv--client-test-request "\n"))
+ (expect subed-mpv--client-proc :to-equal "mock client
process"))
+ (it "and resets subed-mpv--client-proc if the test fails."
+ (spy-on 'make-network-process :and-return-value "mock
client process")
+ (spy-on 'process-send-string :and-throw-error 'error)
+ (setq subed-mpv--client-proc "foo")
+ (subed-mpv--client-connect '(0 0 0))
+ (expect subed-mpv--client-proc :to-be nil))
+ (it "and tries again if the test fails."
+ (spy-on 'make-network-process :and-return-value "mock
client process")
+ (spy-on 'process-send-string :and-throw-error 'error)
+ (subed-mpv--client-connect '(0 0 0))
+ ;; FIXME: This seems to be a bug:
+ ;;
https://github.com/jorgenschaefer/emacs-buttercup/issues/139
+ ;; (expect 'process-send-string
:to-have-been-called-times 3)
+ (expect subed-mpv--client-proc :to-be nil))
+ )
+ (it "sends queued commands and empties the queue."
+ (spy-on 'make-network-process :and-return-value "mock client
process")
+ (spy-on 'process-send-string)
+ (spy-on 'subed-mpv--client-send)
+ (setq subed-mpv--client-command-queue '(foo bar baz))
+ (subed-mpv--client-connect '(0 0 0))
+ (expect 'subed-mpv--client-send :to-have-been-called-with 'foo)
+ (expect 'subed-mpv--client-send :to-have-been-called-with 'bar)
+ (expect 'subed-mpv--client-send :to-have-been-called-with 'baz)
+ (expect subed-mpv--client-command-queue :to-be nil))
+ )
+
+(describe "Sending command"
+ (before-each
+ (spy-on 'delete-process)
+ (setq subed-mpv--client-command-queue nil))
+ (describe "when mpv process is not running"
+ (before-each
+ (spy-on 'subed-mpv--server-started-p :and-return-value
nil))
+ (it "is not queued if not connected."
+ (spy-on 'subed-mpv--client-connected-p
:and-return-value nil)
+ (subed-mpv--client-send '(do this thing))
+ (expect subed-mpv--client-command-queue :to-be nil))
+ )
+ (describe "when mpv process is running"
+ (before-each
+ (spy-on 'subed-mpv--server-started-p :and-return-value t))
+ (it "is queued if not connected."
+ (spy-on 'subed-mpv--client-connected-p
:and-return-value nil)
+ (subed-mpv--client-send '(do this thing))
+ (expect subed-mpv--client-command-queue :to-equal
'((do this thing)))
+ (subed-mpv--client-send '(do something else))
+ (expect subed-mpv--client-command-queue :to-equal
'((do this thing)
+
(do something else))))
+ (it "sends command if connected."
+ (spy-on 'subed-mpv--client-connected-p
:and-return-value t)
+ (spy-on 'process-send-string)
+ (setq subed-mpv--client-proc "mock client process")
+ (subed-mpv--client-send '(do this thing))
+ (expect 'process-send-string :to-have-been-called-with
+ "mock client process"
+ (concat (json-encode (list :command '(do this
thing))) "\n"))
+ (expect subed-mpv--client-command-queue :to-equal nil))
+ (it "disconnects if sending fails even though we're
connected."
+ (spy-on 'subed-mpv--client-connected-p
:and-return-value t)
+ (spy-on 'subed-mpv--client-disconnect)
+ (spy-on 'process-send-string :and-throw-error 'error)
+ (expect (subed-mpv--client-send '(do this thing))
:to-throw 'error)
+ (expect 'subed-mpv--client-disconnect
:to-have-been-called-times 1)
+ (expect subed-mpv--client-command-queue :to-equal nil))
+ )
+ )
+
diff --git a/tests/test-subed-srt.el b/tests/test-subed-srt.el
new file mode 100644
index 0000000..7f57ba8
--- /dev/null
+++ b/tests/test-subed-srt.el
@@ -0,0 +1,489 @@
+(add-to-list 'load-path "./subed")
+(require 'subed)
+
+(defvar mock-srt-data
+ "1
+00:01:01,000 --> 00:01:05,123
+Foo.
+
+2
+00:02:02,234 --> 00:02:10,345
+Bar.
+
+3
+00:03:03,456 --> 00:03:15,567
+Baz.
+")
+
+(describe "Getting"
+ (describe "the subtitle ID"
+ (it "returns the subtitle ID if possible."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-text 2)
+ (expect (subed-srt--subtitle-id) :to-equal 2)))
+ (it "returns nil if no subtitle ID can be found."
+ (with-temp-buffer
+ (expect (subed-srt--subtitle-id) :to-equal nil)))
+ )
+ (describe "the subtitle ID at playback time"
+ (it "returns subtitle ID if time is equal to start time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (cl-loop for target-id from 1 to 3 do
+ (let ((msecs
(subed-srt--subtitle-msecs-start target-id)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal target-id)))))))
+ (it "returns subtitle ID if time is equal to stop time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (cl-loop for target-id from 1 to 3 do
+ (let ((msecs
(subed-srt--subtitle-msecs-stop target-id)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal target-id)))))))
+ (it "returns subtitle ID if time is between start and stop
time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (cl-loop for target-id from 1 to 3 do
+ (let ((msecs (+ 1
(subed-srt--subtitle-msecs-start target-id))))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal target-id)))))))
+ (it "returns first subtitle ID if time is before the first
subtitle's start time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (let ((msecs (- (save-excursion
+ (goto-char (point-min))
+ (subed-srt--subtitle-msecs-start))
1)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal 1))))))
+ (it "returns last subtitle ID if time is after last
subtitle's start time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (let ((msecs (+ (save-excursion
+ (goto-char (point-max))
+ (subed-srt--subtitle-msecs-stop))
1)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal 3))))))
+ (it "returns previous subtitle ID when time is between
subtitles"
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (cl-loop for target-id from 1 to 2 do
+ (let ((msecs (+
(subed-srt--subtitle-msecs-stop target-id) 1)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal target-id))))
+
+ (let ((msecs (-
(subed-srt--subtitle-msecs-start (+ target-id 1)) 1)))
+ (cl-loop for outset-id from 1 to 3 do
+ (progn
+ (subed-srt-move-to-subtitle-id
outset-id)
+ (expect
(subed-srt--subtitle-id-at-msecs msecs) :to-equal target-id)))))))
+ )
+ )
+
+
+(describe "Adjusting subtitle start/stop time"
+ :var (subed-subtitle-time-adjusted-hook)
+ (it "runs the appropriate hook."
+ (let ((foo (setf (symbol-function 'foo) (lambda (sub-id msecs)
()))))
+ (spy-on 'foo)
+ (add-hook 'subed-subtitle-time-adjusted-hook 'foo)
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-increase-start-time-100ms)
+ (expect 'foo :to-have-been-called-with 3 183556)
+ (expect 'foo :to-have-been-called-times 1)
+ (subed-srt-move-to-subtitle-id 1)
+ (subed-srt-increase-stop-time-100ms)
+ (expect 'foo :to-have-been-called-with 1 65223)
+ (expect 'foo :to-have-been-called-times 2)
+ (subed-srt-move-to-subtitle-end 2)
+ (subed-srt-decrease-start-time-100ms)
+ (expect 'foo :to-have-been-called-with 2 122134)
+ (expect 'foo :to-have-been-called-times 3)
+ (subed-srt-move-to-subtitle-text 3)
+ (subed-srt-decrease-stop-time-100ms)
+ (expect 'foo :to-have-been-called-with 3 195467)
+ (expect 'foo :to-have-been-called-times 4))))
+ (it "adjusts the start/stop time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-id 1)
+ (subed-srt-increase-start-time-100ms)
+ (expect (save-excursion (subed-srt-move-to-subtitle-time-start)
+ (thing-at-point 'line)) :to-equal
"00:01:01,100 --> 00:01:05,123\n")
+ (subed-srt-decrease-start-time-100ms)
+ (subed-srt-decrease-start-time-100ms)
+ (expect (save-excursion (subed-srt-move-to-subtitle-time-start)
+ (thing-at-point 'line)) :to-equal
"00:01:00,900 --> 00:01:05,123\n")
+ (subed-srt-increase-stop-time-100ms)
+ (subed-srt-increase-stop-time-100ms)
+ (expect (save-excursion (subed-srt-move-to-subtitle-time-start)
+ (thing-at-point 'line)) :to-equal
"00:01:00,900 --> 00:01:05,323\n")
+ (subed-srt-decrease-stop-time-100ms)
+ (expect (save-excursion (subed-srt-move-to-subtitle-time-start)
+ (thing-at-point 'line)) :to-equal
"00:01:00,900 --> 00:01:05,223\n")))
+ )
+
+
+(describe "Moving"
+ (describe "to current subtitle ID"
+ (it "returns ID's point when point is already on the ID."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (expect (thing-at-point 'word) :to-equal "1")
+ (expect (subed-srt-move-to-subtitle-id) :to-equal 1)
+ (expect (thing-at-point 'word) :to-equal "1")))
+ (it "returns ID's point when point is on the duration."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (search-backward ",234")
+ (expect (thing-at-point 'word) :to-equal "02")
+ (expect (subed-srt-move-to-subtitle-id) :to-equal 39)
+ (expect (thing-at-point 'word) :to-equal "2")))
+ (it "returns ID's point when point is on the text."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (search-backward "Baz.")
+ (expect (thing-at-point 'word) :to-equal "Baz")
+ (expect (subed-srt-move-to-subtitle-id) :to-equal 77)
+ (expect (thing-at-point 'word) :to-equal "3")))
+ (it "returns ID's point when point is after the text."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (search-forward "Bar.\n")
+ (expect (thing-at-point 'line) :to-equal "\n")
+ (expect (subed-srt-move-to-subtitle-id) :to-equal 39)
+ (expect (thing-at-point 'word) :to-equal "2")))
+ (it "returns nil if buffer is empty."
+ (with-temp-buffer
+ (expect (buffer-string) :to-equal "")
+ (expect (subed-srt-move-to-subtitle-id) :to-equal
nil)))
+ )
+ (describe "to specific subtitle ID"
+ (it "returns ID's point if wanted ID exists."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-max))
+ (expect (subed-srt-move-to-subtitle-id 2) :to-equal
39)
+ (expect (thing-at-point 'word) :to-equal "2")
+ (expect (subed-srt-move-to-subtitle-id 1) :to-equal
1)
+ (expect (thing-at-point 'word) :to-equal "1")
+ (expect (subed-srt-move-to-subtitle-id 3) :to-equal
77)
+ (expect (thing-at-point 'word) :to-equal "3")))
+ (it "returns nil and does not move if wanted ID does not
exists."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (search-forward "Foo")
+ (setq stored-point (point))
+ (expect (subed-srt-move-to-subtitle-id 4) :to-equal
nil)
+ (expect stored-point :to-equal (point))))
+ )
+ (describe "to subtitle ID at specific time"
+ (it "returns ID's point if point changed."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-max))
+ (spy-on 'subed-srt--subtitle-id-at-msecs
:and-return-value (point-min))
+ (expect (subed-srt-move-to-subtitle-id-at-msecs
123456) :to-equal (point-min))
+ (expect (point) :to-equal (point-min))
+ (expect 'subed-srt--subtitle-id-at-msecs
:to-have-been-called-with 123456)
+ (expect 'subed-srt--subtitle-id-at-msecs
:to-have-been-called-times 1)))
+ (it "returns nil if point didn't change."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char 75)
+ (spy-on 'subed-srt--subtitle-id-at-msecs
:and-return-value 75)
+ (expect (subed-srt-move-to-subtitle-id-at-msecs
123456) :to-equal nil)
+ (expect (point) :to-equal 75)
+ (expect 'subed-srt--subtitle-id-at-msecs
:to-have-been-called-with 123456)
+ (expect 'subed-srt--subtitle-id-at-msecs
:to-have-been-called-times 1)))
+ )
+ (describe "to subtitle start time"
+ (it "returns start time's point if movement was
successful."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (expect (subed-srt-move-to-subtitle-time-start)
:to-equal 3)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:01:01,000")
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-time-start)
:to-equal 41)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:02:02,234")
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-time-start)
:to-equal 79)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:03:03,456")))
+ (it "returns nil if movement failed."
+ (with-temp-buffer
+ (expect (subed-srt-move-to-subtitle-time-start)
:to-equal nil)))
+ )
+ (describe "to subtitle stop time"
+ (it "returns stop time's point if movement was successful."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (expect (subed-srt-move-to-subtitle-time-stop)
:to-equal 20)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:01:05,123")
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-time-stop)
:to-equal 58)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:02:10,345")
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-time-stop)
:to-equal 96)
+ (expect (buffer-substring (point) (+ (point)
subed-srt--length-timestamp)) :to-equal "00:03:15,567")))
+ (it "returns nil if movement failed."
+ (with-temp-buffer
+ (expect (subed-srt-move-to-subtitle-time-stop)
:to-equal nil)))
+ )
+ (describe "to subtitle text"
+ (it "returns subtitle text's point if movement was
successful."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (expect (subed-srt-move-to-subtitle-text) :to-equal
33)
+ (expect (point) :to-equal (save-excursion (goto-char
(point-max)) (search-backward "Foo.")))
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-text) :to-equal
71)
+ (expect (point) :to-equal (save-excursion (goto-char
(point-max)) (search-backward "Bar.")))
+ (re-search-forward "\n\n")
+ (expect (subed-srt-move-to-subtitle-text) :to-equal
109)
+ (expect (point) :to-equal (save-excursion (goto-char
(point-max)) (search-backward "Baz.")))
+
+ ))
+ (it "returns nil if movement failed."
+ (with-temp-buffer
+ (expect (subed-srt-move-to-subtitle-time-stop)
:to-equal nil)))
+ )
+ (describe "to end of subtitle text"
+ (it "returns end of subtitle text's point if movement was
successful."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (expect (subed-srt-move-to-subtitle-end) :to-be 37)
+ (expect (looking-back "^Foo.$") :to-be t)
+ (forward-char 2)
+ (expect (subed-srt-move-to-subtitle-end) :to-be 75)
+ (expect (looking-back "^Bar.$") :to-be t)
+ (forward-char 2)
+ (expect (subed-srt-move-to-subtitle-end) :to-be 113)
+ (expect (looking-back "^Baz.$") :to-be t)
+ (goto-char (point-max))
+ (backward-char 2)
+ (expect (subed-srt-move-to-subtitle-end) :to-be 113)
+ (expect (looking-back "^Baz.$") :to-be t)
+ ))
+ (it "returns nil if movement failed."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-max))
+ (expect (subed-srt-move-to-subtitle-end) :to-be nil)
+ (expect (looking-back "^Baz.$") :to-be nil)
+ (backward-char 1)
+ (expect (subed-srt-move-to-subtitle-end) :to-be nil)
+ (expect (looking-back "^Baz.$") :to-be t)))
+ )
+ (describe "to next subtitle ID"
+ (it "returns subtitle ID's point when it moved."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-id 2)
+ (expect (thing-at-point 'word) :to-equal "2")
+ (expect (subed-srt-forward-subtitle-id) :to-be 77)
+ (expect (thing-at-point 'word) :to-equal "3")))
+ (it "returns nil and doesn't move when point is on the
last subtitle and there are trailing lines."
+ (with-temp-buffer
+ (insert (concat mock-srt-data "\n\n"))
+ (subed-srt-move-to-subtitle-text 3)
+ (expect (thing-at-point 'word) :to-equal "Baz")
+ (expect (subed-srt-forward-subtitle-id) :to-be nil)
+ (expect (thing-at-point 'word) :to-equal "Baz")))
+ )
+ )
+
+
+(describe "Killing a subtitle"
+ (it "removes it when it is the first one."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-text 1)
+ (subed-srt-subtitle-kill)
+ (expect (buffer-string) :to-equal (concat "2\n"
+ "00:02:02,234 -->
00:02:10,345\n"
+ "Bar.\n"
+ "\n"
+ "3\n"
+ "00:03:03,456 -->
00:03:15,567\n"
+ "Baz.\n"))))
+ (it "removes it when it is in the middle."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-text 2)
+ (subed-srt-subtitle-kill)
+ (expect (buffer-string) :to-equal (concat "1\n"
+ "00:01:01,000 -->
00:01:05,123\n"
+ "Foo.\n"
+ "\n"
+ "3\n"
+ "00:03:03,456 -->
00:03:15,567\n"
+ "Baz.\n"))))
+ (it "removes it when it is the last one."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-text 3)
+ (subed-srt-subtitle-kill)
+ (expect (buffer-string) :to-equal (concat "1\n"
+ "00:01:01,000 -->
00:01:05,123\n"
+ "Foo.\n"
+ "\n"
+ "2\n"
+ "00:02:02,234 -->
00:02:10,345\n"
+ "Bar.\n"))))
+ (it "removes the previous subtitle when point is right above an ID."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (subed-srt-move-to-subtitle-id 3)
+ (backward-char)
+ (expect (looking-at "^\n3\n") :to-be t)
+ (subed-srt-subtitle-kill)
+ (expect (buffer-string) :to-equal (concat "1\n"
+ "00:01:01,000 -->
00:01:05,123\n"
+ "Foo.\n"
+ "\n"
+ "3\n"
+ "00:03:03,456 -->
00:03:15,567\n"
+ "Baz.\n"))))
+ )
+
+
+(describe "Sanitizing"
+ (it "removes trailing tabs and spaces from all lines."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (re-search-forward "\n" nil t)
+ (replace-match " \n"))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data))
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (re-search-forward "\n" nil t)
+ (replace-match "\t\n"))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data)))
+ (it "removes leading tabs and spaces from all lines."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (re-search-forward "\n" nil t)
+ (replace-match "\n "))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data))
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (re-search-forward "\n" nil t)
+ (replace-match "\n\t"))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data))
+ )
+ (it "removes excessive newlines between subtitles."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (re-search-forward "\n\n" nil t)
+ (replace-match "\n \n \t \t\t \n\n \n \n \t\n"))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data)))
+ (it "removes empty lines from beginning of buffer."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (insert " \n\t\n")
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data)))
+ (it "removes empty lines from end of buffer."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-max))
+ (insert " \n\t\n\n")
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data)))
+ (it "ensures a single newline after the last subtitle."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-max))
+ (delete-backward-char 1)
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt-sanitize)
+ (expect (buffer-string) :to-equal mock-srt-data)))
+ )
+
+(describe "Renumbering"
+ (it "ensures consecutive subtitle IDs."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (while (looking-at "^[0-9]$")
+ (replace-match "123"))
+ (expect (buffer-string) :not :to-equal mock-srt-data)
+ (subed-srt--regenerate-ids)
+ (expect (buffer-string) :to-equal mock-srt-data))))
+
+(describe "Sorting"
+ (it "ensures subtitles are ordered by start time."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (re-search-forward "01:01")
+ (replace-match "12:01")
+ (goto-char (point-min))
+ (re-search-forward "02:02")
+ (replace-match "10:02")
+ (goto-char (point-min))
+ (re-search-forward "03:03")
+ (replace-match "11:03")
+ (subed-srt-sort)
+ (expect (buffer-string) :to-equal
+ (concat
+ "1\n"
+ "00:10:02,234 --> 00:02:10,345\n"
+ "Bar.\n"
+ "\n"
+ "2\n"
+ "00:11:03,456 --> 00:03:15,567\n"
+ "Baz.\n"
+ "\n"
+ "3\n"
+ "00:12:01,000 --> 00:01:05,123\n"
+ "Foo.\n"))))
+ (it "preserves point in the current subtitle."
+ (with-temp-buffer
+ (insert mock-srt-data)
+ (goto-char (point-min))
+ (re-search-forward "01:01")
+ (replace-match "12:01")
+ (search-forward "\n")
+ (expect (current-word) :to-equal "Foo")
+ (subed-srt-sort)
+ (expect (current-word) :to-equal "Foo")))
+ )
diff --git a/tests/test-subed.el b/tests/test-subed.el
new file mode 100644
index 0000000..b4b450e
--- /dev/null
+++ b/tests/test-subed.el
@@ -0,0 +1,96 @@
+(add-to-list 'load-path "./subed")
+(require 'subed)
+
+(describe "Syncing player to point"
+ :var (subed-mpv-playback-position)
+ (before-each
+ (setq subed-mpv-playback-position 0)
+ (spy-on 'subed--subtitle-msecs-start :and-return-value 5000)
+ (spy-on 'subed--subtitle-msecs-stop :and-return-value 6500)
+ (spy-on 'subed-mpv-jump)
+ (spy-on 'subed-disable-sync-point-to-player-temporarily))
+ (it "does not seek player if point is on current subtitle."
+ (setq subed-mpv-playback-position 5000)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :not :to-have-been-called)
+ (setq subed-mpv-playback-position 6500)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :not :to-have-been-called))
+ (it "seeks player if point is on future subtitle."
+ (setq subed-mpv-playback-position 6501)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :to-have-been-called-with 5000))
+ (it "seeks player if point is on past subtitle."
+ (setq subed-mpv-playback-position 4999)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :to-have-been-called-with 5000))
+ )
+
+(describe "Syncing point to player"
+ :var (subed-mpv-playback-position)
+ (before-each
+ (setq subed-mpv-playback-position 0)
+ (spy-on 'subed--subtitle-msecs-start :and-return-value 5000)
+ (spy-on 'subed--subtitle-msecs-stop :and-return-value 6500)
+ (spy-on 'subed-mpv-jump))
+ (it "does not seek player if point is on current subtitle."
+ (setq subed-mpv-playback-position 5000)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :not :to-have-been-called)
+ (setq subed-mpv-playback-position 6500)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :not :to-have-been-called))
+ (it "seeks player if point is on future subtitle."
+ (setq subed-mpv-playback-position 6501)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :to-have-been-called-with 5000))
+ (it "seeks player if point is on past subtitle."
+ (setq subed-mpv-playback-position 4999)
+ (subed--sync-player-to-point)
+ (expect 'subed-mpv-jump :to-have-been-called-with 5000))
+ )
+
+(describe "Temporarily disabling point-to-player syncing"
+ (before-each
+ (spy-on 'subed-disable-sync-point-to-player))
+ (describe "when point-to-player syncing is disabled"
+ (before-each
+ (spy-on 'subed-sync-point-to-player-p :and-return-value
nil)
+ (spy-on 'run-at-time))
+ (it "does not disable point-to-player syncing."
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'subed-disable-sync-point-to-player :not
:to-have-been-called))
+ (it "does not schedule re-enabling of point-to-player
syncing."
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'run-at-time :not :to-have-been-called)
+ (expect subed--point-sync-delay-after-motion-timer
:to-be nil))
+ )
+ (describe "when point-to-player syncing is enabled"
+ :var (subed--point-sync-delay-after-motion-timer)
+ (before-each
+ (spy-on 'subed-sync-point-to-player-p :and-return-value t)
+ (spy-on 'run-at-time :and-return-value "mock timer")
+ (spy-on 'cancel-timer)
+ (setq subed--point-sync-delay-after-motion-timer nil))
+ (it "disables point-to-player syncing."
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'subed-disable-sync-point-to-player
:to-have-been-called))
+ (it "schedules re-enabling of point-to-player syncing."
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'run-at-time :to-have-been-called-with
+ subed-point-sync-delay-after-motion nil
+ (lambda ()
+ (setq
subed--point-sync-delay-after-motion-timer nil)
+ (subed-enable-sync-point-to-player)
+ (subed-debug "Re-added: %s"
subed-mpv-playback-position-hook))))
+ (it "cancels previously scheduled re-enabling of
point-to-player syncing."
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'cancel-timer :not :to-have-been-called-with
"mock timer")
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'cancel-timer :to-have-been-called-with "mock
timer")
+ (expect 'cancel-timer :to-have-been-called-times 1)
+ (subed-disable-sync-point-to-player-temporarily)
+ (expect 'cancel-timer :to-have-been-called-with "mock
timer")
+ (expect 'cancel-timer :to-have-been-called-times 2))
+ )
+ )
- [nongnu] branch elpa/subed created (now d0dfa1a), ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 6a12f22 002/389: When debugging, don't autoscroll if debug-window is hidden, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed d8bf4ed 001/389: Initial commit,
ELPA Syncer <=
- [nongnu] elpa/subed 2ff368b 004/389: Move subed-subtitle-time-adjusted-hook where it belongs, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 9d54f39 005/389: Be more robust when getting start/stop time, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 433f7b4 003/389: subed-disable-sync-point-to-player-temporarily: Remove debug msgs, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 70e6cea 015/389: README: Test unicode non-breaking space, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed d98325c 011/389: Add tests for getting subtitle start/stop time, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 73419ca 021/389: Make forward/backward movement tests pass, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 99a235a 019/389: Spoon trailing parens, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed 46aeb10 020/389: Add more tests for forward/backward movement, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed fd7c2ba 017/389: Add tests for getting relative point within subtitle, ELPA Syncer, 2021/12/03
- [nongnu] elpa/subed ac4bc87 025/389: Add more tests for moving to subtitle ID, ELPA Syncer, 2021/12/03