Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
20618 lines (19205 sloc) 949 KB
/*
AutoHotkey
Copyright 2003-2009 Chris Mallett (support@autohotkey.com)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "stdafx.h" // pre-compiled headers
#include "script.h"
#include "globaldata.h" // for a lot of things
#include "util.h" // for strlcpy() etc.
#include "mt19937ar-cok.h" // for random number generator
#include "window.h" // for a lot of things
#include "application.h" // for MsgSleep()
#include "exports.h" // Naveen v8
#include "TextIO.h"
#include "LiteZip.h"
#include "MemoryModule.h"
// Globals that are for only this module:
#define MAX_COMMENT_FLAG_LENGTH 15
static TCHAR g_CommentFlag[MAX_COMMENT_FLAG_LENGTH + 1] = _T(";"); // Adjust the below for any changes.
static size_t g_CommentFlagLength = 1; // pre-calculated for performance
static ExprOpFunc g_ObjGet(BIF_ObjInvoke, IT_GET), g_ObjSet(BIF_ObjInvoke, IT_SET);
static ExprOpFunc g_ObjGetInPlace(BIF_ObjGetInPlace, IT_GET);
static ExprOpFunc g_ObjNew(BIF_ObjNew, IT_CALL);
static ExprOpFunc g_ObjPreInc(BIF_ObjIncDec, SYM_PRE_INCREMENT), g_ObjPreDec(BIF_ObjIncDec, SYM_PRE_DECREMENT)
, g_ObjPostInc(BIF_ObjIncDec, SYM_POST_INCREMENT), g_ObjPostDec(BIF_ObjIncDec, SYM_POST_DECREMENT);
ExprOpFunc g_ObjCall(BIF_ObjInvoke, IT_CALL); // Also needed in script_expression.cpp.
#define VF(name, fn) { _T(#name), fn }
#define A_x(name, fn) { _T(#name), fn }
#define A_(name) A_x(name, BIV_##name)
// IMPORTANT: Both of the following arrays must be kept in alphabetical order
// for binary search to work. See Script::GetBuiltInVar for further comments.
// g_BIV: All built-in vars not beginning with "A_". Keeping these separate allows
// the search to be limited to just these few whenever the var name does not begin
// with "A_", as for most user-defined variables. This helps average-case performance.
VarEntry g_BIV[] =
{
VF(Clipboard, (BuiltInVarType)VAR_CLIPBOARD),
VF(ClipboardAll, (BuiltInVarType)VAR_CLIPBOARDALL),
VF(ComSpec, BIV_ComSpec), // Lacks an "A_" prefix for backward compatibility with pre-NoEnv scripts and also it's easier to type & remember.,
VF(False, BIV_True_False_Null),
VF(Null, BIV_True_False_Null),
VF(ProgramFiles, BIV_SpecialFolderPath), // v1.0.43.08: Added to ease the transition to #NoEnv.,
VF(True, BIV_True_False_Null)
};
// g_BIV_A: All built-in vars beginning with "A_". The prefix is omitted from each
// name to reduce code size and speed up the comparisons.
VarEntry g_BIV_A[] =
{
A_(AhkDir),
A_(AhkPath),
A_(AhkVersion),
A_x(AppData, BIV_SpecialFolderPath),
A_x(AppDataCommon, BIV_SpecialFolderPath),
A_(AutoTrim),
A_x(BatchLines, BIV_BatchLines),
A_x(CaretX, BIV_Caret),
A_x(CaretY, BIV_Caret),
A_x(ComputerName, BIV_UserName_ComputerName),
A_(ComSpec),
A_x(ControlDelay, BIV_xDelay),
A_x(CoordModeCaret, BIV_CoordMode),
A_x(CoordModeMenu, BIV_CoordMode),
A_x(CoordModeMouse, BIV_CoordMode),
A_x(CoordModePixel, BIV_CoordMode),
A_x(CoordModeToolTip, BIV_CoordMode),
A_(Cursor),
A_x(DD, BIV_DateTime),
A_x(DDD, BIV_MMM_DDD),
A_x(DDDD, BIV_MMM_DDD),
#ifndef MINIDLL
A_x(DefaultGui, BIV_DefaultGui),
A_x(DefaultListView, BIV_DefaultGui),
A_(DefaultMouseSpeed),
A_x(DefaultTreeView, BIV_DefaultGui),
#endif
A_x(Desktop, BIV_SpecialFolderPath),
A_x(DesktopCommon, BIV_SpecialFolderPath),
A_(DetectHiddenText),
A_(DetectHiddenWindows),
A_(DllDir),
A_(DllPath),
#ifndef MINIDLL
A_(EndChar),
#endif
A_(EventInfo), // It's called "EventInfo" vs. "GuiEventInfo" because it applies to non-Gui events such as OnClipboardChange.,
A_(ExitReason),
A_(FileEncoding),
A_(FormatFloat),
A_(FormatInteger),
A_(GlobalStruct),
#ifndef MINIDLL
A_x(Gui, BIV_Gui),
A_(GuiControl),
A_x(GuiControlEvent, BIV_GuiEvent),
A_x(GuiEvent, BIV_GuiEvent), // v1.0.36: A_GuiEvent was added as a synonym for A_GuiControlEvent because it seems unlikely that A_GuiEvent will ever be needed for anything:,
A_x(GuiHeight, BIV_Gui),
A_x(GuiWidth, BIV_Gui),
A_x(GuiX, BIV_Gui), // Naming: Brevity seems more a benefit than would A_GuiEventX's improved clarity.,
A_x(GuiY, BIV_Gui), // These can be overloaded if a GuiMove label or similar is ever needed.,
#endif
A_x(Hour, BIV_DateTime),
#ifndef MINIDLL
A_(IconFile),
A_(IconHidden),
A_(IconNumber),
A_(IconTip),
#endif
A_x(Index, BIV_LoopIndex),
A_x(IPAddress1, BIV_IPAddress),
A_x(IPAddress2, BIV_IPAddress),
A_x(IPAddress3, BIV_IPAddress),
A_x(IPAddress4, BIV_IPAddress),
A_(Is64bitOS),
A_(IsAdmin),
A_(IsCompiled),
A_(IsCritical),
A_(IsDll),
A_(IsMini),
A_(IsPaused),
#ifndef MINIDLL
A_(IsSuspended),
#endif
A_(IsUnicode),
A_x(KeyDelay, BIV_xDelay),
A_x(KeyDelayPlay, BIV_xDelay),
A_x(KeyDuration, BIV_xDelay),
A_x(KeyDurationPlay, BIV_xDelay),
A_(Language),
A_(LastError),
A_(LineFile),
A_(LineNumber),
A_(ListLines),
A_(LoopField),
A_(LoopFileAttrib),
A_(LoopFileDir),
A_(LoopFileExt),
A_(LoopFileFullPath),
A_(LoopFileLongPath),
A_(LoopFileName),
A_x(LoopFilePath, BIV_LoopFileFullPath),
A_(LoopFileShortName),
A_(LoopFileShortPath),
A_x(LoopFileSize, BIV_LoopFileSize),
A_x(LoopFileSizeKB, BIV_LoopFileSize),
A_x(LoopFileSizeMB, BIV_LoopFileSize),
A_x(LoopFileTimeAccessed, BIV_LoopFileTime),
A_x(LoopFileTimeCreated, BIV_LoopFileTime),
A_x(LoopFileTimeModified, BIV_LoopFileTime),
A_(LoopReadLine),
A_(LoopRegKey),
A_(LoopRegName),
A_(LoopRegSubKey),
A_(LoopRegTimeModified),
A_(LoopRegType),
A_x(MDay, BIV_DateTime),
A_(MemoryModule),
A_x(Min, BIV_DateTime),
A_x(MM, BIV_DateTime),
A_x(MMM, BIV_MMM_DDD),
A_x(MMMM, BIV_MMM_DDD),
A_(ModuleHandle),
A_x(Mon, BIV_DateTime),
A_x(MouseDelay, BIV_xDelay),
A_x(MouseDelayPlay, BIV_xDelay),
A_x(MSec, BIV_DateTime),
A_(MyDocuments),
A_x(Now, BIV_Now),
A_x(NowUTC, BIV_Now),
A_x(NumBatchLines, BIV_BatchLines),
A_(OSType),
A_(OSVersion),
#ifndef MINIDLL
A_(PriorHotkey),
A_(PriorKey),
#endif
A_x(ProgramFiles, BIV_SpecialFolderPath),
A_x(Programs, BIV_SpecialFolderPath),
A_x(ProgramsCommon, BIV_SpecialFolderPath),
A_(PtrSize),
A_(RegView),
#ifndef MINIDLL
A_(ScreenDPI),
#endif
A_x(ScreenHeight, BIV_ScreenWidth_Height),
A_x(ScreenWidth, BIV_ScreenWidth_Height),
A_(ScriptDir),
A_(ScriptFullPath),
A_(ScriptHwnd),
A_(ScriptName),
A_(ScriptStruct),
A_x(Sec, BIV_DateTime),
A_(SendLevel),
A_(SendMode),
A_x(Space, BIV_Space_Tab),
A_x(StartMenu, BIV_SpecialFolderPath),
A_x(StartMenuCommon, BIV_SpecialFolderPath),
A_x(Startup, BIV_SpecialFolderPath),
A_x(StartupCommon, BIV_SpecialFolderPath),
A_(StoreCapslockMode),
A_(StringCaseSense),
A_x(Tab, BIV_Space_Tab),
A_(Temp), // Debatably should be A_TempDir, but brevity seemed more popular with users, perhaps for heavy uses of the temp folder.,
A_(ThisFunc),
#ifndef MINIDLL
A_(ThisHotkey),
#endif
A_(ThisLabel),
#ifndef MINIDLL
A_(ThisMenu),
A_(ThisMenuItem),
A_(ThisMenuItemPos),
#endif
A_(TickCount),
A_(TimeIdle),
A_x(TimeIdleKeyboard, BIV_TimeIdlePhysical),
A_x(TimeIdleMouse, BIV_TimeIdlePhysical),
A_(TimeIdlePhysical),
#ifndef MINIDLL
A_(TimeSincePriorHotkey),
A_(TimeSinceThisHotkey),
#endif
A_(TitleMatchMode),
A_(TitleMatchModeSpeed),
A_x(UserName, BIV_UserName_ComputerName),
A_x(WDay, BIV_DateTime),
A_x(WinDelay, BIV_xDelay),
A_(WinDir),
A_(WorkingDir),
A_x(YDay, BIV_DateTime),
A_x(Year, BIV_DateTime),
A_x(YWeek, BIV_DateTime),
A_x(YYYY, BIV_DateTime)
};
#undef A_
#undef VF
// See Script::CreateWindows() for details about the following:
typedef BOOL (WINAPI* AddRemoveClipboardListenerType)(HWND);
static AddRemoveClipboardListenerType MyRemoveClipboardListener = (AddRemoveClipboardListenerType)
GetProcAddress(GetModuleHandle(_T("user32")), "RemoveClipboardFormatListener");
static AddRemoveClipboardListenerType MyAddClipboardListener = (AddRemoveClipboardListenerType)
GetProcAddress(GetModuleHandle(_T("user32")), "AddClipboardFormatListener");
static TextMem::Buffer includedtextbuf; //HotKeyIt for dll to read script from memory
// General note about the methods in here:
// Want to be able to support multiple simultaneous points of execution
// because more than one subroutine can be executing simultaneously
// (well, more precisely, there can be more than one script subroutine
// that's in a "currently running" state, even though all such subroutines,
// except for the most recent one, are suspended. So keep this in mind when
// using things such as static data members or static local variables.
Script::Script()
: mFirstLine(NULL), mLastLine(NULL), mCurrLine(NULL), mPlaceholderLabel(NULL), mFirstStaticLine(NULL), mLastStaticLine(NULL)
#ifndef MINIDLL
, mThisHotkeyName(_T("")), mPriorHotkeyName(_T("")), mThisHotkeyStartTime(0), mPriorHotkeyStartTime(0)
, mEndChar(0), mThisHotkeyModifiersLR(0)
#endif
, mNextClipboardViewer(NULL), mOnClipboardChangeIsRunning(false), mOnClipboardChangeLabel(NULL)
, mOnExitLabel(NULL), mExitReason(EXIT_NONE)
, mFirstLabel(NULL), mLastLabel(NULL)
, mFunc(NULL), mFuncCount(0), mFuncCountMax(0)
, mFirstTimer(NULL), mLastTimer(NULL), mTimerEnabledCount(0), mTimerCount(0)
#ifndef MINIDLL
, mFirstMenu(NULL), mLastMenu(NULL), mMenuCount(0), mThisMenuItem(NULL)
#endif
, mVar(NULL), mVarCount(0), mVarCountMax(0), mLazyVar(NULL), mLazyVarCount(0)
, mCurrentFuncOpenBlockCount(0), mNextLineIsFunctionBody(false), mNoUpdateLabels(false)
, mClassObjectCount(0), mUnresolvedClasses(NULL), mClassProperty(NULL), mClassPropertyDef(NULL)
, mCurrFileIndex(0), mCombinedLineNumber(0), mNoHotkeyLabels(true)
#ifndef MINIDLL
, mMenuUseErrorLevel(false)
#endif
, mFileSpec(_T("")), mFileDir(_T("")), mFileName(_T("")), mOurEXE(_T("")), mOurEXEDir(_T("")), mMainWindowTitle(_T(""))
, mIsReadyToExecute(false), mAutoExecSectionIsRunning(false)
, mIsRestart(false), mErrorStdOut(false)
#ifndef AUTOHOTKEYSC
, mIncludeLibraryFunctionsThenExit(NULL)
#endif
, mLinesExecutedThisCycle(0), mUninterruptedLineCountMax(1000), mUninterruptibleTime(15)
#ifndef MINIDLL
, mCustomIcon(NULL), mCustomIconSmall(NULL) // Normally NULL unless there's a custom tray icon loaded dynamically.
, mCustomIconFile(NULL), mIconFrozen(false), mTrayIconTip(NULL) // Allocated on first use.
, mCustomIconNumber(0)
#endif
{
// v1.0.25: mLastScriptRest and mLastPeekTime are now initialized right before the auto-exec
// section of the script is launched, which avoids an initial Sleep(10) in ExecUntil
// that would otherwise occur.
#ifndef MINIDLL
*mThisMenuItemName = *mThisMenuName = '\0';
#endif
ZeroMemory(&mNIC, sizeof(mNIC)); // Constructor initializes this, to be safe.
mNIC.hWnd = NULL; // Set this as an indicator that it tray icon is not installed.
#ifndef MINIDLL
// Lastly (after the above have been initialized), anything that can fail:
if ( !(mTrayMenu = AddMenu(_T("Tray"))) ) // realistically never happens
{
ScriptError(_T("No tray mem"));
ExitApp(EXIT_CRITICAL);
}
else
mTrayMenu->mIncludeStandardItems = true;
#endif
#ifdef _DEBUG
if (ID_FILE_EXIT < ID_MAIN_FIRST) // Not a very thorough check.
ScriptError(_T("DEBUG: ID_FILE_EXIT is too large (conflicts with IDs reserved via ID_USER_FIRST)."));
if (MAX_CONTROLS_PER_GUI > ID_USER_FIRST - 3)
ScriptError(_T("DEBUG: MAX_CONTROLS_PER_GUI is too large (conflicts with IDs reserved via ID_USER_FIRST)."));
if (g_ActionCount != ACT_COUNT) // This enum value only exists in debug mode.
ScriptError(_T("DEBUG: g_act and enum_act are out of sync."));
int LargestMaxParams, i, j;
ActionTypeType *np;
// Find the Largest value of MaxParams used by any command and make sure it
// isn't something larger than expected by the parsing routines:
for (LargestMaxParams = i = 0; i < g_ActionCount; ++i)
{
if (g_act[i].MaxParams > LargestMaxParams)
LargestMaxParams = g_act[i].MaxParams;
// This next part has been tested and it does work, but only if one of the arrays
// contains exactly MAX_NUMERIC_PARAMS number of elements and isn't zero terminated.
// Relies on short-circuit boolean order:
for (np = g_act[i].NumericParams, j = 0; j < MAX_NUMERIC_PARAMS && *np; ++j, ++np);
if (j >= MAX_NUMERIC_PARAMS)
{
ScriptError(_T("DEBUG: At least one command has a NumericParams array that isn't zero-terminated.")
_T(" This would result in reading beyond the bounds of the array."));
return;
}
}
if (LargestMaxParams > MAX_ARGS)
ScriptError(_T("DEBUG: At least one command supports more arguments than allowed."));
if (sizeof(ActionTypeType) == 1 && g_ActionCount > 256)
ScriptError(_T("DEBUG: Since there are now more than 256 Action Types, the ActionTypeType")
_T(" typedef must be changed."));
#endif
#ifndef _USRDLL
OleInitialize(NULL);
#endif
}
Script::~Script() // Destructor.
{
// MSDN: "Before terminating, an application must call the UnhookWindowsHookEx function to free
// system resources associated with the hook."
#ifndef MINIDLL
AddRemoveHooks(0); // Remove all hooks.
if (mNIC.hWnd) // Tray icon is installed.
Shell_NotifyIcon(NIM_DELETE, &mNIC); // Remove it.
// Destroy any Progress/SplashImage windows that haven't already been destroyed. This is necessary
// because sometimes these windows aren't owned by the main window:
#endif
int i;
#ifndef MINIDLL
for (i = 0; i < MAX_PROGRESS_WINDOWS; ++i)
{
if (g_Progress[i].hwnd && IsWindow(g_Progress[i].hwnd))
DestroyWindow(g_Progress[i].hwnd);
if (g_Progress[i].hfont1) // Destroy font only after destroying the window that uses it.
DeleteObject(g_Progress[i].hfont1);
if (g_Progress[i].hfont2) // Destroy font only after destroying the window that uses it.
DeleteObject(g_Progress[i].hfont2);
if (g_Progress[i].hbrush)
DeleteObject(g_Progress[i].hbrush);
}
for (i = 0; i < MAX_SPLASHIMAGE_WINDOWS; ++i)
{
if (g_SplashImage[i].pic_bmp)
{
if (g_SplashImage[i].pic_type == IMAGE_BITMAP)
DeleteObject(g_SplashImage[i].pic_bmp);
else
DestroyIcon(g_SplashImage[i].pic_icon);
}
if (g_SplashImage[i].hwnd && IsWindow(g_SplashImage[i].hwnd))
DestroyWindow(g_SplashImage[i].hwnd);
if (g_SplashImage[i].hfont1) // Destroy font only after destroying the window that uses it.
DeleteObject(g_SplashImage[i].hfont1);
if (g_SplashImage[i].hfont2) // Destroy font only after destroying the window that uses it.
DeleteObject(g_SplashImage[i].hfont2);
if (g_SplashImage[i].hbrush)
DeleteObject(g_SplashImage[i].hbrush);
}
// It is safer/easier to destroy the GUI windows prior to the menus (especially the menu bars).
// This is because one GUI window might get destroyed and take with it a menu bar that is still
// in use by an existing GUI window. GuiType::Destroy() adheres to this philosophy by detaching
// its menu bar prior to destroying its window:
if (g_guiCount)
{
while (g_guiCount)
GuiType::Destroy(*g_gui[g_guiCount - 1]); // Static method to avoid problems with object destroying itself.
free(g_gui);
g_gui = NULL;
g_guiCount = 0;
g_guiCountMax = 0;
}
for (i = 0; i < GuiType::sFontCount; ++i) // Now that GUI windows are gone, delete all GUI fonts.
if (GuiType::sFont[i].hfont)
DeleteObject(GuiType::sFont[i].hfont);
// The above might attempt to delete an HFONT from GetStockObject(DEFAULT_GUI_FONT), etc.
// But that should be harmless:
// MSDN: "It is not necessary (but it is not harmful) to delete stock objects by calling DeleteObject."
// Above: Probably best to have removed icon from tray and destroyed any Gui/Splash windows that were
// using it prior to getting rid of the script's custom icon below:
if (mCustomIcon)
{
DestroyIcon(mCustomIcon);
DestroyIcon(mCustomIconSmall); // Should always be non-NULL if mCustomIcon is non-NULL.
}
// Since they're not associated with a window, we must free the resources for all popup menus.
// Update: Even if a menu is being used as a GUI window's menu bar, see note above for why menu
// destruction is done AFTER the GUI windows are destroyed:
UserMenu *menu_to_delete;
for (UserMenu *m = mFirstMenu; m;)
{
menu_to_delete = m;
m = m->mNextMenu;
#ifdef _USRDLL
if (menu_to_delete != mTrayMenu)
#endif
ScriptDeleteMenu(menu_to_delete);
// Above call should not return FAIL, since the only way FAIL can realistically happen is
// when a GUI window is still using the menu as its menu bar. But all GUI windows are gone now.
}
#ifdef _USRDLL
if (mFirstMenu != mTrayMenu)
{
mFirstMenu = NULL;
mLastMenu = NULL;
mTrayMenu = NULL;
}
else if (mFirstMenu)
{
mFirstMenu->mNextMenu = NULL;
mLastMenu = mFirstMenu;
}
mTrayIconTip = NULL;
mPriorHotkeyStartTime = 0;
#endif
#endif // MINIDLL
// Since tooltip windows are unowned, they should be destroyed to avoid resource leak:
for (i = 0; i < MAX_TOOLTIPS; ++i)
if (g_hWndToolTip[i] && IsWindow(g_hWndToolTip[i]))
DestroyWindow(g_hWndToolTip[i]);
#ifndef MINIDLL
if (g_hFontSplash) // The splash window itself should auto-destroyed, since it's owned by main.
DeleteObject(g_hFontSplash);
#endif
if (mOnClipboardChangeLabel || mOnClipboardChange.Count()) // Remove from viewer chain.
EnableClipboardListener(false);
// We call DestroyWindow() because MainWindowProc() has left that up to us.
// DestroyWindow() will cause MainWindowProc() to immediately receive and process the
// WM_DESTROY msg, which should in turn result in any child windows being destroyed
// and other cleanup being done:
KILL_AUTOEXEC_TIMER
KILL_MAIN_TIMER
if (IsWindow(g_hWnd)) // Adds peace of mind in case WM_DESTROY was already received in some unusual way.
{
g_DestroyWindowCalled = true;
DestroyWindow(g_hWnd);
DestroyWindow(g_hWndEdit);
DeleteObject(g_hFontEdit);
#ifndef MINIDLL
if (g_hWndSplash)
DestroyWindow(g_hWndSplash);
if (g_hFontSplash)
DeleteObject(g_hFontSplash);
#endif // MINIDLL
// Unregister window class registered in Script::CreateWindows
#ifdef UNICODE
if (g_ClassRegistered)
UnregisterClass(WINDOW_CLASS_MAIN, g_hInstance);
#ifndef MINIDLL
if (g_ClassSplashRegistered)
UnregisterClass(WINDOW_CLASS_SPLASH, g_hInstance);
#endif // MINIDLL
#else
if (g_ClassRegistered)
UnregisterClass((LPCSTR)&WINDOW_CLASS_MAIN, g_hInstance);
#ifndef MINIDLL
if (g_ClassSplashRegistered)
UnregisterClass((LPCSTR)&WINDOW_CLASS_SPLASH, g_hInstance);
#endif // MINIDLL
#endif // UNICODE
}
if (g_hAccelTable)
DestroyAcceleratorTable(g_hAccelTable);
// Close any open sound item to prevent hang-on-exit in certain operating systems or conditions.
// If there's any chance that a sound was played and not closed out, or that it is still playing,
// this check is done. Otherwise, the check is avoided since it might be a high overhead call,
// especially if the sound subsystem part of the OS is currently swapped out or something:
if (g_SoundWasPlayed)
{
TCHAR buf[MAX_PATH * 2];
mciSendString(_T("status ") SOUNDPLAY_ALIAS _T(" mode"), buf, _countof(buf), NULL);
if (*buf) // "playing" or "stopped"
mciSendString(_T("close ") SOUNDPLAY_ALIAS, NULL, 0, NULL);
}
}
#ifdef _USRDLL
void Script::Destroy()
// HotKeyIt H1 destroy script for ahkTerminate and ahkReload and ExitApp for dll
{
//reset count for OnMessage
if (g_MsgMonitor.Count())
g_MsgMonitor.RemoveAll();
// free Meta Object
g_MetaObject.Free();
// Disconnect debugger
if (!g_DebuggerHost.IsEmpty())
{
g_DebuggerHost.Empty();
g_Debugger.Disconnect();
}
// L31: Release objects stored in variables, where possible and delete vars.
int v, i;
for (v = 0; v < mVarCount; v++)
{
if (mVar[v]->mType == VAR_BUILTIN || mVar[v]->mType == VAR_CLIPBOARD ||mVar[v]->mType == VAR_CLIPBOARDALL)
continue;
if (mVar[v]->mType == VAR_ALIAS)
mVar[v]->ConvertToNonAliasIfNecessary();
else
mVar[v]->Free();
}
for (v = 0; v < mLazyVarCount; v++)
{
if (mLazyVar[v]->mType == VAR_ALIAS)
mLazyVar[v]->ConvertToNonAliasIfNecessary();
else
mLazyVar[v]->Free();
}
// delete static func vars first
for (i = 0; i < mFuncCount; i++)
{
Func &f = *mFunc[i];
if (f.mIsBuiltIn)
{
if (f.mStaticVar && (f.mBIF = (BuiltInFunctionType)BIF_DllImport))
{
for (int i = 0; i < f.mParamCount; i++)
{
#ifdef UNICODE
CStringA** pStr = ((CStringA**)f.mStaticVar);
#else
CStringA** pStr = ((CStringA**)f.mStaticVar);
#endif
if (pStr[i])
delete pStr[i];
}
}
continue;
}
// Since it doesn't seem feasible to release all var backups created by recursive function
// calls and all tokens in the 'stack' of each currently executing expression, currently
// only static and global variables are released. It seems best for consistency to also
// avoid releasing top-level non-static local variables (i.e. which aren't in var backups).
for (v = 0; v < f.mStaticVarCount; v++)
{
if (f.mStaticVar[v]->mType == VAR_ALIAS && f.mStaticVar[v]->HasObject())
f.mStaticVar[v]->mObject->Release();
f.mStaticVar[v]->ConvertToNonAliasIfNecessary();
f.mStaticVar[v]->Free();
}
for (v = 0; v < f.mStaticLazyVarCount; v++)
{
if (f.mStaticLazyVar[v]->mType == VAR_ALIAS)
f.mStaticLazyVar[v]->ConvertToNonAliasIfNecessary();
else
f.mStaticLazyVar[v]->Free();
}
for (v = 0; v < f.mVarCount; v++)
{
if (f.mVar[v]->mType == VAR_ALIAS)
f.mVar[v]->ConvertToNonAliasIfNecessary();
else
f.mVar[v]->Free();
}
for (v = 0; v < f.mLazyVarCount; v++)
{
if (f.mLazyVar[v]->mType == VAR_ALIAS)
f.mLazyVar[v]->ConvertToNonAliasIfNecessary();
else
f.mLazyVar[v]->Free();
}
}
// Now all objects are freed and variables can be deleted
for (v = 0; v < mVarCount; v++)
{
// H19 fix not to delete Clipboard wars
//if (mVar[v]->mType == VAR_BUILTIN || mVar[v]->mType == VAR_CLIPBOARD || mVar[v]->mType == VAR_CLIPBOARDALL)
//continue;
delete mVar[v];
}
free(mVar);
mVar = NULL;
mVarCount = 0;
mVarCountMax = 0;
for (v = 0; v < mLazyVarCount; v++)
{
delete mLazyVar[v];
}
free(mLazyVar);
mLazyVar = NULL;
mLazyVarCount = 0;
// delete static func vars first
for (i = 0; i < mFuncCount; i++)
{
Func &f = *mFunc[i];
if (f.mIsBuiltIn)
continue;
else if (f.mClass && _tcschr(f.mName,'.'))
f.mClass->Release();
// Since it doesn't seem feasible to release all var backups created by recursive function
// calls and all tokens in the 'stack' of each currently executing expression, currently
// only static and global variables are released. It seems best for consistency to also
// avoid releasing top-level non-static local variables (i.e. which aren't in var backups).
for (v = 0; v < f.mStaticVarCount; v++)
{
delete f.mStaticVar[v];
}
for (v = 0; v < f.mStaticLazyVarCount; v++)
{
delete f.mStaticLazyVar[v];
}
for (v = 0; v < f.mVarCount; v++)
{
delete f.mVar[v];
}
for (v = 0; v < f.mLazyVarCount; v++)
{
delete f.mLazyVar[v];
}
if (mFunc[i]->mStaticVar)
free(mFunc[i]->mStaticVar);
if (mFunc[i]->mStaticLazyVar)
free(mFunc[i]->mStaticLazyVar);
if (mFunc[i]->mVar)
free(mFunc[i]->mVar);
if (mFunc[i]->mLazyVar)
free(mFunc[i]->mLazyVar);
delete mFunc[i];
}
// Destroy Labels
for (Label *label = mFirstLabel,*nextLabel = NULL; label;)
{
nextLabel = label->mNextLabel;
delete label;
label = nextLabel;
}
// Destroy Groups
for (WinGroup *group = mFirstGroup, *nextGroup = NULL; group;)
{
nextGroup = group->mNextGroup;
delete group;
group = nextGroup;
}
for (Line *line = g_script.mLastLine, *nextLine = NULL; line;)
{
nextLine = line->mPrevLine;
line->FreeDerefBufIfLarge();
delete line;
line = nextLine;
}
if (Line::sDerefBuf)
{
free(Line::sDerefBuf);
Line::sDerefBuf = NULL;
}
Script::~Script(); // destroy main script before resetting variables
mVarCount = 0;
mVarCountMax = 0;
mLazyVarCount = 0;
mFuncCount = 0;
mFuncCountMax = 0;
mFirstLabel = NULL ;
mLastLabel = NULL ;
mFirstStaticLine = 0;
mLastStaticLine = 0;
mFirstLine = NULL ;
mLastLine = NULL ;
mCurrLine = NULL ;
mCurrFileIndex = 0 ;
mCombinedLineNumber = 0;
mFirstGroup = NULL;
mLastGroup = NULL;
mFirstTimer = NULL;
mOnExitLabel = NULL;
mOnClipboardChangeLabel = NULL;
mTempFunc = NULL;
mTempLabel = NULL;
mTempLine = NULL;
g_nMessageBoxes = 0;
#ifndef MINIDLL
g_nInputBoxes = 0;
g_nFileDialogs = 0;
g_nFolderDialogs = 0;
g_NoTrayIcon = false;
if (g_IconLarge)
DestroyIcon(g_IconLarge);
if (g_IconSmall)
DestroyIcon(g_IconSmall);
#endif
g_MainTimerExists = false;
g_AutoExecTimerExists = false;
#ifndef MINIDLL
g_InputTimerExists = false;
#endif
g_DerefTimerExists = false;
g_SoundWasPlayed = false;
#ifndef MINIDLL
g_IsSuspended = false; // Make this separate from g_AllowInterruption since that is frequently turned off & on.
#endif
g_DeferMessagesForUnderlyingPump = false;
g_nLayersNeedingTimer = 0;
g_nThreads = 0;
g_nPausedThreads = 0;
g_MaxThreadsTotal = MAX_THREADS_DEFAULT;
#ifndef MINIDLL
g_MaxHistoryKeys = 40;
g_MaxThreadsPerHotkey = 1;
g_MaxHotkeysPerInterval = 70; // Increased to 70 because 60 was still causing the warning dialog for repeating keys sometimes. Increased from 50 to 60 for v1.0.31.02 since 50 would be triggered by keyboard auto-repeat when it is set to its fastest.
g_HotkeyThrottleInterval = 2000; // Milliseconds.
#endif
g_MaxThreadsBuffer = false; // This feature usually does more harm than good, so it defaults to OFF.
g_InputLevel = 0;
#ifndef MINIDLL
g_FirstHotCriterion = NULL;
g_LastHotCriterion = NULL;
g_HotExprTimeout = 1000; // Timeout for #if (expression) evaluation, in milliseconds.
g_HotExprLFW = NULL; // Last Found Window of last #if expression.
g_MenuIsVisible = MENU_TYPE_NONE;
g_guiCount = 0;
// g_guiCountMax = 0; no need because we use realloc for g_gui
#ifndef MINIDLL
g_HSPriority = 0; // default priority is always 0
g_HSKeyDelay = 0; // Fast sends are much nicer for auto-replace and auto-backspace.
g_HSSendMode = SM_INPUT; // v1.0.43: New default for more reliable hotstrings.
g_HSCaseSensitive = false;
g_HSConformToCase = true;
g_HSDoBackspace = true;
g_HSOmitEndChar = false;
g_HSSendRaw = false;
g_HSEndCharRequired = true;
g_HSDetectWhenInsideWord = false;
g_HSDoReset = false;
g_HSResetUponMouseClick = true;
_tcscpy(g_EndChars,_T("-()[]{}:;'\"/\\,.?!\n \t")); // Hotstring default end chars, including a space.
#endif
g_ErrorLevel = NULL; // Allows us (in addition to the user) to set this var to indicate success/failure.
#ifndef MINIDLL
g_ForceKeybdHook = false;
#endif
g_ForceNumLock = NEUTRAL;
g_ForceCapsLock = NEUTRAL;
g_ForceScrollLock = NEUTRAL;
g_BlockInputMode = TOGGLE_DEFAULT;
g_BlockInput = false;
g_BlockMouseMove = false;
#endif
#ifndef MINIDLL
g_KeyHistoryNext = 0;
#ifdef ENABLE_KEY_HISTORY_FILE
g_KeyHistoryToFile = false;
#endif
g_HistoryTickNow = 0;
g_HistoryTickPrev = GetTickCount(); // So that the first logged key doesn't have a huge elapsed time.
g_HistoryHwndPrev = NULL;
#endif
g_DefaultScriptCodepage = CP_ACP;
g_DestroyWindowCalled = false;
g_hWnd = NULL;
g_hWndEdit = NULL;
g_hFontEdit = NULL;
#ifndef MINIDLL
g_hWndSplash = NULL;
g_hFontSplash = NULL; // So that font can be deleted on program close.
#endif
g_StrCmpLogicalW = NULL;
g_TabClassProc = NULL;
g_modifiersLR_logical = 0;
g_modifiersLR_logical_non_ignored = 0;
g_modifiersLR_physical = 0;
#ifdef FUTURE_USE_MOUSE_BUTTONS_LOGICAL
g_mouse_buttons_logical = 0;
#endif
g_BlockWinKeys = false;
g_HookReceiptOfLControlMeansAltGr = 0; // In these cases, zero is used as a false value, any others are true.
g_IgnoreNextLControlDown = 0; //
g_IgnoreNextLControlUp = 0; //
g_MenuMaskKeySC = SC_LCONTROL; // L38: See #MenuMaskKey.
g_MenuMaskKeyVK = VK_CONTROL; // L38: See #MenuMaskKey.
g_HotkeyModifierTimeout = 50; // Reduced from 100, which was a little too large for fast typists.
g_ClipboardTimeout = 1000; // v1.0.31
g_KeybdHook = NULL;
g_MouseHook = NULL;
g_PlaybackHook = NULL;
g_ForceLaunch = false;
g_WinActivateForce = false;
g_Warn_UseUnsetLocal = WARNMODE_OFF;
g_Warn_UseUnsetGlobal = WARNMODE_OFF;
g_Warn_UseEnv = WARNMODE_OFF;
g_Warn_LocalSameAsGlobal = WARNMODE_OFF;
#ifndef _USRDLL
g_AllowOnlyOneInstance = ALLOW_MULTI_INSTANCE;
#endif
g_persistent = false; // Whether the script should stay running even after the auto-exec section finishes.
g_WriteCacheDisabledInt64 = FALSE; // BOOL vs. bool might improve performance a little for
g_WriteCacheDisabledDouble = FALSE; // frequently-accessed variables (it has helped performance in
g_NoEnv = TRUE; // HotKeyIt H5 new default
// g_MaxVarCapacity is used to prevent a buggy script from consuming all available system RAM. It is defined =
g_MaxVarCapacity = 64 * 1024 * 1024;
#ifndef MINIDLL
//g_ScreenDPI = GetScreenDPI();
//HDC hdc = GetDC(NULL);
//g_ScreenDPI = GetDeviceCaps(hdc, LOGPIXELSX);
//ReleaseDC(NULL, hdc);
g_guiCount = 0;
#endif
g_delimiter = ',';
g_DerefChar = '%';
g_EscapeChar = '`';
g_ContinuationLTrim = false;
for(i=1;Line::sSourceFileCount>i;i++) // first include file must not be deleted
free(Line::sSourceFile[i]);
free(Line::sSourceFile);
Line::sSourceFile = NULL;
Line::sSourceFileCount = 0;
Line::sMaxSourceFiles = 0;
//Line::sMaxSourceFiles = 0;
//SimpleHeap::Delete(Line::sSourceFile);
//Line::sSourceFile = 0;
// free(Line::sSourceFile);
#ifndef MINIDLL
// AddRemoveHooks(0); // done in ~Script
Hotkey::AllDestruct();
Hotstring::AllDestruct();
#endif
global_clear_state(*g);
//free(g_Debugger.mStack.mBottom);
#ifndef MINIDLL
free(g_input.match);
#endif
Line::sLogNext = 0;
g_memset(Line::sLog,NULL,sizeof(Line*) * LINE_LOG_SIZE);
SimpleHeap::DeleteAll();
//ZeroMemory(&g_script, sizeof(g_script));
#ifndef MINIDLL
mPriorHotkeyName = mThisHotkeyName = _T("");
#endif
free_compiled_regex();
#ifndef MINIDLL
#ifdef ENABLE_KEY_HISTORY_FILE
KeyHistoryToFile(); // Close the KeyHistory file if it's open.
#endif
#endif // MINIDLL
RemoveVectoredExceptionHandler(g_ExceptionHandler); // Exception handler to remove hooks to avoid system/mouse freeze
// done on DLL_PROCESS_DETACH
// DeleteCriticalSection(&g_CriticalRegExCache); // g_CriticalRegExCache is used elsewhere for thread-safety.
// DeleteCriticalSection(&g_CriticalAhkFunction); // used to call a function in multithreading environment.
// PeekMessage is required to make sure that OleUninitialize does not hang
MSG msg;
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
OleUninitialize();
mIsReadyToExecute = false;
}
#endif
ResultType Script::Init(global_struct &g, LPTSTR aScriptFilename, bool aIsRestart, HINSTANCE hInstance, bool aIsText)
// Returns OK or FAIL.
// Caller has provided an empty string for aScriptFilename if this is a compiled script.
// Otherwise, aScriptFilename can be NULL if caller hasn't determined the filename of the script yet.
{
mIsRestart = aIsRestart;
TCHAR buf[2048]; // Just to make sure we have plenty of room to do things with.
#ifdef AUTOHOTKEYSC
// Fix for v1.0.29: Override the caller's use of __argv[0] by using GetModuleFileName(),
// so that when the script is started from the command line but the user didn't type the
// extension, the extension will be included. This necessary because otherwise
// #SingleInstance wouldn't be able to detect duplicate versions in every case.
// It also provides more consistency.
GetModuleFileName(NULL, buf, _countof(buf));
#else
TCHAR def_buf[MAX_PATH + 1], exe_buf[MAX_PATH + 20]; // For simplicity, allow at least space for +2 (see below) and "AutoHotkey.chm".
if (!aScriptFilename) // v1.0.46.08: Change in policy: store the default script in the My Documents directory rather than in Program Files. It's more correct and solves issues that occur due to Vista's file-protection scheme.
{
// Since no script-file was specified on the command line, use the default name.
// For portability, first check if there's an <EXENAME>.ahk file in the current directory.
LPTSTR suffix, dot;
DWORD exe_len = GetModuleFileName(NULL, exe_buf, MAX_PATH + 2);
// exe_len can be MAX_PATH+2 on Windows XP, in which case it is not null-terminated.
// MAX_PATH+1 could mean it was truncated. Any path longer than MAX_PATH would be rare.
if (exe_len > MAX_PATH)
return FAIL; // Seems the safest option for this unlikely case.
if ( (suffix = _tcsrchr(exe_buf, '\\')) // Find name part of path.
&& (dot = _tcsrchr(suffix, '.')) ) // Find extension part of name.
// Even if the extension is somehow zero characters, more than enough space was
// reserved in exe_buf to add "ahk":
//&& dot - exe_buf + 5 < _countof(exe_buf) ) // Enough space in buffer?
{
_tcscpy(dot, EXT_AUTOHOTKEY);
}
else // Very unlikely.
return FAIL;
aScriptFilename = exe_buf; // Use the entire path, including the exe's directory.
if (!g_hResource && GetFileAttributes(aScriptFilename) == 0xFFFFFFFF) // File doesn't exist, so fall back to new method.
{
aScriptFilename = def_buf;
VarSizeType filespec_length = BIV_MyDocuments(aScriptFilename, _T("")); // e.g. C:\Documents and Settings\Home\My Documents
if (filespec_length + _tcslen(suffix) + 1 > _countof(def_buf))
return FAIL; // Very rare, so for simplicity just abort.
_tcscpy(aScriptFilename + filespec_length, suffix); // Append the filename: .ahk vs. .ini seems slightly better in terms of clarity and usefulness (e.g. the ability to double click the default script to launch it).
if (GetFileAttributes(aScriptFilename) == 0xFFFFFFFF)
{
_tcscpy(suffix, _T("\\") AHK_HELP_FILE); // Replace the executable name.
if (GetFileAttributes(exe_buf) != 0xFFFFFFFF) // Avoids hh.exe showing an error message if the file doesn't exist.
{
_sntprintf(buf, _countof(buf), _T("\"ms-its:%s::/docs/Welcome.htm\""), exe_buf);
if (ActionExec(_T("hh.exe"), buf, exe_buf, false, _T("Max")))
return FAIL;
}
// Since above didn't return, the help file is missing or failed to launch,
// so continue on and let the missing script file be reported as an error.
}
}
//else since the file exists, everything is now set up right. (The file might be a directory, but that isn't checked due to rarity.)
}
// In case the script is a relative filespec (relative to current working dir):
if (g_hResource || (hInstance != NULL && aIsText)) //It is a dll and script was given as text rather than file
{
if (!GetModuleFileName(hInstance, buf, _countof(buf))) //Get dll path in front to make sure we have a valid path anyway
GetModuleFileName(NULL, buf, _countof(buf)); //due to MemoryLoadLibrary dll path might be empty
PROCESS_BASIC_INFORMATION pbi;
ULONG ReturnLength;
HANDLE hProcess = OpenProcess (PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, GetCurrentProcessId());
PFN_NT_QUERY_INFORMATION_PROCESS pfnNtQueryInformationProcess =
(PFN_NT_QUERY_INFORMATION_PROCESS) GetProcAddress (
GetModuleHandle(_T("ntdll.dll")), "NtQueryInformationProcess");
NTSTATUS status = pfnNtQueryInformationProcess (
hProcess, ProcessBasicInformation,
(PVOID)&pbi, sizeof(pbi), &ReturnLength);
if (pbi.PebBaseAddress->ProcessParameters->CommandLine.Length) // && ReadProcessMemory(hProcess, &pbi.PebBaseAddress->ProcessParameters->CommandLine.Buffer,
//&commandLineContents, CommanLineLength, NULL))
{
int dllargc = 0;
TCHAR *param;
#ifndef _UNICODE
LPWSTR wargv = (LPWSTR)_malloca(pbi.PebBaseAddress->ProcessParameters->CommandLine.Length);
#endif
LPWSTR *dllargv = CommandLineToArgvW(pbi.PebBaseAddress->ProcessParameters->CommandLine.Buffer,&dllargc);
if (dllargc > 1 && pbi.PebBaseAddress->ProcessParameters->CommandLine.Length) // Only process if parameters were given
{
for (int i = 1; i < dllargc; ++i) // Start at 1 because 0 contains the program name.
{
#ifndef _UNICODE
param = (TCHAR*) _malloca((wcslen(dllargv[i]) + 1)*sizeof(CHAR));
WideCharToMultiByte(CP_ACP,0,wargv,-1,param,(wcslen(dllargv[i])+1)*sizeof(CHAR),0,0);
#else
param = dllargv[i]; // For performance and convenience.
#endif
if (!_tcsncmp(param, _T("/"),1) || !_tcsncmp(param, _T("-"),1))
continue;
else // since this is not a switch, the end of the [Switches] section has been reached (by design).
{
if (GetFileAttributes(param) == 0xFFFFFFFF)
{
if (!GetModuleFileName(hInstance, buf, _countof(buf))) //Get dll path
GetModuleFileName(NULL, buf, _countof(buf)); //due to MemoryLoadLibrary dll path might be empty
}
else
{
if (!GetFullPathName(param, _countof(buf), buf, NULL)) // This is also relied upon by mIncludeLibraryFunctionsThenExit. Succeeds even on nonexistent files.
return FAIL;
}
break; // No more switches allowed after this point.
}
#ifndef _UNICODE
_freea(param);
#endif
}
}
LocalFree(dllargv);
#ifndef _UNICODE
_freea(wargv);
#endif
}
CloseHandle(hProcess);
}
else if (!GetFullPathName(aScriptFilename, _countof(buf), buf, NULL)) // This is also relied upon by mIncludeLibraryFunctionsThenExit. Succeeds even on nonexistent files.
return FAIL; // Due to rarity, no error msg, just abort.
#endif
if (g_RunStdIn = (*aScriptFilename == '*' && !aScriptFilename[1])) // v1.1.17: Read script from stdin.
{
#ifndef _USRDLL
// Seems best to disable #SingleInstance and enable #NoEnv for stdin scripts.
g_AllowOnlyOneInstance = SINGLE_INSTANCE_OFF;
#endif
g_NoEnv = true;
}
else // i.e. don't call the following function for stdin.
// Using the correct case not only makes it look better in title bar & tray tool tip,
// it also helps with the detection of "this script already running" since otherwise
// it might not find the dupe if the same script name is launched with different
// lowercase/uppercase letters:
ConvertFilespecToCorrectCase(buf); // This might change the length, e.g. due to expansion of 8.3 filename.
if ( !(mFileSpec = SimpleHeap::Malloc(buf)) ) // The full spec is stored for convenience, and it's relied upon by mIncludeLibraryFunctionsThenExit.
return FAIL; // It already displayed the error for us.
LPTSTR filename_marker;
if (filename_marker = _tcsrchr(buf, '\\'))
{
*filename_marker = '\0'; // Terminate buf in this position to divide the string.
if ( !(mFileDir = SimpleHeap::Malloc(buf)) )
return FAIL; // It already displayed the error for us.
++filename_marker;
}
else
{
// The only known cause of this condition is a path being too long for GetFullPathName
// to expand it into buf, in which case buf and mFileSpec are now empty, and this will
// cause LoadFromFile() to fail and the program to exit.
//mFileDir = _T(""); // Already done by the constructor.
filename_marker = buf;
}
if ( !(mFileName = SimpleHeap::Malloc(filename_marker)) )
return FAIL; // It already displayed the error for us.
#ifdef AUTOHOTKEYSC
// Omit AutoHotkey from the window title, like AutoIt3 does for its compiled scripts.
// One reason for this is to reduce backlash if evil-doers create viruses and such
// with the program:
sntprintf(buf, _countof(buf), _T("%s\\%s"), mFileDir, mFileName);
#else
sntprintf(buf, _countof(buf), _T("%s\\%s - %s"), mFileDir, mFileName, T_AHK_NAME_VERSION);
#endif
if ( !(mMainWindowTitle = SimpleHeap::Malloc(buf)) )
return FAIL; // It already displayed the error for us.
// It may be better to get the module name this way rather than reading it from the registry
// (though it might be more proper to parse it out of the command line args or something),
// in case the user has moved it to a folder other than the install folder, hasn't installed it,
// or has renamed the EXE file itself. Also, enclose the full filespec of the module in double
// quotes since that's how callers usually want it because ActionExec() currently needs it that way:
*buf = '"';
if (GetModuleFileName(NULL, buf + 1, _countof(buf) - 2)) // -2 to leave room for the enclosing double quotes.
{
size_t buf_length = _tcslen(buf);
buf[buf_length++] = '"';
buf[buf_length] = '\0';
if ( !(mOurEXE = SimpleHeap::Malloc(buf)) )
return FAIL; // It already displayed the error for us.
else
{
LPTSTR last_backslash = _tcsrchr(buf, '\\');
if (!last_backslash) // probably can't happen due to the nature of GetModuleFileName().
mOurEXEDir = _T("");
*last_backslash = '\0';
if ( !(mOurEXEDir = SimpleHeap::Malloc(buf + 1)) ) // +1 to omit the leading double-quote.
return FAIL; // It already displayed the error for us.
}
}
return OK;
}
ResultType Script::CreateWindows()
// Returns OK or FAIL.
{
if (!mMainWindowTitle || !*mMainWindowTitle) return FAIL; // Init() must be called before this function.
// Register a window class for the main window:
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(wc);
wc.lpszClassName = WINDOW_CLASS_MAIN;
wc.hInstance = g_hInstance;
wc.lpfnWndProc = MainWindowProc;
// The following are left at the default of NULL/0 set higher above:
//wc.style = 0; // CS_HREDRAW | CS_VREDRAW
//wc.cbClsExtra = 0;
//wc.cbWndExtra = 0;
#ifndef MINIDLL
// Load the main icon in the two sizes needed throughout the program:
g_IconLarge = ExtractIconFromExecutable(NULL, -IDI_MAIN, 0, 0);
g_IconSmall = ExtractIconFromExecutable(NULL, -IDI_MAIN, GetSystemMetrics(SM_CXSMICON), 0);
wc.hIcon = g_IconLarge;
wc.hIconSm = g_IconSmall;
wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1); // Needed for ProgressBar. Old: (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU_MAIN); // NULL; // "MainMenu";
#endif
if (!(g_ClassRegistered = RegisterClassEx(&wc)))
{
MsgBox(_T("RegClass")); // Short/generic msg since so rare.
return FAIL;
}
// Register a second class for the splash window. The only difference is that
// it doesn't have the menu bar:
#ifndef MINIDLL
wc.lpszClassName = WINDOW_CLASS_SPLASH;
wc.lpszMenuName = NULL; // Override the non-NULL value set higher above.
if (!(g_ClassSplashRegistered = RegisterClassEx(&wc)))
{
MsgBox(_T("RegClass")); // Short/generic msg since so rare.
return FAIL;
}
#endif // MINIDLL
TCHAR class_name[64];
HWND fore_win = GetForegroundWindow();
bool do_minimize = !fore_win || (GetClassName(fore_win, class_name, _countof(class_name))
&& !_tcsicmp(class_name, _T("Shell_TrayWnd"))); // Shell_TrayWnd is the taskbar's class on Win98/XP and probably the others too.
// Note: the title below must be constructed the same was as is done by our
// WinMain() (so that we can detect whether this script is already running)
// which is why it's standardized in g_script.mMainWindowTitle.
// Create the main window. Prevent momentary disruption of Start Menu, which
// some users understandably don't like, by omitting the taskbar button temporarily.
// This is done because testing shows that minimizing the window further below, even
// though the window is hidden, would otherwise briefly show the taskbar button (or
// at least redraw the taskbar). Sometimes this isn't noticeable, but other times
// (such as when the system is under heavy load) a user reported that it is quite
// noticeable. WS_EX_TOOLWINDOW is used instead of WS_EX_NOACTIVATE because
// WS_EX_NOACTIVATE is available only on 2000/XP.
if ( !(g_hWnd = CreateWindowEx(do_minimize ? WS_EX_TOOLWINDOW : 0
, WINDOW_CLASS_MAIN
, mMainWindowTitle
, WS_OVERLAPPEDWINDOW // Style. Alt: WS_POPUP or maybe 0.
, CW_USEDEFAULT // xpos
, CW_USEDEFAULT // ypos
, CW_USEDEFAULT // width
, CW_USEDEFAULT // height
, NULL // parent window
, NULL // Identifies a menu, or specifies a child-window identifier depending on the window style
, g_hInstance // passed into WinMain
, NULL)) ) // lpParam
{
MsgBox(_T("CreateWindow")); // Short msg since so rare.
return FAIL;
}
#ifdef AUTOHOTKEYSC
HMENU menu = GetMenu(g_hWnd);
// Disable the Edit menu item, since it does nothing for a compiled script:
EnableMenuItem(menu, ID_FILE_EDITSCRIPT, MF_DISABLED | MF_GRAYED);
EnableOrDisableViewMenuItems(menu, MF_DISABLED | MF_GRAYED); // Fix for v1.0.47.06: No point in checking g_AllowMainWindow because the script hasn't starting running yet, so it will always be false.
// But leave the ID_VIEW_REFRESH menu item enabled because if the script contains a
// command such as ListLines in it, Refresh can be validly used.
#endif
if ( !(g_hWndEdit = CreateWindow(_T("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER
| ES_LEFT | ES_MULTILINE | ES_READONLY | WS_VSCROLL // | WS_HSCROLL (saves space)
, 0, 0, 0, 0, g_hWnd, (HMENU)1, g_hInstance, NULL)) )
{
MsgBox(_T("CreateWindow")); // Short msg since so rare.
return FAIL;
}
// FONTS: The font used by default, at least on XP, is GetStockObject(SYSTEM_FONT).
// It seems preferable to smaller fonts such DEFAULT_GUI_FONT(DEFAULT_GUI_FONT).
// For more info on pre-loaded fonts (not too many choices), see MSDN's GetStockObject().
if(g_os.IsWinNT())
{
// Use a more appealing font on NT versions of Windows.
// Windows NT to Windows XP -> Lucida Console
HDC hdc = GetDC(g_hWndEdit);
if(!g_os.IsWinVistaOrLater())
g_hFontEdit = CreateFont(FONT_POINT(hdc, 10), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("Lucida Console"));
else // Windows Vista and later -> Consolas
g_hFontEdit = CreateFont(FONT_POINT(hdc, 10), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, _T("Consolas"));
ReleaseDC(g_hWndEdit, hdc); // In theory it must be done.
SendMessage(g_hWndEdit, WM_SETFONT, (WPARAM)g_hFontEdit, 0);
}
// v1.0.30.05: Specifying a limit of zero opens the control to its maximum text capacity,
// which removes the 32K size restriction. Testing shows that this does not increase the actual
// amount of memory used for controls containing small amounts of text. All it does is allow
// the control to allocate more memory as needed. By specifying zero, a max
// of 64K becomes available on Windows 9x, and perhaps as much as 4 GB on NT/2k/XP.
SendMessage(g_hWndEdit, EM_LIMITTEXT, 0, 0);
// Some of the MSDN docs mention that an app's very first call to ShowWindow() makes that
// function operate in a special mode. Therefore, it seems best to get that first call out
// of the way to avoid the possibility that the first-call behavior will cause problems with
// our normal use of ShowWindow() below and other places. Also, decided to ignore nCmdShow,
// to avoid any momentary visual effects on startup.
// Update: It's done a second time because the main window might now be visible if the process
// that launched ours specified that. It seems best to override the requested state because
// some calling processes might specify "maximize" or "shownormal" as generic launch method.
// The script can display it's own main window with ListLines, etc.
// MSDN: "the nCmdShow value is ignored in the first call to ShowWindow if the program that
// launched the application specifies startup information in the structure. In this case,
// ShowWindow uses the information specified in the STARTUPINFO structure to show the window.
// On subsequent calls, the application must call ShowWindow with nCmdShow set to SW_SHOWDEFAULT
// to use the startup information provided by the program that launched the application."
ShowWindow(g_hWnd, SW_HIDE);
ShowWindow(g_hWnd, SW_HIDE);
// Now that the first call to ShowWindow() is out of the way, minimize the main window so that
// if the script is launched from the Start Menu (and perhaps other places such as the
// Quick-launch toolbar), the window that was active before the Start Menu was displayed will
// become active again. But as of v1.0.25.09, this minimize is done more selectively to prevent
// the launch of a script from knocking the user out of a full-screen game or other application
// that would be disrupted by an SW_MINIMIZE:
if (do_minimize)
{
ShowWindow(g_hWnd, SW_MINIMIZE);
SetWindowLong(g_hWnd, GWL_EXSTYLE, 0); // Give the main window back its taskbar button.
}
// Note: When the window is not minimized, task manager reports that a simple script (such as
// one consisting only of the single line "#Persistent") uses 2600 KB of memory vs. ~452 KB if
// it were immediately minimized. That is probably just due to the vagaries of how the OS
// manages windows and memory and probably doesn't actually impact system performance to the
// degree indicated. In other words, it's hard to imagine that the failure to do
// ShowWidnow(g_hWnd, SW_MINIMIZE) unconditionally upon startup (which causes the side effects
// discussed further above) significantly increases the actual memory load on the system.
g_hAccelTable = LoadAccelerators(g_hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
#ifndef MINIDLL
if (g_NoTrayIcon)
#endif
mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
#ifndef MINIDLL
else
// Even if the below fails, don't return FAIL in case the user is using a different shell
// or something. In other words, it is expected to fail under certain circumstances and
// we want to tolerate that:
CreateTrayIcon();
#endif
if (mOnClipboardChangeLabel)
EnableClipboardListener(true);
return OK;
}
void Script::EnableClipboardListener(bool aEnable)
{
static bool sEnabled = false;
if (aEnable == sEnabled) // Simplifies BIF_On.
return;
if (aEnable)
{
if (MyAddClipboardListener && MyRemoveClipboardListener) // Should be impossible for only one of these to be NULL, but check both anyway to be safe.
{
// The old clipboard viewer chain method is prone to break when some other application uses
// it incorrectly. This newer method should be more reliable, but requires Vista or later:
MyAddClipboardListener(g_hWnd);
// But this method doesn't appear to send an initial WM_CLIPBOARDUPDATE message.
// For consistency with the other method (below) and for backward compatibility,
// run the OnClipboardChange label once when the script first starts:
if (!mIsReadyToExecute)
{
// Pass 1 for wParam so that MsgSleep() will call only the legacy OnClipboardChange label,
// not any functions which are registered between now and when the message is handled.
PostMessage(g_hWnd, AHK_CLIPBOARD_CHANGE, 1, 0);
}
}
else
{
mNextClipboardViewer = SetClipboardViewer(g_hWnd);
// SetClipboardViewer() sends a WM_DRAWCLIPBOARD message and causes MainWindowProc()
// to be called before returning. MainWindowProc() posts an AHK_CLIPBOARD_CHANGE
// message only if an OnClipboardChange label exists, since mOnClipboardChange.Count()
// is always 0 at this point. It also uses wParam for the reason described above.
}
}
else
{
if (MyRemoveClipboardListener && MyAddClipboardListener)
MyRemoveClipboardListener(g_hWnd); // MyAddClipboardListener was used.
else
ChangeClipboardChain(g_hWnd, mNextClipboardViewer); // SetClipboardViewer was used.
}
sEnabled = aEnable;
}
#ifndef MINIDLL
void Script::EnableOrDisableViewMenuItems(HMENU aMenu, UINT aFlags)
{
EnableMenuItem(aMenu, ID_VIEW_KEYHISTORY, aFlags);
EnableMenuItem(aMenu, ID_VIEW_LINES, aFlags);
EnableMenuItem(aMenu, ID_VIEW_VARIABLES, aFlags);
EnableMenuItem(aMenu, ID_VIEW_HOTKEYS, aFlags);
}
void Script::CreateTrayIcon()
// It is the caller's responsibility to ensure that the previous icon is first freed/destroyed
// before calling us to install a new one. However, that is probably not needed if the Explorer
// crashed, since the memory used by the tray icon was probably destroyed along with it.
{
ZeroMemory(&mNIC, sizeof(mNIC)); // To be safe.
// Using NOTIFYICONDATA_V2_SIZE vs. sizeof(NOTIFYICONDATA) improves compatibility with Win9x maybe.
// MSDN: "Using [NOTIFYICONDATA_V2_SIZE] for cbSize will allow your application to use NOTIFYICONDATA
// with earlier Shell32.dll versions, although without the version 6.0 enhancements."
// Update: Using V2 gives an compile error so trying V1. Update: Trying sizeof(NOTIFYICONDATA)
// for compatibility with VC++ 6.x. This is also what AutoIt3 uses:
mNIC.cbSize = sizeof(NOTIFYICONDATA); // NOTIFYICONDATA_V1_SIZE
mNIC.hWnd = g_hWnd;
mNIC.uID = AHK_NOTIFYICON; // This is also used for the ID, see TRANSLATE_AHK_MSG for details.
mNIC.uFlags = NIF_MESSAGE | NIF_TIP | NIF_ICON;
mNIC.uCallbackMessage = AHK_NOTIFYICON;
mNIC.hIcon = mCustomIconSmall ? mCustomIconSmall : g_IconSmall;
UPDATE_TIP_FIELD
// If we were called due to an Explorer crash, I don't think it's necessary to call
// Shell_NotifyIcon() to remove the old tray icon because it was likely destroyed
// along with Explorer. So just add it unconditionally:
if (!Shell_NotifyIcon(NIM_ADD, &mNIC))
mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
}
void Script::UpdateTrayIcon(bool aForceUpdate)
{
if (!mNIC.hWnd) // tray icon is not installed
return;
static bool icon_shows_paused = false;
static bool icon_shows_suspended = false;
if (!aForceUpdate && (mIconFrozen || (g->IsPaused == icon_shows_paused && g_IsSuspended == icon_shows_suspended)))
return; // it's already in the right state
int icon;
if (g->IsPaused && g_IsSuspended)
icon = IDI_PAUSE_SUSPEND;
else if (g->IsPaused)
icon = IDI_PAUSE;
else if (g_IsSuspended)
icon = IDI_SUSPEND;
else
icon = IDI_MAIN;
// Use the custom tray icon if the icon is normal (non-paused & non-suspended):
mNIC.hIcon = (mCustomIconSmall && (mIconFrozen || (!g->IsPaused && !g_IsSuspended))) ? mCustomIconSmall // L17: Always use small icon for tray.
: (icon == IDI_MAIN) ? g_IconSmall // Use the pre-loaded small icon for best quality.
: (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(icon), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED for simplicity and performance more than to conserve memory in this case.
if (Shell_NotifyIcon(NIM_MODIFY, &mNIC))
{
icon_shows_paused = g->IsPaused;
icon_shows_suspended = g_IsSuspended;
}
// else do nothing, just leave it in the same state.
}
#endif
ResultType Script::AutoExecSection()
// Returns FAIL if can't run due to critical error. Otherwise returns OK.
{
// Now that g_MaxThreadsTotal has been permanently set by the processing of script directives like
// #MaxThreads, an appropriately sized array can be allocated:
if (!(g_array = (global_struct *)realloc(g_array,(g_MaxThreadsTotal + TOTAL_ADDITIONAL_THREADS) * sizeof(global_struct))))
return FAIL; // Due to rarity, just abort. It wouldn't be safe to run ExitApp() due to possibility of an OnExit routine.
CopyMemory(g_array, g, sizeof(global_struct)); // Copy the temporary/startup "g" into array[0] to preserve historical behaviors that may rely on the idle thread starting with that "g".
g = g_array; // Must be done after above.
// v1.0.48: Due to switching from SET_UNINTERRUPTIBLE_TIMER to IsInterruptible():
// In spite of the comments in IsInterruptible(), periodically have a timer call IsInterruptible() due to
// the following scenario:
// - Interrupt timeout is 60 seconds (or 60 milliseconds for that matter).
// - For some reason IsInterrupt() isn't called for 24+ hours even though there is a current/active thread.
// - RefreshInterruptibility() fires at 23 hours and marks the thread interruptible.
// - Sometime after that, one of the following happens:
// Computer is suspended/hibernated and stays that way for 50+ days.
// IsInterrupt() is never called (except by RefreshInterruptibility()) for 50+ days.
// (above is currently unlikely because MSG_FILTER_MAX calls IsInterruptible())
// In either case, RefreshInterruptibility() has prevented the uninterruptibility duration from being
// wrongly extended by up to 100% of g_script.mUninterruptibleTime. This isn't a big deal if
// g_script.mUninterruptibleTime is low (like it almost always is); but if it's fairly large, say an hour,
// this can prevent an unwanted extension of up to 1 hour.
// Although any call frequency less than 49.7 days should work, currently calling once per 23 hours
// in case any older operating systems have a SetTimer() limit of less than 0x7FFFFFFF (and also to make
// it less likely that a long suspend/hibernate would cause the above issue). The following was
// actually tested on Windows XP and a message does indeed arrive 23 hours after the script starts.
SetTimer(g_hWnd, TIMER_ID_REFRESH_INTERRUPTIBILITY, 23*60*60*1000, RefreshInterruptibility); // 3rd param must not exceed 0x7FFFFFFF (2147483647; 24.8 days).
ResultType ExecUntil_result;
if (!mFirstLine) // In case it's ever possible to be empty.
ExecUntil_result = OK;
// And continue on to do normal exit routine so that the right ExitCode is returned by the program.
else
{
// Choose a timeout that's a reasonable compromise between the following competing priorities:
// 1) That we want hotkeys to be responsive as soon as possible after the program launches
// in case the user launches by pressing ENTER on a script, for example, and then immediately
// tries to use a hotkey. In addition, we want any timed subroutines to start running ASAP
// because in rare cases the user might rely upon that happening.
// 2) To support the case when the auto-execute section never finishes (such as when it contains
// an infinite loop to do background processing), yet we still want to allow the script
// to put custom defaults into effect globally (for things such as KeyDelay).
// Obviously, the above approach has its flaws; there are ways to construct a script that would
// result in unexpected behavior. However, the combination of this approach with the fact that
// the global defaults are updated *again* when/if the auto-execute section finally completes
// raises the expectation of proper behavior to a very high level. In any case, I'm not sure there
// is any better approach that wouldn't break existing scripts or require a redesign of some kind.
// If this method proves unreliable due to disk activity slowing the program down to a crawl during
// the critical milliseconds after launch, one thing that might fix that is to have ExecUntil()
// be forced to run a minimum of, say, 100 lines (if there are that many) before allowing the
// timer expiration to have its effect. But that's getting complicated and I'd rather not do it
// unless someone actually reports that such a thing ever happens. Still, to reduce the chance
// of such a thing ever happening, it seems best to boost the timeout from 50 up to 100:
SET_AUTOEXEC_TIMER(100);
mAutoExecSectionIsRunning = true;
// v1.0.25: This is now done here, closer to the actual execution of the first line in the script,
// to avoid an unnecessary Sleep(10) that would otherwise occur in ExecUntil:
mLastScriptRest = mLastPeekTime = GetTickCount();
++g_nThreads;
DEBUGGER_STACK_PUSH(_T("Auto-execute"))
ExecUntil_result = mFirstLine->ExecUntil(UNTIL_RETURN); // Might never return (e.g. infinite loop or ExitApp).
DEBUGGER_STACK_POP()
--g_nThreads;
// Our caller will take care of setting g_default properly.
KILL_AUTOEXEC_TIMER // See also: AutoExecSectionTimeout().
mAutoExecSectionIsRunning = false;
}
// REMEMBER: The ExecUntil() call above will never return if the AutoExec section never finishes
// (e.g. infinite loop) or it uses Exit/ExitApp.
// Check if an exception has been thrown
if (g->ThrownToken)
g_script.FreeExceptionToken(g->ThrownToken);
// The below is done even if AutoExecSectionTimeout() already set the values once.
// This is because when the AutoExecute section finally does finish, by definition it's
// supposed to store the global settings that are currently in effect as the default values.
// In other words, the only purpose of AutoExecSectionTimeout() is to handle cases where
// the AutoExecute section takes a long time to complete, or never completes (perhaps because
// it is being used by the script as a "background thread" of sorts):
// Save the values of KeyDelay, WinDelay etc. in case they were changed by the auto-execute part
// of the script. These new defaults will be put into effect whenever a new hotkey subroutine
// is launched. Each launched subroutine may then change the values for its own purposes without
// affecting the settings for other subroutines:
global_clear_state(*g); // Start with a "clean slate" in both g_default and g (in case things like InitNewThread() check some of the values in g prior to launching a new thread).
// Always want g_default.AllowInterruption==true so that InitNewThread() doesn't have to
// set it except when Critical or "Thread Interrupt" require it. If the auto-execute section ended
// without anyone needing to call IsInterruptible() on it, AllowInterruption could be false
// even when Critical is off.
// Even if the still-running AutoExec section has turned on Critical, the assignment below is still okay
// because InitNewThread() adjusts AllowInterruption based on the value of ThreadIsCritical.
// See similar code in AutoExecSectionTimeout().
g->AllowThreadToBeInterrupted = true; // Mostly for the g_default line below. See comments above.
CopyMemory(&g_default, g, sizeof(global_struct)); // g->IsPaused has been set to false higher above in case it's ever possible that it's true as a result of AutoExecSection().
// After this point, the values in g_default should never be changed.
global_maximize_interruptibility(*g); // See below.
// Now that any changes made by the AutoExec section have been saved to g_default (including
// the commands Critical and Thread), ensure that the very first g-item is always interruptible.
// This avoids having to treat the first g-item as special in various places.
// It seems best to set ErrorLevel to NONE after the auto-execute part of the script is done.
// However, it isn't set to NONE right before launching each new thread (e.g. hotkey subroutine)
// because it's more flexible that way (i.e. the user may want one hotkey subroutine to use the value
// of ErrorLevel set by another). This reset was also done by LoadFromFile(), but it is done again
// here in case the auto-execute section changed it:
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
// BEFORE DOING THE BELOW, "g" and "g_default" should be set up properly in case there's an OnExit
// routine (even non-persistent scripts can have one).
// If no hotkeys are in effect, the user hasn't requested a hook to be activated, and the script
// doesn't contain the #Persistent directive we're done unless there is an OnExit subroutine and it
// doesn't do "ExitApp":
if (!IS_PERSISTENT) // Resolve macro again in case any of its components changed since the last time.
g_script.ExitApp(ExecUntil_result == FAIL ? EXIT_ERROR : EXIT_EXIT);
return OK;
}
#ifndef MINIDLL
ResultType Script::Edit()
{
#ifdef AUTOHOTKEYSC
return OK; // Do nothing.
#else
// This is here in case a compiled script ever uses the Edit command. Since the "Edit This
// Script" menu item is not available for compiled scripts, it can't be called from there.
TitleMatchModes old_mode = g->TitleMatchMode;
g->TitleMatchMode = FIND_ANYWHERE;
HWND hwnd = WinExist(*g, mFileName, _T(""), mMainWindowTitle, _T("")); // Exclude our own main window.
g->TitleMatchMode = old_mode;
if (hwnd)
{
TCHAR class_name[32];
GetClassName(hwnd, class_name, _countof(class_name));
if (!_tcscmp(class_name, _T("#32770")) || !_tcsnicmp(class_name, _T("AutoHotkey"), 10)) // MessageBox(), InputBox(), FileSelectFile(), or GUI/script-owned window.
hwnd = NULL; // Exclude it from consideration.
}
if (hwnd) // File appears to already be open for editing, so use the current window.
SetForegroundWindowEx(hwnd);
else
{
TCHAR buf[MAX_PATH * 2];
// Enclose in double quotes anything that might contain spaces since the CreateProcess()
// method, which is attempted first, is more likely to succeed. This is because it uses
// the command line method of creating the process, with everything all lumped together:
sntprintf(buf, _countof(buf), _T("\"%s\""), mFileSpec);
if (!ActionExec(_T("edit"), buf, mFileDir, false)) // Since this didn't work, try notepad.
{
// v1.0.40.06: Try to open .ini files first with their associated editor rather than trying the
// "edit" verb on them:
LPTSTR file_ext;
if ( !(file_ext = _tcsrchr(mFileName, '.')) || _tcsicmp(file_ext, _T(".ini"))
|| !ActionExec(_T("open"), buf, mFileDir, false) ) // Relies on short-circuit boolean order.
{
// Even though notepad properly handles filenames with spaces in them under WinXP,
// even without double quotes around them, it seems safer and more correct to always
// enclose the filename in double quotes for maximum compatibility with all OSes:
if (!ActionExec(_T("notepad.exe"), buf, mFileDir, false))
MsgBox(_T("Could not open script.")); // Short message since so rare.
}
}
}
return OK;
#endif
}
#endif // MINIDLL
ResultType Script::Reload(bool aDisplayErrors)
{
// The new instance we're about to start will tell our process to stop, or it will display
// a syntax error or some other error, in which case our process will still be running:
#ifdef _USRDLL
g_Reloading = true;
//ExitApp(EXIT_RELOAD);
reloadDll();
return EARLY_RETURN;
#else
#ifdef AUTOHOTKEYSC
// This is here in case a compiled script ever uses the Reload command. Since the "Reload This
// Script" menu item is not available for compiled scripts, it can't be called from there.
return g_script.ActionExec(mOurEXE, _T("/restart"), g_WorkingDirOrig, aDisplayErrors);
#else
if (g_hResource)
return g_script.ActionExec(mOurEXE, _T("/restart"), g_WorkingDirOrig, aDisplayErrors);
WCHAR buf[MAX_PATH];
GetModuleFileNameW(NULL, buf, MAX_PATH);
int argc = 0;
LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argc == 1 && !wcscmp(buf, argv[0]))
{
LocalFree(argv);
return g_script.ActionExec(mOurEXE, _T("/restart"), g_WorkingDirOrig, aDisplayErrors);
}
TCHAR arg_string[MAX_PATH + 512];
sntprintf(arg_string, _countof(arg_string), _T("/restart \"%s\""), mFileSpec);
return g_script.ActionExec(mOurEXE, arg_string, g_WorkingDirOrig, aDisplayErrors);
#endif // AUTOHOTKEYSC
#endif // _USRDLL
}
ResultType Script::ExitApp(ExitReasons aExitReason, int aExitCode)
// Normal exit (if aBuf is NULL), or a way to exit immediately on error (which is mostly
// for times when it would be unsafe to call MsgBox() due to the possibility that it would
// make the situation even worse).
{
mExitReason = aExitReason;
static bool sOnExitIsRunning = false, sExitAppShouldTerminate = true;
static int sExitCode;
if (sOnExitIsRunning || !mIsReadyToExecute)
{
// There is another instance of this function beneath us on the stack, executing an
// OnExit subroutine or function. If a legacy OnExit sub is running, we still need
// to execute any other OnExit callbacks before exiting. Otherwise ExitApp should
// terminate the app. Causes of script exit other than ExitApp are expected to
// terminate the app immediately even if OnExit is running.
#ifdef _USRDLL
sOnExitIsRunning = false;
if (sExitAppShouldTerminate && g_Reloading)
{
sExitAppShouldTerminate = false;
return EARLY_EXIT;
}
#endif
if (sExitAppShouldTerminate || aExitReason != EXIT_EXIT)
{
sExitAppShouldTerminate = false;
TerminateApp(aExitReason, aExitCode); // Exit early; don't run the OnExit callbacks (again).
}
if (*Line::sArgDeref[0]) // ExitApp with a parameter -- relies on the aExitReason check above.
sExitCode = aExitCode; // Override the previous exit code.
sExitAppShouldTerminate = true; // Signal our other instance that ExitApp was called.
return EARLY_EXIT; // Exit the thread (our other instance will call TerminateApp).
// MUST NOT create a new thread when sOnExitIsRunning because g_array allows only one
// extra thread for ExitApp() (which allows it to run even when MAX_THREADS_EMERGENCY
// has been reached). See TOTAL_ADDITIONAL_THREADS for details.
}
sExitCode = aExitCode;
// Otherwise, the script contains the special RunOnExit label that we will run here instead
// of exiting. And since it does, we know that the script is in a ready-to-execute state
// because that is the only way an OnExit label could have been defined in the first place.
// Usually, the RunOnExit subroutine will contain an Exit or ExitApp statement
// which results in a recursive call to this function, but this is not required (e.g. the
// Exit subroutine could display an "Are you sure?" prompt, and if the user chooses "No",
// the Exit sequence can be aborted by simply not calling ExitApp and letting the thread
// we create below end normally).
// Next, save the current state of the globals so that they can be restored just prior
// to returning to our caller:
TCHAR ErrorLevel_saved[ERRORLEVEL_SAVED_SIZE];
tcslcpy(ErrorLevel_saved, g_ErrorLevel->Contents(), _countof(ErrorLevel_saved)); // Save caller's errorlevel.
InitNewThread(0, true, true, ACT_INVALID); // Uninterruptibility is handled below. Since this special thread should always run, no checking of g_MaxThreadsTotal is done before calling this.
// Turn on uninterruptibility to forbid any hotkeys, timers, or user defined menu items
// to interrupt. This is mainly done for peace-of-mind (since possible interactions due to
// interruptions have not been studied) and the fact that this most users would not want this
// subroutine to be interruptible (it usually runs quickly anyway). Another reason to make
// it non-interruptible is that some OnExit subroutines might destruct things used by the
// script's hotkeys/timers/menu items, and activating these items during the deconstruction
// would not be safe. Finally, if a logoff or shutdown is occurring, it seems best to prevent
// timed subroutines from running -- which might take too much time and prevent the exit from
// occurring in a timely fashion. An option can be added via the FutureUse param to make it
// interruptible if there is ever a demand for that.
// UPDATE: g_AllowInterruption is now used instead of g->AllowThreadToBeInterrupted for two reasons:
// 1) It avoids the need to do "int mUninterruptedLineCountMax_prev = g_script.mUninterruptedLineCountMax;"
// (Disable this item so that ExecUntil() won't automatically make our new thread uninterruptible
// after it has executed a certain number of lines).
// 2) Mostly obsolete: If the thread we're interrupting is uninterruptible, the uninterruptible timer
// might be currently pending. When it fires, it would make the OnExit subroutine interruptible
// rather than the underlying subroutine. The above fixes the first part of that problem.
// The 2nd part is fixed by reinstating the timer when the uninterruptible thread is resumed.
// This special handling is only necessary here -- not in other places where new threads are
// created -- because OnExit is the only type of thread that can interrupt an uninterruptible
// thread.
BOOL g_AllowInterruption_prev = g_AllowInterruption; // Save current setting.
g_AllowInterruption = FALSE; // Mark the thread just created above as permanently uninterruptible (i.e. until it finishes and is destroyed).
sOnExitIsRunning = true;
DEBUGGER_STACK_PUSH(_T("OnExit"))
bool terminate_afterward = true; // Set default - see below for comments.
// When a legacy OnExit label is present, the default behaviour is to exit the script only if
// it calls ExitApp. Therefore to make OnExit() useful in a script which uses legacy OnExit,
// we need to prevent ExitApp from actually terminating the app:
sExitAppShouldTerminate = false;
// If the subroutine encounters a failure condition such as a runtime error, exit afterward.
// Otherwise, there will be no way to exit the script if the subroutine fails on each attempt.
if (mOnExitLabel && mOnExitLabel->Execute() && !sExitAppShouldTerminate)
{
// The subroutine completed normally and did not call ExitApp, so don't exit.
terminate_afterward = false;
}
sExitAppShouldTerminate = true;
// If an OnExit label was called and didn't call ExitApp, terminate_afterward was set to false,
// so the script isn't exiting. Otherwise, call the chain of OnExit functions:
if (terminate_afterward)
{
ExprTokenType param[] = { GetExitReasonString(aExitReason), (__int64)sExitCode };
if (mOnExit.Call(param, _countof(param), mOnExitLabel ? 0 : 1) == CONDITION_TRUE)
terminate_afterward = false; // A handler returned true to prevent exit.
}
#ifdef _USRDLL
if (aExitReason == EXIT_RELOAD)
return EARLY_EXIT;
#endif
DEBUGGER_STACK_POP()
sOnExitIsRunning = false; // In case the user wanted the thread to end normally (see above).
if (terminate_afterward || aExitReason == EXIT_DESTROY)
TerminateApp(aExitReason, aExitCode);
// Otherwise:
ResumeUnderlyingThread(ErrorLevel_saved);
g_AllowInterruption = g_AllowInterruption_prev; // Restore original setting.
return EARLY_EXIT;
}
void Script::TerminateApp(ExitReasons aExitReason, int aExitCode)
// Note that g_script's destructor takes care of most other cleanup work, such as destroying
// tray icons, menus, and unowned windows such as ToolTip.
{
#ifdef _USRDLL
terminateDll(aExitCode);
#else
// L31: Release objects stored in variables, where possible.
if (aExitCode != CRITICAL_ERROR) // i.e. Avoid making matters worse if CRITICAL_ERROR.
{
// Ensure the current thread is not paused and can't be interrupted
// in case one or more objects need to call a __delete meta-function.
g_AllowInterruption = FALSE;
g->IsPaused = false;
int v, i;
for (v = 0; v < mVarCount; ++v)
if (mVar[v]->IsObject())
mVar[v]->ReleaseObject();
for (v = 0; v < mLazyVarCount; ++v)
if (mLazyVar[v]->IsObject())
mLazyVar[v]->ReleaseObject();
for (i = 0; i < mFuncCount; ++i)
{
Func &f = *mFunc[i];
if (f.mIsBuiltIn)
continue;
// Since it doesn't seem feasible to release all var backups created by recursive function
// calls and all tokens in the 'stack' of each currently executing expression, currently
// only static and global variables are released. It seems best for consistency to also
// avoid releasing top-level non-static local variables (i.e. which aren't in var backups).
// For consistency, only free static vars (see above).
for (v = 0; v < f.mStaticVarCount; ++v)
if (f.mStaticVar[v]->IsObject())
f.mStaticVar[v]->ReleaseObject();
for (v = 0; v < f.mStaticLazyVarCount; ++v)
if (f.mStaticLazyVar[v]->IsObject())
f.mStaticLazyVar[v]->ReleaseObject();
// so no need to run below since static vars are in a separate array
//for (v = 0; v < f.mVarCount; ++v)
// if (f.mVar[v]->IsStatic() && f.mVar[v]->IsObject())
// f.mVar[v]->ReleaseObject();
//for (v = 0; v < f.mLazyVarCount; ++v)
// if (f.mLazyVar[v]->IsStatic() && f.mLazyVar[v]->IsObject())
// f.mLazyVar[v]->ReleaseObject();
}
}
#ifdef CONFIG_DEBUGGER // L34: Exit debugger *after* the above to allow debugging of any invoked __Delete handlers.
g_Debugger.Exit(aExitReason);
#endif
// We call DestroyWindow() because MainWindowProc() has left that up to us.
// DestroyWindow() will cause MainWindowProc() to immediately receive and process the
// WM_DESTROY msg, which should in turn result in any child windows being destroyed
// and other cleanup being done:
if (IsWindow(g_hWnd)) // Adds peace of mind in case WM_DESTROY was already received in some unusual way.
{
g_DestroyWindowCalled = true;
DestroyWindow(g_hWnd);
}
Hotkey::AllDestructAndExit(aExitCode);
#endif
}
#ifndef AUTOHOTKEYSC
LineNumberType Script::LoadFromText(LPTSTR aScript,LPCTSTR aPathToShow, bool aCheckIfExpr)
// HotKeyIt H1 LoadFromText() for text instead LoadFromFile()
// Returns the number of non-comment lines that were loaded, or LOADING_FAILED on error.
{
mNoHotkeyLabels = true; // Indicate that there are no hotkey labels, since we're (re)loading the entire file.
mIsReadyToExecute = mAutoExecSectionIsRunning = false;
// v1.0.42: Placeholder to use in place of a NULL label to simplify code in some places.
// This must be created before loading the script because it's relied upon when creating
// hotkeys to provide an alternative to having a NULL label. It will be given a non-NULL
// mJumpToLine further down.
if ( !(mPlaceholderLabel = new Label(_T(""))) ) // Not added to linked list since it's never looked up.
return LOADING_FAILED;
// L4: Changed this next section to support lines added for #if (expression).
// Each #if (expression) is pre-parsed *before* the main script in order for
// function library auto-inclusions to be processed correctly.
// Load the main script file. This will also load any files it includes with #Include.
if ( LoadIncludedText(aScript, aPathToShow) != OK
|| !AddLine(ACT_EXIT)) // Fix for v1.0.47.04: Add an Exit because otherwise, a script that ends in an IF-statement will crash in PreparseBlocks() because PreparseBlocks() expects every IF-statements mNextLine to be non-NULL (helps loading performance too).
return LOADING_FAILED;
if (!PreparseExpressions(mFirstLine))
return LOADING_FAILED;
// ABOVE: In v1.0.47, the above may have auto-included additional files from the userlib/stdlib.
// That's why the above is done prior to adding the EXIT lines and other things below.
// Preparse static initializers and #if expressions.
PreparseStaticLines(mFirstLine);
if (mFirstStaticLine)
{
// Prepend all Static initializers to the beginning of the auto-execute section.
mLastStaticLine->mNextLine = mFirstLine;
mFirstLine->mPrevLine = mLastStaticLine;
mFirstLine = mFirstStaticLine;
}
// Scan for undeclared local variables which are named the same as a global variable.
// This loop has two purposes (but it's all handled in PreprocessLocalVars()):
//
// 1) Allow super-global variables to be referenced above the point of declaration.
// This is a bit of a hack to work around the fact that variable references are
// resolved as they are encountered, before all declarations have been processed.
//
// 2) Warn the user (if appropriate) since they probably meant it to be global.
//
for (int i = 0; i < mFuncCount; ++i)
{
Func &func = *mFunc[i];
if (!func.mIsBuiltIn && !(func.mDefaultVarType & VAR_FORCE_LOCAL) && !(func.mPreprocessLocalVarsDone))
{
func.mPreprocessLocalVarsDone = true;
PreprocessLocalVars(func, func.mVar, func.mVarCount);
PreprocessLocalVars(func, func.mStaticVar, func.mStaticVarCount);
PreprocessLocalVars(func, func.mLazyVar, func.mLazyVarCount);
PreprocessLocalVars(func, func.mStaticLazyVar, func.mStaticLazyVarCount);
}
}
// Resolve any unresolved base classes.
if (mUnresolvedClasses)
{
if (!ResolveClasses())
return LOADING_FAILED;
mUnresolvedClasses->Release();
mUnresolvedClasses = NULL;
}
// Check for classes potentially being overwritten.
if (g_Warn_ClassOverwrite)
CheckForClassOverwrite();
#ifndef AUTOHOTKEYSC
if (mIncludeLibraryFunctionsThenExit)
{
delete mIncludeLibraryFunctionsThenExit;
return 0; // Tell our caller to do a normal exit.
}
#endif
// v1.0.35.11: Restore original working directory so that changes made to it by the above (via
// "#Include C:\Scripts" or "#Include %A_ScriptDir%" or even stdlib/userlib) do not affect the
// script's runtime working directory. This preserves the flexibility of having a startup-determined
// working directory for the script's runtime (i.e. it seems best that the mere presence of
// "#Include NewDir" should not entirely eliminate this flexibility).
SetCurrentDirectory(g_WorkingDirOrig); // g_WorkingDirOrig previously set by WinMain().
// Rather than do this, which seems kinda nasty if ever someday support same-line
// else actions such as "else return", just add two EXITs to the end of every script.
// That way, if the first EXIT added accidentally "corrects" an actionless ELSE
// or IF, the second one will serve as the anchoring end-point (mRelatedLine) for that
// IF or ELSE. In other words, since we never want mRelatedLine to be NULL, this should
// make absolutely sure of that:
//if (mLastLine->mActionType == ACT_ELSE ||
// ACT_IS_IF(mLastLine->mActionType)
// ...
// Second ACT_EXIT: even if the last line of the script is already "exit", always add
// another one in case the script ends in a label. That way, every label will have
// a non-NULL target, which simplifies other aspects of script execution.
// Making sure that all scripts end with an EXIT ensures that if the script
// file ends with ELSEless IF or an ELSE, that IF's or ELSE's mRelatedLine
// will be non-NULL, which further simplifies script execution.
// Not done since it's number doesn't much matter: ++mCombinedLineNumber;
++mCombinedLineNumber; // So that the EXITs will both show up in ListLines as the line # after the last physical one in the script.
if (!(AddLine(ACT_EXIT) && AddLine(ACT_EXIT))) // Second exit guaranties non-NULL mRelatedLine(s).
return LOADING_FAILED;
mPlaceholderLabel->mJumpToLine = mLastLine; // To follow the rule "all labels should have a non-NULL line before the script starts running".
if (!PreparseBlocks(mFirstLine)
|| !PreparseCommands(mFirstLine))
return LOADING_FAILED; // Error was already displayed by the above calls.
// Use FindOrAdd, not Add, because the user may already have added it simply by
// referring to it in the script:
if ( !(g_ErrorLevel = FindOrAddVar(_T("ErrorLevel"))) )
return LOADING_FAILED; // Error. Above already displayed it for us.
// Initialize the var state to zero right before running anything in the script:
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
// Initialize the random number generator:
// Note: On 32-bit hardware, the generator module uses only 2506 bytes of static
// data, so it doesn't seem worthwhile to put it in a class (so that the mem is
// only allocated on first use of the generator). For v1.0.24, _ftime() is not
// used since it could be as large as 0.5 KB of non-compressed code. A simple call to
// GetSystemTimeAsFileTime() seems just as good or better, since it produces
// a FILETIME, which is "the number of 100-nanosecond intervals since January 1, 1601."
// Use the low-order DWORD since the high-order one rarely changes. If my calculations are correct,
// the low-order 32-bits traverses its full 32-bit range every 7.2 minutes, which seems to make
// using it as a seed superior to GetTickCount for most purposes.
RESEED_RANDOM_GENERATOR;
return TRUE; // Must be non-zero.
// OBSOLETE: mLineCount was always non-zero at this point since above did AddLine().
//return mLineCount; // The count of runnable lines that were loaded, which might be zero.
}
#endif
UINT Script::LoadFromFile()
// Returns the number of non-comment lines that were loaded, or LOADING_FAILED on error.
{
mNoHotkeyLabels = true; // Indicate that there are no hotkey labels, since we're (re)loading the entire file.
mIsReadyToExecute = mAutoExecSectionIsRunning = false;
if (!mFileSpec || !*mFileSpec) return LOADING_FAILED;
#ifndef AUTOHOTKEYSC // When not in stand-alone mode, read an external script file.
DWORD attr = g_RunStdIn ? 0 : GetFileAttributes(mFileSpec); // v1.1.17: Don't check if reading script from stdin.
if (attr == MAXDWORD && !g_hResource) // File does not exist or lacking the authorization to get its attributes.
{
#ifdef MINIDLL
return LOADING_FAILED;
#endif
TCHAR buf[MAX_PATH + 256];
sntprintf(buf, _countof(buf), _T("Script file not found:\n%s"), mFileSpec);
MsgBox(buf, MB_ICONHAND);
return 0;
}
#endif
// v1.0.42: Placeholder to use in place of a NULL label to simplify code in some places.
// This must be created before loading the script because it's relied upon when creating
// hotkeys to provide an alternative to having a NULL label. It will be given a non-NULL
// mJumpToLine further down.
if ( !(mPlaceholderLabel = new Label(_T(""))) ) // Not added to linked list since it's never looked up.
return LOADING_FAILED;
// L4: Changed this next section to support lines added for #if (expression).
// Each #if (expression) is pre-parsed *before* the main script in order for
// function library auto-inclusions to be processed correctly.
// Load the main script file. This will also load any files it includes with #Include.
if ( LoadIncludedFile(g_RunStdIn ? _T("*") : mFileSpec, false, false) != OK
|| !AddLine(ACT_EXIT)) // Add an Exit to ensure lib auto-includes aren't auto-executed, for backward compatibility.
return LOADING_FAILED;
if (!PreparseExpressions(mFirstLine))
return LOADING_FAILED; // Error was already displayed by the above call.
// ABOVE: In v1.0.47, the above may have auto-included additional files from the userlib/stdlib.
// That's why the above is done prior to adding the EXIT lines and other things below.
// Preparse static initializers and #if expressions.
PreparseStaticLines(mFirstLine);
if (mFirstStaticLine)
{
// Prepend all Static initializers to the beginning of the auto-execute section.
mLastStaticLine->mNextLine = mFirstLine;
mFirstLine->mPrevLine = mLastStaticLine;
mFirstLine = mFirstStaticLine;
}
// Scan for undeclared local variables which are named the same as a global variable.
// This loop has two purposes (but it's all handled in PreprocessLocalVars()):
//
// 1) Allow super-global variables to be referenced above the point of declaration.
// This is a bit of a hack to work around the fact that variable references are
// resolved as they are encountered, before all declarations have been processed.
//
// 2) Warn the user (if appropriate) since they probably meant it to be global.
//
for (int i = 0; i < mFuncCount; ++i)
{
Func &func = *mFunc[i];
if (!func.mIsBuiltIn && !(func.mDefaultVarType & VAR_FORCE_LOCAL))
{
PreprocessLocalVars(func, func.mVar, func.mVarCount);
PreprocessLocalVars(func, func.mStaticVar, func.mStaticVarCount);
PreprocessLocalVars(func, func.mLazyVar, func.mLazyVarCount);
PreprocessLocalVars(func, func.mStaticLazyVar, func.mStaticLazyVarCount);
}
}
// Resolve any unresolved base classes.
if (mUnresolvedClasses)
{
if (!ResolveClasses())
return LOADING_FAILED;
mUnresolvedClasses->Release();
mUnresolvedClasses = NULL;
}
// Check for classes potentially being overwritten.
if (g_Warn_ClassOverwrite)
CheckForClassOverwrite();
#ifndef AUTOHOTKEYSC
if (mIncludeLibraryFunctionsThenExit)
{
delete mIncludeLibraryFunctionsThenExit;
return 0; // Tell our caller to do a normal exit.
}
#endif
// v1.0.35.11: Restore original working directory so that changes made to it by the above (via
// "#Include C:\Scripts" or "#Include %A_ScriptDir%" or even stdlib/userlib) do not affect the
// script's runtime working directory. This preserves the flexibility of having a startup-determined
// working directory for the script's runtime (i.e. it seems best that the mere presence of
// "#Include NewDir" should not entirely eliminate this flexibility).
SetCurrentDirectory(g_WorkingDirOrig); // g_WorkingDirOrig previously set by WinMain().
// Rather than do this, which seems kinda nasty if ever someday support same-line
// else actions such as "else return", just add two EXITs to the end of every script.
// That way, if the first EXIT added accidentally "corrects" an actionless ELSE
// or IF, the second one will serve as the anchoring end-point (mRelatedLine) for that
// IF or ELSE. In other words, since we never want mRelatedLine to be NULL, this should
// make absolutely sure of that:
//if (mLastLine->mActionType == ACT_ELSE ||
// ACT_IS_IF(mLastLine->mActionType)
// ...
// Second ACT_EXIT: even if the last line of the script is already "exit", always add
// another one in case the script ends in a label. That way, every label will have
// a non-NULL target, which simplifies other aspects of script execution.
// Making sure that all scripts end with an EXIT ensures that if the script
// file ends with ELSEless IF or an ELSE, that IF's or ELSE's mRelatedLine
// will be non-NULL, which further simplifies script execution.
// Not done since it's number doesn't much matter: ++mCombinedLineNumber;
++mCombinedLineNumber; // So that the EXITs will both show up in ListLines as the line # after the last physical one in the script.
if (!(AddLine(ACT_EXIT) && AddLine(ACT_EXIT))) // Second exit guaranties non-NULL mRelatedLine(s).
return LOADING_FAILED;
mPlaceholderLabel->mJumpToLine = mLastLine; // To follow the rule "all labels should have a non-NULL line before the script starts running".
if ( !PreparseBlocks(mFirstLine)
|| !PreparseCommands(mFirstLine) )
return LOADING_FAILED; // Error was already displayed by the above calls.
// Use FindOrAdd, not Add, because the user may already have added it simply by
// referring to it in the script:
if ( !(g_ErrorLevel = FindOrAddVar(_T("ErrorLevel"))) )
return LOADING_FAILED; // Error. Above already displayed it for us.
// Initialize the var state to zero right before running anything in the script:
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
// Initialize the random number generator:
// Note: On 32-bit hardware, the generator module uses only 2506 bytes of static
// data, so it doesn't seem worthwhile to put it in a class (so that the mem is
// only allocated on first use of the generator). For v1.0.24, _ftime() is not
// used since it could be as large as 0.5 KB of non-compressed code. A simple call to
// GetSystemTimeAsFileTime() seems just as good or better, since it produces
// a FILETIME, which is "the number of 100-nanosecond intervals since January 1, 1601."
// Use the low-order DWORD since the high-order one rarely changes. If my calculations are correct,
// the low-order 32-bits traverses its full 32-bit range every 7.2 minutes, which seems to make
// using it as a seed superior to GetTickCount for most purposes.
RESEED_RANDOM_GENERATOR;
return TRUE; // Must be non-zero.
// OBSOLETE: mLineCount was always non-zero at this point since above did AddLine().
//return mLineCount; // The count of runnable lines that were loaded, which might be zero.
}
bool IsFunction(LPTSTR aBuf, bool *aPendingFunctionHasBrace = NULL)
// Helper function for LoadIncludedFile().
// Caller passes in an aBuf containing a candidate line such as "function(x, y)"
// Caller has ensured that aBuf is rtrim'd.
// Caller should pass NULL for aPendingFunctionHasBrace to indicate that function definitions (open-brace
// on same line as function) are not allowed. When non-NULL *and* aBuf is a function call/def,
// *aPendingFunctionHasBrace is set to true if a brace is present at the end, or false otherwise.
// In addition, any open-brace is removed from aBuf in this mode.
{
LPTSTR action_end = StrChrAny(aBuf, EXPR_ALL_SYMBOLS EXPR_ILLEGAL_CHARS);
// Can't be a function definition or call without an open-parenthesis as first char found by the above.
// In addition, if action_end isn't NULL, that confirms that the string in aBuf prior to action_end contains
// no spaces, tabs, colons, or equal-signs. As a result, it can't be:
// 1) a hotstring, since they always start with at least one colon that would be caught immediately as
// first-expr-char-is-not-open-parenthesis by the above.
// 2) Any kind of math or assignment, such as var:=(x+y) or var+=(x+y).
// The only things it could be other than a function call or function definition are:
// Normal label that ends in single colon but contains an open-parenthesis prior to the colon, e.g. Label(x):
// Single-line hotkey such as KeyName::MsgBox. But since '(' isn't valid inside KeyName, this isn't a concern.
// In addition, note that it isn't necessary to check for colons that lie outside of quoted strings because
// we're only interested in the first "word" of aBuf: If this is indeed a function call or definition, what
// lies to the left of its first open-parenthesis can't contain any colons anyway because the above would
// have caught it as first-expr-char-is-not-open-parenthesis. In other words, there's no way for a function's
// opening parenthesis to occur after a legitimate/quoted colon or double-colon in its parameters.
// v1.0.40.04: Added condition "action_end != aBuf" to allow a hotkey or remap or hotkey such as
// such as "(::" to work even if it ends in a close-parenthesis such as "(::)" or "(::MsgBox )"
if ( !(action_end && *action_end == '(' && action_end != aBuf
&& tcslicmp(aBuf, _T("IF"), action_end - aBuf)
&& tcslicmp(aBuf, _T("WHILE"), action_end - aBuf)) // v1.0.48.04: Recognize While() as loop rather than a function because many programmers are in the habit of writing while() and if().
|| action_end[1] == ':' ) // v1.0.44.07: This prevents "$(::fn_call()" from being seen as a function-call vs. hotkey-with-call. For simplicity and due to rarity, omit_leading_whitespace() isn't called; i.e. assumes that the colon immediate follows the '('.
return false;
LPTSTR aBuf_last_char = action_end + _tcslen(action_end) - 1; // Above has already ensured that action_end is "(...".
if (aPendingFunctionHasBrace) // Caller specified that an optional open-brace may be present at the end of aBuf.
{
if (*aPendingFunctionHasBrace = (*aBuf_last_char == '{')) // Caller has ensured that aBuf is rtrim'd.
{
*aBuf_last_char = '\0'; // For the caller, remove it from further consideration.
aBuf_last_char = aBuf + rtrim(aBuf, aBuf_last_char - aBuf) - 1; // Omit trailing whitespace too.
}
}
return *aBuf_last_char == ')'; // This last check avoids detecting a label such as "Label(x):" as a function.
// Also, it seems best never to allow if(...) to be a function call, even if it's blank inside such as if().
// In addition, it seems best not to allow if(...) to ever be a function definition since such a function
// could never be called as ACT_EXPRESSION since it would be seen as an IF-stmt instead.
}
inline LPTSTR IsClassDefinition(LPTSTR aBuf, bool &aHasOTB)
{
if (_tcsnicmp(aBuf, _T("Class"), 5) || !IS_SPACE_OR_TAB(aBuf[5])) // i.e. it's not "Class" followed by a space or tab.
return NULL;
LPTSTR class_name = omit_leading_whitespace(aBuf + 6);
if (_tcschr(EXPR_ALL_SYMBOLS EXPR_ILLEGAL_CHARS, *class_name))
// It's probably something like "Class := GetClass()".
return NULL;
// Check for opening brace on same line:
LPTSTR aBuf_last_char = class_name + _tcslen(class_name) - 1;
if (aHasOTB = (*aBuf_last_char == '{')) // Caller has ensured that aBuf is rtrim'd.
{
*aBuf_last_char = '\0'; // For the caller, remove it from further consideration.
rtrim(aBuf, aBuf_last_char - aBuf); // Omit trailing whitespace too.
}
// Validation of the name is left up to the caller, for simplicity.
return class_name;
}
#ifndef AUTOHOTKEYSC
ResultType Script::LoadIncludedText(LPTSTR aScript,LPCTSTR aPathToShow)
// Returns OK or FAIL.
// Below: Use double-colon as delimiter to set these apart from normal labels.
// The main reason for this is that otherwise the user would have to worry
// about a normal label being unintentionally valid as a hotkey, e.g.
// "Shift:" might be a legitimate label that the user forgot is also
// a valid hotkey:
#define HOTKEY_FLAG _T("::")
#define HOTKEY_FLAG_LENGTH 2
{
if (!aScript || !*aScript) return FAIL;
// Keep this var on the stack due to recursion, which allows newly created lines to be given the
// correct file number even when some #include's have been encountered in the middle of the script:
int source_file_index = Line::sSourceFileCount;
if (Line::sSourceFileCount >= Line::sMaxSourceFiles)
{
int new_max;
if (Line::sMaxSourceFiles)
{
new_max = 2*Line::sMaxSourceFiles;
if (new_max > ABSOLUTE_MAX_SOURCE_FILES)
new_max = ABSOLUTE_MAX_SOURCE_FILES;
}
else
new_max = 100;
// For simplicity and due to rarity of every needing to, expand by reallocating the array.
// Use a temp var. because realloc() returns NULL on failure but leaves original block allocated.
LPTSTR *realloc_temp = (LPTSTR *)realloc(Line::sSourceFile, new_max * sizeof(LPTSTR)); // If passed NULL, realloc() will do a malloc().
if (!realloc_temp)
return ScriptError(ERR_OUTOFMEM); // Short msg since so rare.
Line::sSourceFile = realloc_temp;
Line::sMaxSourceFiles = new_max;
}
if (!source_file_index)
// Since this is the first source file, it must be the main script file. Just point it to the
// location of the filespec already dynamically allocated:
Line::sSourceFile[source_file_index] = aScript;
else
{
if (aPathToShow)
{
if (!(Line::sSourceFile[source_file_index] = tmalloc(_tcslen(aPathToShow)+1)))
return ScriptError(ERR_OUTOFMEM);
_tcscpy(Line::sSourceFile[source_file_index], aPathToShow);
}
else
Line::sSourceFile[source_file_index] = g_script.mOurEXE;
}
// <buf> should be no larger than LINE_SIZE because some later functions rely upon that:
#ifndef MINIDLL
TCHAR msg_text[MAX_PATH + 256];
#endif
TCHAR buf1[LINE_SIZE], buf2[LINE_SIZE], suffix[16], pending_buf[LINE_SIZE];
*pending_buf = '\0';
LPTSTR buf = buf1, next_buf = buf2; // Oscillate between bufs to improve performance (avoids memcpy from buf2 to buf1).
size_t buf_length, next_buf_length, suffix_length;
bool pending_buf_has_brace;
enum {
Pending_Func,
Pending_Class,
Pending_Property
} pending_buf_type;
++Line::sSourceFileCount;
includedtextbuf.mBuffer = aScript;
includedtextbuf.mLength = (DWORD)(_tcslen(aScript) * sizeof(TCHAR));
includedtextbuf.mOwned = false;
TextMem tmem, *fp = &tmem;
// NOTE: Ahk2Exe strips off the UTF-8 BOM.
//tmem.Open(textbuf, TextStream::READ | TextStream::EOL_CRLF | TextStream::EOL_ORPHAN_CR, CP_UTF16);
tmem.Open(includedtextbuf, TextStream::READ | TextStream::EOL_CRLF | TextStream::EOL_ORPHAN_CR
#ifdef UNICODE
, CP_UTF16
#endif
);
// File is now open, read lines from it.
#ifndef MINIDLL
LPTSTR hotkey_flag, cp, cp1, hotstring_start, hotstring_options;
Hotkey *hk;
#else
LPTSTR hotkey_flag, cp, cp1;
#endif
LineNumberType pending_buf_line_number, saved_line_number;
#ifndef MINIDLL
HookActionType hook_action;
bool is_label, suffix_has_tilde, hook_is_mandatory, in_comment_section, hotstring_options_all_valid;
ResultType hotkey_validity;
#else
bool is_label, in_comment_section;
#endif
#ifndef MINIDLL
// For the remap mechanism, e.g. a::b
int remap_stage;
vk_type remap_source_vk, remap_dest_vk = 0; // Only dest is initialized to enforce the fact that it is the flag/signal to indicate whether remapping is in progress.
TCHAR remap_source[32], remap_dest[32], remap_dest_modifiers[8]; // Must fit the longest key name (currently Browser_Favorites [17]), but buffer overflow is checked just in case.
LPTSTR extra_event;
bool remap_source_is_combo, remap_source_is_mouse, remap_dest_is_mouse, remap_keybd_to_mouse;
#endif
// For the line continuation mechanism:
bool do_ltrim, do_rtrim, literal_escapes, literal_derefs, literal_delimiters
, has_continuation_section, is_continuation_line;
#define CONTINUATION_SECTION_WITHOUT_COMMENTS 1 // MUST BE 1 because it's the default set by anything that's boolean-true.
#define CONTINUATION_SECTION_WITH_COMMENTS 2 // Zero means "not in a continuation section".
int in_continuation_section;
LPTSTR next_option, option_end;
TCHAR orig_char, one_char_string[2], two_char_string[3]; // Line continuation mechanism's option parsing.
one_char_string[1] = '\0'; // Pre-terminate these to simplify code later below.
two_char_string[2] = '\0'; //
int continuation_line_count;
#define MAX_FUNC_VAR_GLOBALS 2000
Var *func_global_var[MAX_FUNC_VAR_GLOBALS];
// Init both for main file and any included files loaded by this function:
mCurrFileIndex = source_file_index; // source_file_index is kept on the stack due to recursion (from #include).
LineNumberType phys_line_number = 0;
buf_length = GetLine(buf, LINE_SIZE - 1, 0, fp);
if (in_comment_section = !_tcsncmp(buf, _T("/*"), 2))
{
// Fixed for v1.0.35.08. Must reset buffer to allow a script's first line to be "/*".
*buf = '\0';
buf_length = 0;
}
while (buf_length != -1) // Compare directly to -1 since length is unsigned.
{
// For each whole line (a line with continuation section is counted as only a single line
// for the purpose of this outer loop).
// Keep track of this line's *physical* line number within its file for A_LineNumber and
// error reporting purposes. This must be done only in the outer loop so that it tracks
// the topmost line of any set of lines merged due to continuation section/line(s)..
mCombinedLineNumber = phys_line_number + 1;
// This must be reset for each iteration because a prior iteration may have changed it, even
// indirectly by calling something that changed it:
mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
// v1.0.44.13: An additional call to IsDirective() is now made up here so that #CommentFlag affects
// the line beneath it the same way as other lines (#EscapeChar et. al. didn't have this bug).
// It's best not to process ALL directives up here because then they would no longer support a
// continuation section beneath them (and possibly other drawbacks because it was never thoroughly
// tested).
if (!_tcsnicmp(buf, _T("#CommentFlag"), 12)) // Have IsDirective() process this now (it will also process it again later, which is harmless).
if (IsDirective(buf) == FAIL) // IsDirective() already displayed the error.
return FAIL;
// Read in the next line (if that next line is the start of a continuation section, append
// it to the line currently being processed:
for (has_continuation_section = false, in_continuation_section = 0;;)
{
// This increment relies on the fact that this loop always has at least one iteration:
++phys_line_number; // Tracks phys. line number in *this* file (independent of any recursion caused by #Include).
next_buf_length = GetLine(next_buf, LINE_SIZE - 1, in_continuation_section, fp);
if (next_buf_length && next_buf_length != -1 // Prevents infinite loop when file ends with an unclosed "/*" section. Compare directly to -1 since length is unsigned.
&& !in_continuation_section) // Multi-line comments can't be used in continuation sections. This line fixes '*/' being discarded in continuation sections (broken by L54).
{
if (!_tcsncmp(next_buf, _T("*/"), 2) // Check this even if !in_comment_section so it can be ignored (for convenience) and not treated as a line-continuation operator.
&& (in_comment_section || next_buf[2] != ':' || next_buf[3] != ':')) // ...but support */:: as a hotkey.
{
in_comment_section = false;
next_buf_length -= 2; // Adjust for removal of /* from the beginning of the string.
tmemmove(next_buf, next_buf + 2, next_buf_length + 1); // +1 to include the string terminator.
next_buf_length = ltrim(next_buf, next_buf_length); // Get rid of any whitespace that was between the comment-end and remaining text.
if (!*next_buf) // The rest of the line is empty, so it was just a naked comment-end.
continue;
}
else if (in_comment_section)
continue;
if (!_tcsncmp(next_buf, _T("/*"), 2))
{
in_comment_section = true;
continue; // It's now commented out, so the rest of this line is ignored.
}
}
if (in_comment_section) // Above has incremented and read the next line, which is everything needed while inside /* .. */
{
if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
break; // By design, it's not an error. This allows "/*" to be used to comment out the bottommost portion of the script without needing a matching "*/".
// Otherwise, continue reading lines so that they can be merged with the line above them
// if they qualify as continuation lines.
continue;
}
if (!in_continuation_section) // This is either the first iteration or the line after the end of a previous continuation section.
{
// v1.0.38.06: The following has been fixed to exclude "(:" and "(::". These should be
// labels/hotkeys, not the start of a continuation section. In addition, a line that starts
// with '(' but that ends with ':' should be treated as a label because labels such as
// "(label):" are far more common than something obscure like a continuation section whose
// join character is colon, namely "(Join:".
if ( !(in_continuation_section = (next_buf_length != -1 && *next_buf == '(' // Compare directly to -1 since length is unsigned.
&& next_buf[1] != ':' && next_buf[next_buf_length - 1] != ':')) ) // Relies on short-circuit boolean order.
{
if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
break;
if (!next_buf_length)
// It is permitted to have blank lines and comment lines in between the line above
// and any continuation section/line that might come after the end of the
// comment/blank lines:
continue;
// SINCE ABOVE DIDN'T BREAK/CONTINUE, NEXT_BUF IS NON-BLANK.
if (next_buf[next_buf_length - 1] == ':' && *next_buf != ',')
// With the exception of lines starting with a comma, the last character of any
// legitimate continuation line can't be a colon because expressions can't end
// in a colon. The only exception is the ternary operator's colon, but that is
// very rare because it requires the line after it also be a continuation line
// or section, which is unusual to say the least -- so much so that it might be
// too obscure to even document as a known limitation. Anyway, by excluding lines
// that end with a colon from consideration ambiguity with normal labels
// and non-single-line hotkeys and hotstrings is eliminated.
break;
is_continuation_line = false; // Set default.
switch(ctoupper(*next_buf)) // Above has ensured *next_buf != '\0' (toupper might have problems with '\0').
{
case 'A': // "AND".
// See comments in the default section further below.
if (!_tcsnicmp(next_buf, _T("and"), 3) && IS_SPACE_OR_TAB_OR_NBSP(next_buf[3])) // Relies on short-circuit boolean order.
{
cp = omit_leading_whitespace(next_buf + 3);
// v1.0.38.06: The following was fixed to use EXPR_CORE vs. EXPR_OPERAND_TERMINATORS
// to properly detect a continuation line whose first char after AND/OR is "!~*&-+()":
if (!_tcschr(EXPR_CORE, *cp))
// This check recognizes the following examples as NON-continuation lines by checking
// that AND/OR aren't followed immediately by something that's obviously an operator:
// and := x, and = 2 (but not and += 2 since the an operand can have a unary plus/minus).
// This is done for backward compatibility. Also, it's documented that
// AND/OR/NOT aren't supported as variable names inside expressions.
is_continuation_line = true; // Override the default set earlier.
}
break;
case 'O': // "OR".
// See comments in the default section further below.
if (ctoupper(next_buf[1]) == 'R' && IS_SPACE_OR_TAB_OR_NBSP(next_buf[2])) // Relies on short-circuit boolean order.
{
cp = omit_leading_whitespace(next_buf + 2);
// v1.0.38.06: The following was fixed to use EXPR_CORE vs. EXPR_OPERAND_TERMINATORS
// to properly detect a continuation line whose first char after AND/OR is "!~*&-+()":
if (!_tcschr(EXPR_CORE, *cp)) // See comment in the "AND" case above.
is_continuation_line = true; // Override the default set earlier.
}
break;
default:
// Desired line continuation operators:
// Pretty much everything, namely:
// +, -, *, /, //, **, <<, >>, &, |, ^, <, >, <=, >=, =, ==, <>, !=, :=, +=, -=, /=, *=, ?, :
// And also the following remaining unaries (i.e. those that aren't also binaries): !, ~
// The first line below checks for ::, ++, and --. Those can't be continuation lines because:
// "::" isn't a valid operator (this also helps performance if there are many hotstrings).
// ++ and -- are ambiguous with an isolated line containing ++Var or --Var (and besides,
// wanting to use ++ to continue an expression seems extremely rare, though if there's ever
// demand for it, might be able to look at what lies to the right of the operator's operand
// -- though that would produce inconsistent continuation behavior since ++Var itself still
// could never be a continuation line due to ambiguity).
//
// The logic here isn't smart enough to differentiate between a leading ! or - that's
// meant as a continuation character and one that isn't. Even if it were, it would
// still be ambiguous in some cases because the author's intent isn't known; for example,
// the leading minus sign on the second line below is ambiguous, so will probably remain
// a continuation character in both v1 and v2:
// x := y
// -z ? a:=1 : func()
if ((*next_buf == ':' || *next_buf == '+' || *next_buf == '-') && next_buf[1] == *next_buf // See above.
// L31: '.' and '?' no longer require spaces; '.' without space is member-access (object) operator.
//|| (*next_buf == '.' || *next_buf == '?') && !IS_SPACE_OR_TAB_OR_NBSP(next_buf[1]) // The "." and "?" operators require a space or tab after them to be legitimate. For ".", this is done in case period is ever a legal character in var names, such as struct support. For "?", it's done for backward compatibility since variable names can contain question marks (though "?" by itself is not considered a variable in v1.0.46).
//&& next_buf[1] != '=' // But allow ".=" (and "?=" too for code simplicity), since ".=" is the concat-assign operator.
|| !_tcschr(CONTINUATION_LINE_SYMBOLS, *next_buf)) // Line doesn't start with a continuation char.
break; // Leave is_continuation_line set to its default of false.
// Some of the above checks must be done before the next ones.
if ( !(hotkey_flag = _tcsstr(next_buf, HOTKEY_FLAG)) ) // Without any "::", it can't be a hotkey or hotstring.
{
is_continuation_line = true; // Override the default set earlier.
break;
}
#ifndef MINIDLL
if (*next_buf == ':') // First char is ':', so it's more likely a hotstring than a hotkey.
{
// Remember that hotstrings can contain what *appear* to be quoted literal strings,
// so detecting whether a "::" is in a quoted/literal string in this case would
// be more complicated. That's one reason this other method is used.
for (hotstring_options_all_valid = true, cp = next_buf + 1; *cp && *cp != ':'; ++cp)
if (!IS_HOTSTRING_OPTION(*cp)) // Not a perfect test, but eliminates most of what little remaining ambiguity exists between ':' as a continuation character vs. ':' as the start of a hotstring. It especially eliminates the ":=" operator.
{
hotstring_options_all_valid = false;
break;
}
if (hotstring_options_all_valid && *cp == ':') // It's almost certainly a hotstring.
break; // So don't treat it as a continuation line.
//else it's not a hotstring but it might still be a hotkey such as ": & x::".
// So continue checking below.
}
#endif
// Since above didn't "break", this line isn't a hotstring but it is probably a hotkey
// because above already discovered that it contains "::" somewhere. So try to find out
// if there's anything that disqualifies this from being a hotkey, such as some
// expression line that contains a quoted/literal "::" (or a line starting with
// a comma that contains an unquoted-but-literal "::" such as for FileAppend).
if (*next_buf == ',')
{
cp = omit_leading_whitespace(next_buf + 1);
// The above has set cp to the position of the non-whitespace item to the right of
// this comma. Normal (single-colon) labels can't contain commas, so only hotkey
// labels are sources of ambiguity. In addition, normal labels and hotstrings have
// already been checked for, higher above.
#ifndef MINIDLL
if ( _tcsncmp(cp, HOTKEY_FLAG, HOTKEY_FLAG_LENGTH) // It's not a hotkey such as ",::action".
&& _tcsncmp(cp - 1, COMPOSITE_DELIMITER, COMPOSITE_DELIMITER_LENGTH) ) // ...and it's not a hotkey such as ", & y::action".
#endif
is_continuation_line = true; // Override the default set earlier.
}
else // First symbol in line isn't a comma but some other operator symbol.
{
// Check if the "::" found earlier appears to be inside a quoted/literal string.
// This check is NOT done for a line beginning with a comma since such lines
// can contain an unquoted-but-literal "::". In addition, this check is done this
// way to detect hotkeys such as the following:
// +keyname:: (and other hotkey modifier symbols such as ! and ^)
// +keyname1 & keyname2::
// +^:: (i.e. a modifier symbol followed by something that is a hotkey modifier and/or a hotkey suffix and/or an expression operator).
// <:: and &:: (i.e. hotkeys that are also expression-continuation symbols)
// By contrast, expressions that qualify as continuation lines can look like:
// . "xxx::yyy"
// + x . "xxx::yyy"
// In addition, hotkeys like the following should continue to be supported regardless
// of how things are done here:
// ^"::
// . & "::
// Finally, keep in mind that an expression-continuation line can start with two
// consecutive unary operators like !! or !*. It can also start with a double-symbol
// operator such as <=, <>, !=, &&, ||, //, **.
for (cp = next_buf; cp < hotkey_flag && *cp != '"'; ++cp);
if (cp == hotkey_flag) // No '"' found to left of "::", so this "::" appears to be a real hotkey flag rather than part of a literal string.
break; // Treat this line as a normal line vs. continuation line.
for (cp = hotkey_flag + HOTKEY_FLAG_LENGTH; *cp && *cp != '"'; ++cp);
if (*cp)
{
// Closing quote was found so "::" is probably inside a literal string of an
// expression (further checking seems unnecessary given the fairly extreme
// rarity of using '"' as a key in a hotkey definition).
is_continuation_line = true; // Override the default set earlier.
}
//else no closing '"' found, so this "::" probably belongs to something like +":: or
// . & "::. Treat this line as a normal line vs. continuation line.
}
} // switch(toupper(*next_buf))
if (is_continuation_line)
{
if (buf_length + next_buf_length >= LINE_SIZE - 1) // -1 to account for the extra space added below.
return ScriptError(ERR_CONTINUATION_SECTION_TOO_LONG, next_buf);
if (*next_buf != ',') // Insert space before expression operators so that built/combined expression works correctly (some operators like 'and', 'or', '.', and '?' currently require spaces on either side) and also for readability of ListLines.
buf[buf_length++] = ' ';
tmemcpy(buf + buf_length, next_buf, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
buf_length += next_buf_length;
continue; // Check for yet more continuation lines after this one.
}
// Since above didn't continue, there is no continuation line or section. In addition,
// since this line isn't blank, no further searching is needed.
break;
} // if (!in_continuation_section)
// OTHERWISE in_continuation_section != 0, so the above has found the first line of a new
// continuation section.
continuation_line_count = 0; // Reset for this new section.
// Otherwise, parse options. First set the defaults, which can be individually overridden
// by any options actually present. RTrim defaults to ON for two reasons:
// 1) Whitespace often winds up at the end of a lines in a text editor by accident. In addition,
// whitespace at the end of any consolidated/merged line will be rtrim'd anyway, since that's
// how command parsing works.
// 2) Copy & paste from the forum and perhaps other web sites leaves a space at the end of each
// line. Although this behavior is probably site/browser-specific, it's a consideration.
do_ltrim = g_ContinuationLTrim; // Start off at global default.
do_rtrim = true; // Seems best to rtrim even if this line is a hotstring, since it is very rare that trailing spaces and tabs would ever be desirable.
// For hotstrings (which could be detected via *buf==':'), it seems best not to default the
// escape character (`) to be literal because the ability to have `t `r and `n inside the
// hotstring continuation section seems more useful/common than the ability to use the
// accent character by itself literally (which seems quite rare in most languages).
literal_escapes = false;
literal_derefs = false;
literal_delimiters = true; // This is the default even for hotstrings because although using (*buf != ':') would improve loading performance, it's not a 100% reliable way to detect hotstrings.
// The default is linefeed because:
// 1) It's the best choice for hotstrings, for which the line continuation mechanism is well suited.
// 2) It's good for FileAppend.
// 3) Minor: Saves memory in large sections by being only one character instead of two.
suffix[0] = '\n';
suffix[1] = '\0';
suffix_length = 1;
for (next_option = omit_leading_whitespace(next_buf + 1); *next_option; next_option = omit_leading_whitespace(option_end))
{
// Find the end of this option item:
if ( !(option_end = StrChrAny(next_option, _T(" \t"))) ) // Space or tab.
option_end = next_option + _tcslen(next_option); // Set to position of zero terminator instead.
// Temporarily terminate to help eliminate ambiguity for words contained inside other words,
// such as hypothetical "Checked" inside of "CheckedGray":
orig_char = *option_end;
*option_end = '\0';
if (!_tcsnicmp(next_option, _T("Join"), 4))
{
next_option += 4;
tcslcpy(suffix, next_option, _countof(suffix)); // The word "Join" by itself will product an empty string, as documented.
// Passing true for the last parameter supports `s as the special escape character,
// which allows space to be used by itself and also at the beginning or end of a string
// containing other chars.
ConvertEscapeSequences(suffix, g_EscapeChar, true);
suffix_length = _tcslen(suffix);
}
else if (!_tcsnicmp(next_option, _T("LTrim"), 5))
do_ltrim = (next_option[5] != '0'); // i.e. Only an explicit zero will turn it off.
else if (!_tcsnicmp(next_option, _T("RTrim"), 5))
do_rtrim = (next_option[5] != '0');
else
{
// Fix for v1.0.36.01: Missing "else" above, because otherwise, the option Join`r`n
// would be processed above but also be processed again below, this time seeing the
// accent and thinking it's the signal to treat accents literally for the entire
// continuation section rather than as escape characters.
// Within this terminated option substring, allow the characters to be adjacent to
// improve usability:
for (; *next_option; ++next_option)
{
switch (*next_option)
{
case '`': // Although not using g_EscapeChar (reduces code size/complexity), #EscapeChar is still supported by continuation sections; it's just that enabling the option uses '`' rather than the custom escape-char (one reason is that that custom escape-char might be ambiguous with future/past options if it's something weird like an alphabetic character).
literal_escapes = true;
break;
case '%': // Same comment as above.
literal_derefs = true;
break;
case ',': // Same comment as above.
literal_delimiters = false;
break;
case 'C': // v1.0.45.03: For simplicity, anything that begins with "C" is enough to
case 'c': // identify it as the option to allow comments in the section.
in_continuation_section = CONTINUATION_SECTION_WITH_COMMENTS; // Override the default, which is boolean true (i.e. 1).
break;
case ')':
// Probably something like (x.y)[z](), which is not intended as the beginning of
// a continuation section. Doing this only when ")" is found should remove the
// need to escape "(" in most real-world expressions while still allowing new
// options to be added later with minimal risk of breaking scripts.
in_continuation_section = 0;
*option_end = orig_char; // Undo the temporary termination.
goto process_completed_line;
}
}
}
// If the item was not handled by the above, ignore it because it is unknown.
*option_end = orig_char; // Undo the temporary termination.
} // for() each item in option list
// "has_continuation_section" indicates whether the line we're about to construct is partially
// composed of continuation lines beneath it. It's separate from continuation_line_count
// in case there is another continuation section immediately after/adjacent to the first one,
// but the second one doesn't have any lines in it:
has_continuation_section = true;
continue; // Now that the open-parenthesis of this continuation section has been processed, proceed to the next line.
} // if (!in_continuation_section)
// Since above didn't "continue", we're in the continuation section and thus next_buf contains
// either a line to be appended onto buf or the closing parenthesis of this continuation section.
if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
return ScriptError(ERR_MISSING_CLOSE_PAREN, buf);
if (next_buf_length == -2) // v1.0.45.03: Special flag that means "this is a commented-out line to be
continue; // entirely omitted from the continuation section." Compare directly to -2 since length is unsigned.
if (*next_buf == ')')
{
in_continuation_section = 0; // Facilitates back-to-back continuation sections and proper incrementing of phys_line_number.
next_buf_length = rtrim(next_buf); // Done because GetLine() wouldn't have done it due to have told it we're in a continuation section.
// Anything that lies to the right of the close-parenthesis gets appended verbatim, with
// no trimming (for flexibility) and no options-driven translation:
cp = next_buf + 1; // Use temp var cp to avoid altering next_buf (for maintainability).
--next_buf_length; // This is now the length of cp, not next_buf.
}
else
{
cp = next_buf;
// The following are done in this block only because anything that comes after the closing
// parenthesis (i.e. the block above) is exempt from translations and custom trimming.
// This means that commas are always delimiters and percent signs are always deref symbols
// in the previous block.
if (do_rtrim)
next_buf_length = rtrim(next_buf, next_buf_length);
if (do_ltrim)
next_buf_length = ltrim(next_buf, next_buf_length);
// Escape each comma and percent sign in the body of the continuation section so that
// the later parsing stages will see them as literals. Although, it's not always
// necessary to do this (e.g. commas in the last parameter of a command don't need to
// be escaped, nor do percent signs in hotstrings' auto-replace text), the settings
// are applied unconditionally because:
// 1) Determining when its safe to omit the translation would add a lot of code size and complexity.
// 2) The translation doesn't affect the functionality of the script since escaped literals
// are always de-escaped at a later stage, at least for everything that's likely to matter
// or that's reasonable to put into a continuation section (e.g. a hotstring's replacement text).
// UPDATE for v1.0.44.11: #EscapeChar, #DerefChar, #Delimiter are now supported by continuation
// sections because there were some requests for that in forum.
int replacement_count = 0;
if (literal_escapes) // literal_escapes must be done FIRST because otherwise it would also replace any accents added for literal_delimiters or literal_derefs.
{
one_char_string[0] = g_EscapeChar; // These strings were terminated earlier, so no need to
two_char_string[0] = g_EscapeChar; // do it here. In addition, these strings must be set by
two_char_string[1] = g_EscapeChar; // each iteration because the #EscapeChar (and similar directives) can occur multiple times, anywhere in the script.
replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
}
if (literal_derefs)
{
one_char_string[0] = g_DerefChar;
two_char_string[0] = g_EscapeChar;
two_char_string[1] = g_DerefChar;
replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
}
if (literal_delimiters)
{
one_char_string[0] = g_delimiter;
two_char_string[0] = g_EscapeChar;
two_char_string[1] = g_delimiter;
replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
}
if (replacement_count) // Update the length if any actual replacements were done.
next_buf_length = _tcslen(next_buf);
} // Handling of a normal line within a continuation section.
// Must check the combined length only after anything that might have expanded the string above.
if (buf_length + next_buf_length + suffix_length >= LINE_SIZE)
return ScriptError(ERR_CONTINUATION_SECTION_TOO_LONG, cp);
++continuation_line_count;
// Append this continuation line onto the primary line.
// The suffix for the previous line gets written immediately prior writing this next line,
// which allows the suffix to be omitted for the final line. But if this is the first line,
// No suffix is written because there is no previous line in the continuation section.
// In addition, cp!=next_buf, this is the special line whose text occurs to the right of the
// continuation section's closing parenthesis. In this case too, the previous line doesn't
// get a suffix.
if (continuation_line_count > 1 && suffix_length && cp == next_buf)
{
tmemcpy(buf + buf_length, suffix, suffix_length + 1); // Append and include the zero terminator.
buf_length += suffix_length; // Must be done only after the old value of buf_length was used above.
}
if (next_buf_length)
{
tmemcpy(buf + buf_length, cp, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
buf_length += next_buf_length; // Must be done only after the old value of buf_length was used above.
}
} // for() each sub-line (continued line) that composes this line.
process_completed_line:
// buf_length can't be -1 (though next_buf_length can) because outer loop's condition prevents it:
if (!buf_length) // Done only after the line number increments above so that the physical line number is properly tracked.
goto continue_main_loop; // In lieu of "continue", for performance.
// Since neither of the above executed, or they did but didn't "continue",
// buf now contains a non-commented line, either by itself or built from
// any continuation sections/lines that might have been present. Also note that
// by design, phys_line_number will be greater than mCombinedLineNumber whenever
// a continuation section/lines were used to build this combined line.
// If there's a previous line waiting to be processed, its fate can now be determined based on the
// nature of *this* line:
if (*pending_buf)
{
// Somewhat messy to decrement then increment later, but it's probably easier than the
// alternatives due to the use of "continue" in some places above. NOTE: phys_line_number
// would not need to be decremented+incremented even if the below resulted in a recursive
// call to us (though it doesn't currently) because line_number's only purpose is to
// remember where this layer left off when the recursion collapses back to us.
// Fix for v1.0.31.05: It's not enough just to decrement mCombinedLineNumber because there
// might be some blank lines or commented-out lines between this function call/definition
// and the line that follows it, each of which will have previously incremented mCombinedLineNumber.
saved_line_number = mCombinedLineNumber;
mCombinedLineNumber = pending_buf_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
// Open brace means this is a function definition. NOTE: buf was already ltrimmed by GetLine().
// Could use *g_act[ACT_BLOCK_BEGIN].Name instead of '{', but it seems too elaborate to be worth it.
if (*buf == '{' || pending_buf_has_brace) // v1.0.41: Support one-true-brace, e.g. fn(...) {
{
switch (pending_buf_type)
{
case Pending_Class:
if (!DefineClass(pending_buf))
return FAIL;
break;
case Pending_Property:
if (!DefineClassProperty(pending_buf))
return FAIL;
break;
case Pending_Func:
// Note that two consecutive function definitions aren't possible:
// fn1()
// fn2()
// {
// ...
// }
// In the above, the first would automatically be deemed a function call by means of
// the check higher above (by virtue of the fact that the line after it isn't an open-brace).
if (g->CurrentFunc)
{
// Though it might be allowed in the future -- perhaps to have nested functions have
// access to their parent functions' local variables, or perhaps just to improve
// script readability and maintainability -- it's currently not allowed because of
// the practice of maintaining the func_global_var list on our stack:
return ScriptError(_T("Functions cannot contain functions."), pending_buf);
}
if (!DefineFunc(pending_buf, func_global_var))
return FAIL;
if (pending_buf_has_brace) // v1.0.41: Support one-true-brace for function def, e.g. fn() {
{
if (!AddLine(ACT_BLOCK_BEGIN))
return FAIL;
mCurrLine = NULL; // L30: Prevents showing misleading vicinity lines if the line after a OTB function def is a syntax error.
}
break;
#ifdef _DEBUG
default:
return ScriptError(_T("DEBUG: pending_buf_type has an unexpected value."));
#endif
}
}
else // It's a function call on a line by itself, such as fn(x). It can't be if(..) because another section checked that.
{
if (pending_buf_type != Pending_Func) // Missing open-brace for class definition.
return ScriptError(ERR_MISSING_OPEN_BRACE, pending_buf);
if (mClassObjectCount && !g->CurrentFunc) // Unexpected function call in class definition.
return ScriptError(mClassProperty ? ERR_MISSING_OPEN_BRACE : ERR_INVALID_LINE_IN_CLASS_DEF, pending_buf);
if (!ParseAndAddLine(pending_buf, ACT_EXPRESSION))
return FAIL;
mCurrLine = NULL; // Prevents showing misleading vicinity lines if the line after a function call is a syntax error.
}
*pending_buf = '\0'; // Reset now that it's been fully handled, as an indicator for subsequent iterations.
if (pending_buf_type != Pending_Func) // Class or property.
{
if (!pending_buf_has_brace)
{
// This is the open-brace of a class definition, so requires no further processing.
if (!*(cp = omit_leading_whitespace(buf + 1)))
{
mCombinedLineNumber = saved_line_number;
goto continue_main_loop;
}
// Otherwise, there's something following the "{", possibly "}" or a function definition.
tmemmove(buf, cp, (buf_length = _tcslen(cp)) + 1);
}
}
mCombinedLineNumber = saved_line_number;
// Now fall through to the below so that *this* line (the one after it) will be processed.
// Note that this line might be a pre-processor directive, label, etc. that won't actually
// become a runtime line per se.
} // if (*pending_function)
if (*buf == '}' && mClassObjectCount && !g->CurrentFunc)
{
cp = buf;
if (mClassProperty)
{
// Close this property definition.
mClassProperty = NULL;
if (mClassPropertyDef)
{
free(mClassPropertyDef);
mClassPropertyDef = NULL;
}
cp = omit_leading_whitespace(cp + 1);
}
// Handling this before the two sections below allows a function or class definition
// to begin immediately after the close-brace of a previous class definition.
// This loop allows something like }}} to terminate multiple nested classes:
for (; *cp == '}' && mClassObjectCount; cp = omit_leading_whitespace(cp + 1))
{
// End of class definition.
--mClassObjectCount;
mClassObject[mClassObjectCount]->EndClassDefinition(); // Remove instance variables from the class object.
mClassObject[mClassObjectCount]->Release();
// Revert to the name of the class this class is nested inside, or "" if none.
if (cp1 = _tcsrchr(mClassName, '.'))
*cp1 = '\0';
else
*mClassName = '\0';
}
// cp now points at the next non-whitespace char after the brace.
if (!*cp)
goto continue_main_loop;
// Otherwise, there is something following this close-brace, so continue on below to process it.
tmemmove(buf, cp, buf_length = _tcslen(cp));
}
if (mClassProperty && !g->CurrentFunc) // This is checked before IsFunction() to prevent method definitions inside a property.
{
if (!_tcsnicmp(buf, _T("Get"), 3) || !_tcsnicmp(buf, _T("Set"), 3))
{
LPTSTR cp = omit_leading_whitespace(buf + 3);
if (!*cp || (*cp == '{' && !cp[1]))
{
// Defer this line until the next line comes in to simplify handling of '{' and OTB.
// For simplicity, pass the property definition to DefineFunc instead of the actual
// line text, even though it makes some error messages a bit inaccurate. (That would
// happen anyway when DefineFunc() finds a syntax error in the parameter list.)
_tcscpy(pending_buf, mClassPropertyDef);
LPTSTR dot = _tcschr(pending_buf, '.');
dot[1] = *buf; // Replace the x in property.xet(params).
pending_buf_line_number = mCombinedLineNumber;
pending_buf_has_brace = *cp == '{';
pending_buf_type = Pending_Func;
goto continue_main_loop;
}
}
return ScriptError(ERR_INVALID_LINE_IN_PROPERTY_DEF, buf);
}
// By doing the following section prior to checking for hotkey and hotstring labels, double colons do
// not need to be escaped inside naked function calls and function definitions such as the following:
// fn("::") ; Function call.
// fn(Str="::") ; Function definition with default value for its parameter.
if (IsFunction(buf, &pending_buf_has_brace)) // If true, it's either a function definition or a function call (to be distinguished later).
{
// Defer this line until the next line comes in, which helps determine whether this line is
// a function call vs. definition:
_tcscpy(pending_buf, buf);
pending_buf_line_number = mCombinedLineNumber;
pending_buf_type = Pending_Func;
goto continue_main_loop; // In lieu of "continue", for performance.
}
if (!g->CurrentFunc)
{
if (LPTSTR class_name = IsClassDefinition(buf, pending_buf_has_brace))
{
// Defer this line until the next line comes in to simplify handling of '{' and OTB:
_tcscpy(pending_buf, class_name);
pending_buf_line_number = mCombinedLineNumber;
pending_buf_type = Pending_Class;
goto continue_main_loop; // In lieu of "continue", for performance.
}
if (mClassObjectCount)
{
// Check for assignment first, in case of something like "Static := 123".
for (cp = buf; cisalnum(*cp) || *cp == '_' || *cp == '.'; ++cp);
if (cp > buf) // i.e. buf begins with an identifier.
{
cp = omit_leading_whitespace(cp);
if (*cp == ':' && cp[1] == '=') // This is an assignment.
{
if (!DefineClassVars(buf, false)) // See above for comments.
return FAIL;
goto continue_main_loop;
}
if (!*cp || *cp == '[' || (*cp == '{' && !cp[1])) // Property
{
size_t length = _tcslen(buf);
if (pending_buf_has_brace = (buf[length - 1] == '{'))
{
// Omit '{' and trailing whitespace from further consideration.
rtrim(buf, length - 1);
}
// Defer this line until the next line comes in to simplify handling of '{' and OTB:
_tcscpy(pending_buf, buf);
pending_buf_line_number = mCombinedLineNumber;
pending_buf_type = Pending_Property;
goto continue_main_loop; // In lieu of "continue", for performance.
}
}
if (!_tcsnicmp(buf, _T("Static"), 6) && IS_SPACE_OR_TAB(buf[6]))
{
if (!DefineClassVars(buf + 7, true))
return FAIL; // Above already displayed the error.
goto continue_main_loop; // In lieu of "continue", for performance.
}
if (*buf == '#') // See the identical section further below for comments.
{
saved_line_number = mCombinedLineNumber;
switch(IsDirective(buf))
{
case CONDITION_TRUE:
mCurrFileIndex = source_file_index;
mCombinedLineNumber = saved_line_number;
goto continue_main_loop;
case FAIL:
return FAIL;
}
}
// Anything not already handled above is not valid directly inside a class definition.
return ScriptError(ERR_INVALID_LINE_IN_CLASS_DEF, buf);
}
}
// The following "examine_line" label skips the following parts above:
// 1) IsFunction() because that's only for a function call or definition alone on a line
// e.g. not "if fn()" or x := fn(). Those who goto this label don't need that processing.
// 2) The "if (*pending_function)" block: Doesn't seem applicable for the callers of this label.
// 3) The inner loop that handles continuation sections: Not needed by the callers of this label.
// 4) Things like the following should be skipped because callers of this label don't want the
// physical line number changed (which would throw off the count of lines that lie beneath a remap):
// mCombinedLineNumber = phys_line_number + 1;
// ++phys_line_number;
// 5) "mCurrLine = NULL": Probably not necessary since it's only for error reporting. Worst thing
// that could happen is that syntax errors would be thrown off, which testing shows isn't the case.
#ifndef MINIDLL
examine_line:
// "::" alone isn't a hotstring, it's a label whose name is colon.
// Below relies on the fact that no valid hotkey can start with a colon, since
// ": & somekey" is not valid (since colon is a shifted key) and colon itself
// should instead be defined as "+;::". It also relies on short-circuit boolean:
hotstring_start = NULL;
hotstring_options = NULL; // Set default as "no options were specified for this hotstring".
hotkey_flag = NULL;
if (buf[0] == ':' && buf[1])
{
if (buf[1] != ':')
{
hotstring_options = buf + 1; // Point it to the hotstring's option letters.
// The following relies on the fact that options should never contain a literal colon.
// ALSO, the following doesn't use IS_HOTSTRING_OPTION() for backward compatibility,
// performance, and because it seems seldom if ever necessary at this late a stage.
if ( !(hotstring_start = _tcschr(hotstring_options, ':')) )
hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
else
++hotstring_start; // Points to the hotstring itself.
}
else // Double-colon, so it's a hotstring if there's more after this (but this means no options are present).
if (buf[2])
hotstring_start = buf + 2; // And leave hotstring_options at its default of NULL to indicate no options.
//else it's just a naked "::", which is considered to be an ordinary label whose name is colon.
}
if (hotstring_start)
{
// Find the hotstring's final double-colon by considering escape sequences from left to right.
// This is necessary for to handles cases such as the following:
// ::abc```::::Replacement String
// The above hotstring translates literally into "abc`::".
LPTSTR escaped_double_colon = NULL;
for (cp = hotstring_start; ; ++cp) // Increment to skip over the symbol just found by the inner for().
{
for (; *cp && *cp != g_EscapeChar && *cp != ':'; ++cp); // Find the next escape char or colon.
if (!*cp) // end of string.
break;
cp1 = cp + 1;
if (*cp == ':')
{
if (*cp1 == ':') // Found a non-escaped double-colon, so this is the right one.
{
hotkey_flag = cp++; // Increment to have loop skip over both colons.
// and the continue with the loop so that escape sequences in the replacement
// text (if there is replacement text) are also translated.
}
// else just a single colon, or the second colon of an escaped pair (`::), so continue.
continue;
}
switch (*cp1)
{
// Only lowercase is recognized for these:
case 'a': *cp1 = '\a'; break; // alert (bell) character
case 'b': *cp1 = '\b'; break; // backspace
case 'f': *cp1 = '\f'; break; // formfeed
case 'n': *cp1 = '\n'; break; // newline
case 'r': *cp1 = '\r'; break; // carriage return
case 't': *cp1 = '\t'; break; // horizontal tab
case 'v': *cp1 = '\v'; break; // vertical tab
// Otherwise, if it's not one of the above, the escape-char is considered to
// mark the next character as literal, regardless of what it is. Examples:
// `` -> `
// `:: -> :: (effectively)
// `; -> ;
// `c -> c (i.e. unknown escape sequences resolve to the char after the `)
}
// Below has a final +1 to include the terminator:
tmemmove(cp, cp1, _tcslen(cp1) + 1);
// Since single colons normally do not need to be escaped, this increments one extra
// for double-colons to skip over the entire pair so that its second colon
// is not seen as part of the hotstring's final double-colon. Example:
// ::ahc```::::Replacement String
if (*cp == ':' && *cp1 == ':')
++cp;
} // for()
if (!hotkey_flag)
hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
}
if (!hotstring_start) // Not a hotstring (hotstring_start is checked *again* in case above block changed it; otherwise hotkeys like ": & x" aren't recognized).
{
// Note that there may be an action following the HOTKEY_FLAG (on the same line).
if (hotkey_flag = _tcsstr(buf, HOTKEY_FLAG)) // Find the first one from the left, in case there's more than 1.
{
if (hotkey_flag == buf && hotkey_flag[2] == ':') // v1.0.46: Support ":::" to mean "colon is a hotkey".
++hotkey_flag;
// v1.0.40: It appears to be a hotkey, but validate it as such before committing to processing
// it as a hotkey. If it fails validation as a hotkey, treat it as a command that just happens
// to contain a double-colon somewhere. This avoids the need to escape double colons in scripts.
// Note: Hotstrings can't suffer from this type of ambiguity because a leading colon or pair of
// colons makes them easier to detect.
cp = omit_trailing_whitespace(buf, hotkey_flag); // For maintainability.
orig_char = *cp;
*cp = '\0'; // Temporarily terminate.
hotkey_validity = Hotkey::TextInterpret(omit_leading_whitespace(buf), NULL, false); // Passing NULL calls it in validate-only mode.
switch (hotkey_validity)
{
case FAIL:
hotkey_flag = NULL; // It's not a valid hotkey, so indicate that it's a command (i.e. one that contains a literal double-colon, which avoids the need to escape the double-colon).
break;
case CONDITION_FALSE:
return FAIL; // It's an invalid hotkey and above already displayed the error message.
//case CONDITION_TRUE:
// It's a key that doesn't exist on the current keyboard layout. Leave hotkey_flag set
// so that the section below handles it as a hotkey. This allows it to end the auto-exec
// section and register the appropriate label even though it won't be an active hotkey.
}
*cp = orig_char; // Undo the temp. termination above.
}
}
// Treat a naked "::" as a normal label whose label name is colon:
if (is_label = (hotkey_flag && hotkey_flag > buf)) // It's a hotkey/hotstring label.
{
if (g->CurrentFunc)
{
// Even if it weren't for the reasons below, the first hotkey/hotstring label in a script
// will end the auto-execute section with a "return". Therefore, if this restriction here
// is ever removed, be sure that that extra return doesn't get put inside the function.
//
// The reason for not allowing hotkeys and hotstrings inside a function's body is that
// when the subroutine is launched, the hotstring/hotkey would be using the function's
// local variables. But that is not appropriate and it's likely to cause problems even
// if it were. It doesn't seem useful in any case. By contrast, normal labels can
// safely exist inside a function body and since the body is a block, other validation
// ensures that a Gosub or Goto can't jump to it from outside the function.
return ScriptError(_T("Hotkeys/hotstrings are not allowed inside functions."), buf);
}
if (mLastLine && mLastLine->mActionType == ACT_IFWINACTIVE)
{
mCurrLine = mLastLine; // To show vicinity lines.
return ScriptError(_T("IfWin should be #IfWin."), buf);
}
*hotkey_flag = '\0'; // Terminate so that buf is now the label itself.
hotkey_flag += HOTKEY_FLAG_LENGTH; // Now hotkey_flag is the hotkey's action, if any.
if (!hotstring_start)
{
ltrim(hotkey_flag); // Has already been rtrimmed by GetLine().
rtrim(buf); // Trim the new substring inside of buf (due to temp termination). It has already been ltrimmed.
cp = hotkey_flag; // Set default, conditionally overridden below (v1.0.44.07).
// v1.0.40: Check if this is a remap rather than hotkey:
if ( *hotkey_flag // This hotkey's action is on the same line as its label.
&& (remap_dest_vk = hotkey_flag[1] ? TextToVK(cp = Hotkey::TextToModifiers(hotkey_flag, NULL)) : 0xFF) ) // And the action appears to be a remap destination rather than a command.
// For above:
// Fix for v1.0.44.07: Set remap_dest_vk to 0xFF if hotkey_flag's length is only 1 because:
// 1) It allows a destination key that doesn't exist in the keyboard layout (such as 6::ð in
// English).
// 2) It improves performance a little by not calling TextToVK except when the destination key
// might be a mouse button or some longer key name whose actual/correct VK value is relied
// upon by other places below.
// Fix for v1.0.40.01: Since remap_dest_vk is also used as the flag to indicate whether
// this line qualifies as a remap, must do it last in the statement above. Otherwise,
// the statement might short-circuit and leave remap_dest_vk as non-zero even though
// the line shouldn't be a remap. For example, I think a hotkey such as "x & y::return"
// would trigger such a bug.
{
// These will be ignored in other stages if it turns out not to be a remap later below:
remap_source_vk = TextToVK(cp1 = Hotkey::TextToModifiers(buf, NULL)); // An earlier stage verified that it's a valid hotkey, though VK could be zero.
remap_source_is_combo = _tcsstr(cp1, COMPOSITE_DELIMITER);
remap_source_is_mouse = IsMouseVK(remap_source_vk);
remap_dest_is_mouse = IsMouseVK(remap_dest_vk);
remap_keybd_to_mouse = !remap_source_is_mouse && remap_dest_is_mouse;
sntprintf(remap_source, _countof(remap_source), _T("%s%s%s")
, remap_source_is_combo ? _T("") : _T("*") // v1.1.27.01: Omit * when the remap source is a custom combo.
, _tcslen(cp1) == 1 && IsCharUpper(*cp1) ? _T("+") : _T("") // Allow A::b to be different than a::b.
, buf); // Include any modifiers too, e.g. ^b::c.
tcslcpy(remap_dest, cp, _countof(remap_dest)); // But exclude modifiers here; they're wanted separately.
tcslcpy(remap_dest_modifiers, hotkey_flag, _countof(remap_dest_modifiers));
if (cp - hotkey_flag < _countof(remap_dest_modifiers)) // Avoid reading beyond the end.
remap_dest_modifiers[cp - hotkey_flag] = '\0'; // Terminate at the proper end of the modifier string.
remap_stage = 0; // Init for use in the next stage.
// In the unlikely event that the dest key has the same name as a command, disqualify it
// from being a remap (as documented). v1.0.40.05: If the destination key has any modifiers,
// it is unambiguously a key name rather than a command, so the switch() isn't necessary.
if (*remap_dest_modifiers)
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
switch (remap_dest_vk)
{
case VK_CONTROL: // Checked in case it was specified as "Control" rather than "Ctrl".
case VK_SLEEP:
if (StrChrAny(hotkey_flag, _T(" \t,"))) // Not using g_delimiter (reduces code size/complexity).
break; // Any space, tab, or enter means this is a command rather than a remap destination.
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
// "Return" and "Pause" as destination keys are always considered commands instead.
// This is documented and is done to preserve backward compatibility.
case VK_RETURN:
// v1.0.40.05: Although "Return" can't be a destination, "Enter" can be. Must compare
// to "Return" not "Enter" so that things like "vk0d" (the VK of "Enter") can also be a
// destination key:
if (!_tcsicmp(remap_dest, _T("Return")))
break;
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
case VK_PAUSE: // Used for both "Pause" and "Break"
break;
default: // All other VKs are valid destinations and thus the remap is valid.
goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
}
// Since above didn't goto, indicate that this is not a remap after all:
remap_dest_vk = 0;
}
}
// else don't trim hotstrings since literal spaces in both substrings are significant.
// If this is the first hotkey label encountered, Add a return before
// adding the label, so that the auto-execute section is terminated.
// Only do this if the label is a hotkey because, for example,
// the user may want to fully execute a normal script that contains
// no hotkeys but does contain normal labels to which the execution
// should fall through, if specified, rather than returning.
// But this might result in weirdness? Example:
//testlabel:
// Sleep, 1
// return
// ^a::
// return
// It would put the hard return in between, which is wrong. But in the case above,
// the first sub shouldn't have a return unless there's a part up top that ends in Exit.
// So if Exit is encountered before the first hotkey, don't add the return?
// Even though wrong, the return is harmless because it's never executed? Except when
// falling through from above into a hotkey (which probably isn't very valid anyway)?
// Update: Below must check if there are any true hotkey labels, not just regular labels.
// Otherwise, a normal (non-hotkey) label in the autoexecute section would count and
// thus the RETURN would never be added here, even though it should be:
// Notes about the below macro:
// Fix for v1.0.34: Don't point labels to this particular RETURN so that labels
// can point to the very first hotkey or hotstring in a script. For example:
// Goto Test
// Test:
// ^!z::ToolTip Without the fix`, this is never displayed by "Goto Test".
// UCHAR_MAX signals it not to point any pending labels to this RETURN.
// mCurrLine = NULL -> signifies that we're in transition, trying to load a new one.
#define CHECK_Text_mNoHotkeyLabels \
if (mNoHotkeyLabels)\
{\
mNoHotkeyLabels = false;\
if (!AddLine(ACT_RETURN, NULL, UCHAR_MAX))\
return FAIL;\
mCurrLine = NULL;\
}
CHECK_Text_mNoHotkeyLabels
// For hotstrings, the below makes the label include leading colon(s) and the full option
// string (if any) so that the uniqueness of labels is preserved. For example, we want
// the following two hotstring labels to be unique rather than considered duplicates:
// ::abc::
// :c:abc::
if (!AddLabel(buf, true)) // Always add a label before adding the first line of its section.
return FAIL;
if (hotstring_start)
{
if (!*hotstring_start)
{
// The following error message won't indicate the correct line number because
// the hotstring (as a label) does not actually exist as a line. But it seems
// best to report it this way in case the hotstring is inside a #Include file,
// so that the correct file name and approximate line number are shown:
return ScriptError(_T("This hotstring is missing its abbreviation."), buf); // Display buf vs. hotkey_flag in case the line is simply "::::".
}
// In the case of hotstrings, hotstring_start is the beginning of the hotstring itself,
// i.e. the character after the second colon. hotstring_options is NULL if no options,
// otherwise it's the first character in the options list (option string is not terminated,
// but instead ends in a colon). hotkey_flag is blank if it's not an auto-replace
// hotstring, otherwise it contains the auto-replace text.
// v1.0.42: Unlike hotkeys, duplicate hotstrings are not detected. This is because
// hotstrings are less commonly used and also because it requires more code to find
// hotstring duplicates (and performs a lot worse if a script has thousands of
// hotstrings) because of all the hotstring options.
if (!Hotstring::AddHotstring(mLastLabel->mName, mLastLabel, hotstring_options ? hotstring_options : _T("")
, hotstring_start, hotkey_flag, has_continuation_section))
return FAIL;
// The following section is similar to the one for hotkeys below, but is done after
// parsing the hotstring's options. An attempt at combining the two sections actually
// increased the final code size, so they're left separate.
if (*hotkey_flag)
{
if (Hotstring::shs[Hotstring::sHotstringCount - 1]->mExecuteAction)
if (!ParseAndAddLine(hotkey_flag))
return FAIL;
// This is done for hotstrings with same-line action via 'E' and also auto-replace
// hotstrings in case gosub/goto is ever used to jump to their labels:
if (!AddLine(ACT_RETURN))
return FAIL;
}
}
else // It's a hotkey vs. hotstring.
{
hook_action = 0; // Set default.
if (*hotkey_flag) // This hotkey's action is on the same line as its label.
{
// Don't add the alt-tabs as a line, since it has no meaning as a script command.
// But do put in the Return regardless, in case this label is ever jumped to
// via Goto/Gosub:
if (!(hook_action = Hotkey::ConvertAltTab(hotkey_flag, false)))
if (!ParseAndAddLine(hotkey_flag))
return FAIL;
// Also add a Return that's implicit for a single-line hotkey.
if (!AddLine(ACT_RETURN))
return FAIL;
}
if (hk = Hotkey::FindHotkeyByTrueNature(buf, suffix_has_tilde, hook_is_mandatory)) // Parent hotkey found. Add a child/variant hotkey for it.
{
if (hook_action) // suffix_has_tilde has always been ignored for these types (alt-tab hotkeys).
{
// Hotkey::Dynamic() contains logic and comments similar to this, so maintain them together.
// An attempt to add an alt-tab variant to an existing hotkey. This might have
// merit if the intention is to make it alt-tab now but to later disable that alt-tab
// aspect via the Hotkey cmd to let the context-sensitive variants shine through
// (take effect).
hk->mHookAction = hook_action;
}
else
{
// Detect duplicate hotkey variants to help spot bugs in scripts.
if (hk->FindVariant()) // See if there's already a variant matching the current criteria (suffix_has_tilde does not make variants distinct form each other because it would require firing two hotkey IDs in response to pressing one hotkey, which currently isn't in the design).
{
mCurrLine = NULL; // Prevents showing unhelpful vicinity lines.
return ScriptError(_T("Duplicate hotkey."), buf);
}
if (!hk->AddVariant(mLastLabel, suffix_has_tilde))
return ScriptError(ERR_OUTOFMEM, buf);
if (hook_is_mandatory || (!g_os.IsWin9x() && g_ForceKeybdHook))
{
// Require the hook for all variants of this hotkey if any variant requires it.
// This seems more intuitive than the old behaviour, which required $ or #UseHook
// to be used on the *first* variant, even though it affected all variants.
#ifdef CONFIG_WIN9X
if (g_os.IsWin9x())
hk->mUnregisterDuringThread = true;
else
#endif
hk->mKeybdHookMandatory = true;
}
}
}
else // No parent hotkey yet, so create it.
if (!(hk = Hotkey::AddHotkey(mLastLabel, hook_action, mLastLabel->mName, suffix_has_tilde, false)))
{
if (hotkey_validity != CONDITION_TRUE)
return FAIL; // It already displayed the error.
// This hotkey uses a single-character key name, which could be valid on some other
// keyboard layout. Allow the script to start, but warn the user about the problem.
// Note that this hotkey's label is still valid even though the hotkey wasn't created.
if (!mIncludeLibraryFunctionsThenExit) // Current keyboard layout is not relevant in /iLib mode.
{
sntprintf(msg_text, _countof(msg_text), _T("Note: The hotkey %s will not be active because it does not exist in the current keyboard layout."), buf);
MsgBox(msg_text);
}
}
}
goto continue_main_loop; // In lieu of "continue", for performance.
} // if (is_label = ...)
#endif
// Otherwise, not a hotkey or hotstring. Check if it's a generic, non-hotkey label:
if (buf[buf_length - 1] == ':') // Labels must end in a colon (buf was previously rtrimmed).
{
if (buf_length == 1) // v1.0.41.01: Properly handle the fact that this line consists of only a colon.
return ScriptError(ERR_UNRECOGNIZED_ACTION, buf);
// Labels (except hotkeys) must contain no whitespace, delimiters, or escape-chars.
// This is to avoid problems where a legitimate action-line ends in a colon,
// such as "WinActivate SomeTitle" and "#Include c:".
// We allow hotkeys to violate this since they may contain commas, and since a normal
// script line (i.e. just a plain command) is unlikely to ever end in a double-colon:
for (cp = buf, is_label = true; *cp; ++cp)
if (IS_SPACE_OR_TAB(*cp) || *cp == g_delimiter || *cp == g_EscapeChar)
{
is_label = false;
break;
}
if (is_label // It's a generic label, since valid hotkeys and hotstrings have already been handled.
&& !(buf[buf_length - 2] == ':' && buf_length > 2)) // i.e. allow "::" as a normal label, but consider anything else with double-colon to be an error (reported at a later stage).
{
// v1.0.44.04: Fixed this check by moving it after the above loop.
// Above has ensured buf_length>1, so it's safe to check for double-colon:
// v1.0.44.03: Don't allow anything that ends in "::" (other than a line consisting only
// of "::") to be a normal label. Assume it's a command instead (if it actually isn't, a
// later stage will report it as "invalid hotkey"). This change avoids the situation in
// which a hotkey like ^!ä:: is seen as invalid because the current keyboard layout doesn't
// have a "ä" key. Without this change, if such a hotkey appears at the top of the script,
// its subroutine would execute immediately as a normal label, which would be especially
// bad if the hotkey were something like the "Shutdown" command.
// Update: Hotkeys with single-character names like ^!ä are now handled earlier, so that
// anything else with double-colon can be detected as an error. The checks above prevent
// something like foo:: from being interpreted as a generic label, so when the line fails
// to resolve to a command or expression, an error message will be shown.
buf[--buf_length] = '\0'; // Remove the trailing colon.
rtrim(buf, buf_length); // Has already been ltrimmed.
if (!AddLabel(buf, false))
return FAIL;
goto continue_main_loop; // In lieu of "continue", for performance.
}
}
// Since above didn't "goto", it's not a label.
if (*buf == '#')
{
saved_line_number = mCombinedLineNumber; // Backup in case IsDirective() processes an include file, which would change mCombinedLineNumber's value.
switch(IsDirective(buf)) // Note that it may alter the contents of buf, at least in the case of #IfWin.
{
case CONDITION_TRUE:
// Since the directive may have been a #include which called us recursively,
// restore the class's values for these two, which are maintained separately
// like this to avoid having to specify them in various calls, especially the
// hundreds of calls to ScriptError() and LineError():
mCurrFileIndex = source_file_index;
mCombinedLineNumber = saved_line_number;
goto continue_main_loop; // In lieu of "continue", for performance.
case FAIL: // IsDirective() already displayed the error.
return FAIL;
//case CONDITION_FALSE: Do nothing; let processing below handle it.
}
}
// Otherwise, treat it as a normal script line.
if (*buf == '{' || *buf == '}')
{
if (!AddLine(*buf == '{' ? ACT_BLOCK_BEGIN : ACT_BLOCK_END))
return FAIL;
// Allow any command/action, directive or label to the right of "{" or "}":
if ( *(buf = omit_leading_whitespace(buf + 1)) )
{
buf_length = _tcslen(buf); // Update.
mCurrLine = NULL; // To signify that we're in transition, trying to load a new line.
goto process_completed_line; // Have the main loop process the contents of "buf" as though it came in from the script.
}
goto continue_main_loop; // It's just a naked "{" or "}", so no more processing needed for this line.
}
// // Parse the command, assignment or expression, including any same-line open brace or sub-action
// for ELSE, TRY, CATCH or FINALLY. Unlike braces at the start of a line (processed above), this
// does not allow directives or labels to the right of the command.
if (!ParseAndAddLine(buf))
return FAIL;
continue_main_loop: // This method is used in lieu of "continue" for performance and code size reduction.
#ifndef MINIDLL
if (remap_dest_vk)
{
// For remapping, decided to use a "macro expansion" approach because I think it's considerably
// smaller in code size and complexity than other approaches would be. I originally wanted to
// do it with the hook by changing the incoming event prior to passing it back out again (for
// example, a::b would transform an incoming 'a' keystroke into 'b' directly without having
// to suppress the original keystroke and simulate a new one). Unfortunately, the low-level
// hooks apparently do not allow this. Here is the test that confirmed it:
// if (event.vkCode == 'A')
// {
// event.vkCode = 'B';
// event.scanCode = 0x30; // Or use vk_to_sc(event.vkCode).
// return CallNextHookEx(g_KeybdHook, aCode, wParam, lParam);
// }
switch (++remap_stage)
{
case 1: // Stage 1: Add key-down hotkey label, e.g. *LButton::
buf_length = _stprintf(buf, _T("%s::"), remap_source); // Should be no risk of buffer overflow due to prior validation.
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
case 2: // Stage 2.
// Copied into a writable buffer for maintainability: AddLine() might rely on this.
// Also, it seems unnecessary to set press-duration to -1 even though the auto-exec section might
// have set it to something higher than -1 because:
// 1) Press-duration doesn't apply to normal remappings since they use down-only and up-only events.
// 2) Although it does apply to remappings such as a::B and a::^b (due to press-duration being
// applied after a change to modifier state), those remappings are fairly rare and supporting
// a non-negative-one press-duration (almost always 0) probably adds a degree of flexibility
// that may be desirable to keep.
// 3) SendInput may become the predominant SendMode, so press-duration won't often be in effect anyway.
// 4) It has been documented that remappings use the auto-execute section's press-duration.
_tcscpy(buf, _T("-1")); // Does NOT need to be "-1, -1" for SetKeyDelay (see above).
// The primary reason for adding Key/MouseDelay -1 is to minimize the chance that a one of
// these hotkey threads will get buried under some other thread such as a timer, which
// would disrupt the remapping if #MaxThreadsPerHotkey is at its default of 1.
AddLine(remap_dest_is_mouse ? ACT_SETMOUSEDELAY : ACT_SETKEYDELAY, &buf, 1, NULL); // PressDuration doesn't need to be specified because it doesn't affect down-only and up-only events.
if (remap_keybd_to_mouse)
{
// Since source is keybd and dest is mouse, prevent keyboard auto-repeat from auto-repeating
// the mouse button (since that would be undesirable 90% of the time). This is done
// by inserting a single extra IF-statement above the Send that produces the down-event:
buf_length = _stprintf(buf, _T("if not GetKeyState(\"%s\")"), remap_dest); // Should be no risk of buffer overflow due to prior validation.
remap_stage = 9; // Have it hit special stage 9+1 next time for code reduction purposes.
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
}
// Otherwise, remap_keybd_to_mouse==false, so fall through to next case.
case 10:
extra_event = _T(""); // Set default.
switch (remap_dest_vk)
{
case VK_LMENU:
case VK_RMENU:
case VK_MENU:
switch (remap_source_vk)
{
case VK_LCONTROL:
case VK_CONTROL:
extra_event = _T("{LCtrl up}"); // Somewhat surprisingly, this is enough to make "Ctrl::Alt" properly remap both right and left control.
break;
case VK_RCONTROL:
extra_event = _T("{RCtrl up}");
break;
// Below is commented out because its only purpose was to allow a shift key remapped to alt
// to be able to alt-tab. But that wouldn't work correctly due to the need for the following
// hotkey, which does more harm than good by impacting the normal Alt key's ability to alt-tab
// (if the normal Alt key isn't remapped): *Tab::Send {Blind}{Tab}
//case VK_LSHIFT:
//case VK_SHIFT:
// extra_event = "{LShift up}";
// break;
//case VK_RSHIFT:
// extra_event = "{RShift up}";
// break;
}
break;
}
mCurrLine = NULL; // v1.0.40.04: Prevents showing misleading vicinity lines for a syntax-error such as %::%
_stprintf(buf, _T("{Blind}%s%s{%s DownR}"), extra_event, remap_dest_modifiers, remap_dest); // v1.0.44.05: DownTemp vs. Down. See Send's DownTemp handler for details.
if (!AddLine(ACT_SEND, &buf, 1, NULL)) // v1.0.40.04: Check for failure due to bad remaps such as %::%.
return FAIL;
AddLine(ACT_RETURN);
mCurrLine = NULL; // Prevents showing misleading vicinity lines for something like "RAlt up::AppsKey" -> "*RAlt up up::".
// Add key-up hotkey label, e.g. *LButton up::
buf_length = _stprintf(buf, _T("%s up::"), remap_source); // Should be no risk of buffer overflow due to prior validation.
remap_stage = 2; // Adjust to hit stage 3 next time (in case this is stage 10).
goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
case 3: // Stage 3.
_tcscpy(buf, _T("-1"));
AddLine(remap_dest_is_mouse ? ACT_SETMOUSEDELAY : ACT_SETKEYDELAY, &buf, 1, NULL);
_stprintf(buf, _T("{Blind}{%s Up}"), remap_dest); // Unlike the down-event above, remap_dest_modifiers is not included for the up-event; e.g. ^{b up} is inappropriate.
AddLine(ACT_SEND, &buf, 1, NULL);
AddLine(ACT_RETURN);
remap_dest_vk = 0; // Reset to signal that the remapping expansion is now complete.
break; // Fall through to the next section so that script loading can resume at the next line.
}
} // if (remap_dest_vk)
#endif
// Since above didn't "continue", resume loading script line by line:
buf = next_buf;
buf_length = next_buf_length;
next_buf = (buf == buf1) ? buf2 : buf1;
// The line above alternates buffers (toggles next_buf to be the unused buffer), which helps
// performance because it avoids memcpy from buf2 to buf1.
} // for each whole/constructed line.
if (*pending_buf) // Since this is the last non-comment line, the pending function must be a function call, not a function definition.
{
// Somewhat messy to decrement then increment later, but it's probably easier than the
// alternatives due to the use of "continue" in some places above.
saved_line_number = mCombinedLineNumber;
mCombinedLineNumber = pending_buf_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
if (pending_buf_type != Pending_Func)
return ScriptError(pending_buf_has_brace ? ERR_MISSING_CLOSE_BRACE : ERR_MISSING_OPEN_BRACE, pending_buf);
if (!ParseAndAddLine(pending_buf, ACT_EXPRESSION)) // Must be function call vs. definition since otherwise the above would have detected the opening brace beneath it and already cleared pending_function.
return FAIL;
mCombinedLineNumber = saved_line_number;
}
if (mClassObjectCount && !source_file_index) // or mClassProperty, which implies mClassObjectCount != 0.
{
// A class definition has not been closed with "}". Previously this was detected by adding
// the open and close braces as lines, but this way is simpler and has less overhead.
// The downside is that the line number won't be shown; however, the class name will.
// Seems okay not to show mClassProperty->mName since the class is missing "}" as well.
return ScriptError(ERR_MISSING_CLOSE_BRACE, mClassName);
}
++mCombinedLineNumber; // L40: Put the implicit ACT_EXIT on the line after the last physical line (for the debugger).
// This is not required, it is called by the destructor.
// fp->Close();
return OK;
}
#endif
ResultType Script::LoadIncludedFile(LPTSTR aFileSpec, bool aAllowDuplicateInclude, bool aIgnoreLoadFailure)
// Returns OK or FAIL.
// Below: Use double-colon as delimiter to set these apart from normal labels.
// The main reason for this is that otherwise the user would have to worry
// about a normal label being unintentionally valid as a hotkey, e.g.
// "Shift:" might be a legitimate label that the user forgot is also
// a valid hotkey:
#define HOTKEY_FLAG _T("::")
#define HOTKEY_FLAG_LENGTH 2
{
if (!aFileSpec || !*aFileSpec) return FAIL;
#ifndef AUTOHOTKEYSC
if (Line::sSourceFileCount >= Line::sMaxSourceFiles)
{
if (Line::sSourceFileCount >= ABSOLUTE_MAX_SOURCE_FILES)
return ScriptError(_T("Too many includes.")); // Short msg since so rare.
int new_max;
if (Line::sMaxSourceFiles)
{
new_max = 2*Line::sMaxSourceFiles;
if (new_max > ABSOLUTE_MAX_SOURCE_FILES)
new_max = ABSOLUTE_MAX_SOURCE_FILES;
}
else
new_max = 100;