1-- syncplayintf.lua -- An interface for communication between mpv and Syncplay 2-- Author: Etoh, utilising repl.lua code by James Ross-Gowan (see below) 3-- Thanks: RiCON, James Ross-Gowan, Argon-, wm4, uau 4 5-- Includes code copied/adapted from repl.lua -- A graphical REPL for mpv input commands 6-- 7-- c 2016, James Ross-Gowan 8-- 9-- Permission to use, copy, modify, and/or distribute this software for any 10-- purpose with or without fee is hereby granted, provided that the above 11-- copyright notice and this permission notice appear in all copies. 12-- 13-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 14-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 15-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 16-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 18-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 19-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 20 21-- See https://github.com/rossy/mpv-repl for a copy of repl.lua 22 23local CANVAS_WIDTH = 1920 24local CANVAS_HEIGHT = 1080 25local ROW_HEIGHT = 100 26local chat_format = "{\\fs50}{\an1}" 27local max_scrolling_rows = 100 28local MOVEMENT_PER_SECOND = 200 29local TICK_INTERVAL = 0.01 30local CHAT_MODE_CHATROOM = "Chatroom" 31local CHAT_MODE_SUBTITLE = "Subtitle" 32local CHAT_MODE_SCROLLING = "Scrolling" 33local last_chat_time = 0 34local use_alpha_rows_for_chat = true 35local MOOD_NEUTRAL = 0 36local MOOD_BAD = 1 37local MOOD_GOOD = 2 38local WORDWRAPIFY_MAGICWORD = "{\\\\fscx0} {\\\\fscx100}" 39local SCROLLING_ADDITIONAL_BOTTOM_MARGIN = 75 40local default_oscvisibility_state = "never" 41 42local ALPHA_WARNING_TEXT_COLOUR = "FF00FF" -- RBG 43local HINT_TEXT_COLOUR = "AAAAAA" -- RBG 44local NEUTRAL_ALERT_TEXT_COLOUR = "FFFFFF" -- RBG 45local BAD_ALERT_TEXT_COLOUR = "0000FF" -- RBG 46local GOOD_ALERT_TEXT_COLOUR = "00FF00" -- RBG 47local NOTIFICATION_TEXT_COLOUR = "FFFF00" -- RBG 48 49local FONT_SIZE_MULTIPLIER = 2 50 51local chat_log = {} 52 53local assdraw = require "mp.assdraw" 54 55local opt = require 'mp.options' 56 57local repl_active = false 58local insert_mode = false 59local line = '' 60local cursor = 1 61local key_hints_enabled = false 62 63non_us_chars = { 64 'А','а', 65 'ą','ć','ę','ł','ń','ś','ź','ż','Ą','Ć','Ę','Ł','Ń','Ś','Ź','Ż', 66 'à','è','ì','ò','ù','À','È','Ì','Ò','Ù', 67 'á', 'é', 'í', 'ó', 'ú', 'ý', 'Á', 'É', 'Í', 'Ó', 'Ú', 'Ý', 68 'â', 'ê', 'î', 'ô', 'û', 'Â', 'Ê', 'Î', 'Ô', 'Û', 69 'ã', 'ñ', 'õ', 'Ã', 'Ñ', 'Õ', 70 'ä', 'ë', 'ï', 'ö', 'ü', 'ÿ', 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Ÿ', 71 'å', 'Å','æ','Æ','œ','Œ','ç','Ç','ð','Ð','ø','Ø','¿','¡','ß', 72 '¤','†','×','÷','‡','±','—','–','¶','§','ˆ','˜','«','»','¦','‰','©','®','™', 73 'ž','Ž', 74 'ª','Þ','þ','ƒ','µ','°','º','•','„','“','…','¬','¥','£','€','¢','¹','²','³','½','¼','¾', 75 '·','Ĉ','ĉ','Ĝ','ĝ','Ĥ','ĥ','Ĵ','ĵ','Ŝ','ŝ','Ŭ','ŭ', 76 'Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж','ж','З','з', 77 'И','и','Й','й','К','к','Л','л','М','м','Н','н','О','о','П','п', 78 'Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ч','ч', 79 'Ш','ш','Щ','щ','Ъ','ъ','Ы','ы','Ь','ь','Э','э','Ю','ю','Я','я', 80 '≥','≠' 81} 82 83function format_scrolling(xpos, ypos, text) 84 local chat_message = "\n"..chat_format .. "{\\pos("..xpos..","..ypos..")\\q2}"..text.."\\N\\n" 85 return string.format(chat_message) 86end 87 88function format_chatroom(text) 89 local chat_message = chat_format .. text .."\\N\\n" 90 return string.format(chat_message) 91end 92 93function clear_chat() 94 chat_log = {} 95end 96 97local alert_osd = "" 98local last_alert_osd_time = nil 99local alert_osd_mood = MOOD_NEUTRAL 100 101local notification_osd = "" 102local last_notification_osd_time = nil 103local notification_osd_mood = MOOD_NEUTRAL 104 105function set_alert_osd(osd_message, mood) 106 alert_osd = osd_message 107 last_alert_osd_time = mp.get_time() 108 alert_osd_mood = mood 109end 110 111function set_notification_osd(osd_message, mood) 112 notification_osd = osd_message 113 last_notification_osd_time = mp.get_time() 114 notification_osd_mood = mood 115end 116 117function add_chat(chat_message, mood) 118 last_chat_time = mp.get_time() 119 local entry = #chat_log+1 120 for i = 1, #chat_log do 121 if chat_log[i].text == '' then 122 entry = i 123 break 124 end 125 end 126 local row = ((entry-1) % max_scrolling_rows)+1 127 if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then 128 if entry > opts['chatMaxLines'] then 129 table.remove(chat_log, 1) 130 entry = entry - 1 131 end 132 end 133 chat_log[entry] = { xpos=CANVAS_WIDTH, timecreated=mp.get_time(), text=tostring(chat_message), row=row } 134end 135 136function chat_update() 137 local ass = assdraw.ass_new() 138 local chat_ass = '' 139 local rowsAdded = 0 140 local to_add = '' 141 local incrementRow = 0 142 if opts['chatOutputMode'] == CHAT_MODE_CHATROOM and chat_log ~= {} then 143 local timedelta = mp.get_time() - last_chat_time 144 if timedelta >= opts['chatTimeout'] then 145 clear_chat() 146 end 147 end 148 rowsAdded,to_add = process_alert_osd() 149 if to_add ~= nil and to_add ~= "" then 150 chat_ass = to_add 151 end 152 incrementRow,to_add = process_notification_osd(rowsAdded) 153 rowsAdded = rowsAdded + incrementRow 154 if to_add ~= nil and to_add ~= "" then 155 chat_ass = chat_ass .. to_add 156 end 157 158 if #chat_log > 0 then 159 for i = 1, #chat_log do 160 local to_add = process_chat_item(i,rowsAdded) 161 if to_add ~= nil and to_add ~= "" then 162 chat_ass = chat_ass .. to_add 163 end 164 end 165 end 166 167 local xpos = opts['chatLeftMargin'] 168 local ypos = opts['chatTopMargin'] 169 chat_ass = "\n".."{\\pos("..xpos..","..ypos..")}".. chat_ass 170 171 if use_alpha_rows_for_chat == false and opts['chatDirectInput'] == true then 172 local alphawarning_ass = assdraw.ass_new() 173 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'] 174 ass:append(alphawarning_ass) 175 elseif opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then 176 ass:append(chat_ass) 177 ass:append(input_ass()) 178 else 179 ass:append(input_ass()) 180 ass:append(chat_ass) 181 end 182 mp.set_osd_ass(CANVAS_WIDTH,CANVAS_HEIGHT, ass.text) 183end 184 185function process_alert_osd() 186 local rowsCreated = 0 187 local stringToAdd = "" 188 if alert_osd ~= "" and mp.get_time() - last_alert_osd_time < opts['alertTimeout'] and last_alert_osd_time ~= nil then 189 local messageColour 190 if alert_osd_mood == MOOD_NEUTRAL then 191 messageColour = "{\\1c&H"..NEUTRAL_ALERT_TEXT_COLOUR.."}" 192 elseif alert_osd_mood == MOOD_BAD then 193 messageColour = "{\\1c&H"..BAD_ALERT_TEXT_COLOUR.."}" 194 elseif alert_osd_mood == MOOD_GOOD then 195 messageColour = "{\\1c&H"..GOOD_ALERT_TEXT_COLOUR.."}" 196 end 197 local messageString = wordwrapify_string(alert_osd) 198 local startRow = 0 199 if messageString ~= '' and messageString ~= nil then 200 local toDisplay 201 rowsCreated = rowsCreated + 1 202 messageString = messageColour..messageString 203 if stringToAdd ~= "" then 204 stringToAdd = stringToAdd .. format_chatroom(messageString) 205 else 206 stringToAdd = format_chatroom(messageString) 207 end 208 end 209 end 210 return rowsCreated, stringToAdd 211end 212 213function process_notification_osd(startRow) 214 local rowsCreated = 0 215 local startRow = startRow 216 local stringToAdd = "" 217 if notification_osd ~= "" and mp.get_time() - last_notification_osd_time < opts['alertTimeout'] and last_notification_osd_time ~= nil then 218 local messageColour 219 messageColour = "{\\1c&H"..NOTIFICATION_TEXT_COLOUR.."}" 220 local messageString 221 messageString = wordwrapify_string(notification_osd) 222 messageString = messageColour..messageString 223 messageString = format_chatroom(messageString) 224 stringToAdd = messageString 225 rowsCreated = 1 226 end 227 return rowsCreated, stringToAdd 228end 229 230 231function process_chat_item(i, rowsAdded) 232 if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then 233 return process_chat_item_chatroom(i, rowsAdded) 234 elseif opts['chatOutputMode'] == CHAT_MODE_SCROLLING then 235 return process_chat_item_scrolling(i) 236 end 237end 238 239function process_chat_item_scrolling(i) 240 local timecreated = chat_log[i].timecreated 241 local timedelta = mp.get_time() - timecreated 242 local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND) 243 local text = chat_log[i].text 244 if text ~= '' then 245 local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) * 1.5 246 if xpos > (-1*roughlen) then 247 local row = chat_log[i].row-1+opts['scrollingFirstRowOffset'] 248 local ypos = opts['chatTopMargin']+(row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) 249 return format_scrolling(xpos,ypos,text) 250 else 251 chat_log[i].text = '' 252 end 253 end 254end 255 256function process_chat_item_chatroom(i, startRow) 257 local text = chat_log[i].text 258 if text ~= '' then 259 local text = wordwrapify_string(text) 260 local rowNumber = i+startRow-1 261 return(format_chatroom(text)) 262 end 263end 264 265function process_chat_item_subtitle(i) 266 local timecreated = chat_log[i].timecreated 267 local timedelta = mp.get_time() - timecreated 268 local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND) 269 local text = chat_log[i].text 270 if text ~= '' then 271 local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) 272 if xpos > (-1*roughlen) then 273 local row = chat_log[i].row 274 local ypos = row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) 275 return(format_scrolling(xpos,ypos,text)) 276 else 277 chat_log[i].text = '' 278 end 279 end 280end 281 282chat_timer=mp.add_periodic_timer(TICK_INTERVAL, chat_update) 283 284mp.register_script_message('chat', function(e) 285 add_chat(e) 286end) 287 288-- Chat OSD 289 290mp.register_script_message('chat-osd-neutral', function(e) 291 add_chat(e,MOOD_NEUTRAL) 292end) 293 294mp.register_script_message('chat-osd-bad', function(e) 295 add_chat(e,MOOD_BAD) 296end) 297 298mp.register_script_message('chat-osd-good', function(e) 299 add_chat(e,MOOD_GOOD) 300end) 301 302-- Alert OSD 303 304mp.register_script_message('alert-osd-neutral', function(e) 305 set_alert_osd(e,MOOD_NEUTRAL) 306end) 307 308mp.register_script_message('alert-osd-bad', function(e) 309 set_alert_osd(e,MOOD_BAD) 310end) 311 312mp.register_script_message('alert-osd-good', function(e) 313 set_alert_osd(e,MOOD_GOOD) 314end) 315 316-- Notification OSD 317 318mp.register_script_message('notification-osd-neutral', function(e) 319 set_notification_osd(e,MOOD_NEUTRAL) 320end) 321 322mp.register_script_message('notification-osd-bad', function(e) 323 set_notification_osd(e,MOOD_BAD) 324end) 325 326mp.register_script_message('notification-osd-good', function(e) 327 set_notification_osd(e,MOOD_GOOD) 328end) 329 330-- 331 332mp.register_script_message('set_syncplayintf_options', function(e) 333 set_syncplayintf_options(e) 334end) 335 336-- Default options 337local utils = require 'mp.utils' 338local options = require 'mp.options' 339opts = { 340 341 -- All drawing is scaled by this value, including the text borders and the 342 -- cursor. Change it if you have a high-DPI display. 343 scale = 1, 344 -- Set the font used for the REPL and the console. This probably doesn't 345 -- have to be a monospaced font. 346 ['chatInputFontFamily'] = 'monospace', 347 -- Enable/Disable 348 ['chatInputEnabled'] = true, 349 ['chatOutputEnabled'] = true, 350 ['OscVisibilityChangeCompatible'] = false, 351 -- Set the font size used for the REPL and the console. This will be 352 -- multiplied by "scale." 353 ['chatInputRelativeFontSize'] = 14, 354 ['chatInputFontWeight'] = 1, 355 ['chatInputFontUnderline'] = false, 356 ['chatInputFontColor'] = "#000000", 357 ['chatInputPosition'] = "Top", 358 ['MaxChatMessageLength'] = 500, 359 ['chatOutputFontFamily'] = "sans serif", 360 ['chatOutputFontSize'] = 50, 361 ['chatOutputFontWeight'] = 1, 362 ['chatOutputFontUnderline'] = false, 363 ['chatOutputFontColor'] = "#FFFFFF", 364 ['chatOutputMode'] = "Chatroom", 365 ['scrollingFirstRowOffset'] = 2, 366 -- Can be "Chatroom", "Subtitle" or "Scrolling" style 367 ['chatMaxLines'] = 7, 368 ['chatTopMargin'] = 25, 369 ['chatLeftMargin'] = 20, 370 ['chatDirectInput'] = true, 371 -- 372 ['notificationTimeout'] = 3, 373 ['alertTimeout'] = 5, 374 ['chatTimeout'] = 7, 375 -- 376 ['inputPromptStartCharacter'] = ">", 377 ['inputPromptEndCharacter'] = "<", 378 ['backslashSubstituteCharacter'] = "|", 379 --Lang: 380 ['mpv-key-tab-hint'] = "[TAB] to toggle access to alphabet row key shortcuts.", 381 ['mpv-key-hint'] = "[ENTER] to send message. [ESC] to escape chat mode.", 382 ['alphakey-mode-warning-first-line'] = "You can temporarily use old mpv bindings with a-z keys.", 383 ['alphakey-mode-warning-second-line'] = "Press [TAB] to return to Syncplay chat mode.", 384} 385 386function detect_platform() 387 local o = {} 388 -- Kind of a dumb way of detecting the platform but whatever 389 if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 390 return 'windows' 391 elseif mp.get_property_native('options/input-app-events', o) ~= o then 392 return 'macos' 393 end 394 return 'linux' 395end 396 397-- Pick a better default font for Windows and macOS 398local platform = detect_platform() 399if platform == 'windows' then 400 opts.font = 'Consolas' 401elseif platform == 'macos' then 402 opts.font = 'Menlo' 403end 404 405-- Apply user-set options 406options.read_options(opts) 407 408-- Escape a string for verbatim display on the OSD 409function ass_escape(str) 410 -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if 411 -- it isn't followed by a recognised character, so add a zero-width 412 -- non-breaking space 413 str = str:gsub('\\', '\\\239\187\191') 414 str = str:gsub('{', '\\{') 415 str = str:gsub('}', '\\}') 416 -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of 417 -- consecutive newlines 418 str = str:gsub('\n', '\239\187\191\n') 419 return str 420end 421 422function update() 423 return 424end 425 426function input_ass() 427 if not repl_active then 428 return "" 429 end 430 last_chat_time = mp.get_time() -- to keep chat messages showing while entering input 431 local bold 432 if opts['chatInputFontWeight'] < 75 then 433 bold = 0 434 else 435 bold = 1 436 end 437 local underline = opts['chatInputFontUnderline'] and 1 or 0 438 local red = string.sub(opts['chatInputFontColor'],2,3) 439 local green = string.sub(opts['chatInputFontColor'],4,5) 440 local blue = string.sub(opts['chatInputFontColor'],6,7) 441 local fontColor = blue .. green .. red 442 local style = '{\\r' .. 443 '\\1a&H00&\\3a&H00&\\4a&H99&' .. 444 '\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' .. 445 '\\fn' .. opts['chatInputFontFamily'] .. '\\fs' .. (opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold .. 446 '\\bord2\\xshad0\\yshad1\\fsp0\\q1}' 447 448 local after_style = '{\\u' .. underline .. '}' 449 local cheight = opts['chatInputRelativeFontSize'] * FONT_SIZE_MULTIPLIER * 8 450 local cglyph = '_' 451 local before_cur = wordwrapify_string(ass_escape(line:sub(1, cursor - 1))) 452 local after_cur = wordwrapify_string(ass_escape(line:sub(cursor))) 453 local secondary_pos = "10,"..tostring(10+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) 454 455 local alignment = 7 456 local position = "5,5" 457 local start_marker = opts['inputPromptStartCharacter'] 458 local end_marker = "" 459 if opts['chatInputPosition'] == "Middle" then 460 alignment = 5 461 position = tostring(CANVAS_WIDTH/2)..","..tostring(CANVAS_HEIGHT/2) 462 secondary_pos = tostring(CANVAS_WIDTH/2)..","..tostring((CANVAS_HEIGHT/2)+20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) 463 end_marker = "{\\u0}"..opts['inputPromptEndCharacter'] 464 elseif opts['chatInputPosition'] == "Bottom" then 465 alignment = 1 466 position = tostring(5)..","..tostring(CANVAS_HEIGHT-5) 467 secondary_pos = "10,"..tostring(CANVAS_HEIGHT-(20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))) 468 end 469 470 local osd_help_message = opts['mpv-key-hint'] 471 if opts['chatDirectInput'] then 472 osd_help_message = opts['mpv-key-tab-hint'] .. " " .. osd_help_message 473 end 474 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 475 476 local firststyle = "{\\an"..alignment.."}{\\pos("..position..")}" 477 if opts['chatOutputEnabled'] and opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then 478 firststyle = get_output_style().."{'\\1c&H'"..fontColor.."}" 479 before_cur = before_cur .. firststyle 480 after_cur = after_cur .. firststyle 481 help_prompt = '\\N\\n'..firststyle..'{\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message .. '\\N\\n' 482 end 483 if key_hints_enabled == false then help_prompt = "" end 484 485 return firststyle..style..start_marker.." "..after_style..before_cur..style..cglyph..style..after_style..after_cur..end_marker..help_prompt 486 487end 488 489function get_output_style() 490 local bold 491 if opts['chatOutputFontWeight'] < 75 then 492 bold = 0 493 else 494 bold = 1 495 end 496 local underline = opts['chatOutputFontUnderline'] and 1 or 0 497 local red = string.sub(opts['chatOutputFontColor'],2,3) 498 local green = string.sub(opts['chatOutputFontColor'],4,5) 499 local blue = string.sub(opts['chatOutputFontColor'],6,7) 500 local fontColor = blue .. green .. red 501 local style = '{\\r' .. 502 '\\1a&H00&\\3a&H00&\\4a&H99&' .. 503 '\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' .. 504 '\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold .. 505 '\\u' .. underline .. '\\a5\\MarginV=500' .. '}' 506 507 --mp.osd_message("",0) 508 return style 509 510end 511 512function escape() 513 set_active(false) 514 clear() 515end 516 517-- Set the REPL visibility (`, Esc) 518function set_active(active) 519 if use_alpha_rows_for_chat == false then active = false end 520 if active == repl_active then return end 521 if active then 522 repl_active = true 523 insert_mode = false 524 mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging') 525 else 526 repl_active = false 527 mp.disable_key_bindings('repl-input') 528 end 529 if default_oscvisibility_state ~= "never" and opts['OscVisibilityChangeCompatible'] == true then 530 if active then 531 mp.commandv("script-message", "osc-visibility","never", "no-osd") 532 else 533 mp.commandv("script-message", "osc-visibility",default_oscvisibility_state, "no-osd") 534 end 535 end 536end 537 538-- Show the repl if hidden and replace its contents with 'text' 539-- (script-message-to repl type) 540function show_and_type(text) 541 text = text or '' 542 543 line = text 544 cursor = line:len() + 1 545 insert_mode = false 546 if repl_active then 547 update() 548 else 549 set_active(true) 550 end 551end 552 553-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' 554-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. 555function next_utf8(str, pos) 556 if pos > str:len() then return pos end 557 repeat 558 pos = pos + 1 559 until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf 560 return pos 561end 562 563-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' 564-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. 565 566 567-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos' 568function prev_utf8(str, pos) 569 if pos <= 1 then return pos end 570 repeat 571 pos = pos - 1 572 until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf 573 return pos 574end 575 576function trim_string(line,maxCharacters) 577-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' 578-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. 579 580 local str = line 581 if str == nil or str == "" or str:len() <= maxCharacters then 582 return str, "" 583 end 584 local pos = 0 585 local oldPos = -1 586 local chars = 0 587 588 repeat 589 oldPos = pos 590 pos = next_utf8(str, pos) 591 chars = chars + 1 592 until pos == oldPos or chars > maxCharacters 593 return str:sub(1,pos-1), str:sub(pos) 594end 595 596function wordwrapify_string(line) 597-- Used to ensure characters wrap on a per-character rather than per-word basis 598-- to avoid issues with long filenames, etc. 599 600 local str = line 601 if str == nil or str == "" then 602 return "" 603 end 604 local newstr = "" 605 local currentChar = 0 606 local nextChar = 0 607 local chars = 0 608 local maxChars = str:len() 609 610 repeat 611 nextChar = next_utf8(str, currentChar) 612 if nextChar == currentChar then 613 return newstr 614 end 615 local charToTest = str:sub(currentChar,nextChar-1) 616 if charToTest ~= "\\" and charToTest ~= "{" and charToTest ~= "}" and charToTest ~= "%" then 617 newstr = newstr .. WORDWRAPIFY_MAGICWORD .. str:sub(currentChar,nextChar-1) 618 else 619 newstr = newstr .. str:sub(currentChar,nextChar-1) 620 end 621 currentChar = nextChar 622 until currentChar > maxChars 623 newstr = string.gsub(newstr,opts['backslashSubstituteCharacter'], '\\\239\187\191') -- Workaround for \ escape issues 624 return newstr 625end 626 627 628function trim_input() 629-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' 630-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. 631 632 local str = line 633 if str == nil or str == "" or str:len() <= opts['MaxChatMessageLength'] then 634 return 635 end 636 local pos = 0 637 local oldPos = -1 638 local chars = 0 639 640 repeat 641 oldPos = pos 642 pos = next_utf8(str, pos) 643 chars = chars + 1 644 until pos == oldPos or chars > opts['MaxChatMessageLength'] 645 line = line:sub(1,pos-1) 646 if cursor > pos then 647 cursor = pos 648 end 649 return 650end 651 652-- Insert a character at the current cursor position (' '-'~', Shift+Enter) 653function handle_char_input(c) 654 if c == nil then return end 655 if c == "\\" then c = opts['backslashSubstituteCharacter'] end 656 if key_hints_enabled and (string.len(line) > 0 or opts['chatDirectInput'] == false) then 657 key_hints_enabled = false 658 end 659 set_active(true) 660 if insert_mode then 661 line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor)) 662 else 663 line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) 664 end 665 cursor = cursor + c:len() 666 trim_input() 667 update() 668end 669 670-- Remove the character behind the cursor (Backspace) 671function handle_backspace() 672 if cursor <= 1 then return end 673 local prev = prev_utf8(line, cursor) 674 line = line:sub(1, prev - 1) .. line:sub(cursor) 675 cursor = prev 676 update() 677end 678 679-- Remove the character in front of the cursor (Del) 680function handle_del() 681 if cursor > line:len() then return end 682 line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) 683 update() 684end 685 686-- Toggle insert mode (Ins) 687function handle_ins() 688 insert_mode = not insert_mode 689end 690 691--local was_active_before_tab = false 692 693function handle_tab() 694 use_alpha_rows_for_chat = not use_alpha_rows_for_chat 695 if use_alpha_rows_for_chat then 696 mp.enable_key_bindings('repl-alpha-input') 697 --set_active(was_active_before_tab) 698 else 699 mp.disable_key_bindings('repl-alpha-input') 700 --was_active_before_tab = repl_active 701 --set_active(false) 702 escape() 703 end 704end 705 706-- Move the cursor to the next character (Right) 707function next_char(amount) 708 cursor = next_utf8(line, cursor) 709 update() 710end 711 712-- Move the cursor to the previous character (Left) 713function prev_char(amount) 714 cursor = prev_utf8(line, cursor) 715 update() 716end 717 718-- Clear the current line (Ctrl+C) 719function clear() 720 line = '' 721 cursor = 1 722 insert_mode = false 723 update() 724end 725 726-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D) 727function maybe_exit() 728 if line == '' then 729 set_active(false) 730 end 731end 732 733-- Run the current command and clear the line (Enter) 734function handle_enter() 735 if not repl_active then 736 set_active(true) 737 return 738 end 739 set_active(false) 740 741 if line == '' then 742 return 743 end 744 key_hints_enabled = false 745 line = string.gsub(line,"\\", "\\\\") 746 line = string.gsub(line,"\"", "\\\"") 747 mp.command('print-text "<chat>'..line..'</chat>"') 748 clear() 749end 750 751-- Move the cursor to the beginning of the line (HOME) 752function go_home() 753 cursor = 1 754 update() 755end 756 757-- Move the cursor to the end of the line (END) 758function go_end() 759 cursor = line:len() + 1 760 update() 761end 762 763-- Delete from the cursor to the end of the line (Ctrl+K) 764function del_to_eol() 765 line = line:sub(1, cursor - 1) 766 update() 767end 768 769-- Delete from the cursor back to the start of the line (Ctrl+U) 770function del_to_start() 771 line = line:sub(cursor) 772 cursor = 1 773 update() 774end 775 776-- Returns a string of UTF-8 text from the clipboard (or the primary selection) 777function get_clipboard(clip) 778 if platform == 'linux' then 779 local res = utils.subprocess({ args = { 780 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' 781 } }) 782 if not res.error then 783 return res.stdout 784 end 785 elseif platform == 'windows' then 786 local res = utils.subprocess({ args = { 787 'powershell', '-NoProfile', '-Command', [[& { 788 Trap { 789 Write-Error -ErrorRecord $_ 790 Exit 1 791 } 792 793 $clip = "" 794 if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) { 795 $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText 796 } else { 797 Add-Type -AssemblyName PresentationCore 798 $clip = [Windows.Clipboard]::GetText() 799 } 800 801 $clip = $clip -Replace "`r","" 802 $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip) 803 [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length) 804 }]] 805 } }) 806 if not res.error then 807 return res.stdout 808 end 809 elseif platform == 'macos' then 810 local res = utils.subprocess({ args = { 'pbpaste' } }) 811 if not res.error then 812 return res.stdout 813 end 814 end 815 return '' 816end 817 818-- Paste text from the window-system's clipboard. 'clip' determines whether the 819-- clipboard or the primary selection buffer is used (on X11 only.) 820function paste(clip) 821 local text = get_clipboard(clip) 822 local before_cur = line:sub(1, cursor - 1) 823 local after_cur = line:sub(cursor) 824 line = before_cur .. text .. after_cur 825 cursor = cursor + text:len() 826 trim_input() 827 update() 828end 829 830-- The REPL has pretty specific requirements for key bindings that aren't 831-- really satisified by any of mpv's helper methods, since they must be in 832-- their own input section, but they must also raise events on key-repeat. 833-- Hence, this function manually creates an input section and puts a list of 834-- bindings in it. 835function add_repl_bindings(bindings) 836 local cfg = '' 837 for i, binding in ipairs(bindings) do 838 local key = binding[1] 839 local fn = binding[2] 840 local name = '__repl_binding_' .. i 841 mp.add_forced_key_binding(nil, name, fn, 'repeatable') 842 cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' .. 843 name .. '\n' 844 end 845 mp.commandv('define-section', 'repl-input', cfg, 'force') 846end 847 848function add_repl_alpharow_bindings(bindings) 849 local cfg = '' 850 for i, binding in ipairs(bindings) do 851 local key = binding[1] 852 local fn = binding[2] 853 local name = '__repl_alpha_binding_' .. i 854 mp.add_forced_key_binding(nil, name, fn, 'repeatable') 855 cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' .. 856 name .. '\n' 857 end 858 mp.commandv('define-section', 'repl-alpha-input', cfg, 'force') 859 mp.enable_key_bindings('repl-alpha-input') 860end 861 862-- Mapping from characters to mpv key names 863local binding_name_map = { 864 [' '] = 'SPACE', 865 ['#'] = 'SHARP', 866} 867 868-- List of input bindings. This is a weird mashup between common GUI text-input 869-- bindings and readline bindings. 870local bindings = { 871 { 'esc', function() escape() end }, 872 { 'bs', handle_backspace }, 873 { 'shift+bs', handle_backspace }, 874 { 'del', handle_del }, 875 { 'shift+del', handle_del }, 876 { 'ins', handle_ins }, 877 { 'left', function() prev_char() end }, 878 { 'right', function() next_char() end }, 879 { 'up', function() clear() end }, 880 { 'home', go_home }, 881 { 'end', go_end }, 882 { 'ctrl+c', clear }, 883 { 'ctrl+d', maybe_exit }, 884 { 'ctrl+k', del_to_eol }, 885 { 'ctrl+l', clear_log_buffer }, 886 { 'ctrl+u', del_to_start }, 887 { 'ctrl+v', function() paste(true) end }, 888 { 'meta+v', function() paste(true) end }, 889} 890local alpharowbindings = {} 891-- Add bindings for all the printable US-ASCII characters from ' ' to '~' 892-- inclusive. Note, this is a pretty hacky way to do text input. mpv's input 893-- system was designed for single-key key bindings rather than text input, so 894-- things like dead-keys and non-ASCII input won't work. This is probably okay 895-- though, since all mpv's commands and properties can be represented in ASCII. 896for b = (' '):byte(), ('~'):byte() do 897 local c = string.char(b) 898 local binding = binding_name_map[c] or c 899 bindings[#bindings + 1] = {binding, function() handle_char_input(c) end} 900end 901 902function add_alpharowbinding(firstchar,lastchar) 903 for b = (firstchar):byte(), (lastchar):byte() do 904 local c = string.char(b) 905 local alphabinding = binding_name_map[c] or c 906 alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(c) end} 907 end 908end 909 910function add_specialalphabindings(charinput) 911 local alphabindingarray = charinput 912 for i, alphabinding in ipairs(alphabindingarray) do 913 alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end } 914 bindings[#bindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end} 915 end 916end 917 918add_alpharowbinding('a','z') 919add_alpharowbinding('A','Z') 920add_alpharowbinding('/','/') 921add_alpharowbinding(':',':') 922add_alpharowbinding('(',')') 923add_alpharowbinding('{','}') 924add_alpharowbinding(':',';') 925add_alpharowbinding('<','>') 926add_alpharowbinding(',','.') 927add_alpharowbinding('|','|') 928add_alpharowbinding('\\','\\') 929add_alpharowbinding('?','?') 930add_alpharowbinding('[',']') 931add_alpharowbinding('#','#') 932add_alpharowbinding('~','~') 933add_alpharowbinding('\'','\'') 934add_alpharowbinding('@','@') 935 936add_specialalphabindings(non_us_chars) 937add_repl_bindings(bindings) 938 939-- Add a script-message to show the REPL and fill it with the provided text 940mp.register_script_message('type', function(text) 941 show_and_type(text) 942end) 943 944local syncplayintfSet = false 945mp.command('print-text "<get_syncplayintf_options>"') 946 947function readyMpvAfterSettingsKnown() 948 if syncplayintfSet == false then 949 local vertical_output_area = CANVAS_HEIGHT-(opts['chatTopMargin']+opts['chatBottomMargin']+((opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)*opts['scrollingFirstRowOffset'])+SCROLLING_ADDITIONAL_BOTTOM_MARGIN) 950 max_scrolling_rows = math.floor(vertical_output_area/(opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) 951 local user_opts = { visibility = "auto", } 952 opt.read_options(user_opts, "osc") 953 default_oscvisibility_state = user_opts.visibility 954 if opts['chatInputEnabled'] == true then 955 key_hints_enabled = true 956 mp.add_forced_key_binding('enter', handle_enter) 957 mp.add_forced_key_binding('kp_enter', handle_enter) 958 if opts['chatDirectInput'] == true then 959 add_repl_alpharow_bindings(alpharowbindings) 960 mp.add_forced_key_binding('tab', handle_tab) 961 end 962 end 963 syncplayintfSet = true 964 end 965end 966 967function set_syncplayintf_options(input) 968 --mp.command('print-text "<chat>...'..input..'</chat>"') 969 for option, value in string.gmatch(input, "([^ ,=]+)=([^,]+)") do 970 local valueType = type(opts[option]) 971 if valueType == "number" then 972 value = tonumber(value) 973 elseif valueType == "boolean" then 974 if value == "True" then 975 value = true 976 else 977 value = false 978 end 979 end 980 opts[option] = value 981 --mp.command('print-text "<chat>'..option.."="..tostring(value).." - "..valueType..'</chat>"') 982 end 983 chat_format = get_output_style() 984 readyMpvAfterSettingsKnown() 985end