/* logger.c
*
* Copyright 2025 Francois Techene
*
* 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 3 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* ----------------------------------------------------------------
* Utility library to log to a file or to the console.
*
* Before using the logger, you must initialize it by calling:
*
* initLogger(DEBUG_LEVEL, LOG_METHOD, LOG_FILE);
*
* Where those variables are defined in logger.h.
*
* You may put all logging code inbetween the USE_LOGGER condition
* so that loggin logic can be easily fully disabled.
*
* #ifdef USE_LOGGER
* initLogger(DEBUG_LEVEL, LOG_METHOD, LOG_FILE);
* #endif
*
* #ifdef USE_LOGGER
* logger.debug("message");
* #endif
*
* If you are logging to the console, you need to forward the
* application's events to the logger and ignore the event if it
* has been handled by the console:
*
* if(logger.onEvent(event)) {
* return;
* }
* ----------------------------------------------------------------
*/
#include
#include
#include
#include
#include "logger.h"
void LogToNone(char *format, ...);
Boolean DoNotIntercept(EventRecord event);
#ifdef USE_LOGGER
typedef struct _LogTextState {
int font;
int size;
int mode;
int face;
} LogTextState;
#define CONSOLE_BAR_SIZE 12
#define CONSOLE_TEXT_FONT monaco
#define CONSOLE_TEXT_SIZE 9
#define CONSOLE_MIN_HEIGHT 67
#define CONSOLE_MIN_WIDTH 150
LogTextState logSavedTextState;
PenState logSavedPentState;
void UseLogger(LogLevel level, LogOutputMethod out, char *filePath);
void LogConsoleInit(void);
void LogConsoleDraw(void);
void LogConsoleUpdate(void);
void LogConsoleInitScrollbar(void);
void LogConsoleResetTE(void);
void LogConsoleResetScroll(void);
void LogConsoleScrollToBottom(void);
void LogConsoleSetLayout();
void LogConsoleResize(Rect* newBounds);
void LogConsoleZoom(void);
void LogConsoleClear(void);
void LogToConsole(char *format, ...);
void LogToFile(char *format, ...);
void LogDefault(char *format, ...);
void LogDebug(char *format, ...);
void LogInfo(char *format, ...);
void LogWarn(char *format, ...);
void LogError(char *format, ...);
int DoLogGrow(Point p);
pascal void DoLogScroll(ControlHandle ctl, short ctlEventIn);
Boolean DoLogEvent(EventRecord event);
void LogSaveTextState(void);
void LogRestoreTextState(void);
#endif
Logger logger;
void
LogToNone(char* format, ...)
{
(void) format; // unused.
return;
}
Boolean
DoNotIntercept(EventRecord event)
{
return false;
}
void
InitLogger(LogLevel level,
LogOutputMethod out,
char* filePath)
{
logger.print = LogToNone;
logger.debug = LogToNone;
logger.info = LogToNone;
logger.warn = LogToNone;
logger.error = LogToNone;
logger.onEvent = DoNotIntercept;
#ifdef USE_LOGGER
UseLogger(level,out, filePath);
#endif
}
#ifdef USE_LOGGER // Only load if required.
void
UseLogger(LogLevel level,
LogOutputMethod out,
char* filePath)
{
logger.log = LogDefault;
logger.debug = LogDebug;
logger.info = LogInfo;
logger.warn = LogWarn;
logger.error = LogError;
logger.logLevel = level;
logger.outputMethod = out;
logger.filePath = filePath;
logger.console.textArea = NULL;
if (out == CONSOLE_OUT) {
logger.onEvent = DoLogEvent;
LogConsoleInit();
}
else if (out == FILE_OUT) {
FILE* file;
// Using Unix New Line character for tail to
// read correctly on Linux.
//
logger.newLine = "\x0A\r";
logger.print = LogToFile;
// Empty log file. If the file cannot be created, the log output
// is set to NONE_OUT;
//
file = fopen(logger.filePath, "w");
if(!file) {
logger.outputMethod = NONE_OUT;
logger.print = LogToNone;
return;
}
fprintf(file, "");
fclose(file);
}
else {
logger.print = LogToNone;
}
}
void
LogConsoleInit(void)
{
GrafPtr oldPort;
Rect winBounds;
Rect viewRect;
Rect textRect;
logger.console.lineHeight = CONSOLE_TEXT_SIZE + 2;
logger.console.width = screenBits.bounds.right - 2;
logger.console.scrollPos = 0;
logger.console.maxScrollPos = 0;
logger.console.nbLines = 0;
logger.console.contentHeight = 0;
logger.console.nbOutputLines = CONSOLE_LINES;
logger.console.height = (logger.console.lineHeight * CONSOLE_LINES)
+ (CONSOLE_BAR_SIZE * 2) - 1;
// Make prevBounds to the zoomed in position for zoom toggle
//
logger.console.growBounds.left = screenBits.bounds.left + 5;
logger.console.growBounds.right = screenBits.bounds.right - 5;
logger.console.growBounds.top = screenBits.bounds.top + 25;
logger.console.growBounds.bottom = screenBits.bounds.bottom - 5;
logger.console.prevBounds = logger.console.growBounds;
logger.newLine = "\r";
logger.print = LogToConsole;
winBounds.left = screenBits.bounds.left + 1;
winBounds.right = screenBits.bounds.right - 1;
winBounds.bottom = screenBits.bounds.bottom - 1;
winBounds.top = winBounds.bottom - logger.console.height;
logger.console.window = NewWindow(0L, &winBounds, "\pConsole", TRUE,
plainDBox, (WindowPtr)-1L, TRUE, 0);
if (!logger.console.window) {
logger.print = LogToNone;
return;
}
logger.console.origBounds = winBounds;
GetPort(&oldPort);
SetPort(logger.console.window);
SendBehind(logger.console.window, NULL);
LogConsoleSetLayout();
// Tex Edit
//
viewRect = logger.console.view;
//InsetRect(&viewRect, 2, 2);
textRect = viewRect;
InsetRect(&textRect, 2, 0);
logger.console.textArea = TENew(&textRect, &viewRect);
(*(logger.console.textArea))->txFont = CONSOLE_TEXT_FONT;
(*(logger.console.textArea))->txSize = CONSOLE_TEXT_SIZE;
(*(logger.console.textArea))->lineHeight = logger.console.lineHeight;
(*(logger.console.textArea))->fontAscent = logger.console.lineHeight - 2;
(*(logger.console.textArea))->txFace = 0;
//TEActivate(logger.console.textArea);
LogConsoleInitScrollbar();
LogConsoleDraw();
SetPort(oldPort);
}
void LogConsoleDraw(void)
{
Rect r;
Pattern tbarPattern;
GrafPtr oldPort;
GetPort(&oldPort);
SetPort(logger.console.window);
LogSaveTextState();
GetPenState(&logSavedPentState);
PenNormal();
EraseRect(&logger.console.view);
GetIndPattern(&tbarPattern, sysPatListID, 24);
FillRect(&logger.console.titleBar, tbarPattern);
FrameRect(&logger.console.titleBar);
FillRect(&logger.console.bottomBar, white);
FrameRect(&logger.console.bottomBar);
FillRect(&logger.console.growBtn, white);
FrameRect(&logger.console.growBtn);
r = logger.console.growBtn;
InsetRect(&r, 5, 3);
OffsetRect(&r, 1, 1);
FrameRect(&r);
r.right -= 1;
r.bottom -= 1;
OffsetRect(&r, -2, -2);
FillRect(&r, white);
FrameRect(&r);
// Lower button
//
FillRect(&logger.console.lowerBtn, white);
FrameRect(&logger.console.lowerBtn);
MoveTo(logger.console.lowerBtn.left + 4,
logger.console.lowerBtn.bottom - 2);
TextFont(geneva);
TextFace(0);
TextSize(12);
DrawString("\p_");
// Clear button
//
FillRect(&logger.console.clearBtn, white);
FrameRect(&logger.console.clearBtn);
MoveTo(logger.console.clearBtn.left + 4,
logger.console.clearBtn.bottom - 2);
TextFont(geneva);
TextFace(0);
TextSize(8);
DrawString("\pclear");
// Close button
//
FillRect(&logger.console.closeBtn, white);
FrameRect(&logger.console.closeBtn);
// Expand button
//
FillRect(&logger.console.zoomBtn, white);
FrameRect(&logger.console.zoomBtn);
LogConsoleUpdate();
LogRestoreTextState();
SetPenState(&logSavedPentState);
SetPort(oldPort);
}
void LogConsoleUpdate(void)
{
TEUpdate(&logger.console.view, logger.console.textArea);
DrawControls(logger.console.window);
}
void
LogConsoleInitScrollbar(void)
{
GrafPtr oldPort;
Rect scrollbarRect;
int value;
GetPort(&oldPort);
SetPort(logger.console.window);
scrollbarRect.left = logger.console.window->portRect.right - 15;
scrollbarRect.right = logger.console.window->portRect.right + 1;
scrollbarRect.top = logger.console.titleBar.bottom - 1;
scrollbarRect.bottom = logger.console.bottomBar.top + 1;
value = logger.console.contentHeight - logger.console.height;
logger.console.vScroll = NewControl(logger.console.window,
&scrollbarRect,
"\p",
TRUE,
0, // intinial value;
0, // min
value, // max
scrollBarProc,
0);
SetCtlMax(logger.console.vScroll,
logger.console.contentHeight - logger.console.height);
SetCtlValue(logger.console.vScroll, logger.console.scrollPos);
HiliteControl(logger.console.vScroll, 255);
SetPort(oldPort);
}
void
LogConsoleResetTE(void)
{
Rect viewRect = logger.console.view;
//InsetRect(&viewRect, 2, 2);
// Resize the size of the text area
//
(*(logger.console.textArea))->viewRect = viewRect;
(*(logger.console.textArea))->destRect.right = viewRect.right - 1;
}
void
LogConsoleResetScroll(void)
{
int oldMaxScrollPos = logger.console.maxScrollPos;
logger.console.maxScrollPos = (logger.console.nbLines
- logger.console.nbOutputLines)
* logger.console.lineHeight;
if (logger.console.maxScrollPos <= 0) {
// If there is nothing to scroll,
// put everything back at the top
//
TEScroll(0, logger.console.scrollPos, logger.console.textArea);
logger.console.scrollPos = 0;
logger.console.maxScrollPos = 0;
}
else if (logger.console.scrollPos > logger.console.maxScrollPos) {
// Window is growing
//
if (logger.console.maxScrollPos < oldMaxScrollPos) {
int delta = logger.console.scrollPos - logger.console.maxScrollPos;
TEScroll(0, delta, logger.console.textArea);
}
logger.console.scrollPos = logger.console.maxScrollPos;
}
else {
HiliteControl(logger.console.vScroll, 0);
}
MoveControl(logger.console.vScroll,
logger.console.bounds.right - 15,
logger.console.titleBar.bottom - 1);
SizeControl(logger.console.vScroll,
16,
logger.console.bottomBar.top
- logger.console.titleBar.bottom + 2);
SetCtlMax(logger.console.vScroll, logger.console.maxScrollPos);
SetCtlValue(logger.console.vScroll, logger.console.scrollPos);
}
void
LogConsoleScrollToBottom(void)
{
int delta = logger.console.maxScrollPos - logger.console.scrollPos;
logger.console.scrollPos = logger.console.maxScrollPos;
SetCtlValue(logger.console.vScroll, logger.console.scrollPos);
TEScroll(0, -delta, logger.console.textArea);
LogConsoleUpdate();
}
void
LogConsoleSetLayout(void)
{
logger.console.bounds = logger.console.window->portRect;
logger.console.view = logger.console.bounds;
logger.console.view.right -= 15;
logger.console.view.top += CONSOLE_BAR_SIZE;
logger.console.view.bottom -= CONSOLE_BAR_SIZE - 1;
// Title bar & buttons
//
logger.console.titleBar.left = logger.console.bounds.left - 1;
logger.console.titleBar.right = logger.console.bounds.right + 1;
logger.console.titleBar.top = logger.console.bounds.top - 1;
logger.console.titleBar.bottom = logger.console.titleBar.top
+ CONSOLE_BAR_SIZE + 1;
logger.console.bottomBar.left = logger.console.bounds.left - 1;
logger.console.bottomBar.right = logger.console.bounds.right + 1;
logger.console.bottomBar.top = logger.console.bounds.bottom
- CONSOLE_BAR_SIZE +1;
logger.console.bottomBar.bottom = logger.console.bounds.bottom + 1;
logger.console.zoomBtn.right = logger.console.bounds.right - 5;
logger.console.zoomBtn.left = logger.console.zoomBtn.right - 9;
logger.console.zoomBtn.top = logger.console.bounds.top + 1;
logger.console.zoomBtn.bottom = logger.console.zoomBtn.top + 9;
logger.console.growBtn.right = logger.console.bounds.right + 1;
logger.console.growBtn.left = logger.console.growBtn.right - 16;
logger.console.growBtn.bottom = logger.console.bounds.bottom + 1;
logger.console.growBtn.top = logger.console.growBtn.bottom
- CONSOLE_BAR_SIZE;
logger.console.lowerBtn.right = logger.console.zoomBtn.left - 10;
logger.console.lowerBtn.left = logger.console.lowerBtn.right - 16;
logger.console.lowerBtn.top = logger.console.bounds.top + 1;
logger.console.lowerBtn.bottom = logger.console.lowerBtn.top + 9;
logger.console.clearBtn.right = logger.console.lowerBtn.left -4;
logger.console.clearBtn.left = logger.console.clearBtn.right - 26;
logger.console.clearBtn.top = logger.console.bounds.top + 1;
logger.console.clearBtn.bottom = logger.console.clearBtn.top + 9;
logger.console.closeBtn.left = logger.console.bounds.left + 5;
logger.console.closeBtn.right = logger.console.closeBtn.left + 9;
logger.console.closeBtn.top = logger.console.bounds.top + 1;
logger.console.closeBtn.bottom = logger.console.closeBtn.top + 9;
}
void
LogConsoleResize(Rect* newBounds)
{
int width = newBounds->right - newBounds->left;
int height = newBounds->bottom - newBounds->top;
int nbLines = (int)((height - (CONSOLE_BAR_SIZE * 2) + 1)
/ logger.console.lineHeight);
// Adjust the hieight of the window to match the number of
// lines and line height of the TE
//
logger.console.nbOutputLines = nbLines;
logger.console.width = width;
logger.console.height = (logger.console.lineHeight * nbLines)
+ (CONSOLE_BAR_SIZE * 2) - 1;
newBounds->bottom = newBounds->top + logger.console.height;
MoveWindow(logger.console.window,
newBounds->left,
newBounds->top,
TRUE);
SizeWindow(logger.console.window,
logger.console.width,
logger.console.height,
FALSE);
EraseRect(newBounds);
LogConsoleSetLayout();
LogConsoleResetTE();
LogConsoleResetScroll();
LogConsoleDraw();
//LogConsoleScrollToBottom();
}
void
LogConsoleZoom(void)
{
Point topLeft;
Point bottomRight;
topLeft.h = logger.console.window->portRect.left;
topLeft.v = logger.console.window->portRect.top;
bottomRight.h = logger.console.window->portRect.right;
bottomRight.v = logger.console.window->portRect.bottom;
LocalToGlobal(&topLeft);
LocalToGlobal(&bottomRight);
LogConsoleResize(&logger.console.prevBounds);
logger.console.prevBounds.left = topLeft.h;
logger.console.prevBounds.top = topLeft.v;
logger.console.prevBounds.right = bottomRight.h;
logger.console.prevBounds.bottom = bottomRight.v;
}
void
LogConsoleClear(void)
{
GrafPtr oldPort;
Rect scrollbarRect;
int value;
GetPort(&oldPort);
SetPort(logger.console.window);
// Move back to the top of the text area
//
TEScroll(0, logger.console.scrollPos, logger.console.textArea);
// Select all and erase
//
TESetSelect(0,32767,logger.console.textArea);
TEDelete(logger.console.textArea);
logger.console.scrollPos = 0;
logger.console.maxScrollPos = 0;
logger.console.nbLines = 0;
logger.console.contentHeight = 0;
SetCtlMax(logger.console.vScroll, 0);
SetCtlValue(logger.console.vScroll, 0);
HiliteControl(logger.console.vScroll, 255);
LogConsoleUpdate();
SetPort(oldPort);
}
void
LogToConsole(char* format, ...)
{
va_list ap;
char log[256] = "";
int oldMaxScroll = logger.console.maxScrollPos;
GrafPtr oldPort;
GetPort(&oldPort);
SetPort(logger.console.window);
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
TEInsert(log, strlen(log), logger.console.textArea);
// Update scroll data
//
logger.console.nbLines = (*(logger.console.textArea))->nLines;
logger.console.contentHeight = logger.console.nbLines * logger.console.lineHeight;
logger.console.maxScrollPos = (logger.console.nbLines
- logger.console.nbOutputLines)
* logger.console.lineHeight;
SetCtlMax(logger.console.vScroll, logger.console.maxScrollPos);
// Auto-scroll to bottom (do this only once)
//
if (logger.console.nbLines == logger.console.nbOutputLines + 1) {
// Enable the scrollbar once it is needed
//
HiliteControl(logger.console.vScroll, 0);
}
if (logger.console.nbLines > logger.console.nbOutputLines &&
logger.console.scrollPos == oldMaxScroll) {
LogConsoleScrollToBottom();
}
SetPort(oldPort);
ShowHide(logger.console.window, TRUE);
}
void
LogToFile(char* format, ...)
{
FILE* file;
va_list ap;
// Open file in Append Binary mode so the
// new line character is not converted.
//
file = fopen(logger.filePath, "ab");
if(!file) {
return;
}
va_start(ap, format);
vfprintf(file, format, ap);
va_end(ap);
fclose(file);
}
void
LogDefault(char* format, ...)
{
va_list ap;
char log[256] = "";
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
logger.print("%s %s", log, logger.newLine);
}
void
LogDebug(char* format, ...)
{
va_list ap;
char log[256] = "";
if (logger.logLevel < DEBUG_LEVEL) {
return;
}
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
logger.print("DEBUG: %s %s", log, logger.newLine);
}
void
LogInfo(char* format, ...)
{
va_list ap;
char log[256] = "";
if (logger.logLevel < INFO_LEVEL) {
return;
}
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
logger.print("INFO: %s %s", log, logger.newLine);
}
void
LogWarn(char* format, ...)
{
va_list ap;
char log[256] = "";
if (logger.logLevel < WARNING_LEVEL) {
return;
}
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
logger.print("WARNING: %s %s", log, logger.newLine);
}
void
LogError(char* format, ...)
{
va_list ap;
char log[256] = "";
if (logger.logLevel < ERROR_LEVEL) {
return;
}
va_start(ap, format);
vsprintf(log, format, ap);
va_end(ap);
logger.print("ERROR: %s %s", log, logger.newLine);
}
int
DoLogGrow(Point p)
{
Rect newBounds;
GrafPtr savePort;
Rect limits;
long theResult;
Point orig = {0, 0};
GetPort(&savePort);
SetPort(logger.console.window);
SetRect(&limits, CONSOLE_MIN_WIDTH, // Min horizontal size
CONSOLE_MIN_HEIGHT, // Min vertical size
screenBits.bounds.right, // Max horizontal size
screenBits.bounds.bottom); // Max vertical size
theResult = GrowWindow(logger.console.window, p, &limits);
if (theResult == 0) {
return 0;
}
LocalToGlobal(&orig);
newBounds.left = orig.h;
newBounds.right = newBounds.left + LoWord(theResult);
newBounds.top = orig.v;
newBounds.bottom = newBounds.top + HiWord(theResult);
LogConsoleResize(&newBounds);
logger.console.prevBounds = logger.console.growBounds;
return 1;
}
pascal void DoLogScroll(ControlHandle ctl, short ctlEventIn)
{
int value = logger.console.scrollPos;
int orig = value;
(void) ctl; // unused
switch (ctlEventIn) {
case inUpButton:
value -= logger.console.lineHeight;
break;
case inDownButton:
value += logger.console.lineHeight;
break;
case inPageUp:
value -= logger.console.lineHeight * (logger.console.nbOutputLines - 1);
break;
case inPageDown:
value += logger.console.lineHeight * (logger.console.nbOutputLines - 1);
break;
}
// Clamp
//
if (value < 0) {
value = 0;
}
else if (value > logger.console.maxScrollPos) {
value = logger.console.maxScrollPos;
}
if (value != orig) {
logger.console.scrollPos = value;
SetCtlValue(logger.console.vScroll, logger.console.scrollPos);
TEScroll(0, -(value - orig), logger.console.textArea);
LogConsoleUpdate();
}
}
Boolean DoLogEvent(EventRecord event)
{
Boolean interceped = FALSE;
GrafPtr oldPort;
WindowPtr eventWin;
ControlHandle ctl;
int eventIn, ctlEventIn;
Point localPt;
if (logger.outputMethod != CONSOLE_OUT) {
return FALSE;
}
GetPort(&oldPort);
SetPort(logger.console.window);
localPt = event.where;
GlobalToLocal(&localPt);
switch (event.what) {
case nullEvent:
break;
case mouseDown:
eventIn = FindWindow(event.where, &eventWin);
if (eventWin != logger.console.window) {
interceped = FALSE;
break;
}
SelectWindow(eventWin);
switch (eventIn) {
case inContent:
ctlEventIn = FindControl(localPt, eventWin, &ctl);
if (PtInRect(localPt, &logger.console.closeBtn)) {
HideWindow(logger.console.window);
interceped = TRUE;
}
else if (PtInRect(localPt, &logger.console.lowerBtn)) {
Rect origBounds = logger.console.origBounds;
logger.console.prevBounds = logger.console.growBounds;
LogConsoleResize(&origBounds);
LogConsoleScrollToBottom();
}
else if (PtInRect(localPt, &logger.console.clearBtn)) {
LogConsoleClear();
}
else if (PtInRect(localPt, &logger.console.zoomBtn)) {
LogConsoleZoom();
LogConsoleScrollToBottom();
}
else if (PtInRect(localPt, &logger.console.titleBar)) {
DragWindow(eventWin, event.where, &screenBits.bounds);
InvalRect(&screenBits.bounds);
}
else if (PtInRect(localPt, &logger.console.growBtn)) {
DoLogGrow(event.where);
}
else if (ctl == logger.console.vScroll) {
if (ctlEventIn == inThumb) {
int ctlValue;
int delta;
// Dragging the thumb
//
TrackControl(ctl, localPt, NULL);
ctlValue = GetCtlValue(ctl);
delta = ctlValue - logger.console.scrollPos;
logger.console.scrollPos = ctlValue;
TEScroll(0, -delta, logger.console.textArea);
LogConsoleUpdate();
}
else {
// Buttons + page scroll
//
TrackControl(ctl, localPt, DoLogScroll);
}
}
interceped = TRUE;
break;
case inGrow:
logger.debug("In grow");
interceped = TRUE;
break;
case inGoAway:
if (TrackGoAway(eventWin, event.where)) {
HideWindow(logger.console.window);
interceped = TRUE;
}
break;
}
break;
case updateEvt:
eventWin = (WindowPtr)event.message;
if (eventWin && eventWin == logger.console.window) {
BeginUpdate(eventWin);
LogConsoleDraw();
EndUpdate(eventWin);
}
break;
case activateEvt:
break;
}
SetPort(oldPort);
return interceped;
}
void
LogSaveTextState(void)
{
logSavedTextState.font = thePort->txFont;
logSavedTextState.size = thePort->txSize;
logSavedTextState.mode = thePort->txMode;
logSavedTextState.face = thePort->txFace;
}
void
LogRestoreTextState(void)
{
TextFont(logSavedTextState.font);
TextSize(logSavedTextState.size);
TextMode(logSavedTextState.mode);
TextFace(logSavedTextState.face);
}
#endif