<?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>
</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.
function SmartAppendToNotepad(title, text)
  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()
 local is_focused = (focused == edit)
 if is_focused and not was_output_focused 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>
