init.lua (24333B)
1 -- LuaJIT OS Spreadsheet Application 2 -- Infinite spreadsheet with CSV support and formula evaluation 3 4 local toolbarHeight = 30 5 local headerHeight = 20 6 local rowHeight = 22 7 local defaultColWidth = 80 8 local rowHeaderWidth = 40 9 10 local initialWidth = 600 11 local initialHeight = 400 12 13 local window = app:newWindow("Spreadsheet", initialWidth, initialHeight) 14 window.resizable = true 15 16 -- Spreadsheet data: rows[y] = {cells...}, grows as needed 17 local rows = {} 18 local colWidths = {} -- Custom column widths 19 20 -- Selection 21 local selectedX = 1 22 local selectedY = 1 23 local editing = false 24 local editBuffer = "" 25 26 -- Formula bar 27 local formulaBarActive = false 28 local formulaBuffer = "" 29 30 -- Scroll position 31 local scrollX = 0 32 local scrollY = 0 33 34 -- Current file 35 local currentFile = nil 36 37 -- Forward declaration 38 local ensureSelectionVisible 39 40 -- Get or create a row 41 local function getRow(y) 42 if not rows[y] then 43 rows[y] = {} 44 end 45 return rows[y] 46 end 47 48 -- Get cell value 49 local function getCell(x, y) 50 local row = rows[y] 51 if row then 52 return row[x] or "" 53 end 54 return "" 55 end 56 57 -- Set cell value 58 local function setCell(x, y, value) 59 local row = getRow(y) 60 row[x] = value 61 end 62 63 -- Get column width 64 local function getColWidth(x) 65 return colWidths[x] or defaultColWidth 66 end 67 68 -- Ensure selected cell is visible (scroll if needed) 69 ensureSelectionVisible = function(winW, winH) 70 -- Calculate cell position 71 local cellX = rowHeaderWidth 72 for col = 1, selectedX - 1 do 73 cellX = cellX + getColWidth(col) 74 end 75 local cellW = getColWidth(selectedX) 76 77 -- Horizontal scrolling 78 local visibleLeft = scrollX 79 local visibleRight = scrollX + (winW - rowHeaderWidth) 80 81 if cellX - rowHeaderWidth < visibleLeft then 82 -- Cell is too far left, scroll left 83 scrollX = cellX - rowHeaderWidth 84 elseif cellX - rowHeaderWidth + cellW > visibleRight then 85 -- Cell is too far right, scroll right 86 scrollX = cellX - rowHeaderWidth + cellW - (winW - rowHeaderWidth) 87 end 88 89 -- Vertical scrolling 90 local visibleTop = scrollY 91 local visibleBottom = scrollY + (winH - toolbarHeight - headerHeight) 92 local cellTop = (selectedY - 1) * rowHeight 93 local cellBottom = cellTop + rowHeight 94 95 if cellTop < visibleTop then 96 -- Cell is above view, scroll up 97 scrollY = cellTop 98 elseif cellBottom > visibleBottom then 99 -- Cell is below view, scroll down 100 scrollY = cellBottom - (winH - toolbarHeight - headerHeight) 101 end 102 103 -- Clamp scroll values 104 if scrollX < 0 then scrollX = 0 end 105 if scrollY < 0 then scrollY = 0 end 106 end 107 108 -- Convert column number to letter (1=A, 2=B, ..., 27=AA) 109 local function colToLetter(n) 110 local result = "" 111 while n > 0 do 112 n = n - 1 113 result = string.char(65 + (n % 26)) .. result 114 n = math.floor(n / 26) 115 end 116 return result 117 end 118 119 -- Convert column letter to number (A=1, B=2, ..., AA=27) 120 local function letterToCol(letters) 121 local result = 0 122 for i = 1, #letters do 123 result = result * 26 + (letters:byte(i) - 64) 124 end 125 return result 126 end 127 128 -- Expand range notation [A1-C4] to list of IN() calls 129 local function expandRange(formula) 130 return formula:gsub("%[([A-Z]+)(%d+)%-([A-Z]+)(%d+)%]", function(col1Str, row1Str, col2Str, row2Str) 131 local col1 = letterToCol(col1Str) 132 local row1 = tonumber(row1Str) 133 local col2 = letterToCol(col2Str) 134 local row2 = tonumber(row2Str) 135 local cells = {} 136 -- Iterate rows first, then columns 137 for row = row1, row2 do 138 for col = col1, col2 do 139 table.insert(cells, "IN(" .. col .. "," .. row .. ")") 140 end 141 end 142 return table.concat(cells, ", ") 143 end) 144 end 145 146 -- Convert cell references like A4, BC12 to IN(col, row) 147 local function preprocessFormula(formula) 148 -- First expand range notation [A1-C4] 149 formula = expandRange(formula) 150 151 -- Then replace cell references A1, BC23, etc. with IN(col, row) 152 -- Use frontier pattern %f to ensure we don't match inside words like SUM, AVG, etc. 153 -- Match cell refs that are preceded by non-letter or start of string 154 local result = "" 155 local i = 1 156 while i <= #formula do 157 -- Check if we're at a potential cell reference 158 local col, row, endPos = formula:match("^([A-Z]+)(%d+)()", i) 159 if col then 160 -- Check if preceded by a letter (would be part of function name) 161 local prevChar = i > 1 and formula:sub(i-1, i-1) or "" 162 if prevChar:match("[A-Za-z]") then 163 -- Part of a function name, don't convert 164 result = result .. col 165 i = i + #col 166 else 167 -- Standalone cell reference, convert it 168 local colNum = letterToCol(col) 169 result = result .. "IN(" .. colNum .. "," .. row .. ")" 170 i = endPos 171 end 172 else 173 result = result .. formula:sub(i, i) 174 i = i + 1 175 end 176 end 177 return result 178 end 179 180 -- Formula sandbox environment 181 local function createFormulaSandbox() 182 local sandbox = {} 183 184 -- IN(x, y) - get cell value at position 185 sandbox.IN = function(x, y) 186 local val = getCell(x, y) 187 -- Try to convert to number 188 local num = tonumber(val) 189 if num then return num end 190 -- Check if it's a formula result 191 if type(val) == "string" and val:sub(1, 2) == "%=" then 192 -- Evaluate nested formula 193 local result = evaluateFormula(val) 194 return result 195 end 196 return val 197 end 198 199 -- SELECT(x, y, direction, length) - get multiple cells 200 sandbox.SELECT = function(x, y, direction, length) 201 local results = {} 202 local dx, dy = 0, 0 203 if direction == "up" then dy = -1 204 elseif direction == "down" then dy = 1 205 elseif direction == "left" then dx = -1 206 elseif direction == "right" then dx = 1 207 end 208 209 for i = 0, length - 1 do 210 local cx = x + dx * i 211 local cy = y + dy * i 212 local val = sandbox.IN(cx, cy) 213 table.insert(results, val) 214 end 215 return unpack(results) 216 end 217 218 -- SUM(...) - sum all numeric arguments 219 sandbox.SUM = function(...) 220 local args = {...} 221 local total = 0 222 for _, v in ipairs(args) do 223 local num = tonumber(v) 224 if num then 225 total = total + num 226 end 227 end 228 return total 229 end 230 231 -- CONCAT(...) - concatenate strings 232 sandbox.CONCAT = function(...) 233 local args = {...} 234 local result = "" 235 for _, v in ipairs(args) do 236 result = result .. tostring(v) 237 end 238 return result 239 end 240 241 -- REPEAT(str, count, separator) 242 sandbox.REPEAT = function(str, count, separator) 243 separator = separator or "" 244 local parts = {} 245 for i = 1, count do 246 table.insert(parts, str) 247 end 248 return table.concat(parts, separator) 249 end 250 251 -- AVG(...) - average of numeric arguments 252 sandbox.AVG = function(...) 253 local args = {...} 254 local total = 0 255 local count = 0 256 for _, v in ipairs(args) do 257 local num = tonumber(v) 258 if num then 259 total = total + num 260 count = count + 1 261 end 262 end 263 if count == 0 then return 0 end 264 return total / count 265 end 266 267 -- MIN/MAX 268 sandbox.MIN = function(...) 269 local args = {...} 270 local result = nil 271 for _, v in ipairs(args) do 272 local num = tonumber(v) 273 if num then 274 if result == nil or num < result then 275 result = num 276 end 277 end 278 end 279 return result or 0 280 end 281 282 sandbox.MAX = function(...) 283 local args = {...} 284 local result = nil 285 for _, v in ipairs(args) do 286 local num = tonumber(v) 287 if num then 288 if result == nil or num > result then 289 result = num 290 end 291 end 292 end 293 return result or 0 294 end 295 296 -- Basic math 297 sandbox.math = math 298 sandbox.tostring = tostring 299 sandbox.tonumber = tonumber 300 sandbox.type = type 301 302 return sandbox 303 end 304 305 -- Evaluate a formula 306 local function evaluateFormula(formula) 307 if type(formula) ~= "string" or formula:sub(1, 2) ~= "%=" then 308 return formula 309 end 310 311 local code = formula:sub(3) -- Remove %= 312 313 -- Preprocess: convert cell references (A1, BC23) to IN(col, row) 314 code = preprocessFormula(code) 315 316 local sandbox = createFormulaSandbox() 317 318 -- Try to compile the formula 319 local fn, err = loadstring("return " .. code) 320 if not fn then 321 return "#ERR: " .. tostring(err) 322 end 323 324 -- Set environment to sandbox 325 setfenv(fn, sandbox) 326 327 local ok, result = pcall(fn) 328 if not ok then 329 return "#ERR: " .. tostring(result) 330 end 331 332 return result 333 end 334 335 -- Get display value for a cell (evaluates formulas) 336 local function getDisplayValue(x, y) 337 local val = getCell(x, y) 338 if type(val) == "string" and val:sub(1, 2) == "%=" then 339 return tostring(evaluateFormula(val)) 340 end 341 return tostring(val) 342 end 343 344 -- Parse CSV line 345 local function parseCSVLine(line) 346 local cells = {} 347 local current = "" 348 local inQuotes = false 349 350 for i = 1, #line do 351 local c = line:sub(i, i) 352 if c == '"' then 353 inQuotes = not inQuotes 354 elseif c == ',' and not inQuotes then 355 table.insert(cells, current) 356 current = "" 357 else 358 current = current .. c 359 end 360 end 361 table.insert(cells, current) 362 363 return cells 364 end 365 366 -- Load CSV file 367 local function loadCSV(path) 368 local data = fs:read(path) 369 if not data then return false end 370 371 rows = {} 372 colWidths = {} 373 374 local y = 1 375 for line in data:gmatch("[^\r\n]+") do 376 local cells = parseCSVLine(line) 377 rows[y] = {} 378 for x, val in ipairs(cells) do 379 rows[y][x] = val 380 end 381 y = y + 1 382 end 383 384 currentFile = path 385 selectedX = 1 386 selectedY = 1 387 scrollX = 0 388 scrollY = 0 389 window:markDirty() 390 return true 391 end 392 393 -- Generate CSV content 394 local function generateCSV() 395 local maxX = 0 396 local maxY = 0 397 398 -- Find bounds 399 for y, row in pairs(rows) do 400 if y > maxY then maxY = y end 401 for x, _ in pairs(row) do 402 if x > maxX then maxX = x end 403 end 404 end 405 406 local lines = {} 407 for y = 1, maxY do 408 local cells = {} 409 for x = 1, maxX do 410 local val = tostring(getCell(x, y) or "") 411 -- Escape commas and quotes 412 if val:find('[,"\n]') then 413 val = '"' .. val:gsub('"', '""') .. '"' 414 end 415 table.insert(cells, val) 416 end 417 table.insert(lines, table.concat(cells, ",")) 418 end 419 420 return table.concat(lines, "\n") 421 end 422 423 -- Save CSV file 424 local function saveCSV(path) 425 local content = generateCSV() 426 local ok = fs:write(path, content) 427 if ok then 428 currentFile = path 429 end 430 return ok 431 end 432 433 -- Clear spreadsheet 434 local function newSpreadsheet() 435 rows = {} 436 colWidths = {} 437 currentFile = nil 438 selectedX = 1 439 selectedY = 1 440 scrollX = 0 441 scrollY = 0 442 editing = false 443 editBuffer = "" 444 window:markDirty() 445 end 446 447 -- Formula bar dimensions 448 local formulaBarX = 145 449 local formulaBarY = 5 450 local formulaBarH = 20 451 452 -- Button definitions 453 local buttons = { 454 {x = 5, y = 5, w = 40, h = 20, label = "New", action = newSpreadsheet}, 455 {x = 50, y = 5, w = 45, h = 20, label = "Open", action = function() 456 local dlg = Dialog.fileOpen("/", { 457 app = app, 458 fs = fs, 459 title = "Open CSV", 460 filter = {"csv"} 461 }) 462 dlg:openDialog(function(path) 463 if path then 464 loadCSV(path) 465 end 466 end) 467 end}, 468 {x = 100, y = 5, w = 40, h = 20, label = "Save", action = function() 469 local defaultName = "spreadsheet.csv" 470 if currentFile then 471 defaultName = currentFile:match("([^/]+)$") or defaultName 472 end 473 local dlg = Dialog.fileSave("/home", defaultName, { 474 app = app, 475 fs = fs, 476 title = "Save CSV" 477 }) 478 dlg:openDialog(function(path) 479 if path then 480 if not path:match("%.csv$") then 481 path = path .. ".csv" 482 end 483 saveCSV(path) 484 end 485 end) 486 end} 487 } 488 489 -- Helper: check if point is inside rect 490 local function isInside(px, py, x, y, w, h) 491 return px >= x and px < x + w and py >= y and py < y + h 492 end 493 494 -- Get column at screen X position 495 local function getColumnAtX(screenX) 496 local x = rowHeaderWidth - scrollX 497 local col = 1 498 while x < screenX do 499 x = x + getColWidth(col) 500 if x >= screenX then 501 return col 502 end 503 col = col + 1 504 if col > 1000 then break end -- Safety limit 505 end 506 return col 507 end 508 509 -- Get row at screen Y position 510 local function getRowAtY(screenY) 511 local contentY = screenY - toolbarHeight - headerHeight 512 if contentY < 0 then return nil end 513 local row = math.floor((contentY + scrollY) / rowHeight) + 1 514 return row 515 end 516 517 -- Get X position for column 518 local function getColumnX(col) 519 local x = rowHeaderWidth 520 for c = 1, col - 1 do 521 x = x + getColWidth(c) 522 end 523 return x - scrollX 524 end 525 526 -- Key handler (onInput for keyboard input) 527 window.onInput = function(key, scancode) 528 -- Scancodes: up=72, down=80, left=75, right=77, escape=1 529 530 if formulaBarActive then 531 -- Formula bar input 532 if key == "\n" then 533 -- Confirm formula - prepend %= and set cell 534 if formulaBuffer ~= "" then 535 setCell(selectedX, selectedY, "%=" .. formulaBuffer) 536 end 537 formulaBarActive = false 538 formulaBuffer = "" 539 elseif key == "\b" then 540 if #formulaBuffer > 0 then 541 formulaBuffer = formulaBuffer:sub(1, -2) 542 end 543 elseif scancode == 1 then -- Escape 544 formulaBarActive = false 545 formulaBuffer = "" 546 elseif key and #key == 1 and key:byte() >= 32 then 547 formulaBuffer = formulaBuffer .. key 548 end 549 elseif editing then 550 if key == "\n" then 551 -- Confirm edit 552 setCell(selectedX, selectedY, editBuffer) 553 editing = false 554 editBuffer = "" 555 selectedY = selectedY + 1 556 elseif key == "\b" then 557 if #editBuffer > 0 then 558 editBuffer = editBuffer:sub(1, -2) 559 end 560 elseif scancode == 1 then -- Escape 561 editing = false 562 editBuffer = "" 563 elseif key and #key == 1 and key:byte() >= 32 then 564 editBuffer = editBuffer .. key 565 end 566 else 567 -- Navigation 568 local moved = false 569 if key == "\n" then 570 -- Start editing 571 editing = true 572 editBuffer = getCell(selectedX, selectedY) 573 elseif key == "\t" then 574 selectedX = selectedX + 1 575 moved = true 576 elseif scancode == 72 then -- Up arrow 577 if selectedY > 1 then selectedY = selectedY - 1 end 578 moved = true 579 elseif scancode == 80 then -- Down arrow 580 selectedY = selectedY + 1 581 moved = true 582 elseif scancode == 75 then -- Left arrow 583 if selectedX > 1 then selectedX = selectedX - 1 end 584 moved = true 585 elseif scancode == 77 then -- Right arrow 586 selectedX = selectedX + 1 587 moved = true 588 elseif key == "\b" then 589 -- Delete cell content 590 setCell(selectedX, selectedY, "") 591 elseif key and #key == 1 and key:byte() >= 32 then 592 -- Start typing immediately 593 editing = true 594 editBuffer = key 595 end 596 597 -- Scroll to keep selection visible 598 if moved then 599 ensureSelectionVisible(window.width, window.height) 600 end 601 end 602 window:markDirty() 603 end 604 605 -- Click handler 606 window.onClick = function(mx, my) 607 -- Save current edit before doing anything else 608 if editing then 609 setCell(selectedX, selectedY, editBuffer) 610 editing = false 611 editBuffer = "" 612 end 613 614 -- Save formula bar edit 615 if formulaBarActive then 616 if formulaBuffer ~= "" then 617 setCell(selectedX, selectedY, "%=" .. formulaBuffer) 618 end 619 formulaBarActive = false 620 formulaBuffer = "" 621 end 622 623 -- Check toolbar buttons 624 if my < toolbarHeight then 625 for _, btn in ipairs(buttons) do 626 if isInside(mx, my, btn.x, btn.y, btn.w, btn.h) then 627 btn.action() 628 return 629 end 630 end 631 632 -- Check formula bar click (from formulaBarX to end of window) 633 local formulaBarW = window.width - formulaBarX - 5 634 if isInside(mx, my, formulaBarX, formulaBarY, formulaBarW, formulaBarH) then 635 formulaBarActive = true 636 -- Load existing formula if cell has one 637 local cellVal = getCell(selectedX, selectedY) 638 if type(cellVal) == "string" and cellVal:sub(1, 2) == "%=" then 639 formulaBuffer = cellVal:sub(3) 640 else 641 formulaBuffer = "" 642 end 643 window:markDirty() 644 return 645 end 646 647 window:markDirty() 648 return 649 end 650 651 -- Check column headers (for future: resize) 652 if my < toolbarHeight + headerHeight then 653 window:markDirty() 654 return 655 end 656 657 -- Check row headers 658 if mx < rowHeaderWidth then 659 local row = getRowAtY(my) 660 if row then 661 selectedY = row 662 selectedX = 1 663 window:markDirty() 664 end 665 return 666 end 667 668 -- Click on cell 669 local col = getColumnAtX(mx) 670 local row = getRowAtY(my) 671 672 if col and row then 673 if selectedX == col and selectedY == row then 674 -- Double-click to edit (single click already selected) 675 editing = true 676 editBuffer = getCell(selectedX, selectedY) 677 else 678 selectedX = col 679 selectedY = row 680 end 681 window:markDirty() 682 end 683 end 684 685 -- Scroll handler 686 window.onScroll = function(delta) 687 scrollY = math.max(0, scrollY - delta * rowHeight * 2) 688 window:markDirty() 689 end 690 691 -- Draw handler 692 window.onDraw = function(gfx) 693 local winW = window.width 694 local winH = window.height 695 696 -- Background 697 gfx:fillRect(0, 0, winW, winH, 0xFFFFFF) 698 699 -- Toolbar 700 gfx:fillRect(0, 0, winW, toolbarHeight, 0xE0E0E0) 701 for _, btn in ipairs(buttons) do 702 gfx:fillRect(btn.x, btn.y, btn.w, btn.h, 0xD0D0D0) 703 gfx:drawRect(btn.x, btn.y, btn.w, btn.h, 0x888888) 704 gfx:drawText(btn.x + 4, btn.y + 4, btn.label, 0x000000) 705 end 706 707 -- Formula bar 708 local formulaBarW = winW - formulaBarX - 5 709 local fBarBgColor = formulaBarActive and 0xFFFFFF or 0xFAFAFA 710 local fBarBorderColor = formulaBarActive and 0x0066CC or 0x888888 711 gfx:fillRect(formulaBarX, formulaBarY, formulaBarW, formulaBarH, fBarBgColor) 712 gfx:drawRect(formulaBarX, formulaBarY, formulaBarW, formulaBarH, fBarBorderColor) 713 714 -- Formula bar content 715 local fBarText = "%=" 716 if formulaBarActive then 717 fBarText = fBarText .. formulaBuffer .. "|" 718 else 719 -- Show existing formula if cell has one 720 local cellVal = getCell(selectedX, selectedY) 721 if type(cellVal) == "string" and cellVal:sub(1, 2) == "%=" then 722 fBarText = cellVal 723 else 724 fBarText = "%=" 725 end 726 end 727 gfx:drawText(formulaBarX + 3, formulaBarY + 4, fBarText, 0x000000) 728 729 -- Column headers background 730 gfx:fillRect(0, toolbarHeight, winW, headerHeight, 0xD0D0D0) 731 732 -- Row headers background 733 gfx:fillRect(0, toolbarHeight + headerHeight, rowHeaderWidth, winH, 0xD0D0D0) 734 735 -- Corner 736 gfx:fillRect(0, toolbarHeight, rowHeaderWidth, headerHeight, 0xC0C0C0) 737 gfx:drawRect(0, toolbarHeight, rowHeaderWidth, headerHeight, 0x888888) 738 739 -- Calculate visible range 740 local startRow = math.floor(scrollY / rowHeight) + 1 741 local visibleRows = math.ceil((winH - toolbarHeight - headerHeight) / rowHeight) + 1 742 local endRow = startRow + visibleRows 743 744 -- Draw column headers 745 local x = rowHeaderWidth - scrollX 746 local col = 1 747 while x < winW do 748 local colW = getColWidth(col) 749 if x + colW > rowHeaderWidth then 750 local drawX = math.max(rowHeaderWidth, x) 751 local drawW = math.min(colW, x + colW - drawX) 752 if x >= rowHeaderWidth then 753 gfx:drawRect(x, toolbarHeight, colW, headerHeight, 0x888888) 754 gfx:drawText(x + 4, toolbarHeight + 4, colToLetter(col), 0x000000) 755 end 756 end 757 x = x + colW 758 col = col + 1 759 if col > 100 then break end -- Limit columns drawn 760 end 761 762 -- Draw rows 763 for row = startRow, endRow do 764 local y = toolbarHeight + headerHeight + (row - 1) * rowHeight - scrollY 765 766 if y + rowHeight > toolbarHeight + headerHeight and y < winH then 767 -- Row header 768 gfx:drawRect(0, y, rowHeaderWidth, rowHeight, 0x888888) 769 gfx:drawText(4, y + 4, tostring(row), 0x000000) 770 771 -- Cells 772 x = rowHeaderWidth - scrollX 773 col = 1 774 while x < winW do 775 local colW = getColWidth(col) 776 777 if x + colW > rowHeaderWidth then 778 -- Cell background 779 local isSelected = (col == selectedX and row == selectedY) 780 local bgColor = isSelected and 0xCCE5FF or 0xFFFFFF 781 782 local cellX = math.max(rowHeaderWidth, x) 783 local cellW = colW - (cellX - x) 784 if cellX + cellW > winW then cellW = winW - cellX end 785 786 if cellW > 0 then 787 gfx:fillRect(cellX, y, cellW, rowHeight, bgColor) 788 gfx:drawRect(cellX, y, cellW, rowHeight, 0xCCCCCC) 789 790 -- Check if cell has a formula 791 local rawVal = getCell(col, row) 792 local isFormula = type(rawVal) == "string" and rawVal:sub(1, 2) == "%=" 793 794 -- Cell content 795 if isSelected and editing then 796 -- Show edit buffer with cursor 797 local displayText = editBuffer .. "|" 798 gfx:drawText(cellX + 2, y + 4, displayText, 0x000000) 799 else 800 local displayVal = getDisplayValue(col, row) 801 if displayVal ~= "" then 802 -- Truncate if too long 803 local maxChars = math.floor((colW - 4) / 7) 804 if #displayVal > maxChars then 805 displayVal = displayVal:sub(1, maxChars - 1) .. "..." 806 end 807 gfx:drawText(cellX + 2, y + 4, displayVal, 0x000000) 808 end 809 end 810 811 -- Formula cell border (light blue) 812 if isFormula and not isSelected then 813 gfx:drawRect(cellX, y, cellW, rowHeight, 0x66AAFF) 814 end 815 816 -- Selection border 817 if isSelected then 818 gfx:drawRect(cellX, y, cellW, rowHeight, 0x0066CC) 819 gfx:drawRect(cellX + 1, y + 1, cellW - 2, rowHeight - 2, 0x0066CC) 820 end 821 end 822 end 823 824 x = x + colW 825 col = col + 1 826 if col > 100 then break end 827 end 828 end 829 end 830 831 -- Grid border 832 gfx:drawRect(0, toolbarHeight, winW, winH - toolbarHeight, 0x888888) 833 end 834 835 print("Spreadsheet loaded")