• Support
  • Articles
  • Resources
  • Products

Tera Term Source Code Overview

13. Macro Language Design and Implementation

Overview

Tera Term macro script is BASIC-style language. It does not use Bison or Flex like lexical analyzer and is written entirely from the scratch using recursive descent parsing method. Therefore, from this point of view Tera Term macro cannot be called a full-fledged scripting language.

Loading Macro File

As soon as ttpmacro.exe starts, it reads entire macro file (.ttl) into the buffer.

    OnInitDialog()#ttmmain.cpp -> InitTTL() -> InitBuff() -> LoadMacroFile()

When ttpmacro.exe reads the content of macro file, it places it into buffer Buff[0]#ttmbuff.c. At this point, since entire content of macro has been read, even accidental deletion of macro file during macro execution will not cause a problem. However, if macro contains "include" statements, included files should exist at the time when "include" macro command is being executed.

  1. #define MAXNESTLEVEL 10 /* defines the maximum number of nested files (up to nine includes) */
  2.  
  3. static int INest; /* current nested number */
  4. static HANDLE BuffHandle [MAXNESTLEVEL]; /* buffer handle received from GlobalAlloc () */
  5. static PCHAR Buff [MAXNESTLEVEL]; /* buffer area */
  6. static BINT BuffLen [MAXNESTLEVEL]; /* file size (buffer size) */
  7. static BINT BuffPtr [MAXNESTLEVEL]; /* offset of buffer (read position) */
Macro Engine

Macro script processing is done within idle loop OnIdle()#ttmmain.cpp. Behavior of macro engine changes depending on the value of idle loop variable TTLStatus. Normal execution state is set IdTTLRun. The list of available operations is shown below.

Condition Handling
TTLStatus==IdTTLEnd Ends the macro program
Unhandled Data (OutLine>0) Sends data to Tera Term core
TTLStatus==IdTTLRun Run the macro line by line
TTLStatus==IdTTLWait Waits ('wait' command)
TTLStatus==IdTTLWaitLn Waits ('waitln' command)
TTLStatus==IdTTLWaitNL Receives line ('recvln' command)
TTLStatus==IdTTLWait2 Waits for string ('waitrecv' command)
Command Interpretation

Every time function Exec()#ttl.c is called from the idle loop, the next line of the loaded macro will be processed. GetNewLine() takes one row out of the buffer and saves it in LineBuff[]#ttmparse.c. Any code with ASCII value below 0x20 (whitespace) except 0x09 (tabulation) is considered as the end of line. Leading and spaces and tabulation are ignored. Semicolon (;) is considered as the beginning of a comment and is discarded together with the remaining part of the string.

  1. char LineBuff [MaxLineLen]; /* one row can store up to 500 bytes */
  2. WORD LinePtr; /* buffer offset */
  3. WORD LineLen; /* buffer size */

ExecCmnd() function called from Exec() performs lexical analysis of the commands. Lexical analysis is done by simple string search within LineBuff[], one byte at a time. High level description of the analysis algorithm is shown below.

  1. Find 'endwhile'
  2. Find 'break'
  3. Find 'endif'
  4. Find 'else'
  5. Execute macro command
  6. Determine identifier
  7. Show error if none of above steps were executed

GetReservedWord() function is responsible for detecting whether the input string contains a macro command, or not. Comparison is done by using _stricmp() function, thus the commands are not case sensitive. If supported macro command is detected then corresponding TTLxxx() function is being called.

GetIdentifier() function is responsible for finding identificators. All tokens containing alphanumeric (a-z, A-Z, 0-9) characters, or underscore (_) and not exceeding 32 characters in length will be treated as variables. Statements where a value is assigned to a variable may contain variable name at the left immediately followed by equal sign (=). This complicates variable name detection process. Determination in done in the following order:

  1. Find a string
  2. Find a formula

