summary refs log tree commit diff
path: root/.config/mvi/scripts
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2020-12-03 12:56:51 +0100
committerRobert Günzler <r@gnzler.io>2020-12-07 19:28:05 +0100
commit0eb11ed2c3dc235349b87d20bf657f2fb9729994 (patch)
treeceba665d591328bb47418ab82bb0de31abff98e5 /.config/mvi/scripts
parente93cee56264c7e1d07b422d66430d996fd8b0c40 (diff)
Add bin/mvi and config; mpv-based image viewer
Diffstat (limited to '.config/mvi/scripts')
-rw-r--r--.config/mvi/scripts/autoload.lua220
-rw-r--r--.config/mvi/scripts/detect-image.lua89
-rw-r--r--.config/mvi/scripts/freeze-window.lua14
l---------.config/mvi/scripts/gallery-thumbgen.lua1
-rw-r--r--.config/mvi/scripts/image-positioning.lua341
l---------.config/mvi/scripts/lib.disable1
-rw-r--r--.config/mvi/scripts/minimap.lua306
l---------.config/mvi/scripts/playlist-view.lua1
-rw-r--r--.config/mvi/scripts/ruler.lua388
-rw-r--r--.config/mvi/scripts/status-line.lua108
10 files changed, 1469 insertions, 0 deletions
diff --git a/.config/mvi/scripts/autoload.lua b/.config/mvi/scripts/autoload.lua
new file mode 100644
index 0000000..7150abb
--- /dev/null
+++ b/.config/mvi/scripts/autoload.lua
@@ -0,0 +1,220 @@
+-- This script automatically loads playlist entries before and after the
+-- the currently played file. It does so by scanning the directory a file is
+-- located in when starting playback. It sorts the directory entries
+-- alphabetically, and adds entries before and after the current file to
+-- the internal playlist. (It stops if it would add an already existing
+-- playlist entry at the same position - this makes it "stable".)
+-- Add at most 5000 * 2 files when starting a file (before + after).
+
+--[[
+To configure this script use file autoload.conf in directory script-opts (the "script-opts"
+directory must be in the mpv configuration directory, typically ~/.config/mpv/).
+
+Example configuration would be:
+
+disabled=no
+images=no
+videos=yes
+audio=yes
+
+--]]
+
+MAXENTRIES = 5000
+
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+o = {
+    disabled = false,
+    images = true,
+    videos = true,
+    audio = true
+}
+options.read_options(o)
+
+function Set (t)
+    local set = {}
+    for _, v in pairs(t) do set[v] = true end
+    return set
+end
+
+function SetUnion (a,b)
+    local res = {}
+    for k in pairs(a) do res[k] = true end
+    for k in pairs(b) do res[k] = true end
+    return res
+end
+
+EXTENSIONS_VIDEO = Set {
+    'mkv', 'avi', 'mp4', 'ogv', 'webm', 'rmvb', 'flv', 'wmv', 'mpeg', 'mpg', 'm4v', '3gp'
+}
+
+EXTENSIONS_AUDIO = Set {
+    'mp3', 'wav', 'ogm', 'flac', 'm4a', 'wma', 'ogg', 'opus'
+}
+
+EXTENSIONS_IMAGES = Set {
+    'jpg', 'jpeg', 'png', 'tif', 'tiff', 'gif', 'webp', 'svg', 'bmp'
+}
+
+EXTENSIONS = Set {}
+if o.videos then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_VIDEO) end
+if o.audio then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_AUDIO) end
+if o.images then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) end
+
+function add_files_at(index, files)
+    index = index - 1
+    local oldcount = mp.get_property_number("playlist-count", 1)
+    for i = 1, #files do
+        mp.commandv("loadfile", files[i], "append")
+        mp.commandv("playlist-move", oldcount + i - 1, index + i - 1)
+    end
+end
+
+function get_extension(path)
+    match = string.match(path, "%.([^%.]+)$" )
+    if match == nil then
+        return "nomatch"
+    else
+        return match
+    end
+end
+
+table.filter = function(t, iter)
+    for i = #t, 1, -1 do
+        if not iter(t[i]) then
+            table.remove(t, i)
+        end
+    end
+end
+
+-- splitbynum and alnumcomp from alphanum.lua (C) Andre Bogus
+-- Released under the MIT License
+-- http://www.davekoelle.com/files/alphanum.lua
+
+-- split a string into a table of number and string values
+function splitbynum(s)
+    local result = {}
+    for x, y in (s or ""):gmatch("(%d*)(%D*)") do
+        if x ~= "" then table.insert(result, tonumber(x)) end
+        if y ~= "" then table.insert(result, y) end
+    end
+    return result
+end
+
+function clean_key(k)
+    k = (' '..k..' '):gsub("%s+", " "):sub(2, -2):lower()
+    return splitbynum(k)
+end
+
+-- compare two strings
+function alnumcomp(x, y)
+    local xt, yt = clean_key(x), clean_key(y)
+    for i = 1, math.min(#xt, #yt) do
+        local xe, ye = xt[i], yt[i]
+        if type(xe) == "string" then ye = tostring(ye)
+        elseif type(ye) == "string" then xe = tostring(xe) end
+        if xe ~= ye then return xe < ye end
+    end
+    return #xt < #yt
+end
+
+local autoloaded = nil
+
+function find_and_add_entries()
+    local path = mp.get_property("path", "")
+    local dir, filename = utils.split_path(path)
+    msg.trace(("dir: %s, filename: %s"):format(dir, filename))
+    if o.disabled then
+        msg.verbose("stopping: autoload disabled")
+        return
+    elseif #dir == 0 then
+        msg.verbose("stopping: not a local path")
+        return
+    end
+
+    local pl_count = mp.get_property_number("playlist-count", 1)
+    -- check if this is a manually made playlist
+    if (pl_count > 1 and autoloaded == nil) or
+       (pl_count == 1 and EXTENSIONS[string.lower(get_extension(filename))] == nil) then
+        msg.verbose("stopping: manually made playlist")
+        return
+    else
+        autoloaded = true
+    end
+
+    local pl = mp.get_property_native("playlist", {})
+    local pl_current = mp.get_property_number("playlist-pos-1", 1)
+    msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current,
+        utils.to_string(pl)))
+
+    local files = utils.readdir(dir, "files")
+    if files == nil then
+        msg.verbose("no other files in directory")
+        return
+    end
+    table.filter(files, function (v, k)
+        if string.match(v, "^%.") then
+            return false
+        end
+        local ext = get_extension(v)
+        if ext == nil then
+            return false
+        end
+        return EXTENSIONS[string.lower(ext)]
+    end)
+    table.sort(files, alnumcomp)
+
+    if dir == "." then
+        dir = ""
+    end
+
+    -- Find the current pl entry (dir+"/"+filename) in the sorted dir list
+    local current
+    for i = 1, #files do
+        if files[i] == filename then
+            current = i
+            break
+        end
+    end
+    if current == nil then
+        return
+    end
+    msg.trace("current file position in files: "..current)
+
+    local append = {[-1] = {}, [1] = {}}
+    for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1
+        for i = 1, MAXENTRIES do
+            local file = files[current + i * direction]
+            local pl_e = pl[pl_current + i * direction]
+            if file == nil or file[1] == "." then
+                break
+            end
+
+            local filepath = dir .. file
+            if pl_e then
+                -- If there's a playlist entry, and it's the same file, stop.
+                msg.trace(pl_e.filename.." == "..filepath.." ?")
+                if pl_e.filename == filepath then
+                    break
+                end
+            end
+
+            if direction == -1 then
+                if pl_current == 1 then -- never add additional entries in the middle
+                    msg.info("Prepending " .. file)
+                    table.insert(append[-1], 1, filepath)
+                end
+            else
+                msg.info("Adding " .. file)
+                table.insert(append[1], filepath)
+            end
+        end
+    end
+
+    add_files_at(pl_current + 1, append[1])
+    add_files_at(pl_current, append[-1])
+end
+
+mp.register_event("start-file", find_and_add_entries)
diff --git a/.config/mvi/scripts/detect-image.lua b/.config/mvi/scripts/detect-image.lua
new file mode 100644
index 0000000..1b3496c
--- /dev/null
+++ b/.config/mvi/scripts/detect-image.lua
@@ -0,0 +1,89 @@
+local opts = {
+    command_on_first_image_loaded="",
+    command_on_image_loaded="",
+    command_on_non_image_loaded="",
+}
+(require 'mp.options').read_options(opts)
+
+if opts.command_on_first_image_loaded == ""
+    and opts.command_on_image_loaded == ""
+    and opts.command_on_non_image_loaded == ""
+then
+    return
+end
+
+local msg = require 'mp.msg'
+
+local was_image = false
+local frame_count = nil
+local audio_tracks = nil
+local out_params_ready = nil
+local path = nil
+
+function run_maybe(str)
+    if str ~= "" then
+        mp.command(str)
+    end
+end
+
+function set_image(is_image)
+    if is_image and not was_image then
+        msg.verbose("First image detected")
+        run_maybe(opts.command_on_first_image_loaded)
+    end
+    if is_image then
+        msg.verbose("Image detected")
+        run_maybe(opts.command_on_image_loaded)
+    end
+    if not is_image and was_image then
+        msg.verbose("Non-image detected")
+        run_maybe(opts.command_on_non_image_loaded)
+    end
+    was_image = is_image
+end
+
+function state_changed()
+    -- only do things when state is consistent
+    if path ~= nil and audio_tracks ~= nil then
+        if frame_count == nil and audio_tracks > 0 then
+            set_image(false)
+        elseif out_params_ready and frame_count ~= nil then
+            -- png have 0 frames, jpg 1 ¯\_(ツ)_/¯
+            set_image((frame_count == 0 or frame_count == 1) and audio_tracks == 0)
+        end
+    end
+end
+
+mp.observe_property("dwidth", "number", function(_, val)
+    out_params_ready = (val ~= nil and val > 0)
+    state_changed()
+end)
+
+mp.observe_property("estimated-frame-count", "number", function(_, val)
+    frame_count = val
+    state_changed()
+end)
+
+mp.observe_property("path", "string", function(_, val)
+    if not val or val == "" then
+        path = nil
+    else
+        path = val
+    end
+    state_changed()
+end)
+
+mp.register_event("tracks-changed", function()
+    audio_tracks = 0
+    local tracks = 0
+    for _, track in ipairs(mp.get_property_native("track-list")) do
+        tracks = tracks + 1
+         if track.type == "audio" then
+             audio_tracks = audio_tracks + 1
+         end
+    end
+    if tracks == 0 then
+        audio_tracks = nil
+    end
+    state_changed()
+end)
diff --git a/.config/mvi/scripts/freeze-window.lua b/.config/mvi/scripts/freeze-window.lua
new file mode 100644
index 0000000..693c717
--- /dev/null
+++ b/.config/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/mvi/scripts/gallery-thumbgen.lua b/.config/mvi/scripts/gallery-thumbgen.lua
new file mode 120000
index 0000000..92816c8
--- /dev/null
+++ b/.config/mvi/scripts/gallery-thumbgen.lua
@@ -0,0 +1 @@
+../../mpv/scripts/mpv-gallery-view/scripts/gallery-thumbgen.lua
\ No newline at end of file
diff --git a/.config/mvi/scripts/image-positioning.lua b/.config/mvi/scripts/image-positioning.lua
new file mode 100644
index 0000000..08fa1e7
--- /dev/null
+++ b/.config/mvi/scripts/image-positioning.lua
@@ -0,0 +1,341 @@
+local opts = {
+    pan_follows_cursor_margin = 50,
+    pan_follows_cursor_move_if_full_view = false,
+
+    drag_to_pan_margin = 50,
+    drag_to_pan_move_if_full_view = false,
+}
+(require 'mp.options').read_options(opts)
+
+function clamp(value, low, high)
+    if value <= low then
+        return low
+    elseif value >= high then
+        return high
+    else
+        return value
+    end
+end
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+function get_video_dimensions()
+    -- this function is very much ripped from video/out/aspect.c in mpv's source
+    local video_params = mp.get_property_native("video-out-params")
+    if not video_params then
+        _video_dimensions = nil
+        return nil
+    end
+    _video_dimensions = {
+        top_left = { 0,  0 },
+        bottom_right = { 0,  0 },
+        size = { 0,  0 },
+        ratios = { 0,  0 }, -- by how much the original video got scaled
+    }
+    local keep_aspect = mp.get_property_bool("keepaspect")
+    local w = video_params["w"]
+    local h = video_params["h"]
+    local dw = video_params["dw"]
+    local dh = video_params["dh"]
+    if mp.get_property_number("video-rotate") % 180 == 90 then
+        w, h = h,w
+        dw, dh = dh, dw
+    end
+    local window_w, window_h = mp.get_osd_size()
+
+    if keep_aspect then
+        local unscaled = mp.get_property_native("video-unscaled")
+        local panscan = mp.get_property_number("panscan")
+
+        local fwidth = window_w
+        local fheight = math.floor(window_w / dw * dh)
+        if fheight > window_h or fheight < h then
+            local tmpw = math.floor(window_h / dh * dw)
+            if tmpw <= window_w then
+                fheight = window_h
+                fwidth = tmpw
+            end
+        end
+        local vo_panscan_area = window_h - fheight
+        local f_w = fwidth / fheight
+        local f_h = 1
+        if vo_panscan_area == 0 then
+            vo_panscan_area = window_h - fwidth
+            f_w = 1
+            f_h = fheight / fwidth
+        end
+        if unscaled or unscaled == "downscale-big" then
+            vo_panscan_area = 0
+            if unscaled or (dw <= window_w and dh <= window_h) then
+                fwidth = dw
+                fheight = dh
+            end
+        end
+
+        local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w)
+        local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h)
+
+        local split_scaling = function (dst_size, scaled_src_size, zoom, align, pan)
+            scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
+            align = (align + 1) / 2
+            local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size)
+            if dst_start < 0 then
+                --account for C int cast truncating as opposed to flooring
+                dst_start = dst_start + 1
+            end
+            local dst_end = dst_start + scaled_src_size;
+            if dst_start >= dst_end then
+                dst_start = 0
+                dst_end = 1
+            end
+            return dst_start, dst_end
+        end
+        local zoom = mp.get_property_number("video-zoom")
+
+        local align_x = mp.get_property_number("video-align-x")
+        local pan_x = mp.get_property_number("video-pan-x")
+        _video_dimensions.top_left[1], _video_dimensions.bottom_right[1] = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)
+
+        local align_y = mp.get_property_number("video-align-y")
+        local pan_y = mp.get_property_number("video-pan-y")
+        _video_dimensions.top_left[2], _video_dimensions.bottom_right[2] = split_scaling(window_h,  scaled_height, zoom, align_y, pan_y)
+    else
+        _video_dimensions.top_left[1] = 0
+        _video_dimensions.bottom_right[1] = window_w
+        _video_dimensions.top_left[2] = 0
+        _video_dimensions.bottom_right[2] = window_h
+    end
+    _video_dimensions.size[1] = _video_dimensions.bottom_right[1] - _video_dimensions.top_left[1]
+    _video_dimensions.size[2] = _video_dimensions.bottom_right[2] - _video_dimensions.top_left[2]
+    _video_dimensions.ratios[1] = _video_dimensions.size[1] / w
+    _video_dimensions.ratios[2] = _video_dimensions.size[2] / h
+    return _video_dimensions
+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 video_dimensions = get_video_dimensions()
+        if not video_dimensions then return end
+        local window_w, window_h = mp.get_osd_size()
+        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 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 video_dimensions.size[1] <= window_w then
+                move_lateral = false
+            end
+            if video_dimensions.size[2] <= window_h 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_dimensions.size[1]
+                    if video_dimensions.size[1] + 2 * margin > window_w then
+                        pX = clamp(pX,
+                            (-margin + window_w / 2) / video_dimensions.size[1] - 0.5,
+                            (margin - window_w / 2) / video_dimensions.size[1] + 0.5)
+                    else
+                        pX = clamp(pX,
+                            (margin - window_w / 2) / video_dimensions.size[1] + 0.5,
+                            (-margin + window_w / 2) / video_dimensions.size[1] - 0.5)
+                    end
+                end
+                if move_up then
+                    pY = video_pan_origin[2] + (mY - mouse_pos_origin[2]) / video_dimensions.size[2]
+                    if video_dimensions.size[2] + 2 * margin > window_h then
+                        pY = clamp(pY,
+                            (-margin + window_h / 2) / video_dimensions.size[2] - 0.5,
+                            (margin - window_h / 2) / video_dimensions.size[2] + 0.5)
+                    else
+                        pY = clamp(pY,
+                            (margin - window_h / 2) / video_dimensions.size[2] + 0.5,
+                            (-margin + window_h / 2) / video_dimensions.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 video_dimensions = get_video_dimensions()
+        if not video_dimensions then return end
+        local window_w, window_h = mp.get_osd_size()
+        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 / window_w + 1, -1))
+                local y = math.min(1, math.max(- 2 * mY / window_h + 1, -1))
+                local command = ""
+                local margin, move_full = opts.pan_follows_cursor_margin, opts.pan_follows_cursor_move_if_full_view
+                if (not move_full and window_w < video_dimensions.size[1]) then
+                    command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size[1] - window_w + 2 * margin) / (2 * video_dimensions.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 (not move_full and window_h < video_dimensions.size[2]) then
+                    command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size[2] - window_h + 2 * margin) / (2 * video_dimensions.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 video_dimensions = get_video_dimensions()
+    if not video_dimensions then return end
+    local mouse_pos_origin, video_pan_origin = {}, {}
+    mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
+    video_pan_origin[1] = mp.get_property("video-pan-x")
+    video_pan_origin[2] = mp.get_property("video-pan-y")
+    local zoom_origin = mp.get_property("video-zoom")
+    -- how far the cursor is form the middle of the video (in percentage)
+    local rx = (video_dimensions.top_left[1] + video_dimensions.size[1] / 2 - mouse_pos_origin[1]) / (video_dimensions.size[1] / 2)
+    local ry = (video_dimensions.top_left[2] + video_dimensions.size[2] / 2 - mouse_pos_origin[2]) / (video_dimensions.size[2] / 2)
+
+    -- the size in pixels of the (in|de)crement
+    local diffHeight = (2 ^ zoom_inc - 1) * video_dimensions.size[2]
+    local diffWidth  = (2 ^ zoom_inc - 1) * video_dimensions.size[1]
+    local newPanX = (video_pan_origin[1] * video_dimensions.size[1] + rx * diffWidth / 2) / (video_dimensions.size[1] + diffWidth)
+    local newPanY = (video_pan_origin[2] * video_dimensions.size[2] + ry * diffHeight / 2) / (video_dimensions.size[2] + diffHeight)
+    mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(newPanX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(newPanY, -3, 3))
+end
+
+function align_border(x, y)
+    local video_dimensions = get_video_dimensions()
+    if not video_dimensions then return end
+    local window_w, window_h = mp.get_osd_size()
+    local x, y = tonumber(x), tonumber(y)
+    local command = ""
+    if x then
+        command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size[1] - window_w) / (2 * video_dimensions.size[1]), -3, 3) .. ";"
+    end
+    if y then
+        command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size[2] - window_h) / (2 * video_dimensions.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)
+    axis = (axis == "x") and 1 or 2
+    if image_constrained == "yes" then
+        local video_dimensions = get_video_dimensions()
+        if not video_dimensions then return end
+        local window = {}
+        window[1], window[2] = mp.get_osd_size()
+        local pixels_moved = amount * video_dimensions.size[axis]
+        -- should somehow refactor this
+        if pixels_moved > 0 then
+            if window[axis] > video_dimensions.size[axis] then
+                if video_dimensions.bottom_right[axis] >= window[axis] then return end
+                if video_dimensions.bottom_right[axis] + pixels_moved > window[axis] then
+                    amount = (window[axis] - video_dimensions.bottom_right[axis]) / video_dimensions.size[axis]
+                end
+            else
+                if video_dimensions.top_left[axis] >= 0 then return end
+                if video_dimensions.top_left[axis] + pixels_moved > 0 then
+                    amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[axis]
+                end
+            end
+        else
+            if window[axis] > video_dimensions.size[axis] then
+                if video_dimensions.top_left[axis] <= 0 then return end
+                if video_dimensions.top_left[axis] + pixels_moved < 0 then
+                    amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[axis]
+                end
+            else
+                if video_dimensions.bottom_right[axis] <= window[axis] then return end
+                if video_dimensions.bottom_right[axis] + pixels_moved < window[axis] then
+                    amount = (window[axis] - video_dimensions.bottom_right[axis]) / video_dimensions.size[axis]
+                end
+            end
+        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 video_dimensions = get_video_dimensions()
+    if not video_dimensions then return end
+    local window_w, window_h = mp.get_osd_size()
+    local command = ""
+    if (window_w >= video_dimensions.size[1]) then
+        command = command .. "no-osd set video-pan-x 0" .. ";"
+    end
+    if (window_h >= video_dimensions.size[2]) 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/mvi/scripts/lib.disable b/.config/mvi/scripts/lib.disable
new file mode 120000
index 0000000..bc6efec
--- /dev/null
+++ b/.config/mvi/scripts/lib.disable
@@ -0,0 +1 @@
+../../mpv/scripts/mpv-gallery-view/scripts/lib.disable
\ No newline at end of file
diff --git a/.config/mvi/scripts/minimap.lua b/.config/mvi/scripts/minimap.lua
new file mode 100644
index 0000000..faa7f62
--- /dev/null
+++ b/.config/mvi/scripts/minimap.lua
@@ -0,0 +1,306 @@
+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,
+}
+(require 'mp.options').read_options(opts)
+
+function process(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = tonumber(str)
+    end
+    return ret
+end
+opts.center=process(opts.center)
+opts.max_size=process(opts.max_size)
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local video_dimensions_stale = true
+
+function get_video_dimensions()
+    -- this function is very much ripped from video/out/aspect.c in mpv's source
+    if not video_dimensions_stale then return _video_dimensions end
+    local video_params = mp.get_property_native("video-out-params")
+    if not video_params then
+        _video_dimensions = nil
+        return nil
+    end
+    if not _timestamp then _timestamp = 0 end
+    _timestamp = _timestamp + 1
+    _video_dimensions = {
+        timestamp = _timestamp,
+        top_left = { 0, 0 },
+        bottom_right = { 0, 0 },
+        size = { 0, 0 },
+        ratios = { 0, 0 }, -- by how much the original video got scaled
+    }
+    local keep_aspect = mp.get_property_bool("keepaspect")
+    local w = video_params["w"]
+    local h = video_params["h"]
+    local dw = video_params["dw"]
+    local dh = video_params["dh"]
+    if mp.get_property_number("video-rotate") % 180 == 90 then
+        w, h = h,w
+        dw, dh = dh, dw
+    end
+    local window_w, window_h = mp.get_osd_size()
+
+    if keep_aspect then
+        local unscaled = mp.get_property_native("video-unscaled")
+        local panscan = mp.get_property_number("panscan")
+
+        local fwidth = window_w
+        local fheight = math.floor(window_w / dw * dh)
+        if fheight > window_h or fheight < h then
+            local tmpw = math.floor(window_h / dh * dw)
+            if tmpw <= window_w then
+                fheight = window_h
+                fwidth = tmpw
+            end
+        end
+        local vo_panscan_area = window_h - fheight
+        local f_w = fwidth / fheight
+        local f_h = 1
+        if vo_panscan_area == 0 then
+            vo_panscan_area = window_h - fwidth
+            f_w = 1
+            f_h = fheight / fwidth
+        end
+        if unscaled or unscaled == "downscale-big" then
+            vo_panscan_area = 0
+            if unscaled or (dw <= window_w and dh <= window_h) then
+                fwidth = dw
+                fheight = dh
+            end
+        end
+
+        local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w)
+        local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h)
+
+        local split_scaling = function (dst_size, scaled_src_size, zoom, align, pan)
+            scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
+            align = (align + 1) / 2
+            local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size)
+            if dst_start < 0 then
+                --account for C int cast truncating as opposed to flooring
+                dst_start = dst_start + 1
+            end
+            local dst_end = dst_start + scaled_src_size;
+            if dst_start >= dst_end then
+                dst_start = 0
+                dst_end = 1
+            end
+            return dst_start, dst_end
+        end
+        local zoom = mp.get_property_number("video-zoom")
+
+        local align_x = mp.get_property_number("video-align-x")
+        local pan_x = mp.get_property_number("video-pan-x")
+        _video_dimensions.top_left[1], _video_dimensions.bottom_right[1] = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)
+
+        local align_y = mp.get_property_number("video-align-y")
+        local pan_y = mp.get_property_number("video-pan-y")
+        _video_dimensions.top_left[2], _video_dimensions.bottom_right[2] = split_scaling(window_h,  scaled_height, zoom, align_y, pan_y)
+    else
+        _video_dimensions.top_left[1] = 0
+        _video_dimensions.bottom_right[1] = window_w
+        _video_dimensions.top_left[2] = 0
+        _video_dimensions.bottom_right[2] = window_h
+    end
+    _video_dimensions.size[1] = _video_dimensions.bottom_right[1] - _video_dimensions.top_left[1]
+    _video_dimensions.size[2] = _video_dimensions.bottom_right[2] - _video_dimensions.top_left[2]
+    _video_dimensions.ratios[1] = _video_dimensions.size[1] / w
+    _video_dimensions.ratios[2] = _video_dimensions.size[2] / h
+
+    if not (_video_dimensions.size[1] > 0 and _video_dimensions.size[2] > 0) then return nil end
+    video_dimensions_stale = false
+    return _video_dimensions
+end
+
+for _, p in ipairs({
+    "keepaspect",
+    "video-out-params",
+    "video-unscaled",
+    "panscan",
+    "video-zoom",
+    "video-align-x",
+    "video-pan-x",
+    "video-align-y",
+    "video-pan-y",
+    "osd-width",
+    "osd-height",
+}) do
+    mp.observe_property(p, nil, function() video_dimensions_stale = true end)
+end
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+local old_timestamp = -1
+
+function refresh_minimap()
+    local dim = get_video_dimensions()
+    if not dim then
+        draw_ass("")
+        return
+    end
+    if dim.timestamp == old_timestamp then return end
+    old_timestamp = dim.timestamp
+    local ww, wh = mp.get_osd_size()
+    if not (ww > 0 and wh > 0) then return end
+    if opts.hide_when_full_image_in_view then
+        if dim.top_left[1] >= 0 and
+           dim.top_left[2] >= 0 and
+           dim.bottom_right[1] <= ww and
+           dim.bottom_right[2] <= wh
+        then
+            draw_ass("")
+            return
+        end
+    end
+    local center = {
+        opts.center[1] * 0.01 * ww,
+        opts.center[2] * 0.01 * wh
+    }
+    local cutoff = {
+        opts.max_size[1] * 0.01 * ww * 0.5,
+        opts.max_size[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.top_left[1] + dim.size[1]/2 - ww/2) / opts.scale,
+             (dim.top_left[2] + dim.size[2]/2 - wh/2) / opts.scale,
+             dim.size[1] / opts.scale,
+             dim.size[2] / 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
+
+local active = false
+
+function enable_minimap()
+    if active then return end
+    active = true
+    mp.register_idle(refresh_minimap)
+end
+
+function disable_minimap()
+    if not active then return end
+    active = false
+    ass.minimap = a.text
+    draw_ass()
+    mp.unregister_idle(refresh_minimap)
+end
+
+function toggle()
+    if active then
+        disable_minimap()
+    else
+        enable_minimap()
+    end
+end
+
+if opts.enabled then
+    enable_minimap()
+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/mvi/scripts/playlist-view.lua b/.config/mvi/scripts/playlist-view.lua
new file mode 120000
index 0000000..ed4ac05
--- /dev/null
+++ b/.config/mvi/scripts/playlist-view.lua
@@ -0,0 +1 @@
+../../mpv/scripts/mpv-gallery-view/scripts/playlist-view.lua
\ No newline at end of file
diff --git a/.config/mvi/scripts/ruler.lua b/.config/mvi/scripts/ruler.lua
new file mode 100644
index 0000000..60355ce
--- /dev/null
+++ b/.config/mvi/scripts/ruler.lua
@@ -0,0 +1,388 @@
+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,
+}
+(require 'mp.options').read_options(opts)
+
+function split(input)
+    local ret = {}
+    for str in string.gmatch(input, "([^,]+)") do
+        ret[#ret + 1] = str
+    end
+    return ret
+end
+opts.confirm_bindings=split(opts.confirm_bindings)
+opts.exit_bindings=split(opts.exit_bindings)
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local video_dimensions_stale = true
+
+function get_video_dimensions()
+    -- this function is very much ripped from video/out/aspect.c in mpv's source
+    if not video_dimensions_stale then return _video_dimensions end
+    local video_params = mp.get_property_native("video-out-params")
+    if not video_params then
+        _video_dimensions = nil
+        return nil
+    end
+    if not _timestamp then _timestamp = 0 end
+    _timestamp = _timestamp + 1
+    _video_dimensions = {
+        timestamp = _timestamp,
+        top_left = { 0, 0 },
+        bottom_right = { 0, 0 },
+        size = { 0, 0 },
+        ratios = { 0, 0 }, -- by how much the original video got scaled
+    }
+    local keep_aspect = mp.get_property_bool("keepaspect")
+    local w = video_params["w"]
+    local h = video_params["h"]
+    local dw = video_params["dw"]
+    local dh = video_params["dh"]
+    if mp.get_property_number("video-rotate") % 180 == 90 then
+        w, h = h,w
+        dw, dh = dh, dw
+    end
+    local window_w, window_h = mp.get_osd_size()
+
+    if keep_aspect then
+        local unscaled = mp.get_property_native("video-unscaled")
+        local panscan = mp.get_property_number("panscan")
+
+        local fwidth = window_w
+        local fheight = math.floor(window_w / dw * dh)
+        if fheight > window_h or fheight < h then
+            local tmpw = math.floor(window_h / dh * dw)
+            if tmpw <= window_w then
+                fheight = window_h
+                fwidth = tmpw
+            end
+        end
+        local vo_panscan_area = window_h - fheight
+        local f_w = fwidth / fheight
+        local f_h = 1
+        if vo_panscan_area == 0 then
+            vo_panscan_area = window_h - fwidth
+            f_w = 1
+            f_h = fheight / fwidth
+        end
+        if unscaled or unscaled == "downscale-big" then
+            vo_panscan_area = 0
+            if unscaled or (dw <= window_w and dh <= window_h) then
+                fwidth = dw
+                fheight = dh
+            end
+        end
+
+        local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w)
+        local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h)
+
+        local split_scaling = function (dst_size, scaled_src_size, zoom, align, pan)
+            scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
+            align = (align + 1) / 2
+            local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size)
+            if dst_start < 0 then
+                --account for C int cast truncating as opposed to flooring
+                dst_start = dst_start + 1
+            end
+            local dst_end = dst_start + scaled_src_size;
+            if dst_start >= dst_end then
+                dst_start = 0
+                dst_end = 1
+            end
+            return dst_start, dst_end
+        end
+        local zoom = mp.get_property_number("video-zoom")
+
+        local align_x = mp.get_property_number("video-align-x")
+        local pan_x = mp.get_property_number("video-pan-x")
+        _video_dimensions.top_left[1], _video_dimensions.bottom_right[1] = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)
+
+        local align_y = mp.get_property_number("video-align-y")
+        local pan_y = mp.get_property_number("video-pan-y")
+        _video_dimensions.top_left[2], _video_dimensions.bottom_right[2] = split_scaling(window_h,  scaled_height, zoom, align_y, pan_y)
+    else
+        _video_dimensions.top_left[1] = 0
+        _video_dimensions.bottom_right[1] = window_w
+        _video_dimensions.top_left[2] = 0
+        _video_dimensions.bottom_right[2] = window_h
+    end
+    _video_dimensions.size[1] = _video_dimensions.bottom_right[1] - _video_dimensions.top_left[1]
+    _video_dimensions.size[2] = _video_dimensions.bottom_right[2] - _video_dimensions.top_left[2]
+    _video_dimensions.ratios[1] = _video_dimensions.size[1] / w
+    _video_dimensions.ratios[2] = _video_dimensions.size[2] / h
+    video_dimensions_stale = false
+    return _video_dimensions
+end
+
+for _, p in ipairs({
+    "keepaspect",
+    "video-out-params",
+    "video-unscaled",
+    "panscan",
+    "video-zoom",
+    "video-align-x",
+    "video-pan-x",
+    "video-align-y",
+    "video-pan-y",
+    "osd-width",
+    "osd-height",
+}) do
+    mp.observe_property(p, nil, function() video_dimensions_stale = true end)
+end
+
+local state = 0 -- {0,1,2,3} = {inactive,setting first point,setting second point,done}
+local first_point = nil -- in video space coordinates
+local second_point = nil -- in video space coordinates
+
+function draw_ass(ass)
+    local ww, wh = mp.get_osd_size()
+    mp.set_osd_ass(ww, wh, ass)
+end
+
+function cursor_video_space()
+    local dim = get_video_dimensions()
+    if not dim then return nil end
+    local mx, my = mp.get_mouse_pos()
+    local ret = {}
+    ret[1] = (mx - dim.top_left[1]) / dim.ratios[1]
+    ret[2] = (my - dim.top_left[2]) / dim.ratios[2]
+    return ret
+end
+
+function video_space_to_screen(point)
+    local dim = get_video_dimensions()
+    if not dim then return nil end
+    local ret = {}
+    ret[1] = point[1] * dim.ratios[1] + dim.top_left[1]
+    ret[2] = point[2] * dim.ratios[2] + dim.top_left[2]
+    return ret
+end
+
+function refresh()
+    local dim = get_video_dimensions()
+    if not dim then
+        draw_ass("")
+        return
+    end
+
+    local line_start = {}
+    local line_end = {}
+    if second_point then
+        line_start.image = first_point
+        line_start.screen = video_space_to_screen(first_point)
+        line_end.image = second_point
+        line_end.screen = video_space_to_screen(second_point)
+    elseif first_point then
+        line_start.image = first_point
+        line_start.screen = video_space_to_screen(first_point)
+        line_end.image = cursor_video_space()
+        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 = cursor_video_space()
+        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 next()
+    if state == 0 then
+        mp.register_idle(refresh)
+        mp.add_forced_key_binding("mouse_move", "ruler-mouse-move", function() end) -- only used to get an idle event on mouse move
+        for _,key in ipairs(opts.confirm_bindings) do
+            mp.add_forced_key_binding(key, "ruler-next-" .. key, next)
+        end
+        for _,key in ipairs(opts.exit_bindings) do
+            mp.add_forced_key_binding(key, "ruler-stop-" .. key, stop)
+        end
+        state = 1
+        if opts.set_first_point_on_begin then
+            next()
+        end
+    elseif state == 1 then
+        first_point = cursor_video_space()
+        state = 2
+    elseif state == 2 then
+        state = 3
+        second_point = cursor_video_space()
+        if opts.clear_on_second_point_set then
+            next()
+        end
+    else
+        stop()
+    end
+end
+
+function stop()
+    if state == 0 then return end
+    mp.unregister_idle(refresh)
+    for _,key in ipairs(opts.confirm_bindings) do
+        mp.remove_key_binding("ruler-next-" .. key)
+    end
+    for _,key in ipairs(opts.exit_bindings) do
+        mp.remove_key_binding("ruler-stop-" .. key)
+    end
+    mp.remove_key_binding("ruler-mouse-move")
+    state = 0
+    first_point = nil
+    second_point = nil
+    draw_ass("")
+end
+
+mp.add_key_binding(nil, "ruler", next)
diff --git a/.config/mvi/scripts/status-line.lua b/.config/mvi/scripts/status-line.lua
new file mode 100644
index 0000000..bc8c129
--- /dev/null
+++ b/.config/mvi/scripts/status-line.lua
@@ -0,0 +1,108 @@
+local opts = {
+    enabled = true,
+    position = "bottom-left",
+    size = 36,
+    text = "${filename} [${playlist-pos-1}/${playlist-count}]",
+}
+(require 'mp.options').read_options(opts)
+
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local stale = true
+
+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 expanded = mp.command_native({ "expand-text", opts.text})
+    if not expanded then
+        msg.error("Error expanding status-line")
+        draw_ass("")
+        return
+    end
+    msg.verbose("Status-line changed to: " .. expanded)
+    local w,h = mp.get_osd_size()
+    local an, x, y
+    local margin = 10
+    if opts.position == "top-left" then
+        x = margin
+        y = margin
+        an = 7
+    elseif opts.position == "top-right" then
+        x = w-margin
+        y = margin
+        an = 9
+    elseif opts.position == "bottom-right" then
+        x = w-margin
+        y = h-margin
+        an = 3
+    elseif opts.position == "bottom-left" then
+        x = margin
+        y = h-margin
+        an = 1
+    else
+        msg.error("Invalid position: " .. opts.position)
+        return
+    end
+    local a = assdraw:ass_new()
+    a:new_event()
+    a:an(an)
+    a:pos(x,y)
+    a:append("{\\fs".. opts.size.. "}{\\bord1.0}")
+    a:append(expanded)
+    draw_ass(a.text)
+end
+
+function mark_stale()
+    stale = true
+end
+
+local active = false
+
+function enable()
+    if active then return end
+    active = true
+    local start = 0
+    while true do
+        local s, e, cap = string.find(opts.text, "%${[?!]?([%l%d-/]*)", start)
+        if not s then break end
+        msg.verbose("Observing property " .. cap)
+        mp.observe_property(cap, nil, mark_stale)
+        start = e
+    end
+    mp.observe_property("osd-width", nil, mark_stale)
+    mp.observe_property("osd-height", nil, mark_stale)
+    mp.register_idle(refresh)
+    mark_stale()
+end
+
+
+function disable()
+    if not active then return end
+    active = false
+    mp.unobserve_property(mark_stale)
+    mp.unregister_idle(refresh)
+    ass.status_line = ""
+    draw_ass()
+end
+
+function toggle()
+    if active then
+        disable()
+    else
+        enable()
+    end
+end
+
+if opts.enabled then
+    enable()
+end
+
+mp.add_key_binding(nil, "enable-status-line", enable)
+mp.add_key_binding(nil, "disable-status-line", disable)
+mp.add_key_binding(nil, "toggle-status-line", toggle)