add c and jai

This commit is contained in:
2026-07-13 13:02:47 -04:00
parent cf8342f8d4
commit c5b14d7c41
33 changed files with 6306 additions and 0 deletions

53
c/base/base_arena.c Normal file
View File

@@ -0,0 +1,53 @@
#include "base/base_arena.h"
#include <stdlib.h>
Arena *arena_alloc(U64 cap) {
U8 *mem = (U8 *)malloc(sizeof(Arena) + cap);
if (!mem) return NULL;
Arena *arena = (Arena *)mem;
arena->base = mem + sizeof(Arena);
arena->pos = 0;
arena->cap = cap;
return arena;
}
void arena_release(Arena *arena) {
if (arena) free(arena);
}
void *arena_push(Arena *arena, U64 size) {
U64 aligned = AlignPow2(size, 8);
if (arena->pos + aligned > arena->cap) {
Assert(!"Arena overflow");
return NULL;
}
void *result = arena->base + arena->pos;
arena->pos += aligned;
MemoryZero(result, aligned);
return result;
}
void *arena_push_no_zero(Arena *arena, U64 size) {
U64 aligned = AlignPow2(size, 8);
if (arena->pos + aligned > arena->cap) {
Assert(!"Arena overflow");
return NULL;
}
void *result = arena->base + arena->pos;
arena->pos += aligned;
return result;
}
U64 arena_pos(Arena *arena) {
return arena->pos;
}
void arena_pop_to(Arena *arena, U64 pos) {
if (pos < arena->pos) {
arena->pos = pos;
}
}
void arena_clear(Arena *arena) {
arena->pos = 0;
}

44
c/base/base_arena.h Normal file
View File

@@ -0,0 +1,44 @@
#pragma once
// base_arena.h - Linear arena allocator
// Simplified from raddebugger's virtual-memory-backed arena to a malloc-based one.
// Suitable for per-frame scratch allocations and persistent state.
#include "base/base_core.h"
////////////////////////////////
// Arena type
typedef struct Arena {
U8 *base;
U64 pos;
U64 cap;
} Arena;
// Temporary scope (save/restore position)
typedef struct Temp {
Arena *arena;
U64 pos;
} Temp;
////////////////////////////////
// Arena functions
Arena *arena_alloc(U64 cap);
void arena_release(Arena *arena);
void *arena_push(Arena *arena, U64 size);
void *arena_push_no_zero(Arena *arena, U64 size);
U64 arena_pos(Arena *arena);
void arena_pop_to(Arena *arena, U64 pos);
void arena_clear(Arena *arena);
////////////////////////////////
// Temporary scope helpers
static inline Temp temp_begin(Arena *arena) { Temp t = {arena, arena->pos}; return t; }
static inline void temp_end(Temp temp) { arena_pop_to(temp.arena, temp.pos); }
////////////////////////////////
// Push helper macros
#define push_array(arena, T, count) ((T *)arena_push((arena), sizeof(T) * (count)))
#define push_array_no_zero(arena, T, count) ((T *)arena_push_no_zero((arena), sizeof(T) * (count)))

181
c/base/base_core.h Normal file
View File

@@ -0,0 +1,181 @@
#pragma once
// base_core.h - Fundamental types, macros, and linked list helpers
// Inspired by raddebugger's base_core.h
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
////////////////////////////////
// Codebase keywords
#ifndef __APPLE__
#define internal static
#define global static
#endif
#define local_persist static
#define trvke 1
////////////////////////////////
// Base types
typedef uint8_t U8;
typedef uint16_t U16;
typedef uint32_t U32;
typedef uint64_t U64;
typedef int8_t S8;
typedef int16_t S16;
typedef int32_t S32;
typedef int64_t S64;
typedef S32 B32;
typedef float F32;
typedef double F64;
////////////////////////////////
// Limits
#define max_U8 0xFF
#define max_U16 0xFFFF
#define max_U32 0xFFFFFFFF
#define max_U64 0xFFFFFFFFFFFFFFFFull
#define max_S8 0x7F
#define max_S16 0x7FFF
#define max_S32 0x7FFFFFFF
#define max_S64 0x7FFFFFFFFFFFFFFFll
////////////////////////////////
// Units
#define KB(n) (((U64)(n)) << 10)
#define MB(n) (((U64)(n)) << 20)
#define GB(n) (((U64)(n)) << 30)
////////////////////////////////
// Clamps, Mins, Maxes
#define Min(A, B) (((A) < (B)) ? (A) : (B))
#define Max(A, B) (((A) > (B)) ? (A) : (B))
#define ClampTop(A, X) Min(A, X)
#define ClampBot(X, B) Max(X, B)
#define Clamp(A, X, B) (((X) < (A)) ? (A) : ((X) > (B)) ? (B) : (X))
////////////////////////////////
// Alignment / Sizing
#define AlignPow2(x, b) (((x) + (b) - 1) & (~((b) - 1)))
#define AlignDownPow2(x, b) ((x) & (~((b) - 1)))
#define ArrayCount(a) (sizeof(a) / sizeof((a)[0]))
////////////////////////////////
// Memory macros
#define MemoryCopy(dst, src, size) memmove((dst), (src), (size))
#define MemorySet(dst, byte, size) memset((dst), (byte), (size))
#define MemoryCompare(a, b, size) memcmp((a), (b), (size))
#define MemoryZero(s, z) memset((s), 0, (z))
#define MemoryZeroStruct(s) MemoryZero((s), sizeof(*(s)))
#define MemoryZeroArray(a) MemoryZero((a), sizeof(a))
#define MemoryMatch(a, b, z) (MemoryCompare((a), (b), (z)) == 0)
////////////////////////////////
// Pointer / integer casts
#define IntFromPtr(ptr) ((U64)(ptr))
#define PtrFromInt(i) (void *)(i)
#define OffsetOf(T, m) IntFromPtr(&(((T *)0)->m))
////////////////////////////////
// Member access
#define CastFromMember(T, m, ptr) (T *)(((U8 *)(ptr)) - OffsetOf(T, m))
////////////////////////////////
// For-Loop construct macros
#define DeferLoop(begin, end) for (int _i_ = ((begin), 0); !_i_; _i_ += 1, (end))
#define DeferLoopChecked(begin, end) for (int _i_ = 2 * !(begin); (_i_ == 2 ? ((end), 0) : !_i_); _i_ += 1, (end))
#define EachIndex(it, count) (U64 it = 0; it < (count); it += 1)
#define EachElement(it, array) (U64 it = 0; it < ArrayCount(array); it += 1)
////////////////////////////////
// Glue / Stringify
#define Stringify_(S) #S
#define Stringify(S) Stringify_(S)
#define Glue_(A, B) A##B
#define Glue(A, B) Glue_(A, B)
#define Swap(T, a, b) do { T t__ = a; a = b; b = t__; } while (0)
////////////////////////////////
// Assert
#if defined(_MSC_VER)
# define Trap() __debugbreak()
#elif defined(__clang__) || defined(__GNUC__)
# define Trap() __builtin_trap()
#else
# define Trap() (*(volatile int *)0 = 0)
#endif
#define AssertAlways(x) do { if (!(x)) { Trap(); } } while (0)
#ifdef _DEBUG
# define Assert(x) AssertAlways(x)
#else
# define Assert(x) (void)(x)
#endif
#define InvalidPath Assert(!"Invalid Path!")
#define NotImplemented Assert(!"Not Implemented!")
////////////////////////////////
// Linked list macros
// Nil-aware doubly-linked-list operations
#define CheckNil(nil, p) ((p) == 0 || (p) == nil)
#define SetNil(nil, p) ((p) = nil)
// Doubly-linked-list (with nil support)
#define DLLInsert_NPZ(nil, f, l, p, n, next, prev) \
(CheckNil(nil, f) ? \
((f) = (l) = (n), SetNil(nil, (n)->next), SetNil(nil, (n)->prev)) : \
CheckNil(nil, p) ? \
((n)->next = (f), (f)->prev = (n), (f) = (n), SetNil(nil, (n)->prev)) : \
((p) == (l)) ? \
((l)->next = (n), (n)->prev = (l), (l) = (n), SetNil(nil, (n)->next)) : \
(((!CheckNil(nil, p) && CheckNil(nil, (p)->next)) ? (0) : ((p)->next->prev = (n))), \
((n)->next = (p)->next), ((p)->next = (n)), ((n)->prev = (p))))
#define DLLPushBack_NPZ(nil, f, l, n, next, prev) DLLInsert_NPZ(nil, f, l, l, n, next, prev)
#define DLLPushFront_NPZ(nil, f, l, n, next, prev) DLLInsert_NPZ(nil, l, f, f, n, prev, next)
#define DLLRemove_NPZ(nil, f, l, n, next, prev) \
(((n) == (f) ? (f) = (n)->next : (0)), \
((n) == (l) ? (l) = (l)->prev : (0)), \
(CheckNil(nil, (n)->prev) ? (0) : ((n)->prev->next = (n)->next)), \
(CheckNil(nil, (n)->next) ? (0) : ((n)->next->prev = (n)->prev)))
// Convenience wrappers using 0 as nil
#define DLLPushBack(f, l, n) DLLPushBack_NPZ(0, f, l, n, next, prev)
#define DLLPushFront(f, l, n) DLLPushFront_NPZ(0, f, l, n, next, prev)
#define DLLRemove(f, l, n) DLLRemove_NPZ(0, f, l, n, next, prev)
// Singly-linked queue (doubly-headed)
#define SLLQueuePush_NZ(nil, f, l, n, next) \
(CheckNil(nil, f) ? \
((f) = (l) = (n), SetNil(nil, (n)->next)) : \
((l)->next = (n), (l) = (n), SetNil(nil, (n)->next)))
#define SLLQueuePush(f, l, n) SLLQueuePush_NZ(0, f, l, n, next)
#define SLLQueuePushFront(f, l, n) (((n)->next = (f)), ((f) = (n)))
#define SLLQueuePop(f, l) ((f) == (l) ? ((f) = 0, (l) = 0) : ((f) = (f)->next))
// Singly-linked stack
#define SLLStackPush(f, n) ((n)->next = (f), (f) = (n))
#define SLLStackPop(f) ((f) = (f)->next)

3
c/base/base_inc.c Normal file
View File

@@ -0,0 +1,3 @@
// base_inc.c - Unity build for the base layer
#include "base/base_arena.c"
#include "base/base_strings.c"

8
c/base/base_inc.h Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
// base_inc.h - Umbrella include for the base layer
// Include this one header to get all base types.
#include "base/base_core.h"
#include "base/base_arena.h"
#include "base/base_math.h"
#include "base/base_strings.h"

110
c/base/base_math.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
// base_math.h - Vector, range, and color types
// Inspired by raddebugger's base_math.h
#include "base/base_core.h"
////////////////////////////////
// Axis enum
typedef enum Axis2 {
Axis2_X = 0,
Axis2_Y = 1,
Axis2_COUNT,
} Axis2;
typedef enum Side {
Side_Min = 0,
Side_Max = 1,
Side_COUNT,
} Side;
typedef enum Corner {
Corner_00 = 0, // top-left
Corner_01 = 1, // top-right
Corner_10 = 2, // bottom-left
Corner_11 = 3, // bottom-right
Corner_COUNT,
} Corner;
////////////////////////////////
// Vector types
typedef struct Vec2F32 { F32 x, y; } Vec2F32;
typedef struct Vec2S32 { S32 x, y; } Vec2S32;
typedef struct Vec3F32 { F32 x, y, z; } Vec3F32;
typedef struct Vec4F32 { F32 x, y, z, w; } Vec4F32;
////////////////////////////////
// Range types
typedef struct Rng1F32 { F32 min, max; } Rng1F32;
typedef struct Rng1S64 { S64 min, max; } Rng1S64;
typedef struct Rng2F32 { Vec2F32 p0, p1; } Rng2F32;
////////////////////////////////
// Constructors
static inline Vec2F32 v2f32(F32 x, F32 y) { Vec2F32 r = {x, y}; return r; }
static inline Vec2S32 v2s32(S32 x, S32 y) { Vec2S32 r = {x, y}; return r; }
static inline Vec3F32 v3f32(F32 x, F32 y, F32 z) { Vec3F32 r = {x, y, z}; return r; }
static inline Vec4F32 v4f32(F32 x, F32 y, F32 z, F32 w) { Vec4F32 r = {x, y, z, w}; return r; }
static inline Rng1F32 rng1f32(F32 min, F32 max) { Rng1F32 r = {min, max}; return r; }
static inline Rng1S64 rng1s64(S64 min, S64 max) { Rng1S64 r = {min, max}; return r; }
static inline Rng2F32 rng2f32(Vec2F32 p0, Vec2F32 p1) { Rng2F32 r = {p0, p1}; return r; }
static inline Rng2F32 rng2f32p(F32 x0, F32 y0, F32 x1, F32 y1) { Rng2F32 r = {{x0, y0}, {x1, y1}}; return r; }
////////////////////////////////
// Vec2F32 operations
static inline Vec2F32 add_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x + b.x, a.y + b.y); }
static inline Vec2F32 sub_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x - b.x, a.y - b.y); }
static inline Vec2F32 mul_2f32(Vec2F32 a, Vec2F32 b) { return v2f32(a.x * b.x, a.y * b.y); }
static inline Vec2F32 scale_2f32(Vec2F32 v, F32 s) { return v2f32(v.x * s, v.y * s); }
// Axis-indexed access
static inline F32 v2f32_axis(Vec2F32 v, Axis2 a) { return a == Axis2_X ? v.x : v.y; }
static inline void v2f32_set_axis(Vec2F32 *v, Axis2 a, F32 val) {
if (a == Axis2_X) v->x = val; else v->y = val;
}
////////////////////////////////
// Vec4F32 operations
static inline Vec4F32 add_4f32(Vec4F32 a, Vec4F32 b) { return v4f32(a.x+b.x, a.y+b.y, a.z+b.z, a.w+b.w); }
static inline Vec4F32 scale_4f32(Vec4F32 v, F32 s) { return v4f32(v.x*s, v.y*s, v.z*s, v.w*s); }
static inline Vec4F32 lerp_4f32(Vec4F32 a, Vec4F32 b, F32 t) {
return v4f32(a.x + (b.x - a.x)*t, a.y + (b.y - a.y)*t, a.z + (b.z - a.z)*t, a.w + (b.w - a.w)*t);
}
////////////////////////////////
// Rng2F32 operations
static inline F32 rng2f32_width(Rng2F32 r) { return r.p1.x - r.p0.x; }
static inline F32 rng2f32_height(Rng2F32 r) { return r.p1.y - r.p0.y; }
static inline Vec2F32 rng2f32_dim(Rng2F32 r) { return v2f32(r.p1.x - r.p0.x, r.p1.y - r.p0.y); }
static inline Vec2F32 rng2f32_center(Rng2F32 r) { return v2f32((r.p0.x + r.p1.x)*0.5f, (r.p0.y + r.p1.y)*0.5f); }
static inline B32 rng2f32_contains(Rng2F32 r, Vec2F32 p) {
return p.x >= r.p0.x && p.x <= r.p1.x && p.y >= r.p0.y && p.y <= r.p1.y;
}
static inline Rng2F32 rng2f32_pad(Rng2F32 r, F32 p) {
return rng2f32p(r.p0.x - p, r.p0.y - p, r.p1.x + p, r.p1.y + p);
}
static inline Rng2F32 rng2f32_shift(Rng2F32 r, Vec2F32 v) {
return rng2f32p(r.p0.x + v.x, r.p0.y + v.y, r.p1.x + v.x, r.p1.y + v.y);
}
static inline Rng2F32 rng2f32_intersect(Rng2F32 a, Rng2F32 b) {
return rng2f32p(Max(a.p0.x, b.p0.x), Max(a.p0.y, b.p0.y),
Min(a.p1.x, b.p1.x), Min(a.p1.y, b.p1.y));
}
// Axis-indexed range dimension
static inline F32 rng2f32_dim_axis(Rng2F32 r, Axis2 a) {
return a == Axis2_X ? (r.p1.x - r.p0.x) : (r.p1.y - r.p0.y);
}
////////////////////////////////
// F32 helpers
static inline F32 lerp_1f32(F32 a, F32 b, F32 t) { return a + (b - a) * t; }
static inline F32 abs_f32(F32 x) { return x < 0 ? -x : x; }

34
c/base/base_strings.c Normal file
View File

@@ -0,0 +1,34 @@
#include "base/base_strings.h"
#include "base/base_arena.h"
Str8 str8_pushf(Arena *arena, const char *fmt, ...) {
va_list args, args2;
va_start(args, fmt);
va_copy(args2, args);
S32 len = vsnprintf(NULL, 0, fmt, args);
va_end(args);
char *buf = push_array(arena, char, len + 1);
vsnprintf(buf, len + 1, fmt, args2);
va_end(args2);
Str8 r = {buf, (U64)len};
return r;
}
Str8 str8_push_copy(Arena *arena, Str8 s) {
if (s.size == 0 || !s.str) { Str8 r = {NULL, 0}; return r; }
char *buf = push_array_no_zero(arena, char, s.size + 1);
MemoryCopy(buf, s.str, s.size);
buf[s.size] = 0;
Str8 r = {buf, s.size};
return r;
}
void str8_list_push(Arena *arena, Str8List *list, Str8 s) {
Str8Node *node = push_array(arena, Str8Node, 1);
node->string = s;
SLLQueuePush(list->first, list->last, node);
list->count++;
list->total_size += s.size;
}

49
c/base/base_strings.h Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
// base_strings.h - Simple length-delimited string type
// Inspired by raddebugger's String8
#include "base/base_core.h"
////////////////////////////////
// String types
typedef struct Str8 {
const char *str;
U64 size;
} Str8;
typedef struct Str8Node {
struct Str8Node *next;
Str8 string;
} Str8Node;
typedef struct Str8List {
Str8Node *first;
Str8Node *last;
U64 count;
U64 total_size;
} Str8List;
////////////////////////////////
// Forward declaration for Arena
typedef struct Arena Arena;
////////////////////////////////
// Constructors
static inline Str8 str8(const char *s, U64 len) { Str8 r = {s, len}; return r; }
static inline Str8 str8_cstr(const char *s) { Str8 r = {s, s ? (U64)strlen(s) : 0}; return r; }
static inline Str8 str8_lit(const char *s) { Str8 r = {s, s ? (U64)strlen(s) : 0}; return r; }
static inline B32 str8_match(Str8 a, Str8 b) {
if (a.size != b.size) return 0;
return MemoryCompare(a.str, b.str, a.size) == 0;
}
static inline B32 str8_is_empty(Str8 s) { return s.size == 0 || s.str == NULL; }
////////////////////////////////
// String operations (require arena)
Str8 str8_pushf(Arena *arena, const char *fmt, ...);
Str8 str8_push_copy(Arena *arena, Str8 s);
void str8_list_push(Arena *arena, Str8List *list, Str8 s);

707
c/build.h Normal file
View File