GetString() function is responsible for finding string values. Since string are surrounded by singe or double quotas ('or "), it is easy to retrieve them.

GetExpression() function detects the formulas that require calculations. It uses recursive descent method for parsing.

CheckVar() function can tell whether the variable exists and if it has numeric or string type. If variable not found, NewStrVar() function will register a new variable.

14. Caret Control

Overview

Usually user moves the cursor by pressing corresponding keys on the keyboard, but if server needs to move the cursor, it sends out escape sequences (sets of characters that start with ESC code). Furthermore, terminal window may be in inactive mode, for example while running in broadcast mode, but it still should be able to move the cursor to simultaneously update multiple terminal windows.

System caret

Tera Term uses system caret to draw the cursor. For this purpose Tera Term utilizes API functions listed below.

Function Description
CreateCaret Creates a new shape for the system caret and assigns ownership of the caret to the specified window.
DestroyCaret Destroys the caret's current shape, frees the caret from the window, and removes the caret from the screen.
GetCaretBlinkTime Retrieves the time required to invert the caret's pixels.
HideCaret Removes the caret from the screen. Hiding a caret does not destroy its current shape or invalidate the insertion point.
SetCaretBlinkTime Sets the caret blink time to the specified number of milliseconds.
SetCaretPos Moves the caret to the specified coordinates.
ShowCaret Makes the caret visible on the screen at the caret's current position.

According to MSDN Library description, the system provides one caret per queue. Caret can be created only when window is active, or has focus. Caret must be destroyed before window becomes inactive, or loses focus.

Above requirement means that CreateCaret() can be called only when the window is active and DestroyCaret() should be called before the window becomes inactive. Caret is displayed by CaretOn()#vtdisp.c function and erased by CaretOff()#vtdisp.c function. These functions are usually called as the result of processing escape sequences parsed by VTParse() function, or when user resizes terminal window with the mouse.

Displaying Caret in Inactive Mode

In inactive windows system caret is invisible. The top window on Windows desktop is the only window that shows the caret. In most cases such behavior is acceptable and doesn't cause problems. However, when Tera Term is in broadcast mode, active Tera Term window can recieve and show commands simultaneously with other inactive Tera Term windows and it will look awkward if during this process the caret in inactive windows is hidden. Therefore, even if the window is inactive, we have to find a way to draw the caret in it. Since system caret cannot be used, we need to draw our own caret. In this case inactive Tera Term window will be able to show and move cursor in response to escape sequences received from remote host, or following user's input from the keyboard.

Current cursor position is stored in CursorX and CursorY. CaretKillFocus() function is called for inactive window. Then custom "polygon cursor" is displayed. Variable ts.VTColor[0] contains the color of the cursor. When cursor position needs to be updated, it is necessary to erase the cursor drawn previously and restore the background color that was previously backed up in ts.VTColor[1]. While drawing polygon cursor in a position that already contains a character, this character will be overwritten. Therefore, it is necessary to re-drawing the character. This can be done by using UpdateCaretKillFocus() function. This function sends WM_PAINT message and triggers InvalidateRect(), which then redraws the character.

  1. void CaretKillFocus(BOOL show)
  2. {
  3. int CaretX, CaretY;
  4. POINT p[5];
  5. HPEN oldpen;
  6. HDC hdc;
  7.  
  8. DispInitDC();
  9. hdc = VTDC;
  10.  
  11. CaretX = (CursorX-WinOrgX)*FontWidth;
  12. CaretY = (CursorY-WinOrgY)*FontHeight;
  13.  
  14. p[0].x = CaretX;
  15. p[0].y = CaretY;
  16. p[1].x = CaretX;
  17. p[1].y = CaretY + FontHeight - 1;
  18. if (CursorOnDBCS)
  19. p[2].x = CaretX + FontWidth*2 - 1;
  20. else
  21. p[2].x = CaretX + FontWidth - 1;
  22. p[2].y = CaretY + FontHeight - 1;
  23. if (CursorOnDBCS)
  24. p[3].x = CaretX + FontWidth*2 - 1;
  25. else
  26. p[3].x = CaretX + FontWidth - 1;
  27. p[3].y = CaretY;
  28. p[4].x = CaretX;
  29. p[4].y = CaretY;
  30.  
  31. if (show) { // Show polygon cursor (non-focused)
  32. oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[0]));
  33. } else {
  34. oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[1]));
  35. }
  36. Polyline(VTDC, p, 5);
  37. oldpen = SelectObject(hdc, oldpen);
  38. DeleteObject(oldpen);
  39.  
  40. DispReleaseDC();
  41. }
  42.  

Handling Inactive Window Cursor States

Since inactive window cursor may be in different states, we need to properly handle cursor state changes. Below is the list of possible cases.

  1. Window is active (Active == TRUE), so there is no need to draw polygon caret (CaretKillFocus).
  2. When CaretOn() is called for non-active (Active == FALSE) window, ShowCaret() has to launch polygon caret drawing function and pass it argument TRUE.
  3. When CaretOff() is called for non-active (Active == FALSE) window, ShowCaret() has to launch polygon caret drawing function and pass it argument FALSE.
  4. While retrieving caret state with IsCaretOn() function, additional condition needs to be added: OR (! Active && (CaretStatus == 0)).
  5. Call to ChangeCaret() does nothing and can be ignored.
  6. When WM_KILLFOCUS message is received while IsCaretOn() is TRUE, polygon caret drawing function has to be launched with argument TRUE.
  7. When WM_ACTIVE message is received while IsCaretOn() is TRUE, polygon caret drawing function has to be launched with argument FALSE.

15. Serial Port

Overview

