syncplay/resources/syncplayintf.lua

985 lines
31 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- syncplayintf.lua -- An interface for communication between mpv and Syncplay
-- Author: Etoh, utilising repl.lua code by James Ross-Gowan (see below)
-- Thanks: RiCON, James Ross-Gowan, Argon-, wm4, uau
-- Includes code copied/adapted from repl.lua -- A graphical REPL for mpv input commands
--
-- c 2016, James Ross-Gowan
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-- See https://github.com/rossy/mpv-repl for a copy of repl.lua
local CANVAS_WIDTH = 1920
local CANVAS_HEIGHT = 1080
local ROW_HEIGHT = 100
local chat_format = "{\\fs50}{\an1}"
local max_scrolling_rows = 100
local MOVEMENT_PER_SECOND = 200
local TICK_INTERVAL = 0.01
local CHAT_MODE_CHATROOM = "Chatroom"
local CHAT_MODE_SUBTITLE = "Subtitle"
local CHAT_MODE_SCROLLING = "Scrolling"
local last_chat_time = 0
local use_alpha_rows_for_chat = true
local MOOD_NEUTRAL = 0
local MOOD_BAD = 1
local MOOD_GOOD = 2
local WORDWRAPIFY_MAGICWORD = "{\\\\fscx0} {\\\\fscx100}"
local SCROLLING_ADDITIONAL_BOTTOM_MARGIN = 75
local default_oscvisibility_state = "never"
local ALPHA_WARNING_TEXT_COLOUR = "FF00FF" -- RBG
local HINT_TEXT_COLOUR = "AAAAAA" -- RBG
local NEUTRAL_ALERT_TEXT_COLOUR = "FFFFFF" -- RBG
local BAD_ALERT_TEXT_COLOUR = "0000FF" -- RBG
local GOOD_ALERT_TEXT_COLOUR = "00FF00" -- RBG
local NOTIFICATION_TEXT_COLOUR = "FFFF00" -- RBG
local FONT_SIZE_MULTIPLIER = 2
local chat_log = {}
local assdraw = require "mp.assdraw"
local opt = require 'mp.options'
local repl_active = false
local insert_mode = false
local line = ''
local cursor = 1
local key_hints_enabled = false
non_us_chars = {
'А','а',
'ą','ć','ę','ł','ń','ś','ź','ż','Ą','Ć','Ę','Ł','Ń','Ś','Ź','Ż',
'à','è','ì','ò','ù','À','È','Ì','Ò','Ù',
'á', 'é', 'í', 'ó', 'ú', 'ý', 'Á', 'É', 'Í', 'Ó', 'Ú', 'Ý',
'â', 'ê', 'î', 'ô', 'û', 'Â', 'Ê', 'Î', 'Ô', 'Û',
'ã', 'ñ', 'õ', 'Ã', 'Ñ', 'Õ',
'ä', 'ë', 'ï', 'ö', 'ü', 'ÿ', 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Ÿ',
'å', 'Å','æ','Æ','œ','Œ','ç','Ç','ð','Ð','ø','Ø','¿','¡','ß',
'¤','','×','÷','','±','','','','§','ˆ','˜','«','»','¦','','©','®','',
'ž','Ž',
'ª','Þ','þ','ƒ','µ','°','º','','','','','¬','¥','£','','¢','¹','²','³','½','¼','¾',
'·','Ĉ','ĉ','Ĝ','ĝ','Ĥ','ĥ','Ĵ','ĵ','Ŝ','ŝ','Ŭ','ŭ',
'Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж','ж','З','з',
'И','и','Й','й','К','к','Л','л','М','м','Н','н','О','о','П','п',
'Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ч','ч',
'Ш','ш','Щ','щ','Ъ','ъ','Ы','ы','Ь','ь','Э','э','Ю','ю','Я','я',
'',''
}
function format_scrolling(xpos, ypos, text)
local chat_message = "\n"..chat_format .. "{\\pos("..xpos..","..ypos..")\\q2}"..text.."\\N\\n"
return string.format(chat_message)
end
function format_chatroom(text)
local chat_message = chat_format .. text .."\\N\\n"
return string.format(chat_message)
end
function clear_chat()
chat_log = {}
end
local alert_osd = ""
local last_alert_osd_time = nil
local alert_osd_mood = MOOD_NEUTRAL
local notification_osd = ""
local last_notification_osd_time = nil
local notification_osd_mood = MOOD_NEUTRAL
function set_alert_osd(osd_message, mood)
alert_osd = osd_message
last_alert_osd_time = mp.get_time()
alert_osd_mood = mood
end
function set_notification_osd(osd_message, mood)
notification_osd = osd_message
last_notification_osd_time = mp.get_time()
notification_osd_mood = mood
end
function add_chat(chat_message, mood)
last_chat_time = mp.get_time()
local entry = #chat_log+1
for i = 1, #chat_log do
if chat_log[i].text == '' then
entry = i
break
end
end
local row = ((entry-1) % max_scrolling_rows)+1
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then
if entry > opts['chatMaxLines'] then
table.remove(chat_log, 1)
entry = entry - 1
end
end
chat_log[entry] = { xpos=CANVAS_WIDTH, timecreated=mp.get_time(), text=tostring(chat_message), row=row }
end
function chat_update()
local ass = assdraw.ass_new()
local chat_ass = ''
local rowsAdded = 0
local to_add = ''
local incrementRow = 0
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM and chat_log ~= {} then
local timedelta = mp.get_time() - last_chat_time
if timedelta >= opts['chatTimeout'] then
clear_chat()
end
end
rowsAdded,to_add = process_alert_osd()
if to_add ~= nil and to_add ~= "" then
chat_ass = to_add
end
incrementRow,to_add = process_notification_osd(rowsAdded)
rowsAdded = rowsAdded + incrementRow
if to_add ~= nil and to_add ~= "" then
chat_ass = chat_ass .. to_add
end
if #chat_log > 0 then
for i = 1, #chat_log do
local to_add = process_chat_item(i,rowsAdded)
if to_add ~= nil and to_add ~= "" then
chat_ass = chat_ass .. to_add
end
end
end
local xpos = opts['chatLeftMargin']
local ypos = opts['chatTopMargin']
chat_ass = "\n".."{\\pos("..xpos..","..ypos..")}".. chat_ass
if use_alpha_rows_for_chat == false and opts['chatDirectInput'] == true then
local alphawarning_ass = assdraw.ass_new()
alphawarning_ass = "{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-first-line'].."\n{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-second-line']
ass:append(alphawarning_ass)
elseif opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then
ass:append(chat_ass)
ass:append(input_ass())
else
ass:append(input_ass())
ass:append(chat_ass)
end
mp.set_osd_ass(CANVAS_WIDTH,CANVAS_HEIGHT, ass.text)
end
function process_alert_osd()
local rowsCreated = 0
local stringToAdd = ""
if alert_osd ~= "" and mp.get_time() - last_alert_osd_time < opts['alertTimeout'] and last_alert_osd_time ~= nil then
local messageColour
if alert_osd_mood == MOOD_NEUTRAL then
messageColour = "{\\1c&H"..NEUTRAL_ALERT_TEXT_COLOUR.."}"
elseif alert_osd_mood == MOOD_BAD then
messageColour = "{\\1c&H"..BAD_ALERT_TEXT_COLOUR.."}"
elseif alert_osd_mood == MOOD_GOOD then
messageColour = "{\\1c&H"..GOOD_ALERT_TEXT_COLOUR.."}"
end
local messageString = wordwrapify_string(alert_osd)
local startRow = 0
if messageString ~= '' and messageString ~= nil then
local toDisplay
rowsCreated = rowsCreated + 1
messageString = messageColour..messageString
if stringToAdd ~= "" then
stringToAdd = stringToAdd .. format_chatroom(messageString)
else
stringToAdd = format_chatroom(messageString)
end
end
end
return rowsCreated, stringToAdd
end
function process_notification_osd(startRow)
local rowsCreated = 0
local startRow = startRow
local stringToAdd = ""
if notification_osd ~= "" and mp.get_time() - last_notification_osd_time < opts['alertTimeout'] and last_notification_osd_time ~= nil then
local messageColour
messageColour = "{\\1c&H"..NOTIFICATION_TEXT_COLOUR.."}"
local messageString
messageString = wordwrapify_string(notification_osd)
messageString = messageColour..messageString
messageString = format_chatroom(messageString)
stringToAdd = messageString
rowsCreated = 1
end
return rowsCreated, stringToAdd
end
function process_chat_item(i, rowsAdded)
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then
return process_chat_item_chatroom(i, rowsAdded)
elseif opts['chatOutputMode'] == CHAT_MODE_SCROLLING then
return process_chat_item_scrolling(i)
end
end
function process_chat_item_scrolling(i)
local timecreated = chat_log[i].timecreated
local timedelta = mp.get_time() - timecreated
local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND)
local text = chat_log[i].text
if text ~= '' then
local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) * 1.5
if xpos > (-1*roughlen) then
local row = chat_log[i].row-1+opts['scrollingFirstRowOffset']
local ypos = opts['chatTopMargin']+(row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
return format_scrolling(xpos,ypos,text)
else
chat_log[i].text = ''
end
end
end
function process_chat_item_chatroom(i, startRow)
local text = chat_log[i].text
if text ~= '' then
local text = wordwrapify_string(text)
local rowNumber = i+startRow-1
return(format_chatroom(text))
end
end
function process_chat_item_subtitle(i)
local timecreated = chat_log[i].timecreated
local timedelta = mp.get_time() - timecreated
local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND)
local text = chat_log[i].text
if text ~= '' then
local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)
if xpos > (-1*roughlen) then
local row = chat_log[i].row
local ypos = row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)
return(format_scrolling(xpos,ypos,text))
else
chat_log[i].text = ''
end
end
end
chat_timer=mp.add_periodic_timer(TICK_INTERVAL, chat_update)
mp.register_script_message('chat', function(e)
add_chat(e)
end)
-- Chat OSD
mp.register_script_message('chat-osd-neutral', function(e)
add_chat(e,MOOD_NEUTRAL)
end)
mp.register_script_message('chat-osd-bad', function(e)
add_chat(e,MOOD_BAD)
end)
mp.register_script_message('chat-osd-good', function(e)
add_chat(e,MOOD_GOOD)
end)
-- Alert OSD
mp.register_script_message('alert-osd-neutral', function(e)
set_alert_osd(e,MOOD_NEUTRAL)
end)
mp.register_script_message('alert-osd-bad', function(e)
set_alert_osd(e,MOOD_BAD)
end)
mp.register_script_message('alert-osd-good', function(e)
set_alert_osd(e,MOOD_GOOD)
end)
-- Notification OSD
mp.register_script_message('notification-osd-neutral', function(e)
set_notification_osd(e,MOOD_NEUTRAL)
end)
mp.register_script_message('notification-osd-bad', function(e)
set_notification_osd(e,MOOD_BAD)
end)
mp.register_script_message('notification-osd-good', function(e)
set_notification_osd(e,MOOD_GOOD)
end)
--
mp.register_script_message('set_syncplayintf_options', function(e)
set_syncplayintf_options(e)
end)
-- Default options
local utils = require 'mp.utils'
local options = require 'mp.options'
opts = {
-- All drawing is scaled by this value, including the text borders and the
-- cursor. Change it if you have a high-DPI display.
scale = 1,
-- Set the font used for the REPL and the console. This probably doesn't
-- have to be a monospaced font.
['chatInputFontFamily'] = 'monospace',
-- Enable/Disable
['chatInputEnabled'] = true,
['chatOutputEnabled'] = true,
['OscVisibilityChangeCompatible'] = false,
-- Set the font size used for the REPL and the console. This will be
-- multiplied by "scale."
['chatInputRelativeFontSize'] = 14,
['chatInputFontWeight'] = 1,
['chatInputFontUnderline'] = false,
['chatInputFontColor'] = "#000000",
['chatInputPosition'] = "Top",
['MaxChatMessageLength'] = 500,
['chatOutputFontFamily'] = "sans serif",
['chatOutputFontSize'] = 50,
['chatOutputFontWeight'] = 1,
['chatOutputFontUnderline'] = false,
['chatOutputFontColor'] = "#FFFFFF",
['chatOutputMode'] = "Chatroom",
['scrollingFirstRowOffset'] = 2,
-- Can be "Chatroom", "Subtitle" or "Scrolling" style
['chatMaxLines'] = 7,
['chatTopMargin'] = 25,
['chatLeftMargin'] = 20,
['chatDirectInput'] = true,
--
['notificationTimeout'] = 3,
['alertTimeout'] = 5,
['chatTimeout'] = 7,
--
['inputPromptStartCharacter'] = ">",
['inputPromptEndCharacter'] = "<",
['backslashSubstituteCharacter'] = "|",
--Lang:
['mpv-key-tab-hint'] = "[TAB] to toggle access to alphabet row key shortcuts.",
['mpv-key-hint'] = "[ENTER] to send message. [ESC] to escape chat mode.",
['alphakey-mode-warning-first-line'] = "You can temporarily use old mpv bindings with a-z keys.",
['alphakey-mode-warning-second-line'] = "Press [TAB] to return to Syncplay chat mode.",
}
function detect_platform()
local o = {}
-- Kind of a dumb way of detecting the platform but whatever
if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
return 'windows'
elseif mp.get_property_native('options/input-app-events', o) ~= o then
return 'macos'
end
return 'linux'
end
-- Pick a better default font for Windows and macOS
local platform = detect_platform()
if platform == 'windows' then
opts.font = 'Consolas'
elseif platform == 'macos' then
opts.font = 'Menlo'
end
-- Apply user-set options
options.read_options(opts)
-- Escape a string for verbatim display on the OSD
function ass_escape(str)
-- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
-- it isn't followed by a recognised character, so add a zero-width
-- non-breaking space
str = str:gsub('\\', '\\\239\187\191')
str = str:gsub('{', '\\{')
str = str:gsub('}', '\\}')
-- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
-- consecutive newlines
str = str:gsub('\n', '\239\187\191\n')
return str
end
function update()
return
end
function input_ass()
if not repl_active then
return ""
end
last_chat_time = mp.get_time() -- to keep chat messages showing while entering input
local bold
if opts['chatInputFontWeight'] < 75 then
bold = 0
else
bold = 1
end
local underline = opts['chatInputFontUnderline'] and 1 or 0
local red = string.sub(opts['chatInputFontColor'],2,3)
local green = string.sub(opts['chatInputFontColor'],4,5)
local blue = string.sub(opts['chatInputFontColor'],6,7)
local fontColor = blue .. green .. red
local style = '{\\r' ..
'\\1a&H00&\\3a&H00&\\4a&H99&' ..
'\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' ..
'\\fn' .. opts['chatInputFontFamily'] .. '\\fs' .. (opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold ..
'\\bord2\\xshad0\\yshad1\\fsp0\\q1}'
local after_style = '{\\u' .. underline .. '}'
local cheight = opts['chatInputRelativeFontSize'] * FONT_SIZE_MULTIPLIER * 8
local cglyph = '_'
local before_cur = wordwrapify_string(ass_escape(line:sub(1, cursor - 1)))
local after_cur = wordwrapify_string(ass_escape(line:sub(cursor)))
local secondary_pos = "10,"..tostring(10+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
local alignment = 7
local position = "5,5"
local start_marker = opts['inputPromptStartCharacter']
local end_marker = ""
if opts['chatInputPosition'] == "Middle" then
alignment = 5
position = tostring(CANVAS_WIDTH/2)..","..tostring(CANVAS_HEIGHT/2)
secondary_pos = tostring(CANVAS_WIDTH/2)..","..tostring((CANVAS_HEIGHT/2)+20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
end_marker = "{\\u0}"..opts['inputPromptEndCharacter']
elseif opts['chatInputPosition'] == "Bottom" then
alignment = 1
position = tostring(5)..","..tostring(CANVAS_HEIGHT-5)
secondary_pos = "10,"..tostring(CANVAS_HEIGHT-(20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)))
end
local osd_help_message = opts['mpv-key-hint']
if opts['chatDirectInput'] then
osd_help_message = opts['mpv-key-tab-hint'] .. " " .. osd_help_message
end
local help_prompt = '\\N\\n{\\an'..alignment..'\\pos('..secondary_pos..')\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. ((opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)/1.25) .. '\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message
local firststyle = "{\\an"..alignment.."}{\\pos("..position..")}"
if opts['chatOutputEnabled'] and opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then
firststyle = get_output_style().."{'\\1c&H'"..fontColor.."}"
before_cur = before_cur .. firststyle
after_cur = after_cur .. firststyle
help_prompt = '\\N\\n'..firststyle..'{\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message .. '\\N\\n'
end
if key_hints_enabled == false then help_prompt = "" end
return firststyle..style..start_marker.." "..after_style..before_cur..style..cglyph..style..after_style..after_cur..end_marker..help_prompt
end
function get_output_style()
local bold
if opts['chatOutputFontWeight'] < 75 then
bold = 0
else
bold = 1
end
local underline = opts['chatOutputFontUnderline'] and 1 or 0
local red = string.sub(opts['chatOutputFontColor'],2,3)
local green = string.sub(opts['chatOutputFontColor'],4,5)
local blue = string.sub(opts['chatOutputFontColor'],6,7)
local fontColor = blue .. green .. red
local style = '{\\r' ..
'\\1a&H00&\\3a&H00&\\4a&H99&' ..
'\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' ..
'\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold ..
'\\u' .. underline .. '\\a5\\MarginV=500' .. '}'
--mp.osd_message("",0)
return style
end
function escape()
set_active(false)
clear()
end
-- Set the REPL visibility (`, Esc)
function set_active(active)
if use_alpha_rows_for_chat == false then active = false end
if active == repl_active then return end
if active then
repl_active = true
insert_mode = false
mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging')
else
repl_active = false
mp.disable_key_bindings('repl-input')
end
if default_oscvisibility_state ~= "never" and opts['OscVisibilityChangeCompatible'] == true then
if active then
mp.commandv("script-message", "osc-visibility","never", "no-osd")
else
mp.commandv("script-message", "osc-visibility",default_oscvisibility_state, "no-osd")
end
end
end
-- Show the repl if hidden and replace its contents with 'text'
-- (script-message-to repl type)
function show_and_type(text)
text = text or ''
line = text
cursor = line:len() + 1
insert_mode = false
if repl_active then
update()
else
set_active(true)
end
end
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
function next_utf8(str, pos)
if pos > str:len() then return pos end
repeat
pos = pos + 1
until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
function prev_utf8(str, pos)
if pos <= 1 then return pos end
repeat
pos = pos - 1
until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
function trim_string(line,maxCharacters)
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
local str = line
if str == nil or str == "" or str:len() <= maxCharacters then
return str, ""
end
local pos = 0
local oldPos = -1
local chars = 0
repeat
oldPos = pos
pos = next_utf8(str, pos)
chars = chars + 1
until pos == oldPos or chars > maxCharacters
return str:sub(1,pos-1), str:sub(pos)
end
function wordwrapify_string(line)
-- Used to ensure characters wrap on a per-character rather than per-word basis
-- to avoid issues with long filenames, etc.
local str = line
if str == nil or str == "" then
return ""
end
local newstr = ""
local currentChar = 0
local nextChar = 0
local chars = 0
local maxChars = str:len()
repeat
nextChar = next_utf8(str, currentChar)
if nextChar == currentChar then
return newstr
end
local charToTest = str:sub(currentChar,nextChar-1)
if charToTest ~= "\\" and charToTest ~= "{" and charToTest ~= "}" and charToTest ~= "%" then
newstr = newstr .. WORDWRAPIFY_MAGICWORD .. str:sub(currentChar,nextChar-1)
else
newstr = newstr .. str:sub(currentChar,nextChar-1)
end
currentChar = nextChar
until currentChar > maxChars
newstr = string.gsub(newstr,opts['backslashSubstituteCharacter'], '\\\239\187\191') -- Workaround for \ escape issues
return newstr
end
function trim_input()
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
local str = line
if str == nil or str == "" or str:len() <= opts['MaxChatMessageLength'] then
return
end
local pos = 0
local oldPos = -1
local chars = 0
repeat
oldPos = pos
pos = next_utf8(str, pos)
chars = chars + 1
until pos == oldPos or chars > opts['MaxChatMessageLength']
line = line:sub(1,pos-1)
if cursor > pos then
cursor = pos
end
return
end
-- Insert a character at the current cursor position (' '-'~', Shift+Enter)
function handle_char_input(c)
if c == nil then return end
if c == "\\" then c = opts['backslashSubstituteCharacter'] end
if key_hints_enabled and (string.len(line) > 0 or opts['chatDirectInput'] == false) then
key_hints_enabled = false
end
set_active(true)
if insert_mode then
line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
else
line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
end
cursor = cursor + c:len()
trim_input()
update()
end
-- Remove the character behind the cursor (Backspace)
function handle_backspace()
if cursor <= 1 then return end
local prev = prev_utf8(line, cursor)
line = line:sub(1, prev - 1) .. line:sub(cursor)
cursor = prev
update()
end
-- Remove the character in front of the cursor (Del)
function handle_del()
if cursor > line:len() then return end
line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
update()
end
-- Toggle insert mode (Ins)
function handle_ins()
insert_mode = not insert_mode
end
--local was_active_before_tab = false
function handle_tab()
use_alpha_rows_for_chat = not use_alpha_rows_for_chat
if use_alpha_rows_for_chat then
mp.enable_key_bindings('repl-alpha-input')
--set_active(was_active_before_tab)
else
mp.disable_key_bindings('repl-alpha-input')
--was_active_before_tab = repl_active
--set_active(false)
escape()
end
end
-- Move the cursor to the next character (Right)
function next_char(amount)
cursor = next_utf8(line, cursor)
update()
end
-- Move the cursor to the previous character (Left)
function prev_char(amount)
cursor = prev_utf8(line, cursor)
update()
end
-- Clear the current line (Ctrl+C)
function clear()
line = ''
cursor = 1
insert_mode = false
update()
end
-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D)
function maybe_exit()
if line == '' then
set_active(false)
end
end
-- Run the current command and clear the line (Enter)
function handle_enter()
if not repl_active then
set_active(true)
return
end
set_active(false)
if line == '' then
return
end
key_hints_enabled = false
line = string.gsub(line,"\\", "\\\\")
line = string.gsub(line,"\"", "\\\"")
mp.command('print-text "<chat>'..line..'</chat>"')
clear()
end
-- Move the cursor to the beginning of the line (HOME)
function go_home()
cursor = 1
update()
end
-- Move the cursor to the end of the line (END)
function go_end()
cursor = line:len() + 1
update()
end
-- Delete from the cursor to the end of the line (Ctrl+K)
function del_to_eol()
line = line:sub(1, cursor - 1)
update()
end
-- Delete from the cursor back to the start of the line (Ctrl+U)
function del_to_start()
line = line:sub(cursor)
cursor = 1
update()
end
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
function get_clipboard(clip)
if platform == 'linux' then
local res = utils.subprocess({ args = {
'xclip', '-selection', clip and 'clipboard' or 'primary', '-out'
} })
if not res.error then
return res.stdout
end
elseif platform == 'windows' then
local res = utils.subprocess({ args = {
'powershell', '-NoProfile', '-Command', [[& {
Trap {
Write-Error -ErrorRecord $_
Exit 1
}
$clip = ""
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
} else {
Add-Type -AssemblyName PresentationCore
$clip = [Windows.Clipboard]::GetText()
}
$clip = $clip -Replace "`r",""
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
}]]
} })
if not res.error then
return res.stdout
end
elseif platform == 'macos' then
local res = utils.subprocess({ args = { 'pbpaste' } })
if not res.error then
return res.stdout
end
end
return ''
end
-- Paste text from the window-system's clipboard. 'clip' determines whether the
-- clipboard or the primary selection buffer is used (on X11 only.)
function paste(clip)
local text = get_clipboard(clip)
local before_cur = line:sub(1, cursor - 1)
local after_cur = line:sub(cursor)
line = before_cur .. text .. after_cur
cursor = cursor + text:len()
trim_input()
update()
end
-- The REPL has pretty specific requirements for key bindings that aren't
-- really satisified by any of mpv's helper methods, since they must be in
-- their own input section, but they must also raise events on key-repeat.
-- Hence, this function manually creates an input section and puts a list of
-- bindings in it.
function add_repl_bindings(bindings)
local cfg = ''
for i, binding in ipairs(bindings) do
local key = binding[1]
local fn = binding[2]
local name = '__repl_binding_' .. i
mp.add_forced_key_binding(nil, name, fn, 'repeatable')
cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' ..
name .. '\n'
end
mp.commandv('define-section', 'repl-input', cfg, 'force')
end
function add_repl_alpharow_bindings(bindings)
local cfg = ''
for i, binding in ipairs(bindings) do
local key = binding[1]
local fn = binding[2]
local name = '__repl_alpha_binding_' .. i
mp.add_forced_key_binding(nil, name, fn, 'repeatable')
cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' ..
name .. '\n'
end
mp.commandv('define-section', 'repl-alpha-input', cfg, 'force')
mp.enable_key_bindings('repl-alpha-input')
end
-- Mapping from characters to mpv key names
local binding_name_map = {
[' '] = 'SPACE',
['#'] = 'SHARP',
}
-- List of input bindings. This is a weird mashup between common GUI text-input
-- bindings and readline bindings.
local bindings = {
{ 'esc', function() escape() end },
{ 'bs', handle_backspace },
{ 'shift+bs', handle_backspace },
{ 'del', handle_del },
{ 'shift+del', handle_del },
{ 'ins', handle_ins },
{ 'left', function() prev_char() end },
{ 'right', function() next_char() end },
{ 'up', function() clear() end },
{ 'home', go_home },
{ 'end', go_end },
{ 'ctrl+c', clear },
{ 'ctrl+d', maybe_exit },
{ 'ctrl+k', del_to_eol },
{ 'ctrl+l', clear_log_buffer },
{ 'ctrl+u', del_to_start },
{ 'ctrl+v', function() paste(true) end },
{ 'meta+v', function() paste(true) end },
}
local alpharowbindings = {}
-- Add bindings for all the printable US-ASCII characters from ' ' to '~'
-- inclusive. Note, this is a pretty hacky way to do text input. mpv's input
-- system was designed for single-key key bindings rather than text input, so
-- things like dead-keys and non-ASCII input won't work. This is probably okay
-- though, since all mpv's commands and properties can be represented in ASCII.
for b = (' '):byte(), ('~'):byte() do
local c = string.char(b)
local binding = binding_name_map[c] or c
bindings[#bindings + 1] = {binding, function() handle_char_input(c) end}
end
function add_alpharowbinding(firstchar,lastchar)
for b = (firstchar):byte(), (lastchar):byte() do
local c = string.char(b)
local alphabinding = binding_name_map[c] or c
alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(c) end}
end
end
function add_specialalphabindings(charinput)
local alphabindingarray = charinput
for i, alphabinding in ipairs(alphabindingarray) do
alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end }
bindings[#bindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end}
end
end
add_alpharowbinding('a','z')
add_alpharowbinding('A','Z')
add_alpharowbinding('/','/')
add_alpharowbinding(':',':')
add_alpharowbinding('(',')')
add_alpharowbinding('{','}')
add_alpharowbinding(':',';')
add_alpharowbinding('<','>')
add_alpharowbinding(',','.')
add_alpharowbinding('|','|')
add_alpharowbinding('\\','\\')
add_alpharowbinding('?','?')
add_alpharowbinding('[',']')
add_alpharowbinding('#','#')
add_alpharowbinding('~','~')
add_alpharowbinding('\'','\'')
add_alpharowbinding('@','@')
add_specialalphabindings(non_us_chars)
add_repl_bindings(bindings)
-- Add a script-message to show the REPL and fill it with the provided text
mp.register_script_message('type', function(text)
show_and_type(text)
end)
local syncplayintfSet = false
mp.command('print-text "<get_syncplayintf_options>"')
function readyMpvAfterSettingsKnown()
if syncplayintfSet == false then
local vertical_output_area = CANVAS_HEIGHT-(opts['chatTopMargin']+opts['chatBottomMargin']+((opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)*opts['scrollingFirstRowOffset'])+SCROLLING_ADDITIONAL_BOTTOM_MARGIN)
max_scrolling_rows = math.floor(vertical_output_area/(opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
local user_opts = { visibility = "auto", }
opt.read_options(user_opts, "osc")
default_oscvisibility_state = user_opts.visibility
if opts['chatInputEnabled'] == true then
key_hints_enabled = true
mp.add_forced_key_binding('enter', handle_enter)
mp.add_forced_key_binding('kp_enter', handle_enter)
if opts['chatDirectInput'] == true then
add_repl_alpharow_bindings(alpharowbindings)
mp.add_forced_key_binding('tab', handle_tab)
end
end
syncplayintfSet = true
end
end
function set_syncplayintf_options(input)
--mp.command('print-text "<chat>...'..input..'</chat>"')
for option, value in string.gmatch(input, "([^ ,=]+)=([^,]+)") do
local valueType = type(opts[option])
if valueType == "number" then
value = tonumber(value)
elseif valueType == "boolean" then
if value == "True" then
value = true
else
value = false
end
end
opts[option] = value
--mp.command('print-text "<chat>'..option.."="..tostring(value).." - "..valueType..'</chat>"')
end
chat_format = get_output_style()
readyMpvAfterSettingsKnown()
end