The tostring() comparison fix wasn't sufficient - jump-to-end was still firing on nearly every incoming line while jump-to-end-on-focus was on. AppendToNotepad's own SetSel/ReplaceSel appears to momentarily perturb GetFocus() around each append, which poll_output_focus was reading as a genuine "just switched to the window" edge. Now skips the jump if a SmartAppendToNotepad call happened within the last 0.5s.
641 lines
16 KiB
XML
641 lines
16 KiB
XML
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
<!DOCTYPE muclient>
|
|
<!-- Saved on Tuesday, April 06, 2010, 8:39 AM -->
|
|
<!-- MuClient version 4.46 -->
|
|
|
|
<!-- Plugin "output_functions" generated by Plugin Wizard -->
|
|
|
|
<muclient>
|
|
<plugin
|
|
name="output_functions"
|
|
author="Weyoun"
|
|
id="54846c23d8b15594e7eb4b8a"
|
|
save_state="y"
|
|
language="Lua"
|
|
purpose="provides functions to help tts users."
|
|
date_written="2010-04-06 08:37:40"
|
|
requires="4.82"
|
|
version="1.1"
|
|
>
|
|
|
|
</plugin>
|
|
<!-- Aliases -->
|
|
|
|
<aliases>
|
|
<alias
|
|
match="Line_Get *"
|
|
enabled="y"
|
|
script="LineGet"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="clearoutputbuffer"
|
|
enabled="y"
|
|
script="clearlines"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="clearoutputwindow"
|
|
enabled="y"
|
|
script="clearwindow"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="prevline"
|
|
enabled="y"
|
|
script="prev_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="toggleoutput"
|
|
enabled="y"
|
|
script="output_toggle"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="toggleinterrupt"
|
|
enabled="y"
|
|
script="interrupt_toggle"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="curline"
|
|
enabled="y"
|
|
script="cur_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="select *"
|
|
enabled="y"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
<send>
|
|
if "%1" == "space" then
|
|
selectscr(" ")
|
|
else
|
|
selectscr("\\r\\n")
|
|
end
|
|
</send>
|
|
</alias>
|
|
|
|
<alias
|
|
match="nextline"
|
|
enabled="y"
|
|
script="next_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="whichline"
|
|
enabled="y"
|
|
script="which_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="topline"
|
|
enabled="y"
|
|
script="top_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="snap_shot"
|
|
enabled="y"
|
|
script="snapshot"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="endline"
|
|
enabled="y"
|
|
script="end_line"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
|
|
<alias
|
|
match="toggle_jump_to_end"
|
|
enabled="y"
|
|
script="toggle_jump_to_end"
|
|
omit_from_output="y"
|
|
omit_from_log="y"
|
|
omit_from_command_history="y"
|
|
send_to="12"
|
|
sequence="100">
|
|
</alias>
|
|
</aliases>
|
|
|
|
<!-- Timers -->
|
|
|
|
<timers>
|
|
<timer
|
|
enabled="y"
|
|
second="0.2"
|
|
script="poll_output_focus"
|
|
>
|
|
</timer>
|
|
</timers>
|
|
|
|
|
|
<!-- Get our standard constants -->
|
|
|
|
<include name="constants.lua"/>
|
|
|
|
<!-- Script -->
|
|
|
|
|
|
<script>
|
|
<![CDATA[
|
|
|
|
for i = 1, 9 do
|
|
Accelerator("ctrl + "..tostring(i), "Line_Get "..tostring(i))
|
|
end
|
|
Accelerator("ctrl +0", "Line_Get 10")
|
|
Accelerator("ctrl + shift + c", "clearoutputbuffer")
|
|
Accelerator("ctrl + alt + shift + c", "clearoutputwindow")
|
|
Accelerator("ctrl+shift+o","nextline")
|
|
Accelerator("ctrl+alt+enter","toggleinterrupt")
|
|
Accelerator("ctrl+alt+o","toggleoutput")
|
|
Accelerator("ctrl+shift+i","curline")
|
|
Accelerator("ctrl+shift+space","select line")
|
|
Accelerator("ctrl+shift+alt+space","select space")
|
|
Accelerator("ctrl+shift+u","prevline")
|
|
Accelerator("ctrl+shift+n","endline")
|
|
Accelerator("ctrl+shift+y","topline")
|
|
Accelerator("ctrl+shift+h","whichline")
|
|
Accelerator("ctrl+shift+alt+s","snap_shot")
|
|
Accelerator("ctrl+alt+j","toggle_jump_to_end")
|
|
|
|
function selectscr(eol)
|
|
buffercheck()
|
|
if selecting ~= 1 then
|
|
selecting = 1
|
|
Execute("tts_interrupt selection start.")
|
|
tselect = {}
|
|
sline = cline
|
|
else
|
|
if eol == " " then
|
|
Execute("tts_interrupt Selection coppied without line breaks.")
|
|
else
|
|
Execute("tts_interrupt Selection coppied.")
|
|
end
|
|
for i = sline, cline do
|
|
table.insert(tselect, GetLineInfo(i, 1))
|
|
end
|
|
SetClipboard(table.concat(tselect, eol))
|
|
selecting = 0
|
|
tselect = {}
|
|
end
|
|
end
|
|
|
|
function output_toggle()
|
|
if GetVariable("output") ~= "1" then
|
|
SetVariable("output", "1")
|
|
Note("output notepad on")
|
|
else
|
|
SetVariable("output", "0")
|
|
Note("output notepad off")
|
|
end
|
|
end
|
|
|
|
function interrupt_toggle()
|
|
if GetVariable("interrupt") ~= "1" then
|
|
SetVariable("interrupt", "1")
|
|
Note("interrupt on enter on")
|
|
else
|
|
SetVariable("interrupt", "0")
|
|
Note("interrupt on enter off")
|
|
end
|
|
end
|
|
|
|
function LineGet(name, line, wc)
|
|
lines = GetLinesInBufferCount()
|
|
line = GetLineInfo(lines - wc[1], 1)
|
|
if line == nil then
|
|
Sound(GetInfo(67).."sounds/pm/bonk.ogg")
|
|
return
|
|
end
|
|
wc = wc[1]
|
|
local timeout = 0.4 -- in seconds
|
|
if modes[wc] == nil then
|
|
modes[wc] = {os.clock(), 1}
|
|
elseif os.clock()-modes[wc][1] >= timeout then
|
|
modes[wc][1] = os.clock()
|
|
modes[wc][2] = 1
|
|
else
|
|
modes[wc][1] = os.clock()
|
|
modes[wc][2] = modes[wc][2] + 1
|
|
end
|
|
if modes[wc][2] >= 3 then
|
|
PasteCommand(line)
|
|
Execute("tts_interrupt pasted.")
|
|
end
|
|
if modes[wc][2] == 2 then
|
|
SetClipboard(line)
|
|
Execute("tts_interrupt copied.")
|
|
end
|
|
if modes[wc][2] == 1 then
|
|
Execute("tts_interrupt "..line)
|
|
end
|
|
end
|
|
|
|
function OnPluginCommandEntered(s)
|
|
if world.GetVariable("interrupt") == "1" then
|
|
Execute("tts_stop")
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
-- Notepad smart-scroll (formerly the separate NotepadSmartScroll
|
|
-- plugin, merged in here). See SmartAppendToNotepad's comment below
|
|
-- for why this needs raw Win32 access via Alien.
|
|
------------------------------------------------------------------
|
|
|
|
require "alien"
|
|
|
|
local user32 = alien.load("user32.dll")
|
|
local kernel32 = alien.load("kernel32.dll")
|
|
|
|
local FindWindowEx = user32.FindWindowExA
|
|
FindWindowEx:types{ ret = "pointer", "pointer", "pointer", "string", "string", abi = "stdcall" }
|
|
|
|
local GetWindowThreadProcessId = user32.GetWindowThreadProcessId
|
|
GetWindowThreadProcessId:types{ ret = "uint", "pointer", "ref uint", abi = "stdcall" }
|
|
|
|
local GetCurrentProcessId = kernel32.GetCurrentProcessId
|
|
GetCurrentProcessId:types{ ret = "uint", abi = "stdcall" }
|
|
|
|
local SendMessage = user32.SendMessageA
|
|
SendMessage:types{ ret = "long", "pointer", "uint", "uint", "long", abi = "stdcall" }
|
|
|
|
local GetModuleHandle = kernel32.GetModuleHandleA
|
|
GetModuleHandle:types{ ret = "pointer", "string", abi = "stdcall" }
|
|
|
|
local GetProcAddress = kernel32.GetProcAddress
|
|
GetProcAddress:types{ ret = "pointer", "pointer", "string", abi = "stdcall" }
|
|
|
|
local GetFocus = user32.GetFocus
|
|
GetFocus:types{ ret = "pointer", abi = "stdcall" }
|
|
|
|
-- EM_GETSEL's wParam/lParam are *pointers* to DWORD (needed to get the
|
|
-- real 32-bit selection range - the classic packed-return form is capped
|
|
-- at 64K). SendMessage above is already typed with "uint"/"long" for its
|
|
-- generic wParam/lParam, so a second binding of the same underlying
|
|
-- function is needed with "pointer" typed args for this one message.
|
|
local user32_handle = GetModuleHandle("user32.dll")
|
|
local SendMessagePtr = alien.funcptr(GetProcAddress(user32_handle, "SendMessageA"))
|
|
SendMessagePtr:types{ ret = "long", "pointer", "uint", "pointer", "pointer", abi = "stdcall" }
|
|
|
|
local WM_GETTEXTLENGTH = 0x000E
|
|
local EM_GETFIRSTVISIBLELINE = 0x00CE
|
|
local EM_LINESCROLL = 0x00B6
|
|
local EM_GETSEL = 0x00B0
|
|
local EM_SETSEL = 0x00B1
|
|
local EM_SCROLLCARET = 0x00B7
|
|
|
|
local my_pid = GetCurrentProcessId()
|
|
|
|
-- Finds the "Edit" control belonging to a MUSHclient notepad MDI child
|
|
-- window whose title exactly matches. Scoped to top-level windows owned
|
|
-- by this process, so it never touches unrelated apps on the desktop.
|
|
local function find_notepad_edit(title)
|
|
local top = nil
|
|
while true do
|
|
top = FindWindowEx(nil, top, nil, nil)
|
|
if not top then return nil end
|
|
|
|
local _, pid = GetWindowThreadProcessId(top, 0)
|
|
if pid == my_pid then
|
|
local mdiClient = FindWindowEx(top, nil, "MDIClient", nil)
|
|
if mdiClient then
|
|
local frame = FindWindowEx(mdiClient, nil, nil, title)
|
|
if frame then
|
|
local edit = FindWindowEx(frame, nil, "Edit", nil)
|
|
if edit then
|
|
return edit
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function get_selection(edit)
|
|
local start_buf = alien.buffer(4)
|
|
local end_buf = alien.buffer(4)
|
|
SendMessagePtr(edit, EM_GETSEL, start_buf, end_buf)
|
|
return start_buf:get(1, "uint"), end_buf:get(1, "uint")
|
|
end
|
|
|
|
local function scroll_to_line(edit, target)
|
|
local current = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
|
|
local delta = target - current
|
|
if delta ~= 0 then
|
|
SendMessage(edit, EM_LINESCROLL, 0, delta)
|
|
end
|
|
end
|
|
|
|
-- Wraps AppendToNotepad(title, text): if the caret wasn't already sitting
|
|
-- at the very end of the text (i.e. the user has moved it to read
|
|
-- somewhere else), both the caret/selection and the scroll position are
|
|
-- restored after the append instead of being left jumped to the end.
|
|
-- AppendToNotepad hardcodes SetSel(len,len)+ReplaceSel on every call (see
|
|
-- scripting/methods/methods_notepad.cpp in the MUSHclient source) with no
|
|
-- scriptable override, so this reads the notepad Edit control's caret and
|
|
-- scroll position via raw Win32 calls (through the Alien FFI library,
|
|
-- running in-process on MUSHclient's own thread) before appending, and
|
|
-- restores them afterward when appropriate.
|
|
--
|
|
-- Note: text should use "\r\n" line endings, not bare "\n" - the plain
|
|
-- Win32 Edit control that backs the notepad doesn't recognise bare "\n"
|
|
-- as a line break for scrolling/line-count purposes.
|
|
last_append_clock = 0
|
|
|
|
function SmartAppendToNotepad(title, text)
|
|
last_append_clock = os.clock()
|
|
|
|
local edit = find_notepad_edit(title)
|
|
local was_at_end = true
|
|
local sel_start, sel_end, first_visible
|
|
|
|
if edit then
|
|
local old_length = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
|
|
sel_start, sel_end = get_selection(edit)
|
|
was_at_end = (sel_start == old_length) and (sel_end == old_length)
|
|
if not was_at_end then
|
|
first_visible = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
|
|
end
|
|
end
|
|
|
|
AppendToNotepad(title, text)
|
|
|
|
if edit and not was_at_end then
|
|
-- Re-find in case the append above created/recreated the window.
|
|
local edit2 = find_notepad_edit(title) or edit
|
|
SendMessage(edit2, EM_SETSEL, sel_start, sel_end)
|
|
scroll_to_line(edit2, first_visible)
|
|
end
|
|
end
|
|
|
|
-- Moves the caret to the very end of a notepad and scrolls it into view.
|
|
local function jump_notepad_to_end(edit)
|
|
local len = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
|
|
SendMessage(edit, EM_SETSEL, len, len)
|
|
SendMessage(edit, EM_SCROLLCARET, 0, 0)
|
|
end
|
|
|
|
function toggle_jump_to_end()
|
|
if GetVariable("jump_to_end_on_focus") == "0" then
|
|
SetVariable("jump_to_end_on_focus", "1")
|
|
Execute("tts_interrupt Jump to end when switching to output: on")
|
|
else
|
|
SetVariable("jump_to_end_on_focus", "0")
|
|
Execute("tts_interrupt Jump to end when switching to output: off")
|
|
end
|
|
end
|
|
|
|
-- Polls for the "output" notepad gaining focus (mouse click, Ctrl+Tab
|
|
-- between MDI windows, ActivateNotepad from a script, etc.), so the
|
|
-- jump-to-end behaviour applies regardless of how the user switches to
|
|
-- it. MUSHclient's own Accelerator/hotkey system can't see keystrokes
|
|
-- while a separate notepad window has focus (confirmed by Nick Gammon on
|
|
-- the MUSHclient forum), so this can't be event-driven - polling every
|
|
-- 0.2s is the safe, non-hooking way to catch it.
|
|
was_output_focused = false
|
|
|
|
function poll_output_focus()
|
|
if GetVariable("jump_to_end_on_focus") == "0" then
|
|
was_output_focused = false
|
|
return
|
|
end
|
|
local edit = find_notepad_edit("output")
|
|
if not edit then
|
|
was_output_focused = false
|
|
return
|
|
end
|
|
local focused = GetFocus()
|
|
-- Compare by string representation, not raw "==": Alien's pointer
|
|
-- returns are full userdata objects, so two separate calls returning
|
|
-- the same real HWND don't reliably compare equal with "==" (which
|
|
-- fell back to identity comparison), causing spurious "just gained
|
|
-- focus" edges and repeated jump-to-end even while focus never moved.
|
|
local is_focused = (tostring(focused) == tostring(edit))
|
|
-- Skip the jump if a SmartAppendToNotepad call happened very recently:
|
|
-- AppendToNotepad's own SetSel/ReplaceSel appears to momentarily
|
|
-- perturb GetFocus() around each append, which was making this poll
|
|
-- see a spurious "just gained focus" edge - and jump to the end - on
|
|
-- practically every incoming line, even though the user never left
|
|
-- the window.
|
|
local recent_append = (os.clock() - last_append_clock) < 0.5
|
|
if is_focused and not was_output_focused and not recent_append then
|
|
jump_notepad_to_end(edit)
|
|
end
|
|
was_output_focused = is_focused
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
|
|
function OnPluginInstall()
|
|
if GetVariable("output") == nil then
|
|
SetVariable("output","1")
|
|
end
|
|
if GetVariable("jump_to_end_on_focus") == nil then
|
|
SetVariable("jump_to_end_on_focus","1")
|
|
end
|
|
modes = {}
|
|
cline = 1
|
|
lastcount=0
|
|
line = 0
|
|
selecting = 0
|
|
end
|
|
|
|
function OnPluginScreendraw(t,l,line)
|
|
buffercheck()
|
|
if(GetVariable("output")=="0") then
|
|
return
|
|
end
|
|
SmartAppendToNotepad("output", line.."\r\n")
|
|
end
|
|
|
|
function prev_line()
|
|
buffercheck()
|
|
oline = cline
|
|
cline = skipblanklines(cline - 1, 'b')
|
|
line = GetLineInfo(cline, 1)
|
|
if cline == oline then
|
|
Sound(GetInfo(67).."sounds/pm/bonk.ogg")
|
|
end
|
|
Execute("tts_interrupt "..line)
|
|
end
|
|
|
|
function cur_line()
|
|
buffercheck()
|
|
line = GetLineInfo(cline, 1)
|
|
Execute("tts_interrupt "..line)
|
|
end
|
|
|
|
function next_line()
|
|
buffercheck()
|
|
oline = cline
|
|
cline = skipblanklines(cline + 1, 'f')
|
|
line=GetLineInfo(cline,1)
|
|
if cline == oline then
|
|
Sound(GetInfo(67).."sounds/pm/bonk.ogg")
|
|
end
|
|
Execute("tts_interrupt "..line)
|
|
end
|
|
|
|
function end_line()
|
|
cline = skipblanklines(GetLinesInBufferCount(), 'b')
|
|
line=GetLineInfo(cline,1)
|
|
Execute("tts_interrupt Bottom: "..line)
|
|
end
|
|
|
|
function which_line()
|
|
buffercheck()
|
|
Execute("tts_interrupt line "..cline.." of "..GetLinesInBufferCount() -1)
|
|
end
|
|
|
|
function top_line()
|
|
cline=skipblanklines(0, 'f')
|
|
line=GetLineInfo(cline,1)
|
|
Execute("tts_interrupt top: "..line)
|
|
end
|
|
|
|
function snapshot()
|
|
require "wait"
|
|
wait.make(function()
|
|
Execute("tts_interrupt Beginning snapshot, please wait....")
|
|
wait.time(0.5)
|
|
for snapline=1, GetLinesInBufferCount() - 1 do
|
|
AppendToNotepad("snapshot",GetLineInfo(snapline,1).."\r\n")
|
|
end
|
|
ActivateNotepad("snapshot")
|
|
end)
|
|
end
|
|
|
|
function clearlines(n,l,w)
|
|
world.DeleteOutput()
|
|
world.Note("output buffer cleared")
|
|
cline=1
|
|
lastcount=0
|
|
line=GetLineInfo(cline,1)
|
|
selecting = 0
|
|
end
|
|
|
|
function clearwindow()
|
|
ReplaceNotepad("output")
|
|
Execute("tts_interrupt output window cleared")
|
|
end
|
|
|
|
function skipblanklines(ln, dir)
|
|
count = GetLinesInBufferCount()
|
|
if ln < 1 then
|
|
return skipblanklines(1, 'f')
|
|
elseif ln > count - 1 then
|
|
return skipblanklines(count - 1, 'b')
|
|
end
|
|
i = ln
|
|
while i > 0 and i < count and GetLineInfo(i, 1) == "" do
|
|
if dir == 'b' then
|
|
i = i - 1
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
if i == 0 or i == count then
|
|
if GetLineInfo(ln, 1) ~= "" then
|
|
return ln
|
|
elseif i == 0 then
|
|
return skipblanklines(1, 'f')
|
|
elseif i == count then
|
|
return skipblanklines(count - 1, 'b')
|
|
end
|
|
else
|
|
return i
|
|
end
|
|
end
|
|
|
|
function buffercheck()
|
|
curcount = GetLinesInBufferCount()
|
|
if curcount < lastcount then
|
|
if cline > 100 then
|
|
cline = cline - 100
|
|
else
|
|
cline = 1
|
|
end
|
|
if selecting == 1 then
|
|
if sline > 100 then
|
|
sline = sline - 100
|
|
else
|
|
sline = 1
|
|
end
|
|
end
|
|
end
|
|
lastcount = curcount
|
|
end
|
|
|
|
]]>
|
|
</script>
|
|
|
|
|
|
</muclient>
|