@@ -0,0 +1,707 @@
// build.h — Minimal C build system (stb-style single header)
//
// Define BUILD_IMPLEMENTATION in exactly one file before including this header.
//
// Bootstrap (one-time):
// Windows: cl /nologo build.c
// macOS: cc build.c -o build
// After that, just run ./build (or build.exe) — it rebuilds itself.
#ifndef BUILD_H
#define BUILD_H
#ifdef _MSC_VER
# define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <errno.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <direct.h>
# include <io.h>
#else
# include <sys/stat.h>
# include <sys/wait.h>
# include <unistd.h>
#endif
////////////////////////////////
// Macros
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
// Self-rebuild: call at the top of main(). Recompiles the build script
// if the source file is newer than the running binary, then re-executes.
#define GO_REBUILD_URSELF(argc, argv) \
go_rebuild_urself((argc), (argv), __FILE__)
////////////////////////////////
// Logging
typedef enum {
LOG_INFO,
LOG_WARNING,
LOG_ERROR,
} Log_Level;
void build_log(Log_Level level, const char *fmt, ...);
////////////////////////////////
// Temp allocator — ring buffer for short-lived sprintf results
#ifndef TEMP_CAPACITY
#define TEMP_CAPACITY (8 * 1024 * 1024)
#endif
char *temp_sprintf(const char *fmt, ...);
void temp_reset(void);
////////////////////////////////
// String builder — growable byte buffer
typedef struct {
char *items;
size_t count;
size_t capacity;
} String_Builder;
bool sb_read_file(String_Builder *sb, const char *path);
void sb_free(String_Builder *sb);
////////////////////////////////
// File I/O
bool write_entire_file(const char *path, const void *data, size_t size);
bool delete_file(const char *path);
bool rename_file(const char *old_path, const char *new_path);
bool mkdir_if_not_exists(const char *path);
////////////////////////////////
// Rebuild checking — returns 1 if rebuild needed, 0 if up to date, -1 on error
int needs_rebuild(const char *output, const char **inputs, size_t count);
int needs_rebuild1(const char *output, const char *input);
////////////////////////////////
// Command builder and runner
typedef struct {
const char **items;
size_t count;
size_t capacity;
} Cmd;
// Appends arguments to a command. Use via the macro which auto-counts args.
void cmd__append(Cmd *cmd, size_t n, ...);
void cmd__append_arr(Cmd *cmd, const char **args, size_t n);
#define cmd_append(cmd, ...) do { \
const char *_cmd_args[] = {__VA_ARGS__}; \
cmd__append_arr((cmd), _cmd_args, sizeof(_cmd_args) / sizeof(_cmd_args[0])); \
} while(0)
// Runs the command synchronously, resets cmd->count to 0, returns success.
bool cmd_run(Cmd *cmd);
// Frees the command's allocated memory.
void cmd_free(Cmd *cmd);
////////////////////////////////
// Self-rebuild
void go_rebuild_urself(int argc, char **argv, const char *source);
////////////////////////////////
// File embedding — convert a binary file to a C header with an array + size.
bool embed_file(const char *input_path, const char *output_path, const char *var_name);
////////////////////////////////
// SPIR-V shader compilation — compile .glsl to .spv via glslc, then embed as C header.
bool compile_shader(const char *glslc_path, const char *src, const char *spv_path, const char *stage);
bool embed_spirv(const char *spv_path, const char *header_path, const char *array_name);
#endif // BUILD_H
////////////////////////////////
// Implementation
////////////////////////////////
#ifdef BUILD_IMPLEMENTATION
////////////////////////////////
// Temp allocator
static size_t g_temp_size = 0;
static char g_temp[TEMP_CAPACITY];
void temp_reset(void) {
g_temp_size = 0;
}
static char *temp_alloc(size_t size) {
if (g_temp_size + size > TEMP_CAPACITY) {
g_temp_size = 0; // wrap around
}
char *result = g_temp + g_temp_size;
g_temp_size += size;
return result;
}
char *temp_sprintf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
va_list args2;
va_copy(args2, args);
int n = vsnprintf(NULL, 0, fmt, args);
va_end(args);
char *result = temp_alloc(n + 1);
vsnprintf(result, n + 1, fmt, args2);
va_end(args2);
return result;
}
////////////////////////////////
// Logging
void build_log(Log_Level level, const char *fmt, ...) {
switch (level) {
case LOG_INFO: fprintf(stderr, "[INFO] "); break;
case LOG_WARNING: fprintf(stderr, "[WARNING] "); break;
case LOG_ERROR: fprintf(stderr, "[ERROR] "); break;
}
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
}
////////////////////////////////
// String builder
static void sb_ensure(String_Builder *sb, size_t needed) {
if (sb->count + needed <= sb->capacity) return;
size_t new_cap = sb->capacity ? sb->capacity * 2 : 256;
while (new_cap < sb->count + needed) new_cap *= 2;
sb->items = (char *)realloc(sb->items, new_cap);
sb->capacity = new_cap;
}
bool sb_read_file(String_Builder *sb, const char *path) {
FILE *f = fopen(path, "rb");
if (!f) {
build_log(LOG_ERROR, "Could not open %s: %s", path, strerror(errno));
return false;
}
fseek(f, 0, SEEK_END);
#ifdef _WIN32
long long m = _telli64(_fileno(f));
#else
long long m = ftell(f);
#endif
if (m < 0) {
build_log(LOG_ERROR, "Could not get size of %s: %s", path, strerror(errno));
fclose(f);
return false;
}
fseek(f, 0, SEEK_SET);
sb_ensure(sb, (size_t)m);
fread(sb->items + sb->count, (size_t)m, 1, f);
if (ferror(f)) {
build_log(LOG_ERROR, "Could not read %s: %s", path, strerror(errno));
fclose(f);
return false;
}
sb->count += (size_t)m;
fclose(f);
return true;
}
void sb_free(String_Builder *sb) {
free(sb->items);
sb->items = NULL;
sb->count = 0;
sb->capacity = 0;
}
////////////////////////////////
// File I/O
bool write_entire_file(const char *path, const void *data, size_t size) {
FILE *f = fopen(path, "wb");
if (!f) {
build_log(LOG_ERROR, "Could not open %s for writing: %s", path, strerror(errno));
return false;
}
const char *buf = (const char *)data;
while (size > 0) {
size_t n = fwrite(buf, 1, size, f);
if (ferror(f)) {
build_log(LOG_ERROR, "Could not write to %s: %s", path, strerror(errno));
fclose(f);
return false;
}
size -= n;
buf += n;
}
fclose(f);
return true;
}
bool delete_file(const char *path) {
build_log(LOG_INFO, "Deleting %s", path);
#ifdef _WIN32
DWORD attr = GetFileAttributesA(path);
if (attr == INVALID_FILE_ATTRIBUTES) return true; // doesn't exist
if (attr & FILE_ATTRIBUTE_DIRECTORY) {
if (!RemoveDirectoryA(path)) {
build_log(LOG_ERROR, "Could not delete directory %s", path);
return false;
}
} else {
if (!DeleteFileA(path)) {
build_log(LOG_ERROR, "Could not delete file %s", path);
return false;
}
}
#else
if (remove(path) < 0 && errno != ENOENT) {
build_log(LOG_ERROR, "Could not delete %s: %s", path, strerror(errno));
return false;
}
#endif
return true;
}
bool rename_file(const char *old_path, const char *new_path) {
build_log(LOG_INFO, "Renaming %s -> %s", old_path, new_path);
#ifdef _WIN32
if (!MoveFileEx(old_path, new_path, MOVEFILE_REPLACE_EXISTING)) {
build_log(LOG_ERROR, "Could not rename %s to %s", old_path, new_path);
return false;
}
#else
if (rename(old_path, new_path) < 0) {
build_log(LOG_ERROR, "Could not rename %s to %s: %s", old_path, new_path, strerror(errno));
return false;
}
#endif
return true;
}
bool mkdir_if_not_exists(const char *path) {
#ifdef _WIN32
int result = _mkdir(path);
#else
int result = mkdir(path, 0755);
#endif
if (result < 0) {
if (errno == EEXIST) return true;
build_log(LOG_ERROR, "Could not create directory %s: %s", path, strerror(errno));
return false;
}
build_log(LOG_INFO, "Created directory %s", path);
return true;
}
////////////////////////////////
// Rebuild checking
int needs_rebuild(const char *output, const char **inputs, size_t count) {
#ifdef _WIN32
HANDLE out_h = CreateFile(output, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
if (out_h == INVALID_HANDLE_VALUE) {
if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1;
build_log(LOG_ERROR, "Could not open %s", output);
return -1;
}
FILETIME out_time;
if (!GetFileTime(out_h, NULL, NULL, &out_time)) {
CloseHandle(out_h);
build_log(LOG_ERROR, "Could not get time of %s", output);
return -1;
}
CloseHandle(out_h);
for (size_t i = 0; i < count; i++) {
HANDLE in_h = CreateFile(inputs[i], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
if (in_h == INVALID_HANDLE_VALUE) {
build_log(LOG_ERROR, "Could not open %s", inputs[i]);
return -1;
}
FILETIME in_time;
if (!GetFileTime(in_h, NULL, NULL, &in_time)) {
CloseHandle(in_h);
build_log(LOG_ERROR, "Could not get time of %s", inputs[i]);
return -1;
}
CloseHandle(in_h);
if (CompareFileTime(&in_time, &out_time) == 1) return 1;
}
return 0;
#else
struct stat sb;
memset(&sb, 0, sizeof(sb));
if (stat(output, &sb) < 0) {
if (errno == ENOENT) return 1;
build_log(LOG_ERROR, "Could not stat %s: %s", output, strerror(errno));
return -1;
}
time_t out_time = sb.st_mtime;
for (size_t i = 0; i < count; i++) {
if (stat(inputs[i], &sb) < 0) {
build_log(LOG_ERROR, "Could not stat %s: %s", inputs[i], strerror(errno));
return -1;
}
if (sb.st_mtime > out_time) return 1;
}
return 0;
#endif
}
int needs_rebuild1(const char *output, const char *input) {
return needs_rebuild(output, &input, 1);
}
////////////////////////////////
// Command builder and runner
static void cmd__grow(Cmd *cmd) {
size_t new_cap = cmd->capacity ? cmd->capacity * 2 : 32;
cmd->items = (const char **)realloc(cmd->items, new_cap * sizeof(const char *));
cmd->capacity = new_cap;
}
void cmd__append(Cmd *cmd, size_t n, ...) {
va_list args;
va_start(args, n);
for (size_t i = 0; i < n; i++) {
const char *arg = va_arg(args, const char *);
if (cmd->count >= cmd->capacity) cmd__grow(cmd);
cmd->items[cmd->count++] = arg;
}
va_end(args);
}
void cmd__append_arr(Cmd *cmd, const char **args, size_t n) {
for (size_t i = 0; i < n; i++) {
if (cmd->count >= cmd->capacity) cmd__grow(cmd);
cmd->items[cmd->count++] = args[i];
}
}
static void cmd_render(Cmd *cmd, char *buf, size_t buf_size) {
size_t pos = 0;
for (size_t i = 0; i < cmd->count && pos < buf_size - 1; i++) {
if (i > 0 && pos < buf_size - 1) buf[pos++] = ' ';
const char *arg = cmd->items[i];
size_t len = strlen(arg);
if (pos + len < buf_size - 1) {
memcpy(buf + pos, arg, len);
pos += len;
}
}
buf[pos] = '\0';
}
#ifdef _WIN32
// Properly quote a command line for CreateProcess on Windows.
static void cmd_quote_win32(Cmd *cmd, String_Builder *quoted) {
for (size_t i = 0; i < cmd->count; i++) {
const char *arg = cmd->items[i];
size_t len = strlen(arg);
if (i > 0) {
sb_ensure(quoted, 1);
quoted->items[quoted->count++] = ' ';
}
if (len != 0 && strpbrk(arg, " \t\n\v\"") == NULL) {
sb_ensure(quoted, len);
memcpy(quoted->items + quoted->count, arg, len);
quoted->count += len;
} else {
sb_ensure(quoted, len * 2 + 3);
quoted->items[quoted->count++] = '"';
size_t backslashes = 0;
for (size_t j = 0; j < len; j++) {
char c = arg[j];
if (c == '\\') {
backslashes++;
} else {
if (c == '"') {
for (size_t k = 0; k < backslashes + 1; k++)
quoted->items[quoted->count++] = '\\';
}
backslashes = 0;
}
quoted->items[quoted->count++] = c;
}
for (size_t k = 0; k < backslashes; k++)
quoted->items[quoted->count++] = '\\';
quoted->items[quoted->count++] = '"';
}
}
sb_ensure(quoted, 1);
quoted->items[quoted->count] = '\0';
}
#endif
bool cmd_run(Cmd *cmd) {
if (cmd->count == 0) {
build_log(LOG_ERROR, "Cannot run empty command");
return false;
}
// Log the command
{
char render_buf[4096];
cmd_render(cmd, render_buf, sizeof(render_buf));
build_log(LOG_INFO, "CMD: %s", render_buf);
}
#ifdef _WIN32
STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.dwFlags |= STARTF_USESTDHANDLES;
PROCESS_INFORMATION pi;
memset(&pi, 0, sizeof(pi));
String_Builder quoted = {0};
cmd_quote_win32(cmd, &quoted);
BOOL ok = CreateProcessA(NULL, quoted.items, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
sb_free(&quoted);
cmd->count = 0;
if (!ok) {
build_log(LOG_ERROR, "Could not create process for %s", cmd->items[0]);
return false;
}
CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exit_code;
if (!GetExitCodeProcess(pi.hProcess, &exit_code)) {
build_log(LOG_ERROR, "Could not get exit code");
CloseHandle(pi.hProcess);
return false;
}
CloseHandle(pi.hProcess);
if (exit_code != 0) {
build_log(LOG_ERROR, "Command exited with code %lu", exit_code);
return false;
}
return true;
#else
pid_t pid = fork();
if (pid < 0) {
build_log(LOG_ERROR, "Could not fork: %s", strerror(errno));
cmd->count = 0;
return false;
}
if (pid == 0) {
// Child: build null-terminated argv
const char **argv_null = (const char **)malloc((cmd->count + 1) * sizeof(const char *));
memcpy(argv_null, cmd->items, cmd->count * sizeof(const char *));
argv_null[cmd->count] = NULL;
execvp(argv_null[0], (char *const *)argv_null);
build_log(LOG_ERROR, "Could not exec %s: %s", argv_null[0], strerror(errno));
exit(1);
}
cmd->count = 0;
int wstatus;
for (;;) {
if (waitpid(pid, &wstatus, 0) < 0) {
build_log(LOG_ERROR, "Could not wait on pid %d: %s", pid, strerror(errno));
return false;
}
if (WIFEXITED(wstatus)) {
int code = WEXITSTATUS(wstatus);
if (code != 0) {
build_log(LOG_ERROR, "Command exited with code %d", code);
return false;
}
return true;
}
if (WIFSIGNALED(wstatus)) {
build_log(LOG_ERROR, "Command killed by signal %d", WTERMSIG(wstatus));
return false;
}
}
#endif
}
void cmd_free(Cmd *cmd) {
free(cmd->items);
cmd->items = NULL;
cmd->count = 0;
cmd->capacity = 0;
}
////////////////////////////////
// Self-rebuild
#ifdef _WIN32
# define REBUILD_CMD(binary, source) "cl.exe", "/nologo", temp_sprintf("/Fe:%s", binary), source
#else
# define REBUILD_CMD(binary, source) "cc", "-o", binary, source
#endif
void go_rebuild_urself(int argc, char **argv, const char *source) {
const char *binary = argv[0];
#ifdef _WIN32
// Ensure .exe extension for Windows
size_t len = strlen(binary);
if (len < 4 || strcmp(binary + len - 4, ".exe") != 0) {
binary = temp_sprintf("%s.exe", binary);
}
#endif
int rebuild = needs_rebuild1(binary, source);
if (rebuild < 0) exit(1);
if (rebuild == 0) return;
const char *old_binary = temp_sprintf("%s.old", binary);
if (!rename_file(binary, old_binary)) exit(1);
Cmd cmd = {0};
cmd_append(&cmd, REBUILD_CMD(binary, source));
if (!cmd_run(&cmd)) {
rename_file(old_binary, binary);
exit(1);
}
// Re-execute with the new binary
cmd_append(&cmd, binary);
{
int i;
for (i = 1; i < argc; i++) {
cmd__append(&cmd, 1, argv[i]);
}
}
if (!cmd_run(&cmd)) exit(1);
exit(0);
}
////////////////////////////////
// File embedding
bool embed_file(const char *input_path, const char *output_path, const char *var_name) {
int rebuild = needs_rebuild1(output_path, input_path);
if (rebuild == 0) {
build_log(LOG_INFO, "Up to date: %s", output_path);
return true;
}
if (rebuild < 0) return false;
build_log(LOG_INFO, "Embedding %s -> %s", input_path, output_path);
String_Builder src = {0};
if (!sb_read_file(&src, input_path)) return false;
String_Builder out = {0};
const char *header = temp_sprintf(
"// Auto-generated from %s — do not edit by hand.\n"
"#ifndef %s_GEN_H\n"
"#define %s_GEN_H\n\n"
"static const unsigned int %s_size = %u;\n\n"
"static const unsigned char %s_data[] = {\n",
input_path, var_name, var_name, var_name, (unsigned)src.count, var_name);
size_t header_len = strlen(header);
sb_ensure(&out, header_len);
memcpy(out.items + out.count, header, header_len);
out.count += header_len;
for (size_t i = 0; i < src.count; i += 16) {
sb_ensure(&out, 4 + 16 * 6 + 2);
memcpy(out.items + out.count, " ", 4);
out.count += 4;
size_t end = i + 16;
if (end > src.count) end = src.count;
for (size_t j = i; j < end; j++) {
unsigned char b = (unsigned char)src.items[j];
char hex[7];
int n = snprintf(hex, sizeof(hex), "0x%02x, ", b);
memcpy(out.items + out.count, hex, (size_t)n);
out.count += (size_t)n;
}
out.items[out.count++] = '\n';
}
const char *footer = "};\n\n#endif\n";
size_t footer_len = strlen(footer);
sb_ensure(&out, footer_len);
memcpy(out.items + out.count, footer, footer_len);
out.count += footer_len;
bool ok = write_entire_file(output_path, out.items, out.count);
sb_free(&src);
sb_free(&out);
return ok;
}
////////////////////////////////
// SPIR-V shader compilation
bool compile_shader(const char *glslc_path, const char *src, const char *spv_path, const char *stage) {
Cmd cmd = {0};
cmd_append(&cmd, glslc_path, temp_sprintf("-fshader-stage=%s", stage), "-o", spv_path, src);
return cmd_run(&cmd);
}
bool embed_spirv(const char *spv_path, const char *header_path, const char *array_name) {
String_Builder sb = {0};
if (!sb_read_file(&sb, spv_path)) return false;
FILE *out = fopen(header_path, "wb");
if (!out) {
build_log(LOG_ERROR, "Could not open %s for writing", header_path);
sb_free(&sb);
return false;
}
fprintf(out, "// Auto-generated from %s — do not edit\n", spv_path);
fprintf(out, "#pragma once\n\n");
size_t word_count = sb.count / 4;
fprintf(out, "#include <stdint.h>\n\n");
fprintf(out, "static const uint32_t %s[] = {\n", array_name);
const uint32_t *words = (const uint32_t *)sb.items;
for (size_t i = 0; i < word_count; i++) {
if (i % 8 == 0) fprintf(out, " ");
fprintf(out, "0x%08x,", words[i]);
if (i % 8 == 7 || i == word_count - 1) fprintf(out, "\n");
}
fprintf(out, "};\n");
fclose(out);
build_log(LOG_INFO, "Generated %s (%zu bytes)", header_path, sb.count);
sb_free(&sb);
return true;
}
#endif // BUILD_IMPLEMENTATION

235
c/config/config.c Normal file
View File

@@ -0,0 +1,235 @@
// config.c — Global program configuration (config.ini next to binary)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifdef __APPLE__
#include <unistd.h>
#include <libgen.h>
#include <mach-o/dyld.h>
#else
#include <windows.h>
#endif
Config g_config = {0};
////////////////////////////////
// Path resolution
static void config_get_path(char *out, S32 out_size) {
char exe_path[CONFIG_PATH_MAX];
#ifdef __APPLE__
U32 sz = sizeof(exe_path);
_NSGetExecutablePath(exe_path, &sz);
#else
GetModuleFileNameA(NULL, exe_path, CONFIG_PATH_MAX);
#endif
// Strip binary name to get directory
char *sep = strrchr(exe_path, '/');
#ifndef __APPLE__
char *bsep = strrchr(exe_path, '\\');
if (bsep && (!sep || bsep > sep)) sep = bsep;
#endif
if (sep) sep[1] = '\0';
snprintf(out, out_size, "%sconfig.ini", exe_path);
}
////////////////////////////////
// Helpers
static void config_trim(char *s) {
char *start = s;
while (*start == ' ' || *start == '\t') start++;
if (start != s) memmove(s, start, strlen(start) + 1);
S32 len = (S32)strlen(s);
while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t' ||
s[len - 1] == '\r' || s[len - 1] == '\n'))
s[--len] = '\0';
}
////////////////////////////////
// Defaults
static void config_set_defaults(void) {
MemoryZeroStruct(&g_config);
snprintf(g_config.theme, sizeof(g_config.theme), "%s", "Gruvbox Dark");
g_config.show_line_numbers = 1;
g_config.syntax_enabled = 1;
g_config.block_cursor = 1;
g_config.cursor_smear = 1;
g_config.smooth_scroll = 1;
g_config.ui_scale = 1.0f;
g_config.editor_font_size = 15.0f;
snprintf(g_config.editor_font, sizeof(g_config.editor_font), "%s", "FiraCode");
snprintf(g_config.ui_font, sizeof(g_config.ui_font), "%s", "Inter");
g_config.window_width = 1280;
g_config.window_height = 800;
g_config.active_project[0] = '\0';
g_config.recent_dir_count = 0;
}
////////////////////////////////
// Load
static void config_load(void) {
config_set_defaults();
char path[CONFIG_PATH_MAX];
config_get_path(path, sizeof(path));
FILE *f = fopen(path, "rb");
if (!f) {
// No config file — write defaults
config_save();
return;
}
char line_buf[CONFIG_PATH_MAX + 64];
char current_section[64] = {0};
while (fgets(line_buf, sizeof(line_buf), f)) {
config_trim(line_buf);
if (line_buf[0] == '\0' || line_buf[0] == '#' || line_buf[0] == ';')
continue;
// Section header
if (line_buf[0] == '[') {
char *close = strchr(line_buf, ']');
if (close) {
*close = '\0';
snprintf(current_section, sizeof(current_section), "%s", line_buf + 1);
}
continue;
}
// Key = value
char *eq = strchr(line_buf, '=');
if (!eq) continue;
*eq = '\0';
char *key = line_buf;
char *val = eq + 1;
config_trim(key);
config_trim(val);
if (strcmp(current_section, "general") == 0) {
if (strcmp(key, "theme") == 0)
snprintf(g_config.theme, sizeof(g_config.theme), "%s", val);
else if (strcmp(key, "show_line_numbers") == 0)
g_config.show_line_numbers = (strcmp(val, "true") == 0);
else if (strcmp(key, "syntax_enabled") == 0)
g_config.syntax_enabled = (strcmp(val, "true") == 0);
else if (strcmp(key, "block_cursor") == 0)
g_config.block_cursor = (strcmp(val, "true") == 0);
else if (strcmp(key, "cursor_smear") == 0)
g_config.cursor_smear = (strcmp(val, "true") == 0);
else if (strcmp(key, "smooth_scroll") == 0)
g_config.smooth_scroll = (strcmp(val, "true") == 0);
else if (strcmp(key, "ui_scale") == 0)
g_config.ui_scale = (F32)atof(val);
else if (strcmp(key, "editor_font_size") == 0)
g_config.editor_font_size = (F32)atof(val);
else if (strcmp(key, "editor_font") == 0)
snprintf(g_config.editor_font, sizeof(g_config.editor_font), "%s", val);
else if (strcmp(key, "ui_font") == 0)
snprintf(g_config.ui_font, sizeof(g_config.ui_font), "%s", val);
else if (strcmp(key, "active_project") == 0)
snprintf(g_config.active_project, sizeof(g_config.active_project), "%s", val);
else if (strcmp(key, "window_width") == 0)
g_config.window_width = atoi(val);
else if (strcmp(key, "window_height") == 0)
g_config.window_height = atoi(val);
} else if (strcmp(current_section, "recent_dirs") == 0) {
// Keys are "0", "1", "2", etc.
S32 idx = atoi(key);
if (idx >= 0 && idx < CONFIG_MAX_RECENT_DIRS && val[0] != '\0') {
snprintf(g_config.recent_dirs[idx], CONFIG_PATH_MAX, "%s", val);
if (idx + 1 > g_config.recent_dir_count)
g_config.recent_dir_count = idx + 1;
}
}
}
fclose(f);
}
////////////////////////////////
// Save
static void config_save(void) {
char path[CONFIG_PATH_MAX];
config_get_path(path, sizeof(path));
FILE *f = fopen(path, "wb");
if (!f) return;
fprintf(f, "# codeMAX configuration — managed by the program\r\n\r\n");
fprintf(f, "[general]\r\n");
fprintf(f, "theme = %s\r\n", g_config.theme);
fprintf(f, "show_line_numbers = %s\r\n", g_config.show_line_numbers ? "true" : "false");
fprintf(f, "syntax_enabled = %s\r\n", g_config.syntax_enabled ? "true" : "false");
fprintf(f, "block_cursor = %s\r\n", g_config.block_cursor ? "true" : "false");
fprintf(f, "cursor_smear = %s\r\n", g_config.cursor_smear ? "true" : "false");
fprintf(f, "smooth_scroll = %s\r\n", g_config.smooth_scroll ? "true" : "false");
fprintf(f, "ui_scale = %.2f\r\n", g_config.ui_scale);
fprintf(f, "editor_font_size = %.1f\r\n", g_config.editor_font_size);
fprintf(f, "editor_font = %s\r\n", g_config.editor_font);
fprintf(f, "ui_font = %s\r\n", g_config.ui_font);
fprintf(f, "window_width = %d\r\n", g_config.window_width);
fprintf(f, "window_height = %d\r\n", g_config.window_height);
if (g_config.active_project[0] != '\0')
fprintf(f, "active_project = %s\r\n", g_config.active_project);
fprintf(f, "\r\n[recent_dirs]\r\n");
for (S32 i = 0; i < g_config.recent_dir_count; i++) {
if (g_config.recent_dirs[i][0] != '\0')
fprintf(f, "%d = %s\r\n", i, g_config.recent_dirs[i]);
}
fclose(f);
}
////////////////////////////////
// Recent directories
static void config_push_recent_dir(const char *dir) {
if (!dir || dir[0] == '\0') return;
// Normalize path for comparison
char norm[CONFIG_PATH_MAX];
snprintf(norm, sizeof(norm), "%s", dir);
for (char *p = norm; *p; p++) {
if (*p == '/') *p = '\\';
}
// Check if already in list — if so, move to front
S32 existing = -1;
for (S32 i = 0; i < g_config.recent_dir_count; i++) {
#ifdef __APPLE__
if (strcmp(g_config.recent_dirs[i], norm) == 0) { existing = i; break; }
#else
if (_stricmp(g_config.recent_dirs[i], norm) == 0) { existing = i; break; }
#endif
}
if (existing == 0) return; // already at front
// Save the entry if it exists, or use the new dir
char entry[CONFIG_PATH_MAX];
snprintf(entry, sizeof(entry), "%s", norm);
// Shift entries down
S32 start = (existing >= 0) ? existing : g_config.recent_dir_count;
if (start >= CONFIG_MAX_RECENT_DIRS) start = CONFIG_MAX_RECENT_DIRS - 1;
for (S32 i = start; i > 0; i--)
memcpy(g_config.recent_dirs[i], g_config.recent_dirs[i - 1], CONFIG_PATH_MAX);
// Insert at front
snprintf(g_config.recent_dirs[0], CONFIG_PATH_MAX, "%s", entry);
if (existing < 0 && g_config.recent_dir_count < CONFIG_MAX_RECENT_DIRS)
g_config.recent_dir_count++;
config_save();
}

51
c/config/config.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
// config.h — Global program configuration (config.ini next to binary)
//
// Stores internal program state: active theme, recent project directories,
// and other preferences. This file is managed by the program, not the user.
// Per-project settings belong in .editorconfig.
#include "base/base_inc.h"
////////////////////////////////
// Constants
#define CONFIG_MAX_RECENT_DIRS 10
#define CONFIG_PATH_MAX 1024
////////////////////////////////
// Config state
typedef struct Config {
char theme[64]; // active theme name
char recent_dirs[CONFIG_MAX_RECENT_DIRS][CONFIG_PATH_MAX]; // most recent first
S32 recent_dir_count;
B32 show_line_numbers;
B32 syntax_enabled;
B32 block_cursor;
B32 cursor_smear;
B32 smooth_scroll;
F32 ui_scale;
F32 editor_font_size;
char editor_font[64]; // "FiraCode", "JetBrains Mono", "Cascadia Mono"
char ui_font[64]; // "Inter", "FiraCode", etc.
S32 window_width;
S32 window_height;
char active_project[CONFIG_PATH_MAX];
} Config;
// Global config instance
extern Config g_config;
// Resolve the path to config.ini (next to the running binary).
static void config_get_path(char *out, S32 out_size);
// Load config from disk. Populates g_config. If the file doesn't exist,
// writes a default one and returns defaults.
static void config_load(void);
// Save current g_config state to disk.
static void config_save(void);
// Push a directory to the front of the recent list (deduplicates).
static void config_push_recent_dir(const char *dir);

604
c/installer/installer.c Normal file
View File

@@ -0,0 +1,604 @@
#define _CRT_SECURE_NO_WARNINGS
// installer.c — Self-contained Windows installer for codeMAX
//
// Uses Win32 PropertySheet wizard for the UI, embeds payload EXEs as
// resources, and handles PATH, Start Menu, and Add/Remove Programs.
//
// When invoked with /uninstall, runs the uninstaller instead.
#include <windows.h>
#include <commctrl.h>
#include <shlobj.h>
#include <objbase.h>
#include <shobjidl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "uuid.lib")
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#include "installer.h"
// Use codebase types
typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;
typedef unsigned long long U64;
typedef int S32;
typedef int B32;
#define ArrayCount(a) (sizeof(a) / sizeof((a)[0]))
////////////////////////////////
// Globals
static wchar_t g_install_dir[MAX_PATH];
static B32 g_add_to_path = 1;
static HINSTANCE g_hinst;
////////////////////////////////
// Helpers
static B32 extract_resource(S32 resource_id, const wchar_t *dest_path) {
HRSRC res = FindResourceW(NULL, MAKEINTRESOURCEW(resource_id), (LPCWSTR)RT_RCDATA);
if (!res) return 0;
HGLOBAL h = LoadResource(NULL, res);
if (!h) return 0;
void *data = LockResource(h);
DWORD size = SizeofResource(NULL, res);
if (!data || size == 0) return 0;
HANDLE f = CreateFileW(dest_path, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (f == INVALID_HANDLE_VALUE) return 0;
DWORD written;
WriteFile(f, data, size, &written, NULL);
CloseHandle(f);
return written == size;
}
static B32 create_directory_recursive(const wchar_t *path) {
wchar_t tmp[MAX_PATH];
wcscpy_s(tmp, MAX_PATH, path);
for (wchar_t *p = tmp + 3; *p; p++) { // skip "C:\"
if (*p == L'\\' || *p == L'/') {
*p = 0;
CreateDirectoryW(tmp, NULL);
*p = L'\\';
}
}
return CreateDirectoryW(tmp, NULL) || GetLastError() == ERROR_ALREADY_EXISTS;
}
static B32 create_shortcut(const wchar_t *lnk_path, const wchar_t *target,
const wchar_t *work_dir, const wchar_t *description,
const wchar_t *icon_path) {
HRESULT hr;
IShellLinkW *sl = NULL;
hr = CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
&IID_IShellLinkW, (void **)&sl);
if (FAILED(hr)) return 0;
sl->lpVtbl->SetPath(sl, target);
sl->lpVtbl->SetWorkingDirectory(sl, work_dir);
if (description) sl->lpVtbl->SetDescription(sl, description);
if (icon_path) sl->lpVtbl->SetIconLocation(sl, icon_path, 0);
IPersistFile *pf = NULL;
hr = sl->lpVtbl->QueryInterface(sl, &IID_IPersistFile, (void **)&pf);
B32 ok = 0;
if (SUCCEEDED(hr)) {
ok = SUCCEEDED(pf->lpVtbl->Save(pf, lnk_path, TRUE));
pf->lpVtbl->Release(pf);
}
sl->lpVtbl->Release(sl);
return ok;
}
static B32 path_add(const wchar_t *dir) {
HKEY key;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return 0;
wchar_t buf[8192] = {0};
DWORD buf_size = sizeof(buf) - sizeof(wchar_t);
DWORD type = REG_EXPAND_SZ;
RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size);
// Check if already present
if (wcsstr(buf, dir)) {
RegCloseKey(key);
return 1;
}
// Append
S32 len = (S32)wcslen(buf);
if (len > 0 && buf[len - 1] != L';')
wcscat_s(buf, ArrayCount(buf), L";");
wcscat_s(buf, ArrayCount(buf), dir);
RegSetValueExW(key, L"Path", 0, type,
(BYTE *)buf, (DWORD)((wcslen(buf) + 1) * sizeof(wchar_t)));
RegCloseKey(key);
return 1;
}
static void path_remove(const wchar_t *dir) {
HKEY key;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return;
wchar_t buf[8192] = {0};
DWORD buf_size = sizeof(buf) - sizeof(wchar_t);
DWORD type = REG_EXPAND_SZ;
RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size);
wchar_t result[8192] = {0};
wchar_t *ctx = NULL;
wchar_t copy[8192];
wcscpy_s(copy, ArrayCount(copy), buf);
wchar_t *tok = wcstok_s(copy, L";", &ctx);
B32 first = 1;
while (tok) {
if (_wcsicmp(tok, dir) != 0) {
if (!first) wcscat_s(result, ArrayCount(result), L";");
wcscat_s(result, ArrayCount(result), tok);
first = 0;
}
tok = wcstok_s(NULL, L";", &ctx);
}
RegSetValueExW(key, L"Path", 0, type,
(BYTE *)result, (DWORD)((wcslen(result) + 1) * sizeof(wchar_t)));
RegCloseKey(key);
}
static void broadcast_env_change(void) {
SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
(LPARAM)L"Environment", SMTO_ABORTIFHUNG, 5000, NULL);
}
////////////////////////////////
// In-memory dialog template builder
typedef struct {
U8 data[4096];
S32 len;
} DlgBuf;
static void dlg_align(DlgBuf *b, S32 align) {
while (b->len % align) b->data[b->len++] = 0;
}
static void dlg_write(DlgBuf *b, const void *src, S32 n) {
memcpy(b->data + b->len, src, n);
b->len += n;
}
static void dlg_write16(DlgBuf *b, U16 v) { dlg_write(b, &v, 2); }
static void dlg_write32(DlgBuf *b, U32 v) { dlg_write(b, &v, 4); }
static void dlg_write_wstr(DlgBuf *b, const wchar_t *s) {
S32 n = (S32)((wcslen(s) + 1) * sizeof(wchar_t));
dlg_write(b, s, n);
}
static DLGTEMPLATE *build_page_template(DlgBuf *b, S32 w, S32 h) {
b->len = 0;
// DLGTEMPLATE
DLGTEMPLATE *dt = (DLGTEMPLATE *)b->data;
dlg_write32(b, WS_CHILD | WS_VISIBLE | DS_SHELLFONT); // style
dlg_write32(b, 0); // dwExtendedStyle
dlg_write16(b, 0); // cdit (control count, filled later)
dlg_write16(b, 0); // x
dlg_write16(b, 0); // y
dlg_write16(b, (U16)w); // cx
dlg_write16(b, (U16)h); // cy
dlg_write16(b, 0); // menu (none)
dlg_write16(b, 0); // class (default)
dlg_write_wstr(b, L""); // title
// DS_SHELLFONT font
dlg_write16(b, 9); // point size
dlg_write_wstr(b, L"Segoe UI");
return dt;
}
static void add_control(DlgBuf *b, DLGTEMPLATE *dt, U32 style, S32 x, S32 y,
S32 w, S32 h, S32 id, const wchar_t *cls, const wchar_t *text) {
dlg_align(b, 4);
// DLGITEMTEMPLATE
dlg_write32(b, style | WS_CHILD | WS_VISIBLE); // style
dlg_write32(b, 0); // dwExtendedStyle
dlg_write16(b, (U16)x);
dlg_write16(b, (U16)y);
dlg_write16(b, (U16)w);
dlg_write16(b, (U16)h);
dlg_write16(b, (U16)id);
dlg_write_wstr(b, cls);
dlg_write_wstr(b, text);
dlg_write16(b, 0); // creation data
dt->cdit++;
}
////////////////////////////////
// Page IDs for controls
#define IDC_DIR_EDIT 1001
#define IDC_DIR_BROWSE 1002
#define IDC_PATH_CHK 1003
#define IDC_PROGRESS 1004
#define IDC_STATUS 1005
#define IDC_LAUNCH_CHK 1006
////////////////////////////////
// Wizard pages
static INT_PTR CALLBACK welcome_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT);
SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0);
}
return TRUE;
}
}
return FALSE;
}
static INT_PTR CALLBACK dir_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
SetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir);
CheckDlgButton(dlg, IDC_PATH_CHK, g_add_to_path ? BST_CHECKED : BST_UNCHECKED);
return TRUE;
case WM_COMMAND:
if (LOWORD(wp) == IDC_DIR_BROWSE) {
BROWSEINFOW bi = {0};
bi.hwndOwner = dlg;
bi.lpszTitle = L"Select installation directory:";
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
LPITEMIDLIST pidl = SHBrowseForFolderW(&bi);
if (pidl) {
wchar_t path[MAX_PATH];
SHGetPathFromIDListW(pidl, path);
wcscat_s(path, MAX_PATH, L"\\codeMAX");
SetDlgItemTextW(dlg, IDC_DIR_EDIT, path);
CoTaskMemFree(pidl);
}
}
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE)
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK | PSWIZB_NEXT);
if (nm->code == PSN_WIZNEXT) {
GetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir, MAX_PATH);
g_add_to_path = (IsDlgButtonChecked(dlg, IDC_PATH_CHK) == BST_CHECKED);
}
return TRUE;
}
}
return FALSE;
}
static B32 do_install(HWND dlg) {
HWND prog = GetDlgItem(dlg, IDC_PROGRESS);
HWND status = GetDlgItem(dlg, IDC_STATUS);
SendMessageW(prog, PBM_SETRANGE, 0, MAKELPARAM(0, 7));
SendMessageW(prog, PBM_SETSTEP, 1, 0);
// 1. Create install directory
SetWindowTextW(status, L"Creating install directory...");
create_directory_recursive(g_install_dir);
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 2. Extract terminal exe
SetWindowTextW(status, L"Installing codemax.exe...");
wchar_t path[MAX_PATH];
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
if (!extract_resource(IDR_EXE_GUI, path)) {
MessageBoxW(dlg, L"Failed to extract codemax.exe", L"Error", MB_ICONERROR);
return 0;
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 4. Copy self as uninstaller
SetWindowTextW(status, L"Creating uninstaller...");
wchar_t self_path[MAX_PATH];
GetModuleFileNameW(NULL, self_path, MAX_PATH);
_snwprintf(path, MAX_PATH, L"%s\\uninstall.exe", g_install_dir);
CopyFileW(self_path, path, FALSE);
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 5. Add to PATH (optional)
if (g_add_to_path) {
SetWindowTextW(status, L"Updating PATH...");
path_add(g_install_dir);
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 6. Create Start Menu shortcuts
SetWindowTextW(status, L"Creating shortcuts...");
CoInitialize(NULL);
{
wchar_t programs[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs);
wchar_t menu_dir[MAX_PATH];
_snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs);
CreateDirectoryW(menu_dir, NULL);
wchar_t exe_path[MAX_PATH];
_snwprintf(exe_path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
wchar_t lnk[MAX_PATH];
_snwprintf(lnk, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir);
create_shortcut(lnk, exe_path, g_install_dir,
L"codeMAX Text Editor", exe_path);
}
CoUninitialize();
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 7. Register in Add/Remove Programs
SetWindowTextW(status, L"Registering application...");
{
HKEY key;
RegCreateKeyExA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY,
0, NULL, 0, KEY_WRITE, NULL, &key, NULL);
char dir_a[MAX_PATH], uninst[MAX_PATH * 2];
WideCharToMultiByte(CP_UTF8, 0, g_install_dir, -1, dir_a, MAX_PATH, NULL, NULL);
snprintf(uninst, sizeof(uninst), "\"%s\\uninstall.exe\" /uninstall", dir_a);
RegSetValueExA(key, "DisplayName", 0, REG_SZ,
(BYTE *)CODEMAX_DISPLAY_NAME, (DWORD)strlen(CODEMAX_DISPLAY_NAME) + 1);
RegSetValueExA(key, "DisplayVersion", 0, REG_SZ,
(BYTE *)CODEMAX_VERSION, (DWORD)strlen(CODEMAX_VERSION) + 1);
RegSetValueExA(key, "Publisher", 0, REG_SZ,
(BYTE *)CODEMAX_PUBLISHER, (DWORD)strlen(CODEMAX_PUBLISHER) + 1);
RegSetValueExA(key, "InstallLocation", 0, REG_SZ,
(BYTE *)dir_a, (DWORD)strlen(dir_a) + 1);
RegSetValueExA(key, "UninstallString", 0, REG_SZ,
(BYTE *)uninst, (DWORD)strlen(uninst) + 1);
char icon[MAX_PATH + 4];
snprintf(icon, sizeof(icon), "%s\\codemax.exe,0", dir_a);
RegSetValueExA(key, "DisplayIcon", 0, REG_SZ,
(BYTE *)icon, (DWORD)strlen(icon) + 1);
DWORD one = 1;
RegSetValueExA(key, "NoModify", 0, REG_DWORD, (BYTE *)&one, sizeof(one));
RegSetValueExA(key, "NoRepair", 0, REG_DWORD, (BYTE *)&one, sizeof(one));
RegCloseKey(key);
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
broadcast_env_change();
SetWindowTextW(status, L"Installation complete.");
return 1;
}
static INT_PTR CALLBACK progress_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
// Disable all buttons during install
PropSheet_SetWizButtons(GetParent(dlg), 0);
// Run install
if (do_install(dlg)) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT);
} else {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK);
}
SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0);
}
return TRUE;
}
}
return FALSE;
}
static INT_PTR CALLBACK finish_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
CheckDlgButton(dlg, IDC_LAUNCH_CHK, BST_CHECKED);
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_FINISH);
}
if (nm->code == PSN_WIZFINISH) {
if (IsDlgButtonChecked(dlg, IDC_LAUNCH_CHK) == BST_CHECKED) {
wchar_t exe[MAX_PATH];
_snwprintf(exe, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
ShellExecuteW(NULL, L"open", exe, NULL, g_install_dir, SW_SHOWNORMAL);
}
}
return TRUE;
}
}
return FALSE;
}
////////////////////////////////
// Wizard launcher
static void run_installer(void) {
// Default install dir
wcscpy_s(g_install_dir, MAX_PATH, L"C:\\Program Files\\codeMAX");
InitCommonControls();
// Build page templates in memory
DlgBuf bufs[4] = {0};
S32 page_w = 317, page_h = 143;
// Page 0: Welcome
DLGTEMPLATE *dt0 = build_page_template(&bufs[0], page_w, page_h);
add_control(&bufs[0], dt0, SS_LEFT, 10, 10, 297, 40, -1,
L"Static", L"Welcome to the codeMAX installer.\n\n"
L"This will install codeMAX on your computer.");
add_control(&bufs[0], dt0, SS_LEFT, 10, 60, 297, 20, -1,
L"Static", L"Click Next to continue.");
// Page 1: Directory + options
DLGTEMPLATE *dt1 = build_page_template(&bufs[1], page_w, page_h);
add_control(&bufs[1], dt1, SS_LEFT, 10, 10, 297, 10, -1,
L"Static", L"Choose the installation directory:");
add_control(&bufs[1], dt1, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP,
10, 28, 230, 14, IDC_DIR_EDIT, L"Edit", L"");
add_control(&bufs[1], dt1, BS_PUSHBUTTON | WS_TABSTOP,
248, 27, 60, 14, IDC_DIR_BROWSE, L"Button", L"Browse...");
add_control(&bufs[1], dt1, BS_AUTOCHECKBOX | WS_TABSTOP,
10, 52, 250, 14, IDC_PATH_CHK, L"Button", L"Add to system PATH");
// Page 2: Progress
DLGTEMPLATE *dt2 = build_page_template(&bufs[2], page_w, page_h);
add_control(&bufs[2], dt2, SS_LEFT, 10, 10, 297, 10, IDC_STATUS,
L"Static", L"Installing...");
add_control(&bufs[2], dt2, 0, 10, 30, 297, 14, IDC_PROGRESS,
PROGRESS_CLASSW, L"");
// Page 3: Finish
DLGTEMPLATE *dt3 = build_page_template(&bufs[3], page_w, page_h);
add_control(&bufs[3], dt3, SS_LEFT, 10, 10, 297, 20, -1,
L"Static", L"codeMAX has been installed successfully.");
add_control(&bufs[3], dt3, BS_AUTOCHECKBOX | WS_TABSTOP,
10, 45, 200, 14, IDC_LAUNCH_CHK, L"Button", L"Launch codeMAX GUI");
PROPSHEETPAGEW pages[4] = {0};
for (S32 i = 0; i < 4; i++) {
pages[i].dwSize = sizeof(PROPSHEETPAGEW);
pages[i].dwFlags = PSP_DLGINDIRECT;
pages[i].hInstance = g_hinst;
}
pages[0].pResource = dt0;
pages[0].pfnDlgProc = welcome_proc;
pages[1].pResource = dt1;
pages[1].pfnDlgProc = dir_proc;
pages[2].pResource = dt2;
pages[2].pfnDlgProc = progress_proc;
pages[3].pResource = dt3;
pages[3].pfnDlgProc = finish_proc;
PROPSHEETHEADERW psh = {0};
psh.dwSize = sizeof(PROPSHEETHEADERW);
psh.dwFlags = PSH_WIZARD | PSH_PROPSHEETPAGE | PSH_USEICONID;
psh.hwndParent = NULL;
psh.hInstance = g_hinst;
psh.pszIcon = MAKEINTRESOURCEW(IDI_CODEMAX);
psh.pszCaption = L"codeMAX Setup";
psh.nPages = 4;
psh.ppsp = pages;
PropertySheetW(&psh);
}
////////////////////////////////
// Uninstaller
static void run_uninstaller(void) {
// Get install dir from our own exe path
wchar_t self[MAX_PATH];
GetModuleFileNameW(NULL, self, MAX_PATH);
wcscpy_s(g_install_dir, MAX_PATH, self);
wchar_t *last_sep = wcsrchr(g_install_dir, L'\\');
if (last_sep) *last_sep = 0;
S32 result = MessageBoxW(NULL,
L"Are you sure you want to uninstall codeMAX?",
L"codeMAX Uninstall", MB_YESNO | MB_ICONQUESTION);
if (result != IDYES) return;
// Delete installed files
wchar_t path[MAX_PATH];
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
DeleteFileW(path);
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
DeleteFileW(path);
// Remove from PATH
path_remove(g_install_dir);
broadcast_env_change();
// Remove Start Menu shortcuts
{
wchar_t programs[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs);
wchar_t menu_dir[MAX_PATH];
_snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs);
_snwprintf(path, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir);
DeleteFileW(path);
_snwprintf(path, MAX_PATH, L"%s\\codeMAX GUI.lnk", menu_dir);
DeleteFileW(path);
RemoveDirectoryW(menu_dir);
}
// Remove registry entry
RegDeleteKeyA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY);
// Schedule self-deletion and remove install dir
// Use cmd.exe to wait for us to exit, then delete
wchar_t cmd[MAX_PATH * 3];
_snwprintf(cmd, ArrayCount(cmd),
L"cmd.exe /c timeout /t 2 /nobreak >nul & del \"%s\\uninstall.exe\" & rmdir \"%s\"",
g_install_dir, g_install_dir);
STARTUPINFOW si = {0};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi = {0};
CreateProcessW(NULL, cmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
MessageBoxW(NULL, L"codeMAX has been uninstalled.", L"codeMAX Uninstall",
MB_OK | MB_ICONINFORMATION);
}
////////////////////////////////
// Entry point
int main(int argc, char **argv) {
(void)argc;
g_hinst = GetModuleHandleW(NULL);
// Check for uninstall flag
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "/uninstall") == 0) {
run_uninstaller();
return 0;
}
}
// Also check via GetCommandLineW for when launched without CRT argc/argv
wchar_t *cmdline = GetCommandLineW();
if (wcsstr(cmdline, L"/uninstall")) {
run_uninstaller();
return 0;
}
run_installer();
return 0;
}

