summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2020-12-16 19:49:01 +0100
committerRobert Günzler <r@gnzler.io>2021-01-21 19:39:13 +0100
commitcb0f13135fbbae8c3d1f66bb236fd2652fd5e4b8 (patch)
tree67c0b3fd6bde5b54db33cff5bb21dde9438f7c1f
parent81bd6e5fbf3001220693d30ce4bd4ebd06a3973f (diff)
mpv: update to make use of script-dirs
-rw-r--r--.config/mpv/input.conf91
-rw-r--r--.config/mpv/script-opts/console.conf2
-rw-r--r--.config/mpv/script-opts/stats.conf4
-rw-r--r--.config/mpv/script-opts/uosc.conf46
-rw-r--r--.config/mpv/scripts/SmartCopyPaste-2.2.lua324
-rw-r--r--.config/mpv/scripts/notify.lua122
-rw-r--r--.config/mpv/scripts/uosc.lua3243
7 files changed, 86 insertions, 3746 deletions
diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf
index 50328d2..67ace36 100644
--- a/.config/mpv/input.conf
+++ b/.config/mpv/input.conf
@@ -40,35 +40,35 @@ WHEEL_UP seek -5
 WHEEL_DOWN seek 5
 
 a cycle audio
-A script-binding uosc/audio  #! select > audio
-#v cycle video
+A script-binding uosc/audio               #! select > audio
 D cycle deinterlace
 
 s cycle sub
-S script-binding uosc/subtitles  #! select > sub
-Ctrl+s script-binding uosc/load-subtitles  #! select > load sub...
-e add sub-scale +0.1  #! sub > scale up
-w add sub-scale -0.1  #! sub > scale down
-r add sub-pos +1  #! sub > pos up
-t add sub-pos -1  #! sub > pos down
-
-z add sub-delay -0.1  #! delay > sub -0.1
-x add sub-delay +0.1  #! delay > sub +0.1
-Z add audio-delay -0.1  #! delay > audio -0.1
-X add audio-delay +0.1  #! delay > audio +0.1
+S script-binding uosc/subtitles           #! select > sub
+Ctrl+s script-binding uosc/load-subtitles #! select > load sub...
+e add sub-scale +0.1                      #! sub > scale up
+w add sub-scale -0.1                      #! sub > scale down
+r add sub-pos +1                          #! sub > pos up
+t add sub-pos -1                          #! sub > pos down
+
+z add sub-delay -0.1                      #! delay > sub -0.1
+x add sub-delay +0.1                      #! delay > sub +0.1
+Z add audio-delay -0.1                    #! delay > audio -0.1
+X add audio-delay +0.1                    #! delay > audio +0.1
 
 Ctrl+Shift+u cycle-values sub-ass-override "force" "strip" "no"  #! sub > ass override
 V cycle sub-visibility  #! sub > visibility
 # Meta+s cycle sub-ass-force-margins
 
 # PLAYER BINDINGS
-i script-binding stats/display-stats-toggle  #! stats
-g screenshot  #! utils > screenshot > default
-G screenshot video  #! utils > screenshot > video
+i script-binding stats/display-stats-toggle                   #! stats
+G screenshot                                                  #! utils > screenshot > default
+Alt+g screenshot video                                        #! utils > screenshot > video
 
+A cycle-values video-aspect "16:9" "4:3" "21:9" "2.35:1" "-1" #! utils > aspect-ratio
+Alt+s cycle-values audio-channels "auto-safe" "auto" "stereo" #! utils > audio channels
+Alt+a cycle-values video-rotate 0 90 180 270                  #! utils > video-rotate
 # T cycle ontop
-A cycle-values video-aspect "16:9" "4:3" "21:9" "2.35:1" "-1"  #! utils > aspect-ratio
-c cycle-values audio-channels "auto-safe" "auto" "stereo"  #! utils > audio channels
 
 q quit
 Q quit-watch-later  #! watch later
@@ -81,13 +81,19 @@ Shift+- add volume -5
 > frame-step
 < frame-back-step
 
-l cycle-values loop-file yes no  #! loop > file
-L ab-loop  #! loop > A-B
+[ multiply speed 1/1.1          #! utils > speed > +10%
+] multiply speed 1.1            #! utils > speed > -10%
+{ multiply speed 0.5            #! utils > speed > +50%
+} multiply speed 2.0            #! utils > speed > -50%
+BS set speed 1.0                #! utils > speed > reset
+
+l cycle-values loop-file yes no #! loop > file
+L ab-loop                       #! loop > A-B
 
 # VAPOURSYNTH/FILTER BINDINGS
 # banding
-b cycle deband  #! fx > deband
-B vf toggle gradfun  #! fx > gradfun
+b cycle deband      #! fx > deband
+B vf toggle gradfun #! fx > gradfun
 # binaural correction
 h cycle-values af lavfi=[bs2b=profile=default] lavfi=[bs2b=profile=jmeier] lavfi=[bs2b=profile=cmoy] lavfi=[earwax] ""  #! fx > bs2b
 
@@ -98,17 +104,30 @@ C script-binding uosc/chapters  #! select > chapters
 Ctrl+space script-binding uosc/peek-timeline
 Alt+space script-binding uosc/flash-timeline
 
-# Ctrl+Shift+k script_binding autocrop   #  utils > autocrop
-Ctrl+C script_binding slicing/slicing_mark  #! utils > slicing > mark
-Ctrl+A script_binding slicing/slicing_audio  #! utils > slicing > audio
-Ctrl+W script_binding webm/display-webm-encoder  #! utils > webm encoder
-
-M script_binding mouse_coords  #! utils > mouse coords
-# 8 script_binding mouse_coords/set
-# 9 script_binding mouse_coords/measure
-
-Ctrl+Shift+1 apply-profile anime  #! profile > anime
-Ctrl+Shift+2 apply-profile cache  #! profile > cache
-Ctrl+Shift+3 apply-profile scale  #! profile > scale
-Ctrl+Shift+4 apply-profile interpolate  #! profile > interpolate
-Ctrl+Shift+5 apply-profile 3d  #! profile > 3d fix
+Alt+w script-binding webm/display-webm-encoder #! utils > webm encoder
+Alt+e script-binding slicing/slicing_audio     #! utils > slicing > audio
+Alt+r script-binding slicing/slicing_mark      #! utils > slicing > mark
+Alt+t script-binding autodeint                 #! utils > autodeint
+Alt+y script-binding autocrop/toggle_crop      #! utils > autocrop
+c script-binding contact-sheet-toggle          #! utils > contact sheet
+g script-binding playlist-view-toggle          #! utils > playlist view
+Alt+m script-binding mouse_coords              #! utils > mouse coords
+
+K ignore
+J ignore
+
+# 1 ignore
+# 2 ignore
+3 ignore
+4 ignore
+5 ignore
+6 ignore
+7 ignore
+8 ignore
+9 ignore
+
+Ctrl+Shift+1 apply-profile anime       # ! profile > anime
+Ctrl+Shift+2 apply-profile cache       # ! profile > cache
+Ctrl+Shift+3 apply-profile scale       # ! profile > scale
+Ctrl+Shift+4 apply-profile interpolate # ! profile > interpolate
+Ctrl+Shift+5 apply-profile 3d          # ! profile > 3d fix
diff --git a/.config/mpv/script-opts/console.conf b/.config/mpv/script-opts/console.conf
index 26581d1..ca236be 100644
--- a/.config/mpv/script-opts/console.conf
+++ b/.config/mpv/script-opts/console.conf
@@ -1,2 +1,2 @@
-font="Iosevka Sparkle"
+font="Iosevka Aile"
 font_size=18
diff --git a/.config/mpv/script-opts/stats.conf b/.config/mpv/script-opts/stats.conf
index d9f699f..064732f 100644
--- a/.config/mpv/script-opts/stats.conf
+++ b/.config/mpv/script-opts/stats.conf
@@ -1,4 +1,4 @@
-font="Iosevka Sparkle"
-font_mono="Iosevka Sparkle"
+font="Iosevka Aile"
+font_mono="Iosevka Aile"
 font_size=8
 border_size=0.6
diff --git a/.config/mpv/script-opts/uosc.conf b/.config/mpv/script-opts/uosc.conf
index 1de4c54..5984480 100644
--- a/.config/mpv/script-opts/uosc.conf
+++ b/.config/mpv/script-opts/uosc.conf
@@ -1,5 +1,5 @@
 # timeline size when fully retracted, 0 will hide it completely
-timeline_size_min=5
+timeline_size_min=2
 # timeline size when fully expanded, in pixels, 0 to disable
 timeline_size_max=40
 # same as ^ but when in fullscreen
@@ -9,18 +9,15 @@ timeline_size_max_fullscreen=60
 timeline_start_hidden=no
 # timeline opacity
 timeline_opacity=0.8
-# top (and bottom in no-border mode) border of background color to help visually
-# separate elapsed bar from a video of similar color or desktop background
+# top border of background color to help visually separate timeline from video
 timeline_border=1
 # when scrolling above timeline, wheel will seek by this amount of seconds
 timeline_step=5
 # display seekable buffered ranges for streaming videos, syntax `color:opacity`,
 # color is an BBGGRR hex code, set to `none` to disable
-timeline_cached_ranges=345433:0.5
+timeline_cached_ranges=671e69:0.6
 # floating number font scale adjustment
 timeline_font_scale=1
-# briefly show timeline on external changes (e.g. seeking with a hotkey)
-timeline_flash=yes
 
 # timeline chapters style: none, dots, lines, lines-top, lines-bottom
 chapters=lines
@@ -34,7 +31,6 @@ volume_opacity=0.8
 volume_border=1
 volume_step=1
 volume_font_scale=1
-volume_flash=yes
 
 # playback speed widget: mouse drag or wheel to change, click to reset
 speed=no
@@ -43,14 +39,25 @@ speed_size_fullscreen=68
 speed_opacity=1
 speed_step=0.1
 speed_font_scale=1
-speed_flash=yes
 
 # controls all menus, such as context menu, subtitle loader/selector, etc
 menu_item_height=36
 menu_item_height_fullscreen=50
+menu_wasd_navigation=no
+menu_hjkl_navigation=yes
 menu_opacity=0.8
 menu_font_scale=1
 
+# top bar with window controls and media title shown only in no-border mode
+top_bar_size=40
+top_bar_size_fullscreen=46
+top_bar_controls=yes
+top_bar_title=yes
+
+# window border drawn in no-border mode
+window_border_size=1
+window_border_opacity=0.8
+
 # pause video on clicks shorter than this number of milliseconds, 0 to disable
 pause_on_click_shorter_than=0
 # for how long in milliseconds to show elements they're it's being flashed
@@ -65,15 +72,14 @@ color_background=000000
 color_background_text=ffffff
 # use bold font weight throughout the whole UI
 font_bold=no
+# show total time instead of time remaining
+total_time=no
 # hide UI when mpv autohides the cursor
 autohide=no
 # can be: none, flash, static
 pause_indicator=none
-# window controls (minimize, maximize, close, title) show only in no-border
-# mode, but you can disable them completely by setting this to `no`
-window_controls=yes
-# display current media title in top window controls bar
-title=yes
+# sizes to list in stream quality menu
+stream_quality_options=4320,2160,1440,1080,720,480,360,240,144
 # load first file when calling next on a last file in a directory and vice versa
 directory_navigation_loops=no
 # file types to look for when navigating media files
@@ -116,13 +122,17 @@ font_height_to_letter_width_ratio=0.5
 #
 # Examples:
 #
-# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
+# Display anime openings and endings as ranges:
 # ```
-# chapter_ranges=sponsor start<3535a5:0.5>sponsor end
+# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
 # ```
 #
-# Display anime openings and endings as ranges:
+# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
 # ```
-# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
+# chapter_ranges=sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
 # ```
-chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end
+chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
+#671e69
+#968638
+#3535a5
+# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, preview$<d2f7cc:0.5>.*
diff --git a/.config/mpv/scripts/SmartCopyPaste-2.2.lua b/.config/mpv/scripts/SmartCopyPaste-2.2.lua
deleted file mode 100644
index 37f695a..0000000
--- a/.config/mpv/scripts/SmartCopyPaste-2.2.lua
+++ /dev/null
@@ -1,324 +0,0 @@
--- Copyright (c) 2020, Eisa AlAwadhi
--- License: BSD 2-Clause License
-
--- Creator: Eisa AlAwadhi
--- Project: SmartCopyPaste
--- Version: 2.2
-
-local utils = require 'mp.utils'
-local msg = require 'mp.msg'
-local protocols
-local extensions
-local pasted = false
-
-----------------------------USER CUSTOMIZATION SETTINGS-----------------------------------
---These settings are for users to manually change some options in the script.
---Keybinds can be defined in the bottom of the script.
-
-local device = nil --nil is for automatic device detection, or manually change to: 'windows' or 'mac' or 'linux'
-
-local linux_copy = 'wl-copy' --copy command that will be used in Linux. OR write a different command
-local linux_paste = 'wl-paste' --paste command that will be used in Linux. OR write a different command
-
-local mac_copy = 'pbcopy' --copy command that will be used in MAC. OR write a different command
-local mac_paste = 'pbpaste' --paste command that will be used in MAC. OR write a different command
-
-local windows_copy = 'powershell' --'powershell' is for using windows powershell to copy. OR write the copy command, e.g: ' clip'
-local windows_paste = 'powershell' --'powershell' is for using windows powershell to paste. OR write the paste command
-
-local paste_anything = false --false is for specific paste based on the specified extensions and protocols. Change to true so paste accepts anything (not recommended to change this).
-
-if not paste_anything then
-	protocols = { --add below (after a comma) any protocol you want SmartCopyPaste to work with; e.g: ,'ftp://'
-		'https?://' ,'magnet:'
-	}
-	extensions = { --add below (after a comma) any extension you want SmartCopyPaste to work with; e.g: ,'pdf'
-		--video & audio
-		'ac3', 'a52', 'eac3', 'mlp', 'dts', 'dts-hd', 'dtshd', 'true-hd', 'thd', 'truehd', 'thd+ac3', 'tta', 'pcm', 'wav', 'aiff', 'aif',  'aifc', 'amr', 'awb', 'au', 'snd', 'lpcm', 'yuv', 'y4m', 'ape', 'wv', 'shn', 'm2ts', 'm2t', 'mts', 'mtv', 'ts', 'tsv', 'tsa', 'tts', 'trp', 'adts', 'adt', 'mpa', 'm1a', 'm2a', 'mp1', 'mp2', 'mp3', 'mpeg', 'mpg', 'mpe', 'mpeg2', 'm1v', 'm2v', 'mp2v', 'mpv', 'mpv2', 'mod', 'tod', 'vob', 'vro', 'evob', 'evo', 'mpeg4', 'm4v', 'mp4', 'mp4v', 'mpg4', 'm4a', 'aac', 'h264', 'avc', 'x264', '264', 'hevc', 'h265', 'x265', '265', 'flac', 'oga', 'ogg', 'opus', 'spx', 'ogv', 'ogm', 'ogx', 'mkv', 'mk3d', 'mka', 'webm', 'weba', 'avi', 'vfw', 'divx', '3iv', 'xvid', 'nut', 'flic', 'fli', 'flc', 'nsv', 'gxf', 'mxf', 'wma', 'wm', 'wmv', 'asf', 'dvr-ms', 'dvr', 'wtv', 'dv', 'hdv', 'flv','f4v', 'f4a', 'qt', 'mov', 'hdmov', 'rm', 'rmvb', 'ra', 'ram', '3ga', '3ga2', '3gpp', '3gp', '3gp2', '3g2', 'ay', 'gbs', 'gym', 'hes', 'kss', 'nsf', 'nsfe', 'sap', 'spc', 'vgm', 'vgz', 'm3u', 'm3u8', 'pls', 'cue',
-		--images
-		"ase", "art", "bmp", "blp", "cd5", "cit", "cpt", "cr2", "cut", "dds", "dib", "djvu", "egt", "exif", "gif", "gpl", "grf", "icns", "ico", "iff", "jng", "jpeg", "jpg", "jfif", "jp2", "jps", "lbm", "max", "miff", "mng", "msp", "nitf", "ota", "pbm", "pc1", "pc2", "pc3", "pcf", "pcx", "pdn", "pgm", "PI1", "PI2", "PI3", "pict", "pct", "pnm", "pns", "ppm", "psb", "psd", "pdd", "psp", "px", "pxm", "pxr", "qfx", "raw", "rle", "sct", "sgi", "rgb", "int", "bw", "tga", "tiff", "tif", "vtf", "xbm", "xcf", "xpm", "3dv", "amf", "ai", "awg", "cgm", "cdr", "cmx", "dxf", "e2d", "egt", "eps", "fs", "gbr", "odg", "svg", "stl", "vrml", "x3d", "sxd", "v2d", "vnd", "wmf", "emf", "art", "xar", "png", "webp", "jxr", "hdp", "wdp", "cur", "ecw", "iff", "lbm", "liff", "nrrd", "pam", "pcx", "pgf", "sgi", "rgb", "rgba", "bw", "int", "inta", "sid", "ras", "sun", "tga",
-		--other types
-		'torrent'
-	}
----------------------------END OF USER CUSTOMIZATION SETTINGS------------------------
-else
-	protocols = {''}
-	extensions = {''}
-end
-
-if not device then
-	if os.getenv('windir') ~= nil then
-		device = 'windows'
-	elseif os.execute '[ -d "/Applications" ]' == 0 and os.execute '[ -d "/Library" ]' == 0 or os.execute '[ -d "/Applications" ]' == true and os.execute '[ -d "/Library" ]' == true then
-		device = 'mac'
-	else
-		device = 'linux'
-  end
-end
-
-function handleres(res, args)
-  if not res.error and res.status == 0 then
-      return res.stdout
-  else
-    msg.error("There was an error getting "..device.." clipboard: ")
-    msg.error("  Status: "..(res.status or ""))
-    msg.error("  Error: "..(res.error or ""))
-    msg.error("  stdout: "..(res.stdout or ""))
-    msg.error("args: "..utils.to_string(args))
-    return ''
-  end
-end
-
-function os.capture(cmd, raw)
-  local f = assert(io.popen(cmd, 'r'))
-  local s = assert(f:read('*a'))
-  f:close()
-  if raw then return s end
-  s = string.gsub(s, '^%s+', '')
-  s = string.gsub(s, '%s+$', '')
-  s = string.gsub(s, '[\n\r]+', ' ')
-  return s
-end
-
-local function get_extension(path)
-    match = string.match(path, '%.([^%.]+)$' )
-    if match == nil then
-        return 'nomatch'
-    else
-        return match
-    end
-end
-
-local function get_extentionpath(path)
-    match = string.match(path,'(.*)%.([^%.]+)$')
-    if match == nil then
-        return 'nomatch'
-    else
-        return match
-    end
-end
-
-local function has_extension (tab, val)
-    for index, value in ipairs(tab) do
-        if value == val then
-            return true
-        end
-    end
-
-    return false
-end
-
-local function starts_protocol (tab, val)
-    for index, value in ipairs(tab) do
-        if (val:find(value) == 1) then
-            return true
-        end
-	end
-    return false
-end
-
-
-function get_clipboard()
-local clip
-  if device == 'linux' then
-	clip = os.capture(linux_paste, false)
-	return clip
-  elseif device == 'windows' then
-	if windows_paste == 'powershell' then
-		local args = {
-		  'powershell', '-NoProfile', '-Command', [[& {
-				Trap {
-					Write-Error -ErrorRecord $_
-					Exit 1
-				}
-				$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
-				if ($clip) {
-					$clip = $clip
-					}
-				else {
-					$clip = Get-Clipboard -Raw -Format FileDropList
-				}
-				$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
-				[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
-			}]]
-		}
-		return handleres(utils.subprocess({ args =  args, cancellable = false }), args)
-	else
-		clip = os.capture(windows_paste, false)
-		return clip
-	end
-  elseif device == 'mac' then
-	clip = os.capture(mac_paste, false)
-	return clip
-  end
-  return ''
-end
-
-
-function set_clipboard(text)
-	local pipe
-	if device == 'linux' then
-		pipe = io.popen(linux_copy, 'w')
-		pipe:write(text)
-		pipe:close()
-	elseif device == 'windows' then
-		if windows_copy == 'powershell' then
-			local res = utils.subprocess({ args = {
-				'powershell', '-NoProfile', '-Command', string.format([[& {
-					Trap {
-						Write-Error -ErrorRecord $_
-						Exit 1
-					}
-					Add-Type -AssemblyName PresentationCore
-					[System.Windows.Clipboard]::SetText('%s')
-				}]], text)
-			} })
-		else
-			pipe = io.popen(windows_copy,'w')
-			pipe:write(text)
-			pipe:close()
-		end
-	elseif device == 'mac' then
-		pipe = io.popen(mac_copy,'w')
-		pipe:write(text)
-		pipe:close()
-	  end
-  return ''
-end
-
-
-local function copy()
-    local filePath = mp.get_property_native('path')
-	if (filePath ~= nil) then
-		local time = math.floor(mp.get_property_number('time-pos'))
-		set_clipboard(filePath..' |time='..tostring(time))
-		mp.osd_message("Copied:\n"..filePath..' |time='..tostring(time))
-	else
-		mp.osd_message('Failed to Copy\nNo Video Found')
-	end
-
-end
-
-
-local function copy_path()
-    local filePath = mp.get_property_native('path')
-	if (filePath ~= nil) then
-		set_clipboard(filePath)
-		mp.osd_message("Copied Video Only:\n"..filePath)
-	else
-		return false
-	end
-end
-
-
-function paste()
-	local clip = get_clipboard()
-	clip = string.gsub(clip, "[\r\n]" , "")
-
-	local filePath = mp.get_property_native('path')
-	local time
-
-	if string.match(clip, '(.*) |time=') then
-		videoFile = string.match(clip, '(.*) |time=')
-		time = string.match(clip, ' |time=(.*)')
-	elseif string.match(clip, '^\"(.*)\"$') then
-		videoFile = string.match(clip, '^\"(.*)\"$')
-	else
-		videoFile = clip
-	end
-	
-	local currentVideoExtension = string.lower(get_extension(videoFile))
-	local currentVideoExtensionPath = (get_extentionpath(videoFile))
-	
-	if (filePath == nil) and has_extension(extensions, currentVideoExtension) and (currentVideoExtensionPath~= '') then
-		mp.commandv('loadfile', videoFile)
-		mp.osd_message("Pasted:\n"..videoFile)
-	elseif (filePath == nil) and (starts_protocol(protocols, videoFile)) then
-		mp.commandv('loadfile', videoFile)
-		mp.osd_message("Pasted:\n"..videoFile)
-	elseif (filePath ~= nil) and (filePath ~= videoFile) and has_extension(extensions, currentVideoExtension) and (currentVideoExtensionPath~= '') or (starts_protocol(protocols, videoFile)) and (filePath ~= videoFile) then
-		mp.commandv('loadfile', videoFile, 'append-play')
-		mp.osd_message('Pasted Into Playlist:\n'..videoFile)
-	elseif (filePath == videoFile) and (time == nil) then
-		mp.osd_message('Same file is already running:\n'..clip)		
-	elseif (filePath == videoFile) and (time ~= nil) then
-		mp.commandv('seek', time, 'absolute', 'exact')
-		mp.osd_message('Resumed to Copied Time')
-	else
-		mp.osd_message('Failed to Paste\nPasted Unsupported Item:\n'..clip)
-	end
-	
-	pasted = true
-end
-
-
-function paste_playlist()
-	local clip = get_clipboard()
-	clip = string.gsub(clip, "[\r\n]" , "")
-	
-	local filePath = mp.get_property_native('path')
-
-	if string.match(clip, '(.*) |time=') then
-		videoFile = string.match(clip, '(.*) |time=')
-	elseif string.match(clip, '^\"(.*)\"$') then
-		videoFile = string.match(clip, '^\"(.*)\"$')
-	else
-		videoFile = clip
-	end
-	local currentVideoExtension = string.lower(get_extension(videoFile))
-	local currentVideoExtensionPath = (get_extentionpath(videoFile))
-	
-	if has_extension(extensions, currentVideoExtension) and (currentVideoExtensionPath~= '') or (starts_protocol(protocols, videoFile)) then
-		mp.commandv('loadfile', videoFile, 'append-play')
-		mp.osd_message('Pasted Into Playlist:\n'..videoFile)
-	else
-		mp.osd_message('Failed to Add Into Playlist\nPasted Unsupported Item:\n'..clip)
-	end
-	
-	pasted = true
-end
-
-
-mp.register_event('end-file', function()
-	pasted = false
-end)
-
-
-mp.register_event('file-loaded', function()
-	if (pasted == true) then
-		local clip = get_clipboard()
-		local time = string.match(clip, ' |time=(.*)')
-		local videoFile = string.match(clip, '(.*) |time=')
-		local filePath = mp.get_property_native('path')
-
-		if (filePath == videoFile) and (time ~= nil) then
-			mp.commandv('seek', time, 'absolute', 'exact')
-		end
-	else
-		return false
-	end
-end)
-
----------------------------KEYBINDS CUSTOMIZATION SETTINGS---------------------------------
-if device == 'mac' then --MAC OS Keybinds
-	mp.add_key_binding('Meta+c', 'copy', copy)
-	mp.add_key_binding('Meta+C', 'copyCaps', copy)
-	mp.add_key_binding('Meta+v', 'paste', paste)
-	mp.add_key_binding('Meta+V', 'pasteCaps', paste)
-
-	mp.add_key_binding('Meta+alt+c', 'copy-path', copy_path)
-	mp.add_key_binding('Meta+alt+C', 'copy-pathCaps', copy_path)
-	mp.add_key_binding('Meta+alt+v', 'paste-playlist', paste_playlist)
-	mp.add_key_binding('Meta+alt+V', 'paste-playlistCaps', paste_playlist)
-else --Windows and Linux Keybinds
-	mp.add_key_binding('ctrl+c', 'copy', copy)
-	mp.add_key_binding('ctrl+C', 'copyCaps', copy)
-	mp.add_key_binding('ctrl+v', 'paste', paste)
-	mp.add_key_binding('ctrl+V', 'pasteCaps', paste)
-
-	mp.add_key_binding('ctrl+alt+c', 'copy-path', copy_path)
-	mp.add_key_binding('ctrl+alt+C', 'copy-pathCaps', copy_path)
-	mp.add_key_binding('ctrl+alt+v', 'paste-playlist', paste_playlist)
-	mp.add_key_binding('ctrl+alt+V', 'paste-playlistCaps', paste_playlist)
-end
----------------------END OF KEYBINDS CUSTOMIZATION SETTINGS---------------------------------
diff --git a/.config/mpv/scripts/notify.lua b/.config/mpv/scripts/notify.lua
deleted file mode 100644
index 183d153..0000000
--- a/.config/mpv/scripts/notify.lua
+++ /dev/null
@@ -1,122 +0,0 @@
--- notify.lua -- Desktop notifications for mpv.
--- Just put this file into your ~/.mpv/lua folder and mpv will find it.
---
--- Copyright (c) 2014 Roland Hieber
---
--- Permission is hereby granted, free of charge, to any person obtaining a copy
--- of this software and associated documentation files (the "Software"), to deal
--- in the Software without restriction, including without limitation the rights
--- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--- copies of the Software, and to permit persons to whom the Software is
--- furnished to do so, subject to the following conditions:
---
--- The above copyright notice and this permission notice shall be included in
--- all copies or substantial portions of the Software.
---
--- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--- SOFTWARE.
-
--------------------------------------------------------------------------------
--- helper functions
--------------------------------------------------------------------------------
-
-function print_debug(s)
-	-- print("DEBUG: " .. s) -- comment out for no debug info
-	return true
-end
-
--- url-escape a string, per RFC 2396, Section 2
-function string.urlescape(str)
-	local s, c = string.gsub(str, "([^A-Za-z0-9_.!~*'()/-])",
-		function(c)
-			return ("%%%02x"):format(c:byte())
-		end)
-	return s;
-end
-
--- escape string for html
-function string.htmlescape(str)
-	local str = string.gsub(str, "<", "&lt;")
-	str = string.gsub(str, ">", "&gt;")
-	str = string.gsub(str, "&", "&amp;")
-	str = string.gsub(str, "\"", "&quot;")
-	str = string.gsub(str, "'", "&apos;")
-	return str
-end
-
--- escape string for shell inclusion
-function string.shellescape(str)
-	return "'"..string.gsub(str, "'", "'\"'\"'").."'"
-end
-
--- converts string to a valid filename on most (modern) filesystems
-function string.safe_filename(str)
-	local s, _ = string.gsub(str, "([^A-Za-z0-9_.-])",
-		function(c)
-			return ("%02x"):format(c:byte())
-		end)
-	return s;
-end
-
-function notify_current_track()
-	local data = mp.get_property_native("metadata")
-	if not data then
-		return
-	end
-
-	function get_metadata(data, keys)
-		for _,v in pairs(keys) do
-			if data[v] and string.len(data[v]) > 0 then
-				return data[v]
-			end
-		end
-		return ""
-	end
-	-- srsly MPV, why do we have to do this? :-(
-	local artist = get_metadata(data, {"artist", "ARTIST"})
-	local album = get_metadata(data, {"album", "ALBUM"})
-	local title = get_metadata(data, {"title", "TITLE", "icy-title"})
-
-	print_debug("notify_current_track: relevant metadata:")
-	print_debug("artist: " .. artist)
-	print_debug("album: " .. album)
-
-	local summary = ""
-	local body = ""
-	local params = ""
-
-	if(artist == "") then
-		summary = string.shellescape("Now playing:")
-	else
-		summary = string.shellescape(string.htmlescape(artist))
-	end
-	if title == "" then
-		body = string.shellescape(mp.get_property_native("filename"))
-	else
-		if album == "" then
-			body = string.shellescape(string.htmlescape(title))
-		else
-			body = string.shellescape(("%s<br /><i>%s</i>"):format(
-				string.htmlescape(title), string.htmlescape(album)))
-		end
-	end
-
-	local command = ("notify-send -a mpv %s -- %s %s"):format(params, summary, body)
-	print_debug("command: " .. command)
-	os.execute(command)
-end
-
-function notify_metadata_updated(name, data)
-	notify_current_track()
-end
-
-
--- insert main() here
-
-mp.register_event("file-loaded", notify_current_track)
-mp.observe_property("metadata", nil, notify_metadata_updated)
diff --git a/.config/mpv/scripts/uosc.lua b/.config/mpv/scripts/uosc.lua
deleted file mode 100644
index 7ace1af..0000000
--- a/.config/mpv/scripts/uosc.lua
+++ /dev/null
@@ -1,3243 +0,0 @@
---[[
-
-uosc 2.8.2 - 2020-May-06 | https://github.com/darsain/uosc
-
-Minimalist cursor proximity based UI for MPV player.
-
-uosc replaces the default osc UI, so that has to be disabled first.
-Place these options into your `mpv.conf` file:
-
-```
-# required so that the 2 UIs don't fight each other
-osc=no
-# uosc provides its own seeking/volume indicators, so you also don't need this
-osd-bar=no
-# uosc will draw its own window controls if you disable window border
-border=no
-```
-
-Options go in `script-opts/uosc.conf`. Defaults:
-
-```
-# timeline size when fully retracted, 0 will hide it completely
-timeline_size_min=2
-# timeline size when fully expanded, in pixels, 0 to disable
-timeline_size_max=40
-# same as ^ but when in fullscreen
-timeline_size_min_fullscreen=0
-timeline_size_max_fullscreen=60
-# same thing as calling toggle-progress command once on startup
-timeline_start_hidden=no
-# timeline opacity
-timeline_opacity=0.8
-# top (and bottom in no-border mode) border of background color to help visually
-# separate elapsed bar from a video of similar color or desktop background
-timeline_border=1
-# when scrolling above timeline, wheel will seek by this amount of seconds
-timeline_step=5
-# display seekable buffered ranges for streaming videos, syntax `color:opacity`,
-# color is an BBGGRR hex code, set to `none` to disable
-timeline_cached_ranges=345433:0.5
-# floating number font scale adjustment
-timeline_font_scale=1
-# briefly show timeline on external changes (e.g. seeking with a hotkey)
-timeline_flash=yes
-
-# timeline chapters style: none, dots, lines, lines-top, lines-bottom
-chapters=dots
-chapters_opacity=0.3
-
-# where to display volume controls: none, left, right
-volume=right
-volume_size=40
-volume_size_fullscreen=60
-volume_opacity=0.8
-volume_border=1
-volume_step=1
-volume_font_scale=1
-volume_flash=yes
-
-# playback speed widget: mouse drag or wheel to change, click to reset
-speed=no
-speed_size=46
-speed_size_fullscreen=68
-speed_opacity=1
-speed_step=0.1
-speed_font_scale=1
-speed_flash=yes
-
-# controls all menus, such as context menu, subtitle loader/selector, etc
-menu_item_height=36
-menu_item_height_fullscreen=50
-menu_opacity=0.8
-menu_font_scale=1
-
-# pause video on clicks shorter than this number of milliseconds, 0 to disable
-pause_on_click_shorter_than=0
-# for how long in milliseconds to show elements they're it's being flashed
-flash_duration=400
-# distances in pixels below which elements are fully faded in/out
-proximity_in=40
-proximity_out=120
-# BBGGRR - BLUE GREEN RED hex color codes
-color_foreground=ffffff
-color_foreground_text=000000
-color_background=000000
-color_background_text=ffffff
-# use bold font weight throughout the whole UI
-font_bold=no
-# hide UI when mpv autohides the cursor
-autohide=no
-# can be: none, flash, static
-pause_indicator=flash
-# window controls (minimize, maximize, close, title) show only in no-border
-# mode, but you can disable them completely by setting this to `no`
-window_controls=yes
-# display current media title in top window controls bar
-title=no
-# load first file when calling next on a last file in a directory and vice versa
-directory_navigation_loops=no
-# file types to look for when navigating media files
-media_types=3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv
-# file types to look for when loading external subtitles
-subtitle_types=aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt
-# used to approximate text width
-# if you are using some wide font and see a lot of right side clipping in menus,
-# try bumping this up
-font_height_to_letter_width_ratio=0.5
-
-# `chapter_ranges` lets you transform chapter indicators into range indicators.
-#
-# Chapter range definition syntax:
-# ```
-# start_pattern<color:opacity>end_pattern
-# ```
-#
-# Multiple start and end patterns can be defined by separating them with `|`:
-# ```
-# p1|pN<color:opacity>p1|pN
-# ```
-#
-# Multiple chapter ranges can be defined by separating them with comma:
-#
-# chapter_ranges=range1,rangeN
-#
-# One of `start_pattern`s can be a custom keyword `{bof}` that will match
-# beginning of file when it makes sense.
-#
-# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of
-# file when it makes sense.
-#
-# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial).
-# They only need to occur in a title, not match it completely.
-# Matching is case insensitive.
-#
-# `color` is a `bbggrr` hexadecimal color code.
-# `opacity` is a float number from 0 to 1.
-#
-# Examples:
-#
-# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
-# ```
-# chapter_ranges=sponsor start<3535a5:0.5>sponsor end
-# ```
-#
-# Display anime openings and endings as ranges:
-# ```
-# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
-# ```
-chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end
-```
-
-Available keybindings (place into `input.conf`):
-
-```
-Key  script-binding uosc/peek-timeline
-Key  script-binding uosc/toggle-progress
-Key  script-binding uosc/menu
-Key  script-binding uosc/load-subtitles
-Key  script-binding uosc/subtitles
-Key  script-binding uosc/audio
-Key  script-binding uosc/video
-Key  script-binding uosc/playlist
-Key  script-binding uosc/chapters
-Key  script-binding uosc/open-file
-Key  script-binding uosc/next
-Key  script-binding uosc/prev
-Key  script-binding uosc/first
-Key  script-binding uosc/last
-Key  script-binding uosc/next-file
-Key  script-binding uosc/prev-file
-Key  script-binding uosc/first-file
-Key  script-binding uosc/last-file
-Key  script-binding uosc/delete-file-next
-Key  script-binding uosc/delete-file-quit
-Key  script-binding uosc/show-in-directory
-Key  script-binding uosc/open-config-directory
-```
-]]
-
-if mp.get_property('osc') == 'yes' then
-	mp.msg.info('Disabled because original osc is enabled!')
-	return
-end
-
-local assdraw = require('mp.assdraw')
-local opt = require('mp.options')
-local utils = require('mp.utils')
-local msg = require('mp.msg')
-local osd = mp.create_osd_overlay('ass-events')
-local infinity = 1e309
-
--- OPTIONS/CONFIG/STATE
-local options = {
-	timeline_size_min = 2,
-	timeline_size_max = 40,
-	timeline_size_min_fullscreen = 0,
-	timeline_size_max_fullscreen = 60,
-	timeline_start_hidden = false,
-	timeline_opacity = 0.8,
-	timeline_border = 1,
-	timeline_step = 5,
-	timeline_cached_ranges = '345433:0.5',
-	timeline_font_scale = 1,
-	timeline_flash = true,
-
-	chapters = 'dots',
-	chapters_opacity = 0.3,
-
-	volume = 'right',
-	volume_size = 40,
-	volume_size_fullscreen = 60,
-	volume_opacity = 0.8,
-	volume_border = 1,
-	volume_step = 1,
-	volume_font_scale = 1,
-	volume_flash = true,
-
-	speed = false,
-	speed_size = 46,
-	speed_size_fullscreen = 68,
-	speed_opacity = 1,
-	speed_step = 0.1,
-	speed_font_scale = 1,
-	speed_flash = true,
-
-	menu_item_height = 36,
-	menu_item_height_fullscreen = 50,
-	menu_opacity = 0.8,
-	menu_font_scale = 1,
-
-	pause_on_click_shorter_than = 0,
-	flash_duration = 400,
-	proximity_in = 40,
-	proximity_out = 120,
-	color_foreground = 'ffffff',
-	color_foreground_text = '000000',
-	color_background = '000000',
-	color_background_text = 'ffffff',
-	font_bold = false,
-	autohide = false,
-	pause_indicator = 'flash',
-	window_controls = true,
-	title = false,
-	directory_navigation_loops = false,
-	media_types = '3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv',
-	subtitle_types = 'aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt',
-	font_height_to_letter_width_ratio = 0.5,
-	chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end',
-}
-opt.read_options(options, 'uosc')
-local config = {
-	render_delay = 0.03, -- sets max rendering frequency
-	font = mp.get_property('options/osd-font'),
-	menu_parent_opacity = 0.4,
-	menu_min_width = 260,
-	window_controls = {
-		button_width = 46,
-		height = 40,
-		icon_opacity = 0.8,
-		background_opacity = 0.8,
-	}
-}
-local bold_tag = options.font_bold and '\\b1' or ''
-local display = {
-	width = 1280,
-	height = 720,
-	aspect = 1.77778,
-}
-local cursor = {
-	hidden = true, -- true when autohidden or outside of the player window
-	x = 0,
-	y = 0,
-}
-local state = {
-	os = (function()
-		if os.getenv('windir') ~= nil then return 'windows' end
-		local homedir = os.getenv('HOME')
-		if homedir ~= nil and string.sub(homedir,1,6) == '/Users' then return 'macos' end
-		return 'linux'
-	end)(),
-	cwd = mp.get_property('working-directory'),
-	media_title = '',
-	duration = nil,
-	position = nil,
-	pause = false,
-	chapters = nil,
-	chapter_ranges = nil,
-	fullscreen = mp.get_property_native('fullscreen'),
-	maximized = mp.get_property_native('window-maximized'),
-	render_timer = nil,
-	render_last_time = 0,
-	volume = nil,
-	volume_max = nil,
-	mute = nil,
-	cursor_autohide_timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function()
-		if not options.autohide then return end
-		handle_mouse_leave()
-	end),
-	mouse_bindings_enabled = false,
-	cached_ranges = nil,
-}
-local forced_key_bindings -- defined at the bottom next to events
-
--- HELPERS
-
-function round(number)
-	local modulus = number % 1
-	return modulus < 0.5 and math.floor(number) or math.ceil(number)
-end
-
-function call_me_maybe(fn, value1, value2, value3)
-	if fn then fn(value1, value2, value3) end
-end
-
-function split(str, pattern)
-	local list = {}
-	local full_pattern = '(.-)' .. pattern
-	local last_end = 1
-	local start_index, end_index, capture = str:find(full_pattern, 1)
-	while start_index do
-		list[#list +1] = capture
-		last_end = end_index + 1
-		start_index, end_index, capture = str:find(full_pattern, last_end)
-	end
-	if last_end <= (#str + 1) then
-		capture = str:sub(last_end)
-		list[#list +1] = capture
-	end
-	return list
-end
-
-function itable_find(haystack, needle)
-	local is_needle = type(needle) == 'function' and needle or function(index, value)
-		return value == needle
-	end
-	for index, value in ipairs(haystack) do
-		if is_needle(index, value) then return index, value end
-	end
-end
-
-function itable_filter(haystack, needle)
-	local is_needle = type(needle) == 'function' and needle or function(index, value)
-		return value == needle
-	end
-	local filtered = {}
-	for index, value in ipairs(haystack) do
-		if is_needle(index, value) then filtered[#filtered + 1] = value end
-	end
-	return filtered
-end
-
-function itable_remove(haystack, needle)
-	local should_remove = type(needle) == 'function' and needle or function(value)
-		return value == needle
-	end
-	local new_table = {}
-	for _, value in ipairs(haystack) do
-		if not should_remove(value) then
-			new_table[#new_table + 1] = value
-		end
-	end
-	return new_table
-end
-
-function itable_slice(haystack, start_pos, end_pos)
-	start_pos = start_pos and start_pos or 1
-	end_pos = end_pos and end_pos or #haystack
-
-	if end_pos < 0 then end_pos = #haystack + end_pos + 1 end
-	if start_pos < 0 then start_pos = #haystack + start_pos + 1 end
-
-	local new_table = {}
-	for index, value in ipairs(haystack) do
-		if index >= start_pos and index <= end_pos then
-			new_table[#new_table + 1] = value
-		end
-	end
-	return new_table
-end
-
-function table_copy(table)
-	local new_table = {}
-	for key, value in pairs(table) do new_table[key] = value end
-	return new_table
-end
-
--- Sorting comparator close to (but not exactly) how file explorers sort files
-local word_order_comparator = (function()
-	local symbol_order
-	local default_order
-
-	if state.os == 'win' then
-		symbol_order = {
-			['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
-			['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
-			['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
-		}
-		default_order = 21
-	else
-		symbol_order = {
-			['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
-			['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
-			['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
-		}
-		default_order = 21
-	end
-
-	return function (a, b)
-		a = a:lower()
-		b = b:lower()
-		for i = 1, math.max(#a, #b) do
-			local ai = a:sub(i, i)
-			local bi = b:sub(i, i)
-			if ai == nil and bi then return true end
-			if bi == nil and ai then return false end
-			local a_order = symbol_order[ai] or default_order
-			local b_order = symbol_order[bi] or default_order
-			if a_order == b_order then
-				return a < b
-			else
-				return a_order < b_order
-			end
-		end
-	end
-end)()
-
--- Creates in-between frames to animate value from `from` to `to` numbers.
--- Returns function that terminates animation.
--- `to` can be a function that returns target value, useful for movable targets.
--- `speed` is an optional float between 1-instant and 0-infinite duration
--- `callback` is called either on animation end, or when animation is canceled
-function tween(from, to, setter, speed, callback)
-	if type(speed) ~= 'number' then
-		callback = speed
-		speed = 0.3
-	end
-	local timeout
-	local getTo = type(to) == 'function' and to or function() return to end
-	local cutoff = math.abs(getTo() - from) * 0.01
-	function tick()
-		from = from + ((getTo() - from) * speed)
-		local is_end = math.abs(getTo() - from) <= cutoff
-		setter(is_end and getTo() or from)
-		request_render()
-		if is_end then
-			call_me_maybe(callback)
-		else
-			timeout:resume()
-		end
-	end
-	timeout = mp.add_timeout(0.016, tick)
-	tick()
-	return function()
-		timeout:kill()
-		call_me_maybe(callback)
-	end
-end
-
--- Kills ongoing animation if one is already running on this element.
--- Killed animation will not get its `on_end` called.
-function tween_element(element, from, to, setter, speed, callback)
-	if type(speed) ~= 'number' then
-		callback = speed
-		speed = 0.3
-	end
-
-	tween_element_stop(element)
-
-	element.stop_current_animation = tween(
-		from, to,
-		function(value) setter(element, value) end,
-		speed,
-		function()
-			element.stop_current_animation = nil
-			call_me_maybe(callback, element)
-		end
-	)
-end
-
--- Stopped animation will not get its on_end called.
-function tween_element_is_tweening(element)
-	return element and element.stop_current_animation
-end
-
--- Stopped animation will not get its on_end called.
-function tween_element_stop(element)
-	call_me_maybe(element and element.stop_current_animation)
-end
-
--- Helper to automatically use an element property setter
-function tween_element_property(element, prop, from, to, speed, callback)
-	tween_element(element, from, to, function(_, value) element[prop] = value end, speed, callback)
-end
-
-function get_point_to_rectangle_proximity(point, rect)
-	local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx + 1)
-	local dy = math.max(rect.ay - point.y, 0, point.y - rect.by + 1)
-	return math.sqrt(dx*dx + dy*dy);
-end
-
-function text_width_estimate(letters, font_size)
-	return letters and letters * font_size * options.font_height_to_letter_width_ratio or 0
-end
-
-function opacity_to_alpha(opacity)
-	return 255 - math.ceil(255 * opacity)
-end
-
-function ass_opacity(opacity, fraction)
-	fraction = fraction ~= nil and fraction or 1
-	if type(opacity) == 'number' then
-		return string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
-	else
-		return string.format(
-			'{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
-			opacity_to_alpha((opacity[1] or 0) * fraction),
-			opacity_to_alpha((opacity[2] or 0) * fraction),
-			opacity_to_alpha((opacity[3] or 0) * fraction),
-			opacity_to_alpha((opacity[4] or 0) * fraction)
-		)
-	end
-end
-
--- Ensures path is absolute and normalizes slashes to the current platform
-function normalize_path(path)
-	if not path or is_protocol(path) then return path end
-
-	-- Ensure path is absolute
-	if not (path:match('^/') or path:match('^%a+:') or path:match('^\\\\')) then
-		path = utils.join_path(state.cwd, path)
-	end
-
-	-- Use proper slashes
-	if state.os == 'windows' then
-		return path:gsub('/', '\\')
-	else
-		return path:gsub('\\', '/')
-	end
-end
-
--- Check if path is a protocol, such as `http://...`
-function is_protocol(path)
-	return path:match('^%a[%a%d-_]+://')
-end
-
-function get_extension(path)
-	local parts = split(path, '%.')
-	return parts and #parts > 1 and parts[#parts] or nil
-end
-
--- Serializes path into its semantic parts
-function serialize_path(path)
-	if not path or is_protocol(path) then return end
-	path = normalize_path(path)
-	local parts = split(path, '[\\/]+')
-	if parts[#parts] == '' then table.remove(parts, #parts) end -- remove trailing separator
-	local basename = parts and parts[#parts] or path
-	local dirname = #parts > 1 and table.concat(itable_slice(parts, 1, #parts - 1), state.os == 'windows' and '\\' or '/') or nil
-	local dot_split = split(basename, '%.')
-	return {
-		path = path:sub(-1) == ':' and state.os == 'windows' and path..'\\' or path,
-		is_root = dirname == nil,
-		dirname = dirname,
-		basename = basename,
-		filename = #dot_split > 1 and table.concat(itable_slice(dot_split, 1, #dot_split - 1), '.') or basename,
-		extension = #dot_split > 1 and dot_split[#dot_split] or nil,
-	}
-end
-
-function get_files_in_directory(directory, allowed_types)
-	local files, error = utils.readdir(directory, 'files')
-
-	if not files then
-		msg.error('Retrieving files failed: '..(error or ''))
-		return
-	end
-
-	-- Filter only requested file types
-	if allowed_types then
-		files = itable_filter(files, function(_, file)
-			local extension = get_extension(file)
-			return extension and itable_find(allowed_types, extension:lower())
-		end)
-	end
-
-	table.sort(files, word_order_comparator)
-
-	return files
-end
-
-function get_adjacent_file(file_path, direction, allowed_types)
-	local current_file = serialize_path(file_path)
-	local files = get_files_in_directory(current_file.dirname, allowed_types)
-
-	if not files then return end
-
-	for index, file in ipairs(files) do
-		if current_file.basename == file then
-			if direction == 'forward' then
-				if files[index + 1] then return utils.join_path(current_file.dirname, files[index + 1]) end
-				if options.directory_navigation_loops and files[1] then return utils.join_path(current_file.dirname, files[1]) end
-			else
-				if files[index - 1] then return utils.join_path(current_file.dirname, files[index - 1]) end
-				if options.directory_navigation_loops and files[#files] then return utils.join_path(current_file.dirname, files[#files]) end
-			end
-
-			-- This is the only file in directory
-			return nil
-		end
-	end
-end
-
--- Ensures chapters are in chronological order
-function get_normalized_chapters()
-	local chapters = mp.get_property_native('chapter-list')
-
-	if not chapters then return end
-
-	-- Copy table
-	chapters = itable_slice(chapters)
-
-	-- Ensure chronological order of chapters
-	table.sort(chapters, function(a, b) return a.time < b.time end)
-
-	return chapters
-end
-
--- Element
---[[
-Signature:
-{
-	-- enables capturing button groups for this element
-	captures = {mouse_buttons = true, wheel = true},
-	-- element rectangle coordinates
-	ax = 0, ay = 0, bx = 0, by = 0,
-	-- cursor<>element relative proximity as a 0-1 floating number
-	-- where 0 = completely away, and 1 = touching/hovering
-	-- so it's easy to work with and throw into equations
-	proximity = 0,
-	-- raw cursor<>element proximity in pixels
-	proximity_raw = infinity,
-	-- called when element is created
-	?init = function(this),
-	-- called manually when disposing of element
-	?destroy = function(this),
-	-- triggered when event happens and cursor is above element
-	?on_{event_name} = function(this),
-	-- triggered when any event happens anywhere on a page
-	?on_global_{event_name} = function(this),
-	-- object
-	?render = function(this_element),
-}
-]]
-local Element = {
-	captures = nil,
-	ax = 0, ay = 0, bx = 0, by = 0,
-	proximity = 0, proximity_raw = infinity,
-}
-Element.__index = Element
-
-function Element.new(props)
-	local element = setmetatable(props, Element)
-	element._eventListeners = {}
-
-	-- Flash timer
-	element._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
-		local getTo = function() return element.proximity end
-		element:tween_property('forced_proximity', 1, getTo, function()
-			element.forced_proximity = nil
-		end)
-	end)
-	element._flash_out_timer:kill()
-
-	element:init()
-
-	return element
-end
-
-function Element:init() end
-function Element:destroy() end
-
--- Call method if it exists
-function Element:maybe(name, ...)
-	if self[name] then return self[name](self, ...) end
-end
-
--- Tween helpers
-function Element:tween(...) tween_element(self, ...) end
-function Element:tween_property(...) tween_element_property(self, ...) end
-function Element:tween_stop() tween_element_stop(self) end
-function Element:is_tweening() tween_element_is_tweening(self) end
-
--- Event listeners
-function Element:on(name, handler)
-	if self._eventListeners[name] == nil then self._eventListeners[name] = {} end
-	local preexistingIndex = itable_find(self._eventListeners[name], handler)
-	if preexistingIndex then
-		return
-	else
-		self._eventListeners[name][#self._eventListeners[name] + 1] = handler
-	end
-end
-function Element:off(name, handler)
-	if self._eventListeners[name] == nil then return end
-	local index = itable_find(self._eventListeners, handler)
-	if index then table.remove(self._eventListeners, index) end
-end
-function Element:trigger(name, ...)
-	self:maybe('on_'..name, ...)
-	if self._eventListeners[name] == nil then return end
-	for _, handler in ipairs(self._eventListeners[name]) do handler(...) end
-end
-
--- Briefly flashes the element for `options.flash_duration` milliseconds.
--- Useful to visualize changes of volume and timeline when changed via hotkeys.
--- Implemented by briefly adding animated `forced_proximity` property to the element.
-function Element:flash()
-	if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
-		self:tween_stop()
-		self.forced_proximity = 1
-		self._flash_out_timer:kill()
-		self._flash_out_timer:resume()
-	end
-end
-
--- ELEMENTS
-
-local Elements = {itable = {}}
-Elements.__index = Elements
-local elements = setmetatable({}, Elements)
-
-function Elements:add(name, element)
-	local insert_index = #Elements.itable + 1
-
-	-- Replace if element already exists
-	if self:has(name) then
-		insert_index = itable_find(Elements.itable, function(_, element)
-			return element.name == name
-		end)
-	end
-
-	element.name = name
-	Elements.itable[insert_index] = element
-	self[name] = element
-
-	request_render()
-end
-
-function Elements:remove(name, props)
-	Elements.itable = itable_remove(Elements.itable, self[name])
-	self[name] = nil
-	request_render()
-end
-
-function Elements:has(name) return self[name] ~= nil end
-function Elements:ipairs() return ipairs(Elements.itable) end
-function Elements:pairs(elements) return pairs(self) end
-
--- MENU
---[[
-Usage:
-```
-local items = {
-	{title = 'Foo title', hint = 'Ctrl+F', value = 'foo'},
-	{title = 'Bar title', hint = 'Ctrl+B', value = 'bar'},
-	{
-		title = 'Submenu',
-		items = {
-			{title = 'Sub item 1', value = 'sub1'},
-			{title = 'Sub item 2', value = 'sub2'}
-		}
-	}
-}
-
-function open_item(value)
-	value -- value from `item.value`
-end
-
-menu:open(items, open_item)
-```
-]]
-local Menu = {}
-Menu.__index = Menu
-local menu = setmetatable({key_bindings = {}, is_closing = false}, Menu)
-
-function Menu:is_open(menu_type)
-	return elements.menu ~= nil and (not menu_type or elements.menu.type == menu_type)
-end
-
-function Menu:open(items, open_item, opts)
-	opts = opts or {}
-
-	if menu:is_open() then
-		if not opts.parent_menu then
-			menu:close(true, function()
-				menu:open(items, open_item, opts)
-			end)
-			return
-		end
-	else
-		menu:enable_key_bindings()
-		elements.curtain:fadein()
-	end
-
-	elements:add('menu', Element.new({
-		captures = {mouse_buttons = true},
-		type = nil, -- menu type such as `menu`, `chapters`, ...
-		title = nil,
-		width = nil,
-		height = nil,
-		offset_x = 0, -- used to animated from/to left when submenu
-		item_height = nil,
-		item_spacing = 1,
-		item_content_spacing = nil,
-		font_size = nil,
-		scroll_step = nil,
-		scroll_height = nil,
-		scroll_y = 0,
-		opacity = 0,
-		relative_parent_opacity = 0.4,
-		items = items,
-		active_item = nil,
-		selected_item = nil,
-		open_item = open_item,
-		parent_menu = nil,
-		init = function(this)
-			-- Already initialized
-			if this.width ~= nil then return end
-
-			-- Apply options
-			for key, value in pairs(opts) do this[key] = value end
-			this.selected_item = this.active_item
-
-			-- Set initial dimensions
-			this:on_display_resize()
-
-			-- Scroll to active item
-			this:scroll_to_item(this.active_item)
-
-			-- Transition in animation
-			menu.transition = {to = 'child', target = this}
-			local start_offset = this.parent_menu and (this.parent_menu.width + this.width) / 2 or 0
-
-			tween_element(menu.transition.target, 0, 1, function(_, pos)
-				this:set_offset_x(round(start_offset * (1 - pos)))
-				this.opacity = pos
-				this:set_parent_opacity(1 - ((1 - config.menu_parent_opacity) * pos))
-			end, function()
-				menu.transition = nil
-				update_proximities()
-			end)
-		end,
-		destroy = function(this)
-			request_render()
-		end,
-		on_display_resize = function(this)
-			this.item_height = (state.fullscreen or state.maximized) and options.menu_item_height_fullscreen or options.menu_item_height
-			this.font_size = round(this.item_height * 0.48 * options.menu_font_scale)
-			this.item_content_spacing = round((this.item_height - this.font_size) * 0.6)
-			this.scroll_step = this.item_height + this.item_spacing
-
-			-- Estimate width of a widest item
-			local estimated_max_width = 0
-			for _, item in ipairs(items) do
-				local item_text_length = ((item.title and item.title:len() or 0) + (item.hint and item.hint:len() or 0))
-				local spacings_in_item = item.hint and 3 or 2
-				local estimated_width = text_width_estimate(item_text_length, this.font_size) + (this.item_content_spacing * spacings_in_item)
-				if estimated_width > estimated_max_width then
-					estimated_max_width = estimated_width
-				end
-			end
-
-			-- Also check menu title
-			local menu_title_length = this.title and this.title:len() or 0
-			local estimated_menu_title_width = text_width_estimate(menu_title_length, this.font_size)
-			if estimated_menu_title_width > estimated_max_width then
-				estimated_max_width = estimated_menu_title_width
-			end
-
-			-- Coordinates and sizes are of the scrollable area to make
-			-- consuming values in rendering easier. Title drawn above this, so
-			-- we need to account for that in max_height and ay position.
-			this.width = round(math.min(math.max(estimated_max_width, config.menu_min_width), display.width * 0.9))
-			local title_height = this.title and this.scroll_step or 0
-			local max_height = round(display.height * 0.9) - title_height
-			this.height = math.min(round(this.scroll_step * #items) - this.item_spacing, max_height)
-			this.scroll_height = math.max((this.scroll_step * #this.items) - this.height - this.item_spacing, 0)
-			this.ax = round((display.width - this.width) / 2) + this.offset_x
-			this.ay = round((display.height - this.height) / 2 + (title_height / 2))
-			this.bx = round(this.ax + this.width)
-			this.by = round(this.ay + this.height)
-
-			if this.parent_menu then
-				this.parent_menu:on_display_resize()
-			end
-		end,
-		set_items = function(this, items, props)
-			this.items = items
-			this.selected_item = nil
-			this.active_item = nil
-			if props then
-				for key, value in pairs(props) do this[key] = value end
-			end
-			this:on_display_resize()
-			request_render()
-		end,
-		set_offset_x = function(this, offset)
-			local delta = offset - this.offset_x
-			this.offset_x = offset
-			this.ax = this.ax + delta
-			this.bx = this.bx + delta
-			if this.parent_menu then
-				this.parent_menu:set_offset_x(offset - ((this.width + this.parent_menu.width) / 2) - this.item_spacing)
-			else
-				update_proximities()
-			end
-		end,
-		fadeout = function(this, callback)
-			this:tween(1, 0, function(this, pos)
-				this.opacity = pos
-				this:set_parent_opacity(pos * config.menu_parent_opacity)
-			end, callback)
-		end,
-		set_parent_opacity = function(this, opacity)
-			if this.parent_menu then
-				this.parent_menu.opacity = opacity
-				this.parent_menu:set_parent_opacity(opacity * config.menu_parent_opacity)
-			end
-		end,
-		get_item_index_below_cursor = function(this)
-			return math.ceil((cursor.y - this.ay + this.scroll_y) / this.scroll_step)
-		end,
-		get_first_visible_index = function(this)
-			return round(this.scroll_y / this.scroll_step) + 1
-		end,
-		get_last_visible_index = function(this)
-			return round((this.scroll_y + this.height) / this.scroll_step)
-		end,
-		get_centermost_visible_index = function(this)
-			return round((this.scroll_y + (this.height / 2)) / this.scroll_step)
-		end,
-		scroll_to = function(this, pos)
-			this.scroll_y = math.max(math.min(pos, this.scroll_height), 0)
-			request_render()
-		end,
-		scroll_to_item = function(this, index)
-			if (index and index >= 1 and index <= #this.items) then
-				this:scroll_to(round((this.scroll_step * (index - 1)) - ((this.height - this.scroll_step) / 2)))
-			end
-		end,
-		select_index = function(this, index)
-			this.selected_item = (index and index >= 1 and index <= #this.items) and index or nil
-			request_render()
-		end,
-		select_value = function(this, value)
-			this:select_index(itable_find(this.items, function(_, item) return item.value == value end))
-		end,
-		activate_index = function(this, index)
-			this.active_item = (index and index >= 1 and index <= #this.items) and index or nil
-			request_render()
-		end,
-		activate_value = function(this, value)
-			this:activate_index(itable_find(this.items, function(_, item) return item.value == value end))
-		end,
-		delete_index = function(this, index)
-			if (index and index >= 1 and index <= #this.items) then
-				local previous_active_value = this.active_index and this.items[this.active_index].value or nil
-				table.remove(this.items, index)
-				this:on_display_resize()
-				if previous_active_value then this:activate_value(previous_active_value) end
-				this:scroll_to_item(this.selected_item)
-			end
-		end,
-		delete_value = function(this, value)
-			this:delete_index(itable_find(this.items, function(_, item) return item.value == value end))
-		end,
-		prev = function(this)
-			local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_last_visible_index()
-			local current_index = this.selected_item or default_anchor + 1
-			this.selected_item = math.max(current_index - 1, 1)
-			this:scroll_to_item(this.selected_item)
-		end,
-		next = function(this)
-			local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_first_visible_index()
-			local current_index = this.selected_item or default_anchor - 1
-			this.selected_item = math.min(current_index + 1, #this.items)
-			this:scroll_to_item(this.selected_item)
-		end,
-		back = function(this)
-			if menu.transition then
-				local transition_target = menu.transition.target
-				local transition_target_type = menu.transition.target
-				tween_element_stop(transition_target)
-				if transition_target_type == 'parent' then
-					elements:add('menu', transition_target)
-				end
-				menu.transition = nil
-				transition_target:back()
-				return
-			else
-				menu.transition = {to = 'parent', target = this.parent_menu}
-			end
-
-			if menu.transition.target == nil then
-				menu:close()
-				return
-			end
-
-			local target = menu.transition.target
-			local to_offset = -target.offset_x + this.offset_x
-
-			tween_element(target, 0, 1, function(_, pos)
-				this:set_offset_x(round(to_offset * pos))
-				this.opacity = 1 - pos
-				this:set_parent_opacity(config.menu_parent_opacity + ((1 - config.menu_parent_opacity) * pos))
-			end, function()
-				menu.transition = nil
-				elements:add('menu', target)
-				update_proximities()
-			end)
-		end,
-		open_selected_item = function(this)
-			-- If there is a transition active and this method got called, it
-			-- means we are animating from this menu to parent menu, and all
-			-- calls to this method should be relayed to the parent menu.
-			if menu.transition and menu.transition.to == 'parent' then
-				local target = menu.transition.target
-				tween_element_stop(target)
-				menu.transition = nil
-				target:open_selected_item()
-				return
-			end
-
-			if this.selected_item then
-				local item = this.items[this.selected_item]
-				-- Is submenu
-				if item.items then
-					local opts = table_copy(opts)
-					opts.parent_menu = this
-					menu:open(item.items, this.open_item, opts)
-				else
-					menu:close(true)
-					this.open_item(item.value)
-				end
-			end
-		end,
-		close = function(this)
-			menu:close()
-		end,
-		on_global_mbtn_left_down = function(this)
-			if this.proximity_raw == 0 then
-				this.selected_item = this:get_item_index_below_cursor()
-				this:open_selected_item()
-			else
-				-- check if this is clicking on any parent menus
-				local parent_menu = this.parent_menu
-				repeat
-					if parent_menu then
-						if get_point_to_rectangle_proximity(cursor, parent_menu) == 0 then
-							this:back()
-							return
-						end
-						parent_menu = parent_menu.parent_menu
-					end
-				until parent_menu == nil
-
-				menu:close()
-			end
-		end,
-		on_global_mouse_move = function(this)
-			if this.proximity_raw == 0 then
-				this.selected_item = this:get_item_index_below_cursor()
-			else
-				if this.selected_item then this.selected_item = nil end
-			end
-			request_render()
-		end,
-		on_wheel_up = function(this)
-			this.selected_item = nil
-			this:scroll_to(this.scroll_y - this.scroll_step)
-			-- Selects item below cursor
-			this:on_global_mouse_move()
-			request_render()
-		end,
-		on_wheel_down = function(this)
-			this.selected_item = nil
-			this:scroll_to(this.scroll_y + this.scroll_step)
-			-- Selects item below cursor
-			this:on_global_mouse_move()
-			request_render()
-		end,
-		on_pgup = function(this)
-			this.selected_item = nil
-			this:scroll_to(this.scroll_y - this.height)
-		end,
-		on_pgdwn = function(this)
-			this.selected_item = nil
-			this:scroll_to(this.scroll_y + this.height)
-		end,
-		on_home = function(this)
-			this.selected_item = nil
-			this:scroll_to(0)
-		end,
-		on_end = function(this)
-			this.selected_item = nil
-			this:scroll_to(this.scroll_height)
-		end,
-		render = render_menu,
-	}))
-
-	elements.menu:maybe('on_open')
-end
-
-function Menu:add_key_binding(key, name, fn, flags)
-	menu.key_bindings[#menu.key_bindings + 1] = name
-	mp.add_forced_key_binding(key, name, fn, flags)
-end
-
-function Menu:enable_key_bindings()
-	menu.key_bindings = {}
-	-- The `mp.set_key_bindings()` method would be easier here, but that
-	-- doesn't support 'repeatable' flag, so we are stuck with this monster.
-	menu:add_key_binding('up',         'menu-prev',        self:create_action('prev'), 'repeatable')
-	menu:add_key_binding('w',          'menu-prev-alt',    self:create_action('prev'), 'repeatable')
-	menu:add_key_binding('k',          'menu-prev-alt2',   self:create_action('prev'), 'repeatable')
-	menu:add_key_binding('down',       'menu-next',        self:create_action('next'), 'repeatable')
-	menu:add_key_binding('s',          'menu-next-alt',    self:create_action('next'), 'repeatable')
-	menu:add_key_binding('j',          'menu-next-alt2',   self:create_action('next'), 'repeatable')
-	menu:add_key_binding('left',       'menu-back',        self:create_action('back'))
-	menu:add_key_binding('a',          'menu-back-alt',    self:create_action('back'))
-	menu:add_key_binding('h',          'menu-back-alt2',   self:create_action('back'))
-	menu:add_key_binding('mbtn_back',  'menu-back-alt3',   self:create_action('back'))
-	menu:add_key_binding('bs',         'menu-back-alt4',   self:create_action('back'))
-	menu:add_key_binding('right',      'menu-select',      self:create_action('open_selected_item'))
-	menu:add_key_binding('d',          'menu-select-alt',  self:create_action('open_selected_item'))
-	menu:add_key_binding('l',          'menu-select-alt2', self:create_action('open_selected_item'))
-	menu:add_key_binding('enter',      'menu-select-alt3', self:create_action('open_selected_item'))
-	menu:add_key_binding('kp_enter',   'menu-select-alt4', self:create_action('open_selected_item'))
-	menu:add_key_binding('esc',        'menu-close',       self:create_action('close'))
-	menu:add_key_binding('pgup',       'menu-page-up',     self:create_action('on_pgup'))
-	menu:add_key_binding('pgdwn',      'menu-page-down',   self:create_action('on_pgdwn'))
-	menu:add_key_binding('home',       'menu-home',        self:create_action('on_home'))
-	menu:add_key_binding('end',        'menu-end',         self:create_action('on_end'))
-end
-
-function Menu:disable_key_bindings()
-	for _, name in ipairs(menu.key_bindings) do mp.remove_key_binding(name) end
-	menu.key_bindings = {}
-end
-
-function Menu:create_action(name)
-	return function(...)
-		if elements.menu then elements.menu:maybe(name, ...) end
-	end
-end
-
-function Menu:close(immediate, callback)
-	if type(immediate) ~= 'boolean' then callback = immediate end
-
-	if elements:has('menu') and not menu.is_closing then
-		function close()
-			elements.menu:maybe('on_close')
-			elements.menu:destroy()
-			elements:remove('menu')
-			menu.is_closing = false
-			update_proximities()
-			menu:disable_key_bindings()
-			call_me_maybe(callback)
-		end
-
-		menu.is_closing = true
-		elements.curtain:fadeout()
-
-		if immediate then
-			close()
-		else
-			elements.menu:fadeout(close)
-		end
-	end
-end
-
--- ICONS
---[[
-ASS \shadN shadows are drawn also below the element, which when there is an
-opacity in play, blends icon colors into ugly greys. The mess below is an
-attempt to fix it by rendering shadows for icons with clipping.
-
-Add icons by adding functions to render them to `icons` table.
-
-Signature: function(pos_x, pos_y, size) => string
-
-Function has to return ass path coordinates to draw the icon centered at pox_x
-and pos_y of passed size.
-]]
-local icons = {}
-function icon(name, icon_x, icon_y, icon_size, shad_x, shad_y, shad_size, backdrop, opacity, clip)
-	local ass = assdraw.ass_new()
-	local icon_path = icons[name](icon_x, icon_y, icon_size)
-	local icon_color = options['color_'..backdrop..'_text']
-	local shad_color = options['color_'..backdrop]
-	local use_border = (shad_x + shad_y) == 0
-	local icon_border = use_border and shad_size or 0
-
-	-- clip can't clip out shadows, a very annoying limitation I can't work
-	-- around without going back to ugly default ass shadows, but atm I actually
-	-- don't need clipping of icons with shadows, so I'm choosing to ignore this
-	if not clip then
-		clip = ''
-	end
-
-	if not use_border then
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\shad0\\1c&H'..shad_color..'\\iclip('..ass.scale..', '..icon_path..')}')
-		ass:append(ass_opacity(opacity))
-		ass:pos(shad_x + shad_size, shad_y + shad_size)
-		ass:draw_start()
-		ass:append(icon_path)
-		ass:draw_stop()
-	end
-
-	ass:new_event()
-	ass:append('{\\blur0\\bord'..icon_border..'\\shad0\\1c&H'..icon_color..'\\3c&H'..shad_color..clip..'}')
-	ass:append(ass_opacity(opacity))
-	ass:pos(0, 0)
-	ass:draw_start()
-	ass:append(icon_path)
-	ass:draw_stop()
-
-	return ass.text
-end
-
-function icons._volume(muted, pos_x, pos_y, size)
-	local ass = assdraw.ass_new()
-	local scale = size / 200
-	function x(number) return pos_x + (number * scale) end
-	function y(number) return pos_y + (number * scale) end
-	ass:move_to(x(-85), y(-35))
-	ass:line_to(x(-50), y(-35))
-	ass:line_to(x(-5), y(-75))
-	ass:line_to(x(-5), y(75))
-	ass:line_to(x(-50), y(35))
-	ass:line_to(x(-85), y(35))
-	if muted then
-		ass:move_to(x(76), y(-35)) ass:line_to(x(50), y(-9)) ass:line_to(x(24), y(-35))
-		ass:line_to(x(15), y(-26)) ass:line_to(x(41), y(0)) ass:line_to(x(15), y(26))
-		ass:line_to(x(24), y(35)) ass:line_to(x(50), y(9)) ass:line_to(x(76), y(35))
-		ass:line_to(x(85), y(26)) ass:line_to(x(59), y(0)) ass:line_to(x(85), y(-26))
-	else
-		ass:move_to(x(20), y(-30)) ass:line_to(x(20), y(30))
-		ass:line_to(x(35), y(30)) ass:line_to(x(35), y(-30))
-
-		ass:move_to(x(55), y(-60)) ass:line_to(x(55), y(60))
-		ass:line_to(x(70), y(60)) ass:line_to(x(70), y(-60))
-	end
-	return ass.text
-end
-function icons.volume(pos_x, pos_y, size) return icons._volume(false, pos_x, pos_y, size) end
-function icons.volume_muted(pos_x, pos_y, size) return icons._volume(true, pos_x, pos_y, size) end
-
-function icons.arrow_right(pos_x, pos_y, size)
-	local ass = assdraw.ass_new()
-	local scale = size / 200
-	function x(number) return pos_x + (number * scale) end
-	function y(number) return pos_y + (number * scale) end
-	ass:move_to(x(-22), y(-80))
-	ass:line_to(x(-45), y(-57))
-	ass:line_to(x(12), y(0))
-	ass:line_to(x(-45), y(57))
-	ass:line_to(x(-22), y(80))
-	ass:line_to(x(58), y(0))
-	return ass.text
-end
-
--- STATE UPDATES
-
-function update_display_dimensions()
-	local o = mp.get_property_native('osd-dimensions')
-	display.width = o.w
-	display.height = o.h
-	display.aspect = o.aspect
-
-	-- Tell elements about this
-	for _, element in elements:ipairs() do
-		if element.on_display_resize ~= nil then
-			element.on_display_resize(element)
-		end
-	end
-end
-
-function update_element_cursor_proximity(element)
-	if cursor.hidden then
-		element.proximity_raw = infinity
-		element.proximity = 0
-	else
-		local range = options.proximity_out - options.proximity_in
-		element.proximity_raw = get_point_to_rectangle_proximity(cursor, element)
-		element.proximity = menu:is_open() and 0 or 1 - (math.min(math.max(element.proximity_raw - options.proximity_in, 0), range) / range)
-	end
-end
-
-function update_proximities()
-	local capture_mouse_buttons = false
-	local capture_wheel = false
-	local menu_only = menu:is_open()
-	local mouse_left_elements = {}
-	local mouse_entered_elements = {}
-
-	-- Calculates proximities and opacities for defined elements
-	for _, element in elements:ipairs() do
-		local previous_proximity_raw = element.proximity_raw
-
-		-- If menu is open, all other elements have to be disabled
-		if menu_only then
-			if element.name == 'menu' then
-				capture_mouse_buttons = true
-				capture_wheel = true
-				update_element_cursor_proximity(element)
-			else
-				element.proximity_raw = infinity
-				element.proximity = 0
-			end
-		else
-			update_element_cursor_proximity(element)
-		end
-
-		if element.proximity_raw == 0 then
-			-- Mouse is over element
-			if element.captures and element.captures.mouse_buttons then capture_mouse_buttons = true end
-			if element.captures and element.captures.wheel then capture_wheel = true end
-
-			-- Mouse entered element area
-			if previous_proximity_raw ~= 0 then
-				mouse_entered_elements[#mouse_entered_elements + 1] = element
-			end
-		else
-			-- Mouse left element area
-			if previous_proximity_raw == 0 then
-				mouse_left_elements[#mouse_left_elements + 1] = element
-			end
-		end
-	end
-
-	-- Enable key group captures elements request.
-	if capture_mouse_buttons then
-		forced_key_bindings.mouse_buttons:enable()
-	else
-		forced_key_bindings.mouse_buttons:disable()
-	end
-	if capture_wheel then
-		forced_key_bindings.wheel:enable()
-	else
-		forced_key_bindings.wheel:disable()
-	end
-
-	-- Trigger `mouse_leave` and `mouse_enter` events
-	for _, element in ipairs(mouse_left_elements) do element:trigger('mouse_leave') end
-	for _, element in ipairs(mouse_entered_elements) do element:trigger('mouse_enter') end
-end
-
--- ELEMENT RENDERERS
-
-function render_timeline(this)
-	if this.size_max == 0 or state.duration == nil or state.position == nil then return end
-
-	local size_min = this:get_effective_size_min()
-	local size = this:get_effective_size()
-
-	if size < 1 then return end
-
-	local ass = assdraw.ass_new()
-
-	-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
-	local hide_text_below = math.max(this.font_size * 0.7, size_min * 2)
-	local hide_text_ramp = hide_text_below / 2
-	local text_opacity = math.max(math.min(size - hide_text_below, hide_text_ramp), 0) / hide_text_ramp
-
-	local spacing = math.max(math.floor((this.size_max - this.font_size) / 2.5), 4)
-	local progress = state.position / state.duration
-
-	-- Background bar coordinates
-	local bax = 0
-	local bay = display.height - size - this.bottom_border - this.top_border
-	local bbx = display.width
-	local bby = display.height
-
-	-- Foreground bar coordinates
-	local fax = bax
-	local fay = bay + this.top_border
-	local fbx = bbx * progress
-	local fby = bby - this.bottom_border
-	local foreground_size = bby - bay
-	local foreground_coordinates = fax..','..fay..','..fbx..','..fby -- for clipping
-
-	-- Background
-	ass:new_event()
-	ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..foreground_coordinates..')}')
-	ass:append(ass_opacity(math.max(options.timeline_opacity - 0.1, 0)))
-	ass:pos(0, 0)
-	ass:draw_start()
-	ass:rect_cw(bax, bay, bbx, bby)
-	ass:draw_stop()
-
-	-- Foreground
-	ass:new_event()
-	ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
-	ass:append(ass_opacity(options.timeline_opacity))
-	ass:pos(0, 0)
-	ass:draw_start()
-	ass:rect_cw(fax, fay, fbx, fby)
-	ass:draw_stop()
-
-	-- Seekable ranges
-	if options.timeline_cached_ranges and state.cached_ranges then
-		local range_height = math.max(foreground_size / 8, size_min)
-		local range_ay = fby - range_height
-		for _, range in ipairs(state.cached_ranges) do
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\1c&H'..options.timeline_cached_ranges.color..'}')
-			ass:append(ass_opacity(options.timeline_cached_ranges.opacity))
-			ass:pos(0, 0)
-			ass:draw_start()
-			ass:rect_cw(
-				bbx * (range['start'] / state.duration), range_ay,
-				bbx * (range['end'] / state.duration), range_ay + range_height
-			)
-			ass:draw_stop()
-		end
-	end
-
-	-- Custom ranges
-	if state.chapter_ranges ~= nil then
-		for i, chapter_range in ipairs(state.chapter_ranges) do
-			for i, range in ipairs(chapter_range.ranges) do
-				local rax = display.width * (range['start'].time / state.duration)
-				local rbx = display.width * (range['end'].time / state.duration)
-				ass:new_event()
-				ass:append('{\\blur0\\bord0\\1c&H'..chapter_range.color..'}')
-				ass:append(ass_opacity(chapter_range.opacity))
-				ass:pos(0, 0)
-				ass:draw_start()
-				-- for 1px chapter size, use the whole size of the bar including padding
-				if size <= 1 then
-					ass:rect_cw(rax, bay, rbx, bby)
-				else
-					ass:rect_cw(rax, fay, rbx, fby)
-				end
-				ass:draw_stop()
-			end
-		end
-	end
-
-	-- Chapters
-	if options.chapters ~= 'none' and state.chapters ~= nil and #state.chapters > 0 then
-		local half_size = size / 2
-		local dots = false
-		local chapter_size, chapter_y
-		if options.chapters == 'dots' then
-			dots = true
-			chapter_size = math.min(6, (foreground_size / 2) + 2)
-			chapter_y = math.min(fay + chapter_size, fay + half_size)
-		elseif options.chapters == 'lines' then
-			chapter_size = size
-			chapter_y = fay + (chapter_size / 2)
-		elseif options.chapters == 'lines-top' then
-			chapter_size = math.min(this.size_max / 3.5, size)
-			chapter_y = fay + (chapter_size / 2)
-		elseif options.chapters == 'lines-bottom' then
-			chapter_size = math.min(this.size_max / 3.5, size)
-			chapter_y = fay + size - (chapter_size / 2)
-		end
-
-		if chapter_size ~= nil then
-			-- for 1px chapter size, use the whole size of the bar including padding
-			chapter_size = size <= 1 and foreground_size or chapter_size
-			local chapter_half_size = chapter_size / 2
-
-			for i, chapter in ipairs(state.chapters) do
-				local chapter_x = display.width * (chapter.time / state.duration)
-				local color = chapter_x > fbx and options.color_foreground or options.color_background
-
-				ass:new_event()
-				ass:append('{\\blur0\\bord0\\1c&H'..color..'}')
-				ass:append(ass_opacity(options.chapters_opacity))
-				ass:pos(0, 0)
-				ass:draw_start()
-
-				if dots then
-					local bezier_stretch = chapter_size * 0.67
-					ass:move_to(chapter_x - chapter_half_size, chapter_y)
-					ass:bezier_curve(
-						chapter_x - chapter_half_size, chapter_y - bezier_stretch,
-						chapter_x + chapter_half_size, chapter_y - bezier_stretch,
-						chapter_x + chapter_half_size, chapter_y
-					)
-					ass:bezier_curve(
-						chapter_x + chapter_half_size, chapter_y + bezier_stretch,
-						chapter_x - chapter_half_size, chapter_y + bezier_stretch,
-						chapter_x - chapter_half_size, chapter_y
-					)
-				else
-					ass:rect_cw(chapter_x, chapter_y - chapter_half_size, chapter_x + 1, chapter_y + chapter_half_size)
-				end
-
-				ass:draw_stop()
-			end
-		end
-	end
-
-	if text_opacity > 0 then
-		-- Elapsed time
-		if state.elapsed_seconds then
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
-			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
-			ass:pos(spacing, fay + (size / 2))
-			ass:an(4)
-			ass:append(state.elapsed_time)
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
-			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
-			ass:pos(spacing, fay + (size / 2))
-			ass:an(4)
-			ass:append(state.elapsed_time)
-		end
-
-		-- Remaining time
-		if state.remaining_seconds then
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
-			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
-			ass:pos(display.width - spacing, fay + (size / 2))
-			ass:an(6)
-			ass:append(state.remaining_time)
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
-			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
-			ass:pos(display.width - spacing, fay + (size / 2))
-			ass:an(6)
-			ass:append(state.remaining_time)
-		end
-	end
-
-	if (this.proximity_raw == 0 or this.pressed) and not (elements.speed and elements.speed.dragging) then
-		-- Hovered time
-		local hovered_seconds = state.duration * (cursor.x / display.width)
-		local box_half_width_guesstimate = (this.font_size * 4.2) / 2
-		ass:new_event()
-		ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'')
-		ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1)))
-		ass:pos(math.min(math.max(cursor.x, box_half_width_guesstimate), display.width - box_half_width_guesstimate), fay)
-		ass:an(2)
-		ass:append(mp.format_time(hovered_seconds))
-
-		-- Cursor line
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\xshad-1\\yshad0\\1c&H'..options.color_foreground..'\\4c&H'..options.color_background..'}')
-		ass:append(ass_opacity(0.2))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(cursor.x, fay, cursor.x + 1, fby)
-		ass:draw_stop()
-	end
-
-	return ass
-end
-
-function render_window_controls(this)
-	local opacity = this:get_effective_proximity()
-
-	if not this.enabled or opacity == 0 then return end
-
-	local ass = assdraw.ass_new()
-
-	-- Close button
-	local close = elements.window_controls_close
-	if close.proximity_raw == 0 then
-		-- Background on hover
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H2311e8}')
-		ass:append(ass_opacity(config.window_controls.background_opacity, opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(close.ax, close.ay, close.bx, close.by)
-		ass:draw_stop()
-	end
-	ass:new_event()
-	ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
-	ass:append(ass_opacity(config.window_controls.icon_opacity, opacity))
-	ass:pos(close.ax + (config.window_controls.button_width / 2), (config.window_controls.height / 2))
-	ass:draw_start()
-	ass:move_to(-5, 5)
-	ass:line_to(5, -5)
-	ass:move_to(-5, -5)
-	ass:line_to(5, 5)
-	ass:draw_stop()
-
-	-- Maximize button
-	local maximize = elements.window_controls_maximize
-	if maximize.proximity_raw == 0 then
-		-- Background on hover
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H222222}')
-		ass:append(ass_opacity(config.window_controls.background_opacity, opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(maximize.ax, maximize.ay, maximize.bx, maximize.by)
-		ass:draw_stop()
-	end
-	ass:new_event()
-	ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&H000000}')
-	ass:append(ass_opacity({[3] = config.window_controls.icon_opacity}, opacity))
-	ass:pos(maximize.ax + (config.window_controls.button_width / 2), (config.window_controls.height / 2))
-	ass:draw_start()
-	ass:rect_cw(-4, -4, 6, 6)
-	ass:draw_stop()
-	ass:new_event()
-	ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&HFFFFFF}')
-	ass:append(ass_opacity({[3] = config.window_controls.icon_opacity}, opacity))
-	ass:pos(maximize.ax + (config.window_controls.button_width / 2), (config.window_controls.height / 2))
-	ass:draw_start()
-	ass:rect_cw(-5, -5, 5, 5)
-	ass:draw_stop()
-
-	-- Minimize button
-	local minimize = elements.window_controls_minimize
-	if minimize.proximity_raw == 0 then
-		-- Background on hover
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H222222}')
-		ass:append(ass_opacity(config.window_controls.background_opacity, opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(minimize.ax, minimize.ay, minimize.bx, minimize.by)
-		ass:draw_stop()
-	end
-	ass:new_event()
-	ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
-	ass:append(ass_opacity(config.window_controls.icon_opacity, opacity))
-	ass:append('{\\1a&HFF&}')
-	ass:pos(minimize.ax + (config.window_controls.button_width / 2), (config.window_controls.height / 2))
-	ass:draw_start()
-	ass:move_to(-5, 0)
-	ass:line_to(5, 0)
-	ass:draw_stop()
-
-	-- Window title
-	if options.title and state.media_title then
-		local spacing = math.ceil(config.window_controls.height * 0.25)
-		local font_size = math.floor(config.window_controls.height - (spacing * 2))
-		local clip_coordinates = '0,0,'..(minimize.ax - spacing)..','..config.window_controls.height
-
-		ass:new_event()
-		ass:append('{\\q2\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\clip('..clip_coordinates..')')
-		ass:append(ass_opacity(1, opacity))
-		ass:pos(0 + spacing, config.window_controls.height / 2)
-		ass:an(4)
-		ass:append(state.media_title)
-	end
-
-	return ass
-end
-
-function render_volume(this)
-	local slider = elements.volume_slider
-	local opacity = this:get_effective_proximity()
-
-	if this.width == 0 or opacity == 0 then return end
-
-	local ass = assdraw.ass_new()
-
-	if slider.height > 0 then
-		-- Background bar coordinates
-		local bax = slider.ax
-		local bay = slider.ay
-		local bbx = slider.bx
-		local bby = slider.by
-
-		-- Foreground bar coordinates
-		local height_without_border = slider.height - (options.volume_border * 2)
-		local fax = slider.ax + options.volume_border
-		local fay = slider.ay + (height_without_border * (1 - math.min(state.volume / state.volume_max, 1))) + options.volume_border
-		local fbx = slider.bx - options.volume_border
-		local fby = slider.by - options.volume_border
-
-		-- Path to draw a foreground bar with a 100% volume indicator, already
-		-- clipped by volume level. Can't just clip it with rectangle, as it itself
-		-- also needs to be used as a path to clip the background bar and volume
-		-- number.
-		local fpath = assdraw.ass_new()
-		fpath:move_to(fbx, fby)
-		fpath:line_to(fax, fby)
-		local nudge_bottom_y = slider.nudge_y + slider.nudge_size
-		if fay <= nudge_bottom_y and slider.draw_nudge then
-			fpath:line_to(fax, math.min(nudge_bottom_y))
-			if fay <= slider.nudge_y then
-				fpath:line_to((fax + slider.nudge_size), slider.nudge_y)
-				local nudge_top_y = slider.nudge_y - slider.nudge_size
-				if fay <= nudge_top_y then
-					fpath:line_to(fax, nudge_top_y)
-					fpath:line_to(fax, fay)
-					fpath:line_to(fbx, fay)
-					fpath:line_to(fbx, nudge_top_y)
-				else
-					local triangle_side = fay - nudge_top_y
-					fpath:line_to((fax + triangle_side), fay)
-					fpath:line_to((fbx - triangle_side), fay)
-				end
-				fpath:line_to((fbx - slider.nudge_size), slider.nudge_y)
-			else
-				local triangle_side = nudge_bottom_y - fay
-				fpath:line_to((fax + triangle_side), fay)
-				fpath:line_to((fbx - triangle_side), fay)
-			end
-			fpath:line_to(fbx, nudge_bottom_y)
-		else
-			fpath:line_to(fax, fay)
-			fpath:line_to(fbx, fay)
-		end
-		fpath:line_to(fbx, fby)
-
-		-- Background
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..fpath.scale..', '..fpath.text..')}')
-		ass:append(ass_opacity(math.max(options.volume_opacity - 0.1, 0), opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:move_to(bax, bay)
-		ass:line_to(bbx, bay)
-		local half_border = options.volume_border / 2
-		if slider.draw_nudge then
-			ass:line_to(bbx, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
-			ass:line_to(bbx - slider.nudge_size + half_border, slider.nudge_y)
-			ass:line_to(bbx, slider.nudge_y + slider.nudge_size - half_border)
-		end
-		ass:line_to(bbx, bby)
-		ass:line_to(bax, bby)
-		if slider.draw_nudge then
-			ass:line_to(bax, slider.nudge_y + slider.nudge_size - half_border)
-			ass:line_to(bax + slider.nudge_size - half_border, slider.nudge_y)
-			ass:line_to(bax, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
-		end
-		ass:line_to(bax, bay)
-		ass:draw_stop()
-
-		-- Foreground
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
-		ass:append(ass_opacity(options.volume_opacity, opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:append(fpath.text)
-		ass:draw_stop()
-
-		-- Current volume value
-		local volume_string = tostring(round(state.volume * 10) / 10)
-		local font_size = round(((this.width * 0.6) - (#volume_string * (this.width / 20))) * options.volume_font_scale)
-		if fay < slider.by - slider.spacing then
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\clip('..fpath.scale..', '..fpath.text..')}')
-			ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
-			ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
-			ass:an(2)
-			ass:append(volume_string)
-		end
-		if fay > slider.by - slider.spacing - font_size then
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\iclip('..fpath.scale..', '..fpath.text..')}')
-			ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
-			ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
-			ass:an(2)
-			ass:append(volume_string)
-		end
-	end
-
-	-- Mute button
-	local mute = elements.volume_mute
-	local icon_name = state.mute and 'volume_muted' or 'volume'
-	ass:new_event()
-	ass:append(icon(
-		icon_name,
-		mute.ax + (mute.width / 2), mute.ay + (mute.height / 2), mute.width * 0.7, -- x, y, size
-		0, 0, options.volume_border, -- shadow_x, shadow_y, shadow_size
-		'background', options.volume_opacity * opacity -- backdrop, opacity
-	))
-	return ass
-end
-
-function render_speed(this)
-	if not this.dragging and (elements.curtain.opacity > 0) then return end
-
-	local timeline = elements.timeline
-	local proximity = timeline:get_effective_proximity()
-	local opacity = this.forced_proximity and this.forced_proximity or (this.dragging and 1 or proximity)
-
-	if opacity == 0 then return end
-
-	local ass = assdraw.ass_new()
-
-	-- Coordinates
-	local ax = this.ax
-	local ay = this.ay + timeline.size_max - timeline:get_effective_size() - timeline.top_border - timeline.bottom_border
-	local bx = this.bx
-	local by = ay + this.height
-	local half_width = (this.width / 2)
-	local half_x = ax + half_width
-
-	-- Notches
-	local speed_at_center = state.speed
-	if this.dragging then
-		speed_at_center = this.dragging.start_speed + ((-this.dragging.distance / this.step_distance) * options.speed_step)
-		speed_at_center = math.min(math.max(speed_at_center, 0.01), 100)
-	end
-	local nearest_notch_speed = round(speed_at_center / this.notch_every) * this.notch_every
-	local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / this.notch_every) * this.notch_spacing)
-	local guide_size = math.floor(this.height / 7.5)
-	local notch_by = by - guide_size
-	local notch_ay_big = ay + round(this.font_size * 1.1)
-	local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
-	local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
-	local from_to_index = math.floor(this.notches / 2)
-
-	for i = -from_to_index, from_to_index do
-		local notch_speed = nearest_notch_speed + (i * this.notch_every)
-
-		if notch_speed < 0 or notch_speed > 100 then goto continue end
-
-		local notch_x = nearest_notch_x + (i * this.notch_spacing)
-		local notch_thickness = 1
-		local notch_ay = notch_ay_small
-		if (notch_speed % (this.notch_every * 10)) < 0.00000001 then
-			notch_ay = notch_ay_big
-			notch_thickness = 1
-		elseif (notch_speed % (this.notch_every * 5)) < 0.00000001 then
-			notch_ay = notch_ay_medium
-		end
-
-		ass:new_event()
-		ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
-		ass:append(ass_opacity(math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1), opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:move_to(notch_x - notch_thickness, notch_ay)
-		ass:line_to(notch_x + notch_thickness, notch_ay)
-		ass:line_to(notch_x + notch_thickness, notch_by)
-		ass:line_to(notch_x - notch_thickness, notch_by)
-		ass:draw_stop()
-
-		::continue::
-	end
-
-	-- Center guide
-	ass:new_event()
-	ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
-	ass:append(ass_opacity(options.speed_opacity, opacity))
-	ass:pos(0, 0)
-	ass:draw_start()
-	ass:move_to(half_x, by - 2 - guide_size)
-	ass:line_to(half_x + guide_size, by - 2)
-	ass:line_to(half_x - guide_size, by - 2)
-	ass:draw_stop()
-
-	-- Speed value
-	local speed_text = (round(state.speed * 100) / 100)..'x'
-	ass:new_event()
-	ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'}')
-	ass:append(ass_opacity(options.speed_opacity, opacity))
-	ass:pos(half_x, ay)
-	ass:an(8)
-	ass:append(speed_text)
-
-	return ass
-end
-
-function render_menu(this)
-	local ass = assdraw.ass_new()
-
-	if this.parent_menu then
-		ass:merge(this.parent_menu:render())
-	end
-
-	-- Menu title
-	if this.title then
-		-- Background
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
-		ass:append(ass_opacity(options.menu_opacity, this.opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(this.ax, this.ay - this.item_height, this.bx, this.ay - 1)
-		ass:draw_stop()
-
-		-- Title
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\shad1\\b1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..'\\q2\\clip('..this.ax..','..this.ay - this.item_height..','..this.bx..','..this.ay..')}')
-		ass:append(ass_opacity(options.menu_opacity, this.opacity))
-		ass:pos(display.width / 2, this.ay - (this.item_height * 0.5))
-		ass:an(5)
-		ass:append(this.title)
-	end
-
-	local scroll_area_clip = '\\clip('..this.ax..','..this.ay..','..this.bx..','..this.by..')'
-
-	for index, item in ipairs(this.items) do
-		local item_ay = this.ay - this.scroll_y + (this.item_height * (index - 1) + this.item_spacing * (index - 1))
-		local item_by = item_ay + this.item_height
-		local item_clip = ''
-
-		-- Clip items overflowing scroll area
-		if item_ay <= this.ay or item_by >= this.by then
-			item_clip = scroll_area_clip
-		end
-
-		if item_by < this.ay or item_ay > this.by then goto continue end
-
-		local is_active = this.active_item == index
-		local font_color, background_color, ass_shadow, ass_shadow_color
-		local icon_size = this.font_size
-
-		if is_active then
-			font_color, background_color = options.color_foreground_text, options.color_foreground
-			ass_shadow, ass_shadow_color = '\\shad0', ''
-		else
-			font_color, background_color = options.color_background_text, options.color_background
-			ass_shadow, ass_shadow_color = '\\shad1', '\\4c&H'..background_color
-		end
-
-		local has_submenu = item.items ~= nil
-		local hint_width = 0
-		if item.hint then
-			hint_width = text_width_estimate(item.hint:len(), this.font_size) + this.item_content_spacing
-		elseif has_submenu then
-			hint_width = icon_size + this.item_content_spacing
-		end
-
-		-- Background
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H'..background_color..item_clip..'}')
-		ass:append(ass_opacity(options.menu_opacity, this.opacity))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(this.ax, item_ay, this.bx, item_by)
-		ass:draw_stop()
-
-		-- Selected highlight
-		if this.selected_item == index then
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..item_clip..'}')
-			ass:append(ass_opacity(0.1, this.opacity))
-			ass:pos(0, 0)
-			ass:draw_start()
-			ass:rect_cw(this.ax, item_ay, this.bx, item_by)
-			ass:draw_stop()
-		end
-
-		-- Title
-		if item.title then
-			item.ass_save_title = item.ass_save_title or item.title:gsub("([{}])","\\%1")
-			local title_clip_x = (this.bx - hint_width - this.item_content_spacing)
-			local title_clip = '\\clip('..this.ax..','..math.max(item_ay, this.ay)..','..title_clip_x..','..math.min(item_by, this.by)..')'
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..font_color..'\\4c&H'..background_color..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..title_clip..'\\q2}')
-			ass:append(ass_opacity(options.menu_opacity, this.opacity))
-			ass:pos(this.ax + this.item_content_spacing, item_ay + (this.item_height / 2))
-			ass:an(4)
-			ass:append(item.ass_save_title)
-		end
-
-		-- Hint
-		if item.hint then
-			item.ass_save_hint = item.ass_save_hint or item.hint:gsub("([{}])","\\%1")
-			ass:new_event()
-			ass:append('{\\blur0\\bord0'..ass_shadow..'\\1c&H'..font_color..''..ass_shadow_color..'\\fn'..config.font..'\\fs'..(this.font_size - 1)..bold_tag..item_clip..'}')
-			ass:append(ass_opacity(options.menu_opacity * (has_submenu and 1 or 0.5), this.opacity))
-			ass:pos(this.bx - this.item_content_spacing, item_ay + (this.item_height / 2))
-			ass:an(6)
-			ass:append(item.ass_save_hint)
-		elseif has_submenu then
-			ass:new_event()
-			ass:append(icon(
-				'arrow_right',
-				this.bx - this.item_content_spacing - (icon_size / 2), -- x
-				item_ay + (this.item_height / 2), -- y
-				icon_size, -- size
-				0, 0, 1, -- shadow_x, shadow_y, shadow_size
-				is_active and 'foreground' or 'background', this.opacity, -- backdrop, opacity
-				item_clip
-			))
-		end
-
-		::continue::
-	end
-
-	-- Scrollbar
-	if this.scroll_height > 0 then
-		local groove_height = this.height - 2
-		local thumb_height = math.max((this.height / (this.scroll_height + this.height)) * groove_height, 40)
-		local thumb_y = this.ay + 1 + ((this.scroll_y / this.scroll_height) * (groove_height - thumb_height))
-		ass:new_event()
-		ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
-		ass:append(ass_opacity(options.menu_opacity, this.opacity * 0.8))
-		ass:pos(0, 0)
-		ass:draw_start()
-		ass:rect_cw(this.bx - 3, thumb_y, this.bx - 1, thumb_y + thumb_height)
-		ass:draw_stop()
-	end
-
-	return ass
-end
-
--- MAIN RENDERING
-
--- Request that render() is called.
--- The render is then either executed immediately, or rate-limited if it was
--- called a small time ago.
-function request_render()
-	if state.render_timer == nil then
-		state.render_timer = mp.add_timeout(0, render)
-	end
-
-	if not state.render_timer:is_enabled() then
-		local now = mp.get_time()
-		local timeout = config.render_delay - (now - state.render_last_time)
-		if timeout < 0 then
-			timeout = 0
-		end
-		state.render_timer.timeout = timeout
-		state.render_timer:resume()
-	end
-end
-
-function render()
-	state.render_last_time = mp.get_time()
-
-	-- Actual rendering
-	local ass = assdraw.ass_new()
-
-	for _, element in elements.ipairs() do
-		local result = element:maybe('render')
-		if result then
-			ass:new_event()
-			ass:merge(result)
-		end
-	end
-
-	-- submit
-	if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then
-		return
-	end
-
-	osd.res_x = display.width
-	osd.res_y = display.height
-	osd.data = ass.text
-	osd.z = 2000
-	osd:update()
-end
-
--- STATIC ELEMENTS
-
-if itable_find({'flash', 'static'}, options.pause_indicator) then
-	elements:add('pause_indicator', Element.new({
-		base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8,
-		paused = false,
-		is_flash = options.pause_indicator == 'flash',
-		is_static = options.pause_indicator == 'static',
-		opacity = 0,
-		init = function(this)
-			local initial_call = true
-			mp.observe_property('pause', 'bool', function(_, paused)
-				if initial_call then
-					initial_call = false
-					return
-				end
-
-				this.paused = paused
-
-				if options.pause_indicator == 'flash' then
-					this.opacity = 1
-					this:tween_property('opacity', 1, 0, 0.15)
-				else
-					this.opacity = paused and 1 or 0
-					request_render()
-				end
-
-			end)
-		end,
-		render = function(this)
-			if this.opacity == 0 then return end
-
-			local ass = assdraw.ass_new()
-
-			-- Background fadeout
-			if this.is_static then
-				ass:new_event()
-				ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
-				ass:append(ass_opacity(0.3, this.opacity))
-				ass:pos(0, 0)
-				ass:draw_start()
-				ass:rect_cw(0, 0, display.width, display.height)
-				ass:draw_stop()
-			end
-
-			-- Icon
-			local size = round((math.min(display.width, display.height) * (this.is_static and 0.20 or 0.15)) / 2)
-
-			size = size + size * (1 - this.opacity)
-
-			if this.paused then
-				ass:new_event()
-				ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
-				ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
-				ass:pos(display.width / 2, display.height / 2)
-				ass:draw_start()
-				ass:rect_cw(-size, -size, -size / 3, size)
-				ass:draw_stop()
-
-				ass:new_event()
-				ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
-				ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
-				ass:pos(display.width / 2, display.height / 2)
-				ass:draw_start()
-				ass:rect_cw(size / 3, -size, size, size)
-				ass:draw_stop()
-			elseif this.is_flash then
-				ass:new_event()
-				ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
-				ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
-				ass:pos(display.width / 2, display.height / 2)
-				ass:draw_start()
-				ass:move_to(-size * 0.6, -size)
-				ass:line_to(size, 0)
-				ass:line_to(-size * 0.6, size)
-				ass:draw_stop()
-			end
-
-			return ass
-		end
-	}))
-end
-elements:add('timeline', Element.new({
-	captures = {mouse_buttons = true, wheel = true},
-	pressed = false,
-	size_max = 0, size_min = 0, -- set in `on_display_resize` handler based on `state.fullscreen`
-	size_min_override = options.timeline_start_hidden and 0 or nil, -- used for toggle-progress command
-	font_size = 0, -- calculated in on_display_resize
-	top_border = options.timeline_border,
-	bottom_border = 0, -- set dynamically in `border` property observer
-	init = function(this)
-		-- Toggle 1px bottom border for timeline in no-border mode
-		mp.observe_property('border', 'bool', function(_, border)
-			this.bottom_border = not border and options.timeline_border or 0
-			request_render()
-		end)
-
-		-- Flash on external changes
-		if options.timeline_flash then
-			mp.register_event('seek', function()
-				local position = mp.get_property_native('playback-time')
-				if position and state.position then
-					local seek_length = math.abs(position - state.position)
-					-- Don't flash on video looping (seek to 0) or tiny seeks (frame-step)
-					if position > 0.5 and seek_length > 0.5 then this:flash() end
-				end
-			end)
-		end
-	end,
-	get_effective_proximity = function(this)
-		if (elements.volume_slider and elements.volume_slider.pressed) then return 0 end
-		if this.pressed then return 1 end
-		return this.forced_proximity and this.forced_proximity or this.proximity
-	end,
-	get_effective_size_min = function(this)
-		return this.size_min_override or this.size_min
-	end,
-	get_effective_size = function(this)
-		if elements.speed and elements.speed.dragging then return this.size_max end
-		local size_min = this:get_effective_size_min()
-		return size_min + math.ceil((this.size_max - size_min) * this:get_effective_proximity())
-	end,
-	on_display_resize = function(this)
-		if state.fullscreen or state.maximized then
-			this.size_min = options.timeline_size_min_fullscreen
-			this.size_max = options.timeline_size_max_fullscreen
-		else
-			this.size_min = options.timeline_size_min
-			this.size_max = options.timeline_size_max
-		end
-		this.font_size = math.floor(math.min((this.size_max + 60) * 0.2, this.size_max * 0.96) * options.timeline_font_scale)
-		this.ax = 0
-		this.ay = display.height - this.size_max - this.top_border - this.bottom_border
-		this.bx = display.width
-		this.by = display.height
-	end,
-	set_from_cursor = function(this)
-		mp.commandv('seek', ((cursor.x / display.width) * 100), 'absolute-percent+exact')
-	end,
-	on_mbtn_left_down = function(this)
-		this.pressed = true
-		this:set_from_cursor()
-	end,
-	on_global_mbtn_left_up = function(this) this.pressed = false end,
-	on_global_mouse_leave = function(this) this.pressed = false end,
-	on_global_mouse_move = function(this)
-		if this.pressed then this:set_from_cursor() end
-	end,
-	on_wheel_up = function(this)
-		if options.timeline_step > 0 then mp.commandv('seek', -options.timeline_step) end
-	end,
-	on_wheel_down = function(this)
-		if options.timeline_step > 0 then mp.commandv('seek', options.timeline_step) end
-	end,
-	render = render_timeline,
-}))
-if options.window_controls then
-	elements:add('window_controls', Element.new({
-		enabled = false,
-		init = function(this)
-			mp.observe_property('border', 'bool', function(_, border)
-				this.enabled = not border
-			end)
-		end,
-		get_effective_proximity = function(this)
-			if (elements.volume_slider and elements.volume_slider.pressed) or elements.curtain.opacity > 0 then return 0 end
-			return this.forced_proximity and this.forced_proximity or this.proximity
-		end,
-		on_display_resize = function(this)
-			this.ax = options.title and 0 or (display.width - (config.window_controls.button_width * 3))
-			this.ay = 0
-			this.bx = display.width
-			this.by = config.window_controls.height
-		end,
-		render = render_window_controls,
-	}))
-	elements:add('window_controls_minimize', Element.new({
-		captures = {mouse_buttons = true},
-		on_display_resize = function(this)
-			this.ax = display.width - (config.window_controls.button_width * 3)
-			this.ay = 0
-			this.bx = this.ax + config.window_controls.button_width
-			this.by = config.window_controls.height
-		end,
-		on_mbtn_left_down = function() mp.commandv('cycle', 'window-minimized') end
-	}))
-	elements:add('window_controls_maximize', Element.new({
-		captures = {mouse_buttons = true},
-		on_display_resize = function(this)
-			this.ax = display.width - (config.window_controls.button_width * 2)
-			this.ay = 0
-			this.bx = this.ax + config.window_controls.button_width
-			this.by = config.window_controls.height
-		end,
-		on_mbtn_left_down = function() mp.commandv('cycle', 'window-maximized') end
-	}))
-	elements:add('window_controls_close', Element.new({
-		captures = {mouse_buttons = true},
-		on_display_resize = function(this)
-			this.ax = display.width - config.window_controls.button_width
-			this.ay = 0
-			this.bx = this.ax + config.window_controls.button_width
-			this.by = config.window_controls.height
-		end,
-		on_mbtn_left_down = function() mp.commandv('quit') end
-	}))
-end
-if itable_find({'left', 'right'}, options.volume) then
-	elements:add('volume', Element.new({
-		width = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
-		height = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
-		margin = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
-		init = function(this)
-			-- FLash on external changes
-			if options.volume_flash then
-				local is_initial_volume_call = true
-				mp.observe_property('volume', 'number', function(_, value)
-					if not is_initial_volume_call then this:flash() end
-					is_initial_volume_call = false
-				end)
-				local is_initial_mute_call = true
-				mp.observe_property('mute', 'bool', function(_, value)
-					if not is_initial_mute_call then this:flash() end
-					is_initial_mute_call = false
-				end)
-			end
-		end,
-		get_effective_proximity = function(this)
-			if elements.volume_slider.pressed then return 1 end
-			if elements.timeline.proximity_raw == 0 or elements.curtain.opacity > 0 then return 0 end
-			return this.forced_proximity and this.forced_proximity or this.proximity
-		end,
-		on_display_resize = function(this)
-			this.width = (state.fullscreen or state.maximized) and options.volume_size_fullscreen or options.volume_size
-			this.height = round(math.min(this.width * 8, (elements.timeline.ay - config.window_controls.height) * 0.8))
-			-- Don't bother rendering this if too small
-			if this.height < (this.width * 2) then
-				this.height = 0
-			end
-			this.margin = this.width / 2
-			this.ax = round(options.volume == 'left' and this.margin or display.width - this.margin - this.width)
-			this.ay = round((display.height - this.height) / 2)
-			this.bx = round(this.ax + this.width)
-			this.by = round(this.ay + this.height)
-		end,
-		render = render_volume,
-	}))
-	elements:add('volume_mute', Element.new({
-		captures = {mouse_buttons = true},
-		width = 0,
-		height = 0,
-		on_display_resize = function(this)
-			this.width = elements.volume.width
-			this.height = this.width
-			this.ax = elements.volume.ax
-			this.ay = elements.volume.by - this.height
-			this.bx = elements.volume.bx
-			this.by = elements.volume.by
-		end,
-		on_mbtn_left_down = function(this) mp.commandv('cycle', 'mute') end
-	}))
-	elements:add('volume_slider', Element.new({
-		captures = {mouse_buttons = true, wheel = true},
-		pressed = false,
-		width = 0,
-		height = 0,
-		nudge_y = 0, -- vertical position where volume overflows 100
-		nudge_size = nil, -- set on resize
-		font_size = nil,
-		spacing = nil,
-		on_display_resize = function(this)
-			this.ax = elements.volume.ax
-			this.ay = elements.volume.ay
-			this.bx = elements.volume.bx
-			this.by = elements.volume_mute.ay
-			this.width = this.bx - this.ax
-			this.height = this.by - this.ay
-			this.nudge_y = this.by - round(this.height * (100 / state.volume_max))
-			this.nudge_size = round(elements.volume.width * 0.18)
-			this.draw_nudge = this.ay < this.nudge_y
-			this.spacing = round(this.width * 0.2)
-		end,
-		set_from_cursor = function(this)
-			local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
-			local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
-			new_volume = round(new_volume / options.volume_step) * options.volume_step
-			if state.volume ~= new_volume then mp.commandv('set', 'volume', math.min(new_volume, state.volume_max)) end
-		end,
-		on_mbtn_left_down = function(this)
-			this.pressed = true
-			this:set_from_cursor()
-		end,
-		on_global_mbtn_left_up = function(this) this.pressed = false end,
-		on_global_mouse_leave = function(this) this.pressed = false end,
-		on_global_mouse_move = function(this)
-			if this.pressed then this:set_from_cursor() end
-		end,
-		on_wheel_up = function(this)
-			local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
-			mp.commandv('set', 'volume', math.min(current_rounded_volume + options.volume_step, state.volume_max))
-		end,
-		on_wheel_down = function(this)
-			local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
-			mp.commandv('set', 'volume', math.min(current_rounded_volume - options.volume_step, state.volume_max))
-		end,
-	}))
-end
-if options.speed then
-	elements:add('speed', Element.new({
-		captures = {mouse_buttons = true, wheel = true},
-		dragging = nil,
-		width = 0,
-		height = 0,
-		notches = 10,
-		notch_every = 0.1,
-		step_distance = nil,
-		font_size = nil,
-		init = function(this)
-			-- Fade out/in on timeline mouse enter/leave
-			elements.timeline:on('mouse_enter', function()
-				if not this.dragging then this:fadeout() end
-			end)
-			elements.timeline:on('mouse_leave', function()
-				if not this.dragging then this:fadein() end
-			end)
-
-			-- Flash on external changes
-			if options.speed_flash then
-				local initial_call = true
-				mp.observe_property('speed', 'number', function()
-					if not initial_call and not this.dragging then this:flash() end
-					initial_call = false
-				end)
-			end
-		end,
-		fadeout = function(this)
-			this:tween_property('forced_proximity', 1, 0, function(this)
-				this.forced_proximity = 0
-			end)
-		end,
-		fadein = function(this)
-			local get_current_proximity = function() return this.proximity end
-			this:tween_property('forced_proximity', 0, get_current_proximity, function(this)
-				this.forced_proximity = nil
-			end)
-		end,
-		on_display_resize = function(this)
-			this.height = (state.fullscreen or state.maximized) and options.speed_size_fullscreen or options.speed_size
-			this.width = round(this.height * 3.6)
-			this.notch_spacing = this.width / this.notches
-			this.step_distance = this.notch_spacing * (options.speed_step / this.notch_every)
-			this.ax = (display.width - this.width) / 2
-			this.by = display.height - elements.timeline.size_max
-			this.ay = this.by - this.height
-			this.bx = this.ax + this.width
-			this.font_size = round(this.height * 0.48 * options.speed_font_scale)
-		end,
-		set_from_cursor = function(this)
-			local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
-			local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
-			new_volume = round(new_volume / options.volume_step) * options.volume_step
-			if state.volume ~= new_volume then mp.commandv('set', 'volume', new_volume) end
-		end,
-		on_mbtn_left_down = function(this)
-			this:tween_stop() -- Stop and cleanup possible ongoing animations
-			this.dragging = {
-				start_time = mp.get_time(),
-				start_x = cursor.x,
-				distance = 0,
-				start_speed = state.speed
-			}
-		end,
-		on_global_mouse_move = function(this)
-			if not this.dragging then return end
-
-			this.dragging.distance = cursor.x - this.dragging.start_x
-			local steps_dragged = round(-this.dragging.distance / this.step_distance)
-			local new_speed = this.dragging.start_speed + (steps_dragged * options.speed_step)
-			mp.set_property_native('speed', round(new_speed * 100) / 100)
-		end,
-		on_mbtn_left_up = function(this)
-			-- Reset speed on short clicks
-			if this.dragging and math.abs(this.dragging.distance) < 6 and mp.get_time() - this.dragging.start_time < 0.15 then
-				mp.set_property_native('speed', 1)
-			end
-		end,
-		on_global_mbtn_left_up = function(this)
-			if this.dragging and elements.timeline.proximity_raw == 0 then
-				this:fadeout()
-			end
-			this.dragging = nil
-			request_render()
-		end,
-		on_global_mouse_leave = function(this)
-			this.dragging = nil
-			request_render()
-		end,
-		on_wheel_up = function(this)
-			mp.set_property_native('speed', state.speed - options.speed_step)
-		end,
-		on_wheel_down = function(this)
-			mp.set_property_native('speed', state.speed + options.speed_step)
-		end,
-		render = render_speed,
-	}))
-end
-elements:add('curtain', Element.new({
-	opacity = 0,
-	fadeout = function(this)
-		this:tween_property('opacity', this.opacity, 0);
-	end,
-	fadein = function(this)
-		this:tween_property('opacity', this.opacity, 1);
-	end,
-	render = function(this)
-		if this.opacity > 0 then
-			local ass = assdraw.ass_new()
-			ass:new_event()
-			ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
-			ass:append(ass_opacity(0.4, this.opacity))
-			ass:pos(0, 0)
-			ass:draw_start()
-			ass:rect_cw(0, 0, display.width, display.height)
-			ass:draw_stop()
-			return ass
-		end
-	end
-}))
-
--- CHAPTERS SERIALIZATION
-
--- Parse `chapter_ranges` option into workable data structure
-for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
-	local start_patterns, color, opacity, end_patterns = string.match(definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)')
-
-	-- Invalid definition
-	if start_patterns == nil then goto continue end
-
-	start_patterns = start_patterns:lower()
-	end_patterns = end_patterns:lower()
-	local uses_bof = start_patterns:find('{bof}') ~= nil
-	local uses_eof = end_patterns:find('{eof}') ~= nil
-	local chapter_range = {
-		start_patterns = split(start_patterns, '|'),
-		end_patterns = split(end_patterns, '|'),
-		color = color,
-		opacity = tonumber(opacity),
-		ranges = {}
-	}
-
-	-- Filter out special keywords so we don't use them when matching titles
-	if uses_bof then
-		chapter_range.start_patterns = itable_remove(chapter_range.start_patterns, '{bof}')
-	end
-	if uses_eof and chapter_range.end_patterns then
-		chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, '{eof}')
-	end
-
-	chapter_range['serialize'] = function (chapters)
-		chapter_range.ranges = {}
-		local current_range = nil
-		-- bof and eof should be used only once per timeline
-		-- eof is only used when last range is missing end
-		local bof_used = false
-
-		function start_range(chapter)
-			-- If there is already a range started, should we append or overwrite?
-			-- I chose overwrite here.
-			current_range = {['start'] = chapter}
-		end
-
-		function end_range(chapter)
-			current_range['end'] = chapter
-			chapter_range.ranges[#chapter_range.ranges + 1] = current_range
-			-- Mark both chapter objects
-			current_range['start']._uosc_used_as_range_point = true
-			current_range['end']._uosc_used_as_range_point = true
-			-- Clear for next range
-			current_range = nil
-		end
-
-		for _, chapter in ipairs(chapters) do
-			if type(chapter.title) == 'string' then
-				local lowercase_title = chapter.title:lower()
-				local is_end = false
-				local is_start = false
-
-				-- Is ending check and handling
-				if chapter_range.end_patterns then
-					for _, end_pattern in ipairs(chapter_range.end_patterns) do
-						is_end = is_end or lowercase_title:find(end_pattern) ~= nil
-					end
-
-					if is_end then
-						if current_range == nil and uses_bof and not bof_used then
-							bof_used = true
-							start_range({time = 0})
-						end
-						if current_range ~= nil then
-							end_range(chapter)
-						else
-							is_end = false
-						end
-					end
-				end
-
-				-- Is start check and handling
-				for _, start_pattern in ipairs(chapter_range.start_patterns) do
-					is_start = is_start or lowercase_title:find(start_pattern) ~= nil
-				end
-
-				if is_start then start_range(chapter) end
-			end
-		end
-
-		-- If there is an unfinished range and range type accepts eof, use it
-		if current_range ~= nil and uses_eof then
-			end_range({time = state.duration or infinity})
-		end
-	end
-
-	state.chapter_ranges = state.chapter_ranges or {}
-	state.chapter_ranges[#state.chapter_ranges + 1] = chapter_range
-
-	::continue::
-end
-
-function parse_chapters()
-	-- Sometimes state.duration is not initialized yet for some reason
-	state.duration = mp.get_property_native('duration')
-
-	local chapters = get_normalized_chapters()
-
-	if not chapters or not state.duration then return end
-
-	-- Reset custom ranges
-	for _, chapter_range in ipairs(state.chapter_ranges or {}) do
-		chapter_range.serialize(chapters)
-	end
-
-	-- Filter out chapters that were used as ranges
-	state.chapters = itable_remove(chapters, function(chapter)
-		return chapter._uosc_used_as_range_point == true
-	end)
-
-	request_render()
-end
-
--- CONTEXT MENU SERIALIZATION
-
-state.context_menu_items = (function()
-	local input_conf_path = mp.command_native({'expand-path', '~~/input.conf'})
-	local input_conf_meta, meta_error = utils.file_info(input_conf_path)
-
-	-- File doesn't exist
-	if not input_conf_meta or not input_conf_meta.is_file then return end
-
-	local items = {}
-	local items_by_command = {}
-	local submenus_by_id = {}
-
-	for line in io.lines(input_conf_path) do
-		local key, command, title = string.match(line, ' *([%S]+) +(.*) #! *(.*)')
-		if key then
-			local is_dummy = key:sub(1, 1) == '#'
-			local submenu_id = ''
-			local target_menu = items
-			local title_parts = split(title or '', ' *> *')
-
-			for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do
-				if index < #title_parts then
-					submenu_id = submenu_id .. title_part
-
-					if not submenus_by_id[submenu_id] then
-						submenus_by_id[submenu_id] = {title = title_part, items = {}}
-						target_menu[#target_menu + 1] = submenus_by_id[submenu_id]
-					end
-
-					target_menu = submenus_by_id[submenu_id].items
-				else
-					-- If command is already in menu, just append the key to it
-					if items_by_command[command] then
-						items_by_command[command].hint = items_by_command[command].hint..', '..key
-					else
-						items_by_command[command] = {
-							title = title_part,
-							hint = not is_dummy and key or nil,
-							value = command
-						}
-						target_menu[#target_menu + 1] = items_by_command[command]
-					end
-				end
-			end
-		end
-	end
-
-	if #items > 0 then return items end
-end)()
-
--- EVENT HANDLERS
-
-function create_state_setter(name)
-	return function(_, value)
-		state[name] = value
-		dispatch_event_to_elements('prop_'..name, value)
-		request_render()
-	end
-end
-
-function dispatch_event_to_elements(name, ...)
-	for _, element in pairs(elements) do
-		if element.proximity_raw == 0 then
-			element:maybe('on_'..name, ...)
-		end
-		element:maybe('on_global_'..name, ...)
-	end
-end
-
-function create_event_to_elements_dispatcher(name, ...)
-	return function(...) dispatch_event_to_elements(name, ...) end
-end
-
-function handle_mouse_leave()
-	-- Slowly fadeout elements that are currently visible
-	for _, element_name in ipairs({'timeline', 'volume', 'window_controls'}) do
-		local element = elements[element_name]
-		if element and element.proximity > 0 then
-			element:tween_property('forced_proximity', element:get_effective_proximity(), 0, function()
-				element.forced_proximity = nil
-			end)
-		end
-	end
-
-	cursor.hidden = true
-	update_proximities()
-	dispatch_event_to_elements('mouse_leave')
-end
-
-function handle_mouse_enter()
-	cursor.hidden = false
-	cursor.x, cursor.y = mp.get_mouse_pos()
-	tween_element_stop(state)
-	dispatch_event_to_elements('mouse_enter')
-end
-
-function handle_mouse_move()
-	-- Handle case when we are in cursor hidden state but not left the actual
-	-- window (i.e. when autohide simulates mouse_leave).
-	if cursor.hidden then
-		handle_mouse_enter()
-		return
-	end
-
-	cursor.x, cursor.y = mp.get_mouse_pos()
-	update_proximities()
-	dispatch_event_to_elements('mouse_move')
-	request_render()
-
-	-- Restart timer that hides UI when mouse is autohidden
-	if options.autohide then
-		state.cursor_autohide_timer:kill()
-		state.cursor_autohide_timer:resume()
-	end
-end
-
-function navigate_directory(direction)
-	local path = mp.get_property_native("path")
-
-	if not path or is_protocol(path) then return end
-
-	local next_file = get_adjacent_file(path, direction, options.media_types)
-
-	if next_file then
-		mp.commandv("loadfile", utils.join_path(serialize_path(path).dirname, next_file))
-	end
-end
-
-function load_file_in_current_directory(index)
-	local path = mp.get_property_native("path")
-
-	if not path or is_protocol(path) then return end
-
-	local dirname = serialize_path(path).dirname
-	local files = get_files_in_directory(dirname, options.media_types)
-
-	if not files then return end
-	if index < 0 then index = #files + index + 1 end
-
-	if files[index] then
-		mp.commandv("loadfile", utils.join_path(dirname, files[index]))
-	end
-end
-
--- MENUS
-
-function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop)
-	return function()
-		if menu:is_open(track_type) then menu:close() return end
-
-		local items = {}
-		local active_item = nil
-
-		for index, track in ipairs(mp.get_property_native('track-list')) do
-			if track.type == track_type then
-				if track.selected then active_item = track.id end
-
-				items[#items + 1] = {
-					title = (track.title and track.title or 'Track '..track.id),
-					hint = track.lang and track.lang:upper() or nil,
-					value = track.id
-				}
-			end
-		end
-
-		-- Add option to disable a subtitle track. This works for all tracks,
-		-- but why would anyone want to disable audio or video? Better to not
-		-- let people mistakenly select what is unwanted 99.999% of the time.
-		-- If I'm mistaken and there is an active need for this, feel free to
-		-- open an issue.
-		if track_type == 'sub' then
-			active_item = active_item and active_item + 1 or 1
-			table.insert(items, 1, {hint = 'disabled', value = nil})
-		end
-
-		menu:open(items, function(id)
-			mp.commandv('set', track_prop, id and id or 'no')
-
-			-- If subtitle track was selected, assume user also wants to see it
-			if id and track_type == 'sub' then
-				mp.commandv('set', 'sub-visibility', 'yes')
-			end
-
-			menu:close()
-		end, {type = track_type, title = menu_title, active_item = active_item})
-	end
-end
-
--- `menu_options`:
--- **allowed_types** - table with file extensions to display
--- **active_path** - full path of a file to preselect
--- Rest of the options are passed to `menu:open()`
-function open_file_navigation_menu(directory, handle_select, menu_options)
-	directory = serialize_path(directory)
-	local directories, error = utils.readdir(directory.path, 'dirs')
-	local files, error = get_files_in_directory(directory.path, menu_options.allowed_types)
-
-	if not files or not directories then
-		msg.error('Retrieving files from '..directory..' failed: '..(error or ''))
-		return
-	end
-
-	-- Files are already sorted
-	table.sort(directories, word_order_comparator)
-
-	-- Pre-populate items with parent directory selector if not at root
-	local items = not directory.dirname and {} or {
-		{title = '..', hint = 'parent dir', value = directory.dirname}
-	}
-
-	for _, dir in ipairs(directories) do
-		local serialized = serialize_path(utils.join_path(directory.path, dir))
-		items[#items + 1] = {title = serialized.basename, value = serialized.path, hint = '/'}
-	end
-
-	menu_options.active_item = nil
-
-	for _, file in ipairs(files) do
-		local serialized = serialize_path(utils.join_path(directory.path, file))
-		local item_index = #items + 1
-
-		items[item_index] = {
-			title = serialized.basename,
-			value = serialized.path,
-		}
-
-		if menu_options.active_path == serialized.path then
-			menu_options.active_item = item_index
-		end
-	end
-
-	menu_options.title = directory.basename..'/'
-
-	menu:open(items, function(path)
-		local meta, error = utils.file_info(path)
-
-		if not meta then
-			msg.error('Retrieving file info for '..path..' failed: '..(error or ''))
-			return
-		end
-
-		if meta.is_dir then
-			open_file_navigation_menu(path, handle_select, menu_options)
-		else
-			handle_select(path)
-			menu:close()
-		end
-	end, menu_options)
-end
-
--- VALUE SERIALIZATION/NORMALIZATION
-
-options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
-options.chapters = itable_find({'dots', 'lines', 'lines-top', 'lines-bottom'}, options.chapters) and options.chapters or 'none'
-options.media_types = split(options.media_types, ' *, *')
-options.subtitle_types = split(options.subtitle_types, ' *, *')
-options.timeline_cached_ranges = (function()
-	if options.timeline_cached_ranges == '' or options.timeline_cached_ranges == 'no' then return nil end
-	local parts = split(options.timeline_cached_ranges, ':')
-	return parts[1] and {color = parts[1], opacity = tonumber(parts[2])} or nil
-end)()
-
--- HOOKS
-mp.register_event('file-loaded', parse_chapters)
-mp.observe_property('chapter-list', 'native', parse_chapters)
-mp.observe_property('duration', 'number', create_state_setter('duration'))
-mp.observe_property('media-title', 'string', create_state_setter('media_title'))
-mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen'))
-mp.observe_property('window-maximized', 'bool', create_state_setter('maximized'))
-mp.observe_property('idle-active', 'bool', create_state_setter('idle'))
-mp.observe_property('speed', 'number', create_state_setter('speed'))
-mp.observe_property('pause', 'bool', create_state_setter('pause'))
-mp.observe_property('volume', 'number', create_state_setter('volume'))
-mp.observe_property('volume-max', 'number', create_state_setter('volume_max'))
-mp.observe_property('mute', 'bool', create_state_setter('mute'))
-mp.observe_property('playback-time', 'number', function(name, val)
-	-- Ignore the initial call with nil value
-	if val == nil then return end
-
-	state.position = val
-	state.elapsed_seconds = val
-	state.elapsed_time = state.elapsed_seconds and mp.format_time(state.elapsed_seconds) or nil
-	state.remaining_seconds = mp.get_property_native('playtime-remaining')
-	state.remaining_time = state.remaining_seconds and mp.format_time(state.remaining_seconds) or nil
-
-	request_render()
-end)
-mp.observe_property('osd-dimensions', 'native', function(name, val)
-	update_display_dimensions()
-	request_render()
-end)
-mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
-	if cache_state == nil then
-		state.cached_ranges = nil
-		return
-	end
-	local cache_ranges = cache_state['seekable-ranges']
-	state.cached_ranges = #cache_ranges > 0 and cache_ranges or nil
-end)
-
--- CONTROLS
-
--- Mouse movement key binds
-local base_keybinds = {
-	{'mouse_move', handle_mouse_move},
-	{'mouse_leave', handle_mouse_leave},
-	{'mouse_enter', handle_mouse_enter},
-}
-if options.pause_on_click_shorter_than > 0 then
-	-- Cycles pause when click is shorter than `options.pause_on_click_shorter_than`
-	-- while filtering out double clicks.
-	local duration_seconds = options.pause_on_click_shorter_than / 1000
-	local last_down_event;
-	local click_timer = mp.add_timeout(duration_seconds, function()
-		mp.command('cycle pause')
-	end);
-	click_timer:kill()
-	base_keybinds[#base_keybinds + 1] = {'mbtn_left', function()
-			if mp.get_time() - last_down_event < duration_seconds then
-				click_timer:resume()
-			end
-		end, function()
-			if click_timer:is_enabled() then
-				click_timer:kill()
-				last_down_event = 0
-			else
-				last_down_event = mp.get_time()
-			end
-		end
-	}
-end
-mp.set_key_bindings(base_keybinds, 'mouse_movement', 'force')
-mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor')
-
--- Context based key bind groups
-
-forced_key_bindings = (function()
-	mp.set_key_bindings({
-		{'mbtn_left', create_event_to_elements_dispatcher('mbtn_left_up'), create_event_to_elements_dispatcher('mbtn_left_down')},
-		{'mbtn_left_dbl', 'ignore'},
-	}, 'mouse_buttons', 'force')
-	mp.set_key_bindings({
-		{'wheel_up', create_event_to_elements_dispatcher('wheel_up')},
-		{'wheel_down', create_event_to_elements_dispatcher('wheel_down')},
-	}, 'wheel', 'force')
-
-	local groups = {}
-	for _, group in ipairs({'mouse_buttons', 'wheel'}) do
-		groups[group] = {
-			is_enabled = false,
-			enable = function(this)
-				if this.is_enabled then return end
-				this.is_enabled = true
-				mp.enable_key_bindings(group)
-			end,
-			disable = function(this)
-				if not this.is_enabled then return end
-				this.is_enabled = false
-				mp.disable_key_bindings(group)
-			end,
-		}
-	end
-	return groups
-end)()
-
--- KEY BINDABLE FEATURES
-
-mp.add_key_binding(nil, 'peek-timeline', function()
-	if elements.timeline.proximity > 0.5 then
-		elements.timeline:tween_property('proximity', elements.timeline.proximity, 0)
-	else
-		elements.timeline:tween_property('proximity', elements.timeline.proximity, 1)
-	end
-end)
-mp.add_key_binding(nil, 'toggle-progress', function()
-	local timeline = elements.timeline
-	if timeline.size_min_override then
-		timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function()
-			timeline.size_min_override = nil
-		end)
-	else
-		timeline:tween_property('size_min_override', timeline.size_min, 0)
-	end
-end)
-mp.add_key_binding(nil, 'menu', function()
-	if menu:is_open('menu') then
-		menu:close()
-	elseif state.context_menu_items then
-		menu:open(state.context_menu_items, function(command)
-			mp.command(command)
-		end, {type = 'menu'})
-	end
-end)
-mp.add_key_binding(nil, 'load-subtitles', function()
-	if menu:is_open('load-subtitles') then menu:close() return end
-
-	local path = mp.get_property_native('path')
-	if path and not is_protocol(path) then
-		open_file_navigation_menu(
-			serialize_path(path).dirname,
-			function(path) mp.commandv('sub-add', path) end,
-			{
-				type = 'load-subtitles',
-				allowed_types = options.subtitle_types
-			}
-		)
-	end
-end)
-mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener('Subtitles', 'sub', 'sid'))
-mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener('Audio', 'audio', 'aid'))
-mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener('Video', 'video', 'vid'))
-mp.add_key_binding(nil, 'playlist', function()
-	if menu:is_open('playlist') then menu:close() return end
-
-	function serialize_playlist()
-		local pos = mp.get_property_number('playlist-pos-1', 0)
-		local items = {}
-		local active_item
-		for index, item in ipairs(mp.get_property_native('playlist')) do
-			local is_url = item.filename:find('://')
-			items[index] = {
-				title = is_url and item.filename or serialize_path(item.filename).basename,
-				hint = tostring(index),
-				value = index
-			}
-
-			if index == pos then active_item = index end
-		end
-		return items, active_item
-	end
-
-	-- Update active index and playlist content on playlist changes
-	function handle_playlist_change()
-		if menu:is_open('playlist') then
-			local items, active_item = serialize_playlist()
-			elements.menu:set_items(items, {
-				active_item = active_item,
-				selected_item = active_item
-			})
-		end
-	end
-
-	local items, active_item = serialize_playlist()
-
-	menu:open(items, function(index)
-		mp.commandv('set', 'playlist-pos-1', tostring(index))
-	end, {
-		type = 'playlist',
-		title = 'Playlist',
-		active_item = active_item,
-		on_open = function()
-			mp.observe_property('playlist', 'native', handle_playlist_change)
-			mp.observe_property('playlist-pos-1', 'native', handle_playlist_change)
-		end,
-		on_close = function()
-			mp.unobserve_property(handle_playlist_change)
-		end,
-	})
-end)
-mp.add_key_binding(nil, 'chapters', function()
-	if menu:is_open('chapters') then menu:close() return end
-
-	local items = {}
-	local chapters = get_normalized_chapters()
-
-	for index, chapter in ipairs(chapters) do
-		items[#items + 1] = {
-			title = chapter.title or '',
-			hint = mp.format_time(chapter.time),
-			value = chapter.time
-		}
-	end
-
-	-- Select first chapter from the end with time lower
-	-- than current playing position (with 100ms leeway).
-	function get_selected_chapter_index()
-		local position = mp.get_property_native('playback-time')
-		if not position then return nil end
-		for index = #items, 1, -1 do
-			if position - 0.1 > items[index].value then return index end
-		end
-	end
-
-	-- Update selected chapter in chapter navigation menu
-	function seek_handler()
-		if menu:is_open('chapters') then
-			elements.menu:activate_index(get_selected_chapter_index())
-		end
-	end
-
-	menu:open(items, function(time)
-		mp.commandv('seek', tostring(time), 'absolute')
-	end, {
-		type = 'chapters',
-		title = 'Chapters',
-		active_item = get_selected_chapter_index(),
-		on_open = function() mp.register_event('seek', seek_handler) end,
-		on_close = function() mp.unregister_event(seek_handler) end
-	})
-end)
-mp.add_key_binding(nil, 'show-in-directory', function()
-	local path = mp.get_property_native('path')
-
-	-- Ignore URLs
-	if not path or is_protocol(path) then return end
-
-	path = normalize_path(path)
-
-	if state.os == 'windows' then
-		utils.subprocess_detached({args = {'explorer', '/select,', path}, cancellable = false})
-	elseif state.os == 'macos' then
-		utils.subprocess_detached({args = {'open', '-R', path}, cancellable = false})
-	elseif state.os == 'linux' then
-		local result = utils.subprocess({args = {'nautilus', path}, cancellable = false})
-
-		-- Fallback opens the folder with xdg-open instead
-		if result.status ~= 0 then
-			utils.subprocess({args = {'xdg-open', serialize_path(path).dirname}, cancellable = false})
-		end
-	end
-end)
-mp.add_key_binding(nil, 'open-file', function()
-	if menu:is_open('open-file') then menu:close() return end
-
-	local path = mp.get_property_native('path')
-	local directory
-	local active_file
-
-	if path == nil or is_protocol(path) then
-		local path = serialize_path(mp.command_native({'expand-path', '~/'}))
-		directory = path.path
-		active_file = nil
-	else
-		local path = serialize_path(path)
-		directory = path.dirname
-		active_file = path.path
-	end
-
-	-- Update selected file in directory navigation menu
-	function handle_file_loaded()
-		if menu:is_open('open-file') then
-			local path = normalize_path(mp.get_property_native('path'))
-			elements.menu:activate_value(path)
-			elements.menu:select_value(path)
-		end
-	end
-
-	open_file_navigation_menu(
-		directory,
-		function(path) mp.commandv('loadfile', path) end,
-		{
-			type = 'open-file',
-			allowed_types = options.media_types,
-			active_path = active_file,
-			on_open = function() mp.register_event('file-loaded', handle_file_loaded) end,
-			on_close = function() mp.unregister_event(handle_file_loaded) end,
-		}
-	)
-end)
-mp.add_key_binding(nil, 'next', function()
-	if mp.get_property_native('playlist-count') > 1 then
-		mp.command('playlist-next')
-	else
-		navigate_directory('forward')
-	end
-end)
-mp.add_key_binding(nil, 'prev', function()
-	if mp.get_property_native('playlist-count') > 1 then
-		mp.command('playlist-prev')
-	else
-		navigate_directory('backward')
-	end
-end)
-mp.add_key_binding(nil, 'next-file', function() navigate_directory('forward') end)
-mp.add_key_binding(nil, 'prev-file', function() navigate_directory('backward') end)
-mp.add_key_binding(nil, 'first', function()
-	if mp.get_property_native('playlist-count') > 1 then
-		mp.commandv('set', 'playlist-pos-1', '1')
-	else
-		load_file_in_current_directory(1)
-	end
-end)
-mp.add_key_binding(nil, 'last', function()
-	local playlist_count = mp.get_property_native('playlist-count')
-	if playlist_count > 1 then
-		mp.commandv('set', 'playlist-pos-1', tostring(playlist_count))
-	else
-		load_file_in_current_directory(-1)
-	end
-end)
-mp.add_key_binding(nil, 'first-file', function() load_file_in_current_directory(1) end)
-mp.add_key_binding(nil, 'last-file', function() load_file_in_current_directory(-1) end)
-mp.add_key_binding(nil, 'delete-file-next', function()
-	local path = mp.get_property_native('path')
-
-	if not path or is_protocol(path) then return end
-
-	path = normalize_path(path)
-	local playlist_count = mp.get_property_native('playlist-count')
-
-	if playlist_count > 1 then
-		mp.commandv('playlist-remove', 'current')
-	else
-		local next_file = get_adjacent_file(path, 'forward', options.media_types)
-
-		if menu:is_open('open-file') then
-			elements.menu:delete_value(path)
-		end
-
-		if next_file then
-			mp.commandv('loadfile', next_file)
-		else
-			mp.commandv('stop')
-		end
-	end
-
-	os.remove(path)
-end)
-mp.add_key_binding(nil, 'delete-file-quit', function()
-	local path = mp.get_property_native('path')
-	if not path or is_protocol(path) then return end
-	os.remove(normalize_path(path))
-	mp.command('quit')
-end)
-mp.add_key_binding(nil, 'open-config-directory', function()
-	local config = serialize_path(mp.command_native({'expand-path', '~~/mpv.conf'}))
-	local args
-
-	if state.os == 'windows' then
-		args = {'explorer', '/select,', config.path}
-	elseif state.os == 'macos' then
-		args = {'open', '-R', config.path}
-	elseif state.os == 'linux' then
-		args = {'xdg-open', config.dirname}
-	end
-
-	utils.subprocess_detached({args = args, cancellable = false})
-end)