add mvi not as submodule

This commit is contained in:
Never Gude 2025-12-31 15:03:07 +01:00
parent 49ebf8a1e1
commit e6714de34b
16 changed files with 1547 additions and 0 deletions

87
.config/mvi/README.md Normal file
View file

@ -0,0 +1,87 @@
This repository aggregates configurations, scripts and tips for using [mpv](https://github.com/mpv-player/mpv) as an image viewer. The affectionate nickname `mvi` is given to mpv in such case.
This README assumes basic familiarity with mpv and its configuration. The information here should be platform-agnostic for the most part.
[![demo](https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F674986351_1280x720.jpg&src1=https%3A%2F%2Ff.vimeocdn.com%2Fimages_v6%2Fshare%2Fplay_icon_overlay.png)](https://vimeo.com/249231479)
# Why?
mpv is a competent video player and sufficiently lightweight to use comfortably to view images. It features advanced scaling algorithms and color management, which are key features in an image viewer.
It is also very extensible which allows us to compensate for some missing image viewer features easily.
It won't compete with `feh` or the likes when it comes to size or startup time, but it is still plenty fast.
# Configuration
There are several options for making an mvi configuration.
* Use the normal mpv config, and apply changes at runtime. You will need `detect-image.lua` and use [input sections](https://mpv.io/manual/master/#input-sections) and [profiles](https://mpv.io/manual/master/#profiles) to make this work.
* Or create an entirely separate `~/.config/mvi/` directory and use the alias `alias mvi='mpv --config-dir=$HOME/.config/mvi'`
The first option is more complex, the second results in essentially two separate programs.
The examples `mpv.conf` and `input.conf` in this repository are commented to highlight (un)desirable settings.
# Scripts
## image-positioning.lua
Adds several high-level commands to zoom and pan:
`drag-to-pan`: pan the image with the cursor, while keeping the same part of the image under the cursor
`pan-follows-cursor`: pan the image in the direction of the cursor
`cursor-centric-zoom`: (de)zoom the video while keeping the same part of the image under the cursor
`align-border`: align the border of the image with the border of the window
`pan-image`: pan the image in a direction, optionally ignoring the zoom or forcing the image to stay visible
`rotate-video`: rotate the image in 90 degrees increment
`reset-pan-if-visible`: reset the pan if the entire image is visible
There are no default bindings, see [`input.conf`](input.conf#L19-L67) for how to bind them.
## status-line.lua
Adds a status line that can show different properties in the corner of the window. By default it shows `filename [positon/total]` in the bottom left.
Can be activated with the commands `status-line-enable`, `status-line-disable`, `status-line-toggle` and configured through [`status_line.conf`](script-opts/status_line.conf).
## detect-image.lua
Allows you to run specific commands when images are being displayed. Does not do anything by default, needs to be configured through [`detect_image.conf`](script-opts/detect_image.conf).
For example, this makes it possible to setup bindings that are only in effect with images, like so:
```
command_on_first_image_loaded=enable-section image-viewer
command_on_non_image_loaded=disable-section image-viewer
```
Where the 'image-viewer' bindings are specified like so [`input.conf`](input.conf#L96-L99).
## minimap.lua
Adds a minimap that displays the position of the image relative to the view.
Can be activated with `minimap-enable`, `minimap-disable`, `minimap-toggle` and configured through [`minimap.conf`](script-opts/minimap.conf).
## ruler.lua
Adds a `ruler` command that lets you measure positions, distances and angles in the image.
Can be configured through [`ruler.conf`](script-opts/ruler.conf).
## freeze-window.lua
By default, mpv automatically resizes the window when the current file changes to fit its size. This script freezes the window so that this does not happen.
There is no configuration.
## Others
Some other mpv scripts work well with mvi, here are a few (feel free to send a PR for others):
[playlist-view](https://github.com/occivink/mpv-gallery-view): show all images in a grid view
[zones](https://github.com/wiiaboo/mpv-scripts/blob/master/zones.lua): send different commands depending on cursor position
[autoload](https://github.com/mpv-player/mpv/blob/master/TOOLS/lua/autoload.lua): automatically load all files in the same directory. Set `videos=no` and `audio=no` in `script-opts/autoload.conf` to only autoload images.
[delete-file](https://github.com/zenyd/mpv-scripts#delete-file): delete the current file
[mpv_crop_script](https://github.com/TheAMM/mpv_crop_script): featureful screenshot tool
[auto-profiles](https://github.com/wiiaboo/mpv-scripts/blob/master/auto-profiles.lua): apply profiles conditionally. Can be used to lower settings with huge images
[crop](https://github.com/occivink/mpv-scripts#croplua): simple cropping script
[playlist-manager](https://github.com/jonniek/mpv-playlistmanager): playlist management script
[blacklist-extensions](https://github.com/occivink/mpv-scripts#blacklist-extensionslua): remove files from the playlist based on their types
# Credits
Thanks to haasn for coming up first with the [image-viewer config](https://gist.github.com/haasn/7919afd765e308fa91cbe19a64631d0f), all the mpv devs and the /mpv/ funposters.

24
.config/mvi/UNLICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

105
.config/mvi/input.conf Normal file
View file

@ -0,0 +1,105 @@
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
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

49
.config/mvi/mpv.conf Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}]

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)