16
c/installer/installer.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
// installer.h — Shared constants for installer and resource script
// Resource IDs
#define IDI_CODEMAX 101
#define IDR_EXE_TERMINAL 201
#define IDR_EXE_GUI 202
// Product info
#define CODEMAX_APP_NAME "codemax"
#define CODEMAX_DISPLAY_NAME "codeMAX"
#define CODEMAX_VERSION "1.0.0"
#define CODEMAX_PUBLISHER "codeMAX"
// Registry
#define CODEMAX_UNINSTALL_KEY "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\codeMAX"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32" name="codemax.installer" version="1.0.0.0" processorArchitecture="amd64"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
</assembly>

14
c/installer/installer.rc Normal file
View File

@@ -0,0 +1,14 @@
// installer.rc — Resource script for codemax installer
// Embeds icon, UAC manifest, and payload executables.
#include "installer.h"
#include <winresrc.h>
// Application icon
IDI_CODEMAX ICON "assets\\icons\\codemax.ico"
// UAC elevation manifest
1 24 "src\\installer\\installer.manifest"
// Payload executable (embedded as RCDATA)
IDR_EXE_GUI RCDATA "build_release\\codemax.exe"

98
c/lexer/lexer.c Normal file
View File

@@ -0,0 +1,98 @@
// lexer.c -- Tokenizer infrastructure, color schemes, common helpers
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
const char *g_lang_names[LANG_COUNT] = {
"Plain Text", // LANG_PLAIN_TEXT
"C", // LANG_C
"Go", // LANG_GO
"JavaScript", // LANG_JS
"Lua", // LANG_LUA
"SQL", // LANG_SQL
};
////////////////////////////////
// Tokenizer helpers
static void tokenizer_init(Tokenizer *tok, const char *data, S32 length) {
tok->buf = data;
tok->t = data;
tok->max_t = data + length;
tok->start_t = data;
}
static void tokenizer_eat_whitespace(Tokenizer *tok) {
while (tok->t < tok->max_t && (*tok->t == ' ' || *tok->t == '\t' ||
*tok->t == '\n' || *tok->t == '\r'))
tok->t++;
}
static void tokenizer_eat_until_newline(Tokenizer *tok) {
while (tok->t < tok->max_t && *tok->t != '\n')
tok->t++;
}
////////////////////////////////
// Paint helper -- fill out_tokens[start..start+len) with a token type
static void paint_token(U8 *out_tokens, S32 start, S32 len, Token_Type type) {
memset(out_tokens + start, (U8)type, len);
}
////////////////////////////////
// Language detection
static Lang lexer_detect_lang(const char *filename) {
if (!filename) return LANG_PLAIN_TEXT;
const char *dot = strrchr(filename, '.');
if (!dot) return LANG_PLAIN_TEXT;
dot++; // skip the '.'
if (strcmp(dot, "c") == 0 || strcmp(dot, "h") == 0 ||
strcmp(dot, "C") == 0 || strcmp(dot, "H") == 0)
return LANG_C;
if (strcmp(dot, "go") == 0)
return LANG_GO;
if (strcmp(dot, "js") == 0 || strcmp(dot, "mjs") == 0 ||
strcmp(dot, "cjs") == 0 || strcmp(dot, "jsx") == 0)
return LANG_JS;
if (strcmp(dot, "lua") == 0)
return LANG_LUA;
if (strcmp(dot, "sql") == 0 || strcmp(dot, "ddl") == 0 ||
strcmp(dot, "dml") == 0 || strcmp(dot, "pgsql") == 0 ||
strcmp(dot, "plsql") == 0 || strcmp(dot, "psql") == 0)
return LANG_SQL;
return LANG_PLAIN_TEXT;
}
////////////////////////////////
// Forward-declare language tokenizers
static void tokenize_c(const char *data, S32 length, U8 *out_tokens);
static void tokenize_go(const char *data, S32 length, U8 *out_tokens);
static void tokenize_js(const char *data, S32 length, U8 *out_tokens);
static void tokenize_lua(const char *data, S32 length, U8 *out_tokens);
static void tokenize_sql(const char *data, S32 length, U8 *out_tokens);
static void tokenize_plain(const char *data, S32 length, U8 *out_tokens) {
(void)data;
memset(out_tokens, TOK_DEFAULT, length);
}
static LexerTokenizeFn lexer_get_tokenize_fn(Lang lang) {
switch (lang) {
case LANG_C: return tokenize_c;
case LANG_GO: return tokenize_go;
case LANG_JS: return tokenize_js;
case LANG_LUA: return tokenize_lua;
case LANG_SQL: return tokenize_sql;
default: return tokenize_plain;
}
}

