summary refs log tree commit diff
path: root/.config/mpv/mvi
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.config/mpv/mvi/input.conf109
-rw-r--r--.config/mpv/mvi/mpv.conf49
-rw-r--r--.config/mpv/mvi/script-opts/detect_image.conf17
-rw-r--r--.config/mpv/mvi/script-opts/image_positioning.conf14
-rw-r--r--.config/mpv/mvi/script-opts/minimap.conf19
-rw-r--r--.config/mpv/mvi/script-opts/ruler.conf21
-rw-r--r--.config/mpv/mvi/script-opts/status_line.conf14
-rw-r--r--.config/mpv/mvi/scripts/detect-image.lua73
-rw-r--r--.config/mpv/mvi/scripts/equalizer.lua176
-rw-r--r--.config/mpv/mvi/scripts/freeze-window.lua14
-rw-r--r--.config/mpv/mvi/scripts/image-positioning.lua277
-rw-r--r--.config/mpv/mvi/scripts/minimap.lua201
l---------.config/mpv/mvi/scripts/mouse_coords.lua1
-rw-r--r--.config/mpv/mvi/scripts/ruler.lua314
-rw-r--r--.config/mpv/mvi/scripts/status-line.lua142
15 files changed, 1441 insertions, 0 deletions
diff --git a/.config/mpv/mvi/input.conf b/.config/mpv/mvi/input.conf
new file mode 100644
index 0000000..1d5b989
--- /dev/null
+++ b/.config/mpv/mvi/input.conf
@@ -0,0 +1,109 @@
+1 change-list script-opts append image_positioning-drag_to_pan_margin=200
+2 change-list script-opts append ruler-exit_bindings=8
+3 change-list script-opts append ruler-line_color=FF
+4 change-list script-opts append ruler-scale=25
+5 change-list script-opts append ruler-max_size=20,20
+
+SPACE repeatable playlist-next
+alt+SPACE repeatable playlist-prev
+
+# familiar binds
+n playlist-next
+p playlist-prev
+
+UP ignore
+DOWN ignore
+LEFT repeatable playlist-prev
+RIGHT repeatable playlist-next
+
+# simple reminder of default bindings
+#1 add contrast -1
+#2 add contrast 1
+#3 add brightness -1
+#4 add brightness 1
+#5 add gamma -1
+#6 add gamma 1
+#7 add saturation -1
+#8 add saturation 1
+
+# mouse-centric bindings
+MBTN_RIGHT script-binding drag-to-pan
+MBTN_LEFT  script-binding pan-follows-cursor
+MBTN_LEFT_DBL ignore
+WHEEL_UP   script-message cursor-centric-zoom 0.1
+WHEEL_DOWN script-message cursor-centric-zoom -0.1
+
+# panning with the keyboard:
+# pan-image takes the following arguments
+# pan-image AXIS AMOUNT ZOOM_INVARIANT IMAGE_CONSTRAINED
+#            ^            ^                  ^
+#          x or y         |                  |
+#                         |                  |
+#   if yes, will pan by the same         if yes, stops panning if the image
+#     amount regardless of zoom             would go outside of the window
+
+ctrl+down  repeatable script-message pan-image y -0.1 yes yes
+ctrl+up    repeatable script-message pan-image y +0.1 yes yes
+ctrl+right repeatable script-message pan-image x -0.1 yes yes
+ctrl+left  repeatable script-message pan-image x +0.1 yes yes
+
+# now with more precision
+alt+down   repeatable script-message pan-image y -0.01 yes yes
+alt+up     repeatable script-message pan-image y +0.01 yes yes
+alt+right  repeatable script-message pan-image x -0.01 yes yes
+alt+left   repeatable script-message pan-image x +0.01 yes yes
+
+# replace at will with h,j,k,l if you prefer vim-style bindings
+
+# on a trackpad you may want to use these
+#WHEEL_UP    repeatable script-message pan-image y -0.02 yes yes
+#WHEEL_DOWN  repeatable script-message pan-image y +0.02 yes yes
+#WHEEL_LEFT  repeatable script-message pan-image x -0.02 yes yes
+#WHEEL_RIGHT repeatable script-message pan-image x +0.02 yes yes
+
+# align the border of the image to the border of the window
+# align-border takes the following arguments:
+# align-border ALIGN_X ALIGN_Y
+# any value for ALIGN_* is accepted, -1 and 1 map to the border of the window
+ctrl+shift+right script-message align-border -1 ""
+ctrl+shift+left  script-message align-border 1 ""
+ctrl+shift+down  script-message align-border "" -1
+ctrl+shift+up    script-message align-border "" 1
+
+# reset the image
+ctrl+0  no-osd set video-pan-x 0; no-osd set video-pan-y 0; no-osd set video-zoom 0
+
++ add video-zoom 0.5
+- add video-zoom -0.5; script-message reset-pan-if-visible
+= no-osd set video-zoom 0; script-message reset-pan-if-visible
+
+e script-message equalizer-toggle
+alt+e script-message equalizer-reset
+
+h no-osd vf toggle hflip; show-text "Horizontal flip"
+v no-osd vf toggle vflip; show-text "Vertical flip"
+
+r script-message rotate-video 90; show-text "Clockwise rotation"
+R script-message rotate-video -90; show-text "Counter-clockwise rotation"
+alt+r no-osd set video-rotate 0; show-text "Reset rotation"
+
+d script-message ruler
+
+# Toggling between pixel-exact reproduction and interpolation
+a cycle-values scale nearest ewa_lanczossharp
+
+# Toggle color management on or off
+c cycle icc-profile-auto
+
+# Screenshot of the window output
+S screenshot window
+
+# Toggle aspect ratio information on and off
+A cycle-values video-aspect-override "-1" "no"
+
+P script-message force-print-filename
+
+# ADVANCED: you can define bindings that belong to a "section" (named "image-viewer" here) like so:
+#alt+SPACE {image-viewer} repeatable playlist-prev
+#SPACE     {image-viewer} repeatable playlist-next
+# to load them conditionally with a command. See scripts-opts/image_viewer.conf for how you can do this
diff --git a/.config/mpv/mvi/mpv.conf b/.config/mpv/mvi/mpv.conf
new file mode 100644
index 0000000..973ba42
--- /dev/null
+++ b/.config/mpv/mvi/mpv.conf
@@ -0,0 +1,49 @@
+## IMAGE
+# classic opengl-hq parameter, change at will
+scale=spline36
+cscale=spline36
+dscale=mitchell
+dither-depth=auto
+correct-downscaling
+sigmoid-upscaling
+# debanding seems rarely useful with images
+#deband
+# dark grey background instead of pure black
+background=color
+background-color=0.2
+
+## MISC
+mute=yes
+# the osc is mostly useful for videos
+osc=no
+# don't try to autoload subtitles or audio files
+sub-auto=no
+audio-file-auto=no
+# get rid of the useless V: 00:00:00 / 00:00:00 line
+term-status-msg=
+
+# replace mpv with mvi in the window title
+title="${?media-title:${media-title}}${!media-title:No file} - mvi"
+
+# don't slideshow by default
+image-display-duration=inf
+# loop files in case of webms or gifs
+loop-file=inf
+# and loop the whole playlist
+loop-playlist=inf
+
+# you need this if you plan to use drag-to-pan or pan-follows-cursor with MOUSE_LEFT 
+window-dragging=no
+
+#according to haasn, aspect ratio info for PNG and JPG is "universally bust"
+[extension.png]
+video-aspect-override=no
+
+[extension.jpg]
+video-aspect-override=no
+
+[extension.jpeg]
+profile=extension.jpg
+
+[silent]
+msg-level=all=no
diff --git a/.config/mpv/mvi/script-opts/detect_image.conf b/.config/mpv/mvi/script-opts/detect_image.conf
new file mode 100644
index 0000000..586fb95
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/detect_image.conf
@@ -0,0 +1,17 @@
+# commands to execute when a file detected as an image (1 frame, no audio) is loaded or unloaded
+
+# an image was loaded, and the previous file was not an image (or there was no previous file)
+command_on_first_image_loaded=
+# an image was loaded (regardless of what the previous file was)
+command_on_image_loaded=
+# a non-image was loaded, and the previous file was an image
+command_on_non_image_loaded=
+
+# the purpose of these "hooks" is to let you change bindings, profiles, reset properties...
+# see https://mpv.io/manual/master/#list-of-input-commands for general command information
+# note that there is no such thing as "unloading a profile", to emulate this you must create an opposite profile and load that
+
+# example possible values:
+#command_on_first_image_loaded=apply-profile image; enable-section image-viewer; script-message status-line-enable
+#command_on_image_loaded=no-osd set video-pan-x 0; script-message align-border "" -1
+#command_on_non_image_loaded=disable-section image-viewer; no-osd set video-pan-x 0; no-osd set video-pan-y 0; no-osd set video-zoom 0; script-message status-line-disable
diff --git a/.config/mpv/mvi/script-opts/image_positioning.conf b/.config/mpv/mvi/script-opts/image_positioning.conf
new file mode 100644
index 0000000..998ec64
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/image_positioning.conf
@@ -0,0 +1,14 @@
+# size of the margins with drag-to-pan
+drag_to_pan_margin=50
+drag_to_pan_move_if_full_view=no
+
+# size of the margins with pan-follows-cursor
+pan_follows_cursor_margin=50
+
+# size of the margins with cursor-centric-zoom
+cursor_centric_zoom_margin=50
+# if the borders would show up, move the image 
+# this makes it not exactly cursor-centric in some cases
+cursor_centric_zoom_auto_center=yes
+# allow zooming out if the image can already fully fit in the window
+cursor_centric_zoom_dezoom_if_full_view=no
diff --git a/.config/mpv/mvi/script-opts/minimap.conf b/.config/mpv/mvi/script-opts/minimap.conf
new file mode 100644
index 0000000..72a05ea
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/minimap.conf
@@ -0,0 +1,19 @@
+# whether to show by default
+enabled=yes
+# the position of the center of the minimap, in percentage of the window (x, y)
+center=92,92
+# the scale of the minimap (i.e. the view rectangle is scale / 100 times the size of the window)
+scale=12
+# the cutoff size of the minimap (i.e. the image rectangle is clipped if it falls outside of the this zone)
+max_size=16,16
+# opacity of the "image" (from 00=opaque to FF=transparent)
+image_opacity=88
+# color of the "image" (#BBGGRR where each component rages from 00 to FF)
+image_color=BBBBBB
+# opacity of the "view"
+view_opacity=BB
+view_color=222222
+# whether the view should be drawn above the image
+view_above_image=yes
+# whether to show the minimap if the current image is fully visible
+hide_when_full_image_in_view=yes
diff --git a/.config/mpv/mvi/script-opts/ruler.conf b/.config/mpv/mvi/script-opts/ruler.conf
new file mode 100644
index 0000000..5e6f48d
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/ruler.conf
@@ -0,0 +1,21 @@
+# whether to show the length of the lines between the two points
+show_distance=yes
+# whether to show the coordinates of the two points
+show_coordinates=yes
+# the coordinate space of the text shown. Can be "image", "window", "both"
+coordinates_space=image
+# can be "degrees", "radians", "both", or "no"
+show_angles=degrees
+line_width=2
+dots_radius=3
+font_size=36
+# ranges from 00 (black) to FF (white)
+line_color=33
+# bindings used to set points. The binding to trigger ruler mode can also be used. Comma-separated list
+confirm_bindings=MBTN_LEFT,ENTER
+# bindings used to set points. The binding to trigger ruler mode can also be used. Comma-separated list
+exit_bindings=ESC
+# if yes, the first point will be immediately set at the cursor position when calling 'ruler'
+set_first_point_on_begin=no
+# if yes, the ruler overlay will be immediately cleared when setting the second point
+clear_on_second_point_set=no
diff --git a/.config/mpv/mvi/script-opts/status_line.conf b/.config/mpv/mvi/script-opts/status_line.conf
new file mode 100644
index 0000000..ec15108
--- /dev/null
+++ b/.config/mpv/mvi/script-opts/status_line.conf
@@ -0,0 +1,14 @@
+# whether to show by default
+enabled=yes
+# its font size
+size=36
+# distance of the text to the borders
+margin=10
+# the text to be expanded
+# see property expansion: https://mpv.io/manual/master/#property-expansion
+# \N can be used for line breaks
+# you can also use ass tags, see here: http://docs.aegisub.org/3.2/ASS_Tags/
+text_top_left=
+text_top_right=
+text_bottom_left=${filename} [${playlist-pos-1}/${playlist-count}]
+text_bottom_right=[${dwidth:X}x${dheight:X}]
diff --git a/.config/mpv/mvi/scripts/detect-image.lua b/.config/mpv/mvi/scripts/detect-image.lua
new file mode 100644
index 0000000..bfe52c2
--- /dev/null
+++ b/.config/mpv/mvi/scripts/detect-image.lua
@@ -0,0 +1,73 @@
+local opts = {
+    command_on_first_image_loaded="",
+    command_on_image_loaded="",
+    command_on_non_image_loaded="",
+}
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+
+options.read_options(opts, nil, function() end)
+
+function run_maybe(str)
+    if str ~= "" then
+        mp.command(str)
+    end
+end
+
+local was_image = false
+
+function set_image(is_image)
+    if is_image and not was_image then
+        msg.info("First image detected")
+        run_maybe(opts.command_on_first_image_loaded)
+    end
+    if is_image then
+        msg.info("Image detected")
+        run_maybe(opts.command_on_image_loaded)
+    end
+    if not is_image and was_image then
+        msg.info("Non-image detected")
+        run_maybe(opts.command_on_non_image_loaded)
+    end
+    was_image = is_image
+end
+
+local properties = {}
+
+function properties_changed()
+    local dwidth = properties["dwidth"]
+    local tracks = properties["track-list"]
+    local path = properties["path"]
+    local framecount = properties["estimated-frame-count"]
+
+    if not path or path == "" then return end
+    if not tracks or #tracks == 0 then return end
+    local audio_tracks = 0
+    for _, track in ipairs(tracks) do
+        if track.type == "audio" then
+            audio_tracks = audio_tracks + 1
+        end
+    end
+
+    -- only do things when state is consistent
+    if not framecount and audio_tracks > 0 then
+        set_image(false)
+    elseif framecount and dwidth and dwidth > 0 then
+        -- png have 0 frames, jpg 1 ¯\_(ツ)_/¯
+        set_image((framecount == 0 or framecount == 1) and audio_tracks == 0)
+    end
+end
+
+function observe(propname)
+    mp.observe_property(propname, "native", function(_, val)
+        if val ~= properties[propname] then
+            properties[propname] = val
+            msg.verbose("Property " .. propname .. " changed")
+            properties_changed()
+        end
+    end)
+end
+observe("estimated-frame-count")
+observe("track-list")
+observe("dwidth")
+observe("path")
diff --git a/.config/mpv/mvi/scripts/equalizer.lua b/.config/mpv/mvi/scripts/equalizer.lua
new file mode 100644
index 0000000..f7a50ab
--- /dev/null
+++ b/.config/mpv/mvi/scripts/equalizer.lua
@@ -0,0 +1,176 @@
+local opts = {
+    bars = 'brightness,contrast,gamma,saturation,hue',
+    draw_icons = true,
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+options.read_options(opts, nil, function(c)
+end)
+
+local enabled = false
+local active_bars = {}
+local bar_being_dragged = nil
+local stale = false
+
+function split_comma(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = str
+    end
+    return ret
+end
+
+function get_position_normalized(x, y, bar)
+    return (x - bar.x) / bar.w, (y - bar.y) / bar.h
+end
+
+function handle_mouse_move()
+    if not bar_being_dragged then return end
+    local bar = bar_being_dragged
+    local mx, my = mp.get_mouse_pos()
+    local nx, _ = get_position_normalized(mx, my, bar)
+    nx = math.max(0, math.min(nx, 1))
+    local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
+    mp.set_property_number(bar.property, val)
+    -- the observe_property call will take care of setting the value
+end
+
+function handle_mouse_left(table)
+    if table["event"] == "down" then
+        local mx, my = mp.get_mouse_pos()
+        for _, bar in ipairs(active_bars) do
+            local nx, ny = get_position_normalized(mx, my, bar)
+            if nx >= 0 and ny >= 0 and nx <= 1 and ny <= 1 then
+                bar_being_dragged = bar
+                local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
+                mp.set_property_number(bar.property, val)
+                mp.add_forced_key_binding("mouse_move", "mouse_move", handle_mouse_move)
+                break
+            end
+        end
+    elseif table["event"] == "up" then
+        mp.remove_key_binding("mouse_move")
+        bar_being_dragged = nil
+    end
+end
+
+function property_changed(prop, val)
+    for _, bar in ipairs(active_bars) do
+        if bar.property == prop then
+            bar.value = val
+            stale = true
+            break
+        end
+    end
+end
+
+function idle_handler()
+    if not stale then return end
+    stale = false
+    local a = assdraw.ass_new()
+    a:new_event()
+    a:append(string.format('{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}', '888888'))
+    a:pos(0, 0)
+    a:draw_start()
+    for _, bar in ipairs(active_bars) do
+        a:rect_cw(bar.x, bar.y, bar.x + bar.w, bar.y + bar.h)
+    end
+    a:new_event()
+    a:append(string.format('{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}', 'dddddd'))
+    a:pos(0, 0)
+    a:draw_start()
+    for _, bar in ipairs(active_bars) do
+        if bar.value > bar.min_value then
+            local val_norm = (bar.value - bar.min_value) / (bar.max_value - bar.min_value)
+            a:rect_cw(bar.x, bar.y, bar.x + val_norm * bar.w, bar.y + bar.h)
+        end
+    end
+    for _, bar in ipairs(active_bars) do
+        a:new_event()
+        a:append("{\\an6\\fs40\\bord2}")
+        a:pos(bar.x - 8, bar.y + bar.h/2 - 2)
+        a:append(bar.property:sub(1,1):upper() .. bar.property:sub(2,-1))
+    end
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, a.text)
+end
+
+function fix_position()
+    local ww, wh = mp.get_osd_size()
+    for i, bar in ipairs(active_bars) do
+        bar.x = ww / 5
+        bar.y = wh / 2 + i * 50
+        bar.w = ww - 2 * (ww / 5)
+        bar.h = 30
+    end
+end
+
+function dimensions_changed()
+    stale = true
+    fix_position()
+end
+
+function enable()
+    if enabled then return end
+    enabled = true
+    mp.add_forced_key_binding("MBTN_LEFT", "mouse_left", handle_mouse_left, {complex=true})
+    for i, prop in ipairs(split_comma(opts.bars)) do
+        local prop_info = mp.get_property_native("option-info/" .. prop)
+        if not prop_info then
+            msg.warn("Property \'" .. prop .. "\' does not exist")
+        elseif not prop_info.type == 'Integer' then
+            msg.warn("Property \'" .. prop .. "\' is not an integer")
+        else
+            mp.observe_property(prop, 'native', property_changed)
+            active_bars[#active_bars + 1] = {
+                property = prop,
+                value = mp.get_property_number(prop),
+                min_value = prop_info.min,
+                max_value = prop_info.max,
+            }
+        end
+    end
+    stale = true
+    fix_position()
+    mp.observe_property("osd-dimensions", "native", dimensions_changed)
+    mp.register_idle(idle_handler)
+end
+
+function disable()
+    if not enabled then return end
+    enabled = false
+    active_bars = {}
+    bar_being_dragged = nil
+    mp.remove_key_binding("mouse_left")
+    mp.remove_key_binding("mouse_move")
+    mp.unobserve_property(property_changed)
+    mp.unobserve_property(dimensions_changed)
+    mp.unregister_idle(idle_handler)
+    mp.set_osd_ass(1280, 720, "")
+end
+
+function toggle()
+    if enabled then
+        disable()
+    else
+        enable()
+    end
+end
+
+function reset()
+    for _, prop in ipairs(split_comma(opts.bars)) do
+        local prop_info = mp.get_property_native("option-info/" .. prop)
+        if prop_info and prop_info["default-value"] then
+            mp.set_property(prop_info["name"], prop_info["default-value"])
+        end
+    end
+end
+
+mp.add_key_binding(nil, "equalizer-enable", enable)
+mp.add_key_binding(nil, "equalizer-disable", disable)
+mp.add_key_binding(nil, "equalizer-toggle", toggle)
+mp.add_key_binding(nil, "equalizer-reset", reset)
diff --git a/.config/mpv/mvi/scripts/freeze-window.lua b/.config/mpv/mvi/scripts/freeze-window.lua
new file mode 100644
index 0000000..693c717
--- /dev/null
+++ b/.config/mpv/mvi/scripts/freeze-window.lua
@@ -0,0 +1,14 @@
+-- credit to TheAMM
+
+local size_changed = false
+
+mp.register_idle(function()
+    if not size_changed then return end
+    local ww, wh = mp.get_osd_size()
+    if not ww or ww <= 0 or not wh or wh <= 0 then return end
+    mp.set_property("geometry", string.format("%dx%d", ww, wh))
+    size_changed = false
+end)
+
+mp.observe_property("osd-width", "native", function() size_changed = true end)
+mp.observe_property("osd-height", "native", function() size_changed = true end)
diff --git a/.config/mpv/mvi/scripts/image-positioning.lua b/.config/mpv/mvi/scripts/image-positioning.lua
new file mode 100644
index 0000000..c0036d9
--- /dev/null
+++ b/.config/mpv/mvi/scripts/image-positioning.lua
@@ -0,0 +1,277 @@
+local opts = {
+    drag_to_pan_margin = 50,
+    drag_to_pan_move_if_full_view=false,
+
+    pan_follows_cursor_margin = 50,
+
+    cursor_centric_zoom_margin = 50,
+    cursor_centric_zoom_auto_center = true,
+    cursor_centric_zoom_dezoom_if_full_view = false,
+}
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+options.read_options(opts, nil, function() end)
+
+function clamp(value, low, high)
+    if value <= low then
+        return low
+    elseif value >= high then
+        return high
+    else
+        return value
+    end
+end
+
+
+local cleanup = nil -- function set up by drag-to-pan/pan-follows cursor and must be called to clean lingering state
+
+function drag_to_pan_handler(table)
+    if cleanup then
+        cleanup()
+        cleanup = nil
+    end
+    if table["event"] == "down" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local mouse_pos_origin, video_pan_origin = {}, {}
+        local moved = false
+        mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
+        video_pan_origin[1] = mp.get_property_number("video-pan-x")
+        video_pan_origin[2] = mp.get_property_number("video-pan-y")
+        local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+        local margin = opts.drag_to_pan_margin
+        local move_up = true
+        local move_lateral = true
+        if not opts.drag_to_pan_move_if_full_view then
+            if dim.ml >= 0 and dim.mr >= 0 then
+                move_lateral = false
+            end
+            if dim.mt >= 0 and dim.mb >= 0 then
+                move_up = false
+            end
+        end
+        if not move_up and not move_lateral then return end
+        local idle = function()
+            if moved then
+                local mX, mY = mp.get_mouse_pos()
+                local pX = video_pan_origin[1]
+                local pY = video_pan_origin[2]
+                if move_lateral then
+                    pX = video_pan_origin[1] + (mX - mouse_pos_origin[1]) / video_size[1]
+                    if 2 * margin > dim.ml + dim.mr then
+                        pX = clamp(pX,
+                            (-margin + dim.w / 2) / video_size[1] - 0.5,
+                            (margin - dim.w / 2) / video_size[1] + 0.5)
+                    else
+                        pX = clamp(pX,
+                            (margin - dim.w / 2) / video_size[1] + 0.5,
+                            (-margin + dim.w / 2) / video_size[1] - 0.5)
+                    end
+                end
+                if move_up then
+                    pY = video_pan_origin[2] + (mY - mouse_pos_origin[2]) / video_size[2]
+                    if 2 * margin > dim.mt + dim.mb then
+                        pY = clamp(pY,
+                            (-margin + dim.h / 2) / video_size[2] - 0.5,
+                            (margin - dim.h / 2) / video_size[2] + 0.5)
+                    else
+                        pY = clamp(pY,
+                            (margin - dim.h / 2) / video_size[2] + 0.5,
+                            (-margin + dim.h / 2) / video_size[2] - 0.5)
+                    end
+                end
+                mp.command("no-osd set video-pan-x " .. clamp(pX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(pY, -3, 3))
+                moved = false
+            end
+        end
+        mp.register_idle(idle)
+        mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
+        cleanup = function()
+            mp.remove_key_binding("image-viewer-mouse-move")
+            mp.unregister_idle(idle)
+        end
+    end
+end
+
+function pan_follows_cursor_handler(table)
+    if cleanup then
+        cleanup()
+        cleanup = nil
+    end
+    if table["event"] == "down" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+        local moved = true
+        local idle = function()
+            if moved then
+                local mX, mY = mp.get_mouse_pos()
+                local x = math.min(1, math.max(- 2 * mX / dim.w + 1, -1))
+                local y = math.min(1, math.max(- 2 * mY / dim.h + 1, -1))
+                local command = ""
+                local margin = opts.pan_follows_cursor_margin
+                if dim.ml + dim.mr < 0 then
+                    command = command .. "no-osd set video-pan-x " .. clamp(x * (2 * margin - dim.ml - dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
+                elseif mp.get_property_number("video-pan-x") ~= 0 then
+                    command = command .. "no-osd set video-pan-x " .. "0;"
+                end
+                if dim.mt + dim.mb < 0 then
+                    command = command .. "no-osd set video-pan-y " .. clamp(y * (2 * margin - dim.mt - dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
+                elseif mp.get_property_number("video-pan-y") ~= 0 then
+                    command = command .. "no-osd set video-pan-y " .. "0;"
+                end
+                if command ~= "" then
+                    mp.command(command)
+                end
+                moved = false
+            end
+        end
+        mp.register_idle(idle)
+        mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
+        cleanup = function()
+            mp.remove_key_binding("image-viewer-mouse-move")
+            mp.unregister_idle(idle)
+        end
+    end
+end
+
+function cursor_centric_zoom_handler(amt)
+    local zoom_inc = tonumber(amt)
+    if not zoom_inc or zoom_inc == 0 then return end
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+
+    local margin = opts.cursor_centric_zoom_margin
+
+    local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+
+    -- the size in pixels of the (in|de)crement
+    local diff_width  = (2 ^ zoom_inc - 1) * video_size[1]
+    local diff_height = (2 ^ zoom_inc - 1) * video_size[2]
+    if not opts.cursor_centric_zoom_dezoom_if_full_view and
+        zoom_inc < 0 and
+        video_size[1] + diff_width + 2 * margin <= dim.w and
+        video_size[2] + diff_height + 2 * margin <= dim.h
+    then
+        -- the zoom decrement is too much, reduce it such that the full image is visible, no more, no less
+        -- in addition, this should take care of trying too zoom out while everything is already visible
+        local new_zoom_inc_x = math.log((dim.w - 2 * margin) / video_size[1]) / math.log(2)
+        local new_zoom_inc_y = math.log((dim.h - 2 * margin) / video_size[2]) / math.log(2)
+        local new_zoom_inc = math.min(0, math.min(new_zoom_inc_x, new_zoom_inc_y))
+        zoom_inc = new_zoom_inc
+        diff_width  = (2 ^ zoom_inc - 1) * video_size[1]
+        diff_height = (2 ^ zoom_inc - 1) * video_size[2]
+    end
+    local new_width = video_size[1] + diff_width
+    local new_height = video_size[2] + diff_height
+
+    local mouse_pos_origin = {}
+    mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
+    local new_pan_x, new_pan_y
+
+    -- some additional constraints:
+    -- if image can be fully visible (in either direction), set pan to 0
+    -- if border would show on either side, then prefer adjusting the pan even if not cursor-centric
+    local auto_c = opts.cursor_centric_zoom_auto_center
+    if auto_c and new_width <= dim.w then
+        new_pan_x = 0
+    else
+        local pan_x = mp.get_property("video-pan-x")
+        local rx = (dim.ml + video_size[1] / 2 - mouse_pos_origin[1]) / (video_size[1] / 2)
+        new_pan_x = (pan_x * video_size[1] + rx * diff_width / 2) / new_width
+        if auto_c then
+            new_pan_x = clamp(new_pan_x, (dim.w - 2 * margin) / (2 * new_width) - 0.5, - (dim.w - 2 * margin) / (2 * new_width) + 0.5)
+        end
+    end
+
+    if auto_c and new_height <= dim.h then
+        new_pan_y = 0
+    else
+        local pan_y = mp.get_property("video-pan-y")
+        local ry = (dim.mt + video_size[2] / 2 - mouse_pos_origin[2]) / (video_size[2] / 2)
+        new_pan_y = (pan_y * video_size[2] + ry * diff_height / 2) / new_height
+        if auto_c then
+            new_pan_y = clamp(new_pan_y, (dim.h - 2 * margin) / (2 * new_height) - 0.5, - (dim.h - 2 * margin) / (2 * new_height) + 0.5)
+        end
+    end
+
+    local zoom_origin = mp.get_property("video-zoom")
+    mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(new_pan_x, -3, 3) .. "; no-osd set video-pan-y " .. clamp(new_pan_y, -3, 3))
+end
+
+function align_border(x, y)
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+    local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
+    local x, y = tonumber(x), tonumber(y)
+    local command = ""
+    if x then
+        command = command .. "no-osd set video-pan-x " .. clamp(- x * (dim.ml + dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
+    end
+    if y then
+        command = command .. "no-osd set video-pan-y " .. clamp(- y * (dim.mt + dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
+    end
+    if command ~= "" then
+        mp.command(command)
+    end
+end
+
+function pan_image(axis, amount, zoom_invariant, image_constrained)
+    amount = tonumber(amount)
+    if not amount or amount == 0 or axis ~= "x" and axis ~= "y" then return end
+    if zoom_invariant == "yes" then
+        amount = amount / 2 ^ mp.get_property_number("video-zoom")
+    end
+    local prop = "video-pan-" .. axis
+    local old_pan = mp.get_property_number(prop)
+    if image_constrained == "yes" then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        local margin =
+            (axis == "x" and amount > 0) and dim.ml
+            or (axis == "x" and amount < 0) and dim.mr
+            or (amount > 0) and dim.mt
+            or (amount < 0) and dim.mb
+        local vid_size = (axis == "x") and (dim.w - dim.ml - dim.mr) or (dim.h - dim.mt - dim.mb)
+        local pixels_moved = math.abs(amount) * vid_size
+        -- the margin is already visible, no point going further
+        if margin >= 0 then
+            return
+        elseif margin + pixels_moved > 0 then
+            amount = -(math.abs(amount) / amount) * margin / vid_size
+        end
+    end
+    mp.set_property_number(prop, old_pan + amount)
+end
+
+function rotate_video(amt)
+    local rot = mp.get_property_number("video-rotate")
+    rot = (rot + amt) % 360
+    mp.set_property_number("video-rotate", rot)
+end
+
+function reset_pan_if_visible()
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then return end
+    local command = ""
+    if (dim.ml + dim.mr >= 0) then
+        command = command .. "no-osd set video-pan-x 0" .. ";"
+    end
+    if (dim.mt + dim.mb >= 0) then
+        command = command .. "no-osd set video-pan-y 0" .. ";"
+    end
+    if command ~= "" then
+        mp.command(command)
+    end
+end
+
+mp.add_key_binding(nil, "drag-to-pan", drag_to_pan_handler, {complex = true})
+mp.add_key_binding(nil, "pan-follows-cursor", pan_follows_cursor_handler, {complex = true})
+mp.add_key_binding(nil, "cursor-centric-zoom", cursor_centric_zoom_handler)
+mp.add_key_binding(nil, "align-border", align_border)
+mp.add_key_binding(nil, "pan-image", pan_image)
+mp.add_key_binding(nil, "rotate-video", rotate_video)
+mp.add_key_binding(nil, "reset-pan-if-visible", reset_pan_if_visible)
+mp.add_key_binding(nil, "force-print-filename", force_print_filename)
diff --git a/.config/mpv/mvi/scripts/minimap.lua b/.config/mpv/mvi/scripts/minimap.lua
new file mode 100644
index 0000000..f1b9a7d
--- /dev/null
+++ b/.config/mpv/mvi/scripts/minimap.lua
@@ -0,0 +1,201 @@
+local opts = {
+    enabled = true,
+    center = "92,92",
+    scale = 12,
+    max_size = "16,16",
+    image_opacity = "88",
+    image_color = "BBBBBB",
+    view_opacity = "BB",
+    view_color = "222222",
+    view_above_image = true,
+    hide_when_full_image_in_view = true,
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+
+options.read_options(opts, nil, function(c)
+    if c["enabled"] then
+        if opts.enabled then
+            enable()
+        else
+            disable()
+        end
+    end
+    mark_stale()
+end)
+
+function split_comma(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = tonumber(str)
+    end
+    return ret
+end
+
+local active = false
+local refresh = true
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+function mark_stale()
+    refresh = true
+end
+
+function refresh_minimap()
+    if not refresh then return end
+    refresh = false
+
+    local dim = mp.get_property_native("osd-dimensions")
+    if not dim then
+        draw_ass("")
+        return
+    end
+    local ww, wh = dim.w, dim.h
+
+    if not (ww > 0 and wh > 0) then return end
+    if opts.hide_when_full_image_in_view then
+        if dim.mt >= 0 and dim.mb >= 0 and dim.ml >= 0 and dim.mr >= 0 then
+            draw_ass("")
+            return
+        end
+    end
+
+    local center = split_comma(opts.center)
+    center[1] = center[1] * 0.01 * ww
+    center[2] = center[2] * 0.01 * wh
+    local cutoff = split_comma(opts.max_size)
+    cutoff[1] = cutoff[1] * 0.01 * ww * 0.5
+    cutoff[2] = cutoff[2] * 0.01 * wh * 0.5
+
+    local a = assdraw.ass_new()
+    local draw = function(x, y, w, h, opacity, color)
+        a:new_event()
+        a:pos(center[1], center[2])
+        a:append("{\\bord0}")
+        a:append("{\\shad0}")
+        a:append("{\\c&" .. color .. "&}")
+        a:append("{\\2a&HFF}")
+        a:append("{\\3a&HFF}")
+        a:append("{\\4a&HFF}")
+        a:append("{\\1a&H" .. opacity .. "}")
+        w = w * 0.5
+        h = h * 0.5
+        a:draw_start()
+        local rounded = {true,true,true,true} -- tl, tr, br, bl
+        local x0,y0,x1,y1 = x-w, y-h, x+w, y+h
+        if x0 < -cutoff[1] then
+            x0 = -cutoff[1]
+            rounded[4] = false
+            rounded[1] = false
+        end
+        if y0 < -cutoff[2] then
+            y0 = -cutoff[2]
+            rounded[1] = false
+            rounded[2] = false
+        end
+        if x1 > cutoff[1] then
+            x1 = cutoff[1]
+            rounded[2] = false
+            rounded[3] = false
+        end
+        if y1 > cutoff[2] then
+            y1 = cutoff[2]
+            rounded[3] = false
+            rounded[4] = false
+        end
+
+        local r = 3
+        local c = 0.551915024494 * r
+        if rounded[0] then
+            a:move_to(x0 + r, y0)
+        else
+            a:move_to(x0,y0)
+        end
+        if rounded[1] then
+            a:line_to(x1 - r, y0)
+            a:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r)
+        else
+            a:line_to(x1, y0)
+        end
+        if rounded[2] then
+            a:line_to(x1, y1 - r)
+            a:bezier_curve(x1, y1 - r + c, x1 - r + c, y1, x1 - r, y1)
+        else
+            a:line_to(x1, y1)
+        end
+        if rounded[3] then
+            a:line_to(x0 + r, y1)
+            a:bezier_curve(x0 + r - c, y1, x0, y1 - r + c, x0, y1 - r)
+        else
+            a:line_to(x0, y1)
+        end
+        if rounded[4] then
+            a:line_to(x0, y0 + r)
+            a:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0)
+        else
+            a:line_to(x0, y0)
+        end
+        a:draw_stop()
+    end
+    local image = function()
+        draw((dim.ml/2 - dim.mr/2) / opts.scale,
+             (dim.mt/2 - dim.mb/2) / opts.scale,
+             (ww - dim.ml - dim.mr) / opts.scale,
+             (wh - dim.mt - dim.mb) / opts.scale,
+             opts.image_opacity,
+             opts.image_color)
+    end
+    local view = function()
+        draw(0,
+             0,
+             ww / opts.scale,
+             wh / opts.scale,
+             opts.view_opacity,
+             opts.view_color)
+    end
+    if opts.view_above_image then
+        image()
+        view()
+    else
+        view()
+        image()
+    end
+    draw_ass(a.text)
+end
+
+function enable()
+    if active then return end
+    active = true
+    mp.observe_property("osd-dimensions", nil, mark_stale)
+    mp.register_idle(refresh_minimap)
+    mark_stale()
+end
+
+function disable()
+    if not active then return end
+    active = false
+    mp.unobserve_property(mark_stale)
+    mp.unregister_idle(refresh_minimap)
+    draw_ass("")
+end
+
+function toggle()
+    if active then
+        disable()
+    else
+        enable()
+    end
+end
+
+if opts.enabled then
+    enable()
+end
+
+mp.add_key_binding(nil, "minimap-enable", enable)
+mp.add_key_binding(nil, "minimap-disable", disable)
+mp.add_key_binding(nil, "minimap-toggle", toggle)
diff --git a/.config/mpv/mvi/scripts/mouse_coords.lua b/.config/mpv/mvi/scripts/mouse_coords.lua
new file mode 120000
index 0000000..ecd3896
--- /dev/null
+++ b/.config/mpv/mvi/scripts/mouse_coords.lua
@@ -0,0 +1 @@
+../../scripts/mouse_coords.lua
\ No newline at end of file
diff --git a/.config/mpv/mvi/scripts/ruler.lua b/.config/mpv/mvi/scripts/ruler.lua
new file mode 100644
index 0000000..2cfcd0f
--- /dev/null
+++ b/.config/mpv/mvi/scripts/ruler.lua
@@ -0,0 +1,314 @@
+local opts = {
+    show_distance = true,
+    show_coordinates = true,
+    coordinates_space = "image",
+    show_angles = "degrees",
+    line_width = 2,
+    dots_radius = 3,
+    font_size = 36,
+    line_color = "33",
+    confirm_bindings = "MBTN_LEFT,ENTER",
+    exit_bindings = "ESC",
+    set_first_point_on_begin = false,
+    clear_on_second_point_set = false,
+}
+
+local options = require 'mp.options'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local state = 0 -- {0,1,2,3} = {inactive,setting first point,setting second point,done}
+local first_point = nil -- in normalized video space coordinates
+local second_point = nil -- in normalized video space coordinates
+local video_dimensions_stale = false
+
+function split(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = str
+    end
+    return ret
+end
+
+local confirm_bindings = split(opts.confirm_bindings)
+local exit_bindings = split(opts.exit_bindings)
+
+options.read_options(opts, nil, function()
+    if state ~= 0 then
+        remove_bindings()
+    end
+    confirm_bindings = split(opts.confirm_bindings)
+    exit_bindings = split(opts.exit_bindings)
+    if state ~= 0 then
+        add_bindings()
+        mark_stale()
+    end
+end)
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+
+function cursor_video_space_normalized(dim)
+    local mx, my = mp.get_mouse_pos()
+    local ret = {}
+    ret[1] = (mx - dim.ml) / (dim.w - dim.ml - dim.mr)
+    ret[2] = (my - dim.mt) / (dim.h - dim.mt - dim.mb)
+    return ret
+end
+
+function refresh()
+    if not video_dimensions_stale then return end
+    video_dimensions_stale = false
+
+    local dim = mp.get_property_native("osd-dimensions")
+    local out_params = mp.get_property_native("video-out-params")
+    if not dim or not out_params then
+        draw_ass("")
+        return
+    end
+    local vid_width = out_params.dw
+    local vid_height = out_params.dh
+
+    function video_space_normalized_to_video(point)
+        local ret = {}
+        ret[1] = point[1] * vid_width
+        ret[2] = point[2] * vid_height
+        return ret
+    end
+    function video_space_normalized_to_screen(point)
+        local ret = {}
+        ret[1] = point[1] * (dim.w - dim.ml - dim.mr) + dim.ml
+        ret[2] = point[2] * (dim.h - dim.mt - dim.mb) + dim.mt
+        return ret
+    end
+
+    local line_start = {}
+    local line_end = {}
+    if second_point then
+        line_start.image = video_space_normalized_to_video(first_point)
+        line_start.screen = video_space_normalized_to_screen(first_point)
+        line_end.image = video_space_normalized_to_video(second_point)
+        line_end.screen = video_space_normalized_to_screen(second_point)
+    elseif first_point then
+        line_start.image = video_space_normalized_to_video(first_point)
+        line_start.screen = video_space_normalized_to_screen(first_point)
+        line_end.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
+        line_end.screen = {}
+        line_end.screen[1], line_end.screen[2] = mp.get_mouse_pos()
+    else
+        local mx, my = mp.get_mouse_pos()
+        line_start.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
+        line_start.screen = {}
+        line_start.screen[1], line_start.screen[2] = mp.get_mouse_pos()
+        line_end = line_start
+    end
+    local distinct = (math.abs(line_start.screen[1] - line_end.screen[1]) >= 1
+                   or math.abs(line_start.screen[2] - line_end.screen[2]) >= 1)
+
+    local a = assdraw:ass_new()
+    local draw_setup = function(bord)
+        a:new_event()
+        a:pos(0,0)
+        a:append("{\\bord" .. bord .. "}")
+        a:append("{\\shad0}")
+        local r = opts.line_color
+        a:append("{\\3c&H".. r .. r .. r .. "&}")
+        a:append("{\\1a&HFF}")
+        a:append("{\\2a&HFF}")
+        a:append("{\\3a&H00}")
+        a:append("{\\4a&HFF}")
+        a:draw_start()
+    end
+    local dot = function(pos, size)
+        draw_setup(size)
+        a:move_to(pos[1], pos[2]-0.5)
+        a:line_to(pos[1], pos[2]+0.5)
+    end
+    local line = function(from, to, size)
+        draw_setup(size)
+        a:move_to(from[1], from[2])
+        a:line_to(to[1], to[2])
+    end
+    if distinct then
+        dot(line_start.screen, opts.dots_radius)
+        line(line_start.screen, line_end.screen, opts.line_width)
+        dot(line_end.screen, opts.dots_radius)
+    else
+        dot(line_start.screen, opts.dots_radius)
+    end
+
+    local line_info = function()
+        if not opts.show_distance then return end
+        a:new_event()
+        a:append("{\\fs36}{\\bord1}")
+        a:pos((line_start.screen[1] + line_end.screen[1]) / 2, (line_start.screen[2] + line_end.screen[2]) / 2)
+        local an = 1
+        if line_start.image[1] < line_end.image[1] then an = an + 2 end
+        if line_start.image[2] < line_end.image[2] then an = an + 6 end
+        a:an(an)
+        local image = math.sqrt(math.pow(line_start.image[1] - line_end.image[1], 2) + math.pow(line_start.image[2] - line_end.image[2], 2))
+        local screen = math.sqrt(math.pow(line_start.screen[1] - line_end.screen[1], 2) + math.pow(line_start.screen[2] - line_end.screen[2], 2))
+        if opts.coordinates_space == "both" then
+            a:append(string.format("image: %.1f\\Nscreen: %.1f", image, screen))
+        elseif opts.coordinates_space == "image" then
+            a:append(string.format("%.1f", image))
+        elseif opts.coordinates_space == "window" then
+            a:append(string.format("%.1f", screen))
+        end
+    end
+    local dot_info = function(pos, opposite)
+        if not opts.show_coordinates then return end
+        a:new_event()
+        a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
+        a:pos(pos.screen[1], pos.screen[2])
+        local an
+        if distinct then
+            an = 1
+            if line_start.image[1] > line_end.image[1] then an = an + 2 end
+            if line_start.image[2] < line_end.image[2] then an = an + 6 end
+        else
+            an = 7
+        end
+        if opposite then
+            an = 9 + 1 - an
+        end
+        a:an(an)
+        if opts.coordinates_space == "both" then
+            a:append(string.format("image: %.1f, %.1f\\Nscreen: %i, %i",
+                pos.image[1], pos.image[2], pos.screen[1], pos.screen[2]))
+        elseif opts.coordinates_space == "image" then
+            a:append(string.format("%.1f, %.1f", pos.image[1], pos.image[2]))
+        elseif opts.coordinates_space == "window" then
+            a:append(string.format("%i, %i", pos.screen[1], pos.screen[2]))
+        end
+    end
+    dot_info(line_start, true)
+    if distinct then
+        line_info()
+        dot_info(line_end, false)
+    end
+    if distinct and opts.show_angles ~= "no" then
+        local dist = 50
+        local pos_from_angle = function(mult, angle)
+            return {
+                line_start.screen[1] + mult * dist * math.cos(angle),
+                line_start.screen[2] + mult * dist * math.sin(angle),
+            }
+        end
+        local extended = { line_start.screen[1], line_start.screen[2] }
+        if line_end.screen[1] > line_start.screen[1] then
+            extended[1] = extended[1] + dist
+        else
+            extended[1] = extended[1] - dist
+        end
+        line(line_start.screen, extended, math.max(0, opts.line_width-0.5))
+        local angle = math.atan(math.abs(line_start.image[2] - line_end.image[2]) / math.abs(line_start.image[1] - line_end.image[1]))
+        local fix_angle
+        local an
+        if line_end.image[2] < line_start.image[2] and line_end.image[1] > line_start.image[1] then
+            -- upper-right
+            an = 4
+            fix_angle = function(angle) return - angle end
+        elseif line_end.image[2] < line_start.image[2] then
+            -- upper-left
+            an = 6
+            fix_angle = function(angle) return math.pi + angle end
+        elseif line_end.image[1] < line_start.image[1] then
+            -- bottom-left
+            an = 6
+            fix_angle = function(angle) return math.pi - angle end
+        else
+            -- bottom-right
+            an = 4
+            fix_angle = function(angle) return angle end
+        end
+        -- should implement this https://math.stackexchange.com/questions/873224/calculate-control-points-of-cubic-bezier-curve-approximating-a-part-of-a-circle
+        local cp1 = pos_from_angle(1, fix_angle(angle*1/4))
+        local cp2 = pos_from_angle(1, fix_angle(angle*3/4))
+        local p2 = pos_from_angle(1, fix_angle(angle))
+        a:bezier_curve(cp1[1], cp1[2], cp2[1], cp2[2], p2[1], p2[2])
+
+        a:new_event()
+        a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
+        local text_pos = pos_from_angle(1.1, fix_angle(angle*2/3)) -- you'd think /2 would make more sense, but *2/3  looks better
+        a:pos(text_pos[1], text_pos[2])
+        a:an(an)
+        if opts.show_angles == "both" then
+            a:append(string.format("%.2f\\N%.1f°", angle, angle / math.pi * 180))
+        elseif opts.show_angles == "degrees" then
+            a:append(string.format("%.1f°", angle / math.pi * 180))
+        elseif opts.show_angles == "radians" then
+            a:append(string.format("%.2f", angle))
+        end
+    end
+
+    draw_ass(a.text)
+end
+
+function mark_stale()
+    video_dimensions_stale = true
+end
+
+function add_bindings()
+    mp.add_forced_key_binding("mouse_move", "ruler-mouse-move", mark_stale)
+    for _, key in ipairs(confirm_bindings) do
+        mp.add_forced_key_binding(key, "ruler-next-" .. key, next_step)
+    end
+    for _, key in ipairs(exit_bindings) do
+        mp.add_forced_key_binding(key, "ruler-stop-" .. key, stop)
+    end
+end
+
+function remove_bindings()
+    for _, key in ipairs(confirm_bindings) do
+        mp.remove_key_binding("ruler-next-" .. key)
+    end
+    for _, key in ipairs(exit_bindings) do
+        mp.remove_key_binding("ruler-stop-" .. key)
+    end
+    mp.remove_key_binding("ruler-mouse-move")
+end
+
+function next_step()
+    if state == 0 then
+        state = 1
+        mp.register_idle(refresh)
+        mp.observe_property("osd-dimensions", nil, mark_stale)
+        mark_stale()
+        add_bindings()
+        if opts.set_first_point_on_begin then
+            next_step()
+        end
+    elseif state == 1 then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        state = 2
+        first_point = cursor_video_space_normalized(dim)
+    elseif state == 2 then
+        local dim = mp.get_property_native("osd-dimensions")
+        if not dim then return end
+        state = 3
+        second_point = cursor_video_space_normalized(dim)
+        if opts.clear_on_second_point_set then
+            next_step()
+        end
+    else
+        stop()
+    end
+end
+
+function stop()
+    if state == 0 then return end
+    mp.unregister_idle(refresh)
+    mp.unobserve_property(mark_stale)
+    remove_bindings()
+    state = 0
+    first_point = nil
+    second_point = nil
+    draw_ass("")
+end
+
+mp.add_key_binding(nil, "ruler", next_step)
diff --git a/.config/mpv/mvi/scripts/status-line.lua b/.config/mpv/mvi/scripts/status-line.lua
new file mode 100644
index 0000000..7ac5168
--- /dev/null
+++ b/.config/mpv/mvi/scripts/status-line.lua
@@ -0,0 +1,142 @@
+local opts = {
+    enabled = true,
+    size = 36,
+    margin = 10,
+    text_top_left = "",
+    text_top_right = "",
+    text_bottom_left = "${filename} [${playlist-pos-1}/${playlist-count}]",
+    text_bottom_right = "[${dwidth:X}x${dheight:X}]",
+}
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+local options = require 'mp.options'
+
+options.read_options(opts, nil, function(c)
+    if c["enabled"] then
+        if opts.enabled then
+            enable()
+        else
+            disable()
+        end
+    end
+    if c["size"] or c["margin"] then
+        mark_stale()
+    end
+    if c["text_top_left"] or
+       c["text_top_right"] or
+       c["text_bottom_left"] or
+       c["text_bottom_right"]
+    then
+        observe_properties()
+        mark_stale()
+    end
+end)
+
+local stale = true
+local active = false
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+function refresh()
+    if not stale then return end
+    stale = false
+    local a = assdraw:ass_new()
+    local draw_text = function(text, an, x, y)
+        if text == "" then return end
+        local expanded = mp.command_native({ "expand-text", text})
+        if not expanded then
+            msg.error("Error expanding status-line")
+            return
+        end
+        msg.verbose("Status-line changed to: " .. expanded)
+        a:new_event()
+        a:an(an)
+        a:pos(x,y)
+        a:append("{\\fs".. opts.size.. "}{\\bord1.0}")
+        a:append(expanded)
+    end
+    local w,h = mp.get_osd_size()
+    local m = opts.margin
+    draw_text(opts.text_top_left, 7, m, m)
+    draw_text(opts.text_top_right, 9, w-m, m)
+    draw_text(opts.text_bottom_left, 1, m, h-m)
+    draw_text(opts.text_bottom_right, 3, w-m, h-m)
+    draw_ass(a.text)
+end
+
+function mark_stale()
+    stale = true
+end
+
+function observe_properties()
+    mp.unobserve_property(mark_stale)
+    if not active then return end
+    for _, str in ipairs({
+        opts.text_top_left,
+        opts.text_top_right,
+        opts.text_bottom_left,
+        opts.text_bottom_right,
+    }) do
+        local start = 0
+        while true do
+            local s, e, cap = string.find(str, "%${[?!]?([%l%d-/]*)", start)
+            if not s then break end
+            msg.verbose("Observing property " .. cap)
+            mp.observe_property(cap, nil, mark_stale)
+            start = e
+        end
+    end
+    mp.observe_property("osd-width", nil, mark_stale)
+    mp.observe_property("osd-height", nil, mark_stale)
+end
+
+function enable()
+    if active then return end
+    active = true
+    observe_properties()
+    mp.register_idle(refresh)
+    mark_stale()
+end
+
+
+function disable()
+    if not active then return end
+    active = false
+    observe_properties()
+    mp.unregister_idle(refresh)
+    draw_ass("")
+end
+
+function toggle()
+    if active then
+        disable()
+    else
+        enable()
+    end
+end
+
+if opts.enabled then
+    enable()
+end
+
+mp.add_key_binding(nil, "status-line-enable", enable)
+mp.add_key_binding(nil, "status-line-disable", disable)
+mp.add_key_binding(nil, "status-line-toggle", toggle)
+
+-- TODO remove
+mp.add_key_binding(nil, "enable-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-enable' instead")
+    enable()
+end)
+mp.add_key_binding(nil, "disable-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-disable' instead")
+    disable()
+end)
+mp.add_key_binding(nil, "toggle-status-line", function()
+    msg.warn("This binding is deprecated, use 'status-line-toggle' instead")
+    toggle()
+end)