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);
}

View File

@@ -0,0 +1,10 @@
swap_buffer :: (screen: *Screen) {
using screen;
write_string(cast(string)buffer);
}
clear_screen :: (screen: *Screen) {
for 0..screen.height * screen.width {
print_color(" ", color = .BLACK);
}
}

View File

@@ -0,0 +1,107 @@
// "Squashes" two dimensional coordinates onto the one dimensional screen buffer
draw :: (using screen: *Screen, p: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height {
buffer[p.y * width + p.x] = char;
}
}
// Implementation of the Bresenham line algorithm
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
draw_line :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
p1_c := p1;
p2_c := p2;
dx := abs(p2_c.x - p1_c.x);
dy := -abs(p2_c.y - p1_c.y);
sx := ifx p1_c.x < p2_c.x then 1 else -1;
sy := ifx p1_c.y < p2_c.y then 1 else -1;
err := dx + dy;
e2 : s64;
while true {
draw(screen, Vec2s64.{p1_c.x, p1_c.y}, char);
if p1_c.x == p2_c.x && p1_c.y == p2_c.y then break;
e2 = 2 * err;
if e2 >= dy {
err += dy;
p1_c.x += sx;
}
if e2 <= dx {
err += dx;
p1_c.y += sy;
}
}
}
draw_triangle :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_line(screen, p1, p2, char);
draw_line(screen, p2, p3, char);
draw_line(screen, p3, p1, char);
}
draw_triangle :: (screen: *Screen, t: Triangle, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_triangle(screen, t.p1, t.p2, t.p3, char);
}
draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
// (x1, y1) => top left
// (x2, y2) => bottom right
draw_line(screen, .{p1.x, p1.y}, .{p1.x, p2.y}, char);
draw_line(screen, .{p1.x, p2.y}, .{p2.x, p2.y}, char);
draw_line(screen, .{p2.x, p2.y}, .{p2.x, p1.y}, char);
draw_line(screen, .{p2.x, p1.y}, .{p1.x, p1.y}, char);
}
draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, p4: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
// (x1, y1) => top left
// (x2, y2) => bottom left
// (x3, y3) => bottom right
// (x4, y4) => top right
draw_line(screen, .{p1.x, p1.y}, .{p2.x, p2.y}, char);
draw_line(screen, .{p2.x, p2.y}, .{p3.x, p3.y}, char);
draw_line(screen, .{p3.x, p3.y}, .{p4.x, p4.y}, char);
draw_line(screen, .{p4.x, p4.y}, .{p1.x, p1.y}, char);
}
draw_quad :: (screen: *Screen, q: Quad, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_quad(screen, q.p1, q.p2, q.p3, q.p4, char);
}
draw_text :: (using screen: *Screen, p: Vec2s64, s: string) {
if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height {
for cast([]u8)s {
buffer[(p.y * width + p.x) + it_index] = it;
}
}
}
// When attempting to draw outside of the visible screen, "clip" the pixels
// by setting their position to the maximum width / height available
clip :: (using screen: *Screen, p: *Vec2s64) {
if p.x < 0 then p.x = 0;
if p.x >= width then p.x = width;
if p.y < 0 then p.y = 0;
if p.y >= height then p.y = height;
}
fill :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
clip(screen, *p1);
clip(screen, *p2);
for x: p1.x..p2.x {
for y: p1.y..p2.y {
draw(screen, .{x, y}, char);
}
}
}
fill_entire_screen :: (screen: *Screen, char: u8 = DEFAULT_PIXEL_CHAR) {
fill(screen, .{0,0}, .{screen.width, screen.height}, char);
}

View File

@@ -0,0 +1,8 @@
// Render current screen state to a file
render_to_file :: (screen: *Screen, preserve_color: bool, filepath: string) {
assert(false, "Not implemented");
}
#scope_file
#import "File";

View File

@@ -0,0 +1,42 @@
get_console_size :: () -> s64, s64 {
//@Hack - This is terrible, but this is typically called only once, so maybe it doesn't matter...
width := string_to_int(get_stdout_from_cmd("tput cols"));
height := string_to_int(get_stdout_from_cmd("tput lines"));
return cast(s64)width, cast(s64)height;
}
#scope_file
get_stdout_from_cmd :: (cmd: string) -> string {
buffer: [256]u8;
stream : *FILE;
data : string;
// m_cmd := tprint("% %", cmd, " 2>&1");
stream = popen(to_c_string(cmd), to_c_string("r"));
if stream {
while !feof(stream) {
if fgets(buffer.data, buffer.count, stream) {
data = tprint("%1%2", data, cast(string)(buffer));
}
}
pclose(stream);
}
return data;
}
to_c_string :: (s: string) -> *u8 {
result := cast(*u8) alloc(s.count + 1);
memcpy(result, s.data, s.count);
result[s.count] = 0;
return result;
}
#import "POSIX";

View File

@@ -0,0 +1,19 @@
get_triangle_centroid :: (t: Triangle) -> Vec2s64 {
return get_triangle_centroid(t.p1, t.p2, t.p3);
}
get_triangle_centroid :: (p1: Vec2s64, p2: Vec2s64, p3: Vec2s64) -> Vec2s64 {
x := (p1.x + p2.x + p3.x) / 3;
y := (p1.y + p2.y + p3.y) / 3;
return .{x, y};
}
make_quad_from_rect :: (p1: Vec2s64, p2: Vec2s64) -> Quad {
return .{
.{ p1.x, p1.y },
.{ p2.x, p1.y },
.{ p2.x, p2.y },
.{ p1.x, p2.y },
};
}

