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

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!-- Saved on Sunday, May 25, 2025, 1:55 PM -->
<!-- Saved on Friday, July 10, 2026, 11:31 PM -->
<!-- MuClient version 5.06 -->
<!-- Written by Nick Gammon -->
<!-- Home Page: http://www.mushclient.com/ -->
@@ -8,13 +8,13 @@
<world
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
auto_say_override_prefix="-"
auto_say_string="say "
chat_name="Name-not-set"
command_stack_character=";"
id="d2211ef7d54c4a7bd0cc9ab7"
id="7c5e6f4701b5e6f94dbeb6b2"
input_font_name="Fixedsys"
mapping_failure="Alas, you cannot go that way."
name="Cosmic Rage"
@@ -111,7 +111,7 @@
<triggers
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<trigger
enabled="y"
@@ -131,7 +131,7 @@
<aliases
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</aliases>
@@ -140,7 +140,7 @@
<timers
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</timers>
@@ -149,7 +149,7 @@
<macros
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<macro name="up" type="send_now" >
@@ -243,7 +243,7 @@
<variables
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
</variables>
@@ -252,7 +252,7 @@
<colours
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<ansi>
@@ -309,7 +309,7 @@
<keypad
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<key name="0" >
@@ -423,7 +423,7 @@
<printing
muclient_version="5.06"
world_file_version="15"
date_saved="2025-05-25 13:55:57"
date_saved="2026-07-10 23:31:00"
>
<ansi>
@@ -448,7 +448,6 @@
</printing>
<!-- plugins -->
<include name="LuaAudio.xml" plugin="y" />
<include name="CaptureMudOutput.xml" plugin="y" />
<include name="Copy_Output.xml" plugin="y" />
<include name="direction_plugin.xml" plugin="y" />
@@ -457,5 +456,7 @@
<include name="Omit_Blank_Lines.xml" plugin="y" />
<include name="Reconnecter.xml" plugin="y" />
<include name="Repeat_Command.xml" plugin="y" />
<include name="GagManager.xml" plugin="y" />
<include name="LuaAudio.xml" plugin="y" />
<include name="CosmicRage.xml" plugin="y" />
</muclient>

View File