98
c/lexer/lexer.h Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
// lexer.h -- Token types, tokenizer interface, and color scheme
//
// Follows the Focus editor pattern: a universal Token_Type enum shared by all
// languages, a per-byte token-type array on each buffer, and language-specific
// tokenizer functions that fill that array.
#include "base/base_core.h"
////////////////////////////////
// Token types (universal across all languages)
//
// The enum value doubles as an index into the color map, so CODE_DEFAULT
// must be 0. Keep this list ordered -- rendering indexes directly.
typedef enum Token_Type {
TOK_DEFAULT = 0,
TOK_COMMENT,
TOK_MULTILINE_COMMENT,
TOK_STRING_LITERAL,
TOK_CHAR_LITERAL,
TOK_NUMBER,
TOK_IDENTIFIER,
TOK_FUNCTION,
TOK_KEYWORD,
TOK_TYPE,
TOK_VALUE, // true, false, NULL, nullptr
TOK_MODIFIER, // const, static, volatile, extern ...
TOK_DIRECTIVE, // #include, #define, ...
TOK_PUNCTUATION,
TOK_OPERATION,
TOK_INVALID,
TOK_COUNT
} Token_Type;
////////////////////////////////
// Language enum
typedef enum Lang {
LANG_PLAIN_TEXT = 0,
LANG_C,
LANG_GO,
LANG_JS,
LANG_LUA,
LANG_SQL,
LANG_COUNT
} Lang;
// Human-readable language names (indexed by Lang)
extern const char *g_lang_names[LANG_COUNT];
////////////////////////////////
// Tokenizer state (shared by all language tokenizers)
typedef struct Tokenizer {
const char *buf; // start of buffer data
const char *t; // current scan cursor
const char *max_t; // one past end
const char *start_t; // cursor at start of current token
} Tokenizer;
////////////////////////////////
// Token (returned by get_next_token functions)
typedef struct Token {
S32 start; // byte offset into buffer
S32 len; // byte length of token
Token_Type type;
} Token;
////////////////////////////////
// Language tokenizer function signature
//
// A tokenizer function takes a buffer's data + length, and writes token types
// into the `out_tokens` array (one byte per source byte, same length as data).
// This is the Focus "paint the token array" approach.
typedef void (*LexerTokenizeFn)(const char *data, S32 length, U8 *out_tokens);
// Get the tokenizer function for a language.
static LexerTokenizeFn lexer_get_tokenize_fn(Lang lang);
// Detect language from a file name / extension.
static Lang lexer_detect_lang(const char *filename);
////////////////////////////////
// Common tokenizer helpers (usable by all language tokenizers)
static void tokenizer_init(Tokenizer *tok, const char *data, S32 length);
static void tokenizer_eat_whitespace(Tokenizer *tok);
static void tokenizer_eat_until_newline(Tokenizer *tok);

423
c/lexer/lexer_c.c Normal file
View File

