emacs-elpa-diffs
[Top][All Lists]
Advanced

[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))
+                    )
+          )



reply via email to

[Prev in Thread] Current Thread [Next in Thread]