Files
egutierrez f1a5e04d4f feat(cpp/core): logger + log_window + selectable_text widgets
Logger global thread-safe con ring buffer in-memory de 2000 entradas + escritura
opcional a archivo. log_window flotante consume el ring buffer con filtros por
nivel, busqueda y autoscroll; se abre desde Settings -> Logs en la menubar.
selectable_text cubre el patron drag-to-select + Ctrl+C en cualquier ventana.

app_menubar y framework run_app integran log_window_render() en el frame loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:50:57 +02:00

157 lines
4.7 KiB
C++

#include "core/logger.h"
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <mutex>
#include <string>
namespace fn_log {
namespace {
std::mutex g_mu;
FILE* g_file = nullptr;
std::string g_path;
Level g_min_level = Level::Info;
// Ring buffer in-memory. g_count es el numero de entradas vivas (clamp a
// capacity); g_head es el indice donde se escribira la proxima entrada.
Entry g_buf[kBufferCapacity];
std::size_t g_count = 0;
std::size_t g_head = 0;
long long now_ms() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
// Formato: "YYYY-MM-DD HH:MM:SS.mmm". out debe tener al menos 24 bytes.
void format_ts(long long ts_ms, char* out, std::size_t out_size) {
std::time_t secs = static_cast<std::time_t>(ts_ms / 1000);
int millis = static_cast<int>(ts_ms % 1000);
if (millis < 0) millis = 0;
if (millis > 999) millis = 999;
std::tm tm_buf{};
#ifdef _WIN32
localtime_s(&tm_buf, &secs);
#else
localtime_r(&secs, &tm_buf);
#endif
std::snprintf(out, out_size, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
(tm_buf.tm_year + 1900) % 10000,
(tm_buf.tm_mon + 1) % 100,
tm_buf.tm_mday % 100,
tm_buf.tm_hour % 100,
tm_buf.tm_min % 100,
tm_buf.tm_sec % 100,
millis);
}
void push_entry(Level level, long long ts_ms, const char* text) {
Entry& e = g_buf[g_head];
e.level = level;
e.ts_ms = ts_ms;
std::snprintf(e.text, kEntryTextMax, "%s", text);
g_head = (g_head + 1) % kBufferCapacity;
if (g_count < kBufferCapacity) ++g_count;
}
// Convierte el indice "logico" (0 = mas antigua) al indice fisico del array.
std::size_t logical_to_physical(std::size_t i) {
if (g_count < kBufferCapacity) return i; // buffer aun no lleno
return (g_head + i) % kBufferCapacity;
}
void emit(Level level, const char* fmt, std::va_list ap) {
if (static_cast<int>(level) < static_cast<int>(g_min_level)) return;
// msg deja 64 bytes para que el prefijo "[ts] [LEVEL] " quepa siempre en
// el buffer destino sin que -Wformat-truncation se queje.
char msg[kEntryTextMax - 64];
std::vsnprintf(msg, sizeof(msg), fmt, ap);
long long ts = now_ms();
char ts_buf[32];
format_ts(ts, ts_buf, sizeof(ts_buf));
char line[kEntryTextMax];
std::snprintf(line, sizeof(line), "[%s] [%s] %s",
ts_buf, level_label(level), msg);
std::lock_guard<std::mutex> lk(g_mu);
push_entry(level, ts, line);
if (g_file) {
std::fputs(line, g_file);
std::fputc('\n', g_file);
std::fflush(g_file);
}
}
} // namespace
bool logger_init(const char* file_path, Level min_level) {
std::lock_guard<std::mutex> lk(g_mu);
if (g_file) {
std::fclose(g_file);
g_file = nullptr;
}
g_min_level = min_level;
g_path.clear();
if (!file_path || !*file_path) return false;
g_file = std::fopen(file_path, "a");
if (!g_file) {
std::fprintf(stderr, "[fn_log] no pude abrir %s para append\n", file_path);
return false;
}
g_path = file_path;
return true;
}
void logger_close() {
std::lock_guard<std::mutex> lk(g_mu);
if (g_file) {
std::fclose(g_file);
g_file = nullptr;
}
g_path.clear();
}
void logger_set_level(Level level) { std::lock_guard<std::mutex> lk(g_mu); g_min_level = level; }
Level logger_level() { std::lock_guard<std::mutex> lk(g_mu); return g_min_level; }
const char* logger_path() { return g_path.c_str(); }
void log_debug(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Debug, fmt, ap); va_end(ap); }
void log_info (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Info, fmt, ap); va_end(ap); }
void log_warn (const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Warn, fmt, ap); va_end(ap); }
void log_error(const char* fmt, ...) { std::va_list ap; va_start(ap, fmt); emit(Level::Error, fmt, ap); va_end(ap); }
std::size_t buffer_size() { std::lock_guard<std::mutex> lk(g_mu); return g_count; }
const Entry* buffer_at(std::size_t i) {
std::lock_guard<std::mutex> lk(g_mu);
if (i >= g_count) return nullptr;
return &g_buf[logical_to_physical(i)];
}
void buffer_clear() {
std::lock_guard<std::mutex> lk(g_mu);
g_count = 0;
g_head = 0;
}
const char* level_label(Level level) {
switch (level) {
case Level::Debug: return "DEBUG";
case Level::Info: return "INFO";
case Level::Warn: return "WARN";
case Level::Error: return "ERROR";
}
return "?";
}
} // namespace fn_log