@@ -0,0 +1,423 @@
// lexer_c.c -- C language tokenizer
//
// Follows the Focus editor pattern: a get_next_token() loop that advances
// a cursor through the source, producing Token structs. After each token,
// we paint the per-byte token-type array with the token's type.
// A second pass retroactively marks identifiers followed by '(' as functions.
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
////////////////////////////////
// C keyword tables
typedef struct KeywordEntry {
const char *word;
Token_Type type;
} KeywordEntry;
static const KeywordEntry c_keywords[] = {
// Control-flow & language keywords
{"auto", TOK_KEYWORD},
{"break", TOK_KEYWORD},
{"case", TOK_KEYWORD},
{"continue", TOK_KEYWORD},
{"default", TOK_KEYWORD},
{"do", TOK_KEYWORD},
{"else", TOK_KEYWORD},
{"enum", TOK_KEYWORD},
{"for", TOK_KEYWORD},
{"goto", TOK_KEYWORD},
{"if", TOK_KEYWORD},
{"inline", TOK_KEYWORD},
{"restrict", TOK_KEYWORD},
{"return", TOK_KEYWORD},
{"sizeof", TOK_KEYWORD},
{"struct", TOK_KEYWORD},
{"switch", TOK_KEYWORD},
{"typedef", TOK_KEYWORD},
{"union", TOK_KEYWORD},
{"while", TOK_KEYWORD},
{"_Alignas", TOK_KEYWORD},
{"alignas", TOK_KEYWORD},
{"_Alignof", TOK_KEYWORD},
{"alignof", TOK_KEYWORD},
{"_Atomic", TOK_KEYWORD},
{"_Generic", TOK_KEYWORD},
{"_Noreturn", TOK_KEYWORD},
{"static_assert", TOK_KEYWORD},
{"_Static_assert", TOK_KEYWORD},
// Types
{"char", TOK_TYPE},
{"double", TOK_TYPE},
{"float", TOK_TYPE},
{"int", TOK_TYPE},
{"long", TOK_TYPE},
{"short", TOK_TYPE},
{"void", TOK_TYPE},
{"bool", TOK_TYPE},
{"_Bool", TOK_TYPE},
{"_Complex", TOK_TYPE},
{"_Imaginary", TOK_TYPE},
// stdint
{"int8_t", TOK_TYPE},
{"int16_t", TOK_TYPE},
{"int32_t", TOK_TYPE},
{"int64_t", TOK_TYPE},
{"uint8_t", TOK_TYPE},
{"uint16_t", TOK_TYPE},
{"uint32_t", TOK_TYPE},
{"uint64_t", TOK_TYPE},
{"size_t", TOK_TYPE},
{"ssize_t", TOK_TYPE},
{"ptrdiff_t", TOK_TYPE},
{"intptr_t", TOK_TYPE},
{"uintptr_t", TOK_TYPE},
{"nullptr_t", TOK_TYPE},
{"FILE", TOK_TYPE},
// Project types
{"U8", TOK_TYPE},
{"U16", TOK_TYPE},
{"U32", TOK_TYPE},
{"U64", TOK_TYPE},
{"S8", TOK_TYPE},
{"S16", TOK_TYPE},
{"S32", TOK_TYPE},
{"S64", TOK_TYPE},
{"B32", TOK_TYPE},
{"F32", TOK_TYPE},
{"F64", TOK_TYPE},
// Values
{"false", TOK_VALUE},
{"true", TOK_VALUE},
{"NULL", TOK_VALUE},
{"nullptr", TOK_VALUE},
// Modifiers
{"const", TOK_MODIFIER},
{"constexpr", TOK_MODIFIER},
{"extern", TOK_MODIFIER},
{"register", TOK_MODIFIER},
{"signed", TOK_MODIFIER},
{"static", TOK_MODIFIER},
{"unsigned", TOK_MODIFIER},
{"volatile", TOK_MODIFIER},
{"thread_local", TOK_MODIFIER},
{"_Thread_local", TOK_MODIFIER},
};
#define C_KEYWORD_COUNT (S32)(sizeof(c_keywords) / sizeof(c_keywords[0]))
static Token_Type c_lookup_keyword(const char *word, S32 len) {
for (S32 i = 0; i < C_KEYWORD_COUNT; i++) {
const char *kw = c_keywords[i].word;
S32 kwlen = (S32)strlen(kw);
if (kwlen == len && memcmp(kw, word, len) == 0)
return c_keywords[i].type;
}
return TOK_IDENTIFIER;
}
////////////////////////////////
// Character helpers
static B32 c_is_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_';
}
static B32 c_is_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_';
}
static B32 c_is_hex(char c) {
return isdigit((unsigned char)c) ||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
////////////////////////////////
// Individual token parsers
static Token c_parse_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
const char *begin = tok->t;
while (tok->t < tok->max_t && c_is_ident_char(*tok->t))
tok->t++;
S32 len = (S32)(tok->t - begin);
token.type = c_lookup_keyword(begin, len);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token c_parse_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
char start_char = *tok->t;
tok->t++;
if (tok->t >= tok->max_t) goto done;
if (start_char == '0' && tok->t < tok->max_t) {
if (*tok->t == 'x' || *tok->t == 'X') {
tok->t++;
while (tok->t < tok->max_t && c_is_hex(*tok->t)) tok->t++;
goto suffixes;
}
if (*tok->t == 'b' || *tok->t == 'B') {
tok->t++;
while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1')) tok->t++;
goto suffixes;
}
}
// Decimal / float
{
B32 seen_dot = 0;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '.')) {
if (*tok->t == '.') {
if (seen_dot) break;
seen_dot = 1;
}
tok->t++;
}
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
// Float suffix
if (tok->t < tok->max_t && seen_dot) {
if (*tok->t == 'f' || *tok->t == 'F' || *tok->t == 'l' || *tok->t == 'L')
tok->t++;
goto done;
}
}
suffixes:
// Integer suffixes: u, l, ll, ul, ull, lu, llu
if (tok->t < tok->max_t && (*tok->t == 'u' || *tok->t == 'U')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) tok->t++;
}
} else if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'l' || *tok->t == 'L')) tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'u' || *tok->t == 'U')) tok->t++;
}
done:
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token c_parse_string(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
B32 escape = 0;
tok->t++; // skip opening "
while (tok->t < tok->max_t && *tok->t != '\n') {
if (*tok->t == '"' && !escape) { tok->t++; break; }
escape = !escape && (*tok->t == '\\');
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token c_parse_char_literal(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_CHAR_LITERAL;
tok->t++; // skip opening '
if (tok->t < tok->max_t && *tok->t == '\\') {
tok->t++; // escape char
if (tok->t < tok->max_t && *tok->t != '\n') tok->t++; // the escaped char
} else if (tok->t < tok->max_t && *tok->t != '\n' && *tok->t != '\'') {
tok->t++; // the char
}
if (tok->t < tok->max_t && *tok->t == '\'') tok->t++; // closing '
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token c_parse_slash_or_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
tok->t++; // skip '/'
if (tok->t >= tok->max_t) {
token.type = TOK_OPERATION;
token.len = 1;
return token;
}
if (*tok->t == '/') {
// Line comment
token.type = TOK_COMMENT;
tok->t++;
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
} else if (*tok->t == '*') {
// Block comment
token.type = TOK_MULTILINE_COMMENT;
tok->t++;
while (tok->t < tok->max_t) {
if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') {
tok->t += 2;
break;
}
tok->t++;
}
} else if (*tok->t == '=') {
token.type = TOK_OPERATION;
tok->t++;
} else {
token.type = TOK_OPERATION;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token c_parse_directive(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_DIRECTIVE;
tok->t++; // skip '#'
// Skip whitespace between # and directive name
while (tok->t < tok->max_t && (*tok->t == ' ' || *tok->t == '\t')) tok->t++;
// Read directive name
while (tok->t < tok->max_t && isalpha((unsigned char)*tok->t)) tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Two-char operator check
static Token c_parse_operator(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_OPERATION;
char c = *tok->t;
tok->t++;
if (tok->t < tok->max_t) {
char n = *tok->t;
// Two-character operators
if ((c == '=' && n == '=') || (c == '!' && n == '=') ||
(c == '<' && n == '=') || (c == '>' && n == '=') ||
(c == '+' && n == '=') || (c == '-' && n == '=') ||
(c == '*' && n == '=') || (c == '%' && n == '=') ||
(c == '&' && n == '=') || (c == '|' && n == '=') ||
(c == '^' && n == '=') || (c == '+' && n == '+') ||
(c == '-' && n == '-') || (c == '&' && n == '&') ||
(c == '|' && n == '|') || (c == '<' && n == '<') ||
(c == '>' && n == '>') || (c == '-' && n == '>')) {
tok->t++;
}
}
// Handle negative number literal: operator '-' followed by digit
// (not done here -- handled by caller context if needed)
token.len = (S32)(tok->t - tok->start_t);
return token;
}
////////////////////////////////
// Main get_next_token
static Token c_get_next_token(Tokenizer *tok) {
tokenizer_eat_whitespace(tok);
Token token = {0};
token.start = (S32)(tok->t - tok->buf);
token.type = TOK_DEFAULT;
if (tok->t >= tok->max_t) {
token.len = 0;
return token; // EOF
}
tok->start_t = tok->t;
char c = *tok->t;
if (c_is_ident_start(c)) {
return c_parse_identifier(tok);
}
if (isdigit((unsigned char)c)) {
return c_parse_number(tok);
}
switch (c) {
case '"': return c_parse_string(tok);
case '\'': return c_parse_char_literal(tok);
case '/': return c_parse_slash_or_comment(tok);
case '#': return c_parse_directive(tok);
// Punctuation
case ';': case ',': case '.':
case '{': case '}': case '(': case ')': case '[': case ']':
case '\\':
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Operators
case '=': case '!': case '<': case '>':
case '+': case '-': case '*': case '%':
case '&': case '|': case '^': case '~':
case '?': case ':':
return c_parse_operator(tok);
default:
token.type = TOK_INVALID;
tok->t++;
token.len = 1;
return token;
}
}
////////////////////////////////
// tokenize_c -- main entry point
//
// Tokenizes the full buffer and paints out_tokens[] with token types.
// Second pass: retroactively marks identifiers before '(' as functions.
static void tokenize_c(const char *data, S32 length, U8 *out_tokens) {
memset(out_tokens, TOK_DEFAULT, length);
Tokenizer tok;
tokenizer_init(&tok, data, length);
Token prev = {0};
prev.type = TOK_DEFAULT;
while (tok.t < tok.max_t) {
Token token = c_get_next_token(&tok);
if (token.len == 0) break;
paint_token(out_tokens, token.start, token.len, token.type);
// Retroactively mark identifier before '(' as a function
if (token.type == TOK_PUNCTUATION && token.len == 1 &&
data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
}
prev = token;
}
}

489
c/lexer/lexer_go.c Normal file
View File

@@ -0,0 +1,489 @@
// lexer_go.c -- Go language tokenizer
//
// Same pattern as lexer_c.c: a get_next_token() loop that advances a cursor
// through the source, producing Token structs. After each token we paint
// the per-byte token-type array. A second pass marks identifiers before '('
// as functions.
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
////////////////////////////////
// Go keyword tables
typedef struct GoKeywordEntry {
const char *word;
Token_Type type;
} GoKeywordEntry;
static const GoKeywordEntry go_keywords[] = {
// Control-flow & language keywords
{"break", TOK_KEYWORD},
{"case", TOK_KEYWORD},
{"chan", TOK_KEYWORD},
{"const", TOK_KEYWORD},
{"continue", TOK_KEYWORD},
{"default", TOK_KEYWORD},
{"defer", TOK_KEYWORD},
{"else", TOK_KEYWORD},
{"fallthrough", TOK_KEYWORD},
{"for", TOK_KEYWORD},
{"func", TOK_KEYWORD},
{"go", TOK_KEYWORD},
{"goto", TOK_KEYWORD},
{"if", TOK_KEYWORD},
{"import", TOK_KEYWORD},
{"interface", TOK_KEYWORD},
{"map", TOK_KEYWORD},
{"package", TOK_KEYWORD},
{"range", TOK_KEYWORD},
{"return", TOK_KEYWORD},
{"select", TOK_KEYWORD},
{"struct", TOK_KEYWORD},
{"switch", TOK_KEYWORD},
{"type", TOK_KEYWORD},
{"var", TOK_KEYWORD},
// Built-in types
{"bool", TOK_TYPE},
{"byte", TOK_TYPE},
{"complex64", TOK_TYPE},
{"complex128", TOK_TYPE},
{"error", TOK_TYPE},
{"float32", TOK_TYPE},
{"float64", TOK_TYPE},
{"int", TOK_TYPE},
{"int8", TOK_TYPE},
{"int16", TOK_TYPE},
{"int32", TOK_TYPE},
{"int64", TOK_TYPE},
{"rune", TOK_TYPE},
{"string", TOK_TYPE},
{"uint", TOK_TYPE},
{"uint8", TOK_TYPE},
{"uint16", TOK_TYPE},
{"uint32", TOK_TYPE},
{"uint64", TOK_TYPE},
{"uintptr", TOK_TYPE},
{"any", TOK_TYPE},
{"comparable", TOK_TYPE},
// Built-in values
{"true", TOK_VALUE},
{"false", TOK_VALUE},
{"nil", TOK_VALUE},
{"iota", TOK_VALUE},
// Built-in functions (treated as modifiers for distinct coloring)
{"append", TOK_MODIFIER},
{"cap", TOK_MODIFIER},
{"clear", TOK_MODIFIER},
{"close", TOK_MODIFIER},
{"complex", TOK_MODIFIER},
{"copy", TOK_MODIFIER},
{"delete", TOK_MODIFIER},
{"imag", TOK_MODIFIER},
{"len", TOK_MODIFIER},
{"make", TOK_MODIFIER},
{"max", TOK_MODIFIER},
{"min", TOK_MODIFIER},
{"new", TOK_MODIFIER},
{"panic", TOK_MODIFIER},
{"print", TOK_MODIFIER},
{"println", TOK_MODIFIER},
{"real", TOK_MODIFIER},
{"recover", TOK_MODIFIER},
};
#define GO_KEYWORD_COUNT (S32)(sizeof(go_keywords) / sizeof(go_keywords[0]))
static Token_Type go_lookup_keyword(const char *word, S32 len) {
for (S32 i = 0; i < GO_KEYWORD_COUNT; i++) {
const char *kw = go_keywords[i].word;
S32 kwlen = (S32)strlen(kw);
if (kwlen == len && memcmp(kw, word, len) == 0)
return go_keywords[i].type;
}
return TOK_IDENTIFIER;
}
////////////////////////////////
// Character helpers
static B32 go_is_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_';
}
static B32 go_is_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_';
}
static B32 go_is_hex(char c) {
return isdigit((unsigned char)c) ||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
static B32 go_is_octal(char c) {
return c >= '0' && c <= '7';
}
////////////////////////////////
// Individual token parsers
static Token go_parse_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
const char *begin = tok->t;
while (tok->t < tok->max_t && go_is_ident_char(*tok->t))
tok->t++;
S32 len = (S32)(tok->t - begin);
token.type = go_lookup_keyword(begin, len);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token go_parse_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
char start_char = *tok->t;
tok->t++;
if (tok->t >= tok->max_t) goto done;
if (start_char == '0' && tok->t < tok->max_t) {
// Hex: 0x or 0X
if (*tok->t == 'x' || *tok->t == 'X') {
tok->t++;
while (tok->t < tok->max_t && (go_is_hex(*tok->t) || *tok->t == '_')) tok->t++;
// Hex float: optional .hex_digits, optional p exponent
if (tok->t < tok->max_t && *tok->t == '.') {
tok->t++;
while (tok->t < tok->max_t && (go_is_hex(*tok->t) || *tok->t == '_')) tok->t++;
}
if (tok->t < tok->max_t && (*tok->t == 'p' || *tok->t == 'P')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++;
}
goto imaginary;
}
// Octal: 0o or 0O
if (*tok->t == 'o' || *tok->t == 'O') {
tok->t++;
while (tok->t < tok->max_t && (go_is_octal(*tok->t) || *tok->t == '_')) tok->t++;
goto imaginary;
}
// Binary: 0b or 0B
if (*tok->t == 'b' || *tok->t == 'B') {
tok->t++;
while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1' || *tok->t == '_')) tok->t++;
goto imaginary;
}
}
// Decimal or float (also handles legacy octal like 0755)
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
// Decimal float: .digits
if (tok->t < tok->max_t && *tok->t == '.') {
// Check next char is digit to avoid consuming '..' or method calls
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++;
} else if ((tok->t + 1) >= tok->max_t || !go_is_ident_start(*(tok->t + 1))) {
// Trailing dot like "1." -- still a float in Go
tok->t++;
}
}
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++;
}
imaginary:
// Imaginary suffix
if (tok->t < tok->max_t && *tok->t == 'i')
tok->t++;
done:
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Dot-starting float: .5, .123e4
static Token go_parse_dot_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
tok->t++; // skip '.'
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_')) tok->t++;
}
// Imaginary
if (tok->t < tok->max_t && *tok->t == 'i')
tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token go_parse_string(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
B32 escape = 0;
tok->t++; // skip opening "
while (tok->t < tok->max_t && *tok->t != '\n') {
if (*tok->t == '"' && !escape) { tok->t++; break; }
escape = !escape && (*tok->t == '\\');
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Raw string literal: `...`
static Token go_parse_raw_string(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
tok->t++; // skip opening `
while (tok->t < tok->max_t) {
if (*tok->t == '`') { tok->t++; break; }
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Rune literal: 'x', '\n', '\u0041', etc.
static Token go_parse_rune_literal(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_CHAR_LITERAL;
tok->t++; // skip opening '
if (tok->t < tok->max_t && *tok->t == '\\') {
tok->t++; // skip backslash
// Consume escape sequence characters
if (tok->t < tok->max_t) {
char esc = *tok->t;
tok->t++;
if (esc == 'x') {
// \xNN
for (int i = 0; i < 2 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++;
} else if (esc == 'u') {
// \uNNNN
for (int i = 0; i < 4 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++;
} else if (esc == 'U') {
// \UNNNNNNNN
for (int i = 0; i < 8 && tok->t < tok->max_t && go_is_hex(*tok->t); i++) tok->t++;
} else if (go_is_octal(esc)) {
// \NNN
for (int i = 0; i < 2 && tok->t < tok->max_t && go_is_octal(*tok->t); i++) tok->t++;
}
// else: simple escape like \n, \t, \\, \' -- already consumed
}
} else if (tok->t < tok->max_t && *tok->t != '\n' && *tok->t != '\'') {
tok->t++; // the rune character
}
if (tok->t < tok->max_t && *tok->t == '\'') tok->t++; // closing '
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token go_parse_slash_or_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
tok->t++; // skip '/'
if (tok->t >= tok->max_t) {
token.type = TOK_OPERATION;
token.len = 1;
return token;
}
if (*tok->t == '/') {
// Line comment
token.type = TOK_COMMENT;
tok->t++;
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
} else if (*tok->t == '*') {
// Block comment
token.type = TOK_MULTILINE_COMMENT;
tok->t++;
while (tok->t < tok->max_t) {
if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') {
tok->t += 2;
break;
}
tok->t++;
}
} else if (*tok->t == '=') {
// /=
token.type = TOK_OPERATION;
tok->t++;
} else {
token.type = TOK_OPERATION;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token go_parse_operator(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_OPERATION;
char c = *tok->t;
tok->t++;
if (tok->t < tok->max_t) {
char n = *tok->t;
// Three-character operators
if (c == '<' && n == '<' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') {
tok->t += 2; goto done;
}
if (c == '>' && n == '>' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') {
tok->t += 2; goto done;
}
if (c == '&' && n == '^' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '=') {
tok->t += 2; goto done;
}
// := (short variable declaration)
if (c == ':' && n == '=') { tok->t++; goto done; }
// Two-character operators
if ((c == '=' && n == '=') || (c == '!' && n == '=') ||
(c == '<' && n == '=') || (c == '>' && n == '=') ||
(c == '+' && n == '=') || (c == '-' && n == '=') ||
(c == '*' && n == '=') || (c == '%' && n == '=') ||
(c == '&' && n == '=') || (c == '|' && n == '=') ||
(c == '^' && n == '=') || (c == '+' && n == '+') ||
(c == '-' && n == '-') || (c == '&' && n == '&') ||
(c == '|' && n == '|') || (c == '<' && n == '<') ||
(c == '>' && n == '>') || (c == '-' && n == '>') ||
(c == '<' && n == '-') || (c == '&' && n == '^')) {
tok->t++;
}
}
done:
token.len = (S32)(tok->t - tok->start_t);
return token;
}
////////////////////////////////
// Main get_next_token
static Token go_get_next_token(Tokenizer *tok) {
tokenizer_eat_whitespace(tok);
Token token = {0};
token.start = (S32)(tok->t - tok->buf);
token.type = TOK_DEFAULT;
if (tok->t >= tok->max_t) {
token.len = 0;
return token; // EOF
}
tok->start_t = tok->t;
char c = *tok->t;
if (go_is_ident_start(c)) {
return go_parse_identifier(tok);
}
if (isdigit((unsigned char)c)) {
return go_parse_number(tok);
}
switch (c) {
case '"': return go_parse_string(tok);
case '`': return go_parse_raw_string(tok);
case '\'': return go_parse_rune_literal(tok);
case '/': return go_parse_slash_or_comment(tok);
// Dot: could start a float literal like .5
case '.':
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
return go_parse_dot_number(tok);
}
// Ellipsis ... or just punctuation
token.type = TOK_PUNCTUATION;
tok->t++;
if (tok->t < tok->max_t && *tok->t == '.' &&
(tok->t + 1) < tok->max_t && *(tok->t + 1) == '.') {
tok->t += 2; // consume ...
}
token.len = (S32)(tok->t - tok->start_t);
return token;
// Punctuation
case ';': case ',':
case '{': case '}': case '(': case ')': case '[': case ']':
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Operators
case '=': case '!': case '<': case '>':
case '+': case '-': case '*': case '%':
case '&': case '|': case '^': case '~':
case ':':
return go_parse_operator(tok);
default:
token.type = TOK_INVALID;
tok->t++;
token.len = 1;
return token;
}
}
////////////////////////////////
// tokenize_go -- main entry point
//
// Tokenizes the full buffer and paints out_tokens[] with token types.
// Second pass: retroactively marks identifiers before '(' as functions.
static void tokenize_go(const char *data, S32 length, U8 *out_tokens) {
memset(out_tokens, TOK_DEFAULT, length);
Tokenizer tok;
tokenizer_init(&tok, data, length);
Token prev = {0};
prev.type = TOK_DEFAULT;
while (tok.t < tok.max_t) {
Token token = go_get_next_token(&tok);
if (token.len == 0) break;
paint_token(out_tokens, token.start, token.len, token.type);
// Retroactively mark identifier before '(' as a function
if (token.type == TOK_PUNCTUATION && token.len == 1 &&
data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
}
prev = token;
}
}

651
c/lexer/lexer_js.c Normal file
View File

@@ -0,0 +1,651 @@
// lexer_js.c -- JavaScript language tokenizer
//
// Supports nested tagged template literals with ${...} interpolation.
// Uses a depth stack to track template literal nesting so that expressions
// inside ${...} can themselves contain template literals at arbitrary depth.
//
// Example: html`outer ${css`inner ${x}`} rest`
// ^^^^ ^^^^^^ ^^^ ^^^^^^ ^ ^ ^^^^^^
// func string func str id str string
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
////////////////////////////////
// JavaScript keyword tables
typedef struct JSKeywordEntry {
const char *word;
Token_Type type;
} JSKeywordEntry;
static const JSKeywordEntry js_keywords[] = {
// Control-flow & language keywords
{"break", TOK_KEYWORD},
{"case", TOK_KEYWORD},
{"catch", TOK_KEYWORD},
{"class", TOK_KEYWORD},
{"const", TOK_KEYWORD},
{"continue", TOK_KEYWORD},
{"debugger", TOK_KEYWORD},
{"default", TOK_KEYWORD},
{"delete", TOK_KEYWORD},
{"do", TOK_KEYWORD},
{"else", TOK_KEYWORD},
{"extends", TOK_KEYWORD},
{"finally", TOK_KEYWORD},
{"for", TOK_KEYWORD},
{"function", TOK_KEYWORD},
{"if", TOK_KEYWORD},
{"in", TOK_KEYWORD},
{"instanceof", TOK_KEYWORD},
{"let", TOK_KEYWORD},
{"new", TOK_KEYWORD},
{"of", TOK_KEYWORD},
{"return", TOK_KEYWORD},
{"switch", TOK_KEYWORD},
{"throw", TOK_KEYWORD},
{"try", TOK_KEYWORD},
{"typeof", TOK_KEYWORD},
{"var", TOK_KEYWORD},
{"void", TOK_KEYWORD},
{"while", TOK_KEYWORD},
{"with", TOK_KEYWORD},
{"yield", TOK_KEYWORD},
{"async", TOK_KEYWORD},
{"await", TOK_KEYWORD},
// Module keywords (directive-style coloring)
{"import", TOK_DIRECTIVE},
{"export", TOK_DIRECTIVE},
{"from", TOK_DIRECTIVE},
{"as", TOK_DIRECTIVE},
// Values
{"true", TOK_VALUE},
{"false", TOK_VALUE},
{"null", TOK_VALUE},
{"undefined", TOK_VALUE},
{"NaN", TOK_VALUE},
{"Infinity", TOK_VALUE},
{"this", TOK_VALUE},
{"super", TOK_VALUE},
// Modifiers / contextual keywords
{"static", TOK_MODIFIER},
{"get", TOK_MODIFIER},
{"set", TOK_MODIFIER},
};
#define JS_KEYWORD_COUNT (S32)(sizeof(js_keywords) / sizeof(js_keywords[0]))
static Token_Type js_lookup_keyword(const char *word, S32 len) {
for (S32 i = 0; i < JS_KEYWORD_COUNT; i++) {
const char *kw = js_keywords[i].word;
S32 kwlen = (S32)strlen(kw);
if (kwlen == len && memcmp(kw, word, len) == 0)
return js_keywords[i].type;
}
return TOK_IDENTIFIER;
}
////////////////////////////////
// Character helpers
static B32 js_is_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_' || c == '$';
}
static B32 js_is_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_' || c == '$';
}
static B32 js_is_hex(char c) {
return isdigit((unsigned char)c) ||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
////////////////////////////////
// Individual token parsers
static Token js_parse_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
const char *begin = tok->t;
while (tok->t < tok->max_t && js_is_ident_char(*tok->t))
tok->t++;
S32 len = (S32)(tok->t - begin);
token.type = js_lookup_keyword(begin, len);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token js_parse_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
char start_char = *tok->t;
tok->t++;
if (tok->t >= tok->max_t) goto done;
if (start_char == '0' && tok->t < tok->max_t) {
// Hex: 0x / 0X
if (*tok->t == 'x' || *tok->t == 'X') {
tok->t++;
while (tok->t < tok->max_t && (js_is_hex(*tok->t) || *tok->t == '_')) tok->t++;
if (tok->t < tok->max_t && *tok->t == 'n') tok->t++; // BigInt
goto done;
}
// Octal: 0o / 0O
if (*tok->t == 'o' || *tok->t == 'O') {
tok->t++;
while (tok->t < tok->max_t && ((*tok->t >= '0' && *tok->t <= '7') || *tok->t == '_')) tok->t++;
if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;
goto done;
}
// Binary: 0b / 0B
if (*tok->t == 'b' || *tok->t == 'B') {
tok->t++;
while (tok->t < tok->max_t && (*tok->t == '0' || *tok->t == '1' || *tok->t == '_')) tok->t++;
if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;
goto done;
}
}
// Decimal digits
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
// Float: .digits
if (tok->t < tok->max_t && *tok->t == '.') {
tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
}
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
}
// BigInt suffix
if (tok->t < tok->max_t && *tok->t == 'n') tok->t++;
done:
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Float starting with dot: .5, .123e4
static Token js_parse_dot_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
tok->t++; // skip '.'
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && (isdigit((unsigned char)*tok->t) || *tok->t == '_'))
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// String literal with given quote character (" or ')
static Token js_parse_string(Tokenizer *tok, char quote) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
B32 escape = 0;
tok->t++; // skip opening quote
while (tok->t < tok->max_t && *tok->t != '\n') {
if (*tok->t == quote && !escape) { tok->t++; break; }
escape = !escape && (*tok->t == '\\');
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// / followed by / or * (comments), or /= (divide-assign), or bare / (divide)
static Token js_parse_slash_or_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
tok->t++; // skip '/'
if (tok->t >= tok->max_t) {
token.type = TOK_OPERATION;
token.len = 1;
return token;
}
if (*tok->t == '/') {
// Line comment
token.type = TOK_COMMENT;
tok->t++;
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
} else if (*tok->t == '*') {
// Block comment
token.type = TOK_MULTILINE_COMMENT;
tok->t++;
while (tok->t < tok->max_t) {
if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') {
tok->t += 2;
break;
}
tok->t++;
}
} else if (*tok->t == '=') {
// /=
token.type = TOK_OPERATION;
tok->t++;
} else {
// bare / (division)
token.type = TOK_OPERATION;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Regex literal: /pattern/flags
static Token js_parse_regex(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL; // color regex like strings
B32 escape = 0;
B32 in_class = 0; // inside character class [...]
tok->t++; // skip opening /
while (tok->t < tok->max_t && *tok->t != '\n') {
if (escape) {
escape = 0;
tok->t++;
continue;
}
if (*tok->t == '\\') {
escape = 1;
tok->t++;
continue;
}
if (*tok->t == '[') { in_class = 1; tok->t++; continue; }
if (*tok->t == ']') { in_class = 0; tok->t++; continue; }
if (*tok->t == '/' && !in_class) {
tok->t++; // closing /
// Consume flags: d, g, i, m, s, u, v, y
while (tok->t < tok->max_t && isalpha((unsigned char)*tok->t)) tok->t++;
break;
}
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Operators (handles multi-character sequences)
static Token js_parse_operator(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_OPERATION;
char c = *tok->t;
tok->t++;
if (tok->t < tok->max_t) {
char n = *tok->t;
// Three-character (and four-character) operators
if ((tok->t + 1) < tok->max_t) {
char nn = *(tok->t + 1);
// >>> and >>>=
if (c == '>' && n == '>' && nn == '>') {
tok->t += 2;
if (tok->t < tok->max_t && *tok->t == '=') tok->t++;
goto done;
}
// ===
if (c == '=' && n == '=' && nn == '=') { tok->t += 2; goto done; }
// !==
if (c == '!' && n == '=' && nn == '=') { tok->t += 2; goto done; }
// **=
if (c == '*' && n == '*' && nn == '=') { tok->t += 2; goto done; }
// <<=
if (c == '<' && n == '<' && nn == '=') { tok->t += 2; goto done; }
// >>=
if (c == '>' && n == '>' && nn == '=') { tok->t += 2; goto done; }
// &&=
if (c == '&' && n == '&' && nn == '=') { tok->t += 2; goto done; }
// ||=
if (c == '|' && n == '|' && nn == '=') { tok->t += 2; goto done; }
// ??=
if (c == '?' && n == '?' && nn == '=') { tok->t += 2; goto done; }
}
// Two-character operators
if ((c == '=' && n == '=') || (c == '!' && n == '=') ||
(c == '<' && n == '=') || (c == '>' && n == '=') ||
(c == '+' && n == '=') || (c == '-' && n == '=') ||
(c == '*' && n == '=') || (c == '%' && n == '=') ||
(c == '&' && n == '=') || (c == '|' && n == '=') ||
(c == '^' && n == '=') || (c == '+' && n == '+') ||
(c == '-' && n == '-') || (c == '&' && n == '&') ||
(c == '|' && n == '|') || (c == '<' && n == '<') ||
(c == '>' && n == '>') || (c == '=' && n == '>') ||
(c == '*' && n == '*') || (c == '?' && n == '?') ||
(c == '?' && n == '.')) {
tok->t++;
}
}
done:
token.len = (S32)(tok->t - tok->start_t);
return token;
}
////////////////////////////////
// Regex vs division heuristic
//
// After identifiers, numbers, values, strings, ) and ] -- slash is division.
// After keywords, operators, most punctuation, start of file -- slash is regex.
static B32 js_slash_is_regex(Token prev, const char *data) {
switch (prev.type) {
case TOK_DEFAULT: return 1; // start of file / no previous token
case TOK_KEYWORD: return 1; // e.g. return /regex/
case TOK_DIRECTIVE: return 1; // after import/export
case TOK_OPERATION:
// After ++ or --, it's division (x++ / y)
if (prev.len == 2) {
char c0 = data[prev.start];
char c1 = data[prev.start + 1];
if ((c0 == '+' && c1 == '+') || (c0 == '-' && c1 == '-'))
return 0;
}
return 1;
case TOK_PUNCTUATION:
// After ) or ], it's division
if (prev.len == 1) {
char c = data[prev.start];
if (c == ')' || c == ']') return 0;
}
return 1;
case TOK_IDENTIFIER:
case TOK_FUNCTION:
case TOK_NUMBER:
case TOK_STRING_LITERAL:
case TOK_CHAR_LITERAL:
case TOK_VALUE:
return 0; // division
default:
return 1;
}
}
////////////////////////////////
// Template literal content scanner
//
// Scans the string content inside a template literal (the text between
// backticks, or between a closing } and the next ${ or closing backtick).
// Paints each byte as TOK_STRING_LITERAL.
//
// Returns 1 if we hit ${ (caller should enter expression mode),
// 0 if we hit closing ` or EOF (template is done).
static int js_scan_template_content(Tokenizer *tok, U8 *out_tokens) {
while (tok->t < tok->max_t) {
if (*tok->t == '`') {
// Closing backtick -- template done
S32 pos = (S32)(tok->t - tok->buf);
out_tokens[pos] = TOK_STRING_LITERAL;
tok->t++;
return 0;
}
if (*tok->t == '$' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '{') {
// Interpolation start -- paint ${ as punctuation
S32 pos = (S32)(tok->t - tok->buf);
out_tokens[pos] = TOK_PUNCTUATION;
out_tokens[pos + 1] = TOK_PUNCTUATION;
tok->t += 2;
return 1;
}
if (*tok->t == '\\' && (tok->t + 1) < tok->max_t) {
// Escape sequence -- paint both chars as string
S32 pos = (S32)(tok->t - tok->buf);
out_tokens[pos] = TOK_STRING_LITERAL;
out_tokens[pos + 1] = TOK_STRING_LITERAL;
tok->t += 2;
continue;
}
// Regular string character
S32 pos = (S32)(tok->t - tok->buf);
out_tokens[pos] = TOK_STRING_LITERAL;
tok->t++;
}
return 0; // EOF -- unclosed template
}
////////////////////////////////
// Get next non-template token
//
// Called by the main loop for normal expression parsing. Template backticks
// and interpolation-closing braces are handled directly by the main loop
// before this function is reached.
static Token js_get_next_token(Tokenizer *tok, Token prev, const char *data) {
tokenizer_eat_whitespace(tok);
Token token = {0};
token.start = (S32)(tok->t - tok->buf);
token.type = TOK_DEFAULT;
if (tok->t >= tok->max_t) {
token.len = 0;
return token;
}
tok->start_t = tok->t;
char c = *tok->t;
if (js_is_ident_start(c)) {
return js_parse_identifier(tok);
}
if (isdigit((unsigned char)c)) {
return js_parse_number(tok);
}
switch (c) {
case '"': return js_parse_string(tok, '"');
case '\'': return js_parse_string(tok, '\'');
case '/':
// Check for comment first (// or /*)
if ((tok->t + 1) < tok->max_t) {
char n = *(tok->t + 1);
if (n == '/' || n == '*')
return js_parse_slash_or_comment(tok);
}
// Regex or division based on context
if (js_slash_is_regex(prev, data))
return js_parse_regex(tok);
return js_parse_slash_or_comment(tok);
// Dot: float literal (.5), spread (...), or member access
case '.':
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
return js_parse_dot_number(tok);
}
if ((tok->t + 2) < tok->max_t && *(tok->t + 1) == '.' && *(tok->t + 2) == '.') {
token.type = TOK_OPERATION;
tok->t += 3;
token.len = 3;
return token;
}
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Punctuation
case ';': case ',':
case '(': case ')': case '[': case ']':
case '{': case '}':
case ':':
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Operators
case '=': case '!': case '<': case '>':
case '+': case '-': case '*': case '%':
case '&': case '|': case '^': case '~':
case '?':
return js_parse_operator(tok);
// Private class fields: #name
case '#':
token.type = TOK_IDENTIFIER;
tok->t++;
while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
// Decorators: @name
case '@':
token.type = TOK_DIRECTIVE;
tok->t++;
while (tok->t < tok->max_t && js_is_ident_char(*tok->t)) tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
default:
token.type = TOK_INVALID;
tok->t++;
token.len = 1;
return token;
}
}
////////////////////////////////
// tokenize_js -- main entry point
//
// Tokenizes the full buffer and paints out_tokens[] with token types.
//
// Template literal nesting is tracked with a depth stack. Each level
// records how many unmatched { braces exist in the current interpolation
// expression. When a } is encountered and the brace count is zero, it
// closes the interpolation and we resume scanning template string content.
//
// html`text ${obj.x} more ${css`inner ${y}`} end`
// |str ||expr | str || |str || || str|
// ^punc ^punc ^punc ^ ^^
// ${ } ${ } }` (nesting!)
#define JS_MAX_TEMPLATE_DEPTH 32
static void tokenize_js(const char *data, S32 length, U8 *out_tokens) {
memset(out_tokens, TOK_DEFAULT, length);
Tokenizer tok;
tokenizer_init(&tok, data, length);
S32 tmpl_depth = 0;
S32 brace_count[JS_MAX_TEMPLATE_DEPTH];
memset(brace_count, 0, sizeof(brace_count));
Token prev = {0};
prev.type = TOK_DEFAULT;
while (tok.t < tok.max_t) {
tokenizer_eat_whitespace(&tok);
if (tok.t >= tok.max_t) break;
char c = *tok.t;
// ---- Template literal: opening backtick ----
// This handles both top-level template literals and nested ones
// (e.g. a tagged template inside a ${...} interpolation).
if (c == '`') {
S32 pos = (S32)(tok.t - tok.buf);
out_tokens[pos] = TOK_STRING_LITERAL;
tok.t++;
int result = js_scan_template_content(&tok, out_tokens);
if (result == 1) {
// Hit ${ -- push template depth
if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) {
brace_count[tmpl_depth] = 0;
tmpl_depth++;
}
}
// result == 0: self-contained template (no interpolation, or
// all interpolations already resolved recursively)
prev.type = TOK_STRING_LITERAL;
prev.start = pos;
prev.len = (S32)(tok.t - tok.buf) - pos;
continue;
}
// ---- Closing brace that ends a template interpolation ----
// When we're inside a template expression and the brace count is
// zero, this } closes the ${...} and we resume string scanning.
if (c == '}' && tmpl_depth > 0 && brace_count[tmpl_depth - 1] == 0) {
S32 pos = (S32)(tok.t - tok.buf);
out_tokens[pos] = TOK_PUNCTUATION;
tok.t++;
tmpl_depth--;
// Resume template string content
int result = js_scan_template_content(&tok, out_tokens);
if (result == 1) {
// Hit another ${ -- re-enter expression mode
if (tmpl_depth < JS_MAX_TEMPLATE_DEPTH) {
brace_count[tmpl_depth] = 0;
tmpl_depth++;
}
}
// result == 0: template closed with backtick
prev.type = TOK_STRING_LITERAL;
prev.start = pos;
prev.len = (S32)(tok.t - tok.buf) - pos;
continue;
}
// ---- Normal token ----
tok.start_t = tok.t;
Token token = js_get_next_token(&tok, prev, data);
if (token.len == 0) break;
paint_token(out_tokens, token.start, token.len, token.type);
// Track brace depth for template interpolation
if (tmpl_depth > 0 && token.type == TOK_PUNCTUATION && token.len == 1) {
char tc = data[token.start];
if (tc == '{') brace_count[tmpl_depth - 1]++;
else if (tc == '}') brace_count[tmpl_depth - 1]--;
}
// Retroactively mark identifier before '(' as function
if (token.type == TOK_PUNCTUATION && token.len == 1 &&
data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
}
prev = token;
}
}

422
c/lexer/lexer_lua.c Normal file
View File

@@ -0,0 +1,422 @@
// lexer_lua.c -- Lua language tokenizer
//
// Same pattern as lexer_go.c: a get_next_token() loop that advances a cursor
// through the source, producing Token structs. After each token we paint
// the per-byte token-type array. A second pass marks identifiers before '('
// as functions.
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
////////////////////////////////
// Lua keyword tables
typedef struct LuaKeywordEntry {
const char *word;
Token_Type type;
} LuaKeywordEntry;
static const LuaKeywordEntry lua_keywords[] = {
// Keywords
{"and", TOK_KEYWORD},
{"break", TOK_KEYWORD},
{"do", TOK_KEYWORD},
{"else", TOK_KEYWORD},
{"elseif", TOK_KEYWORD},
{"end", TOK_KEYWORD},
{"for", TOK_KEYWORD},
{"function", TOK_KEYWORD},
{"goto", TOK_KEYWORD},
{"if", TOK_KEYWORD},
{"in", TOK_KEYWORD},
{"local", TOK_KEYWORD},
{"not", TOK_KEYWORD},
{"or", TOK_KEYWORD},
{"repeat", TOK_KEYWORD},
{"return", TOK_KEYWORD},
{"then", TOK_KEYWORD},
{"until", TOK_KEYWORD},
{"while", TOK_KEYWORD},
// Values
{"true", TOK_VALUE},
{"false", TOK_VALUE},
{"nil", TOK_VALUE},
// Built-in globals / types (use TOK_TYPE for distinct coloring)
{"self", TOK_TYPE},
// Built-in functions (TOK_MODIFIER for distinct coloring)
{"assert", TOK_MODIFIER},
{"collectgarbage", TOK_MODIFIER},
{"dofile", TOK_MODIFIER},
{"error", TOK_MODIFIER},
{"getmetatable", TOK_MODIFIER},
{"ipairs", TOK_MODIFIER},
{"load", TOK_MODIFIER},
{"loadfile", TOK_MODIFIER},
{"next", TOK_MODIFIER},
{"pairs", TOK_MODIFIER},
{"pcall", TOK_MODIFIER},
{"print", TOK_MODIFIER},
{"rawequal", TOK_MODIFIER},
{"rawget", TOK_MODIFIER},
{"rawlen", TOK_MODIFIER},
{"rawset", TOK_MODIFIER},
{"require", TOK_MODIFIER},
{"select", TOK_MODIFIER},
{"setmetatable", TOK_MODIFIER},
{"tonumber", TOK_MODIFIER},
{"tostring", TOK_MODIFIER},
{"type", TOK_MODIFIER},
{"unpack", TOK_MODIFIER},
{"xpcall", TOK_MODIFIER},
};
#define LUA_KEYWORD_COUNT (S32)(sizeof(lua_keywords) / sizeof(lua_keywords[0]))
static Token_Type lua_lookup_keyword(const char *word, S32 len) {
for (S32 i = 0; i < LUA_KEYWORD_COUNT; i++) {
const char *kw = lua_keywords[i].word;
S32 kwlen = (S32)strlen(kw);
if (kwlen == len && memcmp(kw, word, len) == 0)
return lua_keywords[i].type;
}
return TOK_IDENTIFIER;
}
////////////////////////////////
// Character helpers
static B32 lua_is_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_';
}
static B32 lua_is_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_';
}
static B32 lua_is_hex(char c) {
return isdigit((unsigned char)c) ||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
////////////////////////////////
// Long bracket level detection
// Returns the level (number of '=' signs) if we're at a long bracket opening
// like [[ or [==[ etc. Returns -1 if not a long bracket.
static S32 lua_long_bracket_level(const char *p, const char *max_p) {
if (p >= max_p || *p != '[') return -1;
p++;
S32 level = 0;
while (p < max_p && *p == '=') { level++; p++; }
if (p < max_p && *p == '[') return level;
return -1;
}
// Scan past the closing long bracket of the given level.
// Cursor should be right after the opening [=*[.
static void lua_scan_long_bracket_close(Tokenizer *tok, S32 level) {
while (tok->t < tok->max_t) {
if (*tok->t == ']') {
const char *p = tok->t + 1;
S32 count = 0;
while (p < tok->max_t && *p == '=' && count < level) { count++; p++; }
if (count == level && p < tok->max_t && *p == ']') {
tok->t = p + 1;
return;
}
}
tok->t++;
}
}
////////////////////////////////
// Individual token parsers
static Token lua_parse_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
const char *begin = tok->t;
while (tok->t < tok->max_t && lua_is_ident_char(*tok->t))
tok->t++;
S32 len = (S32)(tok->t - begin);
token.type = lua_lookup_keyword(begin, len);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
char c = *tok->t;
tok->t++;
if (c == '0' && tok->t < tok->max_t && (*tok->t == 'x' || *tok->t == 'X')) {
// Hex literal
tok->t++;
while (tok->t < tok->max_t && lua_is_hex(*tok->t)) tok->t++;
// Hex float
if (tok->t < tok->max_t && *tok->t == '.') {
tok->t++;
while (tok->t < tok->max_t && lua_is_hex(*tok->t)) tok->t++;
}
// Hex exponent (p/P)
if (tok->t < tok->max_t && (*tok->t == 'p' || *tok->t == 'P')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
} else {
// Decimal
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
// Float
if (tok->t < tok->max_t && *tok->t == '.') {
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
} else if ((tok->t + 1) >= tok->max_t || !lua_is_ident_start(*(tok->t + 1))) {
tok->t++;
}
}
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_dot_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
tok->t++; // skip '.'
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_string(Tokenizer *tok, char quote) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
B32 escape = 0;
tok->t++; // skip opening quote
while (tok->t < tok->max_t && *tok->t != '\n') {
if (*tok->t == quote && !escape) { tok->t++; break; }
escape = !escape && (*tok->t == '\\');
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_long_string(Tokenizer *tok, S32 level) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
// Skip past opening [=*[
tok->t++; // first [
tok->t += level; // = signs
tok->t++; // second [
lua_scan_long_bracket_close(tok, level);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
tok->t += 2; // skip '--'
// Check for long comment --[=*[
S32 level = lua_long_bracket_level(tok->t, tok->max_t);
if (level >= 0) {
token.type = TOK_MULTILINE_COMMENT;
tok->t++; // first [
tok->t += level; // = signs
tok->t++; // second [
lua_scan_long_bracket_close(tok, level);
} else {
// Single-line comment
token.type = TOK_COMMENT;
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token lua_parse_operator(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_OPERATION;
char c = *tok->t;
tok->t++;
if (tok->t < tok->max_t) {
char n = *tok->t;
// Two-character operators
if ((c == '=' && n == '=') || (c == '~' && n == '=') ||
(c == '<' && n == '=') || (c == '>' && n == '=') ||
(c == '.' && n == '.') || (c == '<' && n == '<') ||
(c == '>' && n == '>') || (c == '/' && n == '/')) {
tok->t++;
// Three-character: .. can be followed by . to make ...
if (c == '.' && n == '.' && tok->t < tok->max_t && *tok->t == '.') {
tok->t++;
}
}
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
////////////////////////////////
// Main get_next_token
static Token lua_get_next_token(Tokenizer *tok) {
tokenizer_eat_whitespace(tok);
Token token = {0};
token.start = (S32)(tok->t - tok->buf);
token.type = TOK_DEFAULT;
if (tok->t >= tok->max_t) {
token.len = 0;
return token;
}
tok->start_t = tok->t;
char c = *tok->t;
if (lua_is_ident_start(c)) {
return lua_parse_identifier(tok);
}
if (isdigit((unsigned char)c)) {
return lua_parse_number(tok);
}
switch (c) {
case '\'':
case '"':
return lua_parse_string(tok, c);
case '[': {
// Check for long string [=*[
S32 level = lua_long_bracket_level(tok->t, tok->max_t);
if (level >= 0) {
return lua_parse_long_string(tok, level);
}
// Plain bracket punctuation
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
}
case '-':
// Check for comment --
if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '-') {
return lua_parse_comment(tok);
}
// Minus operator
token.type = TOK_OPERATION;
tok->t++;
token.len = 1;
return token;
case '.':
// .digits = float literal
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
return lua_parse_dot_number(tok);
}
// .. or ... or just .
return lua_parse_operator(tok);
// Punctuation
case ';': case ',': case ':':
case '{': case '}': case '(': case ')': case ']':
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Directive: shebang or labels (::label::)
case '#':
// Shebang line or length operator
if (tok->t == tok->buf && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '!') {
token.type = TOK_DIRECTIVE;
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
}
token.type = TOK_OPERATION;
tok->t++;
token.len = 1;
return token;
// Operators
case '=': case '~': case '<': case '>':
case '+': case '*': case '/': case '%':
case '^': case '&': case '|':
return lua_parse_operator(tok);
default:
token.type = TOK_INVALID;
tok->t++;
token.len = 1;
return token;
}
}
////////////////////////////////
// tokenize_lua -- main entry point
static void tokenize_lua(const char *data, S32 length, U8 *out_tokens) {
memset(out_tokens, TOK_DEFAULT, length);
Tokenizer tok;
tokenizer_init(&tok, data, length);
Token prev = {0};
prev.type = TOK_DEFAULT;
while (tok.t < tok.max_t) {
Token token = lua_get_next_token(&tok);
if (token.len == 0) break;
paint_token(out_tokens, token.start, token.len, token.type);
// Retroactively mark identifier before '(' as a function
if (token.type == TOK_PUNCTUATION && token.len == 1 &&
data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
}
prev = token;
}
}

645
c/lexer/lexer_sql.c Normal file
View File

@@ -0,0 +1,645 @@
// lexer_sql.c -- SQL language tokenizer
//
// Same pattern as lexer_go.c: a get_next_token() loop that advances a cursor
// through the source, producing Token structs. After each token we paint
// the per-byte token-type array. A second pass marks identifiers before '('
// as functions.
//
// SQL keywords are case-insensitive, so comparisons use a case-insensitive
// match. Covers standard SQL plus common extensions (MySQL, PostgreSQL, etc.).
#include "lexer/lexer.h"
#include <ctype.h>
#include <string.h>
////////////////////////////////
// SQL keyword tables
typedef struct SQLKeywordEntry {
const char *word;
Token_Type type;
} SQLKeywordEntry;
static const SQLKeywordEntry sql_keywords[] = {
// DML / DQL keywords
{"SELECT", TOK_KEYWORD},
{"FROM", TOK_KEYWORD},
{"WHERE", TOK_KEYWORD},
{"INSERT", TOK_KEYWORD},
{"INTO", TOK_KEYWORD},
{"UPDATE", TOK_KEYWORD},
{"DELETE", TOK_KEYWORD},
{"SET", TOK_KEYWORD},
{"VALUES", TOK_KEYWORD},
{"AS", TOK_KEYWORD},
{"ON", TOK_KEYWORD},
{"JOIN", TOK_KEYWORD},
{"INNER", TOK_KEYWORD},
{"LEFT", TOK_KEYWORD},
{"RIGHT", TOK_KEYWORD},
{"OUTER", TOK_KEYWORD},
{"FULL", TOK_KEYWORD},
{"CROSS", TOK_KEYWORD},
{"NATURAL", TOK_KEYWORD},
{"USING", TOK_KEYWORD},
{"ORDER", TOK_KEYWORD},
{"BY", TOK_KEYWORD},
{"GROUP", TOK_KEYWORD},
{"HAVING", TOK_KEYWORD},
{"LIMIT", TOK_KEYWORD},
{"OFFSET", TOK_KEYWORD},
{"UNION", TOK_KEYWORD},
{"ALL", TOK_KEYWORD},
{"DISTINCT", TOK_KEYWORD},
{"TOP", TOK_KEYWORD},
{"FETCH", TOK_KEYWORD},
{"NEXT", TOK_KEYWORD},
{"ROWS", TOK_KEYWORD},
{"ONLY", TOK_KEYWORD},
{"FIRST", TOK_KEYWORD},
{"LAST", TOK_KEYWORD},
// DDL keywords
{"CREATE", TOK_KEYWORD},
{"ALTER", TOK_KEYWORD},
{"DROP", TOK_KEYWORD},
{"TABLE", TOK_KEYWORD},
{"VIEW", TOK_KEYWORD},
{"INDEX", TOK_KEYWORD},
{"DATABASE", TOK_KEYWORD},
{"SCHEMA", TOK_KEYWORD},
{"COLUMN", TOK_KEYWORD},
{"ADD", TOK_KEYWORD},
{"RENAME", TOK_KEYWORD},
{"TRUNCATE", TOK_KEYWORD},
{"REPLACE", TOK_KEYWORD},
{"TEMPORARY", TOK_KEYWORD},
{"TEMP", TOK_KEYWORD},
{"IF", TOK_KEYWORD},
{"EXISTS", TOK_KEYWORD},
{"CASCADE", TOK_KEYWORD},
{"RESTRICT", TOK_KEYWORD},
// Constraints & keys
{"PRIMARY", TOK_KEYWORD},
{"KEY", TOK_KEYWORD},
{"FOREIGN", TOK_KEYWORD},
{"REFERENCES", TOK_KEYWORD},
{"UNIQUE", TOK_KEYWORD},
{"CHECK", TOK_KEYWORD},
{"CONSTRAINT", TOK_KEYWORD},
{"DEFAULT", TOK_KEYWORD},
{"AUTOINCREMENT", TOK_KEYWORD},
{"AUTO_INCREMENT", TOK_KEYWORD},
{"IDENTITY", TOK_KEYWORD},
// Control flow / procedural
{"BEGIN", TOK_KEYWORD},
{"END", TOK_KEYWORD},
{"COMMIT", TOK_KEYWORD},
{"ROLLBACK", TOK_KEYWORD},
{"SAVEPOINT", TOK_KEYWORD},
{"TRANSACTION", TOK_KEYWORD},
{"RETURN", TOK_KEYWORD},
{"RETURNS", TOK_KEYWORD},
{"DECLARE", TOK_KEYWORD},
{"CURSOR", TOK_KEYWORD},
{"OPEN", TOK_KEYWORD},
{"CLOSE", TOK_KEYWORD},
{"DEALLOCATE", TOK_KEYWORD},
{"EXEC", TOK_KEYWORD},
{"EXECUTE", TOK_KEYWORD},
{"CALL", TOK_KEYWORD},
{"PROCEDURE", TOK_KEYWORD},
{"FUNCTION", TOK_KEYWORD},
{"TRIGGER", TOK_KEYWORD},
{"CASE", TOK_KEYWORD},
{"WHEN", TOK_KEYWORD},
{"THEN", TOK_KEYWORD},
{"ELSE", TOK_KEYWORD},
{"WHILE", TOK_KEYWORD},
{"LOOP", TOK_KEYWORD},
{"FOR", TOK_KEYWORD},
{"EACH", TOK_KEYWORD},
{"ROW", TOK_KEYWORD},
{"AFTER", TOK_KEYWORD},
{"BEFORE", TOK_KEYWORD},
{"INSTEAD", TOK_KEYWORD},
{"OF", TOK_KEYWORD},
// Logical operators (keywords)
{"AND", TOK_KEYWORD},
{"OR", TOK_KEYWORD},
{"NOT", TOK_KEYWORD},
{"IN", TOK_KEYWORD},
{"BETWEEN", TOK_KEYWORD},
{"LIKE", TOK_KEYWORD},
{"ILIKE", TOK_KEYWORD},
{"IS", TOK_KEYWORD},
{"ANY", TOK_KEYWORD},
{"SOME", TOK_KEYWORD},
{"EXCEPT", TOK_KEYWORD},
{"INTERSECT", TOK_KEYWORD},
{"WITH", TOK_KEYWORD},
{"RECURSIVE", TOK_KEYWORD},
{"OVER", TOK_KEYWORD},
{"PARTITION", TOK_KEYWORD},
{"WINDOW", TOK_KEYWORD},
{"LATERAL", TOK_KEYWORD},
// Misc
{"GRANT", TOK_KEYWORD},
{"REVOKE", TOK_KEYWORD},
{"EXPLAIN", TOK_KEYWORD},
{"ANALYZE", TOK_KEYWORD},
{"VACUUM", TOK_KEYWORD},
{"PRAGMA", TOK_KEYWORD},
{"DESCRIBE", TOK_KEYWORD},
{"SHOW", TOK_KEYWORD},
{"USE", TOK_KEYWORD},
{"COPY", TOK_KEYWORD},
{"PERFORM", TOK_KEYWORD},
{"RAISE", TOK_KEYWORD},
// Modifiers
{"ASC", TOK_MODIFIER},
{"DESC", TOK_MODIFIER},
{"NULLS", TOK_MODIFIER},
{"NOT", TOK_MODIFIER},
// Data types
{"INT", TOK_TYPE},
{"INTEGER", TOK_TYPE},
{"SMALLINT", TOK_TYPE},
{"BIGINT", TOK_TYPE},
{"TINYINT", TOK_TYPE},
{"MEDIUMINT", TOK_TYPE},
{"SERIAL", TOK_TYPE},
{"BIGSERIAL", TOK_TYPE},
{"FLOAT", TOK_TYPE},
{"REAL", TOK_TYPE},
{"DOUBLE", TOK_TYPE},
{"DECIMAL", TOK_TYPE},
{"NUMERIC", TOK_TYPE},
{"PRECISION", TOK_TYPE},
{"CHAR", TOK_TYPE},
{"VARCHAR", TOK_TYPE},
{"TEXT", TOK_TYPE},
{"NCHAR", TOK_TYPE},
{"NVARCHAR", TOK_TYPE},
{"NTEXT", TOK_TYPE},
{"BLOB", TOK_TYPE},
{"CLOB", TOK_TYPE},
{"BYTEA", TOK_TYPE},
{"BOOLEAN", TOK_TYPE},
{"BOOL", TOK_TYPE},
{"DATE", TOK_TYPE},
{"TIME", TOK_TYPE},
{"DATETIME", TOK_TYPE},
{"TIMESTAMP", TOK_TYPE},
{"TIMESTAMPTZ", TOK_TYPE},
{"INTERVAL", TOK_TYPE},
{"UUID", TOK_TYPE},
{"JSON", TOK_TYPE},
{"JSONB", TOK_TYPE},
{"XML", TOK_TYPE},
{"ARRAY", TOK_TYPE},
{"ENUM", TOK_TYPE},
{"MONEY", TOK_TYPE},
{"BIT", TOK_TYPE},
{"VARBIT", TOK_TYPE},
{"INET", TOK_TYPE},
{"CIDR", TOK_TYPE},
{"MACADDR", TOK_TYPE},
{"POINT", TOK_TYPE},
{"LINE", TOK_TYPE},
{"POLYGON", TOK_TYPE},
{"GEOMETRY", TOK_TYPE},
{"GEOGRAPHY", TOK_TYPE},
// Values
{"NULL", TOK_VALUE},
{"TRUE", TOK_VALUE},
{"FALSE", TOK_VALUE},
{"CURRENT_DATE", TOK_VALUE},
{"CURRENT_TIME", TOK_VALUE},
{"CURRENT_TIMESTAMP", TOK_VALUE},
{"CURRENT_USER", TOK_VALUE},
// Built-in aggregate / window / common functions (TOK_MODIFIER)
{"COUNT", TOK_MODIFIER},
{"SUM", TOK_MODIFIER},
{"AVG", TOK_MODIFIER},
{"MIN", TOK_MODIFIER},
{"MAX", TOK_MODIFIER},
{"COALESCE", TOK_MODIFIER},
{"NULLIF", TOK_MODIFIER},
{"CAST", TOK_MODIFIER},
{"CONVERT", TOK_MODIFIER},
{"IFNULL", TOK_MODIFIER},
{"ISNULL", TOK_MODIFIER},
{"NVL", TOK_MODIFIER},
{"ROW_NUMBER", TOK_MODIFIER},
{"RANK", TOK_MODIFIER},
{"DENSE_RANK", TOK_MODIFIER},
{"NTILE", TOK_MODIFIER},
{"LAG", TOK_MODIFIER},
{"LEAD", TOK_MODIFIER},
{"FIRST_VALUE", TOK_MODIFIER},
{"LAST_VALUE", TOK_MODIFIER},
{"SUBSTR", TOK_MODIFIER},
{"SUBSTRING", TOK_MODIFIER},
{"TRIM", TOK_MODIFIER},
{"UPPER", TOK_MODIFIER},
{"LOWER", TOK_MODIFIER},
{"LENGTH", TOK_MODIFIER},
{"CONCAT", TOK_MODIFIER},
{"REPLACE", TOK_MODIFIER},
{"ABS", TOK_MODIFIER},
{"ROUND", TOK_MODIFIER},
{"CEIL", TOK_MODIFIER},
{"FLOOR", TOK_MODIFIER},
{"NOW", TOK_MODIFIER},
{"EXTRACT", TOK_MODIFIER},
{"STRING_AGG", TOK_MODIFIER},
{"GROUP_CONCAT", TOK_MODIFIER},
{"ARRAY_AGG", TOK_MODIFIER},
{"GREATEST", TOK_MODIFIER},
{"LEAST", TOK_MODIFIER},
};
#define SQL_KEYWORD_COUNT (S32)(sizeof(sql_keywords) / sizeof(sql_keywords[0]))
// Case-insensitive keyword lookup
static Token_Type sql_lookup_keyword(const char *word, S32 len) {
for (S32 i = 0; i < SQL_KEYWORD_COUNT; i++) {
const char *kw = sql_keywords[i].word;
S32 kwlen = (S32)strlen(kw);
if (kwlen != len) continue;
B32 match = 1;
for (S32 j = 0; j < len; j++) {
if (toupper((unsigned char)word[j]) != (unsigned char)kw[j]) {
match = 0;
break;
}
}
if (match) return sql_keywords[i].type;
}
return TOK_IDENTIFIER;
}
////////////////////////////////
// Character helpers
static B32 sql_is_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_';
}
static B32 sql_is_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_';
}
////////////////////////////////
// Individual token parsers
static Token sql_parse_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
const char *begin = tok->t;
while (tok->t < tok->max_t && sql_is_ident_char(*tok->t))
tok->t++;
S32 len = (S32)(tok->t - begin);
token.type = sql_lookup_keyword(begin, len);
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token sql_parse_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t))
tok->t++;
// Decimal part
if (tok->t < tok->max_t && *tok->t == '.') {
tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t))
tok->t++;
}
// Exponent
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token sql_parse_dot_number(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_NUMBER;
tok->t++; // skip '.'
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) {
tok->t++;
if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++;
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Single-quoted string: 'text', with '' as escape for embedded quote
static Token sql_parse_string(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_STRING_LITERAL;
tok->t++; // skip opening '
while (tok->t < tok->max_t) {
if (*tok->t == '\'') {
tok->t++;
// '' is an escaped quote inside a string
if (tok->t < tok->max_t && *tok->t == '\'') {
tok->t++;
continue;
}
break;
}
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Double-quoted identifier: "column_name"
static Token sql_parse_quoted_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_IDENTIFIER;
tok->t++; // skip opening "
while (tok->t < tok->max_t) {
if (*tok->t == '"') {
tok->t++;
// "" is an escaped quote inside a quoted identifier
if (tok->t < tok->max_t && *tok->t == '"') {
tok->t++;
continue;
}
break;
}
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Backtick-quoted identifier (MySQL): `column_name`
static Token sql_parse_backtick_identifier(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_IDENTIFIER;
tok->t++; // skip opening `
while (tok->t < tok->max_t) {
if (*tok->t == '`') { tok->t++; break; }
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Line comment: -- ...
static Token sql_parse_line_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_COMMENT;
tok->t += 2; // skip '--'
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Block comment: /* ... */
static Token sql_parse_block_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_MULTILINE_COMMENT;
tok->t += 2; // skip '/*'
while (tok->t < tok->max_t) {
if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') {
tok->t += 2;
break;
}
tok->t++;
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// MySQL # comment
static Token sql_parse_hash_comment(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_COMMENT;
tok->t++; // skip '#'
while (tok->t < tok->max_t && *tok->t != '\n') tok->t++;
token.len = (S32)(tok->t - tok->start_t);
return token;
}
// Variable / parameter: @var, @@var, :param, $1
static Token sql_parse_variable(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_DIRECTIVE;
char c = *tok->t;
tok->t++;
if (c == '@') {
// @@ for system variables
if (tok->t < tok->max_t && *tok->t == '@') tok->t++;
while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++;
} else if (c == ':') {
while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++;
} else if (c == '$') {
// $1, $2, ... (PostgreSQL positional params)
while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++;
// Also handle $tag$ dollar-quoted strings if no digits follow
if (tok->t == tok->start_t + 1) {
// Just a lone $, treat as punctuation
token.type = TOK_PUNCTUATION;
}
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
static Token sql_parse_operator(Tokenizer *tok) {
Token token;
token.start = (S32)(tok->start_t - tok->buf);
token.type = TOK_OPERATION;
char c = *tok->t;
tok->t++;
if (tok->t < tok->max_t) {
char n = *tok->t;
// Two-character operators
if ((c == '<' && n == '=') || (c == '>' && n == '=') ||
(c == '<' && n == '>') || (c == '!' && n == '=') ||
(c == '|' && n == '|') || (c == ':' && n == ':')) {
tok->t++;
}
}
token.len = (S32)(tok->t - tok->start_t);
return token;
}
////////////////////////////////
// Main get_next_token
static Token sql_get_next_token(Tokenizer *tok) {
tokenizer_eat_whitespace(tok);
Token token = {0};
token.start = (S32)(tok->t - tok->buf);
token.type = TOK_DEFAULT;
if (tok->t >= tok->max_t) {
token.len = 0;
return token;
}
tok->start_t = tok->t;
char c = *tok->t;
if (sql_is_ident_start(c)) {
return sql_parse_identifier(tok);
}
if (isdigit((unsigned char)c)) {
return sql_parse_number(tok);
}
switch (c) {
case '\'':
return sql_parse_string(tok);
case '"':
return sql_parse_quoted_identifier(tok);
case '`':
return sql_parse_backtick_identifier(tok);
case '-':
if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '-') {
return sql_parse_line_comment(tok);
}
token.type = TOK_OPERATION;
tok->t++;
token.len = 1;
return token;
case '/':
if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '*') {
return sql_parse_block_comment(tok);
}
token.type = TOK_OPERATION;
tok->t++;
token.len = 1;
return token;
case '#':
return sql_parse_hash_comment(tok);
case '.':
if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) {
return sql_parse_dot_number(tok);
}
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
case '@': case '$':
return sql_parse_variable(tok);
case ':':
// :param or :: cast operator
if ((tok->t + 1) < tok->max_t) {
char n = *(tok->t + 1);
if (n == ':') return sql_parse_operator(tok);
if (sql_is_ident_start(n)) return sql_parse_variable(tok);
}
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Punctuation
case ';': case ',':
case '{': case '}': case '(': case ')': case '[': case ']':
token.type = TOK_PUNCTUATION;
tok->t++;
token.len = 1;
return token;
// Operators
case '=': case '<': case '>': case '!':
case '+': case '*': case '%': case '&':
case '|': case '^': case '~':
return sql_parse_operator(tok);
default:
token.type = TOK_INVALID;
tok->t++;
token.len = 1;
return token;
}
}
////////////////////////////////
// tokenize_sql -- main entry point
static void tokenize_sql(const char *data, S32 length, U8 *out_tokens) {
memset(out_tokens, TOK_DEFAULT, length);
Tokenizer tok;
tokenizer_init(&tok, data, length);
Token prev = {0};
prev.type = TOK_DEFAULT;
while (tok.t < tok.max_t) {
Token token = sql_get_next_token(&tok);
if (token.len == 0) break;
paint_token(out_tokens, token.start, token.len, token.type);
// Retroactively mark identifier before '(' as a function
if (token.type == TOK_PUNCTUATION && token.len == 1 &&
data[token.start] == '(' && prev.type == TOK_IDENTIFIER) {
paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION);
}
prev = token;
}
}

502
c/lexer/lexer_theme.c Normal file
View File

@@ -0,0 +1,502 @@
// lexer_theme.c -- Built-in color themes
//
// Colors ported from the Focus editor theme files.
// Hex values are RGB (alpha channel dropped, always opaque in terminal).
#include "lexer/lexer_theme.h"
S32 g_active_theme_idx = 0;
Theme g_themes[THEME_COUNT] = {
////////////////////////////////
// Default
// Uses the terminal's own 256-color palette. No background override.
{
.name = "Default",
.use_truecolor = 0,
.colors = {
/* DEFAULT */ RGB_HEX(0xBDBDBD),
/* COMMENT */ RGB_HEX(0x6A6A6A),
/* MULTILINE_COMMENT */ RGB_HEX(0x6A6A6A),
/* STRING_LITERAL */ RGB_HEX(0x6AAF50),
/* CHAR_LITERAL */ RGB_HEX(0x6AAF50),
/* NUMBER */ RGB_HEX(0xC678DD),
/* IDENTIFIER */ RGB_HEX(0xBDBDBD),
/* FUNCTION */ RGB_HEX(0x56B6C2),
/* KEYWORD */ RGB_HEX(0xD19A66),
/* TYPE */ RGB_HEX(0x61AFEF),
/* VALUE */ RGB_HEX(0xC678DD),
/* MODIFIER */ RGB_HEX(0xE06C75),
/* DIRECTIVE */ RGB_HEX(0xE06C75),
/* PUNCTUATION */ RGB_HEX(0xBDBDBD),
/* OPERATION */ RGB_HEX(0xBDBDBD),
/* INVALID */ RGB_HEX(0xFF5555),
},
.ansi_colors = {
/* DEFAULT */ 7,
/* COMMENT */ 242,
/* MULTILINE_COMMENT */ 242,
/* STRING_LITERAL */ 2,
/* CHAR_LITERAL */ 2,
/* NUMBER */ 5,
/* IDENTIFIER */ 7,
/* FUNCTION */ 6,
/* KEYWORD */ 3,
/* TYPE */ 4,
/* VALUE */ 5,
/* MODIFIER */ 1,
/* DIRECTIVE */ 1,
/* PUNCTUATION */ 7,
/* OPERATION */ 7,
/* INVALID */ 9,
},
.background = RGB_HEX(0x1E1E1E),
.set_background = 0,
.status_bg = RGB_HEX(0x252525),
.status_fg = RGB_HEX(0xDDDDDD),
.status_fg_dim = RGB_HEX(0x888888),
.mb_bg = RGB_HEX(0x3D5A80),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0x888888),
},
////////////////////////////////
// Default Light
// Uses the terminal's own 256-color palette with a light background.
{
.name = "Default Light",
.use_truecolor = 0,
.colors = {
/* DEFAULT */ RGB_HEX(0x383A42),
/* COMMENT */ RGB_HEX(0xA0A1A7),
/* MULTILINE_COMMENT */ RGB_HEX(0xA0A1A7),
/* STRING_LITERAL */ RGB_HEX(0xA62C21),
/* CHAR_LITERAL */ RGB_HEX(0xA62C21),
/* NUMBER */ RGB_HEX(0x986801),
/* IDENTIFIER */ RGB_HEX(0x383A42),
/* FUNCTION */ RGB_HEX(0x0184BC),
/* KEYWORD */ RGB_HEX(0x4078F2),
/* TYPE */ RGB_HEX(0x50A14F),
/* VALUE */ RGB_HEX(0x986801),
/* MODIFIER */ RGB_HEX(0x4078F2),
/* DIRECTIVE */ RGB_HEX(0xA62C21),
/* PUNCTUATION */ RGB_HEX(0x383A42),
/* OPERATION */ RGB_HEX(0x383A42),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {
/* DEFAULT */ 0, // black
/* COMMENT */ 243, // dark gray
/* MULTILINE_COMMENT */ 243,
/* STRING_LITERAL */ 1, // red
/* CHAR_LITERAL */ 1,
/* NUMBER */ 5, // magenta
/* IDENTIFIER */ 0, // black
/* FUNCTION */ 6, // cyan
/* KEYWORD */ 4, // blue
/* TYPE */ 2, // green
/* VALUE */ 5, // magenta
/* MODIFIER */ 4, // blue
/* DIRECTIVE */ 1, // red
/* PUNCTUATION */ 0, // black
/* OPERATION */ 0, // black
/* INVALID */ 9, // bright red
},
.background = RGB_HEX(0xFAFAFA),
.set_background = 0,
.status_bg = RGB_HEX(0xD0D0D0),
.status_fg = RGB_HEX(0x000000),
.status_fg_dim = RGB_HEX(0x606060),
.mb_bg = RGB_HEX(0x0060C0),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0x808080),
},
////////////////////////////////
// Focus
// The default Focus editor theme.
{
.name = "Focus",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xBFC9DB),
/* COMMENT */ RGB_HEX(0x87919D),
/* MULTILINE_COMMENT */ RGB_HEX(0x87919D),
/* STRING_LITERAL */ RGB_HEX(0xD4BC7D),
/* CHAR_LITERAL */ RGB_HEX(0xD4BC7D),
/* NUMBER */ RGB_HEX(0xD699B5),
/* IDENTIFIER */ RGB_HEX(0xBFC9DB),
/* FUNCTION */ RGB_HEX(0xD0C5A9),
/* KEYWORD */ RGB_HEX(0xE67D74),
/* TYPE */ RGB_HEX(0x82AAA3),
/* VALUE */ RGB_HEX(0xD699B5),
/* MODIFIER */ RGB_HEX(0xE67D74),
/* DIRECTIVE */ RGB_HEX(0xE67D74),
/* PUNCTUATION */ RGB_HEX(0xBFC9DB),
/* OPERATION */ RGB_HEX(0xE0AD82),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x15212A),
.set_background = 1,
.status_bg = RGB_HEX(0x0C1620),
.status_fg = RGB_HEX(0xBFC9DB),
.status_fg_dim = RGB_HEX(0x607080),
.mb_bg = RGB_HEX(0x1C3040),
.mb_fg = RGB_HEX(0xBFC9DB),
.mb_detail = RGB_HEX(0x607080),
},
////////////////////////////////
// Handmade Hero
// Based on Casey Muratori's emacs theme from the Handmade Hero series.
{
.name = "Handmade Hero",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xCDAA7D),
/* COMMENT */ RGB_HEX(0x7F7F7F),
/* MULTILINE_COMMENT */ RGB_HEX(0x87919D),
/* STRING_LITERAL */ RGB_HEX(0x6B8E23),
/* CHAR_LITERAL */ RGB_HEX(0x6B8E23),
/* NUMBER */ RGB_HEX(0xD699B5),
/* IDENTIFIER */ RGB_HEX(0xBFC9DB),
/* FUNCTION */ RGB_HEX(0xCDAA7D),
/* KEYWORD */ RGB_HEX(0xB8860B),
/* TYPE */ RGB_HEX(0xB8860B),
/* VALUE */ RGB_HEX(0x6B8E23),
/* MODIFIER */ RGB_HEX(0xE67D74),
/* DIRECTIVE */ RGB_HEX(0xE67D74),
/* PUNCTUATION */ RGB_HEX(0xCDAA7D),
/* OPERATION */ RGB_HEX(0xCDAA7D),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x161616),
.set_background = 1,
.status_bg = RGB_HEX(0x0C0C0C),
.status_fg = RGB_HEX(0xCDAA7D),
.status_fg_dim = RGB_HEX(0x6B5A3F),
.mb_bg = RGB_HEX(0x403020),
.mb_fg = RGB_HEX(0xCDAA7D),
.mb_detail = RGB_HEX(0x6B5A3F),
},
////////////////////////////////
// Witness Classic
// Jonathan Blow's classic color scheme.
{
.name = "Witness Classic",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xD3B58D),
/* COMMENT */ RGB_HEX(0xFFFF00),
/* MULTILINE_COMMENT */ RGB_HEX(0x87919D),
/* STRING_LITERAL */ RGB_HEX(0xBEBEBE),
/* CHAR_LITERAL */ RGB_HEX(0xBEBEBE),
/* NUMBER */ RGB_HEX(0xD699B5),
/* IDENTIFIER */ RGB_HEX(0xBFC9DB),
/* FUNCTION */ RGB_HEX(0xD3B58D),
/* KEYWORD */ RGB_HEX(0xFFFFFF),
/* TYPE */ RGB_HEX(0x98FB98),
/* VALUE */ RGB_HEX(0x7FFFD4),
/* MODIFIER */ RGB_HEX(0xE67D74),
/* DIRECTIVE */ RGB_HEX(0xE67D74),
/* PUNCTUATION */ RGB_HEX(0xD3B58D),
/* OPERATION */ RGB_HEX(0xD3B58D),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x292929),
.set_background = 1,
.status_bg = RGB_HEX(0x222222),
.status_fg = RGB_HEX(0xDDDDDD),
.status_fg_dim = RGB_HEX(0x888888),
.mb_bg = RGB_HEX(0x3A3A50),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0x888888),
},
////////////////////////////////
// Witness
// Jonathan Blow's alternative dark-teal theme.
{
.name = "Witness",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xD3B58D),
/* COMMENT */ RGB_HEX(0x3DDF23),
/* MULTILINE_COMMENT */ RGB_HEX(0x87919D),
/* STRING_LITERAL */ RGB_HEX(0x0FDFAF),
/* CHAR_LITERAL */ RGB_HEX(0x0FDFAF),
/* NUMBER */ RGB_HEX(0xD699B5),
/* IDENTIFIER */ RGB_HEX(0xBFC9DB),
/* FUNCTION */ RGB_HEX(0xD3B58D),
/* KEYWORD */ RGB_HEX(0xFFFFFF),
/* TYPE */ RGB_HEX(0x98FB98),
/* VALUE */ RGB_HEX(0x7FFFD4),
/* MODIFIER */ RGB_HEX(0xE67D74),
/* DIRECTIVE */ RGB_HEX(0xE67D74),
/* PUNCTUATION */ RGB_HEX(0xD3B58D),
/* OPERATION */ RGB_HEX(0xE0AD82),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x072626),
.set_background = 1,
.status_bg = RGB_HEX(0x041818),
.status_fg = RGB_HEX(0xD3B58D),
.status_fg_dim = RGB_HEX(0x6B7A6B),
.mb_bg = RGB_HEX(0x0E3A3A),
.mb_fg = RGB_HEX(0xD3B58D),
.mb_detail = RGB_HEX(0x6B7A6B),
},
////////////////////////////////
// Visual Studio Classic
// Light theme matching Visual Studio 6.0 (1998).
{
.name = "VS Classic",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0x000000),
/* COMMENT */ RGB_HEX(0x008000),
/* MULTILINE_COMMENT */ RGB_HEX(0x008000),
/* STRING_LITERAL */ RGB_HEX(0x800000),
/* CHAR_LITERAL */ RGB_HEX(0x800000),
/* NUMBER */ RGB_HEX(0x000000),
/* IDENTIFIER */ RGB_HEX(0x000000),
/* FUNCTION */ RGB_HEX(0x000000),
/* KEYWORD */ RGB_HEX(0x0000FF),
/* TYPE */ RGB_HEX(0x0000FF),
/* VALUE */ RGB_HEX(0x0000FF),
/* MODIFIER */ RGB_HEX(0x0000FF),
/* DIRECTIVE */ RGB_HEX(0x0000FF),
/* PUNCTUATION */ RGB_HEX(0x000000),
/* OPERATION */ RGB_HEX(0x000000),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0xFFFFFF),
.set_background = 1,
.status_bg = RGB_HEX(0xD4D0C8),
.status_fg = RGB_HEX(0x000000),
.status_fg_dim = RGB_HEX(0x606060),
.mb_bg = RGB_HEX(0x0A246A),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0xA0A0A0),
.fb_bg = RGB_HEX(0xFFFFFF),
.set_fb_bg = 1,
},
////////////////////////////////
// RAD Debugger
// Default color scheme from the RAD Debugger project.
{
.name = "RAD Debugger",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xDADADA),
/* COMMENT */ RGB_HEX(0x5D7856),
/* MULTILINE_COMMENT */ RGB_HEX(0x5D7856),
/* STRING_LITERAL */ RGB_HEX(0xFFA070),
/* CHAR_LITERAL */ RGB_HEX(0xFFA070),
/* NUMBER */ RGB_HEX(0xD0D0A0),
/* IDENTIFIER */ RGB_HEX(0xDADADA),
/* FUNCTION */ RGB_HEX(0xDADADA),
/* KEYWORD */ RGB_HEX(0xF0C674),
/* TYPE */ RGB_HEX(0x70C0B0),
/* VALUE */ RGB_HEX(0xD0D0A0),
/* MODIFIER */ RGB_HEX(0xF0C674),
/* DIRECTIVE */ RGB_HEX(0xCC7070),
/* PUNCTUATION */ RGB_HEX(0xDADADA),
/* OPERATION */ RGB_HEX(0xDADADA),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x1C1C1C),
.set_background = 1,
.status_bg = RGB_HEX(0x121212),
.status_fg = RGB_HEX(0xDADADA),
.status_fg_dim = RGB_HEX(0x707070),
.mb_bg = RGB_HEX(0x3A2A1A),
.mb_fg = RGB_HEX(0xDADADA),
.mb_detail = RGB_HEX(0x707070),
},
////////////////////////////////
// 4coder
// Default theme from Allen Webster's 4coder editor.
{
.name = "4coder",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0x90B080),
/* COMMENT */ RGB_HEX(0x2090F0),
/* MULTILINE_COMMENT */ RGB_HEX(0x2090F0),
/* STRING_LITERAL */ RGB_HEX(0x50FF30),
/* CHAR_LITERAL */ RGB_HEX(0x50FF30),
/* NUMBER */ RGB_HEX(0x50FF30),
/* IDENTIFIER */ RGB_HEX(0x90B080),
/* FUNCTION */ RGB_HEX(0x90B080),
/* KEYWORD */ RGB_HEX(0xD08F20),
/* TYPE */ RGB_HEX(0xD08F20),
/* VALUE */ RGB_HEX(0x50FF30),
/* MODIFIER */ RGB_HEX(0xD08F20),
/* DIRECTIVE */ RGB_HEX(0xFF5F5F),
/* PUNCTUATION */ RGB_HEX(0x90B080),
/* OPERATION */ RGB_HEX(0x90B080),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x0C0C0C),
.set_background = 1,
.status_bg = RGB_HEX(0x1A1A1A),
.status_fg = RGB_HEX(0x90B080),
.status_fg_dim = RGB_HEX(0x506050),
.mb_bg = RGB_HEX(0x2A3A20),
.mb_fg = RGB_HEX(0x90B080),
.mb_detail = RGB_HEX(0x606060),
},
////////////////////////////////
// Ryan Fleury
// Ryan Fleury's personal theme, ported from the fleury-theme.el emacs package.
{
.name = "Ryan Fleury",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xB99468),
/* COMMENT */ RGB_HEX(0x666666),
/* MULTILINE_COMMENT */ RGB_HEX(0x666666),
/* STRING_LITERAL */ RGB_HEX(0xFFAA00),
/* CHAR_LITERAL */ RGB_HEX(0xFFAA00),
/* NUMBER */ RGB_HEX(0xFFAA00),
/* IDENTIFIER */ RGB_HEX(0xB99468),
/* FUNCTION */ RGB_HEX(0xDE451F),
/* KEYWORD */ RGB_HEX(0xF0C674),
/* TYPE */ RGB_HEX(0xEDB211),
/* VALUE */ RGB_HEX(0xFFAA00),
/* MODIFIER */ RGB_HEX(0xF0C674),
/* DIRECTIVE */ RGB_HEX(0xDC7575),
/* PUNCTUATION */ RGB_HEX(0xB99468),
/* OPERATION */ RGB_HEX(0xB99468),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x020202),
.set_background = 1,
.status_bg = RGB_HEX(0x1A120B),
.status_fg = RGB_HEX(0xE7AA4D),
.status_fg_dim = RGB_HEX(0x63523D),
.mb_bg = RGB_HEX(0x3A2510),
.mb_fg = RGB_HEX(0xE7AA4D),
.mb_detail = RGB_HEX(0x63523D),
},
////////////////////////////////
// Gruber Darker
// Gruber Darker theme by Alexey Kutepov (rexim).
{
.name = "Gruber Darker",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xE4E4EF),
/* COMMENT */ RGB_HEX(0x565F73),
/* MULTILINE_COMMENT */ RGB_HEX(0x565F73),
/* STRING_LITERAL */ RGB_HEX(0x73C936),
/* CHAR_LITERAL */ RGB_HEX(0x73C936),
/* NUMBER */ RGB_HEX(0xFFDD33),
/* IDENTIFIER */ RGB_HEX(0xE4E4EF),
/* FUNCTION */ RGB_HEX(0x96A6C8),
/* KEYWORD */ RGB_HEX(0xFFDD33),
/* TYPE */ RGB_HEX(0x96A6C8),
/* VALUE */ RGB_HEX(0xFFDD33),
/* MODIFIER */ RGB_HEX(0x9E95C7),
/* DIRECTIVE */ RGB_HEX(0x9E95C7),
/* PUNCTUATION */ RGB_HEX(0xE4E4EF),
/* OPERATION */ RGB_HEX(0xE4E4EF),
/* INVALID */ RGB_HEX(0xF43841),
},
.ansi_colors = {},
.background = RGB_HEX(0x181818),
.set_background = 1,
.status_bg = RGB_HEX(0x101010),
.status_fg = RGB_HEX(0xE4E4EF),
.status_fg_dim = RGB_HEX(0x565F73),
.mb_bg = RGB_HEX(0x282828),
.mb_fg = RGB_HEX(0xE4E4EF),
.mb_detail = RGB_HEX(0x95A99F),
},
////////////////////////////////
// VS Dark
// Visual Studio Dark theme.
{
.name = "VS Dark",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xDCDCAA),
/* COMMENT */ RGB_HEX(0x6A9955),
/* MULTILINE_COMMENT */ RGB_HEX(0x6A9955),
/* STRING_LITERAL */ RGB_HEX(0xCE9178),
/* CHAR_LITERAL */ RGB_HEX(0xCE9178),
/* NUMBER */ RGB_HEX(0xB5CEA8),
/* IDENTIFIER */ RGB_HEX(0x9CDCFE),
/* FUNCTION */ RGB_HEX(0xDCDCAA),
/* KEYWORD */ RGB_HEX(0x569CD6),
/* TYPE */ RGB_HEX(0x4EC9B0),
/* VALUE */ RGB_HEX(0x569CD6),
/* MODIFIER */ RGB_HEX(0x569CD6),
/* DIRECTIVE */ RGB_HEX(0xC586C0),
/* PUNCTUATION */ RGB_HEX(0xD4D4D4),
/* OPERATION */ RGB_HEX(0xD4D4D4),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x1E1E1E),
.set_background = 1,
.status_bg = RGB_HEX(0x252526),
.status_fg = RGB_HEX(0xCCCCCC),
.status_fg_dim = RGB_HEX(0x808080),
.mb_bg = RGB_HEX(0x094771),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0x808080),
},
////////////////////////////////
// Freshcut Contrast
// High-contrast dark theme by Dayle Rees.
{
.name = "Freshcut Contrast",
.use_truecolor = 1,
.colors = {
/* DEFAULT */ RGB_HEX(0xF8F8F2),
/* COMMENT */ RGB_HEX(0x737B84),
/* MULTILINE_COMMENT */ RGB_HEX(0x737B84),
/* STRING_LITERAL */ RGB_HEX(0xE9EE00),
/* CHAR_LITERAL */ RGB_HEX(0xE9EE00),
/* NUMBER */ RGB_HEX(0x8FBE00),
/* IDENTIFIER */ RGB_HEX(0xF8F8F2),
/* FUNCTION */ RGB_HEX(0xAEE239),
/* KEYWORD */ RGB_HEX(0xC8D7E8),
/* TYPE */ RGB_HEX(0x4ECDC4),
/* VALUE */ RGB_HEX(0x8FBE00),
/* MODIFIER */ RGB_HEX(0x00A8C6),
/* DIRECTIVE */ RGB_HEX(0x00A8C6),
/* PUNCTUATION */ RGB_HEX(0xF8F8F2),
/* OPERATION */ RGB_HEX(0xF8F8F2),
/* INVALID */ RGB_HEX(0xFF0000),
},
.ansi_colors = {},
.background = RGB_HEX(0x000000),
.set_background = 1,
.status_bg = RGB_HEX(0x3C3C3C),
.status_fg = RGB_HEX(0xE0E0E0),
.status_fg_dim = RGB_HEX(0x808080),
.mb_bg = RGB_HEX(0x505050),
.mb_fg = RGB_HEX(0xFFFFFF),
.mb_detail = RGB_HEX(0x909090),
},
};