View File

@@ -0,0 +1,122 @@
/******************************************************
* Copyright 2024 Max Amundsen - All Rights Reserved
******************************************************
Console Screen Rendering:
Terminal emulators, or consoles, are programs that display text from a sequence of characters.
These characters are usually fixed-width, meaning each character takes up the same amount of space on screen.
The dimensions of your console window determines how the console will display the text in rows and columns.
This module is designed to draw simple 2d, or 3d shapes, inside a standard terminal emulator program.
--------------------------------------------------------------
Suppose the following sequence of characters is sent to the console:
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U']
For a console displaying 7 characters horizontally, and 3 characters vertically, the result would be the following:
0123456
_________
0 |ABCDEFG|
1 |HIJKLMN|
2 |OPQRSTU|
---------
As displayed in the above figure, the 0th element, 'A', in the sequence represents (0,0) in 2D space, while the 9th element, 'J' represents (2,1).
=> Calculating the two dimensional area of the console output yields the number of elements in the initial 1D sequence:
(7 * 3) = 21
This may appear obvious, however this is an important connection to make, when considering how the console represents its output.
The terminal automatically transforms our 1D sequence, to a 2D sequence displayed on your monitor, based on the size of the window.
Since the console is responsible for transforming our 1D array into a 2D output, we must provide the console with a 1D array.
In order to draw 2D and 3D elements on the screen, we must construct procedures in the code to represent the transformations from
higher dimensions, down to the first dimension.
This simple 2d -> 1d transformation is implemented as the `draw` procedure found in the `draw.jai` file in this module.
*/
BLANK_PIXEL : u8 : 0x20;
DEFAULT_PIXEL_CHAR : u8 : #char "#";
Vec2s64 :: struct {
x: s64;
y: s64;
}
Triangle :: struct {
p1: Vec2s64;
p3: Vec2s64;
p2: Vec2s64;
}
Quad :: struct {
p1: Vec2s64;
p2: Vec2s64;
p3: Vec2s64;
p4: Vec2s64;
}
Screen :: struct {
width : s64;
height: s64;
buffer: [..] u8;
}
init_screen :: (screen: *Screen, width: s64, height: s64) {
buffer : [..] u8;
array_resize(*buffer, width * height);
screen.width = width;
screen.height = height;
screen.buffer = buffer;
}
resize_screen :: (screen: *Screen, width: s64, height: s64) {
screen.width = width;
screen.height = height;
array_resize(*screen.buffer, width * height);
}
maybe_resize_screen :: (screen: *Screen) {
w, h := get_console_size();
should_update := false;
if w != screen.width {
should_update = true;
}
if h != screen.height {
should_update = true;
}
if should_update {
resize_screen(screen, w, h);
}
}
#load "draw.jai";
#load "transform.jai";
#load "file_operations.jai";
#load "console.jai";
#load "math_extras.jai";
#if OS == .MACOS {
#load "macos/console.jai";
} else {
#assert false "Platform unsupported. Sorry.";
}
#scope_module
#import "Basic";
#import "Math";

View File

@@ -0,0 +1,67 @@
// for n points
translate :: (amount: Vec2s64, pn: ..*Vec2s64) {
for pn {
it.x += amount.x;
it.y += amount.y;
}
}
// for a triangle
translate :: (amount: Vec2s64, t: *Triangle) {
translate(amount, *t.p1, *t.p2, *t.p3);
}
// for a quad
translate :: (amount: Vec2s64, q: *Quad) {
translate(amount, *q.p1, *q.p2, *q.p3, *q.p4);
}
// single point rotation
rotate :: (angle: s64, center: Vec2s64, p: *Vec2s64) {
rad := cast(float64)angle * (PI / 180.0);
// translate to center
tx := p.x - center.x;
ty := p.y - center.y;
// apply rotation
rx := cast(s64)(tx * cos(rad) - ty * sin(rad));
ry := cast(s64)(tx * sin(rad) + ty * cos(rad));
// translate back
p.x = rx + center.x;
p.y = ry + center.y;
}
// rotate n points
rotate :: (angle: s64, center: Vec2s64, pn: ..*Vec2s64) {
for pn {
rotate(angle, center, it);
}
}
// rotate 3 points via triangle struct
rotate :: (angle: s64, center: Vec2s64, t: *Triangle) {
rotate(angle, center, *t.p1, *t.p2, *t.p3);
}
// rotate 4 points via quad struct
rotate :: (angle: s64, center: Vec2s64, q: *Quad) {
rotate(angle, center, *q.p1, *q.p2, *q.p3, *q.p4);
}
scale :: (factor: Vector2, pn: ..*Vec2s64) {
for pn {
it.x = cast(s64)(it.x * factor.x);
it.y = cast(s64)(it.y * factor.y);
}
}
scale :: (factor: Vector2, t: *Triangle) {
scale(factor, *t.p1, *t.p2, *t.p3);
}
scale :: (factor: Vector2, q: *Quad) {
scale(factor, *q.p1, *q.p2, *q.p3, *q.p4);
}