Tera Term supports UART (16550A) compatible serial ports. Serial ports are also known as Communication Ports, or COM ports. When operating system detects COM port, it assigns it sequential number starting 1 and uses this number in port name like "COM1", "COM2", etc. Microsoft Windows XP supports up-to 256 COM ports (COM1 - COM256).

In the past most personal computers had one or two COM ports. Modern computers do not have COM ports and if such port is needed, USB to Serial converter is being utilized instead. These converters allow user to choose port number. For example, instead of having ports "COM1" and "COM2", there might be 2 ports "COM1" and "COM7". Tera Term can handle such setup and properly detect port numbers (i.e. "COM1" and "COM7").

List of COM Ports

Older versions of Tera Term would present in connection dialog all possible COM ports from "COM1" to "COM256" and then user had to choose the right port from this huge list. This was not easy. Currently, when connection dialog is called, Tera Term first looks for the COM ports recognized by the operating system and then shows in the list only those ports that are currently available. The detection of available ports is done in DetectComPorts()#ttcmn.c file. API function QueryDosDevice() searches for string "COM" among all MS-DOS device names.

  1. if (((h = GetModuleHandle("kernel32.dll")) != NULL) &&
  2. (GetProcAddress(h, "QueryDosDeviceA") != NULL) &&
  3. (QueryDosDevice(NULL, devicesBuff, 65535) != 0)) {
  4. p = devicesBuff;
  5. while (*p != '\0') {
  6. if (strncmp(p, "COM", 3) == 0 && p[3] != '\0') {
  7. ComPortTable[comports++] = atoi(p+3);
  8. if (comports >= ComPortMax)
  9. break;
  10. }
  11. p += (strlen(p)+1);
  12. }
  13. }

Retrieving COM Port's Full Name

Additionally, ListupSerialPort()#ttcmn.c can retrieve the full name of each COM port.

  1. static void ListupSerialPort(LPWORD ComPortTable, int comports, char **ComPortDesc, int ComPortMax)
  2. {
  3. GUID ClassGuid[1];
  4. DWORD dwRequiredSize;
  5. BOOL bRet;
  6. HDEVINFO DeviceInfoSet = NULL;
  7. SP_DEVINFO_DATA DeviceInfoData;
  8. DWORD dwMemberIndex = 0;
  9. int i;
  10.  
  11. DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
  12.  
  13. bRet =
  14. SetupDiClassGuidsFromName(_T("PORTS"), (LPGUID) & ClassGuid, 1,
  15. &dwRequiredSize);
  16. if (!bRet) {
  17. goto cleanup;
  18. }
  19.  
  20. DeviceInfoSet =
  21. SetupDiGetClassDevs(&ClassGuid[0], NULL, NULL, DIGCF_PRESENT | DIGCF_PROFILE);
  22.  
  23. if (DeviceInfoSet) {
  24. dwMemberIndex = 0;
  25. while (SetupDiEnumDeviceInfo
  26. (DeviceInfoSet, dwMemberIndex++, &DeviceInfoData)) {
  27. TCHAR szFriendlyName[MAX_PATH];
  28. TCHAR szPortName[MAX_PATH];
  29. DWORD dwReqSize = 0;
  30. DWORD dwPropType;
  31. DWORD dwType = REG_SZ;
  32. HKEY hKey = NULL;
  33.  
  34. bRet = SetupDiGetDeviceRegistryProperty(DeviceInfoSet,
  35. &DeviceInfoData,
  36. SPDRP_FRIENDLYNAME,
  37. &dwPropType,
  38. (LPBYTE)
  39. szFriendlyName,
  40. sizeof(szFriendlyName),
  41. &dwReqSize);
  42.  
  43. hKey = SetupDiOpenDevRegKey(DeviceInfoSet,
  44. &DeviceInfoData,
  45. DICS_FLAG_GLOBAL,
  46. 0, DIREG_DEV, KEY_READ);
  47. if (hKey) {
  48. long lRet;
  49. dwReqSize = sizeof(szPortName);
  50. lRet = RegQueryValueEx(hKey,
  51. _T("PortName"),
  52. 0,
  53. &dwType,
  54. (LPBYTE) & szPortName,
  55. &dwReqSize);
  56. RegCloseKey(hKey);
  57. }
  58.  
  59. if (_strnicmp(szPortName, "COM", 3) == 0) { // Found COM port driver
  60. int port = atoi(&szPortName[3]);
  61. int i;
  62.  
  63. for (i = 0 ; i < comports ; i++) {
  64. if (ComPortTable[i] == port) { // Confirm COM connection
  65. ComPortDesc[i] = _strdup(szFriendlyName);
  66. break;
  67. }
  68. }
  69. }
  70. }
  71. }
  72.  
  73. cleanup:
  74. SetupDiDestroyDeviceInfoList(DeviceInfoSet);
  75. }

Copyright © 2008-2015 Tera Term Project