50
c/lexer/lexer_theme.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
// lexer_theme.h -- Color themes for syntax highlighting
//
// Each theme maps Token_Type values to 24-bit RGB colors, plus a background.
// Themes use true-color ANSI escapes (supported by Windows Terminal, most
// modern terminals).
#include "base/base_core.h"
#include "lexer/lexer.h"
////////////////////////////////
// RGB color
typedef struct RGB { U8 r, g, b; } RGB;
// Use for static initializers (no compound literal — MSVC C11 compat)
#define RGB_HEX(hex) { ((hex) >> 16) & 0xFF, ((hex) >> 8) & 0xFF, (hex) & 0xFF }
////////////////////////////////
// Theme
typedef struct Theme {
const char *name;
B32 use_truecolor; // if false, use ansi_colors[] with term_fg() instead
RGB colors[TOK_COUNT]; // foreground color per token type (true color)
U8 ansi_colors[TOK_COUNT]; // foreground per token type (256-color fallback)
RGB background; // terminal background color
B32 set_background; // whether to override terminal background
// UI chrome colors
RGB status_bg; // status bar background
RGB status_fg; // status bar foreground (focused)
RGB status_fg_dim; // status bar foreground (unfocused)
RGB mb_bg; // minibuffer item background (highlighted)
RGB mb_fg; // minibuffer item foreground (highlighted)
RGB mb_detail; // minibuffer detail text color
RGB fb_bg; // file browser background (0,0,0 = use WindowBg)
B32 set_fb_bg; // whether to override file browser background
} Theme;
////////////////////////////////
// Built-in themes
#define THEME_COUNT 13
extern Theme g_themes[THEME_COUNT];
extern S32 g_active_theme_idx;
// Convenience: pointer to the active theme
static inline Theme *theme_active(void) { return &g_themes[g_active_theme_idx]; }

