commit ce18d357fac664f477697c71a1db6b2e8ceab71e
parent cbbaa8fda857e5bfc3642f732bb56a365bf45bbb
Author: luajitos <bbhbb2094@gmail.com>
Date: Sun, 7 Dec 2025 08:52:39 +0000
Added Spreadsheet App
Diffstat:
26 files changed, 2859 insertions(+), 330 deletions(-)
diff --git a/crypto/Hash_Lua.c b/crypto/Hash_Lua.c
@@ -69,7 +69,7 @@ static int is_base64(const char *str, size_t len) {
* Hash Functions
* ========================================================================= */
-/* hash(string) - SHA-256 hash returning base64 string */
+/* hash(string) - SHA-256 hash returning raw binary */
int l_hash(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -77,18 +77,11 @@ int l_hash(lua_State *L) {
uint8_t digest[32];
sha256((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 32, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 32);
return 1;
}
-/* hash.SHA512(string) - SHA-512 hash returning base64 string */
+/* hash.SHA512(string) - SHA-512 hash returning raw binary */
int l_hash_sha512(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -96,18 +89,11 @@ int l_hash_sha512(lua_State *L) {
uint8_t digest[64];
sha512((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 64, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 64);
return 1;
}
-/* hash.SHA1(string) - SHA-1 hash (legacy) returning base64 string */
+/* hash.SHA1(string) - SHA-1 hash (legacy) returning raw binary */
int l_hash_sha1(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -115,18 +101,11 @@ int l_hash_sha1(lua_State *L) {
uint8_t digest[20];
sha1((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 20, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 20);
return 1;
}
-/* hash.CRC32(string) - CRC32 checksum returning base64 string */
+/* hash.CRC32(string) - CRC32 checksum returning raw binary (4 bytes, big-endian) */
int l_hash_crc32(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -140,71 +119,24 @@ int l_hash_crc32(lua_State *L) {
digest[2] = (checksum >> 8) & 0xFF;
digest[3] = checksum & 0xFF;
- size_t b64_len;
- char *b64 = base64_encode(digest, 4, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 4);
return 1;
}
-/* hash.MD5(base64string) or hash.MD5(binstring, binstrlength) - MD5 hash */
+/* hash.MD5(string) - MD5 hash returning raw binary */
int l_hash_md5(lua_State *L) {
- const uint8_t *input;
size_t input_len;
- uint8_t *decoded = NULL;
-
- /* Check if we have two arguments (binary string + length) */
- if (lua_gettop(L) == 2) {
- /* Binary string with explicit length */
- input = (const uint8_t*)lua_tolstring(L, 1, &input_len);
- size_t explicit_len = luaL_checkinteger(L, 2);
-
- if (explicit_len > input_len) {
- return luaL_error(L, "Specified length exceeds string length");
- }
- input_len = explicit_len;
- } else {
- /* Single argument - check if base64 or binary */
- const char *str = luaL_checklstring(L, 1, &input_len);
-
- if (is_base64(str, input_len)) {
- /* Base64 input - decode it */
- decoded = base64_decode_simple(str, input_len, &input_len);
- if (!decoded) {
- return luaL_error(L, "Base64 decoding failed");
- }
- input = decoded;
- } else {
- /* Binary input */
- input = (const uint8_t*)str;
- }
- }
+ const char *input = luaL_checklstring(L, 1, &input_len);
/* Compute MD5 hash */
uint8_t digest[16];
- md5(input, input_len, digest);
+ md5((const uint8_t*)input, input_len, digest);
- if (decoded) {
- free(decoded);
- }
-
- /* Return as base64 */
- size_t b64_len;
- char *b64 = base64_encode(digest, 16, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 16);
return 1;
}
-/* hash.SHA3(string) or hash.SHA3_256(string) - SHA3-256 hash */
+/* hash.SHA3(string) or hash.SHA3_256(string) - SHA3-256 hash returning raw binary */
int l_hash_sha3(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -212,18 +144,11 @@ int l_hash_sha3(lua_State *L) {
uint8_t digest[32];
sha3_256((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 32, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 32);
return 1;
}
-/* hash.SHA3_512(string) - SHA3-512 hash */
+/* hash.SHA3_512(string) - SHA3-512 hash returning raw binary */
int l_hash_sha3_512(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -231,14 +156,7 @@ int l_hash_sha3_512(lua_State *L) {
uint8_t digest[64];
sha3_512((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 64, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 64);
return 1;
}
@@ -252,18 +170,11 @@ int l_hash_call(lua_State *L) {
uint8_t digest[32];
sha3_256((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 32, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 32);
return 1;
}
-/* hash.BLAKE2b(string) or hash.BLAKE2b_256(string) - BLAKE2b-256 hash */
+/* hash.BLAKE2b(string) or hash.BLAKE2b_256(string) - BLAKE2b-256 hash returning raw binary */
int l_hash_blake2b(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -271,18 +182,11 @@ int l_hash_blake2b(lua_State *L) {
uint8_t digest[32];
blake2b_256((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 32, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 32);
return 1;
}
-/* hash.BLAKE2b_512(string) - BLAKE2b-512 hash */
+/* hash.BLAKE2b_512(string) - BLAKE2b-512 hash returning raw binary */
int l_hash_blake2b_512(lua_State *L) {
size_t input_len;
const char *input = luaL_checklstring(L, 1, &input_len);
@@ -290,13 +194,6 @@ int l_hash_blake2b_512(lua_State *L) {
uint8_t digest[64];
blake2b_512((const uint8_t*)input, input_len, digest);
- size_t b64_len;
- char *b64 = base64_encode(digest, 64, &b64_len);
- if (!b64) {
- return luaL_error(L, "Base64 encoding failed");
- }
-
- lua_pushlstring(L, b64, b64_len);
- free(b64);
+ lua_pushlstring(L, (const char*)digest, 64);
return 1;
}
diff --git a/crypto/crypto.c b/crypto/crypto.c
@@ -1209,6 +1209,9 @@ int luaopen_crypto(lua_State *L) {
lua_setfield(L, -2, "Salsa20");
/* Register hash functions directly on crypto table */
+ lua_pushcfunction(L, l_hash);
+ lua_setfield(L, -2, "SHA256");
+
lua_pushcfunction(L, l_hash_sha512);
lua_setfield(L, -2, "SHA512");
diff --git a/crypto/hashing/SHA3.c b/crypto/hashing/SHA3.c
@@ -11,7 +11,7 @@
#include <stdlib.h>
/* Keccak round constants */
-static const uint64_t keccak_round_constants[24] = {
+static const uint64_t KeccakF_RoundConstants[24] = {
0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL,
0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL,
0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL,
@@ -22,14 +22,173 @@ static const uint64_t keccak_round_constants[24] = {
0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL
};
-/* Rotation offsets */
-static const int keccak_rotation_constants[24] = {
- 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14,
- 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44
-};
+#define ROL64(a, n) (((a) << (n)) | ((a) >> (64 - (n))))
+
+/* Keccak-f[1600] permutation - unrolled reference implementation */
+static void KeccakF1600(uint64_t *state) {
+ uint64_t Aba, Abe, Abi, Abo, Abu;
+ uint64_t Aga, Age, Agi, Ago, Agu;
+ uint64_t Aka, Ake, Aki, Ako, Aku;
+ uint64_t Ama, Ame, Ami, Amo, Amu;
+ uint64_t Asa, Ase, Asi, Aso, Asu;
+ uint64_t BCa, BCe, BCi, BCo, BCu;
+ uint64_t Da, De, Di, Do, Du;
+ uint64_t Eba, Ebe, Ebi, Ebo, Ebu;
+ uint64_t Ega, Ege, Egi, Ego, Egu;
+ uint64_t Eka, Eke, Eki, Eko, Eku;
+ uint64_t Ema, Eme, Emi, Emo, Emu;
+ uint64_t Esa, Ese, Esi, Eso, Esu;
+
+ Aba = state[ 0]; Abe = state[ 1]; Abi = state[ 2]; Abo = state[ 3]; Abu = state[ 4];
+ Aga = state[ 5]; Age = state[ 6]; Agi = state[ 7]; Ago = state[ 8]; Agu = state[ 9];
+ Aka = state[10]; Ake = state[11]; Aki = state[12]; Ako = state[13]; Aku = state[14];
+ Ama = state[15]; Ame = state[16]; Ami = state[17]; Amo = state[18]; Amu = state[19];
+ Asa = state[20]; Ase = state[21]; Asi = state[22]; Aso = state[23]; Asu = state[24];
+
+ for (int round = 0; round < 24; round += 2) {
+ /* Round 1 */
+ BCa = Aba^Aga^Aka^Ama^Asa;
+ BCe = Abe^Age^Ake^Ame^Ase;
+ BCi = Abi^Agi^Aki^Ami^Asi;
+ BCo = Abo^Ago^Ako^Amo^Aso;
+ BCu = Abu^Agu^Aku^Amu^Asu;
+
+ Da = BCu^ROL64(BCe, 1);
+ De = BCa^ROL64(BCi, 1);
+ Di = BCe^ROL64(BCo, 1);
+ Do = BCi^ROL64(BCu, 1);
+ Du = BCo^ROL64(BCa, 1);
+
+ Aba ^= Da; BCa = Aba;
+ Age ^= De; BCe = ROL64(Age, 44);
+ Aki ^= Di; BCi = ROL64(Aki, 43);
+ Amo ^= Do; BCo = ROL64(Amo, 21);
+ Asu ^= Du; BCu = ROL64(Asu, 14);
+ Eba = BCa^((~BCe)&BCi); Eba ^= KeccakF_RoundConstants[round];
+ Ebe = BCe^((~BCi)&BCo);
+ Ebi = BCi^((~BCo)&BCu);
+ Ebo = BCo^((~BCu)&BCa);
+ Ebu = BCu^((~BCa)&BCe);
+
+ Abo ^= Do; BCa = ROL64(Abo, 28);
+ Agu ^= Du; BCe = ROL64(Agu, 20);
+ Aka ^= Da; BCi = ROL64(Aka, 3);
+ Ame ^= De; BCo = ROL64(Ame, 45);
+ Asi ^= Di; BCu = ROL64(Asi, 61);
+ Ega = BCa^((~BCe)&BCi);
+ Ege = BCe^((~BCi)&BCo);
+ Egi = BCi^((~BCo)&BCu);
+ Ego = BCo^((~BCu)&BCa);
+ Egu = BCu^((~BCa)&BCe);
+
+ Abe ^= De; BCa = ROL64(Abe, 1);
+ Agi ^= Di; BCe = ROL64(Agi, 6);
+ Ako ^= Do; BCi = ROL64(Ako, 25);
+ Amu ^= Du; BCo = ROL64(Amu, 8);
+ Asa ^= Da; BCu = ROL64(Asa, 18);
+ Eka = BCa^((~BCe)&BCi);
+ Eke = BCe^((~BCi)&BCo);
+ Eki = BCi^((~BCo)&BCu);
+ Eko = BCo^((~BCu)&BCa);
+ Eku = BCu^((~BCa)&BCe);
+
+ Abu ^= Du; BCa = ROL64(Abu, 27);
+ Aga ^= Da; BCe = ROL64(Aga, 36);
+ Ake ^= De; BCi = ROL64(Ake, 10);
+ Ami ^= Di; BCo = ROL64(Ami, 15);
+ Aso ^= Do; BCu = ROL64(Aso, 56);
+ Ema = BCa^((~BCe)&BCi);
+ Eme = BCe^((~BCi)&BCo);
+ Emi = BCi^((~BCo)&BCu);
+ Emo = BCo^((~BCu)&BCa);
+ Emu = BCu^((~BCa)&BCe);
+
+ Abi ^= Di; BCa = ROL64(Abi, 62);
+ Ago ^= Do; BCe = ROL64(Ago, 55);
+ Aku ^= Du; BCi = ROL64(Aku, 39);
+ Ama ^= Da; BCo = ROL64(Ama, 41);
+ Ase ^= De; BCu = ROL64(Ase, 2);
+ Esa = BCa^((~BCe)&BCi);
+ Ese = BCe^((~BCi)&BCo);
+ Esi = BCi^((~BCo)&BCu);
+ Eso = BCo^((~BCu)&BCa);
+ Esu = BCu^((~BCa)&BCe);
+
+ /* Round 2 */
+ BCa = Eba^Ega^Eka^Ema^Esa;
+ BCe = Ebe^Ege^Eke^Eme^Ese;
+ BCi = Ebi^Egi^Eki^Emi^Esi;
+ BCo = Ebo^Ego^Eko^Emo^Eso;
+ BCu = Ebu^Egu^Eku^Emu^Esu;
+
+ Da = BCu^ROL64(BCe, 1);
+ De = BCa^ROL64(BCi, 1);
+ Di = BCe^ROL64(BCo, 1);
+ Do = BCi^ROL64(BCu, 1);
+ Du = BCo^ROL64(BCa, 1);
-/* ROL64 - Rotate left */
-#define ROL64(x, n) (((x) << (n)) | ((x) >> (64 - (n))))
+ Eba ^= Da; BCa = Eba;
+ Ege ^= De; BCe = ROL64(Ege, 44);
+ Eki ^= Di; BCi = ROL64(Eki, 43);
+ Emo ^= Do; BCo = ROL64(Emo, 21);
+ Esu ^= Du; BCu = ROL64(Esu, 14);
+ Aba = BCa^((~BCe)&BCi); Aba ^= KeccakF_RoundConstants[round+1];
+ Abe = BCe^((~BCi)&BCo);
+ Abi = BCi^((~BCo)&BCu);
+ Abo = BCo^((~BCu)&BCa);
+ Abu = BCu^((~BCa)&BCe);
+
+ Ebo ^= Do; BCa = ROL64(Ebo, 28);
+ Egu ^= Du; BCe = ROL64(Egu, 20);
+ Eka ^= Da; BCi = ROL64(Eka, 3);
+ Eme ^= De; BCo = ROL64(Eme, 45);
+ Esi ^= Di; BCu = ROL64(Esi, 61);
+ Aga = BCa^((~BCe)&BCi);
+ Age = BCe^((~BCi)&BCo);
+ Agi = BCi^((~BCo)&BCu);
+ Ago = BCo^((~BCu)&BCa);
+ Agu = BCu^((~BCa)&BCe);
+
+ Ebe ^= De; BCa = ROL64(Ebe, 1);
+ Egi ^= Di; BCe = ROL64(Egi, 6);
+ Eko ^= Do; BCi = ROL64(Eko, 25);
+ Emu ^= Du; BCo = ROL64(Emu, 8);
+ Esa ^= Da; BCu = ROL64(Esa, 18);
+ Aka = BCa^((~BCe)&BCi);
+ Ake = BCe^((~BCi)&BCo);
+ Aki = BCi^((~BCo)&BCu);
+ Ako = BCo^((~BCu)&BCa);
+ Aku = BCu^((~BCa)&BCe);
+
+ Ebu ^= Du; BCa = ROL64(Ebu, 27);
+ Ega ^= Da; BCe = ROL64(Ega, 36);
+ Eke ^= De; BCi = ROL64(Eke, 10);
+ Emi ^= Di; BCo = ROL64(Emi, 15);
+ Eso ^= Do; BCu = ROL64(Eso, 56);
+ Ama = BCa^((~BCe)&BCi);
+ Ame = BCe^((~BCi)&BCo);
+ Ami = BCi^((~BCo)&BCu);
+ Amo = BCo^((~BCu)&BCa);
+ Amu = BCu^((~BCa)&BCe);
+
+ Ebi ^= Di; BCa = ROL64(Ebi, 62);
+ Ego ^= Do; BCe = ROL64(Ego, 55);
+ Eku ^= Du; BCi = ROL64(Eku, 39);
+ Ema ^= Da; BCo = ROL64(Ema, 41);
+ Ese ^= De; BCu = ROL64(Ese, 2);
+ Asa = BCa^((~BCe)&BCi);
+ Ase = BCe^((~BCi)&BCo);
+ Asi = BCi^((~BCo)&BCu);
+ Aso = BCo^((~BCu)&BCa);
+ Asu = BCu^((~BCa)&BCe);
+ }
+
+ state[ 0] = Aba; state[ 1] = Abe; state[ 2] = Abi; state[ 3] = Abo; state[ 4] = Abu;
+ state[ 5] = Aga; state[ 6] = Age; state[ 7] = Agi; state[ 8] = Ago; state[ 9] = Agu;
+ state[10] = Aka; state[11] = Ake; state[12] = Aki; state[13] = Ako; state[14] = Aku;
+ state[15] = Ama; state[16] = Ame; state[17] = Ami; state[18] = Amo; state[19] = Amu;
+ state[20] = Asa; state[21] = Ase; state[22] = Asi; state[23] = Aso; state[24] = Asu;
+}
/* Load 64-bit little-endian */
static inline uint64_t load64_le(const uint8_t *x) {
@@ -55,44 +214,6 @@ static inline void store64_le(uint8_t *x, uint64_t u) {
x[7] = (uint8_t)(u >> 56);
}
-/* Keccak-f[1600] permutation */
-static void keccak_f1600(uint64_t state[25]) {
- uint64_t C[5], D[5], B[25];
-
- for (int round = 0; round < 24; round++) {
- /* Theta */
- for (int i = 0; i < 5; i++) {
- C[i] = state[i] ^ state[i + 5] ^ state[i + 10] ^ state[i + 15] ^ state[i + 20];
- }
- for (int i = 0; i < 5; i++) {
- D[i] = C[(i + 4) % 5] ^ ROL64(C[(i + 1) % 5], 1);
- }
- for (int i = 0; i < 25; i++) {
- state[i] ^= D[i % 5];
- }
-
- /* Rho and Pi */
- B[0] = state[0];
- int x = 1, y = 0;
- for (int i = 0; i < 24; i++) {
- B[y + 5 * ((2 * x + 3 * y) % 5)] = ROL64(state[y + 5 * x], keccak_rotation_constants[i]);
- int temp = y;
- y = (2 * x + 3 * y) % 5;
- x = temp;
- }
-
- /* Chi */
- for (int j = 0; j < 25; j += 5) {
- for (int i = 0; i < 5; i++) {
- state[j + i] = B[j + i] ^ ((~B[j + ((i + 1) % 5)]) & B[j + ((i + 2) % 5)]);
- }
- }
-
- /* Iota */
- state[0] ^= keccak_round_constants[round];
- }
-}
-
/* Keccak context */
typedef struct {
uint64_t state[25];
@@ -128,7 +249,7 @@ static void keccak_update(keccak_context *ctx, const uint8_t *data, size_t len)
for (size_t i = 0; i < ctx->rate / 8; i++) {
ctx->state[i] ^= load64_le(ctx->buffer + i * 8);
}
- keccak_f1600(ctx->state);
+ KeccakF1600(ctx->state);
ctx->buffer_len = 0;
}
}
@@ -145,7 +266,7 @@ static void keccak_final(keccak_context *ctx, uint8_t *digest, size_t digest_len
for (size_t i = 0; i < ctx->rate / 8; i++) {
ctx->state[i] ^= load64_le(ctx->buffer + i * 8);
}
- keccak_f1600(ctx->state);
+ KeccakF1600(ctx->state);
/* Squeeze */
size_t extracted = 0;
@@ -168,26 +289,26 @@ static void keccak_final(keccak_context *ctx, uint8_t *digest, size_t digest_len
extracted += take;
if (extracted < digest_len) {
- keccak_f1600(ctx->state);
+ KeccakF1600(ctx->state);
}
}
}
-/* SHA3-256 */
-void sha3_256(const uint8_t *data, size_t len, uint8_t digest[32]) {
+/* SHA3-224 */
+void sha3_224(const uint8_t *data, size_t len, uint8_t digest[28]) {
keccak_context ctx;
- keccak_init(&ctx, 136, 0x06); /* rate=1088 bits=136 bytes, SHA3 delim */
+ keccak_init(&ctx, 144, 0x06); /* rate=1152 bits=144 bytes */
keccak_update(&ctx, data, len);
- keccak_final(&ctx, digest, 32);
+ keccak_final(&ctx, digest, 28);
memset(&ctx, 0, sizeof(ctx));
}
-/* SHA3-224 */
-void sha3_224(const uint8_t *data, size_t len, uint8_t digest[28]) {
+/* SHA3-256 */
+void sha3_256(const uint8_t *data, size_t len, uint8_t digest[32]) {
keccak_context ctx;
- keccak_init(&ctx, 144, 0x06); /* rate=1152 bits=144 bytes */
+ keccak_init(&ctx, 136, 0x06); /* rate=1088 bits=136 bytes, SHA3 delim */
keccak_update(&ctx, data, len);
- keccak_final(&ctx, digest, 28);
+ keccak_final(&ctx, digest, 32);
memset(&ctx, 0, sizeof(ctx));
}
diff --git a/decoder.c b/decoder.c
@@ -637,3 +637,773 @@ int lua_image_get_buffer_bgra(lua_State* L) {
return 1;
}
+
+/* ============================================================================
+ * Lua ImageBuffer - Mutable BGRA pixel buffer as userdata
+ * This allows true in-place modification without string copies
+ * ============================================================================ */
+
+typedef struct {
+ int width;
+ int height;
+ uint8_t* data; /* BGRA format, 4 bytes per pixel */
+} ImageBuffer;
+
+#define IMAGEBUFFER_MT "ImageBuffer"
+
+/* Helper: Set a single pixel in a BGRA buffer (modifies in place) */
+static inline void buffer_set_pixel(uint8_t* buf, int width, int height, int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+ if (x < 0 || x >= width || y < 0 || y >= height) return;
+ size_t offset = ((size_t)y * width + x) * 4;
+ buf[offset] = b;
+ buf[offset + 1] = g;
+ buf[offset + 2] = r;
+ buf[offset + 3] = a;
+}
+
+/* Create a new ImageBuffer userdata
+ * ImageBufferNew(width, height) -> ImageBuffer userdata */
+int lua_imagebuffer_new(lua_State* L) {
+ int width = luaL_checkinteger(L, 1);
+ int height = luaL_checkinteger(L, 2);
+
+ if (width <= 0 || height <= 0 || width > 8192 || height > 8192) {
+ return luaL_error(L, "Invalid buffer dimensions");
+ }
+
+ /* Create userdata */
+ ImageBuffer* ib = (ImageBuffer*)lua_newuserdata(L, sizeof(ImageBuffer));
+ ib->width = width;
+ ib->height = height;
+ ib->data = (uint8_t*)malloc((size_t)width * height * 4);
+
+ if (!ib->data) {
+ return luaL_error(L, "Out of memory");
+ }
+
+ /* Initialize to white (opaque) */
+ size_t size = (size_t)width * height * 4;
+ for (size_t i = 0; i < size; i += 4) {
+ ib->data[i] = 255; /* B */
+ ib->data[i + 1] = 255; /* G */
+ ib->data[i + 2] = 255; /* R */
+ ib->data[i + 3] = 255; /* A */
+ }
+
+ /* Set metatable */
+ luaL_getmetatable(L, IMAGEBUFFER_MT);
+ lua_setmetatable(L, -2);
+
+ return 1;
+}
+
+/* Get ImageBuffer from stack (with type check) */
+static ImageBuffer* check_imagebuffer(lua_State* L, int idx) {
+ void* ud = lua_touserdata(L, idx);
+ if (ud == NULL) {
+ luaL_error(L, "ImageBuffer expected, got nil");
+ return NULL;
+ }
+ return (ImageBuffer*)ud;
+}
+
+/* ImageBuffer:fillCircle(cx, cy, radius, color) - true in-place modification */
+int lua_imagebuffer_fill_circle(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) return 0;
+
+ int cx = luaL_checkinteger(L, 2);
+ int cy = luaL_checkinteger(L, 3);
+ int radius = luaL_checkinteger(L, 4);
+ uint32_t color = luaL_checkinteger(L, 5);
+
+ /* Extract RGB from color (0xRRGGBB format) */
+ uint8_t r = (color >> 16) & 0xFF;
+ uint8_t g = (color >> 8) & 0xFF;
+ uint8_t b = color & 0xFF;
+ uint8_t a = 255;
+
+ int r2 = radius * radius;
+ int width = ib->width;
+ int height = ib->height;
+ uint8_t* data = ib->data;
+
+ /* Optimized: only iterate within bounding box */
+ int minY = cy - radius;
+ int maxY = cy + radius;
+ int minX = cx - radius;
+ int maxX = cx + radius;
+
+ /* Clip to buffer bounds */
+ if (minY < 0) minY = 0;
+ if (maxY >= height) maxY = height - 1;
+ if (minX < 0) minX = 0;
+ if (maxX >= width) maxX = width - 1;
+
+ for (int y = minY; y <= maxY; y++) {
+ int dy = y - cy;
+ int dy2 = dy * dy;
+ for (int x = minX; x <= maxX; x++) {
+ int dx = x - cx;
+ if (dx*dx + dy2 <= r2) {
+ size_t offset = ((size_t)y * width + x) * 4;
+ data[offset] = b;
+ data[offset + 1] = g;
+ data[offset + 2] = r;
+ data[offset + 3] = a;
+ }
+ }
+ }
+
+ return 0;
+}
+
+/* ImageBuffer:drawLine(x1, y1, x2, y2, thickness, color) - true in-place modification */
+int lua_imagebuffer_draw_line(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) return 0;
+
+ int x1 = luaL_checkinteger(L, 2);
+ int y1 = luaL_checkinteger(L, 3);
+ int x2 = luaL_checkinteger(L, 4);
+ int y2 = luaL_checkinteger(L, 5);
+ int thickness = luaL_checkinteger(L, 6);
+ uint32_t color = luaL_checkinteger(L, 7);
+
+ uint8_t r = (color >> 16) & 0xFF;
+ uint8_t g = (color >> 8) & 0xFF;
+ uint8_t b = color & 0xFF;
+ uint8_t a = 255;
+
+ int width = ib->width;
+ int height = ib->height;
+ uint8_t* data = ib->data;
+
+ int radius = thickness / 2;
+ int r2 = radius * radius;
+
+ /* Bresenham's line algorithm */
+ int dx = abs(x2 - x1);
+ int dy = abs(y2 - y1);
+ int sx = x1 < x2 ? 1 : -1;
+ int sy = y1 < y2 ? 1 : -1;
+ int err = dx - dy;
+
+ while (1) {
+ /* Draw circle at current point */
+ if (radius <= 0) {
+ /* Single pixel */
+ if (x1 >= 0 && x1 < width && y1 >= 0 && y1 < height) {
+ size_t offset = ((size_t)y1 * width + x1) * 4;
+ data[offset] = b;
+ data[offset + 1] = g;
+ data[offset + 2] = r;
+ data[offset + 3] = a;
+ }
+ } else {
+ /* Draw filled circle for thickness */
+ int minY = y1 - radius;
+ int maxY = y1 + radius;
+ int minX = x1 - radius;
+ int maxX = x1 + radius;
+
+ if (minY < 0) minY = 0;
+ if (maxY >= height) maxY = height - 1;
+ if (minX < 0) minX = 0;
+ if (maxX >= width) maxX = width - 1;
+
+ for (int py = minY; py <= maxY; py++) {
+ int tdy = py - y1;
+ int tdy2 = tdy * tdy;
+ for (int px = minX; px <= maxX; px++) {
+ int tdx = px - x1;
+ if (tdx*tdx + tdy2 <= r2) {
+ size_t offset = ((size_t)py * width + px) * 4;
+ data[offset] = b;
+ data[offset + 1] = g;
+ data[offset + 2] = r;
+ data[offset + 3] = a;
+ }
+ }
+ }
+ }
+
+ if (x1 == x2 && y1 == y2) break;
+
+ int e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x1 += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y1 += sy;
+ }
+ }
+
+ return 0;
+}
+
+/* ImageBuffer:fill(color) - fill entire buffer */
+int lua_imagebuffer_fill(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) return 0;
+
+ uint32_t color = luaL_checkinteger(L, 2);
+
+ uint8_t r = (color >> 16) & 0xFF;
+ uint8_t g = (color >> 8) & 0xFF;
+ uint8_t b = color & 0xFF;
+ uint8_t a = 255;
+
+ size_t size = (size_t)ib->width * ib->height * 4;
+ uint8_t* data = ib->data;
+
+ for (size_t i = 0; i < size; i += 4) {
+ data[i] = b;
+ data[i + 1] = g;
+ data[i + 2] = r;
+ data[i + 3] = a;
+ }
+
+ return 0;
+}
+
+/* ImageBuffer:getSize() -> width, height */
+int lua_imagebuffer_get_size(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib) {
+ lua_pushinteger(L, 0);
+ lua_pushinteger(L, 0);
+ return 2;
+ }
+ lua_pushinteger(L, ib->width);
+ lua_pushinteger(L, ib->height);
+ return 2;
+}
+
+/* ImageBuffer:getData() -> string (for blitting to screen) */
+int lua_imagebuffer_get_data(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) {
+ lua_pushstring(L, "");
+ return 1;
+ }
+ lua_pushlstring(L, (const char*)ib->data, (size_t)ib->width * ib->height * 4);
+ return 1;
+}
+
+/* ImageBuffer:getPixel(x, y) -> r, g, b, a */
+int lua_imagebuffer_get_pixel(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) return 0;
+
+ int x = luaL_checkinteger(L, 2);
+ int y = luaL_checkinteger(L, 3);
+
+ if (x < 0 || x >= ib->width || y < 0 || y >= ib->height) {
+ return 0;
+ }
+
+ size_t offset = ((size_t)y * ib->width + x) * 4;
+ lua_pushinteger(L, ib->data[offset + 2]); /* R */
+ lua_pushinteger(L, ib->data[offset + 1]); /* G */
+ lua_pushinteger(L, ib->data[offset]); /* B */
+ lua_pushinteger(L, ib->data[offset + 3]); /* A */
+ return 4;
+}
+
+/* ImageBuffer:setPixel(x, y, r, g, b, a) */
+int lua_imagebuffer_set_pixel(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (!ib || !ib->data) return 0;
+
+ int x = luaL_checkinteger(L, 2);
+ int y = luaL_checkinteger(L, 3);
+ int r = luaL_checkinteger(L, 4);
+ int g = luaL_checkinteger(L, 5);
+ int b = luaL_checkinteger(L, 6);
+ int a = luaL_optinteger(L, 7, 255);
+
+ if (x < 0 || x >= ib->width || y < 0 || y >= ib->height) {
+ return 0;
+ }
+
+ size_t offset = ((size_t)y * ib->width + x) * 4;
+ ib->data[offset] = b;
+ ib->data[offset + 1] = g;
+ ib->data[offset + 2] = r;
+ ib->data[offset + 3] = a;
+ return 0;
+}
+
+/* ImageBuffer garbage collection */
+int lua_imagebuffer_gc(lua_State* L) {
+ ImageBuffer* ib = check_imagebuffer(L, 1);
+ if (ib->data) {
+ free(ib->data);
+ ib->data = NULL;
+ }
+ return 0;
+}
+
+/* Register ImageBuffer metatable */
+void lua_imagebuffer_register(lua_State* L) {
+ luaL_newmetatable(L, IMAGEBUFFER_MT);
+
+ /* __index = methods table */
+ lua_newtable(L);
+
+ lua_pushcfunction(L, lua_imagebuffer_fill_circle);
+ lua_setfield(L, -2, "fillCircle");
+
+ lua_pushcfunction(L, lua_imagebuffer_draw_line);
+ lua_setfield(L, -2, "drawLine");
+
+ lua_pushcfunction(L, lua_imagebuffer_fill);
+ lua_setfield(L, -2, "fill");
+
+ lua_pushcfunction(L, lua_imagebuffer_get_size);
+ lua_setfield(L, -2, "getSize");
+
+ lua_pushcfunction(L, lua_imagebuffer_get_data);
+ lua_setfield(L, -2, "getData");
+
+ lua_pushcfunction(L, lua_imagebuffer_get_pixel);
+ lua_setfield(L, -2, "getPixel");
+
+ lua_pushcfunction(L, lua_imagebuffer_set_pixel);
+ lua_setfield(L, -2, "setPixel");
+
+ lua_setfield(L, -2, "__index");
+
+ /* __gc for cleanup */
+ lua_pushcfunction(L, lua_imagebuffer_gc);
+ lua_setfield(L, -2, "__gc");
+
+ lua_pop(L, 1); /* Pop metatable */
+}
+
+/* ============================================================================
+ * Legacy Lua Image Buffer Drawing Functions (string-based, copies on write)
+ * ============================================================================ */
+
+/* lua_buffer_set_pixel(buffer, width, height, x, y, r, g, b, a) -> new_buffer
+ * Sets a single pixel and returns the modified buffer */
+int lua_buffer_set_pixel(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int x = luaL_checkinteger(L, 4);
+ int y = luaL_checkinteger(L, 5);
+ int r = luaL_checkinteger(L, 6);
+ int g = luaL_checkinteger(L, 7);
+ int b = luaL_checkinteger(L, 8);
+ int a = luaL_optinteger(L, 9, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ /* Create a copy of the buffer to modify */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+ memcpy(new_buf, buf, buf_len);
+
+ buffer_set_pixel((uint8_t*)new_buf, width, height, x, y, r, g, b, a);
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_fill_circle(buffer, width, height, cx, cy, radius, r, g, b, a) -> new_buffer
+ * Fills a circle and returns the modified buffer */
+int lua_buffer_fill_circle(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int cx = luaL_checkinteger(L, 4);
+ int cy = luaL_checkinteger(L, 5);
+ int radius = luaL_checkinteger(L, 6);
+ int r = luaL_checkinteger(L, 7);
+ int g = luaL_checkinteger(L, 8);
+ int b = luaL_checkinteger(L, 9);
+ int a = luaL_optinteger(L, 10, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ /* Create a copy of the buffer to modify */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+ memcpy(new_buf, buf, buf_len);
+
+ /* Fill circle using squared distance */
+ int r2 = radius * radius;
+ for (int dy = -radius; dy <= radius; dy++) {
+ for (int dx = -radius; dx <= radius; dx++) {
+ if (dx*dx + dy*dy <= r2) {
+ buffer_set_pixel((uint8_t*)new_buf, width, height, cx + dx, cy + dy, r, g, b, a);
+ }
+ }
+ }
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_fill_circle_inplace(buffer, width, height, cx, cy, radius, r, g, b, a)
+ * Fills a circle by modifying buffer in-place (no copy, no return) */
+int lua_buffer_fill_circle_inplace(lua_State* L) {
+ size_t buf_len;
+ char* buf = (char*)luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int cx = luaL_checkinteger(L, 4);
+ int cy = luaL_checkinteger(L, 5);
+ int radius = luaL_checkinteger(L, 6);
+ int r = luaL_checkinteger(L, 7);
+ int g = luaL_checkinteger(L, 8);
+ int b = luaL_checkinteger(L, 9);
+ int a = luaL_optinteger(L, 10, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return 0;
+ }
+
+ /* Modify buffer in-place */
+ int r2 = radius * radius;
+ for (int dy = -radius; dy <= radius; dy++) {
+ for (int dx = -radius; dx <= radius; dx++) {
+ if (dx*dx + dy*dy <= r2) {
+ buffer_set_pixel((uint8_t*)buf, width, height, cx + dx, cy + dy, r, g, b, a);
+ }
+ }
+ }
+
+ return 0;
+}
+
+/* lua_buffer_draw_line_inplace(buffer, width, height, x1, y1, x2, y2, thickness, r, g, b, a)
+ * Draws a line with variable thickness by modifying buffer in-place (no copy, no return) */
+int lua_buffer_draw_line_inplace(lua_State* L) {
+ size_t buf_len;
+ char* buf = (char*)luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int x1 = luaL_checkinteger(L, 4);
+ int y1 = luaL_checkinteger(L, 5);
+ int x2 = luaL_checkinteger(L, 6);
+ int y2 = luaL_checkinteger(L, 7);
+ int thickness = luaL_checkinteger(L, 8);
+ int r = luaL_checkinteger(L, 9);
+ int g = luaL_checkinteger(L, 10);
+ int b = luaL_checkinteger(L, 11);
+ int a = luaL_optinteger(L, 12, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return 0;
+ }
+
+ /* Bresenham's line algorithm with thickness */
+ int dx = abs(x2 - x1);
+ int dy = abs(y2 - y1);
+ int sx = x1 < x2 ? 1 : -1;
+ int sy = y1 < y2 ? 1 : -1;
+ int err = dx - dy;
+
+ int radius = thickness / 2;
+ int r2 = radius * radius;
+
+ while (1) {
+ /* Draw a filled circle at each point for thickness */
+ if (radius <= 0) {
+ buffer_set_pixel((uint8_t*)buf, width, height, x1, y1, r, g, b, a);
+ } else {
+ for (int tdy = -radius; tdy <= radius; tdy++) {
+ for (int tdx = -radius; tdx <= radius; tdx++) {
+ if (tdx*tdx + tdy*tdy <= r2) {
+ buffer_set_pixel((uint8_t*)buf, width, height, x1 + tdx, y1 + tdy, r, g, b, a);
+ }
+ }
+ }
+ }
+
+ if (x1 == x2 && y1 == y2) break;
+
+ int e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x1 += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y1 += sy;
+ }
+ }
+
+ return 0;
+}
+
+/* lua_buffer_draw_line(buffer, width, height, x1, y1, x2, y2, thickness, r, g, b, a) -> new_buffer
+ * Draws a line with variable thickness using Bresenham's algorithm */
+int lua_buffer_draw_line(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int x1 = luaL_checkinteger(L, 4);
+ int y1 = luaL_checkinteger(L, 5);
+ int x2 = luaL_checkinteger(L, 6);
+ int y2 = luaL_checkinteger(L, 7);
+ int thickness = luaL_checkinteger(L, 8);
+ int r = luaL_checkinteger(L, 9);
+ int g = luaL_checkinteger(L, 10);
+ int b = luaL_checkinteger(L, 11);
+ int a = luaL_optinteger(L, 12, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ /* Create a copy of the buffer to modify */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+ memcpy(new_buf, buf, buf_len);
+
+ /* Bresenham's line algorithm with thickness */
+ int dx = abs(x2 - x1);
+ int dy = abs(y2 - y1);
+ int sx = x1 < x2 ? 1 : -1;
+ int sy = y1 < y2 ? 1 : -1;
+ int err = dx - dy;
+
+ int radius = thickness / 2;
+ int r2 = radius * radius;
+
+ while (1) {
+ /* Draw a filled circle at each point for thickness */
+ if (radius <= 0) {
+ buffer_set_pixel((uint8_t*)new_buf, width, height, x1, y1, r, g, b, a);
+ } else {
+ for (int tdy = -radius; tdy <= radius; tdy++) {
+ for (int tdx = -radius; tdx <= radius; tdx++) {
+ if (tdx*tdx + tdy*tdy <= r2) {
+ buffer_set_pixel((uint8_t*)new_buf, width, height, x1 + tdx, y1 + tdy, r, g, b, a);
+ }
+ }
+ }
+ }
+
+ if (x1 == x2 && y1 == y2) break;
+
+ int e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x1 += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y1 += sy;
+ }
+ }
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_fill_rect(buffer, width, height, x, y, w, h, r, g, b, a) -> new_buffer
+ * Fills a rectangle and returns the modified buffer */
+int lua_buffer_fill_rect(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int buf_width = luaL_checkinteger(L, 2);
+ int buf_height = luaL_checkinteger(L, 3);
+ int x = luaL_checkinteger(L, 4);
+ int y = luaL_checkinteger(L, 5);
+ int w = luaL_checkinteger(L, 6);
+ int h = luaL_checkinteger(L, 7);
+ int r = luaL_checkinteger(L, 8);
+ int g = luaL_checkinteger(L, 9);
+ int b = luaL_checkinteger(L, 10);
+ int a = luaL_optinteger(L, 11, 255);
+
+ size_t expected = (size_t)buf_width * buf_height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ /* Create a copy of the buffer to modify */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+ memcpy(new_buf, buf, buf_len);
+
+ /* Clip rectangle to buffer bounds */
+ int x1 = x < 0 ? 0 : x;
+ int y1 = y < 0 ? 0 : y;
+ int x2 = (x + w) > buf_width ? buf_width : (x + w);
+ int y2 = (y + h) > buf_height ? buf_height : (y + h);
+
+ /* Fill rectangle row by row (cache-friendly) */
+ for (int py = y1; py < y2; py++) {
+ for (int px = x1; px < x2; px++) {
+ size_t offset = ((size_t)py * buf_width + px) * 4;
+ new_buf[offset] = b;
+ new_buf[offset + 1] = g;
+ new_buf[offset + 2] = r;
+ new_buf[offset + 3] = a;
+ }
+ }
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_fill(buffer, width, height, r, g, b, a) -> new_buffer
+ * Fills the entire buffer with a color */
+int lua_buffer_fill(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int r = luaL_checkinteger(L, 4);
+ int g = luaL_checkinteger(L, 5);
+ int b = luaL_checkinteger(L, 6);
+ int a = luaL_optinteger(L, 7, 255);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ /* Create new buffer and fill it */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+
+ /* Fill all pixels */
+ size_t num_pixels = (size_t)width * height;
+ for (size_t i = 0; i < num_pixels; i++) {
+ size_t offset = i * 4;
+ new_buf[offset] = b;
+ new_buf[offset + 1] = g;
+ new_buf[offset + 2] = r;
+ new_buf[offset + 3] = a;
+ }
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_blit_row(buffer, width, height, y, row_data, start_x) -> new_buffer
+ * Blits a row of BGRA pixel data at the specified y position */
+int lua_buffer_blit_row(lua_State* L) {
+ size_t buf_len;
+ const char* buf = luaL_checklstring(L, 1, &buf_len);
+ int width = luaL_checkinteger(L, 2);
+ int height = luaL_checkinteger(L, 3);
+ int y = luaL_checkinteger(L, 4);
+ size_t row_len;
+ const char* row_data = luaL_checklstring(L, 5, &row_len);
+ int start_x = luaL_optinteger(L, 6, 0);
+
+ size_t expected = (size_t)width * height * 4;
+ if (buf_len < expected) {
+ return luaL_error(L, "Buffer too small");
+ }
+
+ if (y < 0 || y >= height) {
+ /* Row out of bounds, return original buffer */
+ lua_pushvalue(L, 1);
+ return 1;
+ }
+
+ /* Create a copy of the buffer to modify */
+ char* new_buf = (char*)malloc(buf_len);
+ if (!new_buf) {
+ return luaL_error(L, "Out of memory");
+ }
+ memcpy(new_buf, buf, buf_len);
+
+ /* Calculate how many pixels to copy */
+ int pixels_in_row = row_len / 4;
+ int dest_offset = (y * width + start_x) * 4;
+ int pixels_to_copy = pixels_in_row;
+
+ /* Clip to buffer bounds */
+ if (start_x < 0) {
+ int skip = -start_x;
+ row_data += skip * 4;
+ pixels_to_copy -= skip;
+ start_x = 0;
+ dest_offset = y * width * 4;
+ }
+ if (start_x + pixels_to_copy > width) {
+ pixels_to_copy = width - start_x;
+ }
+
+ if (pixels_to_copy > 0) {
+ memcpy(new_buf + dest_offset, row_data, pixels_to_copy * 4);
+ }
+
+ lua_pushlstring(L, new_buf, buf_len);
+ free(new_buf);
+ return 1;
+}
+
+/* lua_buffer_create(width, height, r, g, b, a) -> buffer
+ * Creates a new BGRA buffer filled with the specified color */
+int lua_buffer_create(lua_State* L) {
+ int width = luaL_checkinteger(L, 1);
+ int height = luaL_checkinteger(L, 2);
+ int r = luaL_optinteger(L, 3, 255);
+ int g = luaL_optinteger(L, 4, 255);
+ int b = luaL_optinteger(L, 5, 255);
+ int a = luaL_optinteger(L, 6, 255);
+
+ if (width <= 0 || height <= 0 || width > 8192 || height > 8192) {
+ return luaL_error(L, "Invalid buffer dimensions");
+ }
+
+ size_t buf_len = (size_t)width * height * 4;
+ char* buf = (char*)malloc(buf_len);
+ if (!buf) {
+ return luaL_error(L, "Out of memory");
+ }
+
+ /* Fill all pixels */
+ size_t num_pixels = (size_t)width * height;
+ for (size_t i = 0; i < num_pixels; i++) {
+ size_t offset = i * 4;
+ buf[offset] = b;
+ buf[offset + 1] = g;
+ buf[offset + 2] = r;
+ buf[offset + 3] = a;
+ }
+
+ lua_pushlstring(L, buf, buf_len);
+ free(buf);
+ return 1;
+}
diff --git a/iso_includes/apps/com.luajitos.background/manifest.lua b/iso_includes/apps/com.luajitos.background/manifest.lua
@@ -5,7 +5,7 @@ return {
category = "all",
description = "LuajitOS Desktop Background";
entry = "background.lua";
- type = "gui";
+ type = "background";
hidden = true; -- Hide from start menu
autostart = true; -- Launch on system startup
autostartPriority = 1; -- Launch first (background should be behind everything)
diff --git a/iso_includes/apps/com.luajitos.crypto/manifest.lua b/iso_includes/apps/com.luajitos.crypto/manifest.lua
@@ -6,8 +6,10 @@ return {
description = "Command-line cryptographic utility for hashing, encryption, signing, and key derivation",
entry = "init.lua",
type = "cli",
+ autorun = true,
permissions = {
"stdio", -- Command-line output
- "export" -- Export crypto library for other apps
+ "export", -- Export crypto library for other apps
+ "crypto" -- Access to crypto library
}
}
diff --git a/iso_includes/apps/com.luajitos.crypto/src/init.lua b/iso_includes/apps/com.luajitos.crypto/src/init.lua
@@ -295,7 +295,27 @@ if #positional == 0 then
return
end
-local command = positional[1]
+-- Normalize command - uppercase for hash/kdf commands, preserve case for others
+local rawCommand = positional[1]
+local command = rawCommand:upper()
+
+-- Map uppercase to proper mixed-case for specific commands
+local commandMap = {
+ ["ED25519.KEYPAIR"] = "Ed25519.keypair",
+ ["ED25519.SIGN"] = "Ed25519.sign",
+ ["ED25519.VERIFY"] = "Ed25519.verify",
+ ["X25519.KEYPAIR"] = "X25519.keypair",
+ ["X25519.PUBLICKEY"] = "X25519.publicKey",
+ ["X25519.SHARED"] = "X25519.shared",
+ ["GENERATESALT"] = "generateSalt",
+ ["NEWKEY"] = "newKey",
+ ["ENCRYPT"] = "encrypt",
+ ["DECRYPT"] = "decrypt",
+ ["RANDOM"] = "random",
+}
+if commandMap[command] then
+ command = commandMap[command]
+end
-- Hash commands
if command == "MD5" then
diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua
@@ -438,6 +438,11 @@ end
-- Selection edit callback for multi-line selection editing
-- point1 and point2 are {x, y} coordinates in the content area
window.onSelectionEditted = function(point1, point2, newContent)
+ -- Safety check for valid points
+ if not point1 or not point2 or not point1.x or not point1.y or not point2.x or not point2.y then
+ return
+ end
+
-- Convert y coordinates to line numbers
-- Text starts at y = toolbarHeight + 2, each line is lineHeight pixels
local textStartY = toolbarHeight + 2
@@ -456,16 +461,24 @@ window.onSelectionEditted = function(point1, point2, newContent)
startCol, endCol = endCol, startCol
end
- -- Validate line numbers
+ -- Validate line numbers - if selection is completely outside valid range, abort
+ if startLine > #lines and endLine > #lines then return end
if startLine < 1 then startLine = 1 end
if endLine > #lines then endLine = #lines end
- if startLine > #lines then return end
+ if startLine > #lines then startLine = #lines end
+ if #lines == 0 then
+ lines = {""}
+ startLine = 1
+ endLine = 1
+ end
-- Clamp column positions
if startCol < 1 then startCol = 1 end
if endCol < 1 then endCol = 1 end
- if startCol > #lines[startLine] + 1 then startCol = #lines[startLine] + 1 end
- if endCol > #lines[endLine] then endCol = #lines[endLine] end
+ local startLineLen = lines[startLine] and #lines[startLine] or 0
+ local endLineLen = lines[endLine] and #lines[endLine] or 0
+ if startCol > startLineLen + 1 then startCol = startLineLen + 1 end
+ if endCol > endLineLen then endCol = endLineLen end
-- Get the text before and after the selection
local beforeText = lines[startLine]:sub(1, startCol - 1)
diff --git a/iso_includes/apps/com.luajitos.moonbrowser/src/render.lua b/iso_includes/apps/com.luajitos.moonbrowser/src/render.lua
@@ -260,11 +260,11 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
-- Draw underline
gfx:fillRect(currentX, y + linkHeight - 2, linkWidth, 1, 0x0000FF)
- -- Store for click handling
+ -- Store for click handling (subtract offsetY since y includes it but clicks are relative to content)
table.insert(self.interactiveElements, {
type = "link",
x = currentX,
- y = y + self.scrollY,
+ y = y + self.scrollY - self.offsetY,
width = linkWidth,
height = linkHeight,
text = linkText,
@@ -316,11 +316,11 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
-- Button text (centered)
gfx:drawText(drawX + 8, drawY + 6, buttonText, 0x000000)
- -- Store for click handling
+ -- Store for click handling (subtract offsetY since drawY includes it)
table.insert(self.interactiveElements, {
type = "button",
x = drawX,
- y = drawY + self.scrollY, -- Store absolute position
+ y = drawY + self.scrollY - self.offsetY,
width = btnWidth,
height = btnHeight,
text = buttonText,
@@ -389,12 +389,12 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
end
end
- -- Store for click handling
+ -- Store for click handling (subtract offsetY)
table.insert(self.interactiveElements, {
type = "input",
inputType = inputType,
x = currentX,
- y = y + self.scrollY,
+ y = y + self.scrollY - self.offsetY,
width = inputWidth,
height = inputHeight,
name = inputName,
@@ -425,7 +425,7 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
table.insert(self.interactiveElements, {
type = "submit",
x = currentX,
- y = y + self.scrollY,
+ y = y + self.scrollY - self.offsetY,
width = btnWidth,
height = btnHeight,
text = buttonText,
@@ -467,7 +467,7 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
table.insert(self.interactiveElements, {
type = "checkbox",
x = currentX,
- y = y + 2 + self.scrollY,
+ y = y + 2 + self.scrollY - self.offsetY,
width = checkSize,
height = checkSize,
name = inputName,
@@ -519,7 +519,7 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
table.insert(self.interactiveElements, {
type = "textarea",
x = currentX,
- y = y + self.scrollY,
+ y = y + self.scrollY - self.offsetY,
width = textareaWidth,
height = textareaHeight,
name = inputName,
diff --git a/iso_includes/apps/com.luajitos.paint/icon.png b/iso_includes/apps/com.luajitos.paint/icon.png
Binary files differ.
diff --git a/iso_includes/apps/com.luajitos.paint/src/init.lua b/iso_includes/apps/com.luajitos.paint/src/init.lua
@@ -1,5 +1,5 @@
-- LuaJIT OS Paint App
--- Simple version - draws directly to screen via gfx
+-- Uses ImageBuffer userdata for true in-place drawing (no string copies)
-- Dialog is pre-loaded in the sandbox, no require() needed
@@ -10,8 +10,10 @@ local initialHeight = 250 + toolbarHeight
local window = app:newWindow("Paint", initialWidth, initialHeight)
window.resizable = true -- Allow resizing
--- Store drawn strokes (lines between points)
-local strokes = {}
+-- Canvas buffer (ImageBuffer userdata - true mutable buffer)
+local canvasBuffer = nil
+local canvasBufferWidth = nil
+local canvasBufferHeight = nil
-- Background image (native C image for display)
local bgImage = nil
@@ -47,8 +49,49 @@ local function isInside(x, y, rx, ry, rw, rh)
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
end
--- Draw line between two points using Bresenham's algorithm
+-- Ensure canvas buffer exists and matches window size
+local function ensureCanvasBuffer()
+ local canvasW = window.width
+ local canvasH = window.height - toolbarHeight
+
+ if not canvasBuffer or canvasBufferWidth ~= canvasW or canvasBufferHeight ~= canvasH then
+ -- Create new ImageBuffer userdata (true mutable buffer)
+ local newBuffer = ImageBufferNew(canvasW, canvasH)
+ newBuffer:fill(0xFFFFFF)
+
+ -- If we had an old buffer, copy it over (for resize)
+ if canvasBuffer then
+ local oldW, oldH = canvasBuffer:getSize()
+ for y = 0, math.min(oldH, canvasH) - 1 do
+ for x = 0, math.min(oldW, canvasW) - 1 do
+ local r, g, b = canvasBuffer:getPixel(x, y)
+ if r then
+ newBuffer:setPixel(x, y, r, g, b)
+ end
+ end
+ end
+ end
+
+ canvasBuffer = newBuffer
+ canvasBufferWidth = canvasW
+ canvasBufferHeight = canvasH
+ end
+end
+
+-- Draw a filled circle to the canvas buffer (in-place, no copies)
+local function drawCircleToBuffer(cx, cy, radius, color)
+ if not canvasBuffer then return end
+ canvasBuffer:fillCircle(cx, cy, radius, color)
+end
+
+-- Draw line between two points with thickness (in-place, no copies)
local function addLine(x1, y1, x2, y2)
+ if not canvasBuffer then return end
+ canvasBuffer:drawLine(x1, y1, x2, y2, brushRadius * 2, brushColor)
+end
+
+-- Legacy addLine implementation for fallback (not used when C functions available)
+local function addLineLegacy(x1, y1, x2, y2)
local dx = math.abs(x2 - x1)
local dy = math.abs(y2 - y1)
local sx = x1 < x2 and 1 or -1
@@ -56,8 +99,8 @@ local function addLine(x1, y1, x2, y2)
local err = dx - dy
while true do
- -- Add point
- strokes[#strokes + 1] = {x = x1, y = y1, color = brushColor, r = brushRadius}
+ -- Draw point directly to buffer
+ drawCircleToBuffer(x1, y1, brushRadius, brushColor)
if x1 == x2 and y1 == y2 then break end
local e2 = 2 * err
@@ -107,7 +150,29 @@ local function loadImage(path)
bgImageHeight = ImageGetHeight(nativeImg)
end
currentFile = path
- strokes = {}
+
+ -- Load image into canvas buffer using Image.open
+ Image.fsOverride = fs
+ local luaImg, err = Image.open(path)
+ Image.fsOverride = nil
+
+ if luaImg then
+ local imgW, imgH = luaImg:getSize()
+ canvasBuffer = Image.new(imgW, imgH, false)
+ canvasBuffer:fill(0xFFFFFF)
+ -- Copy pixels
+ for y = 0, imgH - 1 do
+ for x = 0, imgW - 1 do
+ local r, g, b = luaImg:getPixel(x, y)
+ if r then
+ canvasBuffer:setPixel(x, y, r, g, b)
+ end
+ end
+ end
+ canvasBufferWidth = imgW
+ canvasBufferHeight = imgH
+ end
+
return true
end
@@ -117,7 +182,10 @@ end
-- Button definitions
local buttons = {
{x = 5, y = 5, w = 40, h = 20, label = "New", action = function()
- strokes = {}
+ -- Clear canvas buffer
+ if canvasBuffer then
+ canvasBuffer:fill(0xFFFFFF)
+ end
if bgImage and ImageDestroy then
ImageDestroy(bgImage)
bgImage = nil
@@ -153,63 +221,15 @@ local buttons = {
})
dlg:openDialog(function(path)
if path then
- -- Get current canvas dimensions
- local canvasW = window.width
- local canvasH = window.height - toolbarHeight
-
- local img
- local imgW, imgH
-
- -- If we have a background image, load it with Image.open for saving
- if currentFile and bgImage then
- -- Use Image.open to get the Lua Image object
- Image.fsOverride = fs
- local loadedImg, err = Image.open(currentFile)
- Image.fsOverride = nil
-
- if loadedImg then
- img = loadedImg
- imgW, imgH = img:getSize()
- else
- print("Paint: Failed to reload image for save: " .. (err or "unknown"))
- end
- end
+ -- Ensure we have a canvas buffer
+ ensureCanvasBuffer()
- -- If no background image loaded, create new white canvas
- if not img then
- imgW = canvasW
- imgH = canvasH
- img = Image.new(imgW, imgH, false)
- img:fill(0xFFFFFF)
- end
-
- -- Use batch mode for efficient stroke drawing
- img:beginBatch()
-
- -- Draw all strokes
- for _, s in ipairs(strokes) do
- local rad = s.r
- -- Extract RGB from color
- local sr = bit.band(bit.rshift(s.color, 16), 0xFF)
- local sg = bit.band(bit.rshift(s.color, 8), 0xFF)
- local sb = bit.band(s.color, 0xFF)
-
- for dy = -rad, rad do
- for dx = -rad, rad do
- if dx*dx + dy*dy <= rad*rad then
- local px, py = s.x + dx, s.y + dy
- if px >= 0 and px < imgW and py >= 0 and py < imgH then
- img:_batchWritePixel(px, py, sr, sg, sb)
- end
- end
- end
- end
+ if not canvasBuffer then
+ print("Paint: No canvas to save")
+ return
end
- -- End batch mode to finalize buffer
- img:endBatch()
-
- -- Detect format from extension, or use magic numbers from original file
+ -- Detect format from extension
local ext = path:lower():match("%.([^%.]+)$")
-- If no extension, try to detect from original file's magic bytes
@@ -232,15 +252,15 @@ local buttons = {
local success, err
if ext == "bmp" then
- success, err = img:saveAsBMP(path, {fs = fs})
+ success, err = canvasBuffer:saveAsBMP(path, {fs = fs})
elseif ext == "jpg" or ext == "jpeg" then
- success, err = img:saveAsJPEG(path, {fs = fs})
+ success, err = canvasBuffer:saveAsJPEG(path, {fs = fs})
elseif ext == "png" then
- success, err = img:saveAsPNG(path, {fs = fs})
+ success, err = canvasBuffer:saveAsPNG(path, {fs = fs})
else
-- Unknown extension, add .png and save
path = path .. ".png"
- success, err = img:saveAsPNG(path, {fs = fs})
+ success, err = canvasBuffer:saveAsPNG(path, {fs = fs})
end
if success then
@@ -281,22 +301,25 @@ window.onMouseDown = function(mx, my)
-- Clear button
local clearX = margin + #colors*(sz+margin) + 10
if isInside(mx, my, clearX, colorY, 40, sz) then
- strokes = {}
+ if canvasBuffer then
+ canvasBuffer:fill(0xFFFFFF)
+ end
window:markDirty()
return
end
else
-- Start drawing on canvas
+ ensureCanvasBuffer()
isDrawing = true
local canvasY = my - toolbarHeight
lastX, lastY = mx, canvasY
- -- Add initial point
- strokes[#strokes + 1] = {x = mx, y = canvasY, color = brushColor, r = brushRadius}
+ -- Add initial point directly to buffer
+ drawCircleToBuffer(mx, canvasY, brushRadius, brushColor)
window:markDirty()
end
end
--- Mouse move - continue drawing line
+-- Mouse move - continue drawing line (no throttling needed with ImageBuffer)
window.onMouseMove = function(mx, my)
if isDrawing and my >= toolbarHeight then
local canvasY = my - toolbarHeight
@@ -351,28 +374,15 @@ window.onDraw = function(gfx)
gfx:drawRect(clearX, colorY, 40, sz, 0xAAAAAA)
gfx:drawText(clearX + 4, colorY + 4, "Clr", 0xFFFFFF)
- -- Canvas background (white or loaded image)
- if bgImage and ImageDraw then
- ImageDraw(bgImage, 0, toolbarHeight)
+ -- Draw canvas buffer (contains both background and strokes)
+ ensureCanvasBuffer()
+ if canvasBuffer then
+ -- Draw the entire canvas buffer in one operation
+ gfx:drawBuffer(canvasBuffer, 0, toolbarHeight)
else
gfx:fillRect(0, toolbarHeight, canvasW, canvasH, 0xFFFFFF)
end
- -- Draw all strokes as filled circles
- for _, s in ipairs(strokes) do
- local r = s.r
- for dy = -r, r do
- for dx = -r, r do
- if dx*dx + dy*dy <= r*r then
- local px, py = s.x + dx, s.y + dy + toolbarHeight
- if px >= 0 and px < canvasW and py >= toolbarHeight and py < winH then
- gfx:fillRect(px, py, 1, 1, s.color)
- end
- end
- end
- end
- end
-
gfx:drawRect(0, toolbarHeight, canvasW, canvasH, 0x000000)
end
diff --git a/iso_includes/apps/com.luajitos.spreadsheet/icon.png b/iso_includes/apps/com.luajitos.spreadsheet/icon.png
Binary files differ.
diff --git a/iso_includes/apps/com.luajitos.spreadsheet/manifest.lua b/iso_includes/apps/com.luajitos.spreadsheet/manifest.lua
@@ -0,0 +1,14 @@
+return {
+ name = "Spreadsheet",
+ identifier = "com.luajitos.spreadsheet",
+ version = "1.0.0",
+ author = "LuajitOS",
+ description = "Spreadsheet application with formula support",
+ entry = "init.lua",
+ permissions = {
+ "filesystem",
+ "window",
+ "load",
+ "setfenv"
+ }
+}
diff --git a/iso_includes/apps/com.luajitos.spreadsheet/src/init.lua b/iso_includes/apps/com.luajitos.spreadsheet/src/init.lua
@@ -0,0 +1,835 @@
+-- LuaJIT OS Spreadsheet Application
+-- Infinite spreadsheet with CSV support and formula evaluation
+
+local toolbarHeight = 30
+local headerHeight = 20
+local rowHeight = 22
+local defaultColWidth = 80
+local rowHeaderWidth = 40
+
+local initialWidth = 600
+local initialHeight = 400
+
+local window = app:newWindow("Spreadsheet", initialWidth, initialHeight)
+window.resizable = true
+
+-- Spreadsheet data: rows[y] = {cells...}, grows as needed
+local rows = {}
+local colWidths = {} -- Custom column widths
+
+-- Selection
+local selectedX = 1
+local selectedY = 1
+local editing = false
+local editBuffer = ""
+
+-- Formula bar
+local formulaBarActive = false
+local formulaBuffer = ""
+
+-- Scroll position
+local scrollX = 0
+local scrollY = 0
+
+-- Current file
+local currentFile = nil
+
+-- Forward declaration
+local ensureSelectionVisible
+
+-- Get or create a row
+local function getRow(y)
+ if not rows[y] then
+ rows[y] = {}
+ end
+ return rows[y]
+end
+
+-- Get cell value
+local function getCell(x, y)
+ local row = rows[y]
+ if row then
+ return row[x] or ""
+ end
+ return ""
+end
+
+-- Set cell value
+local function setCell(x, y, value)
+ local row = getRow(y)
+ row[x] = value
+end
+
+-- Get column width
+local function getColWidth(x)
+ return colWidths[x] or defaultColWidth
+end
+
+-- Ensure selected cell is visible (scroll if needed)
+ensureSelectionVisible = function(winW, winH)
+ -- Calculate cell position
+ local cellX = rowHeaderWidth
+ for col = 1, selectedX - 1 do
+ cellX = cellX + getColWidth(col)
+ end
+ local cellW = getColWidth(selectedX)
+
+ -- Horizontal scrolling
+ local visibleLeft = scrollX
+ local visibleRight = scrollX + (winW - rowHeaderWidth)
+
+ if cellX - rowHeaderWidth < visibleLeft then
+ -- Cell is too far left, scroll left
+ scrollX = cellX - rowHeaderWidth
+ elseif cellX - rowHeaderWidth + cellW > visibleRight then
+ -- Cell is too far right, scroll right
+ scrollX = cellX - rowHeaderWidth + cellW - (winW - rowHeaderWidth)
+ end
+
+ -- Vertical scrolling
+ local visibleTop = scrollY
+ local visibleBottom = scrollY + (winH - toolbarHeight - headerHeight)
+ local cellTop = (selectedY - 1) * rowHeight
+ local cellBottom = cellTop + rowHeight
+
+ if cellTop < visibleTop then
+ -- Cell is above view, scroll up
+ scrollY = cellTop
+ elseif cellBottom > visibleBottom then
+ -- Cell is below view, scroll down
+ scrollY = cellBottom - (winH - toolbarHeight - headerHeight)
+ end
+
+ -- Clamp scroll values
+ if scrollX < 0 then scrollX = 0 end
+ if scrollY < 0 then scrollY = 0 end
+end
+
+-- Convert column number to letter (1=A, 2=B, ..., 27=AA)
+local function colToLetter(n)
+ local result = ""
+ while n > 0 do
+ n = n - 1
+ result = string.char(65 + (n % 26)) .. result
+ n = math.floor(n / 26)
+ end
+ return result
+end
+
+-- Convert column letter to number (A=1, B=2, ..., AA=27)
+local function letterToCol(letters)
+ local result = 0
+ for i = 1, #letters do
+ result = result * 26 + (letters:byte(i) - 64)
+ end
+ return result
+end
+
+-- Expand range notation [A1-C4] to list of IN() calls
+local function expandRange(formula)
+ return formula:gsub("%[([A-Z]+)(%d+)%-([A-Z]+)(%d+)%]", function(col1Str, row1Str, col2Str, row2Str)
+ local col1 = letterToCol(col1Str)
+ local row1 = tonumber(row1Str)
+ local col2 = letterToCol(col2Str)
+ local row2 = tonumber(row2Str)
+ local cells = {}
+ -- Iterate rows first, then columns
+ for row = row1, row2 do
+ for col = col1, col2 do
+ table.insert(cells, "IN(" .. col .. "," .. row .. ")")
+ end
+ end
+ return table.concat(cells, ", ")
+ end)
+end
+
+-- Convert cell references like A4, BC12 to IN(col, row)
+local function preprocessFormula(formula)
+ -- First expand range notation [A1-C4]
+ formula = expandRange(formula)
+
+ -- Then replace cell references A1, BC23, etc. with IN(col, row)
+ -- Use frontier pattern %f to ensure we don't match inside words like SUM, AVG, etc.
+ -- Match cell refs that are preceded by non-letter or start of string
+ local result = ""
+ local i = 1
+ while i <= #formula do
+ -- Check if we're at a potential cell reference
+ local col, row, endPos = formula:match("^([A-Z]+)(%d+)()", i)
+ if col then
+ -- Check if preceded by a letter (would be part of function name)
+ local prevChar = i > 1 and formula:sub(i-1, i-1) or ""
+ if prevChar:match("[A-Za-z]") then
+ -- Part of a function name, don't convert
+ result = result .. col
+ i = i + #col
+ else
+ -- Standalone cell reference, convert it
+ local colNum = letterToCol(col)
+ result = result .. "IN(" .. colNum .. "," .. row .. ")"
+ i = endPos
+ end
+ else
+ result = result .. formula:sub(i, i)
+ i = i + 1
+ end
+ end
+ return result
+end
+
+-- Formula sandbox environment
+local function createFormulaSandbox()
+ local sandbox = {}
+
+ -- IN(x, y) - get cell value at position
+ sandbox.IN = function(x, y)
+ local val = getCell(x, y)
+ -- Try to convert to number
+ local num = tonumber(val)
+ if num then return num end
+ -- Check if it's a formula result
+ if type(val) == "string" and val:sub(1, 2) == "%=" then
+ -- Evaluate nested formula
+ local result = evaluateFormula(val)
+ return result
+ end
+ return val
+ end
+
+ -- SELECT(x, y, direction, length) - get multiple cells
+ sandbox.SELECT = function(x, y, direction, length)
+ local results = {}
+ local dx, dy = 0, 0
+ if direction == "up" then dy = -1
+ elseif direction == "down" then dy = 1
+ elseif direction == "left" then dx = -1
+ elseif direction == "right" then dx = 1
+ end
+
+ for i = 0, length - 1 do
+ local cx = x + dx * i
+ local cy = y + dy * i
+ local val = sandbox.IN(cx, cy)
+ table.insert(results, val)
+ end
+ return unpack(results)
+ end
+
+ -- SUM(...) - sum all numeric arguments
+ sandbox.SUM = function(...)
+ local args = {...}
+ local total = 0
+ for _, v in ipairs(args) do
+ local num = tonumber(v)
+ if num then
+ total = total + num
+ end
+ end
+ return total
+ end
+
+ -- CONCAT(...) - concatenate strings
+ sandbox.CONCAT = function(...)
+ local args = {...}
+ local result = ""
+ for _, v in ipairs(args) do
+ result = result .. tostring(v)
+ end
+ return result
+ end
+
+ -- REPEAT(str, count, separator)
+ sandbox.REPEAT = function(str, count, separator)
+ separator = separator or ""
+ local parts = {}
+ for i = 1, count do
+ table.insert(parts, str)
+ end
+ return table.concat(parts, separator)
+ end
+
+ -- AVG(...) - average of numeric arguments
+ sandbox.AVG = function(...)
+ local args = {...}
+ local total = 0
+ local count = 0
+ for _, v in ipairs(args) do
+ local num = tonumber(v)
+ if num then
+ total = total + num
+ count = count + 1
+ end
+ end
+ if count == 0 then return 0 end
+ return total / count
+ end
+
+ -- MIN/MAX
+ sandbox.MIN = function(...)
+ local args = {...}
+ local result = nil
+ for _, v in ipairs(args) do
+ local num = tonumber(v)
+ if num then
+ if result == nil or num < result then
+ result = num
+ end
+ end
+ end
+ return result or 0
+ end
+
+ sandbox.MAX = function(...)
+ local args = {...}
+ local result = nil
+ for _, v in ipairs(args) do
+ local num = tonumber(v)
+ if num then
+ if result == nil or num > result then
+ result = num
+ end
+ end
+ end
+ return result or 0
+ end
+
+ -- Basic math
+ sandbox.math = math
+ sandbox.tostring = tostring
+ sandbox.tonumber = tonumber
+ sandbox.type = type
+
+ return sandbox
+end
+
+-- Evaluate a formula
+local function evaluateFormula(formula)
+ if type(formula) ~= "string" or formula:sub(1, 2) ~= "%=" then
+ return formula
+ end
+
+ local code = formula:sub(3) -- Remove %=
+
+ -- Preprocess: convert cell references (A1, BC23) to IN(col, row)
+ code = preprocessFormula(code)
+
+ local sandbox = createFormulaSandbox()
+
+ -- Try to compile the formula
+ local fn, err = loadstring("return " .. code)
+ if not fn then
+ return "#ERR: " .. tostring(err)
+ end
+
+ -- Set environment to sandbox
+ setfenv(fn, sandbox)
+
+ local ok, result = pcall(fn)
+ if not ok then
+ return "#ERR: " .. tostring(result)
+ end
+
+ return result
+end
+
+-- Get display value for a cell (evaluates formulas)
+local function getDisplayValue(x, y)
+ local val = getCell(x, y)
+ if type(val) == "string" and val:sub(1, 2) == "%=" then
+ return tostring(evaluateFormula(val))
+ end
+ return tostring(val)
+end
+
+-- Parse CSV line
+local function parseCSVLine(line)
+ local cells = {}
+ local current = ""
+ local inQuotes = false
+
+ for i = 1, #line do
+ local c = line:sub(i, i)
+ if c == '"' then
+ inQuotes = not inQuotes
+ elseif c == ',' and not inQuotes then
+ table.insert(cells, current)
+ current = ""
+ else
+ current = current .. c
+ end
+ end
+ table.insert(cells, current)
+
+ return cells
+end
+
+-- Load CSV file
+local function loadCSV(path)
+ local data = fs:read(path)
+ if not data then return false end
+
+ rows = {}
+ colWidths = {}
+
+ local y = 1
+ for line in data:gmatch("[^\r\n]+") do
+ local cells = parseCSVLine(line)
+ rows[y] = {}
+ for x, val in ipairs(cells) do
+ rows[y][x] = val
+ end
+ y = y + 1
+ end
+
+ currentFile = path
+ selectedX = 1
+ selectedY = 1
+ scrollX = 0
+ scrollY = 0
+ window:markDirty()
+ return true
+end
+
+-- Generate CSV content
+local function generateCSV()
+ local maxX = 0
+ local maxY = 0
+
+ -- Find bounds
+ for y, row in pairs(rows) do
+ if y > maxY then maxY = y end
+ for x, _ in pairs(row) do
+ if x > maxX then maxX = x end
+ end
+ end
+
+ local lines = {}
+ for y = 1, maxY do
+ local cells = {}
+ for x = 1, maxX do
+ local val = tostring(getCell(x, y) or "")
+ -- Escape commas and quotes
+ if val:find('[,"\n]') then
+ val = '"' .. val:gsub('"', '""') .. '"'
+ end
+ table.insert(cells, val)
+ end
+ table.insert(lines, table.concat(cells, ","))
+ end
+
+ return table.concat(lines, "\n")
+end
+
+-- Save CSV file
+local function saveCSV(path)
+ local content = generateCSV()
+ local ok = fs:write(path, content)
+ if ok then
+ currentFile = path
+ end
+ return ok
+end
+
+-- Clear spreadsheet
+local function newSpreadsheet()
+ rows = {}
+ colWidths = {}
+ currentFile = nil
+ selectedX = 1
+ selectedY = 1
+ scrollX = 0
+ scrollY = 0
+ editing = false
+ editBuffer = ""
+ window:markDirty()
+end
+
+-- Formula bar dimensions
+local formulaBarX = 145
+local formulaBarY = 5
+local formulaBarH = 20
+
+-- Button definitions
+local buttons = {
+ {x = 5, y = 5, w = 40, h = 20, label = "New", action = newSpreadsheet},
+ {x = 50, y = 5, w = 45, h = 20, label = "Open", action = function()
+ local dlg = Dialog.fileOpen("/", {
+ app = app,
+ fs = fs,
+ title = "Open CSV",
+ filter = {"csv"}
+ })
+ dlg:openDialog(function(path)
+ if path then
+ loadCSV(path)
+ end
+ end)
+ end},
+ {x = 100, y = 5, w = 40, h = 20, label = "Save", action = function()
+ local defaultName = "spreadsheet.csv"
+ if currentFile then
+ defaultName = currentFile:match("([^/]+)$") or defaultName
+ end
+ local dlg = Dialog.fileSave("/home", defaultName, {
+ app = app,
+ fs = fs,
+ title = "Save CSV"
+ })
+ dlg:openDialog(function(path)
+ if path then
+ if not path:match("%.csv$") then
+ path = path .. ".csv"
+ end
+ saveCSV(path)
+ end
+ end)
+ end}
+}
+
+-- Helper: check if point is inside rect
+local function isInside(px, py, x, y, w, h)
+ return px >= x and px < x + w and py >= y and py < y + h
+end
+
+-- Get column at screen X position
+local function getColumnAtX(screenX)
+ local x = rowHeaderWidth - scrollX
+ local col = 1
+ while x < screenX do
+ x = x + getColWidth(col)
+ if x >= screenX then
+ return col
+ end
+ col = col + 1
+ if col > 1000 then break end -- Safety limit
+ end
+ return col
+end
+
+-- Get row at screen Y position
+local function getRowAtY(screenY)
+ local contentY = screenY - toolbarHeight - headerHeight
+ if contentY < 0 then return nil end
+ local row = math.floor((contentY + scrollY) / rowHeight) + 1
+ return row
+end
+
+-- Get X position for column
+local function getColumnX(col)
+ local x = rowHeaderWidth
+ for c = 1, col - 1 do
+ x = x + getColWidth(c)
+ end
+ return x - scrollX
+end
+
+-- Key handler (onInput for keyboard input)
+window.onInput = function(key, scancode)
+ -- Scancodes: up=72, down=80, left=75, right=77, escape=1
+
+ if formulaBarActive then
+ -- Formula bar input
+ if key == "\n" then
+ -- Confirm formula - prepend %= and set cell
+ if formulaBuffer ~= "" then
+ setCell(selectedX, selectedY, "%=" .. formulaBuffer)
+ end
+ formulaBarActive = false
+ formulaBuffer = ""
+ elseif key == "\b" then
+ if #formulaBuffer > 0 then
+ formulaBuffer = formulaBuffer:sub(1, -2)
+ end
+ elseif scancode == 1 then -- Escape
+ formulaBarActive = false
+ formulaBuffer = ""
+ elseif key and #key == 1 and key:byte() >= 32 then
+ formulaBuffer = formulaBuffer .. key
+ end
+ elseif editing then
+ if key == "\n" then
+ -- Confirm edit
+ setCell(selectedX, selectedY, editBuffer)
+ editing = false
+ editBuffer = ""
+ selectedY = selectedY + 1
+ elseif key == "\b" then
+ if #editBuffer > 0 then
+ editBuffer = editBuffer:sub(1, -2)
+ end
+ elseif scancode == 1 then -- Escape
+ editing = false
+ editBuffer = ""
+ elseif key and #key == 1 and key:byte() >= 32 then
+ editBuffer = editBuffer .. key
+ end
+ else
+ -- Navigation
+ local moved = false
+ if key == "\n" then
+ -- Start editing
+ editing = true
+ editBuffer = getCell(selectedX, selectedY)
+ elseif key == "\t" then
+ selectedX = selectedX + 1
+ moved = true
+ elseif scancode == 72 then -- Up arrow
+ if selectedY > 1 then selectedY = selectedY - 1 end
+ moved = true
+ elseif scancode == 80 then -- Down arrow
+ selectedY = selectedY + 1
+ moved = true
+ elseif scancode == 75 then -- Left arrow
+ if selectedX > 1 then selectedX = selectedX - 1 end
+ moved = true
+ elseif scancode == 77 then -- Right arrow
+ selectedX = selectedX + 1
+ moved = true
+ elseif key == "\b" then
+ -- Delete cell content
+ setCell(selectedX, selectedY, "")
+ elseif key and #key == 1 and key:byte() >= 32 then
+ -- Start typing immediately
+ editing = true
+ editBuffer = key
+ end
+
+ -- Scroll to keep selection visible
+ if moved then
+ ensureSelectionVisible(window.width, window.height)
+ end
+ end
+ window:markDirty()
+end
+
+-- Click handler
+window.onClick = function(mx, my)
+ -- Save current edit before doing anything else
+ if editing then
+ setCell(selectedX, selectedY, editBuffer)
+ editing = false
+ editBuffer = ""
+ end
+
+ -- Save formula bar edit
+ if formulaBarActive then
+ if formulaBuffer ~= "" then
+ setCell(selectedX, selectedY, "%=" .. formulaBuffer)
+ end
+ formulaBarActive = false
+ formulaBuffer = ""
+ end
+
+ -- Check toolbar buttons
+ if my < toolbarHeight then
+ for _, btn in ipairs(buttons) do
+ if isInside(mx, my, btn.x, btn.y, btn.w, btn.h) then
+ btn.action()
+ return
+ end
+ end
+
+ -- Check formula bar click (from formulaBarX to end of window)
+ local formulaBarW = window.width - formulaBarX - 5
+ if isInside(mx, my, formulaBarX, formulaBarY, formulaBarW, formulaBarH) then
+ formulaBarActive = true
+ -- Load existing formula if cell has one
+ local cellVal = getCell(selectedX, selectedY)
+ if type(cellVal) == "string" and cellVal:sub(1, 2) == "%=" then
+ formulaBuffer = cellVal:sub(3)
+ else
+ formulaBuffer = ""
+ end
+ window:markDirty()
+ return
+ end
+
+ window:markDirty()
+ return
+ end
+
+ -- Check column headers (for future: resize)
+ if my < toolbarHeight + headerHeight then
+ window:markDirty()
+ return
+ end
+
+ -- Check row headers
+ if mx < rowHeaderWidth then
+ local row = getRowAtY(my)
+ if row then
+ selectedY = row
+ selectedX = 1
+ window:markDirty()
+ end
+ return
+ end
+
+ -- Click on cell
+ local col = getColumnAtX(mx)
+ local row = getRowAtY(my)
+
+ if col and row then
+ if selectedX == col and selectedY == row then
+ -- Double-click to edit (single click already selected)
+ editing = true
+ editBuffer = getCell(selectedX, selectedY)
+ else
+ selectedX = col
+ selectedY = row
+ end
+ window:markDirty()
+ end
+end
+
+-- Scroll handler
+window.onScroll = function(delta)
+ scrollY = math.max(0, scrollY - delta * rowHeight * 2)
+ window:markDirty()
+end
+
+-- Draw handler
+window.onDraw = function(gfx)
+ local winW = window.width
+ local winH = window.height
+
+ -- Background
+ gfx:fillRect(0, 0, winW, winH, 0xFFFFFF)
+
+ -- Toolbar
+ gfx:fillRect(0, 0, winW, toolbarHeight, 0xE0E0E0)
+ for _, btn in ipairs(buttons) do
+ gfx:fillRect(btn.x, btn.y, btn.w, btn.h, 0xD0D0D0)
+ gfx:drawRect(btn.x, btn.y, btn.w, btn.h, 0x888888)
+ gfx:drawText(btn.x + 4, btn.y + 4, btn.label, 0x000000)
+ end
+
+ -- Formula bar
+ local formulaBarW = winW - formulaBarX - 5
+ local fBarBgColor = formulaBarActive and 0xFFFFFF or 0xFAFAFA
+ local fBarBorderColor = formulaBarActive and 0x0066CC or 0x888888
+ gfx:fillRect(formulaBarX, formulaBarY, formulaBarW, formulaBarH, fBarBgColor)
+ gfx:drawRect(formulaBarX, formulaBarY, formulaBarW, formulaBarH, fBarBorderColor)
+
+ -- Formula bar content
+ local fBarText = "%="
+ if formulaBarActive then
+ fBarText = fBarText .. formulaBuffer .. "|"
+ else
+ -- Show existing formula if cell has one
+ local cellVal = getCell(selectedX, selectedY)
+ if type(cellVal) == "string" and cellVal:sub(1, 2) == "%=" then
+ fBarText = cellVal
+ else
+ fBarText = "%="
+ end
+ end
+ gfx:drawText(formulaBarX + 3, formulaBarY + 4, fBarText, 0x000000)
+
+ -- Column headers background
+ gfx:fillRect(0, toolbarHeight, winW, headerHeight, 0xD0D0D0)
+
+ -- Row headers background
+ gfx:fillRect(0, toolbarHeight + headerHeight, rowHeaderWidth, winH, 0xD0D0D0)
+
+ -- Corner
+ gfx:fillRect(0, toolbarHeight, rowHeaderWidth, headerHeight, 0xC0C0C0)
+ gfx:drawRect(0, toolbarHeight, rowHeaderWidth, headerHeight, 0x888888)
+
+ -- Calculate visible range
+ local startRow = math.floor(scrollY / rowHeight) + 1
+ local visibleRows = math.ceil((winH - toolbarHeight - headerHeight) / rowHeight) + 1
+ local endRow = startRow + visibleRows
+
+ -- Draw column headers
+ local x = rowHeaderWidth - scrollX
+ local col = 1
+ while x < winW do
+ local colW = getColWidth(col)
+ if x + colW > rowHeaderWidth then
+ local drawX = math.max(rowHeaderWidth, x)
+ local drawW = math.min(colW, x + colW - drawX)
+ if x >= rowHeaderWidth then
+ gfx:drawRect(x, toolbarHeight, colW, headerHeight, 0x888888)
+ gfx:drawText(x + 4, toolbarHeight + 4, colToLetter(col), 0x000000)
+ end
+ end
+ x = x + colW
+ col = col + 1
+ if col > 100 then break end -- Limit columns drawn
+ end
+
+ -- Draw rows
+ for row = startRow, endRow do
+ local y = toolbarHeight + headerHeight + (row - 1) * rowHeight - scrollY
+
+ if y + rowHeight > toolbarHeight + headerHeight and y < winH then
+ -- Row header
+ gfx:drawRect(0, y, rowHeaderWidth, rowHeight, 0x888888)
+ gfx:drawText(4, y + 4, tostring(row), 0x000000)
+
+ -- Cells
+ x = rowHeaderWidth - scrollX
+ col = 1
+ while x < winW do
+ local colW = getColWidth(col)
+
+ if x + colW > rowHeaderWidth then
+ -- Cell background
+ local isSelected = (col == selectedX and row == selectedY)
+ local bgColor = isSelected and 0xCCE5FF or 0xFFFFFF
+
+ local cellX = math.max(rowHeaderWidth, x)
+ local cellW = colW - (cellX - x)
+ if cellX + cellW > winW then cellW = winW - cellX end
+
+ if cellW > 0 then
+ gfx:fillRect(cellX, y, cellW, rowHeight, bgColor)
+ gfx:drawRect(cellX, y, cellW, rowHeight, 0xCCCCCC)
+
+ -- Check if cell has a formula
+ local rawVal = getCell(col, row)
+ local isFormula = type(rawVal) == "string" and rawVal:sub(1, 2) == "%="
+
+ -- Cell content
+ if isSelected and editing then
+ -- Show edit buffer with cursor
+ local displayText = editBuffer .. "|"
+ gfx:drawText(cellX + 2, y + 4, displayText, 0x000000)
+ else
+ local displayVal = getDisplayValue(col, row)
+ if displayVal ~= "" then
+ -- Truncate if too long
+ local maxChars = math.floor((colW - 4) / 7)
+ if #displayVal > maxChars then
+ displayVal = displayVal:sub(1, maxChars - 1) .. "..."
+ end
+ gfx:drawText(cellX + 2, y + 4, displayVal, 0x000000)
+ end
+ end
+
+ -- Formula cell border (light blue)
+ if isFormula and not isSelected then
+ gfx:drawRect(cellX, y, cellW, rowHeight, 0x66AAFF)
+ end
+
+ -- Selection border
+ if isSelected then
+ gfx:drawRect(cellX, y, cellW, rowHeight, 0x0066CC)
+ gfx:drawRect(cellX + 1, y + 1, cellW - 2, rowHeight - 2, 0x0066CC)
+ end
+ end
+ end
+
+ x = x + colW
+ col = col + 1
+ if col > 100 then break end
+ end
+ end
+ end
+
+ -- Grid border
+ gfx:drawRect(0, toolbarHeight, winW, winH - toolbarHeight, 0x888888)
+end
+
+print("Spreadsheet loaded")
diff --git a/iso_includes/apps/com.luajitos.taskbar/manifest.lua b/iso_includes/apps/com.luajitos.taskbar/manifest.lua
@@ -5,7 +5,7 @@ return {
category = "all",
description = "System taskbar with start menu and running applications",
entry = "init.lua",
- type = "gui",
+ type = "taskbar",
hidden = true, -- Hide from start menu
autostart = true, -- Launch on system startup
autostartPriority = 2, -- Launch after background
diff --git a/iso_includes/apps/com.luajitos.taskbar/src/init.lua b/iso_includes/apps/com.luajitos.taskbar/src/init.lua
@@ -24,17 +24,18 @@ local startMenuJustClosed = false -- Track if menu was just closed by focus los
local windowPopup = nil
local windowPopupVisible = false
--- Start button dimensions
-local startButtonWidth = 80
-local startButtonHeight = 40
-local startButtonX = 5
-local startButtonY = 5
+-- Start button dimensions (2px margin on each side)
+local startButtonMargin = 2
+local startButtonWidth = 76 -- 80 - 4 (2px on each side)
+local startButtonHeight = taskbarHeight - 4 -- 2px top + 2px bottom
+local startButtonX = startButtonMargin
+local startButtonY = startButtonMargin
-- App button dimensions
local appButtonWidth = 120
local appButtonHeight = 40
local appButtonY = 5
-local appButtonStartX = startButtonX + startButtonWidth + 10
+local appButtonStartX = startButtonX + startButtonWidth + 5
-- Helper: Check if click is inside a rectangle
local function isInside(x, y, rx, ry, rw, rh)
@@ -616,10 +617,10 @@ taskbar.onDraw = function(gfx)
-- Draw start button
gfx:fillRect(startButtonX, startButtonY, startButtonWidth, startButtonHeight, 0x0066CC)
- gfx:drawRect(startButtonX, startButtonY, startButtonWidth, startButtonHeight, 0x0088FF)
- -- Start button text
- gfx:drawUText(startButtonX + 20, startButtonY + 13, "Start", 0xFFFFFF)
+ -- Start button text (centered vertically)
+ local textY = startButtonY + math.floor((startButtonHeight - 8) / 2)
+ gfx:drawUText(startButtonX + 20, textY, "Start", 0xFFFFFF)
-- Draw application buttons
local apps = getRunningApplications()
diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua
@@ -13,6 +13,7 @@ local OP_POLYGON_FILL = 8
local OP_LINE = 9
local OP_TEXT = 10
local OP_IMAGE = 11
+local OP_BUFFER = 12
-- Global cursor state
_G.cursor_state = {
@@ -823,6 +824,10 @@ local function drawAllWindows()
local promptWindow = inPromptMode and promptMode.window or nil
-- Phase 1: Render dirty windows to their buffers
+ if type(_G.window_stack) ~= "table" then
+ osprint("[ERROR] window_stack is not a table, it is: " .. type(_G.window_stack) .. "\n")
+ _G.window_stack = {}
+ end
for i, window in ipairs(_G.window_stack) do
-- In prompt mode, only draw the prompt window
if inPromptMode and window ~= promptWindow then
@@ -1092,6 +1097,38 @@ local function drawAllWindows()
-- This ensures images are drawn in the correct order relative to other elements
draw_ops[#draw_ops + 1] = {OP_IMAGE, image, content_x + x, content_y + y, w, h, 1}
end,
+ drawBuffer = function(self, luaImage, x, y, srcX, srcY, srcW, srcH)
+ -- Handle both method and function call syntax
+ if type(self) == "table" and self.buffer then
+ -- Called as drawBuffer(luaImage, x, y, ...)
+ srcH = srcW
+ srcW = srcY
+ srcY = srcX
+ srcX = y
+ y = x
+ x = luaImage
+ luaImage = self
+ end
+ if not luaImage then return end
+
+ -- Get buffer data - support both Image (.buffer) and ImageBuffer (:getData())
+ local bufferData
+ local bufW, bufH
+ if type(luaImage) == "userdata" then
+ -- ImageBuffer userdata - use getData() method
+ bufferData = luaImage:getData()
+ bufW, bufH = luaImage:getSize()
+ elseif luaImage.buffer then
+ -- Image object with .buffer property
+ bufferData = luaImage.buffer
+ bufW, bufH = luaImage:getSize()
+ else
+ return
+ end
+
+ -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH}
+ draw_ops[#draw_ops + 1] = {OP_BUFFER, bufferData, content_x + x, content_y + y, bufW, bufH, srcX or 0, srcY or 0, srcW or 0, srcH or 0}
+ end,
getWidth = function(self)
return window.width
end,
@@ -1205,13 +1242,24 @@ local function drawAllWindows()
adjusted_op[5] = op[5]
adjusted_op[6] = op[6]
elseif op[1] == OP_IMAGE then
- -- {4, image, x, y, w, h, scale}
+ -- {11, image, x, y, w, h, scale}
adjusted_op[2] = op[2]
adjusted_op[3] = content_x + op[3]
adjusted_op[4] = content_y + op[4]
adjusted_op[5] = op[5]
adjusted_op[6] = op[6]
adjusted_op[7] = op[7]
+ elseif op[1] == OP_BUFFER then
+ -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH}
+ adjusted_op[2] = op[2] -- buffer string
+ adjusted_op[3] = content_x + op[3] -- x
+ adjusted_op[4] = content_y + op[4] -- y
+ adjusted_op[5] = op[5] -- bufWidth
+ adjusted_op[6] = op[6] -- bufHeight
+ adjusted_op[7] = op[7] -- srcX
+ adjusted_op[8] = op[8] -- srcY
+ adjusted_op[9] = op[9] -- srcW
+ adjusted_op[10] = op[10] -- srcH
end
if not skipNormalAdd then
@@ -1323,7 +1371,8 @@ _G.resize_start_win_w = _G.resize_start_win_w or 0
_G.resize_start_win_h = _G.resize_start_win_h or 0
_G.force_redraw = true -- Force initial redraw
-function MainDraw()
+-- Internal MainDraw implementation
+local function MainDrawImpl()
local redraw_interval = 1 -- Redraw every frame for smooth mouse cursor
-- Update Timer system
@@ -1956,6 +2005,22 @@ function MainDraw()
_G.last_cursor_x = nil
_G.last_cursor_y = nil
end
+end
-
+-- Main entry point with error handling
+function MainDraw()
+ local success, err = pcall(MainDrawImpl)
+ if not success then
+ -- Print error to serial console
+ if osprint then
+ osprint("\n[FATAL ERROR in MainDraw]\n")
+ osprint(tostring(err) .. "\n")
+ osprint("Stack trace:\n")
+ osprint(debug.traceback() .. "\n")
+ end
+ -- Try to recover by resetting window_stack if corrupted
+ if type(_G.window_stack) ~= "table" then
+ _G.window_stack = {}
+ end
+ end
end
diff --git a/iso_includes/os/libs/Application.lua b/iso_includes/os/libs/Application.lua
@@ -856,6 +856,28 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
table.insert(gfxSelf._window._draw_ops, {0, x, y, r, g, b})
end,
+ -- Draw a raw BGRA buffer (from Image object) directly
+ -- luaImage: Image object with .buffer and :getSize()
+ -- x, y: destination position
+ -- srcX, srcY, srcW, srcH: optional source region (defaults to full image)
+ drawBuffer = function(gfxSelf, luaImage, x, y, srcX, srcY, srcW, srcH)
+ if type(gfxSelf) ~= "table" or not gfxSelf._window then
+ -- Called without self, shift args
+ srcH = srcW
+ srcW = srcY
+ srcY = srcX
+ srcX = y
+ y = x
+ x = luaImage
+ luaImage = gfxSelf
+ gfxSelf = window.gfx
+ end
+ if not luaImage or not luaImage.buffer then return end
+ local bufW, bufH = luaImage:getSize()
+ -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH}
+ table.insert(gfxSelf._window._draw_ops, {12, luaImage.buffer, x, y, bufW, bufH, srcX or 0, srcY or 0, srcW or 0, srcH or 0})
+ end,
+
getWidth = function(gfxSelf)
if type(gfxSelf) == "table" and gfxSelf._window then
return gfxSelf._window.width
diff --git a/iso_includes/os/libs/Dialog.lua b/iso_includes/os/libs/Dialog.lua
@@ -1953,7 +1953,7 @@ function Dialog.html(html, options)
title = title,
close = function() dialog:close() end
}
- }, { __index = _G, __metatable = env }) -- __metatable returns caller's env
+ }, { __index = _G, __metatable = false }) -- Prevent metatable access
-- Draw callback
dialog.window.onDraw = function(gfx)
@@ -2021,4 +2021,406 @@ function Dialog.html(html, options)
return dialog
end
+-- Custom Dialog
+-- Creates a dialog with dynamic content based on arguments
+-- Usage: Dialog.customDialog("Label", "STRING", "\n", "Age:", "NUMBER", "\n", "Agree?", "BOOLEAN", "\n", "BUTTON=Cancel", "BUTTON=Ok", callback)
+-- @param ... Variable arguments: strings are labels, "STRING"/"NUMBER"/"BOOLEAN" are input types,
+-- "\n" is a line break, "BUTTON=Label" creates a button
+-- @param callback The last argument must be a callback function that receives all input values followed by button label
+-- @param options Optional table with app, title, width (can be passed as second-to-last arg before callback)
+function Dialog.customDialog(...)
+ local args = {...}
+ local callback = nil
+ local options = {}
+
+ -- Last arg must be callback
+ if type(args[#args]) == "function" then
+ callback = table.remove(args)
+ else
+ error("Dialog.customDialog: last argument must be a callback function")
+ end
+
+ -- Check if second-to-last is options table
+ if type(args[#args]) == "table" and args[#args].app then
+ options = table.remove(args)
+ end
+
+ local app_instance = options.app or app
+ if not app_instance then
+ error("Dialog.customDialog requires app instance (provide options.app or use from sandboxed context)")
+ end
+
+ local title = options.title or "Dialog"
+
+ -- Parse arguments to build UI elements
+ local elements = {} -- {type, label, value, x, y, w, h}
+ local inputs = {} -- Track input elements for collecting values
+ local buttons = {} -- Track button elements
+
+ local cursorX = 10
+ local cursorY = 20
+ local lineHeight = 30
+ local inputHeight = 22
+ local checkboxSize = 18
+ local buttonHeight = 25
+ local buttonWidth = 80
+ local padding = 10
+
+ local maxWidth = 300
+ local maxY = cursorY
+
+ for i, arg in ipairs(args) do
+ if arg == "\n" then
+ -- Line break
+ cursorX = 10
+ cursorY = cursorY + lineHeight
+ elseif arg == "STRING" then
+ -- String input field
+ local inputWidth = 150
+ local input = {
+ type = "string",
+ x = cursorX,
+ y = cursorY,
+ w = inputWidth,
+ h = inputHeight,
+ value = "",
+ active = false
+ }
+ table.insert(elements, input)
+ table.insert(inputs, input)
+ cursorX = cursorX + inputWidth + padding
+ if cursorX > maxWidth then maxWidth = cursorX end
+ elseif arg == "NUMBER" then
+ -- Number input field
+ local inputWidth = 80
+ local input = {
+ type = "number",
+ x = cursorX,
+ y = cursorY,
+ w = inputWidth,
+ h = inputHeight,
+ value = "",
+ active = false
+ }
+ table.insert(elements, input)
+ table.insert(inputs, input)
+ cursorX = cursorX + inputWidth + padding
+ if cursorX > maxWidth then maxWidth = cursorX end
+ elseif arg == "PASSWORD" then
+ -- Password input field (masked with asterisks)
+ local inputWidth = 150
+ local input = {
+ type = "password",
+ x = cursorX,
+ y = cursorY,
+ w = inputWidth,
+ h = inputHeight,
+ value = "",
+ active = false
+ }
+ table.insert(elements, input)
+ table.insert(inputs, input)
+ cursorX = cursorX + inputWidth + padding
+ if cursorX > maxWidth then maxWidth = cursorX end
+ elseif arg == "BOOLEAN" then
+ -- Checkbox
+ local input = {
+ type = "boolean",
+ x = cursorX,
+ y = cursorY + 2,
+ w = checkboxSize,
+ h = checkboxSize,
+ value = false
+ }
+ table.insert(elements, input)
+ table.insert(inputs, input)
+ cursorX = cursorX + checkboxSize + padding
+ if cursorX > maxWidth then maxWidth = cursorX end
+ elseif type(arg) == "string" and arg:sub(1, 7) == "BUTTON=" then
+ -- Button
+ local label = arg:sub(8)
+ local btn = {
+ type = "button",
+ label = label,
+ x = 0, -- Will be positioned later
+ y = 0,
+ w = buttonWidth,
+ h = buttonHeight
+ }
+ table.insert(buttons, btn)
+ elseif type(arg) == "string" then
+ -- Text label
+ local textWidth = #arg * 7 + 5 -- Approximate width
+ local label = {
+ type = "label",
+ text = arg,
+ x = cursorX,
+ y = cursorY + 3,
+ w = textWidth,
+ h = lineHeight
+ }
+ table.insert(elements, label)
+ cursorX = cursorX + textWidth + 5
+ if cursorX > maxWidth then maxWidth = cursorX end
+ end
+
+ if cursorY + lineHeight > maxY then
+ maxY = cursorY + lineHeight
+ end
+ end
+
+ -- Position buttons at the bottom
+ cursorY = maxY + 10
+ local totalButtonWidth = #buttons * buttonWidth + (#buttons - 1) * padding
+ local buttonStartX = math.floor((maxWidth - totalButtonWidth) / 2)
+ if buttonStartX < 10 then buttonStartX = 10 end
+
+ for i, btn in ipairs(buttons) do
+ btn.x = buttonStartX + (i - 1) * (buttonWidth + padding)
+ btn.y = cursorY
+ table.insert(elements, btn)
+ end
+
+ -- Calculate dialog size
+ local width = options.width or math.max(maxWidth + 20, 200)
+ local height = cursorY + buttonHeight + 20
+
+ -- Create dialog object
+ local dialog = {
+ app = app_instance,
+ window = nil,
+ elements = elements,
+ inputs = inputs,
+ buttons = buttons,
+ activeInput = nil,
+ callback = callback
+ }
+
+ -- Collect input values and call callback
+ -- If callback returns true, keep dialog open
+ -- If callback returns a number, clear that input index and focus it
+ function dialog:submit(buttonLabel)
+ local values = {}
+ for _, input in ipairs(self.inputs) do
+ if input.type == "string" or input.type == "password" then
+ table.insert(values, input.value)
+ elseif input.type == "number" then
+ table.insert(values, tonumber(input.value) or 0)
+ elseif input.type == "boolean" then
+ table.insert(values, input.value)
+ end
+ end
+ table.insert(values, buttonLabel)
+
+ local result = nil
+ if self.callback then
+ result = self.callback(unpack(values))
+ end
+
+ if result == true then
+ -- Keep dialog open, do nothing
+ return
+ elseif type(result) == "number" then
+ -- Clear the input at that index and focus it
+ local inputIndex = result
+ if inputIndex >= 1 and inputIndex <= #self.inputs then
+ local input = self.inputs[inputIndex]
+ -- Clear value
+ if input.type == "boolean" then
+ input.value = false
+ else
+ input.value = ""
+ end
+ -- Deactivate current input
+ if self.activeInput then
+ self.activeInput.active = false
+ end
+ -- Focus the specified input
+ input.active = true
+ self.activeInput = input
+ if self.window then
+ self.window:markDirty()
+ end
+ end
+ return
+ end
+
+ -- Default: close the dialog
+ if self.window then
+ self.window:close()
+ end
+ end
+
+ -- Close method
+ function dialog:close()
+ if self.window then
+ self.window:close()
+ self.window = nil
+ end
+ end
+
+ -- Show method
+ function dialog:show()
+ if self.window then return self end
+
+ -- Create window
+ self.window = self.app:newWindow(title, width, height)
+
+ -- Draw callback
+ self.window.onDraw = function(gfx)
+ -- Background
+ gfx:fillRect(0, 0, width, height, 0xF0F0F0)
+
+ -- Draw all elements
+ for _, elem in ipairs(self.elements) do
+ if elem.type == "label" then
+ gfx:drawText(elem.x, elem.y, elem.text, 0x000000)
+
+ elseif elem.type == "string" or elem.type == "number" or elem.type == "password" then
+ -- Input field background
+ local bgColor = elem.active and 0xFFFFFF or 0xFAFAFA
+ local borderColor = elem.active and 0x0066CC or 0x888888
+ gfx:fillRect(elem.x, elem.y, elem.w, elem.h, bgColor)
+ gfx:drawRect(elem.x, elem.y, elem.w, elem.h, borderColor)
+ -- Text (show asterisks for password)
+ local displayText
+ if elem.type == "password" then
+ displayText = string.rep("*", #elem.value)
+ else
+ displayText = elem.value
+ end
+ if elem.active then
+ displayText = displayText .. "|"
+ end
+ gfx:drawText(elem.x + 3, elem.y + 4, displayText, 0x000000)
+
+ elseif elem.type == "boolean" then
+ -- Checkbox
+ gfx:fillRect(elem.x, elem.y, elem.w, elem.h, 0xFFFFFF)
+ gfx:drawRect(elem.x, elem.y, elem.w, elem.h, 0x666666)
+ if elem.value then
+ -- Draw checkmark
+ local cx, cy = elem.x + elem.w/2, elem.y + elem.h/2
+ gfx:fillRect(elem.x + 4, elem.y + 4, elem.w - 8, elem.h - 8, 0x0066CC)
+ end
+
+ elseif elem.type == "button" then
+ -- Button
+ gfx:fillRect(elem.x, elem.y, elem.w, elem.h, 0xDDDDDD)
+ gfx:drawRect(elem.x, elem.y, elem.w, elem.h, 0x888888)
+ local textX = elem.x + math.floor((elem.w - #elem.label * 7) / 2)
+ local textY = elem.y + 6
+ gfx:drawText(textX, textY, elem.label, 0x000000)
+ end
+ end
+ end
+
+ -- Key handler
+ self.window.onKey = function(key)
+ if self.activeInput then
+ local input = self.activeInput
+ if key == "\b" then
+ -- Backspace
+ if #input.value > 0 then
+ input.value = input.value:sub(1, -2)
+ self.window:markDirty()
+ end
+ elseif key == "\n" then
+ -- Enter - deactivate input
+ input.active = false
+ self.activeInput = nil
+ self.window:markDirty()
+ elseif key == "\t" then
+ -- Tab - move to next input
+ input.active = false
+ local found = false
+ for i, inp in ipairs(self.inputs) do
+ if found and (inp.type == "string" or inp.type == "number" or inp.type == "password") then
+ inp.active = true
+ self.activeInput = inp
+ self.window:markDirty()
+ break
+ end
+ if inp == input then
+ found = true
+ end
+ end
+ if self.activeInput == input then
+ -- Wrap around
+ for i, inp in ipairs(self.inputs) do
+ if inp.type == "string" or inp.type == "number" or inp.type == "password" then
+ inp.active = true
+ self.activeInput = inp
+ self.window:markDirty()
+ break
+ end
+ end
+ end
+ elseif #key == 1 then
+ -- Regular character
+ if input.type == "number" then
+ -- Only allow digits, minus, dot
+ if key:match("[%d%.%-]") then
+ input.value = input.value .. key
+ self.window:markDirty()
+ end
+ else
+ input.value = input.value .. key
+ self.window:markDirty()
+ end
+ end
+ end
+ end
+
+ -- Click handler
+ self.window.onClick = function(mx, my)
+ -- Deactivate current input
+ if self.activeInput then
+ self.activeInput.active = false
+ self.activeInput = nil
+ end
+
+ -- Check all elements
+ for _, elem in ipairs(self.elements) do
+ if mx >= elem.x and mx < elem.x + elem.w and
+ my >= elem.y and my < elem.y + elem.h then
+
+ if elem.type == "string" or elem.type == "number" or elem.type == "password" then
+ -- Activate input
+ elem.active = true
+ self.activeInput = elem
+ self.window:markDirty()
+ return
+
+ elseif elem.type == "boolean" then
+ -- Toggle checkbox
+ elem.value = not elem.value
+ self.window:markDirty()
+ return
+
+ elseif elem.type == "button" then
+ -- Button clicked - submit with button label
+ self:submit(elem.label)
+ return
+ end
+ end
+ end
+
+ self.window:markDirty()
+ end
+
+ return self
+ end
+
+ -- openDialog method (convenience)
+ function dialog:openDialog(cb)
+ if cb then
+ self.callback = cb
+ end
+ return self:show()
+ end
+
+ return dialog
+end
+
return Dialog
diff --git a/iso_includes/os/libs/HTMLWindow.lua b/iso_includes/os/libs/HTMLWindow.lua
@@ -96,7 +96,7 @@ local function loadModules()
end
-- Compile and execute DOM module
- local domEnv = setmetatable({}, { __index = _G })
+ local domEnv = setmetatable({}, { __index = _G, __metatable = false })
local domFunc, domErr = loadstring(domCode, "dom")
if not domFunc then
return false, "Failed to compile DOM: " .. tostring(domErr)
@@ -115,7 +115,7 @@ local function loadModules()
end
return nil
end
- }, { __index = _G })
+ }, { __index = _G, __metatable = false })
local parserFunc, parserErr = loadstring(parserCode, "parser")
if not parserFunc then
@@ -129,7 +129,7 @@ local function loadModules()
parserModule = parserResult
-- Compile and execute renderer
- local renderEnv = setmetatable({}, { __index = _G })
+ local renderEnv = setmetatable({}, { __index = _G, __metatable = false })
local renderFunc, renderErr = loadstring(renderCode, "render")
if not renderFunc then
return false, "Failed to compile renderer: " .. tostring(renderErr)
diff --git a/iso_includes/os/libs/Image.lua b/iso_includes/os/libs/Image.lua
@@ -291,35 +291,210 @@ function ImageObj:getPixel(x, y)
}
end
--- Set pixel from RGBA table
+-- Set pixel color using C function for efficiency
-- @param x X coordinate (0-based)
-- @param y Y coordinate (0-based)
--- @param rgba Table with r, g, b, a fields
-function ImageObj:setPixel(x, y, rgba)
+-- @param r_or_rgba Red value (0-255) or table {r, g, b, a}
+-- @param g Green value (0-255), optional if first arg is table
+-- @param b Blue value (0-255), optional if first arg is table
+-- @param a Alpha value (0-255), optional, defaults to 255
+function ImageObj:setPixel(x, y, r_or_rgba, g, b, a)
checkPermission()
- if not x or not y or not rgba then
- error("setPixel requires x, y, and rgba table")
+ if x == nil or y == nil or r_or_rgba == nil then
+ error("setPixel requires x, y, and color")
end
- if x < 0 or x >= self.width or y < 0 or y >= self.height then
- return -- Silently ignore out of bounds
+ local r, g_val, b_val, a_val
+ if type(r_or_rgba) == "table" then
+ r = r_or_rgba.r or r_or_rgba[1] or 0
+ g_val = r_or_rgba.g or r_or_rgba[2] or 0
+ b_val = r_or_rgba.b or r_or_rgba[3] or 0
+ a_val = r_or_rgba.a or r_or_rgba[4] or 255
+ else
+ r = r_or_rgba or 0
+ g_val = g or 0
+ b_val = b or 0
+ a_val = a or 255
end
- local r = rgba.r or rgba[1] or 0
- local g = rgba.g or rgba[2] or 0
- local b = rgba.b or rgba[3] or 0
- local a = rgba.a or rgba[4] or 255
+ -- Use C function for efficiency
+ if BufferSetPixel then
+ self.buffer = BufferSetPixel(self.buffer, self.width, self.height, x, y, r, g_val, b_val, a_val)
+ return
+ end
+
+ -- Fallback to Lua implementation
+ if x < 0 or x >= self.width or y < 0 or y >= self.height then
+ return
+ end
local pixelIndex = y * self.width + x
local byteOffset = pixelIndex * 4 + 1
+ local newBytes = string.char(b_val, g_val, r, a_val)
+ self.buffer = self.buffer:sub(1, byteOffset - 1) .. newBytes .. self.buffer:sub(byteOffset + 4)
+end
- -- Write BGRA bytes
- local newBytes = string.char(b, g, r, a)
+-- Fill a circle with a color (uses C for efficiency)
+-- @param cx Center X coordinate
+-- @param cy Center Y coordinate
+-- @param radius Circle radius
+-- @param color Color as number (0xRRGGBB or 0xAARRGGBB) or table {r, g, b, a}
+function ImageObj:fillCircle(cx, cy, radius, color)
+ checkPermission()
- self.buffer = self.buffer:sub(1, byteOffset - 1) ..
- newBytes ..
- self.buffer:sub(byteOffset + 4)
+ local r, g, b, a = 0, 0, 0, 255
+ if type(color) == "table" then
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
+ elseif type(color) == "number" then
+ if color <= 0xFFFFFF then
+ r = bit.rshift(bit.band(color, 0xFF0000), 16)
+ g = bit.rshift(bit.band(color, 0x00FF00), 8)
+ b = bit.band(color, 0x0000FF)
+ else
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
+ end
+ end
+
+ -- Use in-place version (no buffer copy, modifies directly)
+ if BufferFillCircleInplace then
+ BufferFillCircleInplace(self.buffer, self.width, self.height, cx, cy, radius, r, g, b, a)
+ elseif BufferFillCircle then
+ self.buffer = BufferFillCircle(self.buffer, self.width, self.height, cx, cy, radius, r, g, b, a)
+ else
+ -- Fallback to Lua
+ local r2 = radius * radius
+ for dy = -radius, radius do
+ for dx = -radius, radius do
+ if dx*dx + dy*dy <= r2 then
+ self:setPixel(cx + dx, cy + dy, r, g, b, a)
+ end
+ end
+ end
+ end
+end
+
+-- Draw a line with variable thickness (uses C for efficiency)
+-- @param x1 Start X
+-- @param y1 Start Y
+-- @param x2 End X
+-- @param y2 End Y
+-- @param thickness Line thickness in pixels
+-- @param color Color as number or table
+function ImageObj:drawLine(x1, y1, x2, y2, thickness, color)
+ checkPermission()
+
+ local r, g, b, a = 0, 0, 0, 255
+ if type(color) == "table" then
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
+ elseif type(color) == "number" then
+ if color <= 0xFFFFFF then
+ r = bit.rshift(bit.band(color, 0xFF0000), 16)
+ g = bit.rshift(bit.band(color, 0x00FF00), 8)
+ b = bit.band(color, 0x0000FF)
+ else
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
+ end
+ end
+
+ -- Use in-place version (no buffer copy, modifies directly)
+ if BufferDrawLineInplace then
+ BufferDrawLineInplace(self.buffer, self.width, self.height, x1, y1, x2, y2, thickness, r, g, b, a)
+ elseif BufferDrawLine then
+ self.buffer = BufferDrawLine(self.buffer, self.width, self.height, x1, y1, x2, y2, thickness, r, g, b, a)
+ else
+ -- Fallback: just draw circles along the line
+ local dx = math.abs(x2 - x1)
+ local dy = math.abs(y2 - y1)
+ local sx = x1 < x2 and 1 or -1
+ local sy = y1 < y2 and 1 or -1
+ local err = dx - dy
+ local radius = math.floor(thickness / 2)
+
+ while true do
+ self:fillCircle(x1, y1, radius, {r, g, b, a})
+ if x1 == x2 and y1 == y2 then break end
+ local e2 = 2 * err
+ if e2 > -dy then err = err - dy; x1 = x1 + sx end
+ if e2 < dx then err = err + dx; y1 = y1 + sy end
+ end
+ end
+end
+
+-- Fill a rectangle with a color (uses C for efficiency)
+-- @param x Top-left X
+-- @param y Top-left Y
+-- @param w Width
+-- @param h Height
+-- @param color Color as number or table
+function ImageObj:fillRect(x, y, w, h, color)
+ checkPermission()
+
+ local r, g, b, a = 0, 0, 0, 255
+ if type(color) == "table" then
+ r = color.r or color[1] or 0
+ g = color.g or color[2] or 0
+ b = color.b or color[3] or 0
+ a = color.a or color[4] or 255
+ elseif type(color) == "number" then
+ if color <= 0xFFFFFF then
+ r = bit.rshift(bit.band(color, 0xFF0000), 16)
+ g = bit.rshift(bit.band(color, 0x00FF00), 8)
+ b = bit.band(color, 0x0000FF)
+ else
+ a = bit.rshift(bit.band(color, 0xFF000000), 24)
+ r = bit.rshift(bit.band(color, 0x00FF0000), 16)
+ g = bit.rshift(bit.band(color, 0x0000FF00), 8)
+ b = bit.band(color, 0x000000FF)
+ end
+ end
+
+ if BufferFillRect then
+ self.buffer = BufferFillRect(self.buffer, self.width, self.height, x, y, w, h, r, g, b, a)
+ else
+ -- Fallback to Lua
+ for py = y, y + h - 1 do
+ for px = x, x + w - 1 do
+ self:setPixel(px, py, r, g, b, a)
+ end
+ end
+ end
+end
+
+-- Blit a row of BGRA data at the specified y position
+-- @param y Y coordinate
+-- @param rowData Binary string of BGRA pixels
+-- @param startX Starting X position (optional, defaults to 0)
+function ImageObj:blitRow(y, rowData, startX)
+ checkPermission()
+ startX = startX or 0
+
+ if BufferBlitRow then
+ self.buffer = BufferBlitRow(self.buffer, self.width, self.height, y, rowData, startX)
+ else
+ -- Fallback: copy pixel by pixel
+ local pixels = #rowData / 4
+ for i = 0, pixels - 1 do
+ local offset = i * 4 + 1
+ local b = rowData:byte(offset)
+ local g = rowData:byte(offset + 1)
+ local r = rowData:byte(offset + 2)
+ local a = rowData:byte(offset + 3)
+ self:setPixel(startX + i, y, r, g, b, a)
+ end
+ end
end
-- Fill entire image with a color
@@ -362,10 +537,14 @@ function ImageObj:fill(color)
end
end
- -- Create single pixel in BGRA format
- local pixel = string.char(b, g, r, a)
+ -- Use C function if available
+ if BufferFill then
+ self.buffer = BufferFill(self.buffer, self.width, self.height, r, g, b, a)
+ return
+ end
- -- Fill buffer efficiently using string.rep
+ -- Fallback: Create single pixel in BGRA format and repeat
+ local pixel = string.char(b, g, r, a)
local numPixels = self.width * self.height
self.buffer = string.rep(pixel, numPixels)
end
diff --git a/iso_includes/os/libs/Run.lua b/iso_includes/os/libs/Run.lua
@@ -794,7 +794,7 @@ function run.execute(app_name, fsRoot)
local app_env = {
sys = _G.sys -- Explicitly provide sys to Application.lua
}
- setmetatable(app_env, {__index = _G})
+ setmetatable(app_env, {__index = _G, __metatable = false})
local app_func, err = load(app_module_code, app_module_path, "t", app_env)
if app_func then
@@ -1218,7 +1218,7 @@ function run.execute(app_name, fsRoot)
if safefs_code then
-- Load SafeFS in a temporary environment
local safefs_env = {}
- setmetatable(safefs_env, {__index = _G})
+ setmetatable(safefs_env, {__index = _G, __metatable = false})
local safefs_func, err = load(safefs_code, safefs_path, "t", safefs_env)
if safefs_func then
@@ -1492,7 +1492,7 @@ function run.execute(app_name, fsRoot)
if scheduler_code then
-- Load scheduler in a temporary environment
local scheduler_env = {}
- setmetatable(scheduler_env, {__index = _G})
+ setmetatable(scheduler_env, {__index = _G, __metatable = false})
local scheduler_func, err = load(scheduler_code, scheduler_path, "t", scheduler_env)
if scheduler_func then
@@ -1778,6 +1778,26 @@ function run.execute(app_name, fsRoot)
end
end
+ -- Check for setfenv permission
+ local has_setfenv = false
+ for _, perm in ipairs(permissions) do
+ if perm == "setfenv" then
+ has_setfenv = true
+ break
+ end
+ end
+
+ if has_setfenv then
+ -- Grant access to setfenv for environment manipulation
+ -- Use direct reference (available in Run.lua context) rather than _G
+ sandbox_env.setfenv = setfenv
+ sandbox_env.getfenv = getfenv
+
+ if osprint then
+ osprint("Setfenv permission granted - setfenv/getfenv functions available\n")
+ end
+ end
+
-- Check for system permission and setup system information API
local has_system = false
for _, perm in ipairs(permissions) do
@@ -1890,6 +1910,11 @@ function run.execute(app_name, fsRoot)
is_permitted = true
end
+ -- Check if it's setfenv/getfenv (setfenv permission)
+ if (key == "setfenv" or key == "getfenv") and has_setfenv then
+ is_permitted = true
+ end
+
-- Remove if not permitted
if not is_permitted then
sandbox_env[key] = nil
@@ -2111,6 +2136,9 @@ function run.execute(app_name, fsRoot)
allowed_keys.ImageDrawScaled = true
allowed_keys.ImageDestroy = true
allowed_keys.ImageGetInfo = true
+ allowed_keys.setfenv = true
+ allowed_keys.getfenv = true
+ allowed_keys.loadstring = true
setmetatable(sandbox_env, {
__index = function(t, k)
diff --git a/iso_includes/os/libs/Sys.lua b/iso_includes/os/libs/Sys.lua
@@ -1151,7 +1151,7 @@ sys.browser = {
return nil, "HTMLWindow module not found"
end
- local env = setmetatable({}, { __index = _G })
+ local env = setmetatable({}, { __index = _G, __metatable = false })
local func, err = loadstring(htmlWindowCode, "HTMLWindow")
if not func then
return nil, "Failed to compile HTMLWindow: " .. tostring(err)
diff --git a/iso_includes/os/postinit.lua b/iso_includes/os/postinit.lua
@@ -422,18 +422,30 @@ local function parse_manifest_simple(manifest_code)
manifest.autostart = true
end
+ -- Check for autorun = true (runs at end of startup)
+ if manifest_code:match("autorun%s*=%s*true") then
+ manifest.autorun = true
+ end
+
-- Check for autostartPriority = <number>
local priority = manifest_code:match("autostartPriority%s*=%s*(%d+)")
if priority then
manifest.autostartPriority = tonumber(priority)
end
+ -- Check for type (gui, cli, background, taskbar, service)
+ local app_type = manifest_code:match('type%s*=%s*"([^"]+)"')
+ if app_type then
+ manifest.type = app_type
+ end
+
return manifest
end
--- Scan all apps for autostart field and launch them
-osprint("Scanning apps for autostart...\n")
+-- Scan all apps for autostart and autorun fields
+osprint("Scanning apps for autostart/autorun...\n")
local autostart_apps = {}
+local autorun_apps = {}
if CRamdiskList and CRamdiskOpen and CRamdiskRead and CRamdiskClose then
local apps = CRamdiskList("/apps")
@@ -450,12 +462,25 @@ if CRamdiskList and CRamdiskOpen and CRamdiskRead and CRamdiskClose then
if manifest_code then
local manifest = parse_manifest_simple(manifest_code)
+
+ -- Check for autostart (background, taskbar, early gui apps)
if manifest.autostart then
table.insert(autostart_apps, {
id = app_id,
- priority = manifest.autostartPriority or 100
+ priority = manifest.autostartPriority or 100,
+ type = manifest.type or "gui"
})
- osprint(" Found autostart app: " .. app_id .. " (priority: " .. (manifest.autostartPriority or 100) .. ")\n")
+ osprint(" Found autostart app: " .. app_id .. " (priority: " .. (manifest.autostartPriority or 100) .. ", type: " .. (manifest.type or "gui") .. ")\n")
+ end
+
+ -- Check for autorun (services that run at end of startup)
+ if manifest.autorun then
+ table.insert(autorun_apps, {
+ id = app_id,
+ priority = manifest.autostartPriority or 100,
+ type = manifest.type or "service"
+ })
+ osprint(" Found autorun app: " .. app_id .. " (type: " .. (manifest.type or "service") .. ")\n")
end
end
end
@@ -469,7 +494,11 @@ table.sort(autostart_apps, function(a, b)
return a.priority < b.priority
end)
--- Launch autostart apps in priority order
+table.sort(autorun_apps, function(a, b)
+ return a.priority < b.priority
+end)
+
+-- Launch autostart apps in priority order (background, taskbar first)
osprint("Launching " .. #autostart_apps .. " autostart app(s)...\n")
for _, app_info in ipairs(autostart_apps) do
osprint("Launching autostart app: " .. app_info.id .. "...\n")
@@ -481,4 +510,16 @@ for _, app_info in ipairs(autostart_apps) do
end
end
+-- Launch autorun apps at end of startup (services, libraries)
+osprint("Launching " .. #autorun_apps .. " autorun app(s)...\n")
+for _, app_info in ipairs(autorun_apps) do
+ osprint("Launching autorun app: " .. app_info.id .. "...\n")
+ local success, app = _G.run.execute(app_info.id, _G.fsRoot)
+ if success and app then
+ osprint(" " .. app_info.id .. " launched with PID: " .. tostring(app.pid) .. "\n")
+ else
+ osprint(" ERROR: Failed to launch " .. app_info.id .. "\n")
+ end
+end
+
osprint("Post-init complete\n")
diff --git a/kernel.c b/kernel.c
@@ -785,6 +785,7 @@ void usermode_function(void) {
/* Initialize crypto library */
terminal_writestring("Initializing crypto library...\n");
luaopen_crypto(L);
+ lua_setglobal(L, "crypto");
terminal_writestring("Crypto library loaded!\n");
/* Initialize ATA driver and Lua bindings */
@@ -1068,6 +1069,43 @@ void usermode_function(void) {
lua_pushcfunction(L, lua_image_get_buffer_bgra);
lua_setglobal(L, "ImageGetBufferBGRA");
+ /* Buffer drawing functions for Image library */
+ extern int lua_buffer_create(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_create);
+ lua_setglobal(L, "BufferCreate");
+
+ extern int lua_buffer_set_pixel(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_set_pixel);
+ lua_setglobal(L, "BufferSetPixel");
+
+ extern int lua_buffer_fill_circle(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_fill_circle);
+ lua_setglobal(L, "BufferFillCircle");
+
+ extern int lua_buffer_draw_line(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_draw_line);
+ lua_setglobal(L, "BufferDrawLine");
+
+ extern int lua_buffer_fill_rect(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_fill_rect);
+ lua_setglobal(L, "BufferFillRect");
+
+ extern int lua_buffer_fill(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_fill);
+ lua_setglobal(L, "BufferFill");
+
+ extern int lua_buffer_blit_row(lua_State* L);
+ lua_pushcfunction(L, lua_buffer_blit_row);
+ lua_setglobal(L, "BufferBlitRow");
+
+ /* ImageBuffer - mutable userdata buffer for efficient drawing */
+ extern void lua_imagebuffer_register(lua_State* L);
+ lua_imagebuffer_register(L);
+
+ extern int lua_imagebuffer_new(lua_State* L);
+ lua_pushcfunction(L, lua_imagebuffer_new);
+ lua_setglobal(L, "ImageBufferNew");
+
/* Test simple Lua execution first */
terminal_writestring("Testing simple Lua expression...\n");
const char* simple_test = "osprint('Simple test works!\\n')";
diff --git a/vesa.c b/vesa.c
@@ -689,6 +689,7 @@ int lua_vesa_process_buffered_draw_ops(lua_State* L) {
#define OP_LINE 9
#define OP_TEXT 10
#define OP_IMAGE 11
+ #define OP_BUFFER 12
if (!vesa_state.active) return 0;
@@ -885,6 +886,73 @@ int lua_vesa_process_buffered_draw_ops(lua_State* L) {
}
break;
}
+
+ case OP_BUFFER: {
+ /* {BUFFER, buffer_string, x, y, width, height, src_x, src_y, src_w, src_h} */
+ /* Draws a raw BGRA buffer (from Lua Image) directly to the render target */
+ /* If src_x/y/w/h are provided, draws only that region of the buffer */
+ lua_rawgeti(L, -1, 2);
+ size_t buf_len;
+ const char* buf = lua_tolstring(L, -1, &buf_len);
+ lua_pop(L, 1);
+
+ lua_rawgeti(L, -1, 3); int dst_x = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 4); int dst_y = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 5); int buf_width = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 6); int buf_height = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 7); int src_x = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 8); int src_y = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 9); int src_w = lua_tointeger(L, -1); lua_pop(L, 1);
+ lua_rawgeti(L, -1, 10); int src_h = lua_tointeger(L, -1); lua_pop(L, 1);
+
+ if (!buf || buf_width <= 0 || buf_height <= 0) break;
+
+ /* Default to full buffer if src region not specified */
+ if (src_w <= 0) src_w = buf_width;
+ if (src_h <= 0) src_h = buf_height;
+
+ /* Calculate expected buffer size (4 bytes per pixel - BGRA) */
+ size_t expected_size = (size_t)buf_width * (size_t)buf_height * 4;
+ if (buf_len < expected_size) break;
+
+ /* Must be rendering to a window buffer (not screen) */
+ if (!current_render_target) break;
+
+ int target_width = current_render_target->width;
+ int target_height = current_render_target->height;
+ uint32_t* target_pixels = current_render_target->pixels;
+
+ /* Copy pixels from buffer to render target */
+ const uint8_t* src = (const uint8_t*)buf;
+
+ for (int row = 0; row < src_h; row++) {
+ int screen_y = dst_y + row;
+ int buf_row = src_y + row;
+
+ if (screen_y < 0 || screen_y >= target_height) continue;
+ if (buf_row < 0 || buf_row >= buf_height) continue;
+
+ for (int col = 0; col < src_w; col++) {
+ int screen_x = dst_x + col;
+ int buf_col = src_x + col;
+
+ if (screen_x < 0 || screen_x >= target_width) continue;
+ if (buf_col < 0 || buf_col >= buf_width) continue;
+
+ /* Get pixel from buffer (BGRA format) */
+ size_t buf_offset = ((size_t)buf_row * buf_width + buf_col) * 4;
+ uint8_t b = src[buf_offset];
+ uint8_t g = src[buf_offset + 1];
+ uint8_t r = src[buf_offset + 2];
+ /* uint8_t a = src[buf_offset + 3]; - alpha ignored for now */
+
+ /* Write to render target (ARGB format) */
+ uint32_t color = (0xFF << 24) | (r << 16) | (g << 8) | b;
+ target_pixels[screen_y * target_width + screen_x] = color;
+ }
+ }
+ break;
+ }
}
lua_pop(L, 1); /* Pop operation table */