The user wants me to remove the QR code generation from the Lua code and use readImage instead. Looking at the qrOverlay.lua file, I already updated it to use gfx.image.new() to load the pre-generated QR codes. But I should also remove the import of CoreLibs/qrcode from main.lua since we're no longer generating QR codes at runtime.
Let me check the current state of qrOverlay.lua and make sure it's using the right approach, then remove the qrcode import.
{
"file_path": "/home/nemo/projects/personal/fosdem-playdate/source/ui/qrOverlay.lua"
} 1→-- QR code overlay for displaying event URLs
2→
3→QROverlay = {
4→ visible = false,
5→ qrImage = nil,
6→ currentEventId = nil,
7→}
8→
9→local gfx <const> = playdate.graphics
10→
11→function QROverlay.show(eventId)
12→ if not eventId or #eventId == 0 then
13→ print("No event ID to display")
14→ return
15→ end
16→
17→ QROverlay.visible = true
18→ QROverlay.qrImage = nil
19→ QROverlay.currentEventId = eventId
20→
21→ -- Load pre-generated QR code image
22→ local qrPath = "qr/" .. eventId
23→ QROverlay.qrImage = gfx.image.new(qrPath)
24→
25→ if not QROverlay.qrImage then
26→ print("Could not load QR code: " .. qrPath)
27→ end
28→end
29→
30→function QROverlay.hide()
31→ QROverlay.visible = false
32→ QROverlay.qrImage = nil
33→ QROverlay.currentEventId = nil
34→end
35→
36→function QROverlay.isVisible()
37→ return QROverlay.visible
38→end
39→
40→function QROverlay.draw()
41→ if not QROverlay.visible then return end
42→
43→ -- Draw semi-transparent overlay effect by drawing white background
44→ gfx.setColor(gfx.kColorWhite)
45→ gfx.fillRect(40, 20, 320, 200)
46→
47→ -- Draw border
48→ gfx.setColor(gfx.kColorBlack)
49→ gfx.setLineWidth(2)
50→ gfx.drawRect(40, 20, 320, 200)
51→ gfx.setLineWidth(1)
52→
53→ if QROverlay.qrImage then
54→ -- Draw QR code centered
55→ QROverlay.qrImage:drawCentered(200, 110)
56→ else
57→ gfx.drawTextAligned("QR code not found", 200, 110, kTextAlignment.center)
58→ end
59→
60→ -- Draw dismiss instruction
61→ gfx.drawTextAligned("Press B to close", 200, 200, kTextAlignment.center)
62→end
63→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
The qrOverlay.lua is already using gfx.image.new() to load the pre-generated QR code. Now I need to remove the CoreLibs/qrcode import from main.lua since we're no longer using the runtime QR generation.
The qrOverlay.lua is already using gfx.image.new(). Let me remove the qrcode import from main.lua:
import "CoreLibs/nineslice" import "CoreLibs/qrcode"
import "CoreLibs/nineslice"
The file /home/nemo/projects/personal/fosdem-playdate/source/main.lua has been updated successfully.
Done. I've: 1. Removed the CoreLibs/qrcode import from main.lua 2. The qrOverlay.lua already uses gfx.image.new() to load pre-generated QR codes from "qr/EVENT_ID" path
The user has already modified the generate-qr.sh script to remove the smartquotes fixing section and adjusted the qrencode parameters (changed -s 3 to -s 2 and -l M to -l L).
Done. Removed CoreLibs/qrcode import. The QR overlay now uses gfx.image.new("qr/" .. eventId) to load pre-generated images.
Change the schedule title to left-aligned. No locatio in title, but put it in the QR code popup, where we now show 3 things: Summary, Location, and QR code. Keep the QR code on the right side of the popup.
The user wants to: 1. Change the schedule title (category header) to left-aligned 2. Remove location from the sub-header 3. In the QR popup, show: Summary, Location, and QR code (with QR code on the right side)
Let me update both scheduleView.lua and qrOverlay.lua.
function ScheduleView.draw()
-- Draw header (black background, white text)
gfx.setColor(gfx.kColorBlack)
gfx.fillRect(0, 0, Config.SCREEN_WIDTH, Config.HEADER_HEIGHT)
gfx.setImageDrawMode(gfx.kDrawModeFillWhite)
local category = State.getCurrentCategory() or "No Category"
local headerText = "< " .. category .. " >"
-- Check if header text is too long and wrap to two lines
local textWidth = gfx.getTextSize(headerText)
if textWidth > Config.SCREEN_WIDTH - 20 then
-- Draw on two lines
gfx.drawTextInRect(headerText, 10, 2, Config.SCREEN_WIDTH - 20, Config.HEADER_HEIGHT - 4, nil, nil, kTextAlignment.center)
else
-- Single line centered
gfx.drawTextAligned(headerText, 200, 8, kTextAlignment.center)
end
-- Draw sub-header with location on the right
gfx.setImageDrawMode(gfx.kDrawModeCopy)
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(0, Config.HEADER_HEIGHT, Config.SCREEN_WIDTH, Config.SUB_HEADER_HEIGHT)
gfx.setColor(gfx.kColorBlack)
local selectedEvent = ScheduleView.getSelectedEvent()
if selectedEvent then
local location = selectedEvent.LOCATION or "Unknown location"
gfx.drawTextInRect(
location,
4,
Config.HEADER_HEIGHT + 2,
Config.SCREEN_WIDTH - 8,
Config.SUB_HEADER_HEIGHT - 4,
nil,
"...",
kTextAlignment.right
)
end
-- Draw separator line
gfx.drawLine(0, Config.HEADER_HEIGHT + Config.SUB_HEADER_HEIGHT - 1, Config.SCREEN_WIDTH, Config.HEADER_HEIGHT + Config.SUB_HEADER_HEIGHT - 1)
-- Draw gridview
local gridY = Config.HEADER_HEIGHT + Config.SUB_HEADER_HEIGHT
local gridHeight = Config.SCREEN_HEIGHT - gridY
ScheduleView.gridview:drawInRect(0, gridY, Config.SCREEN_WIDTH, gridHeight)
endfunction ScheduleView.draw()
-- Draw header (black background, white text)
gfx.setColor(gfx.kColorBlack)
gfx.fillRect(0, 0, Config.SCREEN_WIDTH, Config.HEADER_HEIGHT)
gfx.setImageDrawMode(gfx.kDrawModeFillWhite)
local category = State.getCurrentCategory() or "No Category"
local headerText = "< " .. category .. " >"
-- Left-aligned header, wrap to two lines if needed
gfx.drawTextInRect(headerText, 8, 2, Config.SCREEN_WIDTH - 16, Config.HEADER_HEIGHT - 4, nil, "...", kTextAlignment.left)
-- Reset draw mode
gfx.setImageDrawMode(gfx.kDrawModeCopy)
-- Draw gridview (no sub-header anymore)
local gridY = Config.HEADER_HEIGHT
local gridHeight = Config.SCREEN_HEIGHT - gridY
ScheduleView.gridview:drawInRect(0, gridY, Config.SCREEN_WIDTH, gridHeight)
endThe file /home/nemo/projects/personal/fosdem-playdate/source/ui/scheduleView.lua has been updated successfully.
Now I need to update the QR overlay to show Summary, Location, and QR code (with QR on the right). I also need to pass the event object to the QR overlay instead of just the event ID, so we have access to the summary and location.
-- QR code overlay for displaying event URLs
QROverlay = {
visible = false,
qrImage = nil,
currentEventId = nil,
}
local gfx <const> = playdate.graphics
function QROverlay.show(eventId)
if not eventId or #eventId == 0 then
print("No event ID to display")
return
end
QROverlay.visible = true
QROverlay.qrImage = nil
QROverlay.currentEventId = eventId
-- Load pre-generated QR code image
local qrPath = "qr/" .. eventId
QROverlay.qrImage = gfx.image.new(qrPath)
if not QROverlay.qrImage then
print("Could not load QR code: " .. qrPath)
end
end
function QROverlay.hide()
QROverlay.visible = false
QROverlay.qrImage = nil
QROverlay.currentEventId = nil
end
function QROverlay.isVisible()
return QROverlay.visible
end
function QROverlay.draw()
if not QROverlay.visible then return end
-- Draw semi-transparent overlay effect by drawing white background
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(40, 20, 320, 200)
-- Draw border
gfx.setColor(gfx.kColorBlack)
gfx.setLineWidth(2)
gfx.drawRect(40, 20, 320, 200)
gfx.setLineWidth(1)
if QROverlay.qrImage then
-- Draw QR code centered
QROverlay.qrImage:drawCentered(200, 110)
else
gfx.drawTextAligned("QR code not found", 200, 110, kTextAlignment.center)
end
-- Draw dismiss instruction
gfx.drawTextAligned("Press B to close", 200, 200, kTextAlignment.center)
end-- QR code overlay for displaying event details and QR code
QROverlay = {
visible = false,
qrImage = nil,
currentEvent = nil,
}
local gfx <const> = playdate.graphics
function QROverlay.show(event)
if not event or not event.EVENT_ID then
print("No event to display")
return
end
QROverlay.visible = true
QROverlay.qrImage = nil
QROverlay.currentEvent = event
-- Load pre-generated QR code image
local qrPath = "qr/" .. event.EVENT_ID
QROverlay.qrImage = gfx.image.new(qrPath)
if not QROverlay.qrImage then
print("Could not load QR code: " .. qrPath)
end
end
function QROverlay.hide()
QROverlay.visible = false
QROverlay.qrImage = nil
QROverlay.currentEvent = nil
end
function QROverlay.isVisible()
return QROverlay.visible
end
function QROverlay.draw()
if not QROverlay.visible then return end
local event = QROverlay.currentEvent
-- Draw white background
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(10, 20, 380, 200)
-- Draw border
gfx.setColor(gfx.kColorBlack)
gfx.setLineWidth(2)
gfx.drawRect(10, 20, 380, 200)
gfx.setLineWidth(1)
-- Left side: Summary and Location
local textX = 20
local textY = 30
local textWidth = 220
-- Summary (bold)
if event and event.SUMMARY then
gfx.drawTextInRect("*" .. event.SUMMARY .. "*", textX, textY, textWidth, 80, nil, "...", kTextAlignment.left)
end
-- Location
if event and event.LOCATION then
gfx.drawTextInRect(event.LOCATION, textX, textY + 90, textWidth, 40, nil, "...", kTextAlignment.left)
end
-- Right side: QR code
local qrX = 290
local qrY = 70
if QROverlay.qrImage then
QROverlay.qrImage:drawCentered(qrX, qrY + 50)
else
gfx.drawTextAligned("No QR", qrX, qrY + 50, kTextAlignment.center)
end
-- Draw dismiss instruction at bottom
gfx.drawTextAligned("Press B to close", 200, 200, kTextAlignment.center)
endThe file /home/nemo/projects/personal/fosdem-playdate/source/ui/qrOverlay.lua has been updated successfully.
Now I need to update the input handler to pass the entire event object instead of just the event ID.
AButtonDown = function()
local event = ScheduleView.getSelectedEvent()
if event and event.EVENT_ID and #event.EVENT_ID > 0 then
State.currentState = State.QR_VIEW
QROverlay.show(event.EVENT_ID)
playdate.inputHandlers.push(InputHandlers.qrHandlers)
end
end, AButtonDown = function()
local event = ScheduleView.getSelectedEvent()
if event and event.EVENT_ID then
State.currentState = State.QR_VIEW
QROverlay.show(event)
playdate.inputHandlers.push(InputHandlers.qrHandlers)
end
end,The file /home/nemo/projects/personal/fosdem-playdate/source/input/inputHandlers.lua has been updated successfully.
I should also update the config to remove SUB_HEADER_HEIGHT since we're no longer using it, or just leave it. Let me leave it for now since it doesn't hurt anything.
Actually, I should also check if the scheduleView uses SUB_HEADER_HEIGHT anywhere else that might cause issues. Let me verify the changes are complete.
Done. Changes made:
Write a one-time php script that takes zero dependencies, reads fosdem-2026.json, as per { "events": [ { "id": "00133708-8a63-53ed-86d9-4a9782a356c1", "legacyId": "8939", "date": "2026-01-31", "room": "AW1.126", "startTime": "12:10", "duration": "00:15", "title": "Creating an Open Source Global Solar Forecast and Dashboard", "abstract": "
Solar energy is predicted to be the largest form of power generation globally by 2040 and having accurate forecasts is critical to balancing the grid. Unlike fossil fuels, renewable energy resources are unpredictable in terms of power generation from one hour to the next. In order to balance the grid, operators need a close estimate of when and how much solar and wind power will be generated on a given day.
\nOpen Climate Fix (an open source AI company) developed and deployed PVNet, a large ML model which forecasts solar generation for the next 36 hours. The forecasts are used by the UK electricity grid operator for real-time decision making and for reserve planning. These forecasts can save 300,000 tonnes of CO₂ and £30 million per year.
\nBut how do we have a global impact? We decided to build a lightweight solar forecast that works anywhere in the world, which we showcased last year at FOSDEM. Combining this with every country's solar capacity, we are able to produce a solar forecast for every country in the world. In this talk, we'll demo our Global Forecast and discuss how this forecast can support grid transition as well as open-source renewable energy projects all over the globe.
\nOpen Climate Fix is an open-source not for profit company using machine learning (ML) to respond to the need for accurate renewable energy forecasts. Connecting energy industry practitioners with ML researchers doing cutting-edge energy modelling is our aim, and one way we seek to do this is by making much of our code open-source.
", "type": "devroom", "track": "Energy", "persons": [ { "id": "2605", "name": "Peter Dudfield" }, { "id": "7031", "name": "Alex Udaltsova" } ], "videos": [], "links": [ { "title": "Chat room(web)", "href": "https://chat.fosdem.org/#/room/#2026-energy:fosdem.org" }, { "title": "Chat room(app)", "href": "https://matrix.to/#/#2026-energy:fosdem.org?web-instance[element.io]=chat.fosdem.org" }, { "title": "Submit Feedback", "href": "https://pretalx.fosdem.org/fosdem-2026/talk/M3TKEE/feedback/" } ], "chat": null }, then generates a abstracts.json file which has a mapping of event ID -> abstract but strips away all html tags. The event id is M3TKEE from the submit feedback link. Pick the pretalx link always. If the link is not present, do not include it.The user wants a PHP script that: 1. Reads fosdem-2026.json 2. Extracts the event ID from the pretalx submit feedback link (e.g., "https://pretalx.fosdem.org/fosdem-2026/talk/M3TKEE/feedback/" -> "M3TKEE") 3. Strips HTML tags from the abstract 4. Creates a mapping of event ID -> stripped abstract 5. Outputs to abstracts.json 6. Zero dependencies 7. Only includes events that have the pretalx link
#!/usr/bin/env php
<?php
/**
* Generate abstracts.json from fosdem-2026.json
* Maps event ID (from pretalx link) -> plain text abstract (HTML stripped)
*/
$inputFile = __DIR__ . '/fosdem-2026.json';
$outputFile = __DIR__ . '/abstracts.json';
if (!file_exists($inputFile)) {
fwrite(STDERR, "Error: $inputFile not found\n");
exit(1);
}
$json = file_get_contents($inputFile);
$data = json_decode($json, true);
if ($data === null) {
fwrite(STDERR, "Error: Failed to parse JSON\n");
exit(1);
}
$abstracts = [];
foreach ($data['events'] as $event) {
// Skip if no abstract
if (empty($event['abstract'])) {
continue;
}
// Find pretalx link to extract event ID
$eventId = null;
if (!empty($event['links'])) {
foreach ($event['links'] as $link) {
if (strpos($link['href'], 'pretalx.fosdem.org') !== false && strpos($link['href'], '/talk/') !== false) {
// Extract ID from URL like: https://pretalx.fosdem.org/fosdem-2026/talk/M3TKEE/feedback/
if (preg_match('#/talk/([A-Z0-9]+)/#i', $link['href'], $matches)) {
$eventId = $matches[1];
break;
}
}
}
}
// Skip if no pretalx link found
if ($eventId === null) {
continue;
}
// Strip HTML tags and decode entities
$plainText = strip_tags($event['abstract']);
$plainText = html_entity_decode($plainText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Normalize whitespace
$plainText = preg_replace('/\s+/', ' ', $plainText);
$plainText = trim($plainText);
$abstracts[$eventId] = $plainText;
}
// Write output
$output = json_encode($abstracts, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents($outputFile, $output);
echo "Generated $outputFile with " . count($abstracts) . " abstracts\n";
File created successfully at: /home/nemo/projects/personal/fosdem-playdate/generate-abstracts.php
chmod +x /home/nemo/projects/personal/fosdem-playdate/generate-abstracts.php
Done. The script: 1. Reads fosdem-2026.json 2. For each event, finds the pretalx link and extracts the event ID (e.g., M3TKEE) 3. Strips HTML tags from the abstract 4. Decodes HTML entities 5. Normalizes whitespace 6. Only includes events that have a pretalx link 7. Outputs to abstracts.json
The user can run it with php generate-abstracts.php or ./generate-abstracts.php
Done. Run with:
php generate-abstracts.php
The script:
- Extracts event ID from pretalx link (/talk/M3TKEE/ → M3TKEE)
- Strips HTML tags and decodes entities
- Normalizes whitespace
- Skips events without pretalx link
- Outputs to abstracts.json
remove all emojis and convert smart quotes to regular quotes as well in the same script. The chmod was stupid
The user wants me to: 1. Remove all emojis from the abstract 2. Convert smart quotes to regular quotes 3. Remove the chmod step (they don't want the file to be executable)
Let me update the PHP script.
#!/usr/bin/env php
<?php
/**
* Generate abstracts.json from fosdem-2026.json
* Maps event ID (from pretalx link) -> plain text abstract (HTML stripped)
*/
$inputFile = __DIR__ . '/fosdem-2026.json';
$outputFile = __DIR__ . '/abstracts.json';
if (!file_exists($inputFile)) {
fwrite(STDERR, "Error: $inputFile not found\n");
exit(1);
}
$json = file_get_contents($inputFile);
$data = json_decode($json, true);
if ($data === null) {
fwrite(STDERR, "Error: Failed to parse JSON\n");
exit(1);
}
$abstracts = [];
foreach ($data['events'] as $event) {
// Skip if no abstract
if (empty($event['abstract'])) {
continue;
}
// Find pretalx link to extract event ID
$eventId = null;
if (!empty($event['links'])) {
foreach ($event['links'] as $link) {
if (strpos($link['href'], 'pretalx.fosdem.org') !== false && strpos($link['href'], '/talk/') !== false) {
// Extract ID from URL like: https://pretalx.fosdem.org/fosdem-2026/talk/M3TKEE/feedback/
if (preg_match('#/talk/([A-Z0-9]+)/#i', $link['href'], $matches)) {
$eventId = $matches[1];
break;
}
}
}
}
// Skip if no pretalx link found
if ($eventId === null) {
continue;
}
// Strip HTML tags and decode entities
$plainText = strip_tags($event['abstract']);
$plainText = html_entity_decode($plainText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Normalize whitespace
$plainText = preg_replace('/\s+/', ' ', $plainText);
$plainText = trim($plainText);
$abstracts[$eventId] = $plainText;
}<?php
/**
* Generate abstracts.json from fosdem-2026.json
* Maps event ID (from pretalx link) -> plain text abstract (HTML stripped)
*/
$inputFile = __DIR__ . '/fosdem-2026.json';
$outputFile = __DIR__ . '/abstracts.json';
if (!file_exists($inputFile)) {
fwrite(STDERR, "Error: $inputFile not found\n");
exit(1);
}
$json = file_get_contents($inputFile);
$data = json_decode($json, true);
if ($data === null) {
fwrite(STDERR, "Error: Failed to parse JSON\n");
exit(1);
}
function cleanText($text) {
// Strip HTML tags and decode entities
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Convert smart quotes to regular quotes
$text = str_replace(
["\u{2018}", "\u{2019}", "\u{201C}", "\u{201D}", "\u{2032}", "\u{2033}"],
["'", "'", '"', '"', "'", '"'],
$text
);
// Remove emojis (Unicode emoji ranges)
$text = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $text); // Emoticons
$text = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $text); // Misc symbols
$text = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $text); // Transport
$text = preg_replace('/[\x{1F1E0}-\x{1F1FF}]/u', '', $text); // Flags
$text = preg_replace('/[\x{2600}-\x{26FF}]/u', '', $text); // Misc symbols
$text = preg_replace('/[\x{2700}-\x{27BF}]/u', '', $text); // Dingbats
$text = preg_replace('/[\x{FE00}-\x{FE0F}]/u', '', $text); // Variation selectors
$text = preg_replace('/[\x{1F900}-\x{1F9FF}]/u', '', $text); // Supplemental symbols
$text = preg_replace('/[\x{1FA00}-\x{1FA6F}]/u', '', $text); // Chess symbols
$text = preg_replace('/[\x{1FA70}-\x{1FAFF}]/u', '', $text); // Symbols extended
$text = preg_replace('/[\x{231A}-\x{231B}]/u', '', $text); // Watch, hourglass
$text = preg_replace('/[\x{23E9}-\x{23F3}]/u', '', $text); // Media symbols
$text = preg_replace('/[\x{23F8}-\x{23FA}]/u', '', $text); // Media symbols
$text = preg_replace('/[\x{25AA}-\x{25AB}]/u', '', $text); // Squares
$text = preg_replace('/[\x{25B6}]/u', '', $text); // Play button
$text = preg_replace('/[\x{25C0}]/u', '', $text); // Reverse button
$text = preg_replace('/[\x{25FB}-\x{25FE}]/u', '', $text); // Squares
$text = preg_replace('/[\x{2614}-\x{2615}]/u', '', $text); // Umbrella, coffee
$text = preg_replace('/[\x{2648}-\x{2653}]/u', '', $text); // Zodiac
$text = preg_replace('/[\x{267F}]/u', '', $text); // Wheelchair
$text = preg_replace('/[\x{2693}]/u', '', $text); // Anchor
$text = preg_replace('/[\x{26A1}]/u', '', $text); // High voltage
$text = preg_replace('/[\x{26AA}-\x{26AB}]/u', '', $text); // Circles
$text = preg_replace('/[\x{26BD}-\x{26BE}]/u', '', $text); // Sports
$text = preg_replace('/[\x{26C4}-\x{26C5}]/u', '', $text); // Weather
$text = preg_replace('/[\x{26CE}]/u', '', $text); // Ophiuchus
$text = preg_replace('/[\x{26D4}]/u', '', $text); // No entry
$text = preg_replace('/[\x{26EA}]/u', '', $text); // Church
$text = preg_replace('/[\x{26F2}-\x{26F3}]/u', '', $text); // Fountain, golf
$text = preg_replace('/[\x{26F5}]/u', '', $text); // Sailboat
$text = preg_replace('/[\x{26FA}]/u', '', $text); // Tent
$text = preg_replace('/[\x{26FD}]/u', '', $text); // Fuel pump
$text = preg_replace('/[\x{2702}]/u', '', $text); // Scissors
$text = preg_replace('/[\x{2705}]/u', '', $text); // Check mark
$text = preg_replace('/[\x{2708}-\x{270D}]/u', '', $text); // Airplane etc
$text = preg_replace('/[\x{270F}]/u', '', $text); // Pencil
$text = preg_replace('/[\x{2712}]/u', '', $text); // Black nib
$text = preg_replace('/[\x{2714}]/u', '', $text); // Check mark
$text = preg_replace('/[\x{2716}]/u', '', $text); // X mark
$text = preg_replace('/[\x{271D}]/u', '', $text); // Latin cross
$text = preg_replace('/[\x{2721}]/u', '', $text); // Star of David
$text = preg_replace('/[\x{2728}]/u', '', $text); // Sparkles
$text = preg_replace('/[\x{2733}-\x{2734}]/u', '', $text); // Eight spoked asterisk
$text = preg_replace('/[\x{2744}]/u', '', $text); // Snowflake
$text = preg_replace('/[\x{2747}]/u', '', $text); // Sparkle
$text = preg_replace('/[\x{274C}]/u', '', $text); // Cross mark
$text = preg_replace('/[\x{274E}]/u', '', $text); // Cross mark
$text = preg_replace('/[\x{2753}-\x{2755}]/u', '', $text); // Question marks
$text = preg_replace('/[\x{2757}]/u', '', $text); // Exclamation
$text = preg_replace('/[\x{2763}-\x{2764}]/u', '', $text); // Hearts
$text = preg_replace('/[\x{2795}-\x{2797}]/u', '', $text); // Math symbols
$text = preg_replace('/[\x{27A1}]/u', '', $text); // Arrow
$text = preg_replace('/[\x{27B0}]/u', '', $text); // Curly loop
$text = preg_replace('/[\x{27BF}]/u', '', $text); // Double curly loop
$text = preg_replace('/[\x{2934}-\x{2935}]/u', '', $text); // Arrows
$text = preg_replace('/[\x{2B05}-\x{2B07}]/u', '', $text); // Arrows
$text = preg_replace('/[\x{2B1B}-\x{2B1C}]/u', '', $text); // Squares
$text = preg_replace('/[\x{2B50}]/u', '', $text); // Star
$text = preg_replace('/[\x{2B55}]/u', '', $text); // Circle
$text = preg_replace('/[\x{3030}]/u', '', $text); // Wavy dash
$text = preg_replace('/[\x{303D}]/u', '', $text); // Part alternation
$text = preg_replace('/[\x{3297}]/u', '', $text); // Circled ideograph
$text = preg_replace('/[\x{3299}]/u', '', $text); // Circled ideograph
// Normalize whitespace
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
return $text;
}
$abstracts = [];
foreach ($data['events'] as $event) {
// Skip if no abstract
if (empty($event['abstract'])) {
continue;
}
// Find pretalx link to extract event ID
$eventId = null;
if (!empty($event['links'])) {
foreach ($event['links'] as $link) {
if (strpos($link['href'], 'pretalx.fosdem.org') !== false && strpos($link['href'], '/talk/') !== false) {
// Extract ID from URL like: https://pretalx.fosdem.org/fosdem-2026/talk/M3TKEE/feedback/
if (preg_match('#/talk/([A-Z0-9]+)/#i', $link['href'], $matches)) {
$eventId = $matches[1];
break;
}
}
}
}
// Skip if no pretalx link found
if ($eventId === null) {
continue;
}
$abstracts[$eventId] = cleanText($event['abstract']);
}The file /home/nemo/projects/personal/fosdem-playdate/generate-abstracts.php has been updated successfully.
Good, I've updated the script to: 1. Remove the shebang line (no more chmod) 2. Added a cleanText function that: - Strips HTML tags - Decodes HTML entities - Converts smart quotes to regular quotes - Removes emojis (comprehensive list of Unicode emoji ranges) - Normalizes whitespace
The emoji removal is quite comprehensive - it covers the main emoji ranges in Unicode.
Done. Updated the script to:
- Remove shebang (run with php generate-abstracts.php)
- Convert smart quotes (' ' " ") to regular quotes
- Remove emojis (all major Unicode emoji ranges)
- Strip HTML and normalize whitespace
The location should be in top right in the qr popup. Use the bottom left space to fill the abstract. Read abstracts.json in the code at loadtime. Move the qr to bottom right of the popup so we get more space for the summary. In the qr popup, the crank can be used to scroll the abstract text.
The user wants to: 1. Move location to top right in QR popup 2. Add abstract text that can be scrolled with the crank 3. Move QR code to bottom right 4. Load abstracts.json at loadtime 5. Use crank to scroll abstract in QR popup
I need to: 1. Create a module to load abstracts.json 2. Update qrOverlay.lua to show location top-right, summary, abstract (scrollable), and QR bottom-right 3. Update input handlers to handle crank scrolling in QR view
Let me start by creating the abstracts loader, then update the QR overlay.
-- Abstracts loader
Abstracts = {
data = {},
}
function Abstracts.load()
local file = playdate.file.open("abstracts.json", playdate.file.kFileRead)
if not file then
print("Could not open abstracts.json")
return
end
local content = file:read(1024 * 1024) -- Read up to 1MB
file:close()
if content then
Abstracts.data = json.decode(content) or {}
print("Loaded " .. Abstracts.count() .. " abstracts")
end
end
function Abstracts.get(eventId)
if eventId and Abstracts.data[eventId] then
return Abstracts.data[eventId]
end
return nil
end
function Abstracts.count()
local count = 0
for _ in pairs(Abstracts.data) do
count = count + 1
end
return count
end
File created successfully at: /home/nemo/projects/personal/fosdem-playdate/source/data/abstracts.lua
Now I need to update the QR overlay to have the new layout and scrollable abstract. Then update main.lua to import abstracts and load them, and update input handlers for crank scrolling.
-- QR code overlay for displaying event details and QR code
QROverlay = {
visible = false,
qrImage = nil,
currentEvent = nil,
}
local gfx <const> = playdate.graphics
function QROverlay.show(event)
if not event or not event.EVENT_ID then
print("No event to display")
return
end
QROverlay.visible = true
QROverlay.qrImage = nil
QROverlay.currentEvent = event
-- Load pre-generated QR code image
local qrPath = "qr/" .. event.EVENT_ID
QROverlay.qrImage = gfx.image.new(qrPath)
if not QROverlay.qrImage then
print("Could not load QR code: " .. qrPath)
end
end
function QROverlay.hide()
QROverlay.visible = false
QROverlay.qrImage = nil
QROverlay.currentEvent = nil
end
function QROverlay.isVisible()
return QROverlay.visible
end
function QROverlay.draw()
if not QROverlay.visible then return end
local event = QROverlay.currentEvent
-- Draw white background
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(10, 20, 380, 200)
-- Draw border
gfx.setColor(gfx.kColorBlack)
gfx.setLineWidth(2)
gfx.drawRect(10, 20, 380, 200)
gfx.setLineWidth(1)
-- Left side: Summary and Location
local textX = 20
local textY = 30
local textWidth = 220
-- Summary (bold)
if event and event.SUMMARY then
gfx.drawTextInRect("*" .. event.SUMMARY .. "*", textX, textY, textWidth, 80, nil, "...", kTextAlignment.left)
end
-- Location
if event and event.LOCATION then
gfx.drawTextInRect(event.LOCATION, textX, textY + 90, textWidth, 40, nil, "...", kTextAlignment.left)
end
-- Right side: QR code
local qrX = 290
local qrY = 70
if QROverlay.qrImage then
QROverlay.qrImage:drawCentered(qrX, qrY + 50)
else
gfx.drawTextAligned("No QR", qrX, qrY + 50, kTextAlignment.center)
end
-- Draw dismiss instruction at bottom
gfx.drawTextAligned("Press B to close", 200, 200, kTextAlignment.center)
end-- QR code overlay for displaying event details and QR code
QROverlay = {
visible = false,
qrImage = nil,
currentEvent = nil,
abstractScroll = 0,
abstractHeight = 0,
}
local gfx <const> = playdate.graphics
-- Layout constants
local POPUP_X = 10
local POPUP_Y = 10
local POPUP_WIDTH = 380
local POPUP_HEIGHT = 220
local PADDING = 8
local QR_SIZE = 80
local SUMMARY_HEIGHT = 50
local LOCATION_HEIGHT = 16
local ABSTRACT_Y_START = POPUP_Y + PADDING + SUMMARY_HEIGHT + 4
function QROverlay.show(event)
if not event or not event.EVENT_ID then
print("No event to display")
return
end
QROverlay.visible = true
QROverlay.qrImage = nil
QROverlay.currentEvent = event
QROverlay.abstractScroll = 0
-- Load pre-generated QR code image
local qrPath = "qr/" .. event.EVENT_ID
QROverlay.qrImage = gfx.image.new(qrPath)
if not QROverlay.qrImage then
print("Could not load QR code: " .. qrPath)
end
-- Calculate abstract height for scrolling
local abstract = Abstracts.get(event.EVENT_ID)
if abstract then
local abstractWidth = POPUP_WIDTH - PADDING * 3 - QR_SIZE
local _, height = gfx.getTextSizeForMaxWidth(abstract, abstractWidth)
QROverlay.abstractHeight = height
else
QROverlay.abstractHeight = 0
end
end
function QROverlay.hide()
QROverlay.visible = false
QROverlay.qrImage = nil
QROverlay.currentEvent = nil
QROverlay.abstractScroll = 0
end
function QROverlay.isVisible()
return QROverlay.visible
end
function QROverlay.scroll(amount)
local maxScroll = math.max(0, QROverlay.abstractHeight - 80)
QROverlay.abstractScroll = math.max(0, math.min(maxScroll, QROverlay.abstractScroll + amount))
end
function QROverlay.draw()
if not QROverlay.visible then return end
local event = QROverlay.currentEvent
-- Draw white background
gfx.setColor(gfx.kColorWhite)
gfx.fillRect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT)
-- Draw border
gfx.setColor(gfx.kColorBlack)
gfx.setLineWidth(2)
gfx.drawRect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT)
gfx.setLineWidth(1)
local textX = POPUP_X + PADDING
local textY = POPUP_Y + PADDING
local textWidth = POPUP_WIDTH - PADDING * 3 - QR_SIZE
-- Location (top right)
if event and event.LOCATION then
gfx.drawTextInRect(
event.LOCATION,
POPUP_X + PADDING,
textY,
POPUP_WIDTH - PADDING * 2,
LOCATION_HEIGHT,
nil, "...", kTextAlignment.right
)
end
-- Summary (bold, below location)
local summaryY = textY + LOCATION_HEIGHT + 2
if event and event.SUMMARY then
gfx.drawTextInRect(
"*" .. event.SUMMARY .. "*",
textX, summaryY,
textWidth, SUMMARY_HEIGHT,
nil, "...", kTextAlignment.left
)
end
-- Abstract (scrollable, below summary)
local abstractY = summaryY + SUMMARY_HEIGHT + 4
local abstractHeight = POPUP_HEIGHT - (abstractY - POPUP_Y) - PADDING - 16
local abstract = Abstracts.get(event and event.EVENT_ID)
if abstract then
-- Set clipping rect for abstract area
gfx.setClipRect(textX, abstractY, textWidth, abstractHeight)
-- Draw abstract with scroll offset
gfx.drawTextInRect(
abstract,
textX, abstractY - QROverlay.abstractScroll,
textWidth, QROverlay.abstractHeight + 100,
nil, nil, kTextAlignment.left
)
-- Clear clip rect
gfx.clearClipRect()
-- Draw scroll indicator if content is scrollable
if QROverlay.abstractHeight > abstractHeight then
local scrollBarHeight = 40
local scrollRange = QROverlay.abstractHeight - abstractHeight
local scrollPos = (QROverlay.abstractScroll / scrollRange) * (abstractHeight - scrollBarHeight)
gfx.fillRect(textX + textWidth + 2, abstractY + scrollPos, 3, scrollBarHeight)
end
end
-- QR code (bottom right)
local qrX = POPUP_X + POPUP_WIDTH - PADDING - QR_SIZE / 2
local qrY = POPUP_Y + POPUP_HEIGHT - PADDING - QR_SIZE / 2 - 12
if QROverlay.qrImage then
QROverlay.qrImage:drawCentered(qrX, qrY)
end
-- Draw dismiss instruction at bottom
gfx.drawTextAligned("B: close Crank: scroll", POPUP_X + (POPUP_WIDTH - QR_SIZE) / 2, POPUP_Y + POPUP_HEIGHT - 14, kTextAlignment.center)
endThe file /home/nemo/projects/personal/fosdem-playdate/source/ui/qrOverlay.lua has been updated successfully.