130
c/platform/platform.h Normal file
View File

@@ -0,0 +1,130 @@
#pragma once
// platform.h — GUI windowing, input, and system services
//
// Provides window creation, keyboard/mouse input, clipboard access,
// and process spawning. Implementation: platform_win32.c.
#include "base/base_core.h"
#include "base/base_math.h"
////////////////////////////////
// Key codes (matching Windows VK codes)
enum {
PKEY_BACKSPACE = 0x08,
PKEY_TAB = 0x09,
PKEY_RETURN = 0x0D,
PKEY_ESCAPE = 0x1B,
PKEY_PAGEUP = 0x21,
PKEY_PAGEDOWN = 0x22,
PKEY_END = 0x23,
PKEY_HOME = 0x24,
PKEY_LEFT = 0x25,
PKEY_UP = 0x26,
PKEY_RIGHT = 0x27,
PKEY_DOWN = 0x28,
PKEY_DELETE = 0x2E,
PKEY_0 = 0x30,
PKEY_1 = 0x31,
PKEY_2 = 0x32,
PKEY_3 = 0x33,
PKEY_A = 0x41,
PKEY_B = 0x42,
PKEY_C = 0x43,
PKEY_E = 0x45,
PKEY_F = 0x46,
PKEY_G = 0x47,
PKEY_H = 0x48,
PKEY_J = 0x4A,
PKEY_K = 0x4B,
PKEY_L = 0x4C,
PKEY_N = 0x4E,
PKEY_O = 0x4F,
PKEY_P = 0x50,
PKEY_Q = 0x51,
PKEY_S = 0x53,
PKEY_V = 0x56,
PKEY_W = 0x57,
PKEY_X = 0x58,
PKEY_Y = 0x59,
PKEY_Z = 0x5A,
PKEY_F4 = 0x73,
PKEY_F8 = 0x77,
PKEY_EQUAL = 0xBB,
PKEY_MINUS = 0xBD,
PKEY_BACKSLASH = 0xDC,
};
////////////////////////////////
// Input accumulated per frame
#define PLATFORM_MAX_CHARS_PER_FRAME 64
#define PLATFORM_MAX_KEYS_PER_FRAME 32
// Editor-facing input (ASCII chars + key codes + modifiers)
typedef struct PlatformInput {
char chars[PLATFORM_MAX_CHARS_PER_FRAME];
S32 char_count;
U8 keys[PLATFORM_MAX_KEYS_PER_FRAME];
S32 key_count;
B32 ctrl_held;
B32 shift_held;
B32 alt_held;
} PlatformInput;
// Raw windowing input (UTF-16 chars + VK codes + mouse)
typedef struct PlatformRawInput {
U16 chars[PLATFORM_MAX_CHARS_PER_FRAME];
S32 char_count;
U8 keys[PLATFORM_MAX_KEYS_PER_FRAME];
S32 key_count;
B32 ctrl_held;
B32 shift_held;
Vec2F32 mouse_pos;
Vec2F32 scroll_delta;
B32 mouse_down;
B32 was_mouse_down;
} PlatformRawInput;
////////////////////////////////
// Window
typedef struct PlatformWindow PlatformWindow;
typedef struct PlatformWindowDesc {
const char *title;
S32 width;
S32 height;
} PlatformWindowDesc;
typedef void (*PlatformFrameCallback)(void *user_data);
////////////////////////////////
// Window API
PlatformWindow *platform_create_window(PlatformWindowDesc *desc);
void platform_destroy_window(PlatformWindow *window);
B32 platform_poll_events(PlatformWindow *window);
void platform_get_size(PlatformWindow *window, S32 *w, S32 *h);
void *platform_get_native_handle(PlatformWindow *window);
PlatformRawInput platform_get_input(PlatformWindow *window);
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data);
F32 platform_get_dpi_scale(PlatformWindow *window);
////////////////////////////////
// Input adapter — convert raw windowing input to editor input
PlatformInput platform_adapt_input(PlatformRawInput *raw);
////////////////////////////////
// Clipboard
void platform_clipboard_set(const char *text);
const char *platform_clipboard_get(void);
////////////////////////////////
// Process spawning
void platform_spawn_terminal(const char *command, const char *working_dir);

297
c/platform/platform_win32.c Normal file
View File

@@ -0,0 +1,297 @@
// platform_win32.c — Win32 windowing, input, clipboard, and process spawning
#include "platform/platform.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
////////////////////////////////
// Window internals
struct PlatformWindow {
HWND hwnd;
B32 should_close;
S32 width;
S32 height;
PlatformFrameCallback frame_callback;
void *frame_callback_user_data;
PlatformRawInput input;
B32 prev_mouse_down;
};
static PlatformWindow *g_main_window = NULL;
static B32 g_wndclass_registered = false;
////////////////////////////////
// Window procedure
// Forward declare ImGui Win32 handler (defined in imgui_impl_win32.cpp)
extern LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK platform_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
// Let ImGui process input first
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam))
return true;
PlatformWindow *pw = (PlatformWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
switch (msg) {
case WM_SIZE:
if (pw && wparam != SIZE_MINIMIZED) {
pw->width = (S32)LOWORD(lparam);
pw->height = (S32)HIWORD(lparam);
if (pw->frame_callback)
pw->frame_callback(pw->frame_callback_user_data);
}
return 0;
case WM_CHAR:
if (pw && wparam >= 32 && wparam < 0xFFFF) {
PlatformRawInput *ev = &pw->input;
if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME)
ev->chars[ev->char_count++] = (U16)wparam;
}
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (pw) {
PlatformRawInput *ev = &pw->input;
if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
ev->keys[ev->key_count++] = (U8)wparam;
ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
ev->shift_held = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
}
break;
case WM_MOUSEWHEEL:
if (pw) {
S16 wheel_delta = (S16)HIWORD(wparam);
pw->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f;
}
return 0;
case WM_SETCURSOR:
if (LOWORD(lparam) == HTCLIENT) {
SetCursor(LoadCursor(NULL, IDC_IBEAM));
return TRUE;
}
break;
case WM_DPICHANGED:
if (pw) {
RECT *suggested = (RECT *)lparam;
SetWindowPos(hwnd, NULL, suggested->left, suggested->top,
suggested->right - suggested->left, suggested->bottom - suggested->top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
case WM_CLOSE:
if (pw) pw->should_close = true;
return 0;
case WM_DESTROY:
if (pw == g_main_window)
PostQuitMessage(0);
return 0;
case WM_SYSCOMMAND:
if ((wparam & 0xfff0) == SC_KEYMENU)
return 0;
break;
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
////////////////////////////////
// Window lifecycle
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
if (!g_wndclass_registered) {
WNDCLASSEXW wc = {0};
wc.cbSize = sizeof(wc);
wc.style = CS_CLASSDC;
wc.lpfnWndProc = platform_wndproc;
wc.hInstance = GetModuleHandleW(NULL);
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(101));
wc.hIconSm = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(101));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = L"codemax_wc";
RegisterClassExW(&wc);
g_wndclass_registered = true;
}
UINT dpi = GetDpiForSystem();
int screen_w = GetSystemMetrics(SM_CXSCREEN);
int screen_h = GetSystemMetrics(SM_CYSCREEN);
int x = (screen_w - desc->width) / 2;
int y = (screen_h - desc->height) / 2;
DWORD style = WS_OVERLAPPEDWINDOW;
RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height };
AdjustWindowRectExForDpi(&rect, style, FALSE, 0, dpi);
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, NULL, 0);
wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, wtitle, wchar_count);
HWND hwnd = CreateWindowExW(
0, L"codemax_wc", wtitle,
style,
x, y,
rect.right - rect.left,
rect.bottom - rect.top,
NULL, NULL, GetModuleHandleW(NULL), NULL
);
_freea(wtitle);
if (!hwnd) return NULL;
PlatformWindow *window = (PlatformWindow *)calloc(1, sizeof(PlatformWindow));
window->hwnd = hwnd;
window->should_close = false;
window->width = desc->width;
window->height = desc->height;
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)window);
g_main_window = window;
ShowWindow(hwnd, SW_SHOWDEFAULT);
UpdateWindow(hwnd);
return window;
}
void platform_destroy_window(PlatformWindow *window) {
if (!window) return;
if (window->hwnd) DestroyWindow(window->hwnd);
if (g_main_window == window) g_main_window = NULL;
free(window);
}
B32 platform_poll_events(PlatformWindow *window) {
MSG msg;
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
if (msg.message == WM_QUIT)
window->should_close = true;
}
return !window->should_close;
}
void platform_get_size(PlatformWindow *window, S32 *w, S32 *h) {
if (w) *w = window->width;
if (h) *h = window->height;
}
void *platform_get_native_handle(PlatformWindow *window) {
return (void *)window->hwnd;
}
PlatformRawInput platform_get_input(PlatformWindow *window) {
PlatformRawInput result = window->input;
POINT cursor;
GetCursorPos(&cursor);
ScreenToClient(window->hwnd, &cursor);
result.mouse_pos = v2f32((F32)cursor.x, (F32)cursor.y);
result.was_mouse_down = window->prev_mouse_down;
result.mouse_down = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
window->prev_mouse_down = result.mouse_down;
memset(&window->input, 0, sizeof(window->input));
return result;
}
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data) {
window->frame_callback = cb;
window->frame_callback_user_data = user_data;
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
if (!window || !window->hwnd) return 1.0f;
return (F32)GetDpiForWindow(window->hwnd) / 96.0f;
}
////////////////////////////////
// Input adapter
PlatformInput platform_adapt_input(PlatformRawInput *raw) {
PlatformInput pi = {0};
pi.ctrl_held = raw->ctrl_held;
pi.shift_held = raw->shift_held;
pi.alt_held = (GetKeyState(VK_MENU) & 0x8000) != 0;
// Convert UTF-16 chars to ASCII (printable range only)
for (S32 i = 0; i < raw->char_count && pi.char_count < PLATFORM_MAX_CHARS_PER_FRAME; i++) {
U16 ch = raw->chars[i];
if (ch >= 32 && ch < 127)
pi.chars[pi.char_count++] = (char)ch;
}
// Key codes are identical (both use Windows VK codes)
for (S32 i = 0; i < raw->key_count && pi.key_count < PLATFORM_MAX_KEYS_PER_FRAME; i++)
pi.keys[pi.key_count++] = raw->keys[i];
return pi;
}
////////////////////////////////
// Clipboard
void platform_clipboard_set(const char *text) {
if (!text) return;
int len = (int)strlen(text);
if (len == 0) return;
int wlen = MultiByteToWideChar(CP_UTF8, 0, text, len, NULL, 0);
if (wlen == 0) return;
HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t));
if (!hmem) return;
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
MultiByteToWideChar(CP_UTF8, 0, text, len, wbuf, wlen);
wbuf[wlen] = L'\0';
GlobalUnlock(hmem);
HWND hwnd = g_main_window ? g_main_window->hwnd : NULL;
if (OpenClipboard(hwnd)) {
EmptyClipboard();
SetClipboardData(CF_UNICODETEXT, hmem);
CloseClipboard();
} else {
GlobalFree(hmem);
}
}
const char *platform_clipboard_get(void) {
static char buf[64 * 1024];
buf[0] = '\0';
HWND hwnd = g_main_window ? g_main_window->hwnd : NULL;
if (!OpenClipboard(hwnd)) return NULL;
HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT);
if (hmem) {
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
if (wbuf) {
int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, sizeof(buf) - 1, NULL, NULL);
buf[len > 0 ? len - 1 : 0] = '\0';
GlobalUnlock(hmem);
}
}
CloseClipboard();
return buf[0] ? buf : NULL;
}
////////////////////////////////
// Process spawning
void platform_spawn_terminal(const char *command, const char *working_dir) {
char cmd_line[2048];
snprintf(cmd_line, sizeof(cmd_line), "cmd.exe /k %s", command);
STARTUPINFOA si = {0};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {0};
CreateProcessA(NULL, cmd_line, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, working_dir, &si, &pi);
if (pi.hProcess) CloseHandle(pi.hProcess);
if (pi.hThread) CloseHandle(pi.hThread);
}