Fix accessible output window, revert Tab/Ctrl+Space, sync readme keys

- output_functions.xml: merge NotepadSmartScroll's Win32/Alien-based
  smart-scroll logic in directly (was a separate plugin). Fixed the
  caret-restore bug: gating the restore on scroll position was wrong
  since the notepad tracks near the bottom during normal reading; now
  gated on whether the caret was literally at the end of the old text.
  Added a jump-to-end-on-focus toggle (Ctrl+Alt+J) with a 0.2s polling
  timer to catch focus landing on the output notepad by any means.
- Reverted the Tab/Ctrl+Space rebinding - removed switch_to_output and
  trigger_word_completion along with their accelerators.
- cosmic rage.MCL: dropped the dangling NotepadSmartScroll.xml include
  (file no longer exists, was reintroduced by an auto-save).
- readme.md: documented the actual current CosmicRage.xml keys (several
  had drifted out of sync - Alt+F12 is now stop-sounds not restart,
  Alt+Delete/Ctrl+S/Ctrl+F1-3/Alt+F3/Alt+Shift+H/F11 were undocumented)
  and added the output_functions.xml accessible-output keybindings,
  which weren't documented at all before.
This commit is contained in:
OlegTheSnowman
2026-07-11 00:02:28 +03:00
parent 21fa3e0d57
commit 09785a9080
8 changed files with 719 additions and 155 deletions

View File

@@ -14,8 +14,8 @@ save_state="y"
language="Lua"
purpose="provides functions to help tts users."
date_written="2010-04-06 08:37:40"
requires="4.46"
version="1.0"
requires="4.82"
version="1.1"
>
</plugin>
@@ -172,6 +172,17 @@ sequence="100">
</alias>
</aliases>
<!-- Timers -->
<timers>
<timer
enabled="y"
second="0.2"
script="poll_output_focus"
>
</timer>
</timers>
<!-- Get our standard constants -->
@@ -200,6 +211,7 @@ 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()
@@ -280,12 +292,188 @@ function OnPluginCommandEntered(s)
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 = {}
msgbuffer = {}
cline = 1
lastcount=0
line = 0
@@ -297,23 +485,7 @@ function OnPluginScreendraw(t,l,line)
if(GetVariable("output")=="0") then
return
end
if GetInfo(113) then
AppendToNotepad("output", line.."\r\n")
else
table.insert(msgbuffer, line)
end
end
function OnPluginGetFocus()
if GetVariable("output") == "0" then
return
end
if #msgbuffer > 0 then
for i, buffered in ipairs(msgbuffer) do
AppendToNotepad("output", buffered.."\r\n")
end
msgbuffer={}
end
SmartAppendToNotepad("output", line.."\r\n")
end
function prev_line()