@@ -25,7 +25,7 @@ sequence="10000"
for i = 1, 9 do
Accelerator("alt + " .. i, "@buffer " .. i)
end
Accelerator("alt + 0", "@buffer 10") -- 10 is a special case
Accelerator("alt + 0", "@buffer 10")
Accelerator("alt + right", "@buffer forward")
Accelerator("alt + left", "@buffer backward")
Accelerator("alt + down", "@buffer down")
@@ -34,41 +34,64 @@ Accelerator("alt + pagedown", "@buffer scrolldown")
Accelerator("alt + pageup", "@buffer scrollup")
Accelerator("alt + end", "@buffer bottom")
Accelerator("alt + home", "@buffer top")
Accelerator("alt+shift+delete", "@buffer clear")
Accelerator("alt+delete", "@buffer clear")
Accelerator("alt+shift+delete", "@buffer clearall")
Accelerator("alt+shift+right", "@buffer add")
Accelerator("alt+shift+left", "@buffer delete")
Accelerator("f4", "@sp-settings master volume")
Accelerator("f5", "@sp-settings General volume")
Accelerator("f6", "@sp-settings ambiance volume")
Accelerator("f7", "@sp-settings in and out of character communications volume")
Accelerator("f8", "@sp-settings planetary music volume")
Accelerator("ctrl+f5", "@sp-settings clocks volume")
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
Accelerator("ctrl+f7", "@sp-settings IC social volume")
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
Accelerator("shift+f5", "@sp-settings starship combat volume")
Accelerator("shift+f6", "@sp-settings ship volume")
Accelerator("shift+f7", "@sp-settings ship movement volume")
Accelerator("shift+f8", "@sp-settings vehicle volume")
Accelerator("f9", "@sp-settings IC social sounds")
Accelerator("f10", "@sp-settings OOC social sounds")
Accelerator("alt+f9", "@sp-settings panning of sounds")
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
Accelerator("alt+f11", "@sp-settings ambiance")
Accelerator("ctrl+f9", "@sp-settings planetary music")
Accelerator("ctrl+f10", "@sp-settings space combat music")
Accelerator("ctrl+f11", "@sp-settings weather sounds")
Accelerator("alt+f12", "@sp-restart")
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
Accelerator("f2", "@sp-settings")
Accelerator("f5", "@sp-settings General volume")
Accelerator("f6", "@sp-settings ambiance volume")
Accelerator("f7", "@sp-settings in and out of character communications volume")
Accelerator("f8", "@sp-settings planetary music volume")
Accelerator("ctrl+f5", "@sp-settings clocks volume")
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
Accelerator("ctrl+f7", "@sp-settings IC social volume")
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
Accelerator("shift+f5", "@sp-settings starship combat volume")
Accelerator("shift+f6", "@sp-settings ship volume")
Accelerator("shift+f7", "@sp-settings ship movement volume")
Accelerator("shift+f8", "@sp-settings vehicle volume")
Accelerator("f9", "@sp-settings IC social sounds")
Accelerator("f10", "@sp-settings OOC social sounds")
Accelerator("alt+f9", "@sp-settings panning of sounds")
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
Accelerator("alt+f11", "@sp-settings ambiance")
Accelerator("ctrl+f9", "@sp-settings planetary music")
Accelerator("ctrl+f10", "@sp-settings space combat music")
Accelerator("ctrl+f11", "@sp-settings weather sounds")
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
Accelerator("f2", "@sp-settings")
Accelerator("shift+f1", "@sp-toggle")
Accelerator("Ctrl+shift+enter", "@sp-settings buffer sounds")
Accelerator("ctrl+shift+v", "@sp-version")
Accelerator("ctrl+s", "save_state_func")
Accelerator("alt+shift+h", "localsphelp")
Accelerator("ctrl+f2", "mainvoldown")
Accelerator("ctrl+f3", "mainvolup")
Accelerator("alt+f3", "intromusictoggle")
Accelerator("ctrl+f1", "smute")
Accelerator("f11", "f11_stop")
Accelerator("alt+f12", "alt_f12_stop")
function save_state_func()
SaveState()
Execute("tts_interrupt Client-side Soundpack Settings have been saved successfully!")
end
function f11_stop()
ppi.stop(0)
Execute("tts_interrupt All currently playing sounds have been stopped.")
Send("@sp-restart")
end
function alt_f12_stop()
ppi.stop(0)
Execute("tts_interrupt All currently playing sounds have been stopped.")
end
function RegisterMushClient(name, line, wildcards)
@@ -91,7 +114,15 @@ Note("You are currently running version " .. tostring(CURRENT_SP_VERSION).." of
if version_diff > 0 then
ColourNote("yellow", "", "Warning: Your soundpack is " .. version_diff .. " version(s) out of date. We are now on version " .. user_version .. " of the soundpack.")
ColourNote("yellow", "", "Run Updater.bat to pull the latest version.")
ColourNote("yellow", "", "Launching the updater...")
ppi.play(dir.."sounds/ogg/general/comms/announcement.ogg", 0, 0, 100)
local bat_path = dir .. "..\\updator.bat"
-- Signature is (filename, params, defdir, operation, show) - this was
-- previously passing "open" as the filename, so it never actually
-- launched cmd.exe/updator.bat at all.
utils.shellexecute("cmd", '/c "' .. bat_path .. '"', "", "open", 1)
ColourNote("yellow", "", "The updater has been launched in a separate window. Once the update is complete, please restart MUSHclient to apply the updates!")
Execute("tts_interrupt Warning: Your soundpack is out of date. The updater has been launched. Once the update is complete, please restart MUSHclient to apply the updates!")
else
ColourNote("green", "", "Your soundpack is up to date!")
end
@@ -102,6 +133,109 @@ function url_encode(str)
return string.format("%%%02X", string.byte(c))
end))
end
pending_downloads = {}
pending_play = {}
function download_sound(path, action, pan, volume, pitch, id)
if pending_downloads[path] then
table.insert(pending_play[path], {action=action, pan=pan, volume=volume, pitch=pitch, id=id})
return
end
pending_downloads[path] = os.time()
pending_play[path] = { {action=action, pan=pan, volume=volume, pitch=pitch, id=id} }
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local safe_path = sound_path:gsub("/", "\\")
local parent_dir = safe_path:match("^(.+)\\[^\\]+$")
local encoded_path = url_encode(path)
local url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
-- Download to a .part file and only rename it to the real filename after
-- curl succeeds, so check_downloads() can never mistake a half-written or
-- failed transfer for a completed one. Clears any leftover .err first, and
-- deletes it again on success, so a stale error/empty file can't linger.
local cmd_args = string.format(
'/C mkdir "%s" 2>nul & del /q "%s.err" 2>nul & ' ..
'curl.exe -s -S -L -f -o "%s.part" "%s" 2>"%s.err" ' ..
'&& (move /y "%s.part" "%s" >nul & del /q "%s.err" 2>nul) || del /q "%s.part" 2>nul',
parent_dir, safe_path, safe_path, url, safe_path, safe_path, safe_path, safe_path, safe_path
)
-- NOTE: correct shellexecute signature is (filename, params, defdir, operation, show)
utils.shellexecute("cmd", cmd_args, "", "open", 0)
end
-- If a download hasn't finished or failed within this long, give up on it
-- rather than let a stuck curl process silently block that sound forever.
DOWNLOAD_TIMEOUT_SECONDS = 30
function check_downloads(name)
for path, started_at in pairs(pending_downloads) do
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local safe_path = sound_path:gsub("/", "\\")
local f = io.open(sound_path, "r")
if f then
f:close()
local queue = pending_play[path]
pending_downloads[path] = nil
pending_play[path] = nil
if queue then
for _, p in ipairs(queue) do
play_sound_now(path, p.action, p.pan, p.volume, p.pitch, p.id)
end
end
else
local ef = io.open(safe_path .. ".err", "r")
if ef then
local msg = ef:read("*all")
ef:close()
if msg and msg ~= "" then
os.remove(safe_path .. ".err")
pending_downloads[path] = nil
pending_play[path] = nil
Note("Soundpack: download failed for " .. path .. ": " .. msg:gsub("[\r\n]", ""))
end
elseif os.time() - started_at > DOWNLOAD_TIMEOUT_SECONDS then
pending_downloads[path] = nil
pending_play[path] = nil
Note("Soundpack: download timed out for " .. path)
end
end
end
end
function play_sound_now(path, action, pan, volume, pitch, id)
local sound_file = dir.."sounds/ogg/"..path..".ogg"
local temp
if (action == "play") then
temp = ppi.play(sound_file, 0, pan, volume)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
elseif (action == "playrec") then
temp = ppi.play(sound_file, 0, pan, volume)
SetVariable(id, temp)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
elseif (action == "loop") then
temp = ppi.play(sound_file, 1, pan, volume)
SetVariable(id, temp)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
end
end
]]>
@@ -197,106 +331,87 @@ match="^\$sphook\s+(?P<action>[\w]+):(?P<path>[-\w./\\\s]+):(?P<volume>[\w]+):(?
pan="%5"
id="%6"
volume=tonumber(volume)
if (path == "na") then
else
sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
directory_path = sound_path:match("(.*/)")
if directory_path then
test_file = io.open(directory_path .. "tester.txt", "w")
if test_file then
test_file:close()
os.remove(directory_path .. "checker.txt") -- Clean up
if (pitch == "na") then
pitch = -10000
else
os.execute('mkdir "' .. directory_path .. '" 2>nul')
pitch = tonumber(pitch)
pitch = (pitch - 44100) / 98
end
end
file = io.open(sound_path, "r")
if file then
file:close()
else
local encoded_path = url_encode(path)
local download_url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
body = {}
result, status_code, headers, status = socket.http.request{
url = download_url, sink = ltn12.sink.table(body)
}
if result and status_code == 200 then
save_file = io.open(sound_path, "wb")
if save_file then
save_file:write(table.concat(body))
save_file:close()
if (pan == "na") then
pan = -10000
else
pan = tonumber(pan)
end
if (path == "na") then
if (action == "adjustsound") then
temp = GetVariable(id)
if volume and volume > -1 then
ppi.setVol(volume, temp)
end
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
elseif (action == "stop") then
temp = GetVariable(id)
if temp then
ppi.stop(temp)
end
end
else
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
local file = io.open(sound_path, "r")
if file then
file:close()
play_sound_now(path, action, pan, volume, pitch, id)
else
download_sound(path, action, pan, volume, pitch, id)
end
else
Note("Download of "..path.." failed with status code: " .. tostring(status_code))
end
end
end
if (pitch == "na") then
pitch = -10000
else
pitch = tonumber(pitch)
pitch = (pitch - 44100) / 98
end
if (pan == "na") then
pan = -10000
else
pan = tonumber(pan)
end
if (action == "play") then
temp = ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
if (pitch > -10000) then
ppi.setPitch(pitch, temp)
end
if (pan > -10000) then
ppi.setPan(pan, temp)
end
end
if (action=="playrec") then
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
SetVariable(id, temp)
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="loop") then
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 1, pan, volume)
SetVariable(id, temp)
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="adjustsound") then
temp=GetVariable (id)
if(volume>-1) then
ppi.setVol(volume,temp)
end
if(pitch>-10000) then
ppi.setPitch(pitch,temp)
end
if(pan>-10000) then
ppi.setPan(pan,temp)
end
end
if (action=="stop") then
temp=GetVariable(id)
ppi.stop(temp)
end
</send>
</trigger>
</triggers>
<!-- Aliases -->
<!-- Timers -->
<timers>
<timer
enabled="y"
second="0.5"
script="check_downloads"
>
</timer>
</timers>
<aliases>
<alias
match="f11_stop"
enabled="y"
script="f11_stop"
omit_from_output="y"
omit_from_log="y"
omit_from_command_history="y"
send_to="12"
sequence="100"
>
</alias>
<alias
match="alt_f12_stop"
enabled="y"
script="alt_f12_stop"
omit_from_output="y"
omit_from_log="y"
omit_from_command_history="y"
send_to="12"
sequence="100"
>
</alias>
</aliases>
</muclient>

View File

@@ -298,6 +298,10 @@ function OnPluginScreendraw(t, log, line)
-- checking for an empty string, or a string composed only of spaces.
-- If we don't, NVDA says blank.
if (t == 0 or t == 1) and not line:find("^%s*$") then
if GetVariable("skip_speech") == "1" then
SetVariable("skip_speech", "0")
return
end
if subst.status==0 then